bways-report 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -35
- package/esm2022/lib/core/services/index.mjs +2 -1
- package/esm2022/lib/core/services/report-data-discovery.service.mjs +135 -0
- package/esm2022/lib/core/tokens/reporting-config.token.mjs +1 -1
- package/esm2022/lib/core/utils/identifier-humanizer.mjs +15 -4
- package/esm2022/lib/data-access/adapters/http-lookup.adapter.mjs +17 -8
- package/esm2022/lib/features/designer/components/design-canvas/design-canvas.component.mjs +69 -11
- package/esm2022/lib/features/designer/components/property-editor/parameter-binding-editor.component.mjs +345 -213
- package/esm2022/lib/features/designer/components/property-editor/property-editor-action.component.mjs +39 -32
- package/esm2022/lib/features/designer/components/property-editor/property-editor-datasource.component.mjs +35 -3
- package/esm2022/lib/features/designer/components/property-editor/property-editor-grid.component.mjs +97 -5
- package/esm2022/lib/features/designer/components/property-editor/property-editor.component.mjs +112 -39
- package/esm2022/lib/features/designer/designer.component.mjs +3 -3
- package/esm2022/lib/features/designer/services/designer-state.service.mjs +62 -2
- package/esm2022/lib/features/runtime/report-grid-bridge.component.mjs +106 -14
- package/esm2022/lib/primitives/components/dropdown/rpt-dropdown.component.mjs +31 -5
- package/esm2022/lib/provide-reporting.mjs +20 -5
- package/fesm2022/bways-report.mjs +1620 -896
- package/fesm2022/bways-report.mjs.map +1 -1
- package/lib/core/services/index.d.ts +1 -0
- package/lib/core/services/report-data-discovery.service.d.ts +38 -0
- package/lib/core/tokens/reporting-config.token.d.ts +2 -0
- package/lib/data-access/adapters/http-lookup.adapter.d.ts +2 -1
- package/lib/features/designer/components/design-canvas/design-canvas.component.d.ts +5 -1
- package/lib/features/designer/components/property-editor/parameter-binding-editor.component.d.ts +5 -0
- package/lib/features/designer/components/property-editor/property-editor-action.component.d.ts +3 -0
- package/lib/features/designer/components/property-editor/property-editor-grid.component.d.ts +31 -1
- package/lib/features/designer/components/property-editor/property-editor.component.d.ts +19 -0
- package/lib/features/designer/services/designer-state.service.d.ts +12 -1
- package/lib/features/runtime/report-grid-bridge.component.d.ts +7 -58
- package/lib/primitives/components/dropdown/rpt-dropdown.component.d.ts +3 -0
- package/lib/provide-reporting.d.ts +6 -0
- package/package.json +1 -1
|
@@ -4,8 +4,8 @@ import * as i1 from '@angular/forms';
|
|
|
4
4
|
import { Validators, FormControl, FormGroup, ReactiveFormsModule, FormsModule } from '@angular/forms';
|
|
5
5
|
import { toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
6
6
|
import { isObservable, of, throwError, tap as tap$1 } from 'rxjs';
|
|
7
|
+
import { catchError, map, tap, finalize, shareReplay, timeout } from 'rxjs/operators';
|
|
7
8
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
|
8
|
-
import { catchError, map, tap } from 'rxjs/operators';
|
|
9
9
|
import * as i1$1 from '@angular/common';
|
|
10
10
|
import { CommonModule, DOCUMENT } from '@angular/common';
|
|
11
11
|
import { Router, ActivatedRoute, RouterModule } from '@angular/router';
|
|
@@ -511,14 +511,25 @@ function detectDefaultValueField(columns) {
|
|
|
511
511
|
const exactMatch = columns.find((c) => ['id', 'key', 'code', 'guid', 'uuid'].includes(c.field.toLowerCase()));
|
|
512
512
|
if (exactMatch)
|
|
513
513
|
return exactMatch.field;
|
|
514
|
-
// 2. Check columns ending with '
|
|
514
|
+
// 2. Check columns ending with 'acno', 'glacno', 'account_no', 'accountno', '_id', 'id'
|
|
515
|
+
const primaryIdMatch = columns.find((c) => {
|
|
516
|
+
const f = c.field.toLowerCase();
|
|
517
|
+
return (f.endsWith('_acno') ||
|
|
518
|
+
f.endsWith('acno') ||
|
|
519
|
+
f.includes('glacno') ||
|
|
520
|
+
f.endsWith('_id') ||
|
|
521
|
+
f.endsWith('id'));
|
|
522
|
+
});
|
|
523
|
+
if (primaryIdMatch)
|
|
524
|
+
return primaryIdMatch.field;
|
|
525
|
+
// 3. Check columns ending with 'code' or 'key'
|
|
515
526
|
const suffixMatch = columns.find((c) => {
|
|
516
527
|
const f = c.field.toLowerCase();
|
|
517
|
-
return f.endsWith('
|
|
528
|
+
return f.endsWith('_code') || f.endsWith('code') || f.endsWith('_key') || f.endsWith('key');
|
|
518
529
|
});
|
|
519
530
|
if (suffixMatch)
|
|
520
531
|
return suffixMatch.field;
|
|
521
|
-
//
|
|
532
|
+
// 4. Fallback to first column
|
|
522
533
|
return columns[0]?.field;
|
|
523
534
|
}
|
|
524
535
|
/**
|
|
@@ -960,57 +971,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
960
971
|
}] });
|
|
961
972
|
|
|
962
973
|
/**
|
|
963
|
-
*
|
|
964
|
-
*
|
|
965
|
-
* Safe for modern browser and Node environments.
|
|
966
|
-
*
|
|
967
|
-
* @param prefix - Prefix for the ID (default 'cmp').
|
|
968
|
-
* @returns Unique string ID (e.g. 'cmp-4a7b2c9d').
|
|
974
|
+
* Abstract contract for discovering backend database schemas, entities, columns, and parameters.
|
|
975
|
+
* Enables the Visual Designer to discover tables and procedures without tight coupling.
|
|
969
976
|
*/
|
|
970
|
-
|
|
971
|
-
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
972
|
-
return `${prefix}-${crypto.randomUUID().slice(0, 8)}`;
|
|
973
|
-
}
|
|
974
|
-
// Fallback random generation
|
|
975
|
-
const random = Math.random().toString(36).substring(2, 10);
|
|
976
|
-
return `${prefix}-${random}`;
|
|
977
|
+
class ReportSchemaDiscoveryAdapter {
|
|
977
978
|
}
|
|
978
979
|
|
|
979
980
|
/**
|
|
980
|
-
*
|
|
981
|
-
*/
|
|
982
|
-
const MIGRATIONS = {
|
|
983
|
-
// Example migration for v1 to v2 in future:
|
|
984
|
-
// 1: (def) => ({ ...def, schemaVersion: 2, ... }),
|
|
985
|
-
};
|
|
986
|
-
/**
|
|
987
|
-
* Migrates a ReportDefinition to a target schema version.
|
|
988
|
-
*
|
|
989
|
-
* @param definition - Source report definition.
|
|
990
|
-
* @param targetVersion - Desired schema version (default current version = 1).
|
|
991
|
-
* @returns Migrated ReportDefinition.
|
|
981
|
+
* Abstract contract for fetching dynamic lookup/option data for report dropdown components.
|
|
992
982
|
*/
|
|
993
|
-
|
|
994
|
-
if (!definition) {
|
|
995
|
-
throw new SchemaVersionError(0, targetVersion, 'Invalid report definition provided');
|
|
996
|
-
}
|
|
997
|
-
const currentVersion = definition.schemaVersion ?? 1;
|
|
998
|
-
if (currentVersion === targetVersion) {
|
|
999
|
-
return { ...definition, schemaVersion: targetVersion };
|
|
1000
|
-
}
|
|
1001
|
-
if (currentVersion > targetVersion) {
|
|
1002
|
-
throw new SchemaVersionError(currentVersion, targetVersion, `Cannot downgrade report schema from version ${currentVersion} to ${targetVersion}`);
|
|
1003
|
-
}
|
|
1004
|
-
let migrated = { ...definition };
|
|
1005
|
-
for (let v = currentVersion; v < targetVersion; v++) {
|
|
1006
|
-
const migrationFn = MIGRATIONS[v];
|
|
1007
|
-
if (!migrationFn) {
|
|
1008
|
-
throw new SchemaVersionError(v, v + 1, `No migration path defined from schema version ${v} to ${v + 1}`);
|
|
1009
|
-
}
|
|
1010
|
-
migrated = migrationFn(migrated);
|
|
1011
|
-
}
|
|
1012
|
-
migrated.schemaVersion = targetVersion;
|
|
1013
|
-
return migrated;
|
|
983
|
+
class ReportLookupAdapter {
|
|
1014
984
|
}
|
|
1015
985
|
|
|
1016
986
|
/**
|
|
@@ -1248,6 +1218,186 @@ function toFieldKey(paramName) {
|
|
|
1248
1218
|
return clean ? clean.charAt(0).toLowerCase() + clean.slice(1) : paramName;
|
|
1249
1219
|
}
|
|
1250
1220
|
|
|
1221
|
+
/**
|
|
1222
|
+
* Centralized service for discovering database schema entities, parameters,
|
|
1223
|
+
* and column definitions.
|
|
1224
|
+
*
|
|
1225
|
+
* Provides a unified static + dynamic detection pipeline:
|
|
1226
|
+
* If static schema metadata discovery fails (e.g. SQL Server stored procedures
|
|
1227
|
+
* using #temp tables that cannot be analyzed statically), it automatically
|
|
1228
|
+
* falls back to executing a safe dynamic sample query with the active parameter
|
|
1229
|
+
* values and extracting column definitions from the result dataset.
|
|
1230
|
+
*/
|
|
1231
|
+
class ReportDataDiscoveryService {
|
|
1232
|
+
discoveryAdapter = inject(ReportSchemaDiscoveryAdapter, { optional: true });
|
|
1233
|
+
lookupAdapter = inject(ReportLookupAdapter, { optional: true });
|
|
1234
|
+
sessionService = inject(ReportSessionContextService, { optional: true });
|
|
1235
|
+
/**
|
|
1236
|
+
* Retrieves all database tables, views, and stored procedures.
|
|
1237
|
+
*/
|
|
1238
|
+
getEntities(types) {
|
|
1239
|
+
if (!this.discoveryAdapter) {
|
|
1240
|
+
return of([]);
|
|
1241
|
+
}
|
|
1242
|
+
return this.discoveryAdapter.getEntities(types).pipe(catchError((err) => {
|
|
1243
|
+
console.warn('[ReportDataDiscoveryService] Failed to get entities:', err);
|
|
1244
|
+
return of([]);
|
|
1245
|
+
}));
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Retrieves stored procedure parameter metadata.
|
|
1249
|
+
*/
|
|
1250
|
+
getParameters(procedureName, schema = 'dbo') {
|
|
1251
|
+
if (!procedureName || !this.discoveryAdapter) {
|
|
1252
|
+
return of([]);
|
|
1253
|
+
}
|
|
1254
|
+
return this.discoveryAdapter.getParameters(procedureName, schema).pipe(catchError((err) => {
|
|
1255
|
+
console.warn(`[ReportDataDiscoveryService] Failed to get parameters for ${procedureName}:`, err);
|
|
1256
|
+
return of([]);
|
|
1257
|
+
}));
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Unified column detection:
|
|
1261
|
+
* 1. Attempts static schema discovery via ReportSchemaDiscoveryAdapter.
|
|
1262
|
+
* 2. If static discovery returns 0 columns or throws an error (e.g. SQL Error 11526 with #temp tables),
|
|
1263
|
+
* automatically runs dynamic sample query via ReportLookupAdapter using resolved parameters.
|
|
1264
|
+
*/
|
|
1265
|
+
autoDetectColumns(entityName, entityType = 'stored_procedure', mappings = {}, schema = 'dbo') {
|
|
1266
|
+
if (!entityName) {
|
|
1267
|
+
return of([]);
|
|
1268
|
+
}
|
|
1269
|
+
// Try static discovery first
|
|
1270
|
+
if (this.discoveryAdapter) {
|
|
1271
|
+
return this.discoveryAdapter.getColumns(entityName, schema, entityType).pipe(map((cols) => {
|
|
1272
|
+
if (cols && cols.length > 0) {
|
|
1273
|
+
return this.mapStaticColumnsToGrid(cols);
|
|
1274
|
+
}
|
|
1275
|
+
throw new Error('STATIC_EMPTY');
|
|
1276
|
+
}), catchError(() => {
|
|
1277
|
+
// Fallback to dynamic execution
|
|
1278
|
+
return this.fallbackDynamicDetection(entityName, entityType, mappings, schema);
|
|
1279
|
+
}));
|
|
1280
|
+
}
|
|
1281
|
+
return this.fallbackDynamicDetection(entityName, entityType, mappings, schema);
|
|
1282
|
+
}
|
|
1283
|
+
mapStaticColumnsToGrid(cols) {
|
|
1284
|
+
return cols.map((c) => ({
|
|
1285
|
+
field: c.field,
|
|
1286
|
+
label: humanizeIdentifier(c.label || c.field),
|
|
1287
|
+
dataType: c.dataType || 'string',
|
|
1288
|
+
visible: true,
|
|
1289
|
+
sortable: true,
|
|
1290
|
+
align: c.dataType === 'number' ? 'right' : 'left',
|
|
1291
|
+
}));
|
|
1292
|
+
}
|
|
1293
|
+
fallbackDynamicDetection(entityName, sourceType, mappings, schema = 'dbo') {
|
|
1294
|
+
if (!this.lookupAdapter) {
|
|
1295
|
+
return of([]);
|
|
1296
|
+
}
|
|
1297
|
+
const session = this.sessionService?.getCurrentSessionContext() ?? {};
|
|
1298
|
+
const resolvedParams = resolveParameterMappings(mappings, session, {});
|
|
1299
|
+
return this.lookupAdapter
|
|
1300
|
+
.fetchOptions({
|
|
1301
|
+
sourceType: (sourceType === 'stored_procedure' ? 'procedure' : sourceType),
|
|
1302
|
+
entityName,
|
|
1303
|
+
schema,
|
|
1304
|
+
valueField: '',
|
|
1305
|
+
displayField: '',
|
|
1306
|
+
parameters: resolvedParams,
|
|
1307
|
+
})
|
|
1308
|
+
.pipe(map((items) => {
|
|
1309
|
+
if (items && items.length > 0 && items[0].raw) {
|
|
1310
|
+
const sampleRow = items[0].raw;
|
|
1311
|
+
const keys = Object.keys(sampleRow);
|
|
1312
|
+
if (keys.length > 0) {
|
|
1313
|
+
return keys.map((key) => {
|
|
1314
|
+
const val = sampleRow[key];
|
|
1315
|
+
let type = 'string';
|
|
1316
|
+
if (typeof val === 'number')
|
|
1317
|
+
type = 'number';
|
|
1318
|
+
else if (typeof val === 'boolean')
|
|
1319
|
+
type = 'boolean';
|
|
1320
|
+
else if (typeof val === 'string' && /^\d{4}-\d{2}-\d{2}/.test(val))
|
|
1321
|
+
type = 'date';
|
|
1322
|
+
return {
|
|
1323
|
+
field: key,
|
|
1324
|
+
label: humanizeIdentifier(key),
|
|
1325
|
+
dataType: type,
|
|
1326
|
+
visible: true,
|
|
1327
|
+
sortable: true,
|
|
1328
|
+
align: (type === 'number' ? 'right' : 'left'),
|
|
1329
|
+
};
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
return [];
|
|
1334
|
+
}), catchError((err) => {
|
|
1335
|
+
console.warn(`[ReportDataDiscoveryService] Dynamic sample detection failed for ${entityName}:`, err);
|
|
1336
|
+
return of([]);
|
|
1337
|
+
}));
|
|
1338
|
+
}
|
|
1339
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportDataDiscoveryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1340
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportDataDiscoveryService, providedIn: 'root' });
|
|
1341
|
+
}
|
|
1342
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportDataDiscoveryService, decorators: [{
|
|
1343
|
+
type: Injectable,
|
|
1344
|
+
args: [{ providedIn: 'root' }]
|
|
1345
|
+
}] });
|
|
1346
|
+
|
|
1347
|
+
/**
|
|
1348
|
+
* Generates a unique identifier with a given prefix.
|
|
1349
|
+
*
|
|
1350
|
+
* Safe for modern browser and Node environments.
|
|
1351
|
+
*
|
|
1352
|
+
* @param prefix - Prefix for the ID (default 'cmp').
|
|
1353
|
+
* @returns Unique string ID (e.g. 'cmp-4a7b2c9d').
|
|
1354
|
+
*/
|
|
1355
|
+
function generateId(prefix = 'cmp') {
|
|
1356
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
1357
|
+
return `${prefix}-${crypto.randomUUID().slice(0, 8)}`;
|
|
1358
|
+
}
|
|
1359
|
+
// Fallback random generation
|
|
1360
|
+
const random = Math.random().toString(36).substring(2, 10);
|
|
1361
|
+
return `${prefix}-${random}`;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/**
|
|
1365
|
+
* Migration registry mapping sourceVersion -> targetVersion migration function.
|
|
1366
|
+
*/
|
|
1367
|
+
const MIGRATIONS = {
|
|
1368
|
+
// Example migration for v1 to v2 in future:
|
|
1369
|
+
// 1: (def) => ({ ...def, schemaVersion: 2, ... }),
|
|
1370
|
+
};
|
|
1371
|
+
/**
|
|
1372
|
+
* Migrates a ReportDefinition to a target schema version.
|
|
1373
|
+
*
|
|
1374
|
+
* @param definition - Source report definition.
|
|
1375
|
+
* @param targetVersion - Desired schema version (default current version = 1).
|
|
1376
|
+
* @returns Migrated ReportDefinition.
|
|
1377
|
+
*/
|
|
1378
|
+
function migrateDefinition(definition, targetVersion = 1) {
|
|
1379
|
+
if (!definition) {
|
|
1380
|
+
throw new SchemaVersionError(0, targetVersion, 'Invalid report definition provided');
|
|
1381
|
+
}
|
|
1382
|
+
const currentVersion = definition.schemaVersion ?? 1;
|
|
1383
|
+
if (currentVersion === targetVersion) {
|
|
1384
|
+
return { ...definition, schemaVersion: targetVersion };
|
|
1385
|
+
}
|
|
1386
|
+
if (currentVersion > targetVersion) {
|
|
1387
|
+
throw new SchemaVersionError(currentVersion, targetVersion, `Cannot downgrade report schema from version ${currentVersion} to ${targetVersion}`);
|
|
1388
|
+
}
|
|
1389
|
+
let migrated = { ...definition };
|
|
1390
|
+
for (let v = currentVersion; v < targetVersion; v++) {
|
|
1391
|
+
const migrationFn = MIGRATIONS[v];
|
|
1392
|
+
if (!migrationFn) {
|
|
1393
|
+
throw new SchemaVersionError(v, v + 1, `No migration path defined from schema version ${v} to ${v + 1}`);
|
|
1394
|
+
}
|
|
1395
|
+
migrated = migrationFn(migrated);
|
|
1396
|
+
}
|
|
1397
|
+
migrated.schemaVersion = targetVersion;
|
|
1398
|
+
return migrated;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1251
1401
|
/**
|
|
1252
1402
|
* Data-access contracts — abstract classes the host application implements.
|
|
1253
1403
|
*
|
|
@@ -1334,19 +1484,6 @@ class ReportPersistenceAdapter {
|
|
|
1334
1484
|
class ReportTranslationAdapter {
|
|
1335
1485
|
}
|
|
1336
1486
|
|
|
1337
|
-
/**
|
|
1338
|
-
* Abstract contract for discovering backend database schemas, entities, columns, and parameters.
|
|
1339
|
-
* Enables the Visual Designer to discover tables and procedures without tight coupling.
|
|
1340
|
-
*/
|
|
1341
|
-
class ReportSchemaDiscoveryAdapter {
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
/**
|
|
1345
|
-
* Abstract contract for fetching dynamic lookup/option data for report dropdown components.
|
|
1346
|
-
*/
|
|
1347
|
-
class ReportLookupAdapter {
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
1487
|
/**
|
|
1351
1488
|
* Shared API Contract DTOs for BWays Reporting.
|
|
1352
1489
|
* These types match the REST contract implemented by the backend and used by HTTP adapters.
|
|
@@ -1468,13 +1605,14 @@ const HttpSchemaDiscoveryAdapter = HttpReportSchemaDiscoveryAdapter;
|
|
|
1468
1605
|
|
|
1469
1606
|
/**
|
|
1470
1607
|
* Built-in HTTP adapter for fetching dropdown options from backend database tables and SPs.
|
|
1471
|
-
* Includes in-memory caching with TTL and
|
|
1608
|
+
* Includes in-memory caching with TTL, in-flight request deduplication, and fallback mock data.
|
|
1472
1609
|
*/
|
|
1473
1610
|
class HttpReportLookupAdapter extends ReportLookupAdapter {
|
|
1474
1611
|
http = inject(HttpClient, { optional: true });
|
|
1475
1612
|
config = inject(REPORTING_CONFIG, { optional: true });
|
|
1476
1613
|
sessionService = inject(ReportSessionContextService, { optional: true });
|
|
1477
1614
|
cache = new Map();
|
|
1615
|
+
inFlight = new Map();
|
|
1478
1616
|
mockDataStore = {
|
|
1479
1617
|
Customers: [
|
|
1480
1618
|
{ customerId: 101, customerName: 'Acme Corporation', branchId: 1, isActive: true },
|
|
@@ -1521,6 +1659,10 @@ class HttpReportLookupAdapter extends ReportLookupAdapter {
|
|
|
1521
1659
|
if (cached && cached.expiresAt > now) {
|
|
1522
1660
|
return of(cached.data);
|
|
1523
1661
|
}
|
|
1662
|
+
const running = this.inFlight.get(cacheKey);
|
|
1663
|
+
if (running) {
|
|
1664
|
+
return running;
|
|
1665
|
+
}
|
|
1524
1666
|
const baseUrl = this.config?.apiBaseUrl;
|
|
1525
1667
|
if (baseUrl && this.http) {
|
|
1526
1668
|
const session = this.sessionService?.sessionContext() ?? {};
|
|
@@ -1531,7 +1673,7 @@ class HttpReportLookupAdapter extends ReportLookupAdapter {
|
|
|
1531
1673
|
if (session['branchId'] !== undefined && session['branchId'] !== null) {
|
|
1532
1674
|
headers = headers.set('X-Branch-Id', String(session['branchId']));
|
|
1533
1675
|
}
|
|
1534
|
-
|
|
1676
|
+
const obs$ = this.http.post(`${baseUrl}/lookup`, request, { headers }).pipe(map((response) => {
|
|
1535
1677
|
const rawItems = extractRowArray(response);
|
|
1536
1678
|
return rawItems.map((r) => {
|
|
1537
1679
|
if (r && typeof r === 'object' && 'value' in r) {
|
|
@@ -1544,17 +1686,21 @@ class HttpReportLookupAdapter extends ReportLookupAdapter {
|
|
|
1544
1686
|
}), tap((items) => {
|
|
1545
1687
|
// Never cache empty results: parameters may simply not have been filled yet.
|
|
1546
1688
|
if (items.length > 0)
|
|
1547
|
-
this.setCache(cacheKey, items);
|
|
1689
|
+
this.setCache(cacheKey, items, 600);
|
|
1548
1690
|
}), catchError((err) => {
|
|
1549
1691
|
const msg = err?.error?.message || err?.message || 'Lookup request failed';
|
|
1550
1692
|
return throwError(() => new Error(Array.isArray(msg) ? msg.join(', ') : msg));
|
|
1551
|
-
}))
|
|
1693
|
+
}), finalize(() => {
|
|
1694
|
+
this.inFlight.delete(cacheKey);
|
|
1695
|
+
}), shareReplay({ bufferSize: 1, refCount: true }));
|
|
1696
|
+
this.inFlight.set(cacheKey, obs$);
|
|
1697
|
+
return obs$;
|
|
1552
1698
|
}
|
|
1553
1699
|
const fallback = this.resolveMockOptions(request);
|
|
1554
|
-
this.setCache(cacheKey, fallback);
|
|
1700
|
+
this.setCache(cacheKey, fallback, 600);
|
|
1555
1701
|
return of(fallback);
|
|
1556
1702
|
}
|
|
1557
|
-
setCache(key, data, ttlSeconds =
|
|
1703
|
+
setCache(key, data, ttlSeconds = 600) {
|
|
1558
1704
|
this.cache.set(key, {
|
|
1559
1705
|
expiresAt: Date.now() + ttlSeconds * 1000,
|
|
1560
1706
|
data,
|
|
@@ -2496,6 +2642,7 @@ class RptDropdownComponent {
|
|
|
2496
2642
|
masterDataProvider = inject(MasterDataProvider, { optional: true });
|
|
2497
2643
|
lookupAdapter = inject(ReportLookupAdapter, { optional: true });
|
|
2498
2644
|
sessionService = inject(ReportSessionContextService, { optional: true });
|
|
2645
|
+
reportingConfig = inject(REPORTING_CONFIG, { optional: true });
|
|
2499
2646
|
elementRef = inject(ElementRef);
|
|
2500
2647
|
config = input.required();
|
|
2501
2648
|
control = input();
|
|
@@ -2712,6 +2859,16 @@ class RptDropdownComponent {
|
|
|
2712
2859
|
this.searchTerm.set('');
|
|
2713
2860
|
this.focusedIndex.set(-1);
|
|
2714
2861
|
}
|
|
2862
|
+
/** Manually retry loading dynamic lookup options (e.g. after database timeout). */
|
|
2863
|
+
retryLookup(event) {
|
|
2864
|
+
if (event) {
|
|
2865
|
+
event.stopPropagation();
|
|
2866
|
+
event.preventDefault();
|
|
2867
|
+
}
|
|
2868
|
+
this.lastRequestKey = '';
|
|
2869
|
+
this.loadError.set(null);
|
|
2870
|
+
this.reloadDataSourceOptions(this.sessionService?.sessionContext() ?? {});
|
|
2871
|
+
}
|
|
2715
2872
|
clearSearch(event) {
|
|
2716
2873
|
event.stopPropagation();
|
|
2717
2874
|
this.searchTerm.set('');
|
|
@@ -2800,7 +2957,7 @@ class RptDropdownComponent {
|
|
|
2800
2957
|
if (form && this.isDynamicSource()) {
|
|
2801
2958
|
this.formChangesSub = form.valueChanges.subscribe(() => this.scheduleReload());
|
|
2802
2959
|
}
|
|
2803
|
-
untracked(() => this.
|
|
2960
|
+
untracked(() => this.scheduleReload());
|
|
2804
2961
|
}, { allowSignalWrites: true });
|
|
2805
2962
|
}
|
|
2806
2963
|
ngOnDestroy() {
|
|
@@ -2854,10 +3011,22 @@ class RptDropdownComponent {
|
|
|
2854
3011
|
if (requestKey === this.lastRequestKey)
|
|
2855
3012
|
return; // nothing changed
|
|
2856
3013
|
this.lastRequestKey = requestKey;
|
|
3014
|
+
// Stored procedures on local databases can take a couple minutes to execute.
|
|
3015
|
+
// Default timeout is 300,000ms (5 minutes), configurable via component properties or reportingConfig.
|
|
3016
|
+
const timeoutMs = ds?.timeoutMs ||
|
|
3017
|
+
this.config().properties?.['timeoutMs'] ||
|
|
3018
|
+
this.reportingConfig?.lookupTimeoutMs ||
|
|
3019
|
+
300000;
|
|
2857
3020
|
this.isLoading.set(true);
|
|
2858
3021
|
this.loadError.set(null);
|
|
2859
3022
|
this.lookupSub?.unsubscribe();
|
|
2860
|
-
this.lookupSub = this.lookupAdapter
|
|
3023
|
+
this.lookupSub = this.lookupAdapter
|
|
3024
|
+
.fetchOptions(request)
|
|
3025
|
+
.pipe(timeout({
|
|
3026
|
+
each: timeoutMs,
|
|
3027
|
+
with: () => throwError(() => new Error(`Lookup query timed out after ${Math.round(timeoutMs / 1000)}s (stored procedure took too long to respond on local database)`)),
|
|
3028
|
+
}))
|
|
3029
|
+
.subscribe({
|
|
2861
3030
|
next: (items) => {
|
|
2862
3031
|
this.isLoading.set(false);
|
|
2863
3032
|
this.hasLoaded.set(true);
|
|
@@ -2907,14 +3076,14 @@ class RptDropdownComponent {
|
|
|
2907
3076
|
}
|
|
2908
3077
|
}
|
|
2909
3078
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RptDropdownComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2910
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: RptDropdownComponent, isStandalone: true, selector: "rpt-dropdown", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, control: { classPropertyName: "control", publicName: "control", isSignal: true, isRequired: false, transformFunction: null }, parentForm: { classPropertyName: "parentForm", publicName: "parentForm", isSignal: true, isRequired: false, transformFunction: null }, showStatus: { classPropertyName: "showStatus", publicName: "showStatus", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()" }, properties: { "class.is-open": "isOpen()", "class.opens-above": "opensAbove()" } }, viewQueries: [{ propertyName: "comboboxWrapperEl", first: true, predicate: ["comboboxWrapper"], descendants: true, isSignal: true }, { propertyName: "searchInputEl", first: true, predicate: ["searchInput"], descendants: true, isSignal: true }, { propertyName: "optionsListEl", first: true, predicate: ["optionsListEl"], descendants: true, isSignal: true }], ngImport: i0, template: " <div class=\"rpt-field-group\">\n @if (config().label) {\n <label [for]=\"elementId()\" class=\"rpt-label\">\n {{ config().label }}\n @if (config().validation?.required) {\n <span class=\"rpt-required-marker\">*</span>\n }\n </label>\n }\n\n @if (isSearchable()) {\n <!-- Searchable Typeahead Combobox -->\n <div class=\"rpt-combobox-wrapper\" #comboboxWrapper>\n <button\n type=\"button\"\n class=\"rpt-combobox-trigger\"\n [id]=\"elementId()\"\n [class.rpt-invalid]=\"isInvalid()\"\n [class.is-open]=\"isOpen()\"\n [disabled]=\"formControl().disabled\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"'listbox'\"\n (click)=\"toggleOpen($event)\"\n (keydown)=\"onTriggerKeydown($event)\"\n >\n <span class=\"trigger-label\" [class.is-placeholder]=\"!selectedOption()\">\n {{ selectedOption() ? selectedOption()!.label : placeholder() }}\n </span>\n\n <div class=\"trigger-actions\">\n @if (selectedOption() && !formControl().disabled) {\n <span\n role=\"button\"\n tabindex=\"0\"\n class=\"trigger-clear-btn\"\n title=\"Clear selection\"\n aria-label=\"Clear selection\"\n (click)=\"clearSelection($event)\"\n (keydown.enter)=\"clearSelection($event)\"\n (keydown.space)=\"clearSelection($event)\"\n >\n \u2715\n </span>\n }\n <span class=\"trigger-arrow\" [class.rotated]=\"isOpen()\" aria-hidden=\"true\">\n <svg width=\"12\" height=\"8\" viewBox=\"0 0 12 8\" fill=\"none\">\n <path d=\"M1 1.5L6 6.5L11 1.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </span>\n </div>\n </button>\n\n @if (isOpen()) {\n <div\n class=\"rpt-combobox-menu\"\n [class.opens-above]=\"opensAbove()\"\n role=\"listbox\"\n (click)=\"$event.stopPropagation()\"\n >\n <!-- Real-time Filter Search Box -->\n <div class=\"combobox-search-box\">\n <span class=\"combobox-search-icon\" aria-hidden=\"true\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"></circle>\n <line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"></line>\n </svg>\n </span>\n <input\n #searchInput\n type=\"text\"\n class=\"combobox-search-input\"\n placeholder=\"Type to filter...\"\n [value]=\"searchTerm()\"\n [attr.aria-activedescendant]=\"focusedIndex() >= 0 ? (elementId() + '_opt_' + focusedIndex()) : null\"\n (input)=\"onSearchInput($event)\"\n (keydown)=\"onSearchKeydown($event)\"\n />\n @if (searchTerm()) {\n <button\n type=\"button\"\n class=\"combobox-search-clear\"\n title=\"Clear filter\"\n (click)=\"clearSearch($event)\"\n >\n \u2715\n </button>\n }\n </div>\n\n <!-- Filtered Options List -->\n <div class=\"combobox-options-list\" #optionsListEl>\n @if (isLoading()) {\n <div class=\"combobox-status-row loading\">\n <span class=\"status-dot dot-pulse\"></span>\n <span>Loading options from database...</span>\n </div>\n } @else if (loadError()) {\n <div class=\"combobox-status-row error\">\n <span>Error: {{ loadError() }}</span>\n </div>\n } @else if (isDynamicSource() && hasLoaded() && allOptions().length === 0) {\n <div class=\"combobox-status-row empty\">\n <span>No records found (check parameters)</span>\n </div>\n } @else if (filteredOptions().length === 0) {\n <div class=\"combobox-status-row empty\">\n <span>No options matching \"{{ searchTerm() }}\"</span>\n </div>\n } @else {\n @for (opt of visibleFilteredOptions(); track opt.key; let i = $index) {\n <div\n role=\"option\"\n [id]=\"elementId() + '_opt_' + i\"\n class=\"combobox-option-item\"\n [class.is-selected]=\"isSelected(opt)\"\n [class.is-focused]=\"focusedIndex() === i\"\n [attr.aria-selected]=\"isSelected(opt)\"\n (click)=\"selectOption(opt)\"\n (mouseenter)=\"focusedIndex.set(i)\"\n >\n <span class=\"opt-label\">{{ opt.label }}</span>\n @if (isSelected(opt)) {\n <span class=\"opt-check\" aria-hidden=\"true\">\u2713</span>\n }\n </div>\n }\n @if (excessFilteredCount() > 0) {\n <div class=\"combobox-status-row hint\">\n <span>Showing {{ MAX_FILTER_RENDER }} of {{ filteredOptions().length }} matches. Type to refine.</span>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n } @else {\n <!-- Native Browser Select Fallback -->\n <div class=\"rpt-select-wrapper\">\n <select\n [id]=\"elementId()\"\n [formControl]=\"formControl()\"\n [class.rpt-invalid]=\"isInvalid()\"\n class=\"rpt-select\"\n >\n @if (isLoading()) {\n <option value=\"\" disabled selected>Loading options from database...</option>\n } @else if (loadError()) {\n <option value=\"\" disabled selected>Error: {{ loadError() }}</option>\n } @else if (isDynamicSource() && hasLoaded() && allOptions().length === 0) {\n <option value=\"\" disabled selected>-- No records found (check parameters) --</option>\n } @else if (placeholder()) {\n <option value=\"\" [disabled]=\"config().validation?.required\">{{ placeholder() }}</option>\n }\n @for (opt of renderedOptions(); track opt.key) {\n <option [value]=\"opt.key\">{{ opt.label }}</option>\n }\n @if (hasExcessOptions()) {\n <option value=\"\" disabled>... and {{ excessOptionsCount() }} more records (refine filter) ...</option>\n }\n </select>\n <div class=\"rpt-select-arrow\" aria-hidden=\"true\">\n <svg width=\"12\" height=\"8\" viewBox=\"0 0 12 8\" fill=\"none\">\n <path d=\"M1 1.5L6 6.5L11 1.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </div>\n </div>\n }\n\n @if (showStatus() && isDynamicSource() && statusText()) {\n <div class=\"rpt-status-badge\" [attr.data-status]=\"statusKind()\">\n @switch (statusKind()) {\n @case ('loading') {\n <span class=\"status-dot dot-pulse\"></span>\n <span>Loading from database\u2026</span>\n }\n @case ('ok') {\n <span class=\"status-dot dot-ok\"></span>\n <span>{{ dynamicOptions().length }} records loaded</span>\n }\n @case ('waiting') {\n <span class=\"status-dot dot-warn\"></span>\n <span>Waiting for:</span>\n <div class=\"waiting-tokens\">\n @for (k of waitingFor(); track k) {\n <span class=\"waiting-token\">{{ k }}</span>\n }\n </div>\n }\n @case ('error') {\n <span class=\"status-dot dot-err\"></span>\n <span>{{ loadError() }}</span>\n }\n @case ('empty') {\n <span class=\"status-dot\"></span>\n <span>No records found for parameters</span>\n }\n @default {\n <span class=\"status-dot\"></span>\n }\n }\n </div>\n }\n\n @if (errorMessage()) {\n <span class=\"rpt-error-text\">{{ errorMessage() }}</span>\n }\n </div>", styles: [":host{display:block;width:100%;max-width:100%;min-width:0;box-sizing:border-box;font-family:var(--rpt-font-family, inherit);position:relative}.rpt-status-badge{display:flex;flex-wrap:wrap;align-items:center;gap:4px;font-size:.6875rem;padding:3px 6px;border-radius:4px;margin-top:4px;font-weight:500;line-height:1.3;max-width:100%;box-sizing:border-box;overflow:hidden;word-break:break-word}.rpt-status-badge>span{overflow-wrap:anywhere;word-break:break-word}.rpt-status-badge[data-status=loading]{background:#eff6ff;color:#2563eb;border:1px solid #dbeafe}.rpt-status-badge[data-status=ok]{background:#f0fdf4;color:#16a34a;border:1px solid #dcfce7}.rpt-status-badge[data-status=waiting]{background:#fffbeb;color:#d97706;border:1px solid #fef3c7}.rpt-status-badge[data-status=error]{background:#fef2f2;color:#dc2626;border:1px solid #fee2e2}.rpt-status-badge[data-status=empty]{background:#f8fafc;color:#64748b;border:1px solid #e2e8f0}.waiting-tokens{display:inline-flex;flex-wrap:wrap;align-items:center;gap:3px;margin-left:2px;max-width:100%}.waiting-token{background:#b453091f;border:1px solid rgba(180,83,9,.3);padding:1px 5px;border-radius:3px;font-weight:700;font-size:.625rem;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block;box-sizing:border-box}.status-dot{width:6px;height:6px;border-radius:50%;background:currentColor}.dot-pulse{animation:rpt-pulse 1s infinite alternate}@keyframes rpt-pulse{0%{opacity:.3}to{opacity:1}}.rpt-field-group{display:flex;flex-direction:column;max-width:100%;min-width:0;gap:4px;box-sizing:border-box;position:relative}.rpt-label{font-size:var(--rpt-font-size-sm, .75rem);font-weight:500;color:var(--rpt-color-text, #0f172a);flex-shrink:0}.rpt-required-marker{color:var(--rpt-color-danger, #ef4444);margin-left:2px}.rpt-select-wrapper{position:relative;width:100%;flex:1;min-height:0;display:flex;align-items:center}.rpt-select{width:100%;height:100%;min-height:38px;box-sizing:border-box;padding:6px 36px 6px 12px;font-size:var(--rpt-font-size-base, .875rem);color:var(--rpt-color-text, #0f172a);background-color:var(--rpt-color-bg, #ffffff);border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius, 6px);appearance:none;-webkit-appearance:none;outline:none;cursor:pointer;transition:border-color var(--rpt-transition-speed, .15s) ease,box-shadow var(--rpt-transition-speed, .15s) ease}.rpt-select:hover:not(:disabled){border-color:var(--rpt-color-border-hover, #94a3b8)}.rpt-select:focus{border-color:var(--rpt-color-border-focus, #2563eb);box-shadow:var(--rpt-box-shadow-focus, 0 0 0 3px rgba(37, 99, 235, .25))}.rpt-select:disabled{background-color:var(--rpt-color-surface, #f8fafc);color:var(--rpt-color-text-muted, #64748b);cursor:not-allowed}.rpt-select.rpt-invalid{border-color:var(--rpt-color-danger, #ef4444)}.rpt-select.rpt-invalid:focus{box-shadow:0 0 0 3px #ef444440}.rpt-select-arrow{position:absolute;right:12px;top:50%;transform:translateY(-50%);pointer-events:none;color:var(--rpt-color-text-muted, #64748b);display:flex;align-items:center}.rpt-combobox-wrapper{position:relative;width:100%;flex:1;min-height:0}.rpt-combobox-trigger{width:100%;height:100%;min-height:38px;box-sizing:border-box;padding:6px 10px 6px 12px;display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:var(--rpt-font-size-base, .875rem);font-family:inherit;color:var(--rpt-color-text, #0f172a);background-color:var(--rpt-color-bg, #ffffff);border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius, 6px);outline:none;cursor:pointer;text-align:left;transition:border-color var(--rpt-transition-speed, .15s) ease,box-shadow var(--rpt-transition-speed, .15s) ease}.rpt-combobox-trigger:hover:not(:disabled){border-color:var(--rpt-color-border-hover, #94a3b8)}.rpt-combobox-trigger:focus-visible,.rpt-combobox-trigger.is-open{border-color:var(--rpt-color-border-focus, #2563eb);box-shadow:var(--rpt-box-shadow-focus, 0 0 0 3px rgba(37, 99, 235, .25))}.rpt-combobox-trigger:disabled{background-color:var(--rpt-color-surface, #f8fafc);color:var(--rpt-color-text-muted, #64748b);cursor:not-allowed}.rpt-combobox-trigger.rpt-invalid{border-color:var(--rpt-color-danger, #ef4444)}.rpt-combobox-trigger.rpt-invalid:focus-visible{box-shadow:0 0 0 3px #ef444440}.trigger-label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.3}.trigger-label.is-placeholder{color:var(--rpt-color-text-muted, #94a3b8)}.trigger-actions{display:flex;align-items:center;gap:6px;flex-shrink:0}.trigger-clear-btn{display:flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;font-size:.6875rem;color:var(--rpt-color-text-muted, #64748b);background-color:#f1f5f9;cursor:pointer;-webkit-user-select:none;user-select:none;transition:background-color .15s ease,color .15s ease}.trigger-clear-btn:hover{background-color:#e2e8f0;color:var(--rpt-color-text, #0f172a)}.trigger-arrow{display:flex;align-items:center;color:var(--rpt-color-text-muted, #64748b);transition:transform .18s ease}.trigger-arrow.rotated{transform:rotate(180deg)}.rpt-combobox-menu{position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:1050;background-color:var(--rpt-color-bg, #ffffff);border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius, 6px);box-shadow:0 10px 25px -5px #0000001f,0 8px 10px -6px #00000014;overflow:hidden;display:flex;flex-direction:column;animation:rpt-dropdown-pop .12s cubic-bezier(0,0,.2,1)}.rpt-combobox-menu.opens-above{top:auto;bottom:calc(100% + 4px);box-shadow:0 -10px 25px -5px #0000001f,0 -8px 10px -6px #00000014;animation:rpt-dropdown-pop-above .12s cubic-bezier(0,0,.2,1)}@keyframes rpt-dropdown-pop{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}@keyframes rpt-dropdown-pop-above{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.combobox-search-box{display:flex;align-items:center;gap:8px;padding:8px 10px;border-bottom:1px solid var(--rpt-color-border, #e2e8f0);background-color:#f8fafc}.combobox-search-icon{color:var(--rpt-color-text-muted, #94a3b8);display:flex;align-items:center;flex-shrink:0}.combobox-search-input{flex:1;border:none;outline:none;background:transparent;font-size:var(--rpt-font-size-sm, .8125rem);font-family:inherit;color:var(--rpt-color-text, #0f172a);min-width:0}.combobox-search-input::placeholder{color:#94a3b8}.combobox-search-clear{border:none;background:#e2e8f0;color:#64748b;width:16px;height:16px;border-radius:50%;font-size:.625rem;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0}.combobox-search-clear:hover{background:#cbd5e1;color:#0f172a}.combobox-options-list{max-height:220px;overflow-y:auto;overscroll-behavior:contain;padding:4px}.combobox-option-item{display:flex;align-items:center;justify-content:space-between;padding:7px 10px;border-radius:4px;font-size:var(--rpt-font-size-base, .84rem);color:var(--rpt-color-text, #0f172a);cursor:pointer;-webkit-user-select:none;user-select:none;transition:background-color .1s ease}.combobox-option-item.is-focused{background-color:var(--rpt-color-surface-hover, #f1f5f9)}.combobox-option-item.is-selected{background-color:#eff6ff;color:var(--rpt-color-primary, #2563eb);font-weight:600}.opt-label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.opt-check{font-size:.875rem;font-weight:700;color:var(--rpt-color-primary, #2563eb);margin-left:8px}.combobox-status-row{padding:10px 12px;font-size:.78rem;text-align:center;color:var(--rpt-color-text-muted, #64748b)}.combobox-status-row.loading{display:flex;align-items:center;justify-content:center;gap:6px;color:#2563eb}.combobox-status-row.error{color:var(--rpt-color-danger, #dc2626)}.combobox-status-row.hint{font-size:.72rem;color:#94a3b8;border-top:1px dashed #e2e8f0;margin-top:4px;padding:6px}.rpt-error-text{font-size:.7rem;color:var(--rpt-color-danger, #ef4444);margin-top:3px;line-height:1.25;max-width:100%;overflow-wrap:anywhere;word-break:break-word;display:block}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3079
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: RptDropdownComponent, isStandalone: true, selector: "rpt-dropdown", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, control: { classPropertyName: "control", publicName: "control", isSignal: true, isRequired: false, transformFunction: null }, parentForm: { classPropertyName: "parentForm", publicName: "parentForm", isSignal: true, isRequired: false, transformFunction: null }, showStatus: { classPropertyName: "showStatus", publicName: "showStatus", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()" }, properties: { "class.is-open": "isOpen()", "class.opens-above": "opensAbove()" } }, viewQueries: [{ propertyName: "comboboxWrapperEl", first: true, predicate: ["comboboxWrapper"], descendants: true, isSignal: true }, { propertyName: "searchInputEl", first: true, predicate: ["searchInput"], descendants: true, isSignal: true }, { propertyName: "optionsListEl", first: true, predicate: ["optionsListEl"], descendants: true, isSignal: true }], ngImport: i0, template: " <div class=\"rpt-field-group\">\n @if (config().label) {\n <label [for]=\"elementId()\" class=\"rpt-label\">\n {{ config().label }}\n @if (config().validation?.required) {\n <span class=\"rpt-required-marker\">*</span>\n }\n </label>\n }\n\n @if (isSearchable()) {\n <!-- Searchable Typeahead Combobox -->\n <div class=\"rpt-combobox-wrapper\" #comboboxWrapper>\n <button\n type=\"button\"\n class=\"rpt-combobox-trigger\"\n [id]=\"elementId()\"\n [class.rpt-invalid]=\"isInvalid()\"\n [class.is-open]=\"isOpen()\"\n [disabled]=\"formControl().disabled\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"'listbox'\"\n (click)=\"toggleOpen($event)\"\n (keydown)=\"onTriggerKeydown($event)\"\n >\n <span class=\"trigger-label\" [class.is-placeholder]=\"!selectedOption()\">\n {{ selectedOption() ? selectedOption()!.label : placeholder() }}\n </span>\n\n <div class=\"trigger-actions\">\n @if (isLoading()) {\n <span class=\"status-dot dot-pulse\" style=\"color: #2563eb; margin-right: 4px;\" title=\"Executing query on database...\"></span>\n }\n @if (loadError() && !isLoading()) {\n <button\n type=\"button\"\n class=\"trigger-retry-btn\"\n title=\"Retry database query\"\n aria-label=\"Retry database query\"\n (click)=\"retryLookup($event)\"\n >\n \u21BB Retry\n </button>\n }\n @if (selectedOption() && !formControl().disabled) {\n <span\n role=\"button\"\n tabindex=\"0\"\n class=\"trigger-clear-btn\"\n title=\"Clear selection\"\n aria-label=\"Clear selection\"\n (click)=\"clearSelection($event)\"\n (keydown.enter)=\"clearSelection($event)\"\n (keydown.space)=\"clearSelection($event)\"\n >\n \u2715\n </span>\n }\n <span class=\"trigger-arrow\" [class.rotated]=\"isOpen()\" aria-hidden=\"true\">\n <svg width=\"12\" height=\"8\" viewBox=\"0 0 12 8\" fill=\"none\">\n <path d=\"M1 1.5L6 6.5L11 1.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </span>\n </div>\n </button>\n\n @if (isOpen()) {\n <div\n class=\"rpt-combobox-menu\"\n [class.opens-above]=\"opensAbove()\"\n role=\"listbox\"\n (click)=\"$event.stopPropagation()\"\n >\n <!-- Real-time Filter Search Box -->\n <div class=\"combobox-search-box\">\n <span class=\"combobox-search-icon\" aria-hidden=\"true\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"></circle>\n <line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"></line>\n </svg>\n </span>\n <input\n #searchInput\n type=\"text\"\n class=\"combobox-search-input\"\n placeholder=\"Type to filter...\"\n [value]=\"searchTerm()\"\n [attr.aria-activedescendant]=\"focusedIndex() >= 0 ? (elementId() + '_opt_' + focusedIndex()) : null\"\n (input)=\"onSearchInput($event)\"\n (keydown)=\"onSearchKeydown($event)\"\n />\n @if (searchTerm()) {\n <button\n type=\"button\"\n class=\"combobox-search-clear\"\n title=\"Clear filter\"\n (click)=\"clearSearch($event)\"\n >\n \u2715\n </button>\n }\n </div>\n\n <!-- Filtered Options List -->\n <div class=\"combobox-options-list\" #optionsListEl>\n @if (waitingFor().length > 0) {\n <div class=\"combobox-status-row waiting\">\n <span>Waiting for {{ waitingLabel() }}...</span>\n </div>\n } @else if (isLoading()) {\n <div class=\"combobox-status-row loading\">\n <span class=\"status-dot dot-pulse\"></span>\n <span>Executing query on database (may take 1\u20132 minutes on local database)...</span>\n </div>\n } @else if (loadError()) {\n <div class=\"combobox-status-row error\">\n <span>Error: {{ loadError() }}</span>\n <button type=\"button\" class=\"combobox-retry-btn\" (click)=\"retryLookup($event)\">\u21BB Retry Query</button>\n </div>\n } @else if (isDynamicSource() && hasLoaded() && allOptions().length === 0) {\n <div class=\"combobox-status-row empty\">\n <span>No records found (check parameters)</span>\n </div>\n } @else if (filteredOptions().length === 0) {\n <div class=\"combobox-status-row empty\">\n <span>No options matching \"{{ searchTerm() }}\"</span>\n </div>\n } @else {\n @for (opt of visibleFilteredOptions(); track opt.key; let i = $index) {\n <div\n role=\"option\"\n [id]=\"elementId() + '_opt_' + i\"\n class=\"combobox-option-item\"\n [class.is-selected]=\"isSelected(opt)\"\n [class.is-focused]=\"focusedIndex() === i\"\n [attr.aria-selected]=\"isSelected(opt)\"\n (click)=\"selectOption(opt)\"\n (mouseenter)=\"focusedIndex.set(i)\"\n >\n <span class=\"opt-label\">{{ opt.label }}</span>\n @if (isSelected(opt)) {\n <span class=\"opt-check\" aria-hidden=\"true\">\u2713</span>\n }\n </div>\n }\n @if (excessFilteredCount() > 0) {\n <div class=\"combobox-status-row hint\">\n <span>Showing {{ MAX_FILTER_RENDER }} of {{ filteredOptions().length }} matches. Type to refine.</span>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n } @else {\n <!-- Native Browser Select Fallback -->\n <div class=\"rpt-select-wrapper\">\n <select\n [id]=\"elementId()\"\n [formControl]=\"formControl()\"\n [class.rpt-invalid]=\"isInvalid()\"\n class=\"rpt-select\"\n >\n @if (waitingFor().length > 0) {\n <option value=\"\" disabled selected>Waiting for {{ waitingLabel() }}...</option>\n } @else if (isLoading()) {\n <option value=\"\" disabled selected>Loading options from database...</option>\n } @else if (loadError()) {\n <option value=\"\" disabled selected>Error: {{ loadError() }}</option>\n } @else if (isDynamicSource() && hasLoaded() && allOptions().length === 0) {\n <option value=\"\" disabled selected>-- No records found (check parameters) --</option>\n } @else if (placeholder()) {\n <option value=\"\" [disabled]=\"config().validation?.required\">{{ placeholder() }}</option>\n }\n @for (opt of renderedOptions(); track opt.key) {\n <option [value]=\"opt.key\">{{ opt.label }}</option>\n }\n @if (hasExcessOptions()) {\n <option value=\"\" disabled>... and {{ excessOptionsCount() }} more records (refine filter) ...</option>\n }\n </select>\n <div class=\"rpt-select-arrow\" aria-hidden=\"true\">\n <svg width=\"12\" height=\"8\" viewBox=\"0 0 12 8\" fill=\"none\">\n <path d=\"M1 1.5L6 6.5L11 1.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </div>\n </div>\n }\n\n @if (showStatus() && isDynamicSource() && statusText()) {\n <div class=\"rpt-status-badge\" [attr.data-status]=\"statusKind()\">\n @switch (statusKind()) {\n @case ('loading') {\n <span class=\"status-dot dot-pulse\"></span>\n <span>Loading from database\u2026</span>\n }\n @case ('ok') {\n <span class=\"status-dot dot-ok\"></span>\n <span>{{ dynamicOptions().length }} records loaded</span>\n }\n @case ('waiting') {\n <span class=\"status-dot dot-warn\"></span>\n <span>Waiting for:</span>\n <div class=\"waiting-tokens\">\n @for (k of waitingFor(); track k) {\n <span class=\"waiting-token\">{{ k }}</span>\n }\n </div>\n }\n @case ('error') {\n <span class=\"status-dot dot-err\"></span>\n <span>{{ loadError() }}</span>\n <button type=\"button\" class=\"rpt-badge-retry-btn\" (click)=\"retryLookup($event)\">\u21BB Retry</button>\n }\n @case ('empty') {\n <span class=\"status-dot\"></span>\n <span>No records found for parameters</span>\n }\n @default {\n <span class=\"status-dot\"></span>\n }\n }\n </div>\n }\n\n @if (errorMessage()) {\n <span class=\"rpt-error-text\">{{ errorMessage() }}</span>\n }\n </div>", styles: [":host{display:block;width:100%;max-width:100%;min-width:0;box-sizing:border-box;font-family:var(--rpt-font-family, inherit);position:relative}.rpt-status-badge{display:flex;flex-wrap:wrap;align-items:center;gap:4px;font-size:.6875rem;padding:3px 6px;border-radius:4px;margin-top:4px;font-weight:500;line-height:1.3;max-width:100%;box-sizing:border-box;overflow:hidden;word-break:break-word}.rpt-status-badge>span{overflow-wrap:anywhere;word-break:break-word}.rpt-status-badge[data-status=loading]{background:#eff6ff;color:#2563eb;border:1px solid #dbeafe}.rpt-status-badge[data-status=ok]{background:#f0fdf4;color:#16a34a;border:1px solid #dcfce7}.rpt-status-badge[data-status=waiting]{background:#fffbeb;color:#d97706;border:1px solid #fef3c7}.rpt-status-badge[data-status=error]{background:#fef2f2;color:#dc2626;border:1px solid #fee2e2}.rpt-status-badge[data-status=empty]{background:#f8fafc;color:#64748b;border:1px solid #e2e8f0}.waiting-tokens{display:inline-flex;flex-wrap:wrap;align-items:center;gap:3px;margin-left:2px;max-width:100%}.waiting-token{background:#b453091f;border:1px solid rgba(180,83,9,.3);padding:1px 5px;border-radius:3px;font-weight:700;font-size:.625rem;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block;box-sizing:border-box}.status-dot{width:6px;height:6px;border-radius:50%;background:currentColor}.dot-pulse{animation:rpt-pulse 1s infinite alternate}@keyframes rpt-pulse{0%{opacity:.3}to{opacity:1}}.rpt-field-group{display:flex;flex-direction:column;max-width:100%;min-width:0;gap:4px;box-sizing:border-box;position:relative}.rpt-label{font-size:var(--rpt-font-size-sm, .75rem);font-weight:500;color:var(--rpt-color-text, #0f172a);flex-shrink:0}.rpt-required-marker{color:var(--rpt-color-danger, #ef4444);margin-left:2px}.rpt-select-wrapper{position:relative;width:100%;flex:1;min-height:0;display:flex;align-items:center}.rpt-select{width:100%;height:100%;min-height:38px;box-sizing:border-box;padding:6px 36px 6px 12px;font-size:var(--rpt-font-size-base, .875rem);color:var(--rpt-color-text, #0f172a);background-color:var(--rpt-color-bg, #ffffff);border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius, 6px);appearance:none;-webkit-appearance:none;outline:none;cursor:pointer;transition:border-color var(--rpt-transition-speed, .15s) ease,box-shadow var(--rpt-transition-speed, .15s) ease}.rpt-select:hover:not(:disabled){border-color:var(--rpt-color-border-hover, #94a3b8)}.rpt-select:focus{border-color:var(--rpt-color-border-focus, #2563eb);box-shadow:var(--rpt-box-shadow-focus, 0 0 0 3px rgba(37, 99, 235, .25))}.rpt-select:disabled{background-color:var(--rpt-color-surface, #f8fafc);color:var(--rpt-color-text-muted, #64748b);cursor:not-allowed}.rpt-select.rpt-invalid{border-color:var(--rpt-color-danger, #ef4444)}.rpt-select.rpt-invalid:focus{box-shadow:0 0 0 3px #ef444440}.rpt-select-arrow{position:absolute;right:12px;top:50%;transform:translateY(-50%);pointer-events:none;color:var(--rpt-color-text-muted, #64748b);display:flex;align-items:center}.rpt-combobox-wrapper{position:relative;width:100%;flex:1;min-height:0}.rpt-combobox-trigger{width:100%;height:100%;min-height:38px;box-sizing:border-box;padding:6px 10px 6px 12px;display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:var(--rpt-font-size-base, .875rem);font-family:inherit;color:var(--rpt-color-text, #0f172a);background-color:var(--rpt-color-bg, #ffffff);border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius, 6px);outline:none;cursor:pointer;text-align:left;transition:border-color var(--rpt-transition-speed, .15s) ease,box-shadow var(--rpt-transition-speed, .15s) ease}.rpt-combobox-trigger:hover:not(:disabled){border-color:var(--rpt-color-border-hover, #94a3b8)}.rpt-combobox-trigger:focus-visible,.rpt-combobox-trigger.is-open{border-color:var(--rpt-color-border-focus, #2563eb);box-shadow:var(--rpt-box-shadow-focus, 0 0 0 3px rgba(37, 99, 235, .25))}.rpt-combobox-trigger:disabled{background-color:var(--rpt-color-surface, #f8fafc);color:var(--rpt-color-text-muted, #64748b);cursor:not-allowed}.rpt-combobox-trigger.rpt-invalid{border-color:var(--rpt-color-danger, #ef4444)}.rpt-combobox-trigger.rpt-invalid:focus-visible{box-shadow:0 0 0 3px #ef444440}.trigger-label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.3}.trigger-label.is-placeholder{color:var(--rpt-color-text-muted, #94a3b8)}.trigger-actions{display:flex;align-items:center;gap:6px;flex-shrink:0}.trigger-clear-btn{display:flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;font-size:.6875rem;color:var(--rpt-color-text-muted, #64748b);background-color:#f1f5f9;cursor:pointer;-webkit-user-select:none;user-select:none;transition:background-color .15s ease,color .15s ease}.trigger-clear-btn:hover{background-color:#e2e8f0;color:var(--rpt-color-text, #0f172a)}.trigger-arrow{display:flex;align-items:center;color:var(--rpt-color-text-muted, #64748b);transition:transform .18s ease}.trigger-arrow.rotated{transform:rotate(180deg)}.rpt-combobox-menu{position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:1050;background-color:var(--rpt-color-bg, #ffffff);border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius, 6px);box-shadow:0 10px 25px -5px #0000001f,0 8px 10px -6px #00000014;overflow:hidden;display:flex;flex-direction:column;animation:rpt-dropdown-pop .12s cubic-bezier(0,0,.2,1)}.rpt-combobox-menu.opens-above{top:auto;bottom:calc(100% + 4px);box-shadow:0 -10px 25px -5px #0000001f,0 -8px 10px -6px #00000014;animation:rpt-dropdown-pop-above .12s cubic-bezier(0,0,.2,1)}@keyframes rpt-dropdown-pop{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}@keyframes rpt-dropdown-pop-above{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.combobox-search-box{display:flex;align-items:center;gap:8px;padding:8px 10px;border-bottom:1px solid var(--rpt-color-border, #e2e8f0);background-color:#f8fafc}.combobox-search-icon{color:var(--rpt-color-text-muted, #94a3b8);display:flex;align-items:center;flex-shrink:0}.combobox-search-input{flex:1;border:none;outline:none;background:transparent;font-size:var(--rpt-font-size-sm, .8125rem);font-family:inherit;color:var(--rpt-color-text, #0f172a);min-width:0}.combobox-search-input::placeholder{color:#94a3b8}.combobox-search-clear{border:none;background:#e2e8f0;color:#64748b;width:16px;height:16px;border-radius:50%;font-size:.625rem;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0}.combobox-search-clear:hover{background:#cbd5e1;color:#0f172a}.combobox-options-list{max-height:220px;overflow-y:auto;overscroll-behavior:contain;padding:4px}.combobox-option-item{display:flex;align-items:center;justify-content:space-between;padding:7px 10px;border-radius:4px;font-size:var(--rpt-font-size-base, .84rem);color:var(--rpt-color-text, #0f172a);cursor:pointer;-webkit-user-select:none;user-select:none;transition:background-color .1s ease}.combobox-option-item.is-focused{background-color:var(--rpt-color-surface-hover, #f1f5f9)}.combobox-option-item.is-selected{background-color:#eff6ff;color:var(--rpt-color-primary, #2563eb);font-weight:600}.opt-label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.opt-check{font-size:.875rem;font-weight:700;color:var(--rpt-color-primary, #2563eb);margin-left:8px}.combobox-status-row{padding:10px 12px;font-size:.78rem;text-align:center;color:var(--rpt-color-text-muted, #64748b)}.combobox-status-row.loading{display:flex;align-items:center;justify-content:center;gap:6px;color:#2563eb}.combobox-status-row.error{display:flex;flex-direction:column;align-items:center;gap:6px;color:var(--rpt-color-danger, #dc2626)}.trigger-retry-btn,.combobox-retry-btn,.rpt-badge-retry-btn{display:inline-flex;align-items:center;gap:3px;font-size:.6875rem;font-weight:600;color:#dc2626;background:#fee2e2;border:1px solid #fca5a5;border-radius:4px;padding:2px 7px;cursor:pointer;line-height:1.2;transition:background-color .12s ease,color .12s ease}.trigger-retry-btn:hover,.combobox-retry-btn:hover,.rpt-badge-retry-btn:hover{background:#fecaca;color:#b91c1c;border-color:#f87171}.combobox-status-row.hint{font-size:.72rem;color:#94a3b8;border-top:1px dashed #e2e8f0;margin-top:4px;padding:6px}.rpt-error-text{font-size:.7rem;color:var(--rpt-color-danger, #ef4444);margin-top:3px;line-height:1.25;max-width:100%;overflow-wrap:anywhere;word-break:break-word;display:block}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2911
3080
|
}
|
|
2912
3081
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RptDropdownComponent, decorators: [{
|
|
2913
3082
|
type: Component,
|
|
2914
3083
|
args: [{ selector: 'rpt-dropdown', standalone: true, imports: [ReactiveFormsModule], host: {
|
|
2915
3084
|
'[class.is-open]': 'isOpen()',
|
|
2916
3085
|
'[class.opens-above]': 'opensAbove()',
|
|
2917
|
-
}, changeDetection: ChangeDetectionStrategy.OnPush, template: " <div class=\"rpt-field-group\">\n @if (config().label) {\n <label [for]=\"elementId()\" class=\"rpt-label\">\n {{ config().label }}\n @if (config().validation?.required) {\n <span class=\"rpt-required-marker\">*</span>\n }\n </label>\n }\n\n @if (isSearchable()) {\n <!-- Searchable Typeahead Combobox -->\n <div class=\"rpt-combobox-wrapper\" #comboboxWrapper>\n <button\n type=\"button\"\n class=\"rpt-combobox-trigger\"\n [id]=\"elementId()\"\n [class.rpt-invalid]=\"isInvalid()\"\n [class.is-open]=\"isOpen()\"\n [disabled]=\"formControl().disabled\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"'listbox'\"\n (click)=\"toggleOpen($event)\"\n (keydown)=\"onTriggerKeydown($event)\"\n >\n <span class=\"trigger-label\" [class.is-placeholder]=\"!selectedOption()\">\n {{ selectedOption() ? selectedOption()!.label : placeholder() }}\n </span>\n\n <div class=\"trigger-actions\">\n @if (selectedOption() && !formControl().disabled) {\n <span\n role=\"button\"\n tabindex=\"0\"\n class=\"trigger-clear-btn\"\n title=\"Clear selection\"\n aria-label=\"Clear selection\"\n (click)=\"clearSelection($event)\"\n (keydown.enter)=\"clearSelection($event)\"\n (keydown.space)=\"clearSelection($event)\"\n >\n \u2715\n </span>\n }\n <span class=\"trigger-arrow\" [class.rotated]=\"isOpen()\" aria-hidden=\"true\">\n <svg width=\"12\" height=\"8\" viewBox=\"0 0 12 8\" fill=\"none\">\n <path d=\"M1 1.5L6 6.5L11 1.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </span>\n </div>\n </button>\n\n @if (isOpen()) {\n <div\n class=\"rpt-combobox-menu\"\n [class.opens-above]=\"opensAbove()\"\n role=\"listbox\"\n (click)=\"$event.stopPropagation()\"\n >\n <!-- Real-time Filter Search Box -->\n <div class=\"combobox-search-box\">\n <span class=\"combobox-search-icon\" aria-hidden=\"true\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"></circle>\n <line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"></line>\n </svg>\n </span>\n <input\n #searchInput\n type=\"text\"\n class=\"combobox-search-input\"\n placeholder=\"Type to filter...\"\n [value]=\"searchTerm()\"\n [attr.aria-activedescendant]=\"focusedIndex() >= 0 ? (elementId() + '_opt_' + focusedIndex()) : null\"\n (input)=\"onSearchInput($event)\"\n (keydown)=\"onSearchKeydown($event)\"\n />\n @if (searchTerm()) {\n <button\n type=\"button\"\n class=\"combobox-search-clear\"\n title=\"Clear filter\"\n (click)=\"clearSearch($event)\"\n >\n \u2715\n </button>\n }\n </div>\n\n <!-- Filtered Options List -->\n <div class=\"combobox-options-list\" #optionsListEl>\n @if (isLoading()) {\n <div class=\"combobox-status-row loading\">\n <span class=\"status-dot dot-pulse\"></span>\n <span>Loading options from database...</span>\n </div>\n } @else if (loadError()) {\n <div class=\"combobox-status-row error\">\n <span>Error: {{ loadError() }}</span>\n </div>\n } @else if (isDynamicSource() && hasLoaded() && allOptions().length === 0) {\n <div class=\"combobox-status-row empty\">\n <span>No records found (check parameters)</span>\n </div>\n } @else if (filteredOptions().length === 0) {\n <div class=\"combobox-status-row empty\">\n <span>No options matching \"{{ searchTerm() }}\"</span>\n </div>\n } @else {\n @for (opt of visibleFilteredOptions(); track opt.key; let i = $index) {\n <div\n role=\"option\"\n [id]=\"elementId() + '_opt_' + i\"\n class=\"combobox-option-item\"\n [class.is-selected]=\"isSelected(opt)\"\n [class.is-focused]=\"focusedIndex() === i\"\n [attr.aria-selected]=\"isSelected(opt)\"\n (click)=\"selectOption(opt)\"\n (mouseenter)=\"focusedIndex.set(i)\"\n >\n <span class=\"opt-label\">{{ opt.label }}</span>\n @if (isSelected(opt)) {\n <span class=\"opt-check\" aria-hidden=\"true\">\u2713</span>\n }\n </div>\n }\n @if (excessFilteredCount() > 0) {\n <div class=\"combobox-status-row hint\">\n <span>Showing {{ MAX_FILTER_RENDER }} of {{ filteredOptions().length }} matches. Type to refine.</span>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n } @else {\n <!-- Native Browser Select Fallback -->\n <div class=\"rpt-select-wrapper\">\n <select\n [id]=\"elementId()\"\n [formControl]=\"formControl()\"\n [class.rpt-invalid]=\"isInvalid()\"\n class=\"rpt-select\"\n >\n @if (isLoading()) {\n <option value=\"\" disabled selected>Loading options from database...</option>\n } @else if (loadError()) {\n <option value=\"\" disabled selected>Error: {{ loadError() }}</option>\n } @else if (isDynamicSource() && hasLoaded() && allOptions().length === 0) {\n <option value=\"\" disabled selected>-- No records found (check parameters) --</option>\n } @else if (placeholder()) {\n <option value=\"\" [disabled]=\"config().validation?.required\">{{ placeholder() }}</option>\n }\n @for (opt of renderedOptions(); track opt.key) {\n <option [value]=\"opt.key\">{{ opt.label }}</option>\n }\n @if (hasExcessOptions()) {\n <option value=\"\" disabled>... and {{ excessOptionsCount() }} more records (refine filter) ...</option>\n }\n </select>\n <div class=\"rpt-select-arrow\" aria-hidden=\"true\">\n <svg width=\"12\" height=\"8\" viewBox=\"0 0 12 8\" fill=\"none\">\n <path d=\"M1 1.5L6 6.5L11 1.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </div>\n </div>\n }\n\n @if (showStatus() && isDynamicSource() && statusText()) {\n <div class=\"rpt-status-badge\" [attr.data-status]=\"statusKind()\">\n @switch (statusKind()) {\n @case ('loading') {\n <span class=\"status-dot dot-pulse\"></span>\n <span>Loading from database\u2026</span>\n }\n @case ('ok') {\n <span class=\"status-dot dot-ok\"></span>\n <span>{{ dynamicOptions().length }} records loaded</span>\n }\n @case ('waiting') {\n <span class=\"status-dot dot-warn\"></span>\n <span>Waiting for:</span>\n <div class=\"waiting-tokens\">\n @for (k of waitingFor(); track k) {\n <span class=\"waiting-token\">{{ k }}</span>\n }\n </div>\n }\n @case ('error') {\n <span class=\"status-dot dot-err\"></span>\n <span>{{ loadError() }}</span>\n }\n @case ('empty') {\n <span class=\"status-dot\"></span>\n <span>No records found for parameters</span>\n }\n @default {\n <span class=\"status-dot\"></span>\n }\n }\n </div>\n }\n\n @if (errorMessage()) {\n <span class=\"rpt-error-text\">{{ errorMessage() }}</span>\n }\n </div>", styles: [":host{display:block;width:100%;max-width:100%;min-width:0;box-sizing:border-box;font-family:var(--rpt-font-family, inherit);position:relative}.rpt-status-badge{display:flex;flex-wrap:wrap;align-items:center;gap:4px;font-size:.6875rem;padding:3px 6px;border-radius:4px;margin-top:4px;font-weight:500;line-height:1.3;max-width:100%;box-sizing:border-box;overflow:hidden;word-break:break-word}.rpt-status-badge>span{overflow-wrap:anywhere;word-break:break-word}.rpt-status-badge[data-status=loading]{background:#eff6ff;color:#2563eb;border:1px solid #dbeafe}.rpt-status-badge[data-status=ok]{background:#f0fdf4;color:#16a34a;border:1px solid #dcfce7}.rpt-status-badge[data-status=waiting]{background:#fffbeb;color:#d97706;border:1px solid #fef3c7}.rpt-status-badge[data-status=error]{background:#fef2f2;color:#dc2626;border:1px solid #fee2e2}.rpt-status-badge[data-status=empty]{background:#f8fafc;color:#64748b;border:1px solid #e2e8f0}.waiting-tokens{display:inline-flex;flex-wrap:wrap;align-items:center;gap:3px;margin-left:2px;max-width:100%}.waiting-token{background:#b453091f;border:1px solid rgba(180,83,9,.3);padding:1px 5px;border-radius:3px;font-weight:700;font-size:.625rem;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block;box-sizing:border-box}.status-dot{width:6px;height:6px;border-radius:50%;background:currentColor}.dot-pulse{animation:rpt-pulse 1s infinite alternate}@keyframes rpt-pulse{0%{opacity:.3}to{opacity:1}}.rpt-field-group{display:flex;flex-direction:column;max-width:100%;min-width:0;gap:4px;box-sizing:border-box;position:relative}.rpt-label{font-size:var(--rpt-font-size-sm, .75rem);font-weight:500;color:var(--rpt-color-text, #0f172a);flex-shrink:0}.rpt-required-marker{color:var(--rpt-color-danger, #ef4444);margin-left:2px}.rpt-select-wrapper{position:relative;width:100%;flex:1;min-height:0;display:flex;align-items:center}.rpt-select{width:100%;height:100%;min-height:38px;box-sizing:border-box;padding:6px 36px 6px 12px;font-size:var(--rpt-font-size-base, .875rem);color:var(--rpt-color-text, #0f172a);background-color:var(--rpt-color-bg, #ffffff);border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius, 6px);appearance:none;-webkit-appearance:none;outline:none;cursor:pointer;transition:border-color var(--rpt-transition-speed, .15s) ease,box-shadow var(--rpt-transition-speed, .15s) ease}.rpt-select:hover:not(:disabled){border-color:var(--rpt-color-border-hover, #94a3b8)}.rpt-select:focus{border-color:var(--rpt-color-border-focus, #2563eb);box-shadow:var(--rpt-box-shadow-focus, 0 0 0 3px rgba(37, 99, 235, .25))}.rpt-select:disabled{background-color:var(--rpt-color-surface, #f8fafc);color:var(--rpt-color-text-muted, #64748b);cursor:not-allowed}.rpt-select.rpt-invalid{border-color:var(--rpt-color-danger, #ef4444)}.rpt-select.rpt-invalid:focus{box-shadow:0 0 0 3px #ef444440}.rpt-select-arrow{position:absolute;right:12px;top:50%;transform:translateY(-50%);pointer-events:none;color:var(--rpt-color-text-muted, #64748b);display:flex;align-items:center}.rpt-combobox-wrapper{position:relative;width:100%;flex:1;min-height:0}.rpt-combobox-trigger{width:100%;height:100%;min-height:38px;box-sizing:border-box;padding:6px 10px 6px 12px;display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:var(--rpt-font-size-base, .875rem);font-family:inherit;color:var(--rpt-color-text, #0f172a);background-color:var(--rpt-color-bg, #ffffff);border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius, 6px);outline:none;cursor:pointer;text-align:left;transition:border-color var(--rpt-transition-speed, .15s) ease,box-shadow var(--rpt-transition-speed, .15s) ease}.rpt-combobox-trigger:hover:not(:disabled){border-color:var(--rpt-color-border-hover, #94a3b8)}.rpt-combobox-trigger:focus-visible,.rpt-combobox-trigger.is-open{border-color:var(--rpt-color-border-focus, #2563eb);box-shadow:var(--rpt-box-shadow-focus, 0 0 0 3px rgba(37, 99, 235, .25))}.rpt-combobox-trigger:disabled{background-color:var(--rpt-color-surface, #f8fafc);color:var(--rpt-color-text-muted, #64748b);cursor:not-allowed}.rpt-combobox-trigger.rpt-invalid{border-color:var(--rpt-color-danger, #ef4444)}.rpt-combobox-trigger.rpt-invalid:focus-visible{box-shadow:0 0 0 3px #ef444440}.trigger-label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.3}.trigger-label.is-placeholder{color:var(--rpt-color-text-muted, #94a3b8)}.trigger-actions{display:flex;align-items:center;gap:6px;flex-shrink:0}.trigger-clear-btn{display:flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;font-size:.6875rem;color:var(--rpt-color-text-muted, #64748b);background-color:#f1f5f9;cursor:pointer;-webkit-user-select:none;user-select:none;transition:background-color .15s ease,color .15s ease}.trigger-clear-btn:hover{background-color:#e2e8f0;color:var(--rpt-color-text, #0f172a)}.trigger-arrow{display:flex;align-items:center;color:var(--rpt-color-text-muted, #64748b);transition:transform .18s ease}.trigger-arrow.rotated{transform:rotate(180deg)}.rpt-combobox-menu{position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:1050;background-color:var(--rpt-color-bg, #ffffff);border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius, 6px);box-shadow:0 10px 25px -5px #0000001f,0 8px 10px -6px #00000014;overflow:hidden;display:flex;flex-direction:column;animation:rpt-dropdown-pop .12s cubic-bezier(0,0,.2,1)}.rpt-combobox-menu.opens-above{top:auto;bottom:calc(100% + 4px);box-shadow:0 -10px 25px -5px #0000001f,0 -8px 10px -6px #00000014;animation:rpt-dropdown-pop-above .12s cubic-bezier(0,0,.2,1)}@keyframes rpt-dropdown-pop{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}@keyframes rpt-dropdown-pop-above{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.combobox-search-box{display:flex;align-items:center;gap:8px;padding:8px 10px;border-bottom:1px solid var(--rpt-color-border, #e2e8f0);background-color:#f8fafc}.combobox-search-icon{color:var(--rpt-color-text-muted, #94a3b8);display:flex;align-items:center;flex-shrink:0}.combobox-search-input{flex:1;border:none;outline:none;background:transparent;font-size:var(--rpt-font-size-sm, .8125rem);font-family:inherit;color:var(--rpt-color-text, #0f172a);min-width:0}.combobox-search-input::placeholder{color:#94a3b8}.combobox-search-clear{border:none;background:#e2e8f0;color:#64748b;width:16px;height:16px;border-radius:50%;font-size:.625rem;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0}.combobox-search-clear:hover{background:#cbd5e1;color:#0f172a}.combobox-options-list{max-height:220px;overflow-y:auto;overscroll-behavior:contain;padding:4px}.combobox-option-item{display:flex;align-items:center;justify-content:space-between;padding:7px 10px;border-radius:4px;font-size:var(--rpt-font-size-base, .84rem);color:var(--rpt-color-text, #0f172a);cursor:pointer;-webkit-user-select:none;user-select:none;transition:background-color .1s ease}.combobox-option-item.is-focused{background-color:var(--rpt-color-surface-hover, #f1f5f9)}.combobox-option-item.is-selected{background-color:#eff6ff;color:var(--rpt-color-primary, #2563eb);font-weight:600}.opt-label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.opt-check{font-size:.875rem;font-weight:700;color:var(--rpt-color-primary, #2563eb);margin-left:8px}.combobox-status-row{padding:10px 12px;font-size:.78rem;text-align:center;color:var(--rpt-color-text-muted, #64748b)}.combobox-status-row.loading{display:flex;align-items:center;justify-content:center;gap:6px;color:#2563eb}.combobox-status-row.error{color:var(--rpt-color-danger, #dc2626)}.combobox-status-row.hint{font-size:.72rem;color:#94a3b8;border-top:1px dashed #e2e8f0;margin-top:4px;padding:6px}.rpt-error-text{font-size:.7rem;color:var(--rpt-color-danger, #ef4444);margin-top:3px;line-height:1.25;max-width:100%;overflow-wrap:anywhere;word-break:break-word;display:block}\n"] }]
|
|
3086
|
+
}, changeDetection: ChangeDetectionStrategy.OnPush, template: " <div class=\"rpt-field-group\">\n @if (config().label) {\n <label [for]=\"elementId()\" class=\"rpt-label\">\n {{ config().label }}\n @if (config().validation?.required) {\n <span class=\"rpt-required-marker\">*</span>\n }\n </label>\n }\n\n @if (isSearchable()) {\n <!-- Searchable Typeahead Combobox -->\n <div class=\"rpt-combobox-wrapper\" #comboboxWrapper>\n <button\n type=\"button\"\n class=\"rpt-combobox-trigger\"\n [id]=\"elementId()\"\n [class.rpt-invalid]=\"isInvalid()\"\n [class.is-open]=\"isOpen()\"\n [disabled]=\"formControl().disabled\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"'listbox'\"\n (click)=\"toggleOpen($event)\"\n (keydown)=\"onTriggerKeydown($event)\"\n >\n <span class=\"trigger-label\" [class.is-placeholder]=\"!selectedOption()\">\n {{ selectedOption() ? selectedOption()!.label : placeholder() }}\n </span>\n\n <div class=\"trigger-actions\">\n @if (isLoading()) {\n <span class=\"status-dot dot-pulse\" style=\"color: #2563eb; margin-right: 4px;\" title=\"Executing query on database...\"></span>\n }\n @if (loadError() && !isLoading()) {\n <button\n type=\"button\"\n class=\"trigger-retry-btn\"\n title=\"Retry database query\"\n aria-label=\"Retry database query\"\n (click)=\"retryLookup($event)\"\n >\n \u21BB Retry\n </button>\n }\n @if (selectedOption() && !formControl().disabled) {\n <span\n role=\"button\"\n tabindex=\"0\"\n class=\"trigger-clear-btn\"\n title=\"Clear selection\"\n aria-label=\"Clear selection\"\n (click)=\"clearSelection($event)\"\n (keydown.enter)=\"clearSelection($event)\"\n (keydown.space)=\"clearSelection($event)\"\n >\n \u2715\n </span>\n }\n <span class=\"trigger-arrow\" [class.rotated]=\"isOpen()\" aria-hidden=\"true\">\n <svg width=\"12\" height=\"8\" viewBox=\"0 0 12 8\" fill=\"none\">\n <path d=\"M1 1.5L6 6.5L11 1.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </span>\n </div>\n </button>\n\n @if (isOpen()) {\n <div\n class=\"rpt-combobox-menu\"\n [class.opens-above]=\"opensAbove()\"\n role=\"listbox\"\n (click)=\"$event.stopPropagation()\"\n >\n <!-- Real-time Filter Search Box -->\n <div class=\"combobox-search-box\">\n <span class=\"combobox-search-icon\" aria-hidden=\"true\">\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <circle cx=\"11\" cy=\"11\" r=\"8\"></circle>\n <line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"></line>\n </svg>\n </span>\n <input\n #searchInput\n type=\"text\"\n class=\"combobox-search-input\"\n placeholder=\"Type to filter...\"\n [value]=\"searchTerm()\"\n [attr.aria-activedescendant]=\"focusedIndex() >= 0 ? (elementId() + '_opt_' + focusedIndex()) : null\"\n (input)=\"onSearchInput($event)\"\n (keydown)=\"onSearchKeydown($event)\"\n />\n @if (searchTerm()) {\n <button\n type=\"button\"\n class=\"combobox-search-clear\"\n title=\"Clear filter\"\n (click)=\"clearSearch($event)\"\n >\n \u2715\n </button>\n }\n </div>\n\n <!-- Filtered Options List -->\n <div class=\"combobox-options-list\" #optionsListEl>\n @if (waitingFor().length > 0) {\n <div class=\"combobox-status-row waiting\">\n <span>Waiting for {{ waitingLabel() }}...</span>\n </div>\n } @else if (isLoading()) {\n <div class=\"combobox-status-row loading\">\n <span class=\"status-dot dot-pulse\"></span>\n <span>Executing query on database (may take 1\u20132 minutes on local database)...</span>\n </div>\n } @else if (loadError()) {\n <div class=\"combobox-status-row error\">\n <span>Error: {{ loadError() }}</span>\n <button type=\"button\" class=\"combobox-retry-btn\" (click)=\"retryLookup($event)\">\u21BB Retry Query</button>\n </div>\n } @else if (isDynamicSource() && hasLoaded() && allOptions().length === 0) {\n <div class=\"combobox-status-row empty\">\n <span>No records found (check parameters)</span>\n </div>\n } @else if (filteredOptions().length === 0) {\n <div class=\"combobox-status-row empty\">\n <span>No options matching \"{{ searchTerm() }}\"</span>\n </div>\n } @else {\n @for (opt of visibleFilteredOptions(); track opt.key; let i = $index) {\n <div\n role=\"option\"\n [id]=\"elementId() + '_opt_' + i\"\n class=\"combobox-option-item\"\n [class.is-selected]=\"isSelected(opt)\"\n [class.is-focused]=\"focusedIndex() === i\"\n [attr.aria-selected]=\"isSelected(opt)\"\n (click)=\"selectOption(opt)\"\n (mouseenter)=\"focusedIndex.set(i)\"\n >\n <span class=\"opt-label\">{{ opt.label }}</span>\n @if (isSelected(opt)) {\n <span class=\"opt-check\" aria-hidden=\"true\">\u2713</span>\n }\n </div>\n }\n @if (excessFilteredCount() > 0) {\n <div class=\"combobox-status-row hint\">\n <span>Showing {{ MAX_FILTER_RENDER }} of {{ filteredOptions().length }} matches. Type to refine.</span>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n } @else {\n <!-- Native Browser Select Fallback -->\n <div class=\"rpt-select-wrapper\">\n <select\n [id]=\"elementId()\"\n [formControl]=\"formControl()\"\n [class.rpt-invalid]=\"isInvalid()\"\n class=\"rpt-select\"\n >\n @if (waitingFor().length > 0) {\n <option value=\"\" disabled selected>Waiting for {{ waitingLabel() }}...</option>\n } @else if (isLoading()) {\n <option value=\"\" disabled selected>Loading options from database...</option>\n } @else if (loadError()) {\n <option value=\"\" disabled selected>Error: {{ loadError() }}</option>\n } @else if (isDynamicSource() && hasLoaded() && allOptions().length === 0) {\n <option value=\"\" disabled selected>-- No records found (check parameters) --</option>\n } @else if (placeholder()) {\n <option value=\"\" [disabled]=\"config().validation?.required\">{{ placeholder() }}</option>\n }\n @for (opt of renderedOptions(); track opt.key) {\n <option [value]=\"opt.key\">{{ opt.label }}</option>\n }\n @if (hasExcessOptions()) {\n <option value=\"\" disabled>... and {{ excessOptionsCount() }} more records (refine filter) ...</option>\n }\n </select>\n <div class=\"rpt-select-arrow\" aria-hidden=\"true\">\n <svg width=\"12\" height=\"8\" viewBox=\"0 0 12 8\" fill=\"none\">\n <path d=\"M1 1.5L6 6.5L11 1.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </div>\n </div>\n }\n\n @if (showStatus() && isDynamicSource() && statusText()) {\n <div class=\"rpt-status-badge\" [attr.data-status]=\"statusKind()\">\n @switch (statusKind()) {\n @case ('loading') {\n <span class=\"status-dot dot-pulse\"></span>\n <span>Loading from database\u2026</span>\n }\n @case ('ok') {\n <span class=\"status-dot dot-ok\"></span>\n <span>{{ dynamicOptions().length }} records loaded</span>\n }\n @case ('waiting') {\n <span class=\"status-dot dot-warn\"></span>\n <span>Waiting for:</span>\n <div class=\"waiting-tokens\">\n @for (k of waitingFor(); track k) {\n <span class=\"waiting-token\">{{ k }}</span>\n }\n </div>\n }\n @case ('error') {\n <span class=\"status-dot dot-err\"></span>\n <span>{{ loadError() }}</span>\n <button type=\"button\" class=\"rpt-badge-retry-btn\" (click)=\"retryLookup($event)\">\u21BB Retry</button>\n }\n @case ('empty') {\n <span class=\"status-dot\"></span>\n <span>No records found for parameters</span>\n }\n @default {\n <span class=\"status-dot\"></span>\n }\n }\n </div>\n }\n\n @if (errorMessage()) {\n <span class=\"rpt-error-text\">{{ errorMessage() }}</span>\n }\n </div>", styles: [":host{display:block;width:100%;max-width:100%;min-width:0;box-sizing:border-box;font-family:var(--rpt-font-family, inherit);position:relative}.rpt-status-badge{display:flex;flex-wrap:wrap;align-items:center;gap:4px;font-size:.6875rem;padding:3px 6px;border-radius:4px;margin-top:4px;font-weight:500;line-height:1.3;max-width:100%;box-sizing:border-box;overflow:hidden;word-break:break-word}.rpt-status-badge>span{overflow-wrap:anywhere;word-break:break-word}.rpt-status-badge[data-status=loading]{background:#eff6ff;color:#2563eb;border:1px solid #dbeafe}.rpt-status-badge[data-status=ok]{background:#f0fdf4;color:#16a34a;border:1px solid #dcfce7}.rpt-status-badge[data-status=waiting]{background:#fffbeb;color:#d97706;border:1px solid #fef3c7}.rpt-status-badge[data-status=error]{background:#fef2f2;color:#dc2626;border:1px solid #fee2e2}.rpt-status-badge[data-status=empty]{background:#f8fafc;color:#64748b;border:1px solid #e2e8f0}.waiting-tokens{display:inline-flex;flex-wrap:wrap;align-items:center;gap:3px;margin-left:2px;max-width:100%}.waiting-token{background:#b453091f;border:1px solid rgba(180,83,9,.3);padding:1px 5px;border-radius:3px;font-weight:700;font-size:.625rem;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block;box-sizing:border-box}.status-dot{width:6px;height:6px;border-radius:50%;background:currentColor}.dot-pulse{animation:rpt-pulse 1s infinite alternate}@keyframes rpt-pulse{0%{opacity:.3}to{opacity:1}}.rpt-field-group{display:flex;flex-direction:column;max-width:100%;min-width:0;gap:4px;box-sizing:border-box;position:relative}.rpt-label{font-size:var(--rpt-font-size-sm, .75rem);font-weight:500;color:var(--rpt-color-text, #0f172a);flex-shrink:0}.rpt-required-marker{color:var(--rpt-color-danger, #ef4444);margin-left:2px}.rpt-select-wrapper{position:relative;width:100%;flex:1;min-height:0;display:flex;align-items:center}.rpt-select{width:100%;height:100%;min-height:38px;box-sizing:border-box;padding:6px 36px 6px 12px;font-size:var(--rpt-font-size-base, .875rem);color:var(--rpt-color-text, #0f172a);background-color:var(--rpt-color-bg, #ffffff);border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius, 6px);appearance:none;-webkit-appearance:none;outline:none;cursor:pointer;transition:border-color var(--rpt-transition-speed, .15s) ease,box-shadow var(--rpt-transition-speed, .15s) ease}.rpt-select:hover:not(:disabled){border-color:var(--rpt-color-border-hover, #94a3b8)}.rpt-select:focus{border-color:var(--rpt-color-border-focus, #2563eb);box-shadow:var(--rpt-box-shadow-focus, 0 0 0 3px rgba(37, 99, 235, .25))}.rpt-select:disabled{background-color:var(--rpt-color-surface, #f8fafc);color:var(--rpt-color-text-muted, #64748b);cursor:not-allowed}.rpt-select.rpt-invalid{border-color:var(--rpt-color-danger, #ef4444)}.rpt-select.rpt-invalid:focus{box-shadow:0 0 0 3px #ef444440}.rpt-select-arrow{position:absolute;right:12px;top:50%;transform:translateY(-50%);pointer-events:none;color:var(--rpt-color-text-muted, #64748b);display:flex;align-items:center}.rpt-combobox-wrapper{position:relative;width:100%;flex:1;min-height:0}.rpt-combobox-trigger{width:100%;height:100%;min-height:38px;box-sizing:border-box;padding:6px 10px 6px 12px;display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:var(--rpt-font-size-base, .875rem);font-family:inherit;color:var(--rpt-color-text, #0f172a);background-color:var(--rpt-color-bg, #ffffff);border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius, 6px);outline:none;cursor:pointer;text-align:left;transition:border-color var(--rpt-transition-speed, .15s) ease,box-shadow var(--rpt-transition-speed, .15s) ease}.rpt-combobox-trigger:hover:not(:disabled){border-color:var(--rpt-color-border-hover, #94a3b8)}.rpt-combobox-trigger:focus-visible,.rpt-combobox-trigger.is-open{border-color:var(--rpt-color-border-focus, #2563eb);box-shadow:var(--rpt-box-shadow-focus, 0 0 0 3px rgba(37, 99, 235, .25))}.rpt-combobox-trigger:disabled{background-color:var(--rpt-color-surface, #f8fafc);color:var(--rpt-color-text-muted, #64748b);cursor:not-allowed}.rpt-combobox-trigger.rpt-invalid{border-color:var(--rpt-color-danger, #ef4444)}.rpt-combobox-trigger.rpt-invalid:focus-visible{box-shadow:0 0 0 3px #ef444440}.trigger-label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.3}.trigger-label.is-placeholder{color:var(--rpt-color-text-muted, #94a3b8)}.trigger-actions{display:flex;align-items:center;gap:6px;flex-shrink:0}.trigger-clear-btn{display:flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;font-size:.6875rem;color:var(--rpt-color-text-muted, #64748b);background-color:#f1f5f9;cursor:pointer;-webkit-user-select:none;user-select:none;transition:background-color .15s ease,color .15s ease}.trigger-clear-btn:hover{background-color:#e2e8f0;color:var(--rpt-color-text, #0f172a)}.trigger-arrow{display:flex;align-items:center;color:var(--rpt-color-text-muted, #64748b);transition:transform .18s ease}.trigger-arrow.rotated{transform:rotate(180deg)}.rpt-combobox-menu{position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:1050;background-color:var(--rpt-color-bg, #ffffff);border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius, 6px);box-shadow:0 10px 25px -5px #0000001f,0 8px 10px -6px #00000014;overflow:hidden;display:flex;flex-direction:column;animation:rpt-dropdown-pop .12s cubic-bezier(0,0,.2,1)}.rpt-combobox-menu.opens-above{top:auto;bottom:calc(100% + 4px);box-shadow:0 -10px 25px -5px #0000001f,0 -8px 10px -6px #00000014;animation:rpt-dropdown-pop-above .12s cubic-bezier(0,0,.2,1)}@keyframes rpt-dropdown-pop{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}@keyframes rpt-dropdown-pop-above{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.combobox-search-box{display:flex;align-items:center;gap:8px;padding:8px 10px;border-bottom:1px solid var(--rpt-color-border, #e2e8f0);background-color:#f8fafc}.combobox-search-icon{color:var(--rpt-color-text-muted, #94a3b8);display:flex;align-items:center;flex-shrink:0}.combobox-search-input{flex:1;border:none;outline:none;background:transparent;font-size:var(--rpt-font-size-sm, .8125rem);font-family:inherit;color:var(--rpt-color-text, #0f172a);min-width:0}.combobox-search-input::placeholder{color:#94a3b8}.combobox-search-clear{border:none;background:#e2e8f0;color:#64748b;width:16px;height:16px;border-radius:50%;font-size:.625rem;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0}.combobox-search-clear:hover{background:#cbd5e1;color:#0f172a}.combobox-options-list{max-height:220px;overflow-y:auto;overscroll-behavior:contain;padding:4px}.combobox-option-item{display:flex;align-items:center;justify-content:space-between;padding:7px 10px;border-radius:4px;font-size:var(--rpt-font-size-base, .84rem);color:var(--rpt-color-text, #0f172a);cursor:pointer;-webkit-user-select:none;user-select:none;transition:background-color .1s ease}.combobox-option-item.is-focused{background-color:var(--rpt-color-surface-hover, #f1f5f9)}.combobox-option-item.is-selected{background-color:#eff6ff;color:var(--rpt-color-primary, #2563eb);font-weight:600}.opt-label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.opt-check{font-size:.875rem;font-weight:700;color:var(--rpt-color-primary, #2563eb);margin-left:8px}.combobox-status-row{padding:10px 12px;font-size:.78rem;text-align:center;color:var(--rpt-color-text-muted, #64748b)}.combobox-status-row.loading{display:flex;align-items:center;justify-content:center;gap:6px;color:#2563eb}.combobox-status-row.error{display:flex;flex-direction:column;align-items:center;gap:6px;color:var(--rpt-color-danger, #dc2626)}.trigger-retry-btn,.combobox-retry-btn,.rpt-badge-retry-btn{display:inline-flex;align-items:center;gap:3px;font-size:.6875rem;font-weight:600;color:#dc2626;background:#fee2e2;border:1px solid #fca5a5;border-radius:4px;padding:2px 7px;cursor:pointer;line-height:1.2;transition:background-color .12s ease,color .12s ease}.trigger-retry-btn:hover,.combobox-retry-btn:hover,.rpt-badge-retry-btn:hover{background:#fecaca;color:#b91c1c;border-color:#f87171}.combobox-status-row.hint{font-size:.72rem;color:#94a3b8;border-top:1px dashed #e2e8f0;margin-top:4px;padding:6px}.rpt-error-text{font-size:.7rem;color:var(--rpt-color-danger, #ef4444);margin-top:3px;line-height:1.25;max-width:100%;overflow-wrap:anywhere;word-break:break-word;display:block}\n"] }]
|
|
2918
3087
|
}], ctorParameters: () => [], propDecorators: { onDocumentClick: [{
|
|
2919
3088
|
type: HostListener,
|
|
2920
3089
|
args: ['document:click', ['$event']]
|
|
@@ -5598,6 +5767,9 @@ class RptGridOutletDirective {
|
|
|
5598
5767
|
vcr = inject(ViewContainerRef);
|
|
5599
5768
|
compRef = null;
|
|
5600
5769
|
subs = [];
|
|
5770
|
+
lastEmittedReportState = null;
|
|
5771
|
+
lastEmittedColumnDefs = null;
|
|
5772
|
+
appliedInputs = new Map();
|
|
5601
5773
|
componentType = input(null, { alias: 'rptGridOutlet' });
|
|
5602
5774
|
inputs = input({});
|
|
5603
5775
|
gridStateChange = output();
|
|
@@ -5611,16 +5783,89 @@ class RptGridOutletDirective {
|
|
|
5611
5783
|
});
|
|
5612
5784
|
effect(() => {
|
|
5613
5785
|
const currentInputs = this.inputs();
|
|
5614
|
-
if (this.compRef)
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5786
|
+
if (!this.compRef)
|
|
5787
|
+
return;
|
|
5788
|
+
for (const [key, value] of Object.entries(currentInputs)) {
|
|
5789
|
+
if (key === 'reportState') {
|
|
5790
|
+
// Suppress circular input echo: if incoming reportState matches what this grid instance
|
|
5791
|
+
// just emitted, skip setInput. Re-applying reportState causes ngOnChanges -> applyImportedFilters,
|
|
5792
|
+
// which destroys columns and dismisses active filter popovers/inputs.
|
|
5793
|
+
if (this.lastEmittedReportState && this.isReportStateMatching(this.lastEmittedReportState, value)) {
|
|
5794
|
+
continue;
|
|
5795
|
+
}
|
|
5796
|
+
if (this.appliedInputs.has('reportState') && this.isReportStateMatching(this.appliedInputs.get('reportState'), value)) {
|
|
5797
|
+
continue;
|
|
5798
|
+
}
|
|
5799
|
+
}
|
|
5800
|
+
if (key === 'columns' || key === 'columnDefs') {
|
|
5801
|
+
if (this.lastEmittedColumnDefs && this.isColumnDefsMatching(this.lastEmittedColumnDefs, value)) {
|
|
5802
|
+
continue;
|
|
5803
|
+
}
|
|
5804
|
+
if (this.appliedInputs.has(key) && this.isColumnDefsMatching(this.appliedInputs.get(key), value)) {
|
|
5805
|
+
continue;
|
|
5806
|
+
}
|
|
5807
|
+
}
|
|
5808
|
+
if (key === 'config') {
|
|
5809
|
+
const prev = this.appliedInputs.get('config');
|
|
5810
|
+
if (prev && JSON.stringify(prev) === JSON.stringify(value)) {
|
|
5811
|
+
continue;
|
|
5812
|
+
}
|
|
5813
|
+
}
|
|
5814
|
+
if (key === 'rowData') {
|
|
5815
|
+
const prev = this.appliedInputs.get('rowData');
|
|
5816
|
+
if (prev === value) {
|
|
5817
|
+
continue;
|
|
5618
5818
|
}
|
|
5619
|
-
catch { }
|
|
5620
5819
|
}
|
|
5820
|
+
if (this.appliedInputs.get(key) === value) {
|
|
5821
|
+
continue;
|
|
5822
|
+
}
|
|
5823
|
+
try {
|
|
5824
|
+
this.compRef.setInput(key, value);
|
|
5825
|
+
this.appliedInputs.set(key, value);
|
|
5826
|
+
}
|
|
5827
|
+
catch { }
|
|
5621
5828
|
}
|
|
5622
5829
|
});
|
|
5623
5830
|
}
|
|
5831
|
+
isReportStateMatching(a, b) {
|
|
5832
|
+
if (a === b)
|
|
5833
|
+
return true;
|
|
5834
|
+
if (!a || !b)
|
|
5835
|
+
return a === b;
|
|
5836
|
+
try {
|
|
5837
|
+
const getComparable = (s) => ({
|
|
5838
|
+
activeFilters: s.activeFilters,
|
|
5839
|
+
conditionFilters: s.conditionFilters,
|
|
5840
|
+
sortModel: s.sortModel,
|
|
5841
|
+
groupModel: s.groupModel,
|
|
5842
|
+
});
|
|
5843
|
+
return JSON.stringify(getComparable(a)) === JSON.stringify(getComparable(b));
|
|
5844
|
+
}
|
|
5845
|
+
catch {
|
|
5846
|
+
return false;
|
|
5847
|
+
}
|
|
5848
|
+
}
|
|
5849
|
+
isColumnDefsMatching(a, b) {
|
|
5850
|
+
if (a === b)
|
|
5851
|
+
return true;
|
|
5852
|
+
if (!Array.isArray(a) || !Array.isArray(b))
|
|
5853
|
+
return false;
|
|
5854
|
+
if (a.length !== b.length)
|
|
5855
|
+
return false;
|
|
5856
|
+
try {
|
|
5857
|
+
const getComparable = (cols) => cols.map((c) => ({
|
|
5858
|
+
field: c.field,
|
|
5859
|
+
width: c.width,
|
|
5860
|
+
hide: c.hide,
|
|
5861
|
+
pinned: c.pinned,
|
|
5862
|
+
}));
|
|
5863
|
+
return JSON.stringify(getComparable(a)) === JSON.stringify(getComparable(b));
|
|
5864
|
+
}
|
|
5865
|
+
catch {
|
|
5866
|
+
return false;
|
|
5867
|
+
}
|
|
5868
|
+
}
|
|
5624
5869
|
recreateComponent(type) {
|
|
5625
5870
|
this.clear();
|
|
5626
5871
|
if (!type)
|
|
@@ -5629,13 +5874,22 @@ class RptGridOutletDirective {
|
|
|
5629
5874
|
this.compRef = this.vcr.createComponent(type);
|
|
5630
5875
|
const instance = this.compRef.instance;
|
|
5631
5876
|
if (instance.gridStateChanged && typeof instance.gridStateChanged.subscribe === 'function') {
|
|
5632
|
-
this.subs.push(instance.gridStateChanged.subscribe((state) =>
|
|
5877
|
+
this.subs.push(instance.gridStateChanged.subscribe((state) => {
|
|
5878
|
+
this.lastEmittedReportState = state;
|
|
5879
|
+
this.gridStateChange.emit(state);
|
|
5880
|
+
}));
|
|
5633
5881
|
}
|
|
5634
5882
|
if (instance.reportStateChange && typeof instance.reportStateChange.subscribe === 'function') {
|
|
5635
|
-
this.subs.push(instance.reportStateChange.subscribe((state) =>
|
|
5883
|
+
this.subs.push(instance.reportStateChange.subscribe((state) => {
|
|
5884
|
+
this.lastEmittedReportState = state;
|
|
5885
|
+
this.gridStateChange.emit(state);
|
|
5886
|
+
}));
|
|
5636
5887
|
}
|
|
5637
5888
|
if (instance.columnDefsChanged && typeof instance.columnDefsChanged.subscribe === 'function') {
|
|
5638
|
-
this.subs.push(instance.columnDefsChanged.subscribe((defs) =>
|
|
5889
|
+
this.subs.push(instance.columnDefsChanged.subscribe((defs) => {
|
|
5890
|
+
this.lastEmittedColumnDefs = defs;
|
|
5891
|
+
this.columnDefsChange.emit(defs);
|
|
5892
|
+
}));
|
|
5639
5893
|
}
|
|
5640
5894
|
const emitRow = (data) => {
|
|
5641
5895
|
const row = data?.data ?? data?.row ?? data;
|
|
@@ -5664,6 +5918,7 @@ class RptGridOutletDirective {
|
|
|
5664
5918
|
for (const [key, value] of Object.entries(currentInputs)) {
|
|
5665
5919
|
try {
|
|
5666
5920
|
this.compRef.setInput(key, value);
|
|
5921
|
+
this.appliedInputs.set(key, value);
|
|
5667
5922
|
}
|
|
5668
5923
|
catch { }
|
|
5669
5924
|
}
|
|
@@ -5675,6 +5930,9 @@ class RptGridOutletDirective {
|
|
|
5675
5930
|
clear() {
|
|
5676
5931
|
this.subs.forEach((s) => s.unsubscribe());
|
|
5677
5932
|
this.subs = [];
|
|
5933
|
+
this.lastEmittedReportState = null;
|
|
5934
|
+
this.lastEmittedColumnDefs = null;
|
|
5935
|
+
this.appliedInputs.clear();
|
|
5678
5936
|
if (this.compRef) {
|
|
5679
5937
|
this.compRef.destroy();
|
|
5680
5938
|
this.compRef = null;
|
|
@@ -5835,13 +6093,16 @@ class ReportGridBridgeComponent {
|
|
|
5835
6093
|
});
|
|
5836
6094
|
gridReportState = computed(() => {
|
|
5837
6095
|
const opts = this.gridDefinition()?.options;
|
|
6096
|
+
const existing = opts?.reportState || {};
|
|
5838
6097
|
return {
|
|
5839
|
-
columnGroupDefs: null,
|
|
6098
|
+
columnGroupDefs: existing.columnGroupDefs ?? null,
|
|
5840
6099
|
columnDefs: this.bridgedColumns(),
|
|
5841
|
-
sortModel: opts?.sortModel || [],
|
|
5842
|
-
groupModel: opts?.groupModel || [],
|
|
5843
|
-
activeFilters: {},
|
|
5844
|
-
|
|
6100
|
+
sortModel: opts?.sortModel || existing.sortModel || [],
|
|
6101
|
+
groupModel: opts?.groupModel || existing.groupModel || [],
|
|
6102
|
+
activeFilters: existing.activeFilters || {},
|
|
6103
|
+
conditionFilters: existing.conditionFilters || {},
|
|
6104
|
+
highlightRules: existing.highlightRules || [],
|
|
6105
|
+
...existing,
|
|
5845
6106
|
};
|
|
5846
6107
|
});
|
|
5847
6108
|
gridInputs = computed(() => ({
|
|
@@ -8282,12 +8543,72 @@ class DesignerStateService {
|
|
|
8282
8543
|
/**
|
|
8283
8544
|
* Updates grid definition settings.
|
|
8284
8545
|
*/
|
|
8285
|
-
updateGridSettings(grid) {
|
|
8546
|
+
updateGridSettings(grid, options) {
|
|
8547
|
+
if (options?.skipHistory) {
|
|
8548
|
+
this.definition.update((def) => ({
|
|
8549
|
+
...def,
|
|
8550
|
+
grid,
|
|
8551
|
+
}));
|
|
8552
|
+
this.isDirty.set(true);
|
|
8553
|
+
return;
|
|
8554
|
+
}
|
|
8286
8555
|
this.mutateDefinition((def) => ({
|
|
8287
8556
|
...def,
|
|
8288
8557
|
grid,
|
|
8289
8558
|
}));
|
|
8290
8559
|
}
|
|
8560
|
+
/**
|
|
8561
|
+
* Sets or updates the primary data source for the report's result grid.
|
|
8562
|
+
*/
|
|
8563
|
+
setGridDataSource(ds) {
|
|
8564
|
+
this.mutateDefinition((def) => {
|
|
8565
|
+
const existingSources = def.dataSources ?? [];
|
|
8566
|
+
const idx = existingSources.findIndex((s) => s.id === ds.id);
|
|
8567
|
+
let nextSources;
|
|
8568
|
+
if (idx >= 0) {
|
|
8569
|
+
nextSources = [...existingSources];
|
|
8570
|
+
nextSources[idx] = ds;
|
|
8571
|
+
}
|
|
8572
|
+
else {
|
|
8573
|
+
nextSources = [ds, ...existingSources];
|
|
8574
|
+
}
|
|
8575
|
+
const updatedGrid = {
|
|
8576
|
+
...(def.grid ?? {
|
|
8577
|
+
dataSourceId: ds.id,
|
|
8578
|
+
columns: [],
|
|
8579
|
+
options: { pagination: true, pageSize: 25, showRowNumbers: true, showFooter: true },
|
|
8580
|
+
}),
|
|
8581
|
+
dataSourceId: ds.id,
|
|
8582
|
+
};
|
|
8583
|
+
return {
|
|
8584
|
+
...def,
|
|
8585
|
+
dataSources: nextSources,
|
|
8586
|
+
grid: updatedGrid,
|
|
8587
|
+
};
|
|
8588
|
+
});
|
|
8589
|
+
}
|
|
8590
|
+
/**
|
|
8591
|
+
* Updates the parameter mappings for the primary grid data source.
|
|
8592
|
+
*/
|
|
8593
|
+
updateGridDataSourceParameters(mappings) {
|
|
8594
|
+
this.mutateDefinition((def) => {
|
|
8595
|
+
const gridDsId = def.grid?.dataSourceId;
|
|
8596
|
+
const sources = def.dataSources ?? [];
|
|
8597
|
+
const targetIdx = sources.findIndex((s) => s.id === gridDsId || s.name === gridDsId);
|
|
8598
|
+
if (targetIdx >= 0) {
|
|
8599
|
+
const nextSources = [...sources];
|
|
8600
|
+
nextSources[targetIdx] = {
|
|
8601
|
+
...nextSources[targetIdx],
|
|
8602
|
+
parameterMappings: mappings,
|
|
8603
|
+
};
|
|
8604
|
+
return {
|
|
8605
|
+
...def,
|
|
8606
|
+
dataSources: nextSources,
|
|
8607
|
+
};
|
|
8608
|
+
}
|
|
8609
|
+
return def;
|
|
8610
|
+
});
|
|
8611
|
+
}
|
|
8291
8612
|
/**
|
|
8292
8613
|
* Reverts to the previous state in history.
|
|
8293
8614
|
*/
|
|
@@ -8988,6 +9309,8 @@ function matchesSearchQuery(item, rawQuery) {
|
|
|
8988
9309
|
|
|
8989
9310
|
class DesignCanvasComponent {
|
|
8990
9311
|
designerState = inject(DesignerStateService);
|
|
9312
|
+
gridFilterDebounceTimer = null;
|
|
9313
|
+
pendingGridState = null;
|
|
8991
9314
|
templateService = inject(ReportTemplateService);
|
|
8992
9315
|
formBuilder = inject(DynamicFormBuilder);
|
|
8993
9316
|
executionService = inject(ReportExecutionService);
|
|
@@ -9309,6 +9632,12 @@ class DesignCanvasComponent {
|
|
|
9309
9632
|
setTimeout(() => this.gridExecError.set(null), 4000);
|
|
9310
9633
|
}
|
|
9311
9634
|
}
|
|
9635
|
+
ngOnDestroy() {
|
|
9636
|
+
if (this.gridFilterDebounceTimer) {
|
|
9637
|
+
clearTimeout(this.gridFilterDebounceTimer);
|
|
9638
|
+
this.gridFilterDebounceTimer = null;
|
|
9639
|
+
}
|
|
9640
|
+
}
|
|
9312
9641
|
onCanvasGridStateChange(state) {
|
|
9313
9642
|
if (!state || !this.definition().grid)
|
|
9314
9643
|
return;
|
|
@@ -9318,34 +9647,84 @@ class DesignCanvasComponent {
|
|
|
9318
9647
|
const newSerialized = JSON.stringify(state ?? null);
|
|
9319
9648
|
if (currentSerialized === newSerialized)
|
|
9320
9649
|
return;
|
|
9650
|
+
// Distinguish between true structural layout changes (groupModel, sortModel)
|
|
9651
|
+
// vs interactive column data filtering (activeFilters, conditionFilters).
|
|
9652
|
+
const currentGroup = JSON.stringify(currentGrid.options?.groupModel ?? []);
|
|
9653
|
+
const newGroup = JSON.stringify(state.groupModel ?? []);
|
|
9654
|
+
const currentSort = JSON.stringify(currentGrid.options?.sortModel ?? []);
|
|
9655
|
+
const newSort = JSON.stringify(state.sortModel ?? []);
|
|
9656
|
+
const isLayoutChange = currentGroup !== newGroup || currentSort !== newSort;
|
|
9321
9657
|
const updatedOptions = {
|
|
9322
9658
|
...(currentGrid.options || {}),
|
|
9323
9659
|
groupModel: state.groupModel ?? currentGrid.options?.groupModel,
|
|
9324
9660
|
sortModel: state.sortModel ?? currentGrid.options?.sortModel,
|
|
9325
9661
|
reportState: state,
|
|
9326
9662
|
};
|
|
9327
|
-
|
|
9328
|
-
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
|
|
9663
|
+
if (isLayoutChange) {
|
|
9664
|
+
// Clear any pending debounced filter update
|
|
9665
|
+
if (this.gridFilterDebounceTimer) {
|
|
9666
|
+
clearTimeout(this.gridFilterDebounceTimer);
|
|
9667
|
+
this.gridFilterDebounceTimer = null;
|
|
9668
|
+
}
|
|
9669
|
+
this.pendingGridState = null;
|
|
9670
|
+
// Grouping or sorting changed — update layout immediately and show visual indicator
|
|
9671
|
+
this.designerState.updateGridSettings({
|
|
9672
|
+
...currentGrid,
|
|
9673
|
+
options: updatedOptions,
|
|
9674
|
+
});
|
|
9675
|
+
this.layoutJustUpdated.set(true);
|
|
9676
|
+
setTimeout(() => this.layoutJustUpdated.set(false), 1200);
|
|
9677
|
+
}
|
|
9678
|
+
else {
|
|
9679
|
+
// Pure column filter adjustment (activeFilters / conditionFilters).
|
|
9680
|
+
// 1. Do NOT trigger the "Layout auto-persisted" indicator (filtering rows is not a layout change).
|
|
9681
|
+
// 2. Debounce saving the state with skipHistory so typing or checking boxes
|
|
9682
|
+
// does not flood undo history or interrupt column filter interaction.
|
|
9683
|
+
this.pendingGridState = state;
|
|
9684
|
+
if (this.gridFilterDebounceTimer) {
|
|
9685
|
+
clearTimeout(this.gridFilterDebounceTimer);
|
|
9686
|
+
}
|
|
9687
|
+
this.gridFilterDebounceTimer = setTimeout(() => {
|
|
9688
|
+
this.gridFilterDebounceTimer = null;
|
|
9689
|
+
if (!this.pendingGridState || !this.definition().grid)
|
|
9690
|
+
return;
|
|
9691
|
+
const latestGrid = this.definition().grid;
|
|
9692
|
+
const latestOptions = {
|
|
9693
|
+
...(latestGrid.options || {}),
|
|
9694
|
+
reportState: this.pendingGridState,
|
|
9695
|
+
};
|
|
9696
|
+
this.pendingGridState = null;
|
|
9697
|
+
this.designerState.updateGridSettings({
|
|
9698
|
+
...latestGrid,
|
|
9699
|
+
options: latestOptions,
|
|
9700
|
+
}, { skipHistory: true });
|
|
9701
|
+
}, 1200);
|
|
9702
|
+
}
|
|
9333
9703
|
}
|
|
9334
9704
|
onCanvasColumnDefsChange(defs) {
|
|
9335
9705
|
if (!Array.isArray(defs) || !this.definition().grid)
|
|
9336
9706
|
return;
|
|
9337
9707
|
const currentGrid = this.definition().grid;
|
|
9708
|
+
let anyChanged = false;
|
|
9338
9709
|
const updatedCols = currentGrid.columns.map((c) => {
|
|
9339
9710
|
const match = defs.find((d) => d.field === c.field);
|
|
9340
9711
|
if (match) {
|
|
9712
|
+
const newWidth = match.width ? `${match.width}px` : c.width;
|
|
9713
|
+
const newVisible = match.hide !== true;
|
|
9714
|
+
if (newWidth !== c.width || newVisible !== c.visible) {
|
|
9715
|
+
anyChanged = true;
|
|
9716
|
+
}
|
|
9341
9717
|
return {
|
|
9342
9718
|
...c,
|
|
9343
|
-
width:
|
|
9344
|
-
visible:
|
|
9719
|
+
width: newWidth,
|
|
9720
|
+
visible: newVisible,
|
|
9345
9721
|
};
|
|
9346
9722
|
}
|
|
9347
9723
|
return c;
|
|
9348
9724
|
});
|
|
9725
|
+
// If no column widths or visibilities actually changed, do not trigger auto-persist
|
|
9726
|
+
if (!anyChanged)
|
|
9727
|
+
return;
|
|
9349
9728
|
this.designerState.updateGridSettings({
|
|
9350
9729
|
...currentGrid,
|
|
9351
9730
|
columns: updatedCols,
|
|
@@ -9365,7 +9744,7 @@ class DesignCanvasComponent {
|
|
|
9365
9744
|
this.designerState.addComponent(type);
|
|
9366
9745
|
}
|
|
9367
9746
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DesignCanvasComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
9368
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: DesignCanvasComponent, isStandalone: true, selector: "rpt-design-canvas", host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: " <div class=\"canvas-wrapper\" (click)=\"onCanvasBackgroundClick($event)\">\n <!-- Canvas Status / Quick Controls -->\n <div class=\"canvas-toolbar\">\n <div class=\"canvas-info\">\n <span class=\"info-label\">Grid Layout:</span>\n <span class=\"info-value\">{{ totalColumns() }} Columns</span>\n <span class=\"info-divider\">\u2022</span>\n <span class=\"info-label\">Components:</span>\n <span class=\"info-value\">{{ components().length }}</span>\n <span class=\"info-divider\">\u2022</span>\n <span class=\"live-mode-badge\" title=\"Interactive canvas: Values typed here run live queries and test dropdowns.\">\n \u26A1 Live Interactive Mode\n </span>\n </div>\n\n <div class=\"canvas-actions\">\n @if (!definition().grid) {\n <button\n type=\"button\"\n class=\"canvas-action-btn btn-grid-add\"\n (click)=\"addResultGrid($event)\"\n title=\"Add a Result Grid to this report\"\n >\n <rpt-icon name=\"table\" [size]=\"14\" />\n <span>+ Add Grid</span>\n </button>\n } @else {\n <button\n type=\"button\"\n class=\"canvas-action-btn btn-grid-configured\"\n (click)=\"selectGridSettings($event)\"\n title=\"Configure Result Data Grid\"\n >\n <rpt-icon name=\"table\" [size]=\"14\" />\n <span>Grid ({{ definition().grid?.columns?.length ?? 0 }})</span>\n </button>\n }\n\n <button\n type=\"button\"\n class=\"canvas-action-btn\"\n (click)=\"selectReportSettings($event)\"\n title=\"Configure Page Layout & Grid Settings\"\n >\n <rpt-icon name=\"settings\" [size]=\"14\" />\n <span>Report Settings</span>\n </button>\n </div>\n </div>\n\n <!-- Canvas Drop Area -->\n <div class=\"canvas-scroll-area\">\n <div\n class=\"canvas-artboard\"\n [style.padding.px]=\"definition().layout.padding ?? 24\"\n >\n <!-- Report Header Preview -->\n <div class=\"canvas-report-header\">\n <h2 class=\"canvas-report-title\">{{ definition().name || 'Untitled Report' }}</h2>\n @if (definition().description) {\n <p class=\"canvas-report-desc\">{{ definition().description }}</p>\n }\n </div>\n\n <!-- Component Drop List Grid with 2D mixed orientation and data binding -->\n <div\n cdkDropList\n id=\"designer-canvas-drop-list\"\n class=\"canvas-grid\"\n cdkDropListOrientation=\"mixed\"\n [cdkDropListData]=\"sortedComponents()\"\n [style.grid-template-columns]=\"gridTemplateColumns()\"\n [style.gap.px]=\"definition().layout.gap ?? 16\"\n (cdkDropListDropped)=\"onDrop($event)\"\n >\n @for (comp of sortedComponents(); track comp.id; let idx = $index) {\n <div\n cdkDrag\n [cdkDragData]=\"comp\"\n [cdkDragDisabled]=\"isAnyResizing()\"\n class=\"canvas-component-card\"\n [class.is-selected]=\"isSelected(comp.id)\"\n [class.is-resizing]=\"resizingCompId() === comp.id\"\n [class.is-field-highlighted]=\"isFieldHighlighted(comp)\"\n [class.is-narrow]=\"getSpan(comp) <= 4\"\n [class.is-compact]=\"getSpan(comp) <= 2\"\n [style.grid-column]=\"getGridColumn(comp)\"\n [style.grid-row]=\"getGridRow(comp)\"\n [style.width]=\"comp.layout?.width || 'auto'\"\n [style.height]=\"getComponentHeight(comp)\"\n [style.min-height]=\"getComponentMinHeight(comp)\"\n (click)=\"onSelect(comp.id, $event)\"\n >\n <!-- Card Header with Drag Handle & Actions -->\n <div class=\"card-header\">\n <div class=\"card-title-group\" cdkDragHandle title=\"Drag to reorder\">\n <span class=\"drag-handle-dots\">\u22EE\u22EE</span>\n <rpt-icon [name]=\"getComponentIconName(comp.type)\" [size]=\"14\" />\n <span class=\"comp-title\" [title]=\"comp.label || comp.key || comp.type\">\n {{ comp.label || comp.key || comp.type }}\n </span>\n </div>\n\n <div class=\"card-actions\">\n <!-- Static span pill & new row / start col pill when not hovered / not selected -->\n @if (comp.layout?.column; as col) {\n <span class=\"new-row-pill-static\" title=\"Starts at Column {{ col }}\">Col {{ col }}</span>\n } @else if (comp.layout?.newRow) {\n <span class=\"new-row-pill-static\" title=\"Starts on separate row\">\u21B3 Row</span>\n }\n <span\n class=\"span-pill-static\"\n title=\"Column span: {{ getSpan(comp) }} of {{ totalColumns() }} columns\"\n >\n {{ getSpan(comp) }}/{{ totalColumns() }}\n </span>\n\n <!-- Quick Action Stepper & Buttons (revealed on hover or selection) -->\n <div class=\"card-quick-controls\">\n <button\n type=\"button\"\n class=\"card-action-btn\"\n [class.btn-active-toggle]=\"comp.layout?.newRow\"\n (click)=\"toggleNewRow(comp, $event)\"\n title=\"{{ comp.layout?.newRow ? 'Starts on new row (Click to place inline)' : 'Start on separate row (Row Break)' }}\"\n >\n <rpt-icon name=\"corner-down-right\" [size]=\"11\" />\n </button>\n\n <div class=\"span-control-group\" title=\"Adjust Column Span\">\n <button\n type=\"button\"\n class=\"span-btn\"\n [disabled]=\"getSpan(comp) <= 1\"\n (click)=\"adjustSpan(comp, -1, $event)\"\n title=\"Shrink Column Span\"\n >\n <rpt-icon name=\"minus\" [size]=\"9\" />\n </button>\n <span class=\"span-badge\" [title]=\"'Column span: ' + getSpan(comp) + ' of ' + totalColumns()\">\n <span class=\"span-num\">{{ getSpan(comp) }}</span><span class=\"span-total\">/{{ totalColumns() }}</span>\n </span>\n <button\n type=\"button\"\n class=\"span-btn\"\n [disabled]=\"getSpan(comp) >= totalColumns()\"\n (click)=\"adjustSpan(comp, 1, $event)\"\n title=\"Expand Column Span\"\n >\n <rpt-icon name=\"plus\" [size]=\"9\" />\n </button>\n </div>\n\n <button\n type=\"button\"\n class=\"card-action-btn\"\n (click)=\"onDuplicate(comp.id, $event)\"\n title=\"Duplicate component\"\n >\n <rpt-icon name=\"copy\" [size]=\"12\" />\n </button>\n <button\n type=\"button\"\n class=\"card-action-btn btn-delete\"\n (click)=\"onDelete(comp.id, $event)\"\n title=\"Remove component\"\n >\n <rpt-icon name=\"trash\" [size]=\"12\" />\n </button>\n </div>\n </div>\n </div>\n\n <!-- Card Body Visual Preview -->\n <div class=\"card-body\">\n\n @switch (comp.type) {\n @case ('input') {\n <div class=\"mock-input-field\" (click)=\"onControlInteract(comp, $event)\">\n <span class=\"mock-label\">{{ comp.label || 'Input' }}</span>\n @if (getCanvasControl(comp); as ctrl) {\n <input\n type=\"text\"\n class=\"canvas-active-input\"\n [placeholder]=\"comp.properties?.['placeholder'] || 'Enter text...'\"\n [formControl]=\"ctrl\"\n (focus)=\"onControlInteract(comp, $event)\"\n />\n } @else {\n <div class=\"mock-input-box\">{{ comp.properties?.['placeholder'] || 'Enter text...' }}</div>\n }\n </div>\n }\n @case ('dropdown') {\n <div class=\"canvas-live-control-wrapper\" (click)=\"onControlInteract(comp, $event)\">\n <rpt-dropdown\n [config]=\"comp\"\n [showStatus]=\"true\"\n [parentForm]=\"canvasForm()\"\n [control]=\"getCanvasControl(comp)\"\n />\n </div>\n }\n @case ('date') {\n <div class=\"mock-input-field\" (click)=\"onControlInteract(comp, $event)\">\n <span class=\"mock-label\">{{ comp.label || 'Date' }}</span>\n @if (getCanvasControl(comp); as ctrl) {\n <input\n type=\"date\"\n class=\"canvas-active-input\"\n [formControl]=\"ctrl\"\n (focus)=\"onControlInteract(comp, $event)\"\n />\n } @else {\n <div class=\"mock-select-box\">\n <span>YYYY-MM-DD</span>\n <rpt-icon name=\"calendar\" [size]=\"12\" />\n </div>\n }\n </div>\n }\n @case ('date-range') {\n <div class=\"canvas-live-control-wrapper\" (click)=\"onControlInteract(comp, $event)\">\n <rpt-date-range\n [config]=\"comp\"\n [control]=\"getCanvasControl(comp)\"\n />\n </div>\n }\n @case ('number') {\n <div class=\"mock-input-field\" (click)=\"onControlInteract(comp, $event)\">\n <span class=\"mock-label\">{{ comp.label || 'Number' }}</span>\n @if (getCanvasControl(comp); as ctrl) {\n <input\n type=\"number\"\n class=\"canvas-active-input\"\n [formControl]=\"ctrl\"\n (focus)=\"onControlInteract(comp, $event)\"\n />\n } @else {\n <div class=\"mock-input-box\">\n {{ comp.properties?.['prefix'] || '' }} 0.00 {{ comp.properties?.['suffix'] || '' }}\n </div>\n }\n </div>\n }\n @case ('checkbox') {\n <div class=\"mock-checkbox-field\" (click)=\"onControlInteract(comp, $event)\">\n @if (getCanvasControl(comp); as ctrl) {\n <label class=\"canvas-active-checkbox-lbl\">\n <input type=\"checkbox\" [formControl]=\"ctrl\" (focus)=\"onControlInteract(comp, $event)\" />\n <span class=\"mock-label\">{{ comp.label || 'Checkbox' }}</span>\n </label>\n } @else {\n <span class=\"mock-check-box\">\n <rpt-icon name=\"check\" [size]=\"10\" />\n </span>\n <span class=\"mock-label\">{{ comp.label || 'Checkbox' }}</span>\n }\n </div>\n }\n @case ('button') {\n <div class=\"mock-btn-wrapper\">\n <button\n type=\"button\"\n class=\"mock-btn\"\n [class]=\"'btn-' + (comp.properties?.['variant'] || 'primary')\"\n [class.mock-btn-full]=\"comp.properties?.['fullWidth'] === true || comp.layout?.width === '100%'\"\n [style.height]=\"getCustomBtnHeight(comp)\"\n [style.width]=\"getCustomBtnWidth(comp)\"\n (click)=\"onCanvasButtonClick(comp, $event)\"\n >\n @if (getCustomBtnIcon(comp); as btnIcon) {\n <rpt-icon [name]=\"btnIcon\" [size]=\"15\" />\n }\n <span>{{ comp.label || 'Button' }}</span>\n </button>\n </div>\n }\n @case ('kpi-card') {\n <div class=\"mock-kpi-field\">\n <div class=\"mock-kpi-header\">\n <span class=\"mock-label\">{{ comp.label || 'KPI Metric' }}</span>\n <rpt-icon name=\"kpi\" [size]=\"14\" />\n </div>\n <div class=\"mock-kpi-value\">{{ comp.properties?.['value'] || '$128,450' }}</div>\n <div class=\"mock-kpi-footer\">\n <span class=\"mock-kpi-trend\">+12.5%</span>\n <span class=\"mock-kpi-sub\">vs last period</span>\n </div>\n </div>\n }\n @case ('chart') {\n <div class=\"mock-chart-field\">\n <div class=\"mock-chart-header\">\n <span class=\"mock-label\">{{ comp.label || 'Quarterly Sales Revenue ($000)' }}</span>\n </div>\n <div class=\"mock-chart-bars\">\n <div class=\"mock-chart-col\">\n <div class=\"mock-chart-bar\" style=\"height: 44%\"></div>\n <span class=\"mock-bar-lbl\">Q1 Jan-Mar</span>\n </div>\n <div class=\"mock-chart-col\">\n <div class=\"mock-chart-bar\" style=\"height: 61%\"></div>\n <span class=\"mock-bar-lbl\">Q2 Apr-Jun</span>\n </div>\n <div class=\"mock-chart-col\">\n <div class=\"mock-chart-bar\" style=\"height: 79%\"></div>\n <span class=\"mock-bar-lbl\">Q3 Jul-Sep</span>\n </div>\n <div class=\"mock-chart-col\">\n <div class=\"mock-chart-bar\" style=\"height: 100%\"></div>\n <span class=\"mock-bar-lbl\">Q4 Oct-Dec</span>\n </div>\n </div>\n </div>\n }\n @default {\n <div class=\"mock-generic-box\">\n <span>{{ comp.type }}</span>\n </div>\n }\n }\n </div>\n\n <!-- Resizing Handles (East, South, South-East) -->\n <div\n class=\"resize-handle handle-e\"\n (pointerdown)=\"startResize($event, comp, 'e')\"\n (dblclick)=\"resetDimensions(comp, $event)\"\n title=\"Drag to resize column width \u2022 Double-click to reset\"\n ></div>\n <div\n class=\"resize-handle handle-s\"\n (pointerdown)=\"startResize($event, comp, 's')\"\n (dblclick)=\"resetDimensions(comp, $event)\"\n title=\"Drag to resize height \u2022 Double-click to reset\"\n ></div>\n <div\n class=\"resize-handle handle-se\"\n (pointerdown)=\"startResize($event, comp, 'se')\"\n (dblclick)=\"resetDimensions(comp, $event)\"\n title=\"Drag to resize width and height \u2022 Double-click to reset\"\n ></div>\n\n <!-- Resizing Dimension Ghost / Badge -->\n @if (resizingCompId() === comp.id) {\n <div class=\"resize-dimension-tooltip\">\n {{ resizingTooltip(comp) }}\n </div>\n }\n\n <!-- Drag placeholder with matched grid column span -->\n <div\n *cdkDragPlaceholder\n class=\"canvas-drag-placeholder\"\n [style.grid-column]=\"getGridColumn(comp)\"\n ></div>\n\n <!-- Drag preview -->\n <div *cdkDragPreview class=\"canvas-drag-preview\">\n <span class=\"drag-handle-dots\">\u22EE\u22EE</span>\n <rpt-icon [name]=\"getComponentIconName(comp.type)\" [size]=\"14\" />\n <span class=\"preview-title\">{{ comp.label || comp.key || comp.type }}</span>\n <span class=\"preview-span\">{{ getSpan(comp) }}/{{ totalColumns() }}</span>\n </div>\n </div>\n }\n\n @if (components().length === 0) {\n <div class=\"canvas-empty-state\" role=\"region\" aria-label=\"Empty canvas starter options\">\n <div class=\"empty-hero-badge\">\n <rpt-icon name=\"design\" [size]=\"28\" />\n </div>\n <h3 class=\"empty-hero-title\">Start Designing Your Custom Report</h3>\n <p class=\"empty-hero-desc\">\n Drag controls from the left component palette, or jumpstart your design with pre-configured enterprise blueprints.\n </p>\n\n <div class=\"empty-hero-actions\">\n <button\n type=\"button\"\n class=\"empty-btn empty-btn-primary\"\n (click)=\"applyBlueprint('blueprint-audit-register')\"\n title=\"Load complete Audit Register blueprint with parameters and result grid\"\n >\n <rpt-icon name=\"table\" [size]=\"15\" />\n <span>Audit Register Blueprint</span>\n </button>\n <button\n type=\"button\"\n class=\"empty-btn empty-btn-secondary\"\n (click)=\"applyBlueprint('blueprint-kpi-summary')\"\n title=\"Load Executive KPI summary blueprint\"\n >\n <rpt-icon name=\"chart-bar\" [size]=\"15\" />\n <span>KPI Summary Blueprint</span>\n </button>\n <button\n type=\"button\"\n class=\"empty-btn empty-btn-outline\"\n (click)=\"addStarterComponent('date-range')\"\n title=\"Add a Date Range filter component\"\n >\n <rpt-icon name=\"calendar\" [size]=\"14\" />\n <span>+ Add Date Filter</span>\n </button>\n <button\n type=\"button\"\n class=\"empty-btn empty-btn-outline\"\n (click)=\"addStarterComponent('button')\"\n title=\"Add an Execute Action button\"\n >\n <rpt-icon name=\"play\" [size]=\"14\" />\n <span>+ Add Run Button</span>\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Live Result Grid Preview with Bways-Grid Features -->\n @if (definition().grid) {\n <div class=\"canvas-grid-box\">\n <div class=\"grid-preview-header\">\n <div style=\"display: flex; align-items: center; gap: 8px;\">\n <rpt-icon name=\"table\" [size]=\"15\" />\n <span class=\"grid-title\">Bways Grid ({{ definition().grid?.columns?.length ?? 0 }} columns)</span>\n @if (isExecutingGrid()) {\n <span class=\"grid-exec-badge is-loading\">Loading data from database...</span>\n } @else if (gridExecError()) {\n <span class=\"grid-exec-badge is-error\">{{ gridExecError() }}</span>\n } @else if (canvasGridData().length > 0) {\n <span class=\"grid-exec-badge is-success\">{{ canvasGridData().length }} rows loaded</span>\n }\n </div>\n <div style=\"display: flex; align-items: center; gap: 8px;\">\n <button\n type=\"button\"\n class=\"btn-canvas-run-grid\"\n (click)=\"executeCanvasReport($event)\"\n [disabled]=\"isExecutingGrid()\"\n title=\"Execute query and load data into Bways Grid\"\n >\n <rpt-icon name=\"play\" [size]=\"11\" />\n <span>{{ isExecutingGrid() ? 'Running...' : 'Run / Refresh Grid' }}</span>\n </button>\n <button\n type=\"button\"\n class=\"btn-canvas-edit-grid\"\n (click)=\"selectGridSettings($event)\"\n title=\"Configure columns and grid options\"\n >\n <rpt-icon name=\"settings\" [size]=\"11\" />\n <span>Configure</span>\n </button>\n <button\n type=\"button\"\n class=\"btn-remove-grid\"\n (click)=\"removeGrid($event)\"\n title=\"Remove Result Grid from this report\"\n >\n <rpt-icon name=\"trash\" [size]=\"13\" />\n </button>\n </div>\n </div>\n\n @if (getGridSummaryText(); as summary) {\n <div class=\"grid-layout-status-strip\">\n <div class=\"grid-status-info\">\n <rpt-icon name=\"table\" [size]=\"12\" />\n <span>{{ summary }}</span>\n </div>\n <div class=\"grid-status-sync\" [class.is-pulsing]=\"layoutJustUpdated()\">\n <rpt-icon name=\"check\" [size]=\"11\" />\n <span>Layout auto-persisted</span>\n </div>\n </div>\n }\n\n <!-- Live Bways Grid bridge -->\n <div class=\"canvas-live-grid-container\" (click)=\"$event.stopPropagation()\">\n <rpt-grid-bridge\n [gridDefinition]=\"definition().grid!\"\n [data]=\"canvasGridData()\"\n (gridStateChange)=\"onCanvasGridStateChange($event)\"\n (columnDefsChange)=\"onCanvasColumnDefsChange($event)\"\n />\n </div>\n </div>\n } @else {\n <div class=\"canvas-add-grid-banner\" (click)=\"addResultGrid($event)\">\n <div class=\"add-grid-icon\">\n <rpt-icon name=\"table\" [size]=\"20\" />\n </div>\n <div class=\"add-grid-text\">\n <span class=\"add-grid-title\">+ Add Result Data Grid</span>\n <span class=\"add-grid-sub\">Attach an interactive data table or UltraGrid to display report execution output</span>\n </div>\n <button type=\"button\" class=\"btn-banner-add\">Add Grid</button>\n </div>\n }\n </div>\n </div>\n </div>", styles: [":host{display:block;height:100%;background-color:#f1f5f9;font-family:var(--rpt-font-family, inherit);box-sizing:border-box}.canvas-wrapper{display:flex;flex-direction:column;height:100%}.canvas-toolbar{display:flex;align-items:center;justify-content:space-between;min-height:42px;height:42px;padding:0 24px;background:var(--rpt-color-surface, #ffffff);border-bottom:1px solid var(--rpt-color-border, #cbd5e1);box-sizing:border-box;gap:12px;overflow-x:auto;scrollbar-width:none;font-family:var(--rpt-font-family, inherit);flex-shrink:0}.canvas-toolbar::-webkit-scrollbar{display:none}.canvas-info{display:flex;align-items:center;gap:8px;font-family:var(--rpt-font-family, inherit);font-size:var(--rpt-font-size-sm, .75rem);color:var(--rpt-color-text-muted, #64748b);white-space:nowrap;line-height:1}.info-label{color:var(--rpt-color-text-muted, #64748b);font-weight:500;font-size:var(--rpt-font-size-sm, .75rem)}.info-value{font-weight:600;color:var(--rpt-color-text, #0f172a);font-size:var(--rpt-font-size-sm, .75rem)}.info-divider{color:var(--rpt-color-border, #cbd5e1);margin:0 2px}.live-mode-badge{display:inline-flex;align-items:center;gap:4px;font-family:var(--rpt-font-family, inherit);font-size:var(--rpt-font-size-xs, .7rem);font-weight:600;color:#047857;background:#ecfdf5;border:1px solid #a7f3d0;padding:2px 8px;border-radius:9999px;line-height:1}.canvas-actions{display:flex;align-items:center;gap:8px;white-space:nowrap}.canvas-action-btn{display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 10px;font-family:var(--rpt-font-family, inherit);font-size:var(--rpt-font-size-sm, .75rem);font-weight:500;letter-spacing:-.01em;color:var(--rpt-color-text-secondary, #334155);background:var(--rpt-color-surface, #ffffff);border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius, 6px);cursor:pointer;line-height:1;white-space:nowrap;box-shadow:0 1px 2px #0000000a;transition:all var(--rpt-transition-speed, .15s) ease}.canvas-action-btn:hover{background:var(--rpt-color-surface-hover, #f8fafc);border-color:var(--rpt-color-border-hover, #94a3b8);color:var(--rpt-color-text, #0f172a);box-shadow:0 2px 4px #0000000f}.canvas-action-btn.btn-grid-configured{background:#eff6ff;border-color:#bfdbfe;color:#2563eb;font-weight:600}.canvas-action-btn.btn-grid-configured:hover{background:#dbeafe;color:#1d4ed8;border-color:#93c5fd}.canvas-action-btn.btn-grid-add{background:#f0fdf4;border-color:#bbf7d0;color:#16a34a;font-weight:600}.canvas-action-btn.btn-grid-add:hover{background:#dcfce7;color:#15803d;border-color:#86efac}.canvas-action-btn:active{transform:translateY(1px)}.canvas-scroll-area{flex:1;overflow-y:auto;padding:16px}.canvas-artboard{width:100%;max-width:1400px;margin:0 auto;background:#fff;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius-lg, 8px);box-shadow:0 4px 6px -1px #0000000d;min-height:480px;box-sizing:border-box}@media (max-width: 900px){.canvas-toolbar{min-height:36px;height:36px;padding:0 10px;gap:6px}.live-mode-badge{display:none}.canvas-info{font-size:.75rem;gap:5px}.canvas-action-btn{padding:0 8px;font-size:.725rem;height:28px;gap:4px}.canvas-scroll-area{padding:10px}.canvas-artboard{min-height:360px}}@media (max-width: 600px){.canvas-toolbar{padding:0 8px;gap:4px}.canvas-info .info-label,.canvas-action-btn span{display:none}.canvas-action-btn{padding:0 6px;min-width:28px;justify-content:center}}.canvas-report-header{margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid var(--rpt-color-border, #e2e8f0)}.canvas-report-title{font-size:var(--rpt-font-size-xl, 1.5rem);font-weight:700;color:var(--rpt-color-text, #0f172a);margin:0 0 6px}.canvas-report-desc{font-size:var(--rpt-font-size-base, .875rem);color:var(--rpt-color-text-muted, #64748b);margin:0}.canvas-grid{display:grid;width:100%;min-height:180px;align-items:start;border-radius:6px;transition:all var(--rpt-transition-speed, .15s) ease}.canvas-component-card{container-type:inline-size;background:#fff;border:1.5px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius, 6px);box-shadow:var(--rpt-box-shadow, 0 1px 2px rgba(0, 0, 0, .05));box-sizing:border-box;transition:all var(--rpt-transition-speed, .15s) ease;cursor:pointer;position:relative;overflow:visible;display:flex;flex-direction:column;min-height:104px;z-index:1}.canvas-component-card:hover{border-color:var(--rpt-color-primary, #2563eb);box-shadow:0 4px 6px -1px #2563eb1a;z-index:2}.canvas-component-card.is-selected{border-color:var(--rpt-color-primary, #2563eb);box-shadow:0 0 0 2px #2563eb40;z-index:10}.canvas-component-card:focus-within,.canvas-component-card:has(.rpt-combobox-menu),.canvas-component-card:has(.is-open){z-index:100}.canvas-component-card.is-resizing{-webkit-user-select:none;user-select:none;border-color:var(--rpt-color-primary, #2563eb);box-shadow:0 0 0 2px #2563eb59;z-index:30;transition:none!important}.resize-handle{position:absolute;z-index:20;touch-action:none;opacity:0;pointer-events:none;transition:opacity .15s ease}.canvas-component-card.is-selected .resize-handle{opacity:1;pointer-events:auto}.resize-handle.handle-e{top:0;right:0;width:6px;height:100%;cursor:ew-resize;display:flex;align-items:center;justify-content:center}.resize-handle.handle-e:after{content:\"\";width:3px;height:24px;background:#cbd5e1;border-radius:2px;transition:background .15s ease}.resize-handle.handle-e:hover:after,.canvas-component-card.is-selected .resize-handle.handle-e:after{background:var(--rpt-color-primary, #2563eb)}.resize-handle.handle-s{bottom:0;left:0;height:6px;width:100%;cursor:ns-resize;display:flex;align-items:center;justify-content:center}.resize-handle.handle-s:after{content:\"\";height:3px;width:24px;background:#cbd5e1;border-radius:2px;transition:background .15s ease}.resize-handle.handle-s:hover:after,.canvas-component-card.is-selected .resize-handle.handle-s:after{background:var(--rpt-color-primary, #2563eb)}.resize-handle.handle-se{bottom:0;right:0;width:12px;height:12px;cursor:nwse-resize;display:flex;align-items:flex-end;justify-content:flex-end;padding:2px}.resize-handle.handle-se:after{content:\"\";width:5px;height:5px;border-right:2px solid #cbd5e1;border-bottom:2px solid #cbd5e1;transition:border-color .15s ease}.resize-handle.handle-se:hover:after,.canvas-component-card.is-selected .resize-handle.handle-se:after{border-color:var(--rpt-color-primary, #2563eb)}.resize-dimension-tooltip{position:absolute;top:38px;right:8px;background:#0f172ae6;color:#fff;font-size:.7rem;font-weight:600;padding:3px 8px;border-radius:4px;white-space:nowrap;pointer-events:none;z-index:50;box-shadow:0 2px 4px #0003}.card-header{display:flex;align-items:center;justify-content:space-between;gap:6px;padding:5px 8px;background:var(--rpt-color-surface, #f8fafc);border-bottom:1px solid var(--rpt-color-border, #e2e8f0);border-top-left-radius:var(--rpt-border-radius, 6px);border-top-right-radius:var(--rpt-border-radius, 6px);min-height:32px;box-sizing:border-box;overflow:hidden;position:relative}.canvas-component-card.is-selected .card-header{overflow:visible}.card-title-group{display:flex;align-items:center;gap:5px;cursor:grab;min-width:0;flex:1;overflow:hidden}.drag-handle-dots{color:var(--rpt-color-text-muted, #94a3b8);font-size:.85rem;letter-spacing:-2px;flex-shrink:0;-webkit-user-select:none;user-select:none}.comp-title{font-size:.75rem;font-weight:600;color:var(--rpt-color-text, #0f172a);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;-webkit-user-select:none;user-select:none}.card-actions{display:flex;align-items:center;gap:4px;flex-shrink:0;margin-left:auto}.new-row-pill-static{font-size:.65rem;font-weight:600;color:#7c3aed;background:#f5f3ff;border:1px solid #ddd6fe;padding:1px 5px;border-radius:9999px;white-space:nowrap;line-height:1;flex-shrink:0}.span-pill-static{display:inline-flex;align-items:center;justify-content:center;font-size:.65rem;font-weight:600;color:var(--rpt-color-text-muted, #64748b);background:var(--rpt-color-surface, #f1f5f9);border:1px solid var(--rpt-color-border, #e2e8f0);padding:1px 6px;border-radius:9999px;line-height:1.2;white-space:nowrap;-webkit-user-select:none;user-select:none}.card-quick-controls{display:none;align-items:center;gap:3px;flex-shrink:0}.canvas-component-card:hover .span-pill-static,.canvas-component-card.is-selected .span-pill-static,.canvas-component-card:hover .new-row-pill-static,.canvas-component-card.is-selected .new-row-pill-static{display:none}.canvas-component-card:hover .card-quick-controls,.canvas-component-card.is-selected .card-quick-controls{display:inline-flex}.canvas-component-card.is-narrow .card-header{padding:4px 6px;gap:3px}.canvas-component-card.is-narrow:hover .comp-title,.canvas-component-card.is-narrow.is-selected .comp-title,.canvas-component-card.is-narrow:hover .drag-handle-dots,.canvas-component-card.is-narrow.is-selected .drag-handle-dots{display:none}.canvas-component-card.is-narrow .card-quick-controls{gap:2px}.canvas-component-card.is-narrow .span-control-group{height:18px}.canvas-component-card.is-narrow .span-btn{width:14px;height:16px}.canvas-component-card.is-narrow .span-badge{font-size:.6rem;padding:0 2px}.canvas-component-card.is-narrow .span-total{display:none}.canvas-component-card.is-narrow .card-action-btn{width:18px;height:18px}@container (max-width: 250px){.card-header{padding:4px 6px;gap:3px}.canvas-component-card:hover .comp-title,.canvas-component-card.is-selected .comp-title,.canvas-component-card:hover .drag-handle-dots,.canvas-component-card.is-selected .drag-handle-dots,.span-total{display:none}.card-quick-controls{gap:2px}.card-action-btn{width:18px;height:18px}.span-control-group{height:18px}.span-btn{width:14px;height:16px}.span-badge{font-size:.6rem;padding:0 2px}}@container (max-width: 160px){.canvas-component-card.is-selected .card-quick-controls{position:absolute;top:-30px;right:0;background:#fff;border:1.5px solid var(--rpt-color-primary, #2563eb);border-radius:6px;padding:2px 4px;gap:3px;box-shadow:0 4px 12px #2563eb2e,0 2px 4px #00000014;z-index:120;white-space:nowrap}}@media (max-width: 900px){.canvas-component-card:hover .comp-title,.canvas-component-card.is-selected .comp-title,.canvas-component-card:hover .drag-handle-dots,.canvas-component-card.is-selected .drag-handle-dots,.canvas-component-card .span-total{display:none}.canvas-component-card.is-selected .card-header{overflow:visible}.canvas-component-card.is-selected .card-quick-controls{position:absolute;top:-30px;right:0;background:#fff;border:1.5px solid var(--rpt-color-primary, #2563eb);border-radius:6px;padding:2px 4px;gap:3px;box-shadow:0 4px 12px #2563eb2e,0 2px 4px #00000014;z-index:120;white-space:nowrap}}.span-control-group{display:inline-flex;align-items:center;background:#fff;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:4px;height:20px;padding:0 1px;white-space:nowrap;flex-shrink:0}.span-btn{width:16px;height:18px;display:inline-flex;align-items:center;justify-content:center;border:none;background:transparent;padding:0;color:var(--rpt-color-text-muted, #64748b);cursor:pointer;border-radius:2px;transition:all .12s ease}.span-btn:hover:not(:disabled){background:var(--rpt-color-surface-hover, #f1f5f9);color:var(--rpt-color-primary, #2563eb)}.span-btn:disabled{opacity:.3;cursor:not-allowed}.span-badge{font-size:.65rem;font-weight:600;color:var(--rpt-color-text, #0f172a);padding:0 3px;white-space:nowrap;-webkit-user-select:none;user-select:none;line-height:1}.span-total{font-size:.6rem;color:var(--rpt-color-text-muted, #64748b);font-weight:500}.card-action-btn{width:20px;height:20px;border:none;background:transparent;border-radius:4px;display:inline-flex;align-items:center;justify-content:center;padding:0;cursor:pointer;color:var(--rpt-color-text-muted, #64748b);transition:all .12s ease;flex-shrink:0}.card-action-btn:hover{background:#e2e8f0;color:var(--rpt-color-text, #0f172a)}.card-action-btn.btn-active-toggle{background:#7c3aed;color:#fff;border-color:#7c3aed}.card-action-btn.btn-active-toggle:hover{background:#6d28d9;color:#fff}.btn-delete:hover{background:#fee2e2;color:#ef4444}.card-body{padding:8px 10px 14px;flex:1;display:flex;flex-direction:column;min-height:0;min-width:0;max-width:100%;box-sizing:border-box;overflow:visible}.mock-input-field{display:flex;flex-direction:column;gap:4px;flex:1;height:100%;min-height:0}.mock-label{font-size:.7rem;font-weight:600;color:var(--rpt-color-text-muted, #475569);flex-shrink:0}.mock-input-box,.mock-select-box{border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:6px;padding:6px 12px;font-size:var(--rpt-font-size-base, .875rem);color:var(--rpt-color-text-muted, #94a3b8);background:#fff;flex:1;min-height:38px;height:100%;display:flex;align-items:center;justify-content:space-between;box-sizing:border-box}.mock-checkbox-field{display:flex;align-items:center;gap:6px;flex:1;height:100%;min-height:0;padding-top:14px}.mock-check-box{width:18px;height:18px;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:.65rem;color:var(--rpt-color-primary, #2563eb);flex-shrink:0}.mock-btn-wrapper{display:flex;flex:1;height:100%;min-height:38px;align-items:center;justify-content:flex-start;width:100%;min-width:0;box-sizing:border-box}.mock-btn{border-radius:var(--rpt-border-radius, 6px);padding:0 16px;height:36px;font-size:var(--rpt-font-size-base, .875rem);font-weight:500;border:1px solid transparent;cursor:pointer;width:auto;max-width:100%;display:inline-flex;align-items:center;justify-content:center;gap:6px;box-sizing:border-box;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:all .15s ease}.mock-btn.mock-btn-full{width:100%}.canvas-component-card.is-narrow .mock-btn{padding:0 6px;font-size:.775rem;width:100%;height:34px}.mock-kpi-field{display:flex;flex-direction:column;justify-content:space-between;flex:1;height:100%;min-height:0}.mock-kpi-header{display:flex;align-items:center;justify-content:space-between}.mock-kpi-value{font-size:1.25rem;font-weight:700;color:var(--rpt-color-text, #0f172a);margin:2px 0}.mock-kpi-footer{display:flex;align-items:center;gap:6px;font-size:.65rem}.mock-kpi-trend{color:#16a34a;font-weight:600}.mock-kpi-sub{color:#64748b}.mock-chart-field{display:flex;flex-direction:column;flex:1;height:100%;min-height:140px}.mock-chart-bars{display:flex;align-items:flex-end;justify-content:space-around;gap:16px;flex:1;min-height:100px;padding-top:12px;box-sizing:border-box}.mock-chart-col{flex:1;display:flex;flex-direction:column;align-items:center;height:100%;min-width:60px;max-width:140px}.mock-chart-bar{width:38px;max-width:44px;margin:0 auto;background:var(--rpt-color-primary, #2563eb);border-radius:6px 6px 0 0;opacity:.85;min-height:10px;transition:height .2s ease}.mock-bar-lbl{font-size:.6875rem;color:#64748b;margin-top:6px;text-align:center;white-space:normal;line-height:1.2}.btn-primary{background:var(--rpt-color-primary, #2563eb);color:#fff}.btn-secondary{background:#f1f5f9;color:#0f172a;border:1px solid #cbd5e1}.btn-outline{background:transparent;color:#2563eb;border:1px solid #2563eb}.btn-danger{background:#ef4444;color:#fff}.mock-generic-box{border:1px dashed #cbd5e1;border-radius:4px;padding:8px;font-size:.75rem;text-align:center;color:#64748b}.canvas-drag-placeholder{background:#2563eb0f;border:2px dashed var(--rpt-color-primary, #2563eb);border-radius:6px;min-height:80px;box-sizing:border-box;transition:transform .15s ease}.canvas-drag-preview,::ng-deep .canvas-drag-preview{background:#fff;border:2px solid var(--rpt-color-primary, #2563eb);border-radius:6px;padding:8px 14px;display:inline-flex;align-items:center;gap:8px;font-family:var(--rpt-font-family, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif)!important;font-size:.8rem;font-weight:600;color:var(--rpt-color-text, #0f172a);box-shadow:0 12px 24px -4px #00000040;z-index:10000}.preview-title,::ng-deep .preview-title{font-family:inherit!important;max-width:160px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.preview-span,::ng-deep .preview-span{font-family:inherit!important;font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border:1px solid #bfdbfe;padding:1px 6px;border-radius:9999px}.canvas-empty-state{grid-column:1 / -1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:56px 28px;border:2px dashed var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius-xl, 12px);background:var(--rpt-color-surface, #ffffff);text-align:center;box-shadow:var(--rpt-box-shadow-xs, 0 1px 2px 0 rgba(0, 0, 0, .04));transition:border-color var(--rpt-transition-speed, .15s) ease}.canvas-empty-state:hover{border-color:var(--rpt-color-primary, #2563eb)}.empty-hero-badge{width:56px;height:56px;border-radius:var(--rpt-border-radius-xl, 12px);background:var(--rpt-color-primary-light, #eff6ff);color:var(--rpt-color-primary, #2563eb);display:flex;align-items:center;justify-content:center;margin-bottom:16px;box-shadow:0 2px 8px #2563eb1f}.empty-hero-title{font-size:var(--rpt-font-size-lg, 1.125rem);font-weight:700;color:var(--rpt-color-text, #0f172a);margin:0 0 8px;letter-spacing:-.01em}.empty-hero-desc{font-size:var(--rpt-font-size-base, .875rem);color:var(--rpt-color-text-muted, #64748b);max-width:520px;line-height:1.5;margin:0 0 24px}.empty-hero-actions{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:10px}.empty-btn{display:inline-flex;align-items:center;gap:7px;padding:8px 16px;font-size:var(--rpt-font-size-sm, .75rem);font-weight:600;border-radius:var(--rpt-border-radius, 6px);cursor:pointer;transition:all var(--rpt-transition-speed, .15s) ease;font-family:inherit}.empty-btn-primary{background:var(--rpt-color-primary, #2563eb);color:#fff;border:1px solid var(--rpt-color-primary, #2563eb);box-shadow:0 1px 3px #2563eb33}.empty-btn-primary:hover{background:var(--rpt-color-primary-hover, #1d4ed8);border-color:var(--rpt-color-primary-hover, #1d4ed8);transform:translateY(-1px)}.empty-btn-secondary{background:#f8fafc;color:#334155;border:1px solid var(--rpt-color-border, #cbd5e1)}.empty-btn-secondary:hover{background:#f1f5f9;color:#0f172a;border-color:#94a3b8;transform:translateY(-1px)}.empty-btn-outline{background:#fff;color:var(--rpt-color-primary, #2563eb);border:1px solid #bfdbfe}.empty-btn-outline:hover{background:#eff6ff;border-color:var(--rpt-color-primary, #2563eb);transform:translateY(-1px)}.canvas-active-input{width:100%;height:32px;padding:4px 8px;font-size:.8125rem;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:4px;outline:none;box-sizing:border-box;background:#fff;color:#0f172a;transition:border-color .15s ease}.canvas-active-input:focus{border-color:var(--rpt-color-primary, #2563eb)}.canvas-active-checkbox-lbl{display:inline-flex;align-items:center;gap:6px;cursor:pointer}.canvas-live-control-wrapper{width:100%;max-width:100%;min-width:0;position:relative;z-index:1;box-sizing:border-box}.canvas-grid-box{margin-top:24px;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:8px;background:#fff;box-shadow:0 1px 3px #0000000d;overflow:hidden}.grid-preview-header{display:flex;align-items:center;justify-content:space-between;padding:10px 16px;background:var(--rpt-color-surface, #f8fafc);border-bottom:1px solid var(--rpt-color-border, #cbd5e1);font-size:.8rem;font-weight:600;color:var(--rpt-color-text, #0f172a)}.grid-title{font-weight:700}.grid-exec-badge{font-size:.6875rem;font-weight:600;padding:2px 8px;border-radius:10px}.grid-exec-badge.is-loading{background:#eff6ff;color:#2563eb}.grid-exec-badge.is-error{background:#fee2e2;color:#ef4444}.grid-exec-badge.is-success{background:#ecfdf5;color:#059669}.btn-canvas-run-grid{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;font-size:.72rem;font-weight:600;color:#fff;background:var(--rpt-color-primary, #2563eb);border:none;border-radius:4px;cursor:pointer;transition:background .15s ease}.btn-canvas-run-grid:hover:not(:disabled){background:#1d4ed8}.btn-canvas-run-grid:disabled{opacity:.6;cursor:not-allowed}.btn-canvas-edit-grid{display:inline-flex;align-items:center;gap:4px;padding:4px 8px;font-size:.72rem;font-weight:500;color:#334155;background:#fff;border:1px solid #cbd5e1;border-radius:4px;cursor:pointer;transition:all .12s ease}.btn-canvas-edit-grid:hover{background:#f1f5f9}.canvas-live-grid-container{width:100%;min-height:480px;background:#fff}.live-mode-badge{display:inline-flex;align-items:center;gap:4px;font-size:.6875rem;font-weight:700;color:#047857;background:#ecfdf5;border:1px solid #a7f3d0;padding:1px 8px;border-radius:9999px}.canvas-component-card.is-field-highlighted{border-color:#f59e0b!important;box-shadow:0 0 0 3px #f59e0b59!important;animation:rpt-pulse-border 1s ease-in-out infinite alternate}@keyframes rpt-pulse-border{0%{box-shadow:0 0 0 2px #f59e0b40}to{box-shadow:0 0 0 5px #f59e0b80}}.grid-layout-status-strip{display:flex;align-items:center;justify-content:space-between;padding:6px 16px;background:#f1f5f9;border-bottom:1px solid var(--rpt-color-border, #cbd5e1);font-size:.6875rem;color:#475569}.grid-status-info{display:inline-flex;align-items:center;gap:6px;font-weight:600}.grid-status-sync{display:inline-flex;align-items:center;gap:4px;color:#059669;font-weight:600;transition:transform .2s ease}.grid-status-sync.is-pulsing{transform:scale(1.08);color:#047857}.canvas-actions{display:flex;align-items:center;gap:8px}.btn-grid-add{background:#f0fdf4;border-color:#bbf7d0;color:#16a34a}.btn-grid-add:hover{background:#dcfce7;border-color:#86efac;color:#15803d}.btn-remove-grid{display:inline-flex;align-items:center;justify-content:center;padding:4px;border:none;background:transparent;color:#94a3b8;border-radius:4px;cursor:pointer;transition:all .15s ease}.btn-remove-grid:hover{background:#fee2e2;color:#ef4444}.canvas-add-grid-banner{margin-top:24px;border:2px dashed #cbd5e1;border-radius:8px;background:#fff;padding:16px 20px;display:flex;align-items:center;gap:16px;cursor:pointer;transition:all .15s ease}.canvas-add-grid-banner:hover{border-color:#2563eb;background:#f8fafc;box-shadow:0 2px 8px #2563eb14}.add-grid-icon{width:40px;height:40px;border-radius:8px;background:#eff6ff;color:#2563eb;display:flex;align-items:center;justify-content:center;flex-shrink:0}.add-grid-text{display:flex;flex-direction:column;gap:2px;flex:1}.add-grid-title{font-size:.875rem;font-weight:600;color:#0f172a}.add-grid-sub{font-size:.75rem;color:#64748b}.btn-banner-add{padding:6px 14px;font-size:.75rem;font-weight:600;color:#2563eb;background:#eff6ff;border:1px solid #bfdbfe;border-radius:6px;cursor:pointer;white-space:nowrap;transition:all .15s ease}.canvas-add-grid-banner:hover .btn-banner-add{background:#2563eb;color:#fff;border-color:#2563eb}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$2.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i1$2.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i1$2.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "directive", type: i1$2.CdkDragPreview, selector: "ng-template[cdkDragPreview]", inputs: ["data", "matchSize"] }, { kind: "directive", type: i1$2.CdkDragPlaceholder, selector: "ng-template[cdkDragPlaceholder]", inputs: ["data"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.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: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: RptIconComponent, selector: "rpt-icon", inputs: ["name", "size", "color", "strokeWidth"] }, { kind: "component", type: RptDropdownComponent, selector: "rpt-dropdown", inputs: ["config", "control", "parentForm", "showStatus"] }, { kind: "component", type: RptDateRangeComponent, selector: "rpt-date-range", inputs: ["config", "control"] }, { kind: "component", type: ReportGridBridgeComponent, selector: "rpt-grid-bridge", inputs: ["gridDefinition", "data"], outputs: ["gridStateChange", "columnDefsChange", "rowClick", "selectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
9747
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: DesignCanvasComponent, isStandalone: true, selector: "rpt-design-canvas", host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: " <div class=\"canvas-wrapper\" (click)=\"onCanvasBackgroundClick($event)\">\n <!-- Canvas Status / Quick Controls -->\n <div class=\"canvas-toolbar\">\n <div class=\"canvas-info\">\n <span class=\"info-label\">Grid Layout:</span>\n <span class=\"info-value\">{{ totalColumns() }} Columns</span>\n <span class=\"info-divider\">\u2022</span>\n <span class=\"info-label\">Components:</span>\n <span class=\"info-value\">{{ components().length }}</span>\n <span class=\"info-divider\">\u2022</span>\n <span class=\"live-mode-badge\" title=\"Interactive canvas: Values typed here run live queries and test dropdowns.\">\n \u26A1 Live Interactive Mode\n </span>\n </div>\n\n <div class=\"canvas-actions\">\n @if (!definition().grid) {\n <button\n type=\"button\"\n class=\"canvas-action-btn btn-grid-add\"\n (click)=\"addResultGrid($event)\"\n title=\"Add a Result Grid to this report\"\n >\n <rpt-icon name=\"table\" [size]=\"14\" />\n <span>+ Add Grid</span>\n </button>\n } @else {\n <button\n type=\"button\"\n class=\"canvas-action-btn btn-grid-configured\"\n (click)=\"selectGridSettings($event)\"\n title=\"Configure Result Data Grid\"\n >\n <rpt-icon name=\"table\" [size]=\"14\" />\n <span>Grid ({{ definition().grid?.columns?.length ?? 0 }})</span>\n </button>\n }\n\n <button\n type=\"button\"\n class=\"canvas-action-btn\"\n (click)=\"selectReportSettings($event)\"\n title=\"Configure Page Layout & Grid Settings\"\n >\n <rpt-icon name=\"settings\" [size]=\"14\" />\n <span>Report Settings</span>\n </button>\n </div>\n </div>\n\n <!-- Canvas Drop Area -->\n <div class=\"canvas-scroll-area\">\n <div\n class=\"canvas-artboard\"\n [style.padding.px]=\"definition().layout.padding ?? 24\"\n >\n <!-- Report Header Preview -->\n <div class=\"canvas-report-header\">\n <h2 class=\"canvas-report-title\">{{ definition().name || 'Untitled Report' }}</h2>\n @if (definition().description) {\n <p class=\"canvas-report-desc\">{{ definition().description }}</p>\n }\n </div>\n\n <!-- Component Drop List Grid with 2D mixed orientation and data binding -->\n <div\n cdkDropList\n id=\"designer-canvas-drop-list\"\n class=\"canvas-grid\"\n cdkDropListOrientation=\"mixed\"\n [cdkDropListData]=\"sortedComponents()\"\n [style.grid-template-columns]=\"gridTemplateColumns()\"\n [style.gap.px]=\"definition().layout.gap ?? 16\"\n (cdkDropListDropped)=\"onDrop($event)\"\n >\n @for (comp of sortedComponents(); track comp.id; let idx = $index) {\n <div\n cdkDrag\n [cdkDragData]=\"comp\"\n [cdkDragDisabled]=\"isAnyResizing()\"\n class=\"canvas-component-card\"\n [class.is-selected]=\"isSelected(comp.id)\"\n [class.is-resizing]=\"resizingCompId() === comp.id\"\n [class.is-field-highlighted]=\"isFieldHighlighted(comp)\"\n [class.is-narrow]=\"getSpan(comp) <= 4\"\n [class.is-compact]=\"getSpan(comp) <= 2\"\n [style.grid-column]=\"getGridColumn(comp)\"\n [style.grid-row]=\"getGridRow(comp)\"\n [style.width]=\"comp.layout?.width || 'auto'\"\n [style.height]=\"getComponentHeight(comp)\"\n [style.min-height]=\"getComponentMinHeight(comp)\"\n (click)=\"onSelect(comp.id, $event)\"\n >\n <!-- Card Header with Drag Handle & Actions -->\n <div class=\"card-header\">\n <div class=\"card-title-group\" cdkDragHandle title=\"Drag to reorder\">\n <span class=\"drag-handle-dots\">\u22EE\u22EE</span>\n <rpt-icon [name]=\"getComponentIconName(comp.type)\" [size]=\"14\" />\n <span class=\"comp-title\" [title]=\"comp.label || comp.key || comp.type\">\n {{ comp.label || comp.key || comp.type }}\n </span>\n </div>\n\n <div class=\"card-actions\">\n <!-- Static span pill & new row / start col pill when not hovered / not selected -->\n @if (comp.layout?.column; as col) {\n <span class=\"new-row-pill-static\" title=\"Starts at Column {{ col }}\">Col {{ col }}</span>\n } @else if (comp.layout?.newRow) {\n <span class=\"new-row-pill-static\" title=\"Starts on separate row\">\u21B3 Row</span>\n }\n <span\n class=\"span-pill-static\"\n title=\"Column span: {{ getSpan(comp) }} of {{ totalColumns() }} columns\"\n >\n {{ getSpan(comp) }}/{{ totalColumns() }}\n </span>\n\n <!-- Quick Action Stepper & Buttons (revealed on hover or selection) -->\n <div class=\"card-quick-controls\">\n <button\n type=\"button\"\n class=\"card-action-btn\"\n [class.btn-active-toggle]=\"comp.layout?.newRow\"\n (click)=\"toggleNewRow(comp, $event)\"\n title=\"{{ comp.layout?.newRow ? 'Starts on new row (Click to place inline)' : 'Start on separate row (Row Break)' }}\"\n >\n <rpt-icon name=\"corner-down-right\" [size]=\"11\" />\n </button>\n\n <div class=\"span-control-group\" title=\"Adjust Column Span\">\n <button\n type=\"button\"\n class=\"span-btn\"\n [disabled]=\"getSpan(comp) <= 1\"\n (click)=\"adjustSpan(comp, -1, $event)\"\n title=\"Shrink Column Span\"\n >\n <rpt-icon name=\"minus\" [size]=\"9\" />\n </button>\n <span class=\"span-badge\" [title]=\"'Column span: ' + getSpan(comp) + ' of ' + totalColumns()\">\n <span class=\"span-num\">{{ getSpan(comp) }}</span><span class=\"span-total\">/{{ totalColumns() }}</span>\n </span>\n <button\n type=\"button\"\n class=\"span-btn\"\n [disabled]=\"getSpan(comp) >= totalColumns()\"\n (click)=\"adjustSpan(comp, 1, $event)\"\n title=\"Expand Column Span\"\n >\n <rpt-icon name=\"plus\" [size]=\"9\" />\n </button>\n </div>\n\n <button\n type=\"button\"\n class=\"card-action-btn\"\n (click)=\"onDuplicate(comp.id, $event)\"\n title=\"Duplicate component\"\n >\n <rpt-icon name=\"copy\" [size]=\"12\" />\n </button>\n <button\n type=\"button\"\n class=\"card-action-btn btn-delete\"\n (click)=\"onDelete(comp.id, $event)\"\n title=\"Remove component\"\n >\n <rpt-icon name=\"trash\" [size]=\"12\" />\n </button>\n </div>\n </div>\n </div>\n\n <!-- Card Body Visual Preview -->\n <div class=\"card-body\">\n\n @switch (comp.type) {\n @case ('input') {\n <div class=\"mock-input-field\" (click)=\"onControlInteract(comp, $event)\">\n <span class=\"mock-label\">{{ comp.label || 'Input' }}</span>\n @if (getCanvasControl(comp); as ctrl) {\n <input\n type=\"text\"\n class=\"canvas-active-input\"\n [placeholder]=\"comp.properties?.['placeholder'] || 'Enter text...'\"\n [formControl]=\"ctrl\"\n (focus)=\"onControlInteract(comp, $event)\"\n />\n } @else {\n <div class=\"mock-input-box\">{{ comp.properties?.['placeholder'] || 'Enter text...' }}</div>\n }\n </div>\n }\n @case ('dropdown') {\n <div class=\"canvas-live-control-wrapper\" (click)=\"onControlInteract(comp, $event)\">\n <rpt-dropdown\n [config]=\"comp\"\n [showStatus]=\"true\"\n [parentForm]=\"canvasForm()\"\n [control]=\"getCanvasControl(comp)\"\n />\n </div>\n }\n @case ('date') {\n <div class=\"mock-input-field\" (click)=\"onControlInteract(comp, $event)\">\n <span class=\"mock-label\">{{ comp.label || 'Date' }}</span>\n @if (getCanvasControl(comp); as ctrl) {\n <input\n type=\"date\"\n class=\"canvas-active-input\"\n [formControl]=\"ctrl\"\n (focus)=\"onControlInteract(comp, $event)\"\n />\n } @else {\n <div class=\"mock-select-box\">\n <span>YYYY-MM-DD</span>\n <rpt-icon name=\"calendar\" [size]=\"12\" />\n </div>\n }\n </div>\n }\n @case ('date-range') {\n <div class=\"canvas-live-control-wrapper\" (click)=\"onControlInteract(comp, $event)\">\n <rpt-date-range\n [config]=\"comp\"\n [control]=\"getCanvasControl(comp)\"\n />\n </div>\n }\n @case ('number') {\n <div class=\"mock-input-field\" (click)=\"onControlInteract(comp, $event)\">\n <span class=\"mock-label\">{{ comp.label || 'Number' }}</span>\n @if (getCanvasControl(comp); as ctrl) {\n <input\n type=\"number\"\n class=\"canvas-active-input\"\n [formControl]=\"ctrl\"\n (focus)=\"onControlInteract(comp, $event)\"\n />\n } @else {\n <div class=\"mock-input-box\">\n {{ comp.properties?.['prefix'] || '' }} 0.00 {{ comp.properties?.['suffix'] || '' }}\n </div>\n }\n </div>\n }\n @case ('checkbox') {\n <div class=\"mock-checkbox-field\" (click)=\"onControlInteract(comp, $event)\">\n @if (getCanvasControl(comp); as ctrl) {\n <label class=\"canvas-active-checkbox-lbl\">\n <input type=\"checkbox\" [formControl]=\"ctrl\" (focus)=\"onControlInteract(comp, $event)\" />\n <span class=\"mock-label\">{{ comp.label || 'Checkbox' }}</span>\n </label>\n } @else {\n <span class=\"mock-check-box\">\n <rpt-icon name=\"check\" [size]=\"10\" />\n </span>\n <span class=\"mock-label\">{{ comp.label || 'Checkbox' }}</span>\n }\n </div>\n }\n @case ('button') {\n <div class=\"mock-btn-wrapper\">\n <button\n type=\"button\"\n class=\"mock-btn\"\n [class]=\"'btn-' + (comp.properties?.['variant'] || 'primary')\"\n [class.mock-btn-full]=\"comp.properties?.['fullWidth'] === true || comp.layout?.width === '100%'\"\n [style.height]=\"getCustomBtnHeight(comp)\"\n [style.width]=\"getCustomBtnWidth(comp)\"\n (click)=\"onCanvasButtonClick(comp, $event)\"\n >\n @if (getCustomBtnIcon(comp); as btnIcon) {\n <rpt-icon [name]=\"btnIcon\" [size]=\"15\" />\n }\n <span>{{ comp.label || 'Button' }}</span>\n </button>\n </div>\n }\n @case ('kpi-card') {\n <div class=\"mock-kpi-field\">\n <div class=\"mock-kpi-header\">\n <span class=\"mock-label\">{{ comp.label || 'KPI Metric' }}</span>\n <rpt-icon name=\"kpi\" [size]=\"14\" />\n </div>\n <div class=\"mock-kpi-value\">{{ comp.properties?.['value'] || '$128,450' }}</div>\n <div class=\"mock-kpi-footer\">\n <span class=\"mock-kpi-trend\">+12.5%</span>\n <span class=\"mock-kpi-sub\">vs last period</span>\n </div>\n </div>\n }\n @case ('chart') {\n <div class=\"mock-chart-field\">\n <div class=\"mock-chart-header\">\n <span class=\"mock-label\">{{ comp.label || 'Quarterly Sales Revenue ($000)' }}</span>\n </div>\n <div class=\"mock-chart-bars\">\n <div class=\"mock-chart-col\">\n <div class=\"mock-chart-bar\" style=\"height: 44%\"></div>\n <span class=\"mock-bar-lbl\">Q1 Jan-Mar</span>\n </div>\n <div class=\"mock-chart-col\">\n <div class=\"mock-chart-bar\" style=\"height: 61%\"></div>\n <span class=\"mock-bar-lbl\">Q2 Apr-Jun</span>\n </div>\n <div class=\"mock-chart-col\">\n <div class=\"mock-chart-bar\" style=\"height: 79%\"></div>\n <span class=\"mock-bar-lbl\">Q3 Jul-Sep</span>\n </div>\n <div class=\"mock-chart-col\">\n <div class=\"mock-chart-bar\" style=\"height: 100%\"></div>\n <span class=\"mock-bar-lbl\">Q4 Oct-Dec</span>\n </div>\n </div>\n </div>\n }\n @default {\n <div class=\"mock-generic-box\">\n <span>{{ comp.type }}</span>\n </div>\n }\n }\n </div>\n\n <!-- Resizing Handles (East, South, South-East) -->\n <div\n class=\"resize-handle handle-e\"\n (pointerdown)=\"startResize($event, comp, 'e')\"\n (dblclick)=\"resetDimensions(comp, $event)\"\n title=\"Drag to resize column width \u2022 Double-click to reset\"\n ></div>\n <div\n class=\"resize-handle handle-s\"\n (pointerdown)=\"startResize($event, comp, 's')\"\n (dblclick)=\"resetDimensions(comp, $event)\"\n title=\"Drag to resize height \u2022 Double-click to reset\"\n ></div>\n <div\n class=\"resize-handle handle-se\"\n (pointerdown)=\"startResize($event, comp, 'se')\"\n (dblclick)=\"resetDimensions(comp, $event)\"\n title=\"Drag to resize width and height \u2022 Double-click to reset\"\n ></div>\n\n <!-- Resizing Dimension Ghost / Badge -->\n @if (resizingCompId() === comp.id) {\n <div class=\"resize-dimension-tooltip\">\n {{ resizingTooltip(comp) }}\n </div>\n }\n\n <!-- Drag placeholder with matched grid column span -->\n <div\n *cdkDragPlaceholder\n class=\"canvas-drag-placeholder\"\n [style.grid-column]=\"getGridColumn(comp)\"\n ></div>\n\n <!-- Drag preview -->\n <div *cdkDragPreview class=\"canvas-drag-preview\">\n <span class=\"drag-handle-dots\">\u22EE\u22EE</span>\n <rpt-icon [name]=\"getComponentIconName(comp.type)\" [size]=\"14\" />\n <span class=\"preview-title\">{{ comp.label || comp.key || comp.type }}</span>\n <span class=\"preview-span\">{{ getSpan(comp) }}/{{ totalColumns() }}</span>\n </div>\n </div>\n }\n\n @if (components().length === 0) {\n <div class=\"canvas-empty-state\" role=\"region\" aria-label=\"Empty canvas starter options\">\n <div class=\"empty-hero-badge\">\n <rpt-icon name=\"design\" [size]=\"28\" />\n </div>\n <h3 class=\"empty-hero-title\">Start Designing Your Custom Report</h3>\n <p class=\"empty-hero-desc\">\n Drag controls from the left component palette, or jumpstart your design with pre-configured enterprise blueprints.\n </p>\n\n <div class=\"empty-hero-actions\">\n <button\n type=\"button\"\n class=\"empty-btn empty-btn-primary\"\n (click)=\"applyBlueprint('blueprint-audit-register')\"\n title=\"Load complete Audit Register blueprint with parameters and result grid\"\n >\n <rpt-icon name=\"table\" [size]=\"15\" />\n <span>Audit Register Blueprint</span>\n </button>\n <button\n type=\"button\"\n class=\"empty-btn empty-btn-secondary\"\n (click)=\"applyBlueprint('blueprint-kpi-summary')\"\n title=\"Load Executive KPI summary blueprint\"\n >\n <rpt-icon name=\"chart-bar\" [size]=\"15\" />\n <span>KPI Summary Blueprint</span>\n </button>\n <button\n type=\"button\"\n class=\"empty-btn empty-btn-outline\"\n (click)=\"addStarterComponent('date-range')\"\n title=\"Add a Date Range filter component\"\n >\n <rpt-icon name=\"calendar\" [size]=\"14\" />\n <span>+ Add Date Filter</span>\n </button>\n <button\n type=\"button\"\n class=\"empty-btn empty-btn-outline\"\n (click)=\"addStarterComponent('button')\"\n title=\"Add an Execute Action button\"\n >\n <rpt-icon name=\"play\" [size]=\"14\" />\n <span>+ Add Run Button</span>\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Live Result Grid Preview with Bways-Grid Features -->\n @if (definition().grid) {\n <div class=\"canvas-grid-box\">\n <div class=\"grid-preview-header\">\n <div style=\"display: flex; align-items: center; gap: 8px;\">\n <rpt-icon name=\"table\" [size]=\"15\" />\n <span class=\"grid-title\">Bways Grid ({{ definition().grid?.columns?.length ?? 0 }} columns)</span>\n @if (isExecutingGrid()) {\n <span class=\"grid-exec-badge is-loading\">Loading data from database...</span>\n } @else if (gridExecError()) {\n <span class=\"grid-exec-badge is-error\">{{ gridExecError() }}</span>\n } @else if (canvasGridData().length > 0) {\n <span class=\"grid-exec-badge is-success\">{{ canvasGridData().length }} rows loaded</span>\n }\n </div>\n <div style=\"display: flex; align-items: center; gap: 8px;\">\n <button\n type=\"button\"\n class=\"btn-canvas-run-grid\"\n (click)=\"executeCanvasReport($event)\"\n [disabled]=\"isExecutingGrid()\"\n title=\"Execute query and load data into Bways Grid\"\n >\n <rpt-icon name=\"play\" [size]=\"11\" />\n <span>{{ isExecutingGrid() ? 'Running...' : 'Run / Refresh Grid' }}</span>\n </button>\n <button\n type=\"button\"\n class=\"btn-canvas-edit-grid\"\n (click)=\"selectGridSettings($event)\"\n title=\"Configure columns and grid options\"\n >\n <rpt-icon name=\"settings\" [size]=\"11\" />\n <span>Configure</span>\n </button>\n <button\n type=\"button\"\n class=\"btn-remove-grid\"\n (click)=\"removeGrid($event)\"\n title=\"Remove Result Grid from this report\"\n >\n <rpt-icon name=\"trash\" [size]=\"13\" />\n </button>\n </div>\n </div>\n\n @if (getGridSummaryText(); as summary) {\n <div class=\"grid-layout-status-strip\">\n <div class=\"grid-status-info\">\n <rpt-icon name=\"table\" [size]=\"12\" />\n <span>{{ summary }}</span>\n </div>\n <div class=\"grid-status-sync\" [class.is-pulsing]=\"layoutJustUpdated()\">\n <rpt-icon name=\"check\" [size]=\"11\" />\n <span>Layout auto-persisted</span>\n </div>\n </div>\n }\n\n <!-- Live Bways Grid bridge -->\n <div class=\"canvas-live-grid-container\" (click)=\"$event.stopPropagation()\" style=\"position: relative; min-height: 250px;\">\n @if (isExecutingGrid()) {\n <div class=\"canvas-grid-loading-overlay\">\n <span class=\"status-dot dot-pulse\" style=\"width: 14px; height: 14px; color: #2563eb; background: #2563eb; border-radius: 50%;\"></span>\n <span class=\"grid-loading-title\">Executing stored procedure on SQL Server...</span>\n <span class=\"grid-loading-sub\">Calculating outstanding balances and ledger amounts. Please wait...</span>\n </div>\n }\n <rpt-grid-bridge\n [gridDefinition]=\"definition().grid!\"\n [data]=\"canvasGridData()\"\n (gridStateChange)=\"onCanvasGridStateChange($event)\"\n (columnDefsChange)=\"onCanvasColumnDefsChange($event)\"\n />\n </div>\n </div>\n } @else {\n <div class=\"canvas-add-grid-banner\" (click)=\"addResultGrid($event)\">\n <div class=\"add-grid-icon\">\n <rpt-icon name=\"table\" [size]=\"20\" />\n </div>\n <div class=\"add-grid-text\">\n <span class=\"add-grid-title\">+ Add Result Data Grid</span>\n <span class=\"add-grid-sub\">Attach an interactive data table or UltraGrid to display report execution output</span>\n </div>\n <button type=\"button\" class=\"btn-banner-add\">Add Grid</button>\n </div>\n }\n </div>\n </div>\n </div>", styles: [":host{display:block;height:100%;background-color:#f1f5f9;font-family:var(--rpt-font-family, inherit);box-sizing:border-box}.canvas-wrapper{display:flex;flex-direction:column;height:100%}.canvas-toolbar{display:flex;align-items:center;justify-content:space-between;min-height:42px;height:42px;padding:0 24px;background:var(--rpt-color-surface, #ffffff);border-bottom:1px solid var(--rpt-color-border, #cbd5e1);box-sizing:border-box;gap:12px;overflow-x:auto;scrollbar-width:none;font-family:var(--rpt-font-family, inherit);flex-shrink:0}.canvas-toolbar::-webkit-scrollbar{display:none}.canvas-info{display:flex;align-items:center;gap:8px;font-family:var(--rpt-font-family, inherit);font-size:var(--rpt-font-size-sm, .75rem);color:var(--rpt-color-text-muted, #64748b);white-space:nowrap;line-height:1}.info-label{color:var(--rpt-color-text-muted, #64748b);font-weight:500;font-size:var(--rpt-font-size-sm, .75rem)}.info-value{font-weight:600;color:var(--rpt-color-text, #0f172a);font-size:var(--rpt-font-size-sm, .75rem)}.info-divider{color:var(--rpt-color-border, #cbd5e1);margin:0 2px}.live-mode-badge{display:inline-flex;align-items:center;gap:4px;font-family:var(--rpt-font-family, inherit);font-size:var(--rpt-font-size-xs, .7rem);font-weight:600;color:#047857;background:#ecfdf5;border:1px solid #a7f3d0;padding:2px 8px;border-radius:9999px;line-height:1}.canvas-actions{display:flex;align-items:center;gap:8px;white-space:nowrap}.canvas-action-btn{display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 10px;font-family:var(--rpt-font-family, inherit);font-size:var(--rpt-font-size-sm, .75rem);font-weight:500;letter-spacing:-.01em;color:var(--rpt-color-text-secondary, #334155);background:var(--rpt-color-surface, #ffffff);border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius, 6px);cursor:pointer;line-height:1;white-space:nowrap;box-shadow:0 1px 2px #0000000a;transition:all var(--rpt-transition-speed, .15s) ease}.canvas-action-btn:hover{background:var(--rpt-color-surface-hover, #f8fafc);border-color:var(--rpt-color-border-hover, #94a3b8);color:var(--rpt-color-text, #0f172a);box-shadow:0 2px 4px #0000000f}.canvas-action-btn.btn-grid-configured{background:#eff6ff;border-color:#bfdbfe;color:#2563eb;font-weight:600}.canvas-action-btn.btn-grid-configured:hover{background:#dbeafe;color:#1d4ed8;border-color:#93c5fd}.canvas-action-btn.btn-grid-add{background:#f0fdf4;border-color:#bbf7d0;color:#16a34a;font-weight:600}.canvas-action-btn.btn-grid-add:hover{background:#dcfce7;color:#15803d;border-color:#86efac}.canvas-action-btn:active{transform:translateY(1px)}.canvas-scroll-area{flex:1;overflow-y:auto;padding:16px}.canvas-artboard{width:100%;max-width:1400px;margin:0 auto;background:#fff;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius-lg, 8px);box-shadow:0 4px 6px -1px #0000000d;min-height:480px;box-sizing:border-box}@media (max-width: 900px){.canvas-toolbar{min-height:36px;height:36px;padding:0 10px;gap:6px}.live-mode-badge{display:none}.canvas-info{font-size:.75rem;gap:5px}.canvas-action-btn{padding:0 8px;font-size:.725rem;height:28px;gap:4px}.canvas-scroll-area{padding:10px}.canvas-artboard{min-height:360px}}@media (max-width: 600px){.canvas-toolbar{padding:0 8px;gap:4px}.canvas-info .info-label,.canvas-action-btn span{display:none}.canvas-action-btn{padding:0 6px;min-width:28px;justify-content:center}}.canvas-report-header{margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid var(--rpt-color-border, #e2e8f0)}.canvas-report-title{font-size:var(--rpt-font-size-xl, 1.5rem);font-weight:700;color:var(--rpt-color-text, #0f172a);margin:0 0 6px}.canvas-report-desc{font-size:var(--rpt-font-size-base, .875rem);color:var(--rpt-color-text-muted, #64748b);margin:0}.canvas-grid{display:grid;width:100%;min-height:180px;align-items:start;border-radius:6px;transition:all var(--rpt-transition-speed, .15s) ease}.canvas-component-card{container-type:inline-size;background:#fff;border:1.5px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius, 6px);box-shadow:var(--rpt-box-shadow, 0 1px 2px rgba(0, 0, 0, .05));box-sizing:border-box;transition:all var(--rpt-transition-speed, .15s) ease;cursor:pointer;position:relative;overflow:visible;display:flex;flex-direction:column;min-height:104px;z-index:1}.canvas-component-card:hover{border-color:var(--rpt-color-primary, #2563eb);box-shadow:0 4px 6px -1px #2563eb1a;z-index:2}.canvas-component-card.is-selected{border-color:var(--rpt-color-primary, #2563eb);box-shadow:0 0 0 2px #2563eb40;z-index:10}.canvas-component-card:focus-within,.canvas-component-card:has(.rpt-combobox-menu),.canvas-component-card:has(.is-open){z-index:100}.canvas-component-card.is-resizing{-webkit-user-select:none;user-select:none;border-color:var(--rpt-color-primary, #2563eb);box-shadow:0 0 0 2px #2563eb59;z-index:30;transition:none!important}.resize-handle{position:absolute;z-index:20;touch-action:none;opacity:0;pointer-events:none;transition:opacity .15s ease}.canvas-component-card.is-selected .resize-handle{opacity:1;pointer-events:auto}.resize-handle.handle-e{top:0;right:0;width:6px;height:100%;cursor:ew-resize;display:flex;align-items:center;justify-content:center}.resize-handle.handle-e:after{content:\"\";width:3px;height:24px;background:#cbd5e1;border-radius:2px;transition:background .15s ease}.resize-handle.handle-e:hover:after,.canvas-component-card.is-selected .resize-handle.handle-e:after{background:var(--rpt-color-primary, #2563eb)}.resize-handle.handle-s{bottom:0;left:0;height:6px;width:100%;cursor:ns-resize;display:flex;align-items:center;justify-content:center}.resize-handle.handle-s:after{content:\"\";height:3px;width:24px;background:#cbd5e1;border-radius:2px;transition:background .15s ease}.resize-handle.handle-s:hover:after,.canvas-component-card.is-selected .resize-handle.handle-s:after{background:var(--rpt-color-primary, #2563eb)}.resize-handle.handle-se{bottom:0;right:0;width:12px;height:12px;cursor:nwse-resize;display:flex;align-items:flex-end;justify-content:flex-end;padding:2px}.resize-handle.handle-se:after{content:\"\";width:5px;height:5px;border-right:2px solid #cbd5e1;border-bottom:2px solid #cbd5e1;transition:border-color .15s ease}.resize-handle.handle-se:hover:after,.canvas-component-card.is-selected .resize-handle.handle-se:after{border-color:var(--rpt-color-primary, #2563eb)}.resize-dimension-tooltip{position:absolute;top:38px;right:8px;background:#0f172ae6;color:#fff;font-size:.7rem;font-weight:600;padding:3px 8px;border-radius:4px;white-space:nowrap;pointer-events:none;z-index:50;box-shadow:0 2px 4px #0003}.card-header{display:flex;align-items:center;justify-content:space-between;gap:6px;padding:5px 8px;background:var(--rpt-color-surface, #f8fafc);border-bottom:1px solid var(--rpt-color-border, #e2e8f0);border-top-left-radius:var(--rpt-border-radius, 6px);border-top-right-radius:var(--rpt-border-radius, 6px);min-height:32px;box-sizing:border-box;overflow:hidden;position:relative}.canvas-component-card.is-selected .card-header{overflow:visible}.card-title-group{display:flex;align-items:center;gap:5px;cursor:grab;min-width:0;flex:1;overflow:hidden}.drag-handle-dots{color:var(--rpt-color-text-muted, #94a3b8);font-size:.85rem;letter-spacing:-2px;flex-shrink:0;-webkit-user-select:none;user-select:none}.comp-title{font-size:.75rem;font-weight:600;color:var(--rpt-color-text, #0f172a);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;-webkit-user-select:none;user-select:none}.card-actions{display:flex;align-items:center;gap:4px;flex-shrink:0;margin-left:auto}.new-row-pill-static{font-size:.65rem;font-weight:600;color:#7c3aed;background:#f5f3ff;border:1px solid #ddd6fe;padding:1px 5px;border-radius:9999px;white-space:nowrap;line-height:1;flex-shrink:0}.span-pill-static{display:inline-flex;align-items:center;justify-content:center;font-size:.65rem;font-weight:600;color:var(--rpt-color-text-muted, #64748b);background:var(--rpt-color-surface, #f1f5f9);border:1px solid var(--rpt-color-border, #e2e8f0);padding:1px 6px;border-radius:9999px;line-height:1.2;white-space:nowrap;-webkit-user-select:none;user-select:none}.card-quick-controls{display:none;align-items:center;gap:3px;flex-shrink:0}.canvas-component-card:hover .span-pill-static,.canvas-component-card.is-selected .span-pill-static,.canvas-component-card:hover .new-row-pill-static,.canvas-component-card.is-selected .new-row-pill-static{display:none}.canvas-component-card:hover .card-quick-controls,.canvas-component-card.is-selected .card-quick-controls{display:inline-flex}.canvas-component-card.is-narrow .card-header{padding:4px 6px;gap:3px}.canvas-component-card.is-narrow:hover .comp-title,.canvas-component-card.is-narrow.is-selected .comp-title,.canvas-component-card.is-narrow:hover .drag-handle-dots,.canvas-component-card.is-narrow.is-selected .drag-handle-dots{display:none}.canvas-component-card.is-narrow .card-quick-controls{gap:2px}.canvas-component-card.is-narrow .span-control-group{height:18px}.canvas-component-card.is-narrow .span-btn{width:14px;height:16px}.canvas-component-card.is-narrow .span-badge{font-size:.6rem;padding:0 2px}.canvas-component-card.is-narrow .span-total{display:none}.canvas-component-card.is-narrow .card-action-btn{width:18px;height:18px}@container (max-width: 250px){.card-header{padding:4px 6px;gap:3px}.canvas-component-card:hover .comp-title,.canvas-component-card.is-selected .comp-title,.canvas-component-card:hover .drag-handle-dots,.canvas-component-card.is-selected .drag-handle-dots,.span-total{display:none}.card-quick-controls{gap:2px}.card-action-btn{width:18px;height:18px}.span-control-group{height:18px}.span-btn{width:14px;height:16px}.span-badge{font-size:.6rem;padding:0 2px}}@container (max-width: 160px){.canvas-component-card.is-selected .card-quick-controls{position:absolute;top:-30px;right:0;background:#fff;border:1.5px solid var(--rpt-color-primary, #2563eb);border-radius:6px;padding:2px 4px;gap:3px;box-shadow:0 4px 12px #2563eb2e,0 2px 4px #00000014;z-index:120;white-space:nowrap}}@media (max-width: 900px){.canvas-component-card:hover .comp-title,.canvas-component-card.is-selected .comp-title,.canvas-component-card:hover .drag-handle-dots,.canvas-component-card.is-selected .drag-handle-dots,.canvas-component-card .span-total{display:none}.canvas-component-card.is-selected .card-header{overflow:visible}.canvas-component-card.is-selected .card-quick-controls{position:absolute;top:-30px;right:0;background:#fff;border:1.5px solid var(--rpt-color-primary, #2563eb);border-radius:6px;padding:2px 4px;gap:3px;box-shadow:0 4px 12px #2563eb2e,0 2px 4px #00000014;z-index:120;white-space:nowrap}}.span-control-group{display:inline-flex;align-items:center;background:#fff;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:4px;height:20px;padding:0 1px;white-space:nowrap;flex-shrink:0}.span-btn{width:16px;height:18px;display:inline-flex;align-items:center;justify-content:center;border:none;background:transparent;padding:0;color:var(--rpt-color-text-muted, #64748b);cursor:pointer;border-radius:2px;transition:all .12s ease}.span-btn:hover:not(:disabled){background:var(--rpt-color-surface-hover, #f1f5f9);color:var(--rpt-color-primary, #2563eb)}.span-btn:disabled{opacity:.3;cursor:not-allowed}.span-badge{font-size:.65rem;font-weight:600;color:var(--rpt-color-text, #0f172a);padding:0 3px;white-space:nowrap;-webkit-user-select:none;user-select:none;line-height:1}.span-total{font-size:.6rem;color:var(--rpt-color-text-muted, #64748b);font-weight:500}.card-action-btn{width:20px;height:20px;border:none;background:transparent;border-radius:4px;display:inline-flex;align-items:center;justify-content:center;padding:0;cursor:pointer;color:var(--rpt-color-text-muted, #64748b);transition:all .12s ease;flex-shrink:0}.card-action-btn:hover{background:#e2e8f0;color:var(--rpt-color-text, #0f172a)}.card-action-btn.btn-active-toggle{background:#7c3aed;color:#fff;border-color:#7c3aed}.card-action-btn.btn-active-toggle:hover{background:#6d28d9;color:#fff}.btn-delete:hover{background:#fee2e2;color:#ef4444}.card-body{padding:8px 10px 14px;flex:1;display:flex;flex-direction:column;min-height:0;min-width:0;max-width:100%;box-sizing:border-box;overflow:visible}.mock-input-field{display:flex;flex-direction:column;gap:4px;flex:1;height:100%;min-height:0}.mock-label{font-size:.7rem;font-weight:600;color:var(--rpt-color-text-muted, #475569);flex-shrink:0}.mock-input-box,.mock-select-box{border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:6px;padding:6px 12px;font-size:var(--rpt-font-size-base, .875rem);color:var(--rpt-color-text-muted, #94a3b8);background:#fff;flex:1;min-height:38px;height:100%;display:flex;align-items:center;justify-content:space-between;box-sizing:border-box}.mock-checkbox-field{display:flex;align-items:center;gap:6px;flex:1;height:100%;min-height:0;padding-top:14px}.mock-check-box{width:18px;height:18px;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:.65rem;color:var(--rpt-color-primary, #2563eb);flex-shrink:0}.mock-btn-wrapper{display:flex;flex:1;height:100%;min-height:38px;align-items:center;justify-content:flex-start;width:100%;min-width:0;box-sizing:border-box}.mock-btn{border-radius:var(--rpt-border-radius, 6px);padding:0 16px;height:36px;font-size:var(--rpt-font-size-base, .875rem);font-weight:500;border:1px solid transparent;cursor:pointer;width:auto;max-width:100%;display:inline-flex;align-items:center;justify-content:center;gap:6px;box-sizing:border-box;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:all .15s ease}.mock-btn.mock-btn-full{width:100%}.canvas-component-card.is-narrow .mock-btn{padding:0 6px;font-size:.775rem;width:100%;height:34px}.mock-kpi-field{display:flex;flex-direction:column;justify-content:space-between;flex:1;height:100%;min-height:0}.mock-kpi-header{display:flex;align-items:center;justify-content:space-between}.mock-kpi-value{font-size:1.25rem;font-weight:700;color:var(--rpt-color-text, #0f172a);margin:2px 0}.mock-kpi-footer{display:flex;align-items:center;gap:6px;font-size:.65rem}.mock-kpi-trend{color:#16a34a;font-weight:600}.mock-kpi-sub{color:#64748b}.mock-chart-field{display:flex;flex-direction:column;flex:1;height:100%;min-height:140px}.mock-chart-bars{display:flex;align-items:flex-end;justify-content:space-around;gap:16px;flex:1;min-height:100px;padding-top:12px;box-sizing:border-box}.mock-chart-col{flex:1;display:flex;flex-direction:column;align-items:center;height:100%;min-width:60px;max-width:140px}.mock-chart-bar{width:38px;max-width:44px;margin:0 auto;background:var(--rpt-color-primary, #2563eb);border-radius:6px 6px 0 0;opacity:.85;min-height:10px;transition:height .2s ease}.mock-bar-lbl{font-size:.6875rem;color:#64748b;margin-top:6px;text-align:center;white-space:normal;line-height:1.2}.btn-primary{background:var(--rpt-color-primary, #2563eb);color:#fff}.btn-secondary{background:#f1f5f9;color:#0f172a;border:1px solid #cbd5e1}.btn-outline{background:transparent;color:#2563eb;border:1px solid #2563eb}.btn-danger{background:#ef4444;color:#fff}.mock-generic-box{border:1px dashed #cbd5e1;border-radius:4px;padding:8px;font-size:.75rem;text-align:center;color:#64748b}.canvas-drag-placeholder{background:#2563eb0f;border:2px dashed var(--rpt-color-primary, #2563eb);border-radius:6px;min-height:80px;box-sizing:border-box;transition:transform .15s ease}.canvas-drag-preview,::ng-deep .canvas-drag-preview{background:#fff;border:2px solid var(--rpt-color-primary, #2563eb);border-radius:6px;padding:8px 14px;display:inline-flex;align-items:center;gap:8px;font-family:var(--rpt-font-family, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif)!important;font-size:.8rem;font-weight:600;color:var(--rpt-color-text, #0f172a);box-shadow:0 12px 24px -4px #00000040;z-index:10000}.preview-title,::ng-deep .preview-title{font-family:inherit!important;max-width:160px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.preview-span,::ng-deep .preview-span{font-family:inherit!important;font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border:1px solid #bfdbfe;padding:1px 6px;border-radius:9999px}.canvas-empty-state{grid-column:1 / -1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:56px 28px;border:2px dashed var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius-xl, 12px);background:var(--rpt-color-surface, #ffffff);text-align:center;box-shadow:var(--rpt-box-shadow-xs, 0 1px 2px 0 rgba(0, 0, 0, .04));transition:border-color var(--rpt-transition-speed, .15s) ease}.canvas-empty-state:hover{border-color:var(--rpt-color-primary, #2563eb)}.empty-hero-badge{width:56px;height:56px;border-radius:var(--rpt-border-radius-xl, 12px);background:var(--rpt-color-primary-light, #eff6ff);color:var(--rpt-color-primary, #2563eb);display:flex;align-items:center;justify-content:center;margin-bottom:16px;box-shadow:0 2px 8px #2563eb1f}.empty-hero-title{font-size:var(--rpt-font-size-lg, 1.125rem);font-weight:700;color:var(--rpt-color-text, #0f172a);margin:0 0 8px;letter-spacing:-.01em}.empty-hero-desc{font-size:var(--rpt-font-size-base, .875rem);color:var(--rpt-color-text-muted, #64748b);max-width:520px;line-height:1.5;margin:0 0 24px}.empty-hero-actions{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:10px}.empty-btn{display:inline-flex;align-items:center;gap:7px;padding:8px 16px;font-size:var(--rpt-font-size-sm, .75rem);font-weight:600;border-radius:var(--rpt-border-radius, 6px);cursor:pointer;transition:all var(--rpt-transition-speed, .15s) ease;font-family:inherit}.empty-btn-primary{background:var(--rpt-color-primary, #2563eb);color:#fff;border:1px solid var(--rpt-color-primary, #2563eb);box-shadow:0 1px 3px #2563eb33}.empty-btn-primary:hover{background:var(--rpt-color-primary-hover, #1d4ed8);border-color:var(--rpt-color-primary-hover, #1d4ed8);transform:translateY(-1px)}.empty-btn-secondary{background:#f8fafc;color:#334155;border:1px solid var(--rpt-color-border, #cbd5e1)}.empty-btn-secondary:hover{background:#f1f5f9;color:#0f172a;border-color:#94a3b8;transform:translateY(-1px)}.empty-btn-outline{background:#fff;color:var(--rpt-color-primary, #2563eb);border:1px solid #bfdbfe}.empty-btn-outline:hover{background:#eff6ff;border-color:var(--rpt-color-primary, #2563eb);transform:translateY(-1px)}.canvas-active-input{width:100%;height:32px;padding:4px 8px;font-size:.8125rem;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:4px;outline:none;box-sizing:border-box;background:#fff;color:#0f172a;transition:border-color .15s ease}.canvas-active-input:focus{border-color:var(--rpt-color-primary, #2563eb)}.canvas-active-checkbox-lbl{display:inline-flex;align-items:center;gap:6px;cursor:pointer}.canvas-live-control-wrapper{width:100%;max-width:100%;min-width:0;position:relative;z-index:1;box-sizing:border-box}.canvas-grid-box{margin-top:24px;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:8px;background:#fff;box-shadow:0 1px 3px #0000000d;overflow:hidden}.grid-preview-header{display:flex;align-items:center;justify-content:space-between;padding:10px 16px;background:var(--rpt-color-surface, #f8fafc);border-bottom:1px solid var(--rpt-color-border, #cbd5e1);font-size:.8rem;font-weight:600;color:var(--rpt-color-text, #0f172a)}.grid-title{font-weight:700}.grid-exec-badge{font-size:.6875rem;font-weight:600;padding:2px 8px;border-radius:10px}.grid-exec-badge.is-loading{background:#eff6ff;color:#2563eb}.grid-exec-badge.is-error{background:#fee2e2;color:#ef4444}.grid-exec-badge.is-success{background:#ecfdf5;color:#059669}.btn-canvas-run-grid{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;font-size:.72rem;font-weight:600;color:#fff;background:var(--rpt-color-primary, #2563eb);border:none;border-radius:4px;cursor:pointer;transition:background .15s ease}.btn-canvas-run-grid:hover:not(:disabled){background:#1d4ed8}.btn-canvas-run-grid:disabled{opacity:.6;cursor:not-allowed}.btn-canvas-edit-grid{display:inline-flex;align-items:center;gap:4px;padding:4px 8px;font-size:.72rem;font-weight:500;color:#334155;background:#fff;border:1px solid #cbd5e1;border-radius:4px;cursor:pointer;transition:all .12s ease}.btn-canvas-edit-grid:hover{background:#f1f5f9}.canvas-live-grid-container{width:100%;min-height:480px;background:#fff}.live-mode-badge{display:inline-flex;align-items:center;gap:4px;font-size:.6875rem;font-weight:700;color:#047857;background:#ecfdf5;border:1px solid #a7f3d0;padding:1px 8px;border-radius:9999px}.canvas-component-card.is-field-highlighted{border-color:#f59e0b!important;box-shadow:0 0 0 3px #f59e0b59!important;animation:rpt-pulse-border 1s ease-in-out infinite alternate}@keyframes rpt-pulse-border{0%{box-shadow:0 0 0 2px #f59e0b40}to{box-shadow:0 0 0 5px #f59e0b80}}.grid-layout-status-strip{display:flex;align-items:center;justify-content:space-between;padding:6px 16px;background:#f1f5f9;border-bottom:1px solid var(--rpt-color-border, #cbd5e1);font-size:.6875rem;color:#475569}.grid-status-info{display:inline-flex;align-items:center;gap:6px;font-weight:600}.grid-status-sync{display:inline-flex;align-items:center;gap:4px;color:#059669;font-weight:600;transition:transform .2s ease}.grid-status-sync.is-pulsing{transform:scale(1.08);color:#047857}.canvas-actions{display:flex;align-items:center;gap:8px}.btn-grid-add{background:#f0fdf4;border-color:#bbf7d0;color:#16a34a}.btn-grid-add:hover{background:#dcfce7;border-color:#86efac;color:#15803d}.btn-remove-grid{display:inline-flex;align-items:center;justify-content:center;padding:4px;border:none;background:transparent;color:#94a3b8;border-radius:4px;cursor:pointer;transition:all .15s ease}.btn-remove-grid:hover{background:#fee2e2;color:#ef4444}.canvas-add-grid-banner{margin-top:24px;border:2px dashed #cbd5e1;border-radius:8px;background:#fff;padding:16px 20px;display:flex;align-items:center;gap:16px;cursor:pointer;transition:all .15s ease}.canvas-add-grid-banner:hover{border-color:#2563eb;background:#f8fafc;box-shadow:0 2px 8px #2563eb14}.add-grid-icon{width:40px;height:40px;border-radius:8px;background:#eff6ff;color:#2563eb;display:flex;align-items:center;justify-content:center;flex-shrink:0}.add-grid-text{display:flex;flex-direction:column;gap:2px;flex:1}.add-grid-title{font-size:.875rem;font-weight:600;color:#0f172a}.add-grid-sub{font-size:.75rem;color:#64748b}.btn-banner-add{padding:6px 14px;font-size:.75rem;font-weight:600;color:#2563eb;background:#eff6ff;border:1px solid #bfdbfe;border-radius:6px;cursor:pointer;white-space:nowrap;transition:all .15s ease}.canvas-add-grid-banner:hover .btn-banner-add{background:#2563eb;color:#fff;border-color:#2563eb}.canvas-grid-loading-overlay{position:absolute;inset:0;background:#ffffffe6;backdrop-filter:blur(2px);z-index:10;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;font-family:inherit;border-radius:6px}.grid-loading-title{font-weight:600;font-size:.95rem;color:#1e293b}.grid-loading-sub{font-size:.8rem;color:#64748b}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$2.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i1$2.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i1$2.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "directive", type: i1$2.CdkDragPreview, selector: "ng-template[cdkDragPreview]", inputs: ["data", "matchSize"] }, { kind: "directive", type: i1$2.CdkDragPlaceholder, selector: "ng-template[cdkDragPlaceholder]", inputs: ["data"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.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: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: RptIconComponent, selector: "rpt-icon", inputs: ["name", "size", "color", "strokeWidth"] }, { kind: "component", type: RptDropdownComponent, selector: "rpt-dropdown", inputs: ["config", "control", "parentForm", "showStatus"] }, { kind: "component", type: RptDateRangeComponent, selector: "rpt-date-range", inputs: ["config", "control"] }, { kind: "component", type: ReportGridBridgeComponent, selector: "rpt-grid-bridge", inputs: ["gridDefinition", "data"], outputs: ["gridStateChange", "columnDefsChange", "rowClick", "selectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
9369
9748
|
}
|
|
9370
9749
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DesignCanvasComponent, decorators: [{
|
|
9371
9750
|
type: Component,
|
|
@@ -9377,7 +9756,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
9377
9756
|
RptDropdownComponent,
|
|
9378
9757
|
RptDateRangeComponent,
|
|
9379
9758
|
ReportGridBridgeComponent,
|
|
9380
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, template: " <div class=\"canvas-wrapper\" (click)=\"onCanvasBackgroundClick($event)\">\n <!-- Canvas Status / Quick Controls -->\n <div class=\"canvas-toolbar\">\n <div class=\"canvas-info\">\n <span class=\"info-label\">Grid Layout:</span>\n <span class=\"info-value\">{{ totalColumns() }} Columns</span>\n <span class=\"info-divider\">\u2022</span>\n <span class=\"info-label\">Components:</span>\n <span class=\"info-value\">{{ components().length }}</span>\n <span class=\"info-divider\">\u2022</span>\n <span class=\"live-mode-badge\" title=\"Interactive canvas: Values typed here run live queries and test dropdowns.\">\n \u26A1 Live Interactive Mode\n </span>\n </div>\n\n <div class=\"canvas-actions\">\n @if (!definition().grid) {\n <button\n type=\"button\"\n class=\"canvas-action-btn btn-grid-add\"\n (click)=\"addResultGrid($event)\"\n title=\"Add a Result Grid to this report\"\n >\n <rpt-icon name=\"table\" [size]=\"14\" />\n <span>+ Add Grid</span>\n </button>\n } @else {\n <button\n type=\"button\"\n class=\"canvas-action-btn btn-grid-configured\"\n (click)=\"selectGridSettings($event)\"\n title=\"Configure Result Data Grid\"\n >\n <rpt-icon name=\"table\" [size]=\"14\" />\n <span>Grid ({{ definition().grid?.columns?.length ?? 0 }})</span>\n </button>\n }\n\n <button\n type=\"button\"\n class=\"canvas-action-btn\"\n (click)=\"selectReportSettings($event)\"\n title=\"Configure Page Layout & Grid Settings\"\n >\n <rpt-icon name=\"settings\" [size]=\"14\" />\n <span>Report Settings</span>\n </button>\n </div>\n </div>\n\n <!-- Canvas Drop Area -->\n <div class=\"canvas-scroll-area\">\n <div\n class=\"canvas-artboard\"\n [style.padding.px]=\"definition().layout.padding ?? 24\"\n >\n <!-- Report Header Preview -->\n <div class=\"canvas-report-header\">\n <h2 class=\"canvas-report-title\">{{ definition().name || 'Untitled Report' }}</h2>\n @if (definition().description) {\n <p class=\"canvas-report-desc\">{{ definition().description }}</p>\n }\n </div>\n\n <!-- Component Drop List Grid with 2D mixed orientation and data binding -->\n <div\n cdkDropList\n id=\"designer-canvas-drop-list\"\n class=\"canvas-grid\"\n cdkDropListOrientation=\"mixed\"\n [cdkDropListData]=\"sortedComponents()\"\n [style.grid-template-columns]=\"gridTemplateColumns()\"\n [style.gap.px]=\"definition().layout.gap ?? 16\"\n (cdkDropListDropped)=\"onDrop($event)\"\n >\n @for (comp of sortedComponents(); track comp.id; let idx = $index) {\n <div\n cdkDrag\n [cdkDragData]=\"comp\"\n [cdkDragDisabled]=\"isAnyResizing()\"\n class=\"canvas-component-card\"\n [class.is-selected]=\"isSelected(comp.id)\"\n [class.is-resizing]=\"resizingCompId() === comp.id\"\n [class.is-field-highlighted]=\"isFieldHighlighted(comp)\"\n [class.is-narrow]=\"getSpan(comp) <= 4\"\n [class.is-compact]=\"getSpan(comp) <= 2\"\n [style.grid-column]=\"getGridColumn(comp)\"\n [style.grid-row]=\"getGridRow(comp)\"\n [style.width]=\"comp.layout?.width || 'auto'\"\n [style.height]=\"getComponentHeight(comp)\"\n [style.min-height]=\"getComponentMinHeight(comp)\"\n (click)=\"onSelect(comp.id, $event)\"\n >\n <!-- Card Header with Drag Handle & Actions -->\n <div class=\"card-header\">\n <div class=\"card-title-group\" cdkDragHandle title=\"Drag to reorder\">\n <span class=\"drag-handle-dots\">\u22EE\u22EE</span>\n <rpt-icon [name]=\"getComponentIconName(comp.type)\" [size]=\"14\" />\n <span class=\"comp-title\" [title]=\"comp.label || comp.key || comp.type\">\n {{ comp.label || comp.key || comp.type }}\n </span>\n </div>\n\n <div class=\"card-actions\">\n <!-- Static span pill & new row / start col pill when not hovered / not selected -->\n @if (comp.layout?.column; as col) {\n <span class=\"new-row-pill-static\" title=\"Starts at Column {{ col }}\">Col {{ col }}</span>\n } @else if (comp.layout?.newRow) {\n <span class=\"new-row-pill-static\" title=\"Starts on separate row\">\u21B3 Row</span>\n }\n <span\n class=\"span-pill-static\"\n title=\"Column span: {{ getSpan(comp) }} of {{ totalColumns() }} columns\"\n >\n {{ getSpan(comp) }}/{{ totalColumns() }}\n </span>\n\n <!-- Quick Action Stepper & Buttons (revealed on hover or selection) -->\n <div class=\"card-quick-controls\">\n <button\n type=\"button\"\n class=\"card-action-btn\"\n [class.btn-active-toggle]=\"comp.layout?.newRow\"\n (click)=\"toggleNewRow(comp, $event)\"\n title=\"{{ comp.layout?.newRow ? 'Starts on new row (Click to place inline)' : 'Start on separate row (Row Break)' }}\"\n >\n <rpt-icon name=\"corner-down-right\" [size]=\"11\" />\n </button>\n\n <div class=\"span-control-group\" title=\"Adjust Column Span\">\n <button\n type=\"button\"\n class=\"span-btn\"\n [disabled]=\"getSpan(comp) <= 1\"\n (click)=\"adjustSpan(comp, -1, $event)\"\n title=\"Shrink Column Span\"\n >\n <rpt-icon name=\"minus\" [size]=\"9\" />\n </button>\n <span class=\"span-badge\" [title]=\"'Column span: ' + getSpan(comp) + ' of ' + totalColumns()\">\n <span class=\"span-num\">{{ getSpan(comp) }}</span><span class=\"span-total\">/{{ totalColumns() }}</span>\n </span>\n <button\n type=\"button\"\n class=\"span-btn\"\n [disabled]=\"getSpan(comp) >= totalColumns()\"\n (click)=\"adjustSpan(comp, 1, $event)\"\n title=\"Expand Column Span\"\n >\n <rpt-icon name=\"plus\" [size]=\"9\" />\n </button>\n </div>\n\n <button\n type=\"button\"\n class=\"card-action-btn\"\n (click)=\"onDuplicate(comp.id, $event)\"\n title=\"Duplicate component\"\n >\n <rpt-icon name=\"copy\" [size]=\"12\" />\n </button>\n <button\n type=\"button\"\n class=\"card-action-btn btn-delete\"\n (click)=\"onDelete(comp.id, $event)\"\n title=\"Remove component\"\n >\n <rpt-icon name=\"trash\" [size]=\"12\" />\n </button>\n </div>\n </div>\n </div>\n\n <!-- Card Body Visual Preview -->\n <div class=\"card-body\">\n\n @switch (comp.type) {\n @case ('input') {\n <div class=\"mock-input-field\" (click)=\"onControlInteract(comp, $event)\">\n <span class=\"mock-label\">{{ comp.label || 'Input' }}</span>\n @if (getCanvasControl(comp); as ctrl) {\n <input\n type=\"text\"\n class=\"canvas-active-input\"\n [placeholder]=\"comp.properties?.['placeholder'] || 'Enter text...'\"\n [formControl]=\"ctrl\"\n (focus)=\"onControlInteract(comp, $event)\"\n />\n } @else {\n <div class=\"mock-input-box\">{{ comp.properties?.['placeholder'] || 'Enter text...' }}</div>\n }\n </div>\n }\n @case ('dropdown') {\n <div class=\"canvas-live-control-wrapper\" (click)=\"onControlInteract(comp, $event)\">\n <rpt-dropdown\n [config]=\"comp\"\n [showStatus]=\"true\"\n [parentForm]=\"canvasForm()\"\n [control]=\"getCanvasControl(comp)\"\n />\n </div>\n }\n @case ('date') {\n <div class=\"mock-input-field\" (click)=\"onControlInteract(comp, $event)\">\n <span class=\"mock-label\">{{ comp.label || 'Date' }}</span>\n @if (getCanvasControl(comp); as ctrl) {\n <input\n type=\"date\"\n class=\"canvas-active-input\"\n [formControl]=\"ctrl\"\n (focus)=\"onControlInteract(comp, $event)\"\n />\n } @else {\n <div class=\"mock-select-box\">\n <span>YYYY-MM-DD</span>\n <rpt-icon name=\"calendar\" [size]=\"12\" />\n </div>\n }\n </div>\n }\n @case ('date-range') {\n <div class=\"canvas-live-control-wrapper\" (click)=\"onControlInteract(comp, $event)\">\n <rpt-date-range\n [config]=\"comp\"\n [control]=\"getCanvasControl(comp)\"\n />\n </div>\n }\n @case ('number') {\n <div class=\"mock-input-field\" (click)=\"onControlInteract(comp, $event)\">\n <span class=\"mock-label\">{{ comp.label || 'Number' }}</span>\n @if (getCanvasControl(comp); as ctrl) {\n <input\n type=\"number\"\n class=\"canvas-active-input\"\n [formControl]=\"ctrl\"\n (focus)=\"onControlInteract(comp, $event)\"\n />\n } @else {\n <div class=\"mock-input-box\">\n {{ comp.properties?.['prefix'] || '' }} 0.00 {{ comp.properties?.['suffix'] || '' }}\n </div>\n }\n </div>\n }\n @case ('checkbox') {\n <div class=\"mock-checkbox-field\" (click)=\"onControlInteract(comp, $event)\">\n @if (getCanvasControl(comp); as ctrl) {\n <label class=\"canvas-active-checkbox-lbl\">\n <input type=\"checkbox\" [formControl]=\"ctrl\" (focus)=\"onControlInteract(comp, $event)\" />\n <span class=\"mock-label\">{{ comp.label || 'Checkbox' }}</span>\n </label>\n } @else {\n <span class=\"mock-check-box\">\n <rpt-icon name=\"check\" [size]=\"10\" />\n </span>\n <span class=\"mock-label\">{{ comp.label || 'Checkbox' }}</span>\n }\n </div>\n }\n @case ('button') {\n <div class=\"mock-btn-wrapper\">\n <button\n type=\"button\"\n class=\"mock-btn\"\n [class]=\"'btn-' + (comp.properties?.['variant'] || 'primary')\"\n [class.mock-btn-full]=\"comp.properties?.['fullWidth'] === true || comp.layout?.width === '100%'\"\n [style.height]=\"getCustomBtnHeight(comp)\"\n [style.width]=\"getCustomBtnWidth(comp)\"\n (click)=\"onCanvasButtonClick(comp, $event)\"\n >\n @if (getCustomBtnIcon(comp); as btnIcon) {\n <rpt-icon [name]=\"btnIcon\" [size]=\"15\" />\n }\n <span>{{ comp.label || 'Button' }}</span>\n </button>\n </div>\n }\n @case ('kpi-card') {\n <div class=\"mock-kpi-field\">\n <div class=\"mock-kpi-header\">\n <span class=\"mock-label\">{{ comp.label || 'KPI Metric' }}</span>\n <rpt-icon name=\"kpi\" [size]=\"14\" />\n </div>\n <div class=\"mock-kpi-value\">{{ comp.properties?.['value'] || '$128,450' }}</div>\n <div class=\"mock-kpi-footer\">\n <span class=\"mock-kpi-trend\">+12.5%</span>\n <span class=\"mock-kpi-sub\">vs last period</span>\n </div>\n </div>\n }\n @case ('chart') {\n <div class=\"mock-chart-field\">\n <div class=\"mock-chart-header\">\n <span class=\"mock-label\">{{ comp.label || 'Quarterly Sales Revenue ($000)' }}</span>\n </div>\n <div class=\"mock-chart-bars\">\n <div class=\"mock-chart-col\">\n <div class=\"mock-chart-bar\" style=\"height: 44%\"></div>\n <span class=\"mock-bar-lbl\">Q1 Jan-Mar</span>\n </div>\n <div class=\"mock-chart-col\">\n <div class=\"mock-chart-bar\" style=\"height: 61%\"></div>\n <span class=\"mock-bar-lbl\">Q2 Apr-Jun</span>\n </div>\n <div class=\"mock-chart-col\">\n <div class=\"mock-chart-bar\" style=\"height: 79%\"></div>\n <span class=\"mock-bar-lbl\">Q3 Jul-Sep</span>\n </div>\n <div class=\"mock-chart-col\">\n <div class=\"mock-chart-bar\" style=\"height: 100%\"></div>\n <span class=\"mock-bar-lbl\">Q4 Oct-Dec</span>\n </div>\n </div>\n </div>\n }\n @default {\n <div class=\"mock-generic-box\">\n <span>{{ comp.type }}</span>\n </div>\n }\n }\n </div>\n\n <!-- Resizing Handles (East, South, South-East) -->\n <div\n class=\"resize-handle handle-e\"\n (pointerdown)=\"startResize($event, comp, 'e')\"\n (dblclick)=\"resetDimensions(comp, $event)\"\n title=\"Drag to resize column width \u2022 Double-click to reset\"\n ></div>\n <div\n class=\"resize-handle handle-s\"\n (pointerdown)=\"startResize($event, comp, 's')\"\n (dblclick)=\"resetDimensions(comp, $event)\"\n title=\"Drag to resize height \u2022 Double-click to reset\"\n ></div>\n <div\n class=\"resize-handle handle-se\"\n (pointerdown)=\"startResize($event, comp, 'se')\"\n (dblclick)=\"resetDimensions(comp, $event)\"\n title=\"Drag to resize width and height \u2022 Double-click to reset\"\n ></div>\n\n <!-- Resizing Dimension Ghost / Badge -->\n @if (resizingCompId() === comp.id) {\n <div class=\"resize-dimension-tooltip\">\n {{ resizingTooltip(comp) }}\n </div>\n }\n\n <!-- Drag placeholder with matched grid column span -->\n <div\n *cdkDragPlaceholder\n class=\"canvas-drag-placeholder\"\n [style.grid-column]=\"getGridColumn(comp)\"\n ></div>\n\n <!-- Drag preview -->\n <div *cdkDragPreview class=\"canvas-drag-preview\">\n <span class=\"drag-handle-dots\">\u22EE\u22EE</span>\n <rpt-icon [name]=\"getComponentIconName(comp.type)\" [size]=\"14\" />\n <span class=\"preview-title\">{{ comp.label || comp.key || comp.type }}</span>\n <span class=\"preview-span\">{{ getSpan(comp) }}/{{ totalColumns() }}</span>\n </div>\n </div>\n }\n\n @if (components().length === 0) {\n <div class=\"canvas-empty-state\" role=\"region\" aria-label=\"Empty canvas starter options\">\n <div class=\"empty-hero-badge\">\n <rpt-icon name=\"design\" [size]=\"28\" />\n </div>\n <h3 class=\"empty-hero-title\">Start Designing Your Custom Report</h3>\n <p class=\"empty-hero-desc\">\n Drag controls from the left component palette, or jumpstart your design with pre-configured enterprise blueprints.\n </p>\n\n <div class=\"empty-hero-actions\">\n <button\n type=\"button\"\n class=\"empty-btn empty-btn-primary\"\n (click)=\"applyBlueprint('blueprint-audit-register')\"\n title=\"Load complete Audit Register blueprint with parameters and result grid\"\n >\n <rpt-icon name=\"table\" [size]=\"15\" />\n <span>Audit Register Blueprint</span>\n </button>\n <button\n type=\"button\"\n class=\"empty-btn empty-btn-secondary\"\n (click)=\"applyBlueprint('blueprint-kpi-summary')\"\n title=\"Load Executive KPI summary blueprint\"\n >\n <rpt-icon name=\"chart-bar\" [size]=\"15\" />\n <span>KPI Summary Blueprint</span>\n </button>\n <button\n type=\"button\"\n class=\"empty-btn empty-btn-outline\"\n (click)=\"addStarterComponent('date-range')\"\n title=\"Add a Date Range filter component\"\n >\n <rpt-icon name=\"calendar\" [size]=\"14\" />\n <span>+ Add Date Filter</span>\n </button>\n <button\n type=\"button\"\n class=\"empty-btn empty-btn-outline\"\n (click)=\"addStarterComponent('button')\"\n title=\"Add an Execute Action button\"\n >\n <rpt-icon name=\"play\" [size]=\"14\" />\n <span>+ Add Run Button</span>\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Live Result Grid Preview with Bways-Grid Features -->\n @if (definition().grid) {\n <div class=\"canvas-grid-box\">\n <div class=\"grid-preview-header\">\n <div style=\"display: flex; align-items: center; gap: 8px;\">\n <rpt-icon name=\"table\" [size]=\"15\" />\n <span class=\"grid-title\">Bways Grid ({{ definition().grid?.columns?.length ?? 0 }} columns)</span>\n @if (isExecutingGrid()) {\n <span class=\"grid-exec-badge is-loading\">Loading data from database...</span>\n } @else if (gridExecError()) {\n <span class=\"grid-exec-badge is-error\">{{ gridExecError() }}</span>\n } @else if (canvasGridData().length > 0) {\n <span class=\"grid-exec-badge is-success\">{{ canvasGridData().length }} rows loaded</span>\n }\n </div>\n <div style=\"display: flex; align-items: center; gap: 8px;\">\n <button\n type=\"button\"\n class=\"btn-canvas-run-grid\"\n (click)=\"executeCanvasReport($event)\"\n [disabled]=\"isExecutingGrid()\"\n title=\"Execute query and load data into Bways Grid\"\n >\n <rpt-icon name=\"play\" [size]=\"11\" />\n <span>{{ isExecutingGrid() ? 'Running...' : 'Run / Refresh Grid' }}</span>\n </button>\n <button\n type=\"button\"\n class=\"btn-canvas-edit-grid\"\n (click)=\"selectGridSettings($event)\"\n title=\"Configure columns and grid options\"\n >\n <rpt-icon name=\"settings\" [size]=\"11\" />\n <span>Configure</span>\n </button>\n <button\n type=\"button\"\n class=\"btn-remove-grid\"\n (click)=\"removeGrid($event)\"\n title=\"Remove Result Grid from this report\"\n >\n <rpt-icon name=\"trash\" [size]=\"13\" />\n </button>\n </div>\n </div>\n\n @if (getGridSummaryText(); as summary) {\n <div class=\"grid-layout-status-strip\">\n <div class=\"grid-status-info\">\n <rpt-icon name=\"table\" [size]=\"12\" />\n <span>{{ summary }}</span>\n </div>\n <div class=\"grid-status-sync\" [class.is-pulsing]=\"layoutJustUpdated()\">\n <rpt-icon name=\"check\" [size]=\"11\" />\n <span>Layout auto-persisted</span>\n </div>\n </div>\n }\n\n <!-- Live Bways Grid bridge -->\n <div class=\"canvas-live-grid-container\" (click)=\"$event.stopPropagation()\">\n <rpt-grid-bridge\n [gridDefinition]=\"definition().grid!\"\n [data]=\"canvasGridData()\"\n (gridStateChange)=\"onCanvasGridStateChange($event)\"\n (columnDefsChange)=\"onCanvasColumnDefsChange($event)\"\n />\n </div>\n </div>\n } @else {\n <div class=\"canvas-add-grid-banner\" (click)=\"addResultGrid($event)\">\n <div class=\"add-grid-icon\">\n <rpt-icon name=\"table\" [size]=\"20\" />\n </div>\n <div class=\"add-grid-text\">\n <span class=\"add-grid-title\">+ Add Result Data Grid</span>\n <span class=\"add-grid-sub\">Attach an interactive data table or UltraGrid to display report execution output</span>\n </div>\n <button type=\"button\" class=\"btn-banner-add\">Add Grid</button>\n </div>\n }\n </div>\n </div>\n </div>", styles: [":host{display:block;height:100%;background-color:#f1f5f9;font-family:var(--rpt-font-family, inherit);box-sizing:border-box}.canvas-wrapper{display:flex;flex-direction:column;height:100%}.canvas-toolbar{display:flex;align-items:center;justify-content:space-between;min-height:42px;height:42px;padding:0 24px;background:var(--rpt-color-surface, #ffffff);border-bottom:1px solid var(--rpt-color-border, #cbd5e1);box-sizing:border-box;gap:12px;overflow-x:auto;scrollbar-width:none;font-family:var(--rpt-font-family, inherit);flex-shrink:0}.canvas-toolbar::-webkit-scrollbar{display:none}.canvas-info{display:flex;align-items:center;gap:8px;font-family:var(--rpt-font-family, inherit);font-size:var(--rpt-font-size-sm, .75rem);color:var(--rpt-color-text-muted, #64748b);white-space:nowrap;line-height:1}.info-label{color:var(--rpt-color-text-muted, #64748b);font-weight:500;font-size:var(--rpt-font-size-sm, .75rem)}.info-value{font-weight:600;color:var(--rpt-color-text, #0f172a);font-size:var(--rpt-font-size-sm, .75rem)}.info-divider{color:var(--rpt-color-border, #cbd5e1);margin:0 2px}.live-mode-badge{display:inline-flex;align-items:center;gap:4px;font-family:var(--rpt-font-family, inherit);font-size:var(--rpt-font-size-xs, .7rem);font-weight:600;color:#047857;background:#ecfdf5;border:1px solid #a7f3d0;padding:2px 8px;border-radius:9999px;line-height:1}.canvas-actions{display:flex;align-items:center;gap:8px;white-space:nowrap}.canvas-action-btn{display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 10px;font-family:var(--rpt-font-family, inherit);font-size:var(--rpt-font-size-sm, .75rem);font-weight:500;letter-spacing:-.01em;color:var(--rpt-color-text-secondary, #334155);background:var(--rpt-color-surface, #ffffff);border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius, 6px);cursor:pointer;line-height:1;white-space:nowrap;box-shadow:0 1px 2px #0000000a;transition:all var(--rpt-transition-speed, .15s) ease}.canvas-action-btn:hover{background:var(--rpt-color-surface-hover, #f8fafc);border-color:var(--rpt-color-border-hover, #94a3b8);color:var(--rpt-color-text, #0f172a);box-shadow:0 2px 4px #0000000f}.canvas-action-btn.btn-grid-configured{background:#eff6ff;border-color:#bfdbfe;color:#2563eb;font-weight:600}.canvas-action-btn.btn-grid-configured:hover{background:#dbeafe;color:#1d4ed8;border-color:#93c5fd}.canvas-action-btn.btn-grid-add{background:#f0fdf4;border-color:#bbf7d0;color:#16a34a;font-weight:600}.canvas-action-btn.btn-grid-add:hover{background:#dcfce7;color:#15803d;border-color:#86efac}.canvas-action-btn:active{transform:translateY(1px)}.canvas-scroll-area{flex:1;overflow-y:auto;padding:16px}.canvas-artboard{width:100%;max-width:1400px;margin:0 auto;background:#fff;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius-lg, 8px);box-shadow:0 4px 6px -1px #0000000d;min-height:480px;box-sizing:border-box}@media (max-width: 900px){.canvas-toolbar{min-height:36px;height:36px;padding:0 10px;gap:6px}.live-mode-badge{display:none}.canvas-info{font-size:.75rem;gap:5px}.canvas-action-btn{padding:0 8px;font-size:.725rem;height:28px;gap:4px}.canvas-scroll-area{padding:10px}.canvas-artboard{min-height:360px}}@media (max-width: 600px){.canvas-toolbar{padding:0 8px;gap:4px}.canvas-info .info-label,.canvas-action-btn span{display:none}.canvas-action-btn{padding:0 6px;min-width:28px;justify-content:center}}.canvas-report-header{margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid var(--rpt-color-border, #e2e8f0)}.canvas-report-title{font-size:var(--rpt-font-size-xl, 1.5rem);font-weight:700;color:var(--rpt-color-text, #0f172a);margin:0 0 6px}.canvas-report-desc{font-size:var(--rpt-font-size-base, .875rem);color:var(--rpt-color-text-muted, #64748b);margin:0}.canvas-grid{display:grid;width:100%;min-height:180px;align-items:start;border-radius:6px;transition:all var(--rpt-transition-speed, .15s) ease}.canvas-component-card{container-type:inline-size;background:#fff;border:1.5px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius, 6px);box-shadow:var(--rpt-box-shadow, 0 1px 2px rgba(0, 0, 0, .05));box-sizing:border-box;transition:all var(--rpt-transition-speed, .15s) ease;cursor:pointer;position:relative;overflow:visible;display:flex;flex-direction:column;min-height:104px;z-index:1}.canvas-component-card:hover{border-color:var(--rpt-color-primary, #2563eb);box-shadow:0 4px 6px -1px #2563eb1a;z-index:2}.canvas-component-card.is-selected{border-color:var(--rpt-color-primary, #2563eb);box-shadow:0 0 0 2px #2563eb40;z-index:10}.canvas-component-card:focus-within,.canvas-component-card:has(.rpt-combobox-menu),.canvas-component-card:has(.is-open){z-index:100}.canvas-component-card.is-resizing{-webkit-user-select:none;user-select:none;border-color:var(--rpt-color-primary, #2563eb);box-shadow:0 0 0 2px #2563eb59;z-index:30;transition:none!important}.resize-handle{position:absolute;z-index:20;touch-action:none;opacity:0;pointer-events:none;transition:opacity .15s ease}.canvas-component-card.is-selected .resize-handle{opacity:1;pointer-events:auto}.resize-handle.handle-e{top:0;right:0;width:6px;height:100%;cursor:ew-resize;display:flex;align-items:center;justify-content:center}.resize-handle.handle-e:after{content:\"\";width:3px;height:24px;background:#cbd5e1;border-radius:2px;transition:background .15s ease}.resize-handle.handle-e:hover:after,.canvas-component-card.is-selected .resize-handle.handle-e:after{background:var(--rpt-color-primary, #2563eb)}.resize-handle.handle-s{bottom:0;left:0;height:6px;width:100%;cursor:ns-resize;display:flex;align-items:center;justify-content:center}.resize-handle.handle-s:after{content:\"\";height:3px;width:24px;background:#cbd5e1;border-radius:2px;transition:background .15s ease}.resize-handle.handle-s:hover:after,.canvas-component-card.is-selected .resize-handle.handle-s:after{background:var(--rpt-color-primary, #2563eb)}.resize-handle.handle-se{bottom:0;right:0;width:12px;height:12px;cursor:nwse-resize;display:flex;align-items:flex-end;justify-content:flex-end;padding:2px}.resize-handle.handle-se:after{content:\"\";width:5px;height:5px;border-right:2px solid #cbd5e1;border-bottom:2px solid #cbd5e1;transition:border-color .15s ease}.resize-handle.handle-se:hover:after,.canvas-component-card.is-selected .resize-handle.handle-se:after{border-color:var(--rpt-color-primary, #2563eb)}.resize-dimension-tooltip{position:absolute;top:38px;right:8px;background:#0f172ae6;color:#fff;font-size:.7rem;font-weight:600;padding:3px 8px;border-radius:4px;white-space:nowrap;pointer-events:none;z-index:50;box-shadow:0 2px 4px #0003}.card-header{display:flex;align-items:center;justify-content:space-between;gap:6px;padding:5px 8px;background:var(--rpt-color-surface, #f8fafc);border-bottom:1px solid var(--rpt-color-border, #e2e8f0);border-top-left-radius:var(--rpt-border-radius, 6px);border-top-right-radius:var(--rpt-border-radius, 6px);min-height:32px;box-sizing:border-box;overflow:hidden;position:relative}.canvas-component-card.is-selected .card-header{overflow:visible}.card-title-group{display:flex;align-items:center;gap:5px;cursor:grab;min-width:0;flex:1;overflow:hidden}.drag-handle-dots{color:var(--rpt-color-text-muted, #94a3b8);font-size:.85rem;letter-spacing:-2px;flex-shrink:0;-webkit-user-select:none;user-select:none}.comp-title{font-size:.75rem;font-weight:600;color:var(--rpt-color-text, #0f172a);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;-webkit-user-select:none;user-select:none}.card-actions{display:flex;align-items:center;gap:4px;flex-shrink:0;margin-left:auto}.new-row-pill-static{font-size:.65rem;font-weight:600;color:#7c3aed;background:#f5f3ff;border:1px solid #ddd6fe;padding:1px 5px;border-radius:9999px;white-space:nowrap;line-height:1;flex-shrink:0}.span-pill-static{display:inline-flex;align-items:center;justify-content:center;font-size:.65rem;font-weight:600;color:var(--rpt-color-text-muted, #64748b);background:var(--rpt-color-surface, #f1f5f9);border:1px solid var(--rpt-color-border, #e2e8f0);padding:1px 6px;border-radius:9999px;line-height:1.2;white-space:nowrap;-webkit-user-select:none;user-select:none}.card-quick-controls{display:none;align-items:center;gap:3px;flex-shrink:0}.canvas-component-card:hover .span-pill-static,.canvas-component-card.is-selected .span-pill-static,.canvas-component-card:hover .new-row-pill-static,.canvas-component-card.is-selected .new-row-pill-static{display:none}.canvas-component-card:hover .card-quick-controls,.canvas-component-card.is-selected .card-quick-controls{display:inline-flex}.canvas-component-card.is-narrow .card-header{padding:4px 6px;gap:3px}.canvas-component-card.is-narrow:hover .comp-title,.canvas-component-card.is-narrow.is-selected .comp-title,.canvas-component-card.is-narrow:hover .drag-handle-dots,.canvas-component-card.is-narrow.is-selected .drag-handle-dots{display:none}.canvas-component-card.is-narrow .card-quick-controls{gap:2px}.canvas-component-card.is-narrow .span-control-group{height:18px}.canvas-component-card.is-narrow .span-btn{width:14px;height:16px}.canvas-component-card.is-narrow .span-badge{font-size:.6rem;padding:0 2px}.canvas-component-card.is-narrow .span-total{display:none}.canvas-component-card.is-narrow .card-action-btn{width:18px;height:18px}@container (max-width: 250px){.card-header{padding:4px 6px;gap:3px}.canvas-component-card:hover .comp-title,.canvas-component-card.is-selected .comp-title,.canvas-component-card:hover .drag-handle-dots,.canvas-component-card.is-selected .drag-handle-dots,.span-total{display:none}.card-quick-controls{gap:2px}.card-action-btn{width:18px;height:18px}.span-control-group{height:18px}.span-btn{width:14px;height:16px}.span-badge{font-size:.6rem;padding:0 2px}}@container (max-width: 160px){.canvas-component-card.is-selected .card-quick-controls{position:absolute;top:-30px;right:0;background:#fff;border:1.5px solid var(--rpt-color-primary, #2563eb);border-radius:6px;padding:2px 4px;gap:3px;box-shadow:0 4px 12px #2563eb2e,0 2px 4px #00000014;z-index:120;white-space:nowrap}}@media (max-width: 900px){.canvas-component-card:hover .comp-title,.canvas-component-card.is-selected .comp-title,.canvas-component-card:hover .drag-handle-dots,.canvas-component-card.is-selected .drag-handle-dots,.canvas-component-card .span-total{display:none}.canvas-component-card.is-selected .card-header{overflow:visible}.canvas-component-card.is-selected .card-quick-controls{position:absolute;top:-30px;right:0;background:#fff;border:1.5px solid var(--rpt-color-primary, #2563eb);border-radius:6px;padding:2px 4px;gap:3px;box-shadow:0 4px 12px #2563eb2e,0 2px 4px #00000014;z-index:120;white-space:nowrap}}.span-control-group{display:inline-flex;align-items:center;background:#fff;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:4px;height:20px;padding:0 1px;white-space:nowrap;flex-shrink:0}.span-btn{width:16px;height:18px;display:inline-flex;align-items:center;justify-content:center;border:none;background:transparent;padding:0;color:var(--rpt-color-text-muted, #64748b);cursor:pointer;border-radius:2px;transition:all .12s ease}.span-btn:hover:not(:disabled){background:var(--rpt-color-surface-hover, #f1f5f9);color:var(--rpt-color-primary, #2563eb)}.span-btn:disabled{opacity:.3;cursor:not-allowed}.span-badge{font-size:.65rem;font-weight:600;color:var(--rpt-color-text, #0f172a);padding:0 3px;white-space:nowrap;-webkit-user-select:none;user-select:none;line-height:1}.span-total{font-size:.6rem;color:var(--rpt-color-text-muted, #64748b);font-weight:500}.card-action-btn{width:20px;height:20px;border:none;background:transparent;border-radius:4px;display:inline-flex;align-items:center;justify-content:center;padding:0;cursor:pointer;color:var(--rpt-color-text-muted, #64748b);transition:all .12s ease;flex-shrink:0}.card-action-btn:hover{background:#e2e8f0;color:var(--rpt-color-text, #0f172a)}.card-action-btn.btn-active-toggle{background:#7c3aed;color:#fff;border-color:#7c3aed}.card-action-btn.btn-active-toggle:hover{background:#6d28d9;color:#fff}.btn-delete:hover{background:#fee2e2;color:#ef4444}.card-body{padding:8px 10px 14px;flex:1;display:flex;flex-direction:column;min-height:0;min-width:0;max-width:100%;box-sizing:border-box;overflow:visible}.mock-input-field{display:flex;flex-direction:column;gap:4px;flex:1;height:100%;min-height:0}.mock-label{font-size:.7rem;font-weight:600;color:var(--rpt-color-text-muted, #475569);flex-shrink:0}.mock-input-box,.mock-select-box{border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:6px;padding:6px 12px;font-size:var(--rpt-font-size-base, .875rem);color:var(--rpt-color-text-muted, #94a3b8);background:#fff;flex:1;min-height:38px;height:100%;display:flex;align-items:center;justify-content:space-between;box-sizing:border-box}.mock-checkbox-field{display:flex;align-items:center;gap:6px;flex:1;height:100%;min-height:0;padding-top:14px}.mock-check-box{width:18px;height:18px;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:.65rem;color:var(--rpt-color-primary, #2563eb);flex-shrink:0}.mock-btn-wrapper{display:flex;flex:1;height:100%;min-height:38px;align-items:center;justify-content:flex-start;width:100%;min-width:0;box-sizing:border-box}.mock-btn{border-radius:var(--rpt-border-radius, 6px);padding:0 16px;height:36px;font-size:var(--rpt-font-size-base, .875rem);font-weight:500;border:1px solid transparent;cursor:pointer;width:auto;max-width:100%;display:inline-flex;align-items:center;justify-content:center;gap:6px;box-sizing:border-box;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:all .15s ease}.mock-btn.mock-btn-full{width:100%}.canvas-component-card.is-narrow .mock-btn{padding:0 6px;font-size:.775rem;width:100%;height:34px}.mock-kpi-field{display:flex;flex-direction:column;justify-content:space-between;flex:1;height:100%;min-height:0}.mock-kpi-header{display:flex;align-items:center;justify-content:space-between}.mock-kpi-value{font-size:1.25rem;font-weight:700;color:var(--rpt-color-text, #0f172a);margin:2px 0}.mock-kpi-footer{display:flex;align-items:center;gap:6px;font-size:.65rem}.mock-kpi-trend{color:#16a34a;font-weight:600}.mock-kpi-sub{color:#64748b}.mock-chart-field{display:flex;flex-direction:column;flex:1;height:100%;min-height:140px}.mock-chart-bars{display:flex;align-items:flex-end;justify-content:space-around;gap:16px;flex:1;min-height:100px;padding-top:12px;box-sizing:border-box}.mock-chart-col{flex:1;display:flex;flex-direction:column;align-items:center;height:100%;min-width:60px;max-width:140px}.mock-chart-bar{width:38px;max-width:44px;margin:0 auto;background:var(--rpt-color-primary, #2563eb);border-radius:6px 6px 0 0;opacity:.85;min-height:10px;transition:height .2s ease}.mock-bar-lbl{font-size:.6875rem;color:#64748b;margin-top:6px;text-align:center;white-space:normal;line-height:1.2}.btn-primary{background:var(--rpt-color-primary, #2563eb);color:#fff}.btn-secondary{background:#f1f5f9;color:#0f172a;border:1px solid #cbd5e1}.btn-outline{background:transparent;color:#2563eb;border:1px solid #2563eb}.btn-danger{background:#ef4444;color:#fff}.mock-generic-box{border:1px dashed #cbd5e1;border-radius:4px;padding:8px;font-size:.75rem;text-align:center;color:#64748b}.canvas-drag-placeholder{background:#2563eb0f;border:2px dashed var(--rpt-color-primary, #2563eb);border-radius:6px;min-height:80px;box-sizing:border-box;transition:transform .15s ease}.canvas-drag-preview,::ng-deep .canvas-drag-preview{background:#fff;border:2px solid var(--rpt-color-primary, #2563eb);border-radius:6px;padding:8px 14px;display:inline-flex;align-items:center;gap:8px;font-family:var(--rpt-font-family, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif)!important;font-size:.8rem;font-weight:600;color:var(--rpt-color-text, #0f172a);box-shadow:0 12px 24px -4px #00000040;z-index:10000}.preview-title,::ng-deep .preview-title{font-family:inherit!important;max-width:160px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.preview-span,::ng-deep .preview-span{font-family:inherit!important;font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border:1px solid #bfdbfe;padding:1px 6px;border-radius:9999px}.canvas-empty-state{grid-column:1 / -1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:56px 28px;border:2px dashed var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius-xl, 12px);background:var(--rpt-color-surface, #ffffff);text-align:center;box-shadow:var(--rpt-box-shadow-xs, 0 1px 2px 0 rgba(0, 0, 0, .04));transition:border-color var(--rpt-transition-speed, .15s) ease}.canvas-empty-state:hover{border-color:var(--rpt-color-primary, #2563eb)}.empty-hero-badge{width:56px;height:56px;border-radius:var(--rpt-border-radius-xl, 12px);background:var(--rpt-color-primary-light, #eff6ff);color:var(--rpt-color-primary, #2563eb);display:flex;align-items:center;justify-content:center;margin-bottom:16px;box-shadow:0 2px 8px #2563eb1f}.empty-hero-title{font-size:var(--rpt-font-size-lg, 1.125rem);font-weight:700;color:var(--rpt-color-text, #0f172a);margin:0 0 8px;letter-spacing:-.01em}.empty-hero-desc{font-size:var(--rpt-font-size-base, .875rem);color:var(--rpt-color-text-muted, #64748b);max-width:520px;line-height:1.5;margin:0 0 24px}.empty-hero-actions{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:10px}.empty-btn{display:inline-flex;align-items:center;gap:7px;padding:8px 16px;font-size:var(--rpt-font-size-sm, .75rem);font-weight:600;border-radius:var(--rpt-border-radius, 6px);cursor:pointer;transition:all var(--rpt-transition-speed, .15s) ease;font-family:inherit}.empty-btn-primary{background:var(--rpt-color-primary, #2563eb);color:#fff;border:1px solid var(--rpt-color-primary, #2563eb);box-shadow:0 1px 3px #2563eb33}.empty-btn-primary:hover{background:var(--rpt-color-primary-hover, #1d4ed8);border-color:var(--rpt-color-primary-hover, #1d4ed8);transform:translateY(-1px)}.empty-btn-secondary{background:#f8fafc;color:#334155;border:1px solid var(--rpt-color-border, #cbd5e1)}.empty-btn-secondary:hover{background:#f1f5f9;color:#0f172a;border-color:#94a3b8;transform:translateY(-1px)}.empty-btn-outline{background:#fff;color:var(--rpt-color-primary, #2563eb);border:1px solid #bfdbfe}.empty-btn-outline:hover{background:#eff6ff;border-color:var(--rpt-color-primary, #2563eb);transform:translateY(-1px)}.canvas-active-input{width:100%;height:32px;padding:4px 8px;font-size:.8125rem;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:4px;outline:none;box-sizing:border-box;background:#fff;color:#0f172a;transition:border-color .15s ease}.canvas-active-input:focus{border-color:var(--rpt-color-primary, #2563eb)}.canvas-active-checkbox-lbl{display:inline-flex;align-items:center;gap:6px;cursor:pointer}.canvas-live-control-wrapper{width:100%;max-width:100%;min-width:0;position:relative;z-index:1;box-sizing:border-box}.canvas-grid-box{margin-top:24px;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:8px;background:#fff;box-shadow:0 1px 3px #0000000d;overflow:hidden}.grid-preview-header{display:flex;align-items:center;justify-content:space-between;padding:10px 16px;background:var(--rpt-color-surface, #f8fafc);border-bottom:1px solid var(--rpt-color-border, #cbd5e1);font-size:.8rem;font-weight:600;color:var(--rpt-color-text, #0f172a)}.grid-title{font-weight:700}.grid-exec-badge{font-size:.6875rem;font-weight:600;padding:2px 8px;border-radius:10px}.grid-exec-badge.is-loading{background:#eff6ff;color:#2563eb}.grid-exec-badge.is-error{background:#fee2e2;color:#ef4444}.grid-exec-badge.is-success{background:#ecfdf5;color:#059669}.btn-canvas-run-grid{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;font-size:.72rem;font-weight:600;color:#fff;background:var(--rpt-color-primary, #2563eb);border:none;border-radius:4px;cursor:pointer;transition:background .15s ease}.btn-canvas-run-grid:hover:not(:disabled){background:#1d4ed8}.btn-canvas-run-grid:disabled{opacity:.6;cursor:not-allowed}.btn-canvas-edit-grid{display:inline-flex;align-items:center;gap:4px;padding:4px 8px;font-size:.72rem;font-weight:500;color:#334155;background:#fff;border:1px solid #cbd5e1;border-radius:4px;cursor:pointer;transition:all .12s ease}.btn-canvas-edit-grid:hover{background:#f1f5f9}.canvas-live-grid-container{width:100%;min-height:480px;background:#fff}.live-mode-badge{display:inline-flex;align-items:center;gap:4px;font-size:.6875rem;font-weight:700;color:#047857;background:#ecfdf5;border:1px solid #a7f3d0;padding:1px 8px;border-radius:9999px}.canvas-component-card.is-field-highlighted{border-color:#f59e0b!important;box-shadow:0 0 0 3px #f59e0b59!important;animation:rpt-pulse-border 1s ease-in-out infinite alternate}@keyframes rpt-pulse-border{0%{box-shadow:0 0 0 2px #f59e0b40}to{box-shadow:0 0 0 5px #f59e0b80}}.grid-layout-status-strip{display:flex;align-items:center;justify-content:space-between;padding:6px 16px;background:#f1f5f9;border-bottom:1px solid var(--rpt-color-border, #cbd5e1);font-size:.6875rem;color:#475569}.grid-status-info{display:inline-flex;align-items:center;gap:6px;font-weight:600}.grid-status-sync{display:inline-flex;align-items:center;gap:4px;color:#059669;font-weight:600;transition:transform .2s ease}.grid-status-sync.is-pulsing{transform:scale(1.08);color:#047857}.canvas-actions{display:flex;align-items:center;gap:8px}.btn-grid-add{background:#f0fdf4;border-color:#bbf7d0;color:#16a34a}.btn-grid-add:hover{background:#dcfce7;border-color:#86efac;color:#15803d}.btn-remove-grid{display:inline-flex;align-items:center;justify-content:center;padding:4px;border:none;background:transparent;color:#94a3b8;border-radius:4px;cursor:pointer;transition:all .15s ease}.btn-remove-grid:hover{background:#fee2e2;color:#ef4444}.canvas-add-grid-banner{margin-top:24px;border:2px dashed #cbd5e1;border-radius:8px;background:#fff;padding:16px 20px;display:flex;align-items:center;gap:16px;cursor:pointer;transition:all .15s ease}.canvas-add-grid-banner:hover{border-color:#2563eb;background:#f8fafc;box-shadow:0 2px 8px #2563eb14}.add-grid-icon{width:40px;height:40px;border-radius:8px;background:#eff6ff;color:#2563eb;display:flex;align-items:center;justify-content:center;flex-shrink:0}.add-grid-text{display:flex;flex-direction:column;gap:2px;flex:1}.add-grid-title{font-size:.875rem;font-weight:600;color:#0f172a}.add-grid-sub{font-size:.75rem;color:#64748b}.btn-banner-add{padding:6px 14px;font-size:.75rem;font-weight:600;color:#2563eb;background:#eff6ff;border:1px solid #bfdbfe;border-radius:6px;cursor:pointer;white-space:nowrap;transition:all .15s ease}.canvas-add-grid-banner:hover .btn-banner-add{background:#2563eb;color:#fff;border-color:#2563eb}\n"] }]
|
|
9759
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: " <div class=\"canvas-wrapper\" (click)=\"onCanvasBackgroundClick($event)\">\n <!-- Canvas Status / Quick Controls -->\n <div class=\"canvas-toolbar\">\n <div class=\"canvas-info\">\n <span class=\"info-label\">Grid Layout:</span>\n <span class=\"info-value\">{{ totalColumns() }} Columns</span>\n <span class=\"info-divider\">\u2022</span>\n <span class=\"info-label\">Components:</span>\n <span class=\"info-value\">{{ components().length }}</span>\n <span class=\"info-divider\">\u2022</span>\n <span class=\"live-mode-badge\" title=\"Interactive canvas: Values typed here run live queries and test dropdowns.\">\n \u26A1 Live Interactive Mode\n </span>\n </div>\n\n <div class=\"canvas-actions\">\n @if (!definition().grid) {\n <button\n type=\"button\"\n class=\"canvas-action-btn btn-grid-add\"\n (click)=\"addResultGrid($event)\"\n title=\"Add a Result Grid to this report\"\n >\n <rpt-icon name=\"table\" [size]=\"14\" />\n <span>+ Add Grid</span>\n </button>\n } @else {\n <button\n type=\"button\"\n class=\"canvas-action-btn btn-grid-configured\"\n (click)=\"selectGridSettings($event)\"\n title=\"Configure Result Data Grid\"\n >\n <rpt-icon name=\"table\" [size]=\"14\" />\n <span>Grid ({{ definition().grid?.columns?.length ?? 0 }})</span>\n </button>\n }\n\n <button\n type=\"button\"\n class=\"canvas-action-btn\"\n (click)=\"selectReportSettings($event)\"\n title=\"Configure Page Layout & Grid Settings\"\n >\n <rpt-icon name=\"settings\" [size]=\"14\" />\n <span>Report Settings</span>\n </button>\n </div>\n </div>\n\n <!-- Canvas Drop Area -->\n <div class=\"canvas-scroll-area\">\n <div\n class=\"canvas-artboard\"\n [style.padding.px]=\"definition().layout.padding ?? 24\"\n >\n <!-- Report Header Preview -->\n <div class=\"canvas-report-header\">\n <h2 class=\"canvas-report-title\">{{ definition().name || 'Untitled Report' }}</h2>\n @if (definition().description) {\n <p class=\"canvas-report-desc\">{{ definition().description }}</p>\n }\n </div>\n\n <!-- Component Drop List Grid with 2D mixed orientation and data binding -->\n <div\n cdkDropList\n id=\"designer-canvas-drop-list\"\n class=\"canvas-grid\"\n cdkDropListOrientation=\"mixed\"\n [cdkDropListData]=\"sortedComponents()\"\n [style.grid-template-columns]=\"gridTemplateColumns()\"\n [style.gap.px]=\"definition().layout.gap ?? 16\"\n (cdkDropListDropped)=\"onDrop($event)\"\n >\n @for (comp of sortedComponents(); track comp.id; let idx = $index) {\n <div\n cdkDrag\n [cdkDragData]=\"comp\"\n [cdkDragDisabled]=\"isAnyResizing()\"\n class=\"canvas-component-card\"\n [class.is-selected]=\"isSelected(comp.id)\"\n [class.is-resizing]=\"resizingCompId() === comp.id\"\n [class.is-field-highlighted]=\"isFieldHighlighted(comp)\"\n [class.is-narrow]=\"getSpan(comp) <= 4\"\n [class.is-compact]=\"getSpan(comp) <= 2\"\n [style.grid-column]=\"getGridColumn(comp)\"\n [style.grid-row]=\"getGridRow(comp)\"\n [style.width]=\"comp.layout?.width || 'auto'\"\n [style.height]=\"getComponentHeight(comp)\"\n [style.min-height]=\"getComponentMinHeight(comp)\"\n (click)=\"onSelect(comp.id, $event)\"\n >\n <!-- Card Header with Drag Handle & Actions -->\n <div class=\"card-header\">\n <div class=\"card-title-group\" cdkDragHandle title=\"Drag to reorder\">\n <span class=\"drag-handle-dots\">\u22EE\u22EE</span>\n <rpt-icon [name]=\"getComponentIconName(comp.type)\" [size]=\"14\" />\n <span class=\"comp-title\" [title]=\"comp.label || comp.key || comp.type\">\n {{ comp.label || comp.key || comp.type }}\n </span>\n </div>\n\n <div class=\"card-actions\">\n <!-- Static span pill & new row / start col pill when not hovered / not selected -->\n @if (comp.layout?.column; as col) {\n <span class=\"new-row-pill-static\" title=\"Starts at Column {{ col }}\">Col {{ col }}</span>\n } @else if (comp.layout?.newRow) {\n <span class=\"new-row-pill-static\" title=\"Starts on separate row\">\u21B3 Row</span>\n }\n <span\n class=\"span-pill-static\"\n title=\"Column span: {{ getSpan(comp) }} of {{ totalColumns() }} columns\"\n >\n {{ getSpan(comp) }}/{{ totalColumns() }}\n </span>\n\n <!-- Quick Action Stepper & Buttons (revealed on hover or selection) -->\n <div class=\"card-quick-controls\">\n <button\n type=\"button\"\n class=\"card-action-btn\"\n [class.btn-active-toggle]=\"comp.layout?.newRow\"\n (click)=\"toggleNewRow(comp, $event)\"\n title=\"{{ comp.layout?.newRow ? 'Starts on new row (Click to place inline)' : 'Start on separate row (Row Break)' }}\"\n >\n <rpt-icon name=\"corner-down-right\" [size]=\"11\" />\n </button>\n\n <div class=\"span-control-group\" title=\"Adjust Column Span\">\n <button\n type=\"button\"\n class=\"span-btn\"\n [disabled]=\"getSpan(comp) <= 1\"\n (click)=\"adjustSpan(comp, -1, $event)\"\n title=\"Shrink Column Span\"\n >\n <rpt-icon name=\"minus\" [size]=\"9\" />\n </button>\n <span class=\"span-badge\" [title]=\"'Column span: ' + getSpan(comp) + ' of ' + totalColumns()\">\n <span class=\"span-num\">{{ getSpan(comp) }}</span><span class=\"span-total\">/{{ totalColumns() }}</span>\n </span>\n <button\n type=\"button\"\n class=\"span-btn\"\n [disabled]=\"getSpan(comp) >= totalColumns()\"\n (click)=\"adjustSpan(comp, 1, $event)\"\n title=\"Expand Column Span\"\n >\n <rpt-icon name=\"plus\" [size]=\"9\" />\n </button>\n </div>\n\n <button\n type=\"button\"\n class=\"card-action-btn\"\n (click)=\"onDuplicate(comp.id, $event)\"\n title=\"Duplicate component\"\n >\n <rpt-icon name=\"copy\" [size]=\"12\" />\n </button>\n <button\n type=\"button\"\n class=\"card-action-btn btn-delete\"\n (click)=\"onDelete(comp.id, $event)\"\n title=\"Remove component\"\n >\n <rpt-icon name=\"trash\" [size]=\"12\" />\n </button>\n </div>\n </div>\n </div>\n\n <!-- Card Body Visual Preview -->\n <div class=\"card-body\">\n\n @switch (comp.type) {\n @case ('input') {\n <div class=\"mock-input-field\" (click)=\"onControlInteract(comp, $event)\">\n <span class=\"mock-label\">{{ comp.label || 'Input' }}</span>\n @if (getCanvasControl(comp); as ctrl) {\n <input\n type=\"text\"\n class=\"canvas-active-input\"\n [placeholder]=\"comp.properties?.['placeholder'] || 'Enter text...'\"\n [formControl]=\"ctrl\"\n (focus)=\"onControlInteract(comp, $event)\"\n />\n } @else {\n <div class=\"mock-input-box\">{{ comp.properties?.['placeholder'] || 'Enter text...' }}</div>\n }\n </div>\n }\n @case ('dropdown') {\n <div class=\"canvas-live-control-wrapper\" (click)=\"onControlInteract(comp, $event)\">\n <rpt-dropdown\n [config]=\"comp\"\n [showStatus]=\"true\"\n [parentForm]=\"canvasForm()\"\n [control]=\"getCanvasControl(comp)\"\n />\n </div>\n }\n @case ('date') {\n <div class=\"mock-input-field\" (click)=\"onControlInteract(comp, $event)\">\n <span class=\"mock-label\">{{ comp.label || 'Date' }}</span>\n @if (getCanvasControl(comp); as ctrl) {\n <input\n type=\"date\"\n class=\"canvas-active-input\"\n [formControl]=\"ctrl\"\n (focus)=\"onControlInteract(comp, $event)\"\n />\n } @else {\n <div class=\"mock-select-box\">\n <span>YYYY-MM-DD</span>\n <rpt-icon name=\"calendar\" [size]=\"12\" />\n </div>\n }\n </div>\n }\n @case ('date-range') {\n <div class=\"canvas-live-control-wrapper\" (click)=\"onControlInteract(comp, $event)\">\n <rpt-date-range\n [config]=\"comp\"\n [control]=\"getCanvasControl(comp)\"\n />\n </div>\n }\n @case ('number') {\n <div class=\"mock-input-field\" (click)=\"onControlInteract(comp, $event)\">\n <span class=\"mock-label\">{{ comp.label || 'Number' }}</span>\n @if (getCanvasControl(comp); as ctrl) {\n <input\n type=\"number\"\n class=\"canvas-active-input\"\n [formControl]=\"ctrl\"\n (focus)=\"onControlInteract(comp, $event)\"\n />\n } @else {\n <div class=\"mock-input-box\">\n {{ comp.properties?.['prefix'] || '' }} 0.00 {{ comp.properties?.['suffix'] || '' }}\n </div>\n }\n </div>\n }\n @case ('checkbox') {\n <div class=\"mock-checkbox-field\" (click)=\"onControlInteract(comp, $event)\">\n @if (getCanvasControl(comp); as ctrl) {\n <label class=\"canvas-active-checkbox-lbl\">\n <input type=\"checkbox\" [formControl]=\"ctrl\" (focus)=\"onControlInteract(comp, $event)\" />\n <span class=\"mock-label\">{{ comp.label || 'Checkbox' }}</span>\n </label>\n } @else {\n <span class=\"mock-check-box\">\n <rpt-icon name=\"check\" [size]=\"10\" />\n </span>\n <span class=\"mock-label\">{{ comp.label || 'Checkbox' }}</span>\n }\n </div>\n }\n @case ('button') {\n <div class=\"mock-btn-wrapper\">\n <button\n type=\"button\"\n class=\"mock-btn\"\n [class]=\"'btn-' + (comp.properties?.['variant'] || 'primary')\"\n [class.mock-btn-full]=\"comp.properties?.['fullWidth'] === true || comp.layout?.width === '100%'\"\n [style.height]=\"getCustomBtnHeight(comp)\"\n [style.width]=\"getCustomBtnWidth(comp)\"\n (click)=\"onCanvasButtonClick(comp, $event)\"\n >\n @if (getCustomBtnIcon(comp); as btnIcon) {\n <rpt-icon [name]=\"btnIcon\" [size]=\"15\" />\n }\n <span>{{ comp.label || 'Button' }}</span>\n </button>\n </div>\n }\n @case ('kpi-card') {\n <div class=\"mock-kpi-field\">\n <div class=\"mock-kpi-header\">\n <span class=\"mock-label\">{{ comp.label || 'KPI Metric' }}</span>\n <rpt-icon name=\"kpi\" [size]=\"14\" />\n </div>\n <div class=\"mock-kpi-value\">{{ comp.properties?.['value'] || '$128,450' }}</div>\n <div class=\"mock-kpi-footer\">\n <span class=\"mock-kpi-trend\">+12.5%</span>\n <span class=\"mock-kpi-sub\">vs last period</span>\n </div>\n </div>\n }\n @case ('chart') {\n <div class=\"mock-chart-field\">\n <div class=\"mock-chart-header\">\n <span class=\"mock-label\">{{ comp.label || 'Quarterly Sales Revenue ($000)' }}</span>\n </div>\n <div class=\"mock-chart-bars\">\n <div class=\"mock-chart-col\">\n <div class=\"mock-chart-bar\" style=\"height: 44%\"></div>\n <span class=\"mock-bar-lbl\">Q1 Jan-Mar</span>\n </div>\n <div class=\"mock-chart-col\">\n <div class=\"mock-chart-bar\" style=\"height: 61%\"></div>\n <span class=\"mock-bar-lbl\">Q2 Apr-Jun</span>\n </div>\n <div class=\"mock-chart-col\">\n <div class=\"mock-chart-bar\" style=\"height: 79%\"></div>\n <span class=\"mock-bar-lbl\">Q3 Jul-Sep</span>\n </div>\n <div class=\"mock-chart-col\">\n <div class=\"mock-chart-bar\" style=\"height: 100%\"></div>\n <span class=\"mock-bar-lbl\">Q4 Oct-Dec</span>\n </div>\n </div>\n </div>\n }\n @default {\n <div class=\"mock-generic-box\">\n <span>{{ comp.type }}</span>\n </div>\n }\n }\n </div>\n\n <!-- Resizing Handles (East, South, South-East) -->\n <div\n class=\"resize-handle handle-e\"\n (pointerdown)=\"startResize($event, comp, 'e')\"\n (dblclick)=\"resetDimensions(comp, $event)\"\n title=\"Drag to resize column width \u2022 Double-click to reset\"\n ></div>\n <div\n class=\"resize-handle handle-s\"\n (pointerdown)=\"startResize($event, comp, 's')\"\n (dblclick)=\"resetDimensions(comp, $event)\"\n title=\"Drag to resize height \u2022 Double-click to reset\"\n ></div>\n <div\n class=\"resize-handle handle-se\"\n (pointerdown)=\"startResize($event, comp, 'se')\"\n (dblclick)=\"resetDimensions(comp, $event)\"\n title=\"Drag to resize width and height \u2022 Double-click to reset\"\n ></div>\n\n <!-- Resizing Dimension Ghost / Badge -->\n @if (resizingCompId() === comp.id) {\n <div class=\"resize-dimension-tooltip\">\n {{ resizingTooltip(comp) }}\n </div>\n }\n\n <!-- Drag placeholder with matched grid column span -->\n <div\n *cdkDragPlaceholder\n class=\"canvas-drag-placeholder\"\n [style.grid-column]=\"getGridColumn(comp)\"\n ></div>\n\n <!-- Drag preview -->\n <div *cdkDragPreview class=\"canvas-drag-preview\">\n <span class=\"drag-handle-dots\">\u22EE\u22EE</span>\n <rpt-icon [name]=\"getComponentIconName(comp.type)\" [size]=\"14\" />\n <span class=\"preview-title\">{{ comp.label || comp.key || comp.type }}</span>\n <span class=\"preview-span\">{{ getSpan(comp) }}/{{ totalColumns() }}</span>\n </div>\n </div>\n }\n\n @if (components().length === 0) {\n <div class=\"canvas-empty-state\" role=\"region\" aria-label=\"Empty canvas starter options\">\n <div class=\"empty-hero-badge\">\n <rpt-icon name=\"design\" [size]=\"28\" />\n </div>\n <h3 class=\"empty-hero-title\">Start Designing Your Custom Report</h3>\n <p class=\"empty-hero-desc\">\n Drag controls from the left component palette, or jumpstart your design with pre-configured enterprise blueprints.\n </p>\n\n <div class=\"empty-hero-actions\">\n <button\n type=\"button\"\n class=\"empty-btn empty-btn-primary\"\n (click)=\"applyBlueprint('blueprint-audit-register')\"\n title=\"Load complete Audit Register blueprint with parameters and result grid\"\n >\n <rpt-icon name=\"table\" [size]=\"15\" />\n <span>Audit Register Blueprint</span>\n </button>\n <button\n type=\"button\"\n class=\"empty-btn empty-btn-secondary\"\n (click)=\"applyBlueprint('blueprint-kpi-summary')\"\n title=\"Load Executive KPI summary blueprint\"\n >\n <rpt-icon name=\"chart-bar\" [size]=\"15\" />\n <span>KPI Summary Blueprint</span>\n </button>\n <button\n type=\"button\"\n class=\"empty-btn empty-btn-outline\"\n (click)=\"addStarterComponent('date-range')\"\n title=\"Add a Date Range filter component\"\n >\n <rpt-icon name=\"calendar\" [size]=\"14\" />\n <span>+ Add Date Filter</span>\n </button>\n <button\n type=\"button\"\n class=\"empty-btn empty-btn-outline\"\n (click)=\"addStarterComponent('button')\"\n title=\"Add an Execute Action button\"\n >\n <rpt-icon name=\"play\" [size]=\"14\" />\n <span>+ Add Run Button</span>\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Live Result Grid Preview with Bways-Grid Features -->\n @if (definition().grid) {\n <div class=\"canvas-grid-box\">\n <div class=\"grid-preview-header\">\n <div style=\"display: flex; align-items: center; gap: 8px;\">\n <rpt-icon name=\"table\" [size]=\"15\" />\n <span class=\"grid-title\">Bways Grid ({{ definition().grid?.columns?.length ?? 0 }} columns)</span>\n @if (isExecutingGrid()) {\n <span class=\"grid-exec-badge is-loading\">Loading data from database...</span>\n } @else if (gridExecError()) {\n <span class=\"grid-exec-badge is-error\">{{ gridExecError() }}</span>\n } @else if (canvasGridData().length > 0) {\n <span class=\"grid-exec-badge is-success\">{{ canvasGridData().length }} rows loaded</span>\n }\n </div>\n <div style=\"display: flex; align-items: center; gap: 8px;\">\n <button\n type=\"button\"\n class=\"btn-canvas-run-grid\"\n (click)=\"executeCanvasReport($event)\"\n [disabled]=\"isExecutingGrid()\"\n title=\"Execute query and load data into Bways Grid\"\n >\n <rpt-icon name=\"play\" [size]=\"11\" />\n <span>{{ isExecutingGrid() ? 'Running...' : 'Run / Refresh Grid' }}</span>\n </button>\n <button\n type=\"button\"\n class=\"btn-canvas-edit-grid\"\n (click)=\"selectGridSettings($event)\"\n title=\"Configure columns and grid options\"\n >\n <rpt-icon name=\"settings\" [size]=\"11\" />\n <span>Configure</span>\n </button>\n <button\n type=\"button\"\n class=\"btn-remove-grid\"\n (click)=\"removeGrid($event)\"\n title=\"Remove Result Grid from this report\"\n >\n <rpt-icon name=\"trash\" [size]=\"13\" />\n </button>\n </div>\n </div>\n\n @if (getGridSummaryText(); as summary) {\n <div class=\"grid-layout-status-strip\">\n <div class=\"grid-status-info\">\n <rpt-icon name=\"table\" [size]=\"12\" />\n <span>{{ summary }}</span>\n </div>\n <div class=\"grid-status-sync\" [class.is-pulsing]=\"layoutJustUpdated()\">\n <rpt-icon name=\"check\" [size]=\"11\" />\n <span>Layout auto-persisted</span>\n </div>\n </div>\n }\n\n <!-- Live Bways Grid bridge -->\n <div class=\"canvas-live-grid-container\" (click)=\"$event.stopPropagation()\" style=\"position: relative; min-height: 250px;\">\n @if (isExecutingGrid()) {\n <div class=\"canvas-grid-loading-overlay\">\n <span class=\"status-dot dot-pulse\" style=\"width: 14px; height: 14px; color: #2563eb; background: #2563eb; border-radius: 50%;\"></span>\n <span class=\"grid-loading-title\">Executing stored procedure on SQL Server...</span>\n <span class=\"grid-loading-sub\">Calculating outstanding balances and ledger amounts. Please wait...</span>\n </div>\n }\n <rpt-grid-bridge\n [gridDefinition]=\"definition().grid!\"\n [data]=\"canvasGridData()\"\n (gridStateChange)=\"onCanvasGridStateChange($event)\"\n (columnDefsChange)=\"onCanvasColumnDefsChange($event)\"\n />\n </div>\n </div>\n } @else {\n <div class=\"canvas-add-grid-banner\" (click)=\"addResultGrid($event)\">\n <div class=\"add-grid-icon\">\n <rpt-icon name=\"table\" [size]=\"20\" />\n </div>\n <div class=\"add-grid-text\">\n <span class=\"add-grid-title\">+ Add Result Data Grid</span>\n <span class=\"add-grid-sub\">Attach an interactive data table or UltraGrid to display report execution output</span>\n </div>\n <button type=\"button\" class=\"btn-banner-add\">Add Grid</button>\n </div>\n }\n </div>\n </div>\n </div>", styles: [":host{display:block;height:100%;background-color:#f1f5f9;font-family:var(--rpt-font-family, inherit);box-sizing:border-box}.canvas-wrapper{display:flex;flex-direction:column;height:100%}.canvas-toolbar{display:flex;align-items:center;justify-content:space-between;min-height:42px;height:42px;padding:0 24px;background:var(--rpt-color-surface, #ffffff);border-bottom:1px solid var(--rpt-color-border, #cbd5e1);box-sizing:border-box;gap:12px;overflow-x:auto;scrollbar-width:none;font-family:var(--rpt-font-family, inherit);flex-shrink:0}.canvas-toolbar::-webkit-scrollbar{display:none}.canvas-info{display:flex;align-items:center;gap:8px;font-family:var(--rpt-font-family, inherit);font-size:var(--rpt-font-size-sm, .75rem);color:var(--rpt-color-text-muted, #64748b);white-space:nowrap;line-height:1}.info-label{color:var(--rpt-color-text-muted, #64748b);font-weight:500;font-size:var(--rpt-font-size-sm, .75rem)}.info-value{font-weight:600;color:var(--rpt-color-text, #0f172a);font-size:var(--rpt-font-size-sm, .75rem)}.info-divider{color:var(--rpt-color-border, #cbd5e1);margin:0 2px}.live-mode-badge{display:inline-flex;align-items:center;gap:4px;font-family:var(--rpt-font-family, inherit);font-size:var(--rpt-font-size-xs, .7rem);font-weight:600;color:#047857;background:#ecfdf5;border:1px solid #a7f3d0;padding:2px 8px;border-radius:9999px;line-height:1}.canvas-actions{display:flex;align-items:center;gap:8px;white-space:nowrap}.canvas-action-btn{display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 10px;font-family:var(--rpt-font-family, inherit);font-size:var(--rpt-font-size-sm, .75rem);font-weight:500;letter-spacing:-.01em;color:var(--rpt-color-text-secondary, #334155);background:var(--rpt-color-surface, #ffffff);border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius, 6px);cursor:pointer;line-height:1;white-space:nowrap;box-shadow:0 1px 2px #0000000a;transition:all var(--rpt-transition-speed, .15s) ease}.canvas-action-btn:hover{background:var(--rpt-color-surface-hover, #f8fafc);border-color:var(--rpt-color-border-hover, #94a3b8);color:var(--rpt-color-text, #0f172a);box-shadow:0 2px 4px #0000000f}.canvas-action-btn.btn-grid-configured{background:#eff6ff;border-color:#bfdbfe;color:#2563eb;font-weight:600}.canvas-action-btn.btn-grid-configured:hover{background:#dbeafe;color:#1d4ed8;border-color:#93c5fd}.canvas-action-btn.btn-grid-add{background:#f0fdf4;border-color:#bbf7d0;color:#16a34a;font-weight:600}.canvas-action-btn.btn-grid-add:hover{background:#dcfce7;color:#15803d;border-color:#86efac}.canvas-action-btn:active{transform:translateY(1px)}.canvas-scroll-area{flex:1;overflow-y:auto;padding:16px}.canvas-artboard{width:100%;max-width:1400px;margin:0 auto;background:#fff;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius-lg, 8px);box-shadow:0 4px 6px -1px #0000000d;min-height:480px;box-sizing:border-box}@media (max-width: 900px){.canvas-toolbar{min-height:36px;height:36px;padding:0 10px;gap:6px}.live-mode-badge{display:none}.canvas-info{font-size:.75rem;gap:5px}.canvas-action-btn{padding:0 8px;font-size:.725rem;height:28px;gap:4px}.canvas-scroll-area{padding:10px}.canvas-artboard{min-height:360px}}@media (max-width: 600px){.canvas-toolbar{padding:0 8px;gap:4px}.canvas-info .info-label,.canvas-action-btn span{display:none}.canvas-action-btn{padding:0 6px;min-width:28px;justify-content:center}}.canvas-report-header{margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid var(--rpt-color-border, #e2e8f0)}.canvas-report-title{font-size:var(--rpt-font-size-xl, 1.5rem);font-weight:700;color:var(--rpt-color-text, #0f172a);margin:0 0 6px}.canvas-report-desc{font-size:var(--rpt-font-size-base, .875rem);color:var(--rpt-color-text-muted, #64748b);margin:0}.canvas-grid{display:grid;width:100%;min-height:180px;align-items:start;border-radius:6px;transition:all var(--rpt-transition-speed, .15s) ease}.canvas-component-card{container-type:inline-size;background:#fff;border:1.5px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius, 6px);box-shadow:var(--rpt-box-shadow, 0 1px 2px rgba(0, 0, 0, .05));box-sizing:border-box;transition:all var(--rpt-transition-speed, .15s) ease;cursor:pointer;position:relative;overflow:visible;display:flex;flex-direction:column;min-height:104px;z-index:1}.canvas-component-card:hover{border-color:var(--rpt-color-primary, #2563eb);box-shadow:0 4px 6px -1px #2563eb1a;z-index:2}.canvas-component-card.is-selected{border-color:var(--rpt-color-primary, #2563eb);box-shadow:0 0 0 2px #2563eb40;z-index:10}.canvas-component-card:focus-within,.canvas-component-card:has(.rpt-combobox-menu),.canvas-component-card:has(.is-open){z-index:100}.canvas-component-card.is-resizing{-webkit-user-select:none;user-select:none;border-color:var(--rpt-color-primary, #2563eb);box-shadow:0 0 0 2px #2563eb59;z-index:30;transition:none!important}.resize-handle{position:absolute;z-index:20;touch-action:none;opacity:0;pointer-events:none;transition:opacity .15s ease}.canvas-component-card.is-selected .resize-handle{opacity:1;pointer-events:auto}.resize-handle.handle-e{top:0;right:0;width:6px;height:100%;cursor:ew-resize;display:flex;align-items:center;justify-content:center}.resize-handle.handle-e:after{content:\"\";width:3px;height:24px;background:#cbd5e1;border-radius:2px;transition:background .15s ease}.resize-handle.handle-e:hover:after,.canvas-component-card.is-selected .resize-handle.handle-e:after{background:var(--rpt-color-primary, #2563eb)}.resize-handle.handle-s{bottom:0;left:0;height:6px;width:100%;cursor:ns-resize;display:flex;align-items:center;justify-content:center}.resize-handle.handle-s:after{content:\"\";height:3px;width:24px;background:#cbd5e1;border-radius:2px;transition:background .15s ease}.resize-handle.handle-s:hover:after,.canvas-component-card.is-selected .resize-handle.handle-s:after{background:var(--rpt-color-primary, #2563eb)}.resize-handle.handle-se{bottom:0;right:0;width:12px;height:12px;cursor:nwse-resize;display:flex;align-items:flex-end;justify-content:flex-end;padding:2px}.resize-handle.handle-se:after{content:\"\";width:5px;height:5px;border-right:2px solid #cbd5e1;border-bottom:2px solid #cbd5e1;transition:border-color .15s ease}.resize-handle.handle-se:hover:after,.canvas-component-card.is-selected .resize-handle.handle-se:after{border-color:var(--rpt-color-primary, #2563eb)}.resize-dimension-tooltip{position:absolute;top:38px;right:8px;background:#0f172ae6;color:#fff;font-size:.7rem;font-weight:600;padding:3px 8px;border-radius:4px;white-space:nowrap;pointer-events:none;z-index:50;box-shadow:0 2px 4px #0003}.card-header{display:flex;align-items:center;justify-content:space-between;gap:6px;padding:5px 8px;background:var(--rpt-color-surface, #f8fafc);border-bottom:1px solid var(--rpt-color-border, #e2e8f0);border-top-left-radius:var(--rpt-border-radius, 6px);border-top-right-radius:var(--rpt-border-radius, 6px);min-height:32px;box-sizing:border-box;overflow:hidden;position:relative}.canvas-component-card.is-selected .card-header{overflow:visible}.card-title-group{display:flex;align-items:center;gap:5px;cursor:grab;min-width:0;flex:1;overflow:hidden}.drag-handle-dots{color:var(--rpt-color-text-muted, #94a3b8);font-size:.85rem;letter-spacing:-2px;flex-shrink:0;-webkit-user-select:none;user-select:none}.comp-title{font-size:.75rem;font-weight:600;color:var(--rpt-color-text, #0f172a);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;-webkit-user-select:none;user-select:none}.card-actions{display:flex;align-items:center;gap:4px;flex-shrink:0;margin-left:auto}.new-row-pill-static{font-size:.65rem;font-weight:600;color:#7c3aed;background:#f5f3ff;border:1px solid #ddd6fe;padding:1px 5px;border-radius:9999px;white-space:nowrap;line-height:1;flex-shrink:0}.span-pill-static{display:inline-flex;align-items:center;justify-content:center;font-size:.65rem;font-weight:600;color:var(--rpt-color-text-muted, #64748b);background:var(--rpt-color-surface, #f1f5f9);border:1px solid var(--rpt-color-border, #e2e8f0);padding:1px 6px;border-radius:9999px;line-height:1.2;white-space:nowrap;-webkit-user-select:none;user-select:none}.card-quick-controls{display:none;align-items:center;gap:3px;flex-shrink:0}.canvas-component-card:hover .span-pill-static,.canvas-component-card.is-selected .span-pill-static,.canvas-component-card:hover .new-row-pill-static,.canvas-component-card.is-selected .new-row-pill-static{display:none}.canvas-component-card:hover .card-quick-controls,.canvas-component-card.is-selected .card-quick-controls{display:inline-flex}.canvas-component-card.is-narrow .card-header{padding:4px 6px;gap:3px}.canvas-component-card.is-narrow:hover .comp-title,.canvas-component-card.is-narrow.is-selected .comp-title,.canvas-component-card.is-narrow:hover .drag-handle-dots,.canvas-component-card.is-narrow.is-selected .drag-handle-dots{display:none}.canvas-component-card.is-narrow .card-quick-controls{gap:2px}.canvas-component-card.is-narrow .span-control-group{height:18px}.canvas-component-card.is-narrow .span-btn{width:14px;height:16px}.canvas-component-card.is-narrow .span-badge{font-size:.6rem;padding:0 2px}.canvas-component-card.is-narrow .span-total{display:none}.canvas-component-card.is-narrow .card-action-btn{width:18px;height:18px}@container (max-width: 250px){.card-header{padding:4px 6px;gap:3px}.canvas-component-card:hover .comp-title,.canvas-component-card.is-selected .comp-title,.canvas-component-card:hover .drag-handle-dots,.canvas-component-card.is-selected .drag-handle-dots,.span-total{display:none}.card-quick-controls{gap:2px}.card-action-btn{width:18px;height:18px}.span-control-group{height:18px}.span-btn{width:14px;height:16px}.span-badge{font-size:.6rem;padding:0 2px}}@container (max-width: 160px){.canvas-component-card.is-selected .card-quick-controls{position:absolute;top:-30px;right:0;background:#fff;border:1.5px solid var(--rpt-color-primary, #2563eb);border-radius:6px;padding:2px 4px;gap:3px;box-shadow:0 4px 12px #2563eb2e,0 2px 4px #00000014;z-index:120;white-space:nowrap}}@media (max-width: 900px){.canvas-component-card:hover .comp-title,.canvas-component-card.is-selected .comp-title,.canvas-component-card:hover .drag-handle-dots,.canvas-component-card.is-selected .drag-handle-dots,.canvas-component-card .span-total{display:none}.canvas-component-card.is-selected .card-header{overflow:visible}.canvas-component-card.is-selected .card-quick-controls{position:absolute;top:-30px;right:0;background:#fff;border:1.5px solid var(--rpt-color-primary, #2563eb);border-radius:6px;padding:2px 4px;gap:3px;box-shadow:0 4px 12px #2563eb2e,0 2px 4px #00000014;z-index:120;white-space:nowrap}}.span-control-group{display:inline-flex;align-items:center;background:#fff;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:4px;height:20px;padding:0 1px;white-space:nowrap;flex-shrink:0}.span-btn{width:16px;height:18px;display:inline-flex;align-items:center;justify-content:center;border:none;background:transparent;padding:0;color:var(--rpt-color-text-muted, #64748b);cursor:pointer;border-radius:2px;transition:all .12s ease}.span-btn:hover:not(:disabled){background:var(--rpt-color-surface-hover, #f1f5f9);color:var(--rpt-color-primary, #2563eb)}.span-btn:disabled{opacity:.3;cursor:not-allowed}.span-badge{font-size:.65rem;font-weight:600;color:var(--rpt-color-text, #0f172a);padding:0 3px;white-space:nowrap;-webkit-user-select:none;user-select:none;line-height:1}.span-total{font-size:.6rem;color:var(--rpt-color-text-muted, #64748b);font-weight:500}.card-action-btn{width:20px;height:20px;border:none;background:transparent;border-radius:4px;display:inline-flex;align-items:center;justify-content:center;padding:0;cursor:pointer;color:var(--rpt-color-text-muted, #64748b);transition:all .12s ease;flex-shrink:0}.card-action-btn:hover{background:#e2e8f0;color:var(--rpt-color-text, #0f172a)}.card-action-btn.btn-active-toggle{background:#7c3aed;color:#fff;border-color:#7c3aed}.card-action-btn.btn-active-toggle:hover{background:#6d28d9;color:#fff}.btn-delete:hover{background:#fee2e2;color:#ef4444}.card-body{padding:8px 10px 14px;flex:1;display:flex;flex-direction:column;min-height:0;min-width:0;max-width:100%;box-sizing:border-box;overflow:visible}.mock-input-field{display:flex;flex-direction:column;gap:4px;flex:1;height:100%;min-height:0}.mock-label{font-size:.7rem;font-weight:600;color:var(--rpt-color-text-muted, #475569);flex-shrink:0}.mock-input-box,.mock-select-box{border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:6px;padding:6px 12px;font-size:var(--rpt-font-size-base, .875rem);color:var(--rpt-color-text-muted, #94a3b8);background:#fff;flex:1;min-height:38px;height:100%;display:flex;align-items:center;justify-content:space-between;box-sizing:border-box}.mock-checkbox-field{display:flex;align-items:center;gap:6px;flex:1;height:100%;min-height:0;padding-top:14px}.mock-check-box{width:18px;height:18px;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:.65rem;color:var(--rpt-color-primary, #2563eb);flex-shrink:0}.mock-btn-wrapper{display:flex;flex:1;height:100%;min-height:38px;align-items:center;justify-content:flex-start;width:100%;min-width:0;box-sizing:border-box}.mock-btn{border-radius:var(--rpt-border-radius, 6px);padding:0 16px;height:36px;font-size:var(--rpt-font-size-base, .875rem);font-weight:500;border:1px solid transparent;cursor:pointer;width:auto;max-width:100%;display:inline-flex;align-items:center;justify-content:center;gap:6px;box-sizing:border-box;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:all .15s ease}.mock-btn.mock-btn-full{width:100%}.canvas-component-card.is-narrow .mock-btn{padding:0 6px;font-size:.775rem;width:100%;height:34px}.mock-kpi-field{display:flex;flex-direction:column;justify-content:space-between;flex:1;height:100%;min-height:0}.mock-kpi-header{display:flex;align-items:center;justify-content:space-between}.mock-kpi-value{font-size:1.25rem;font-weight:700;color:var(--rpt-color-text, #0f172a);margin:2px 0}.mock-kpi-footer{display:flex;align-items:center;gap:6px;font-size:.65rem}.mock-kpi-trend{color:#16a34a;font-weight:600}.mock-kpi-sub{color:#64748b}.mock-chart-field{display:flex;flex-direction:column;flex:1;height:100%;min-height:140px}.mock-chart-bars{display:flex;align-items:flex-end;justify-content:space-around;gap:16px;flex:1;min-height:100px;padding-top:12px;box-sizing:border-box}.mock-chart-col{flex:1;display:flex;flex-direction:column;align-items:center;height:100%;min-width:60px;max-width:140px}.mock-chart-bar{width:38px;max-width:44px;margin:0 auto;background:var(--rpt-color-primary, #2563eb);border-radius:6px 6px 0 0;opacity:.85;min-height:10px;transition:height .2s ease}.mock-bar-lbl{font-size:.6875rem;color:#64748b;margin-top:6px;text-align:center;white-space:normal;line-height:1.2}.btn-primary{background:var(--rpt-color-primary, #2563eb);color:#fff}.btn-secondary{background:#f1f5f9;color:#0f172a;border:1px solid #cbd5e1}.btn-outline{background:transparent;color:#2563eb;border:1px solid #2563eb}.btn-danger{background:#ef4444;color:#fff}.mock-generic-box{border:1px dashed #cbd5e1;border-radius:4px;padding:8px;font-size:.75rem;text-align:center;color:#64748b}.canvas-drag-placeholder{background:#2563eb0f;border:2px dashed var(--rpt-color-primary, #2563eb);border-radius:6px;min-height:80px;box-sizing:border-box;transition:transform .15s ease}.canvas-drag-preview,::ng-deep .canvas-drag-preview{background:#fff;border:2px solid var(--rpt-color-primary, #2563eb);border-radius:6px;padding:8px 14px;display:inline-flex;align-items:center;gap:8px;font-family:var(--rpt-font-family, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif)!important;font-size:.8rem;font-weight:600;color:var(--rpt-color-text, #0f172a);box-shadow:0 12px 24px -4px #00000040;z-index:10000}.preview-title,::ng-deep .preview-title{font-family:inherit!important;max-width:160px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.preview-span,::ng-deep .preview-span{font-family:inherit!important;font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border:1px solid #bfdbfe;padding:1px 6px;border-radius:9999px}.canvas-empty-state{grid-column:1 / -1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:56px 28px;border:2px dashed var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius-xl, 12px);background:var(--rpt-color-surface, #ffffff);text-align:center;box-shadow:var(--rpt-box-shadow-xs, 0 1px 2px 0 rgba(0, 0, 0, .04));transition:border-color var(--rpt-transition-speed, .15s) ease}.canvas-empty-state:hover{border-color:var(--rpt-color-primary, #2563eb)}.empty-hero-badge{width:56px;height:56px;border-radius:var(--rpt-border-radius-xl, 12px);background:var(--rpt-color-primary-light, #eff6ff);color:var(--rpt-color-primary, #2563eb);display:flex;align-items:center;justify-content:center;margin-bottom:16px;box-shadow:0 2px 8px #2563eb1f}.empty-hero-title{font-size:var(--rpt-font-size-lg, 1.125rem);font-weight:700;color:var(--rpt-color-text, #0f172a);margin:0 0 8px;letter-spacing:-.01em}.empty-hero-desc{font-size:var(--rpt-font-size-base, .875rem);color:var(--rpt-color-text-muted, #64748b);max-width:520px;line-height:1.5;margin:0 0 24px}.empty-hero-actions{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:10px}.empty-btn{display:inline-flex;align-items:center;gap:7px;padding:8px 16px;font-size:var(--rpt-font-size-sm, .75rem);font-weight:600;border-radius:var(--rpt-border-radius, 6px);cursor:pointer;transition:all var(--rpt-transition-speed, .15s) ease;font-family:inherit}.empty-btn-primary{background:var(--rpt-color-primary, #2563eb);color:#fff;border:1px solid var(--rpt-color-primary, #2563eb);box-shadow:0 1px 3px #2563eb33}.empty-btn-primary:hover{background:var(--rpt-color-primary-hover, #1d4ed8);border-color:var(--rpt-color-primary-hover, #1d4ed8);transform:translateY(-1px)}.empty-btn-secondary{background:#f8fafc;color:#334155;border:1px solid var(--rpt-color-border, #cbd5e1)}.empty-btn-secondary:hover{background:#f1f5f9;color:#0f172a;border-color:#94a3b8;transform:translateY(-1px)}.empty-btn-outline{background:#fff;color:var(--rpt-color-primary, #2563eb);border:1px solid #bfdbfe}.empty-btn-outline:hover{background:#eff6ff;border-color:var(--rpt-color-primary, #2563eb);transform:translateY(-1px)}.canvas-active-input{width:100%;height:32px;padding:4px 8px;font-size:.8125rem;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:4px;outline:none;box-sizing:border-box;background:#fff;color:#0f172a;transition:border-color .15s ease}.canvas-active-input:focus{border-color:var(--rpt-color-primary, #2563eb)}.canvas-active-checkbox-lbl{display:inline-flex;align-items:center;gap:6px;cursor:pointer}.canvas-live-control-wrapper{width:100%;max-width:100%;min-width:0;position:relative;z-index:1;box-sizing:border-box}.canvas-grid-box{margin-top:24px;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:8px;background:#fff;box-shadow:0 1px 3px #0000000d;overflow:hidden}.grid-preview-header{display:flex;align-items:center;justify-content:space-between;padding:10px 16px;background:var(--rpt-color-surface, #f8fafc);border-bottom:1px solid var(--rpt-color-border, #cbd5e1);font-size:.8rem;font-weight:600;color:var(--rpt-color-text, #0f172a)}.grid-title{font-weight:700}.grid-exec-badge{font-size:.6875rem;font-weight:600;padding:2px 8px;border-radius:10px}.grid-exec-badge.is-loading{background:#eff6ff;color:#2563eb}.grid-exec-badge.is-error{background:#fee2e2;color:#ef4444}.grid-exec-badge.is-success{background:#ecfdf5;color:#059669}.btn-canvas-run-grid{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;font-size:.72rem;font-weight:600;color:#fff;background:var(--rpt-color-primary, #2563eb);border:none;border-radius:4px;cursor:pointer;transition:background .15s ease}.btn-canvas-run-grid:hover:not(:disabled){background:#1d4ed8}.btn-canvas-run-grid:disabled{opacity:.6;cursor:not-allowed}.btn-canvas-edit-grid{display:inline-flex;align-items:center;gap:4px;padding:4px 8px;font-size:.72rem;font-weight:500;color:#334155;background:#fff;border:1px solid #cbd5e1;border-radius:4px;cursor:pointer;transition:all .12s ease}.btn-canvas-edit-grid:hover{background:#f1f5f9}.canvas-live-grid-container{width:100%;min-height:480px;background:#fff}.live-mode-badge{display:inline-flex;align-items:center;gap:4px;font-size:.6875rem;font-weight:700;color:#047857;background:#ecfdf5;border:1px solid #a7f3d0;padding:1px 8px;border-radius:9999px}.canvas-component-card.is-field-highlighted{border-color:#f59e0b!important;box-shadow:0 0 0 3px #f59e0b59!important;animation:rpt-pulse-border 1s ease-in-out infinite alternate}@keyframes rpt-pulse-border{0%{box-shadow:0 0 0 2px #f59e0b40}to{box-shadow:0 0 0 5px #f59e0b80}}.grid-layout-status-strip{display:flex;align-items:center;justify-content:space-between;padding:6px 16px;background:#f1f5f9;border-bottom:1px solid var(--rpt-color-border, #cbd5e1);font-size:.6875rem;color:#475569}.grid-status-info{display:inline-flex;align-items:center;gap:6px;font-weight:600}.grid-status-sync{display:inline-flex;align-items:center;gap:4px;color:#059669;font-weight:600;transition:transform .2s ease}.grid-status-sync.is-pulsing{transform:scale(1.08);color:#047857}.canvas-actions{display:flex;align-items:center;gap:8px}.btn-grid-add{background:#f0fdf4;border-color:#bbf7d0;color:#16a34a}.btn-grid-add:hover{background:#dcfce7;border-color:#86efac;color:#15803d}.btn-remove-grid{display:inline-flex;align-items:center;justify-content:center;padding:4px;border:none;background:transparent;color:#94a3b8;border-radius:4px;cursor:pointer;transition:all .15s ease}.btn-remove-grid:hover{background:#fee2e2;color:#ef4444}.canvas-add-grid-banner{margin-top:24px;border:2px dashed #cbd5e1;border-radius:8px;background:#fff;padding:16px 20px;display:flex;align-items:center;gap:16px;cursor:pointer;transition:all .15s ease}.canvas-add-grid-banner:hover{border-color:#2563eb;background:#f8fafc;box-shadow:0 2px 8px #2563eb14}.add-grid-icon{width:40px;height:40px;border-radius:8px;background:#eff6ff;color:#2563eb;display:flex;align-items:center;justify-content:center;flex-shrink:0}.add-grid-text{display:flex;flex-direction:column;gap:2px;flex:1}.add-grid-title{font-size:.875rem;font-weight:600;color:#0f172a}.add-grid-sub{font-size:.75rem;color:#64748b}.btn-banner-add{padding:6px 14px;font-size:.75rem;font-weight:600;color:#2563eb;background:#eff6ff;border:1px solid #bfdbfe;border-radius:6px;cursor:pointer;white-space:nowrap;transition:all .15s ease}.canvas-add-grid-banner:hover .btn-banner-add{background:#2563eb;color:#fff;border-color:#2563eb}.canvas-grid-loading-overlay{position:absolute;inset:0;background:#ffffffe6;backdrop-filter:blur(2px);z-index:10;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;font-family:inherit;border-radius:6px}.grid-loading-title{font-weight:600;font-size:.95rem;color:#1e293b}.grid-loading-sub{font-size:.8rem;color:#64748b}\n"] }]
|
|
9381
9760
|
}], propDecorators: { onEscape: [{
|
|
9382
9761
|
type: HostListener,
|
|
9383
9762
|
args: ['document:keydown.escape']
|
|
@@ -10040,826 +10419,1048 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
10040
10419
|
`, styles: [".property-group{display:flex;flex-direction:column;gap:12px}.group-title{font-size:.7rem;font-weight:700;color:var(--rpt-color-text-muted, #64748b);text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--rpt-color-border, #e2e8f0);padding-bottom:4px}.field-row{display:flex;flex-direction:column;gap:4px}.field-label-row{display:flex;align-items:center;justify-content:space-between}.field-badge-val{font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border:1px solid #bfdbfe;padding:1px 6px;border-radius:9999px}.field-label{font-size:.7rem;font-weight:600;color:var(--rpt-color-text, #0f172a)}.field-input,.field-select,.field-textarea{width:100%;box-sizing:border-box;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius-sm, 4px);padding:6px 10px;font-size:var(--rpt-font-size-sm, .75rem);color:var(--rpt-color-text, #0f172a);background:#fff;outline:none;transition:all var(--rpt-transition-speed, .15s) ease}.field-select{appearance:none;-webkit-appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right 10px center;background-size:13px 13px;padding-right:28px;cursor:pointer}.field-input:focus,.field-select:focus,.field-textarea:focus{border-color:var(--rpt-color-primary, #2563eb);box-shadow:var(--rpt-box-shadow-focus, 0 0 0 3px rgba(37, 99, 235, .25))}.checkbox-toggle{display:inline-flex;align-items:center;gap:8px;font-size:.75rem;font-weight:500;color:var(--rpt-color-text, #0f172a);cursor:pointer}.preset-pill-group{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.preset-pill{background:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;padding:2px 8px;font-size:.65rem;font-weight:600;color:#475569;cursor:pointer;transition:all .12s ease}.preset-pill:hover{background:#e2e8f0;color:#0f172a;border-color:#cbd5e1}.preset-pill-active{background:var(--rpt-color-primary, #2563eb)!important;color:#fff!important;border-color:var(--rpt-color-primary, #2563eb)!important}.preset-pill-reset{color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border-color:#bfdbfe}.preset-pill-reset:hover{background:#dbeafe;color:#1d4ed8;border-color:#93c5fd}\n"] }]
|
|
10041
10420
|
}] });
|
|
10042
10421
|
|
|
10043
|
-
class
|
|
10044
|
-
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10054
|
-
|
|
10422
|
+
class ParameterBindingEditorComponent {
|
|
10423
|
+
sessionService = inject(ReportSessionContextService);
|
|
10424
|
+
param = input.required();
|
|
10425
|
+
currentValue = input('');
|
|
10426
|
+
allComponents = input([]);
|
|
10427
|
+
currentComponentId = input('');
|
|
10428
|
+
isAdvancedMode = input(false);
|
|
10429
|
+
valueChange = output();
|
|
10430
|
+
linkedFieldChange = output();
|
|
10431
|
+
internalMode = signal(null);
|
|
10432
|
+
sessionMatch = computed(() => this.sessionService.matchParameter(this.param().name));
|
|
10433
|
+
friendlyLabel = computed(() => {
|
|
10434
|
+
return this.param().friendlyLabel || humanizeIdentifier(this.param().name);
|
|
10055
10435
|
});
|
|
10056
|
-
|
|
10057
|
-
|
|
10436
|
+
displayType = computed(() => {
|
|
10437
|
+
switch (this.param().dataType) {
|
|
10438
|
+
case 'boolean':
|
|
10439
|
+
return 'Yes / No';
|
|
10440
|
+
case 'number':
|
|
10441
|
+
return 'Number';
|
|
10442
|
+
case 'date':
|
|
10443
|
+
return 'Date';
|
|
10444
|
+
default:
|
|
10445
|
+
return 'Text';
|
|
10446
|
+
}
|
|
10058
10447
|
});
|
|
10059
|
-
|
|
10060
|
-
return
|
|
10448
|
+
isCurrentValue = (val) => {
|
|
10449
|
+
return String(this.currentValue()) === String(val);
|
|
10450
|
+
};
|
|
10451
|
+
paramQuickSuggestions = computed(() => {
|
|
10452
|
+
const name = this.param().name.toLowerCase().replace(/^@/, '');
|
|
10453
|
+
if (name === 'inttranorduedate') {
|
|
10454
|
+
return [
|
|
10455
|
+
{ label: '0: Transaction Date', value: 0 },
|
|
10456
|
+
{ label: '1: Due Date', value: 1 },
|
|
10457
|
+
{ label: '2: GIR Date', value: 2 },
|
|
10458
|
+
];
|
|
10459
|
+
}
|
|
10460
|
+
if (name === 'intonlydue') {
|
|
10461
|
+
return [
|
|
10462
|
+
{ label: '0: All Bills', value: 0 },
|
|
10463
|
+
{ label: '1: Only Due Bills', value: 1 },
|
|
10464
|
+
];
|
|
10465
|
+
}
|
|
10466
|
+
if (name === 'intvalue') {
|
|
10467
|
+
return [
|
|
10468
|
+
{ label: '11: Customer Ledger', value: 11 },
|
|
10469
|
+
{ label: '12: Supplier Ledger', value: 12 },
|
|
10470
|
+
];
|
|
10471
|
+
}
|
|
10472
|
+
if (name === 'chrcompanyid') {
|
|
10473
|
+
return [
|
|
10474
|
+
{ label: '101: Default Co', value: '101' },
|
|
10475
|
+
];
|
|
10476
|
+
}
|
|
10477
|
+
if (name === 'chrcompunitid') {
|
|
10478
|
+
return [
|
|
10479
|
+
{ label: '101: Unit 101', value: '101' },
|
|
10480
|
+
{ label: '101,102,103: All Units', value: '101,102,103' },
|
|
10481
|
+
];
|
|
10482
|
+
}
|
|
10483
|
+
if (name === 'chrsubacnolist') {
|
|
10484
|
+
return [
|
|
10485
|
+
{ label: '0: All Accounts', value: '0' },
|
|
10486
|
+
];
|
|
10487
|
+
}
|
|
10488
|
+
if (name === 'intwithcity') {
|
|
10489
|
+
return [
|
|
10490
|
+
{ label: '0: No', value: 0 },
|
|
10491
|
+
{ label: '1: Yes', value: 1 },
|
|
10492
|
+
];
|
|
10493
|
+
}
|
|
10494
|
+
return [];
|
|
10061
10495
|
});
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
if (!mapping)
|
|
10065
|
-
return [];
|
|
10066
|
-
return Object.entries(mapping).map(([key, value]) => ({ key, value }));
|
|
10496
|
+
compatibleControls = computed(() => {
|
|
10497
|
+
return this.allComponents().filter((c) => c.id !== this.currentComponentId());
|
|
10067
10498
|
});
|
|
10068
|
-
|
|
10069
|
-
this.
|
|
10070
|
-
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
|
|
10076
|
-
|
|
10499
|
+
hasValue = computed(() => {
|
|
10500
|
+
const v = this.currentValue();
|
|
10501
|
+
return v !== undefined && v !== null && v !== '';
|
|
10502
|
+
});
|
|
10503
|
+
activeMode = computed(() => {
|
|
10504
|
+
if (this.internalMode() !== null) {
|
|
10505
|
+
return this.internalMode();
|
|
10506
|
+
}
|
|
10507
|
+
const val = String(this.currentValue() ?? '');
|
|
10508
|
+
// If matches session context and has no explicit manual override, default to session
|
|
10509
|
+
if (this.sessionMatch() !== null && (!val || val.startsWith('{{session.'))) {
|
|
10510
|
+
return 'session';
|
|
10511
|
+
}
|
|
10512
|
+
if (val.startsWith('{{context.')) {
|
|
10513
|
+
return 'context';
|
|
10514
|
+
}
|
|
10515
|
+
if (val.startsWith('{{session.')) {
|
|
10516
|
+
return 'session';
|
|
10517
|
+
}
|
|
10518
|
+
if (val.startsWith('{{') && val.endsWith('}}')) {
|
|
10519
|
+
const inner = val.slice(2, -2).trim();
|
|
10520
|
+
const match = this.compatibleControls().find((c) => (c.key || c.id) === inner);
|
|
10521
|
+
if (match) {
|
|
10522
|
+
return 'control';
|
|
10523
|
+
}
|
|
10524
|
+
return 'custom';
|
|
10525
|
+
}
|
|
10526
|
+
const plainMatch = this.compatibleControls().find((c) => (c.key || c.id) === val);
|
|
10527
|
+
if (plainMatch) {
|
|
10528
|
+
return 'control';
|
|
10529
|
+
}
|
|
10530
|
+
if (!val) {
|
|
10531
|
+
// Smart default: numeric or flag parameters default to 'fixed' (Enter Value)
|
|
10532
|
+
const name = this.param().name.toLowerCase();
|
|
10533
|
+
if (this.param().dataType === 'number' ||
|
|
10534
|
+
name.startsWith('int') ||
|
|
10535
|
+
name.includes('flag') ||
|
|
10536
|
+
name.includes('type') ||
|
|
10537
|
+
name.includes('only')) {
|
|
10538
|
+
return 'fixed';
|
|
10539
|
+
}
|
|
10540
|
+
// If there are compatible form fields on canvas, default to control, otherwise fixed
|
|
10541
|
+
return this.compatibleControls().length > 0 ? 'control' : 'fixed';
|
|
10542
|
+
}
|
|
10543
|
+
return 'fixed';
|
|
10544
|
+
});
|
|
10545
|
+
isAutoSessionDerived = computed(() => {
|
|
10546
|
+
return this.sessionMatch() !== null && this.activeMode() === 'session';
|
|
10547
|
+
});
|
|
10548
|
+
selectedControlKey = computed(() => {
|
|
10549
|
+
const val = String(this.currentValue() ?? '');
|
|
10550
|
+
if (val.startsWith('{{') && val.endsWith('}}')) {
|
|
10551
|
+
return val.slice(2, -2).trim();
|
|
10552
|
+
}
|
|
10553
|
+
return val;
|
|
10554
|
+
});
|
|
10555
|
+
selectedContextKey = computed(() => {
|
|
10556
|
+
const val = String(this.currentValue() ?? '');
|
|
10557
|
+
if (val.startsWith('{{context.')) {
|
|
10558
|
+
return val;
|
|
10559
|
+
}
|
|
10560
|
+
return '';
|
|
10561
|
+
});
|
|
10562
|
+
booleanFixedValue = computed(() => {
|
|
10563
|
+
const v = this.currentValue();
|
|
10564
|
+
return v === true || v === 'true' || v === 1 || v === '1';
|
|
10565
|
+
});
|
|
10566
|
+
setMode(mode) {
|
|
10567
|
+
this.internalMode.set(mode);
|
|
10568
|
+
if (mode === 'session') {
|
|
10569
|
+
const match = this.sessionMatch();
|
|
10570
|
+
if (match) {
|
|
10571
|
+
this.valueChange.emit(`{{session.${match.sessionKey}}}`);
|
|
10572
|
+
}
|
|
10573
|
+
this.linkedFieldChange.emit({
|
|
10574
|
+
paramName: this.param().name,
|
|
10575
|
+
fieldKey: '',
|
|
10576
|
+
isLinked: false,
|
|
10077
10577
|
});
|
|
10578
|
+
return;
|
|
10078
10579
|
}
|
|
10079
|
-
|
|
10080
|
-
this.
|
|
10580
|
+
if (mode === 'fixed') {
|
|
10581
|
+
const current = this.currentValue();
|
|
10582
|
+
if (current !== undefined && current !== null && current !== '' && !String(current).startsWith('{{')) {
|
|
10583
|
+
this.valueChange.emit(current);
|
|
10584
|
+
}
|
|
10585
|
+
else if (this.param().dataType === 'boolean') {
|
|
10586
|
+
const def = this.param().defaultValue !== undefined ? Boolean(this.param().defaultValue) : true;
|
|
10587
|
+
this.valueChange.emit(def);
|
|
10588
|
+
}
|
|
10589
|
+
else if (this.param().dataType === 'number') {
|
|
10590
|
+
const def = this.param().defaultValue !== undefined ? Number(this.param().defaultValue) : 0;
|
|
10591
|
+
this.valueChange.emit(def);
|
|
10592
|
+
}
|
|
10593
|
+
else {
|
|
10594
|
+
this.valueChange.emit('');
|
|
10595
|
+
}
|
|
10596
|
+
this.linkedFieldChange.emit({
|
|
10597
|
+
paramName: this.param().name,
|
|
10598
|
+
fieldKey: '',
|
|
10599
|
+
isLinked: false,
|
|
10600
|
+
});
|
|
10081
10601
|
}
|
|
10082
10602
|
}
|
|
10083
|
-
|
|
10084
|
-
|
|
10085
|
-
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10603
|
+
onControlSelected(key) {
|
|
10604
|
+
if (key) {
|
|
10605
|
+
const expr = `{{${key}}}`;
|
|
10606
|
+
this.valueChange.emit(expr);
|
|
10607
|
+
this.linkedFieldChange.emit({
|
|
10608
|
+
paramName: this.param().name,
|
|
10609
|
+
fieldKey: key,
|
|
10610
|
+
isLinked: true,
|
|
10611
|
+
});
|
|
10612
|
+
}
|
|
10613
|
+
else {
|
|
10614
|
+
this.valueChange.emit('');
|
|
10615
|
+
this.linkedFieldChange.emit({
|
|
10616
|
+
paramName: this.param().name,
|
|
10617
|
+
fieldKey: '',
|
|
10618
|
+
isLinked: false,
|
|
10619
|
+
});
|
|
10096
10620
|
}
|
|
10097
|
-
mapping[key] = 'row.id';
|
|
10098
|
-
this.updateRowClickAction.emit({ ...current, parameterMapping: mapping });
|
|
10099
10621
|
}
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10103
|
-
|
|
10104
|
-
|
|
10105
|
-
|
|
10106
|
-
const k = i === index ? newKey : p.key;
|
|
10107
|
-
if (k.trim()) {
|
|
10108
|
-
nextMapping[k.trim()] = p.value;
|
|
10109
|
-
}
|
|
10622
|
+
onBooleanChanged(checked) {
|
|
10623
|
+
this.valueChange.emit(checked);
|
|
10624
|
+
this.linkedFieldChange.emit({
|
|
10625
|
+
paramName: this.param().name,
|
|
10626
|
+
fieldKey: '',
|
|
10627
|
+
isLinked: false,
|
|
10110
10628
|
});
|
|
10111
|
-
const current = this.rowClickAction() ?? { type: 'open-drawer' };
|
|
10112
|
-
this.updateRowClickAction.emit({ ...current, parameterMapping: nextMapping });
|
|
10113
10629
|
}
|
|
10114
|
-
|
|
10115
|
-
|
|
10116
|
-
|
|
10117
|
-
|
|
10118
|
-
|
|
10119
|
-
|
|
10120
|
-
const v = i === index ? newValue : p.value;
|
|
10121
|
-
if (p.key.trim()) {
|
|
10122
|
-
nextMapping[p.key.trim()] = v;
|
|
10123
|
-
}
|
|
10630
|
+
onFixedValueChanged(val) {
|
|
10631
|
+
this.valueChange.emit(val);
|
|
10632
|
+
this.linkedFieldChange.emit({
|
|
10633
|
+
paramName: this.param().name,
|
|
10634
|
+
fieldKey: '',
|
|
10635
|
+
isLinked: false,
|
|
10124
10636
|
});
|
|
10125
|
-
const current = this.rowClickAction() ?? { type: 'open-drawer' };
|
|
10126
|
-
this.updateRowClickAction.emit({ ...current, parameterMapping: nextMapping });
|
|
10127
10637
|
}
|
|
10128
|
-
|
|
10129
|
-
|
|
10130
|
-
|
|
10131
|
-
|
|
10132
|
-
|
|
10133
|
-
|
|
10134
|
-
}
|
|
10638
|
+
onContextSelected(ctxKey) {
|
|
10639
|
+
this.valueChange.emit(ctxKey);
|
|
10640
|
+
this.linkedFieldChange.emit({
|
|
10641
|
+
paramName: this.param().name,
|
|
10642
|
+
fieldKey: '',
|
|
10643
|
+
isLinked: false,
|
|
10135
10644
|
});
|
|
10136
|
-
const current = this.rowClickAction() ?? { type: 'open-drawer' };
|
|
10137
|
-
this.updateRowClickAction.emit({ ...current, parameterMapping: nextMapping });
|
|
10138
10645
|
}
|
|
10139
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type:
|
|
10140
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: PropertyEditorGridComponent, isStandalone: true, selector: "rpt-property-editor-grid", inputs: { grid: { classPropertyName: "grid", publicName: "grid", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { addGrid: "addGrid", removeGrid: "removeGrid", addColumn: "addColumn", autoDetectColumns: "autoDetectColumns", removeColumn: "removeColumn", updateColumn: "updateColumn", updateOption: "updateOption", updateRowClickAction: "updateRowClickAction" }, ngImport: i0, template: " @if (!grid()) {\n <div class=\"empty-grid-card\">\n <div class=\"empty-grid-icon\">\n <rpt-icon name=\"table\" [size]=\"28\" />\n </div>\n <h4 class=\"empty-grid-title\">No Result Grid Attached</h4>\n <p class=\"empty-grid-desc\">Attach an interactive Result Data Grid (or UltraGrid) to this report definition to render queries and stored procedure results.</p>\n <button type=\"button\" class=\"btn-create-grid-action\" (click)=\"addGrid.emit()\">\n <rpt-icon name=\"plus\" [size]=\"14\" />\n <span>+ Add Result Grid</span>\n </button>\n </div>\n } @else {\n <div class=\"props-form\">\n <div class=\"property-group\">\n <div class=\"group-header-with-btn\">\n <span class=\"group-title\">Columns ({{ columns().length }})</span>\n <div style=\"display: flex; gap: 4px;\">\n <button\n type=\"button\"\n class=\"btn-add-col\"\n style=\"background: #eff6ff; color: #2563eb; border-color: #bfdbfe;\"\n (click)=\"autoDetectColumns.emit()\"\n title=\"Auto-detect all columns from procedure or data table\"\n >\n <rpt-icon name=\"refresh\" [size]=\"11\" />\n <span>Auto-Detect</span>\n </button>\n <button type=\"button\" class=\"btn-add-col\" (click)=\"addColumn.emit()\">\n <rpt-icon name=\"plus\" [size]=\"12\" />\n <span>Add Column</span>\n </button>\n </div>\n </div>\n\n <div class=\"columns-editor-list\">\n @for (col of columns(); track col.field; let idx = $index) {\n <div class=\"col-edit-card\">\n <div class=\"col-card-header\">\n <div class=\"col-header-title-group\">\n <span class=\"col-drag-dots\">\u22EE\u22EE</span>\n <span class=\"col-idx-badge\">#{{ idx + 1 }}</span>\n <input\n type=\"text\"\n class=\"col-title-input\"\n placeholder=\"Column Label\"\n [ngModel]=\"col.label\"\n (ngModelChange)=\"onUpdateCol(idx, 'label', $event)\"\n />\n </div>\n <div class=\"col-card-actions\">\n <span class=\"col-type-pill\">{{ col.dataType || 'string' }}</span>\n <button\n type=\"button\"\n class=\"col-action-btn btn-delete\"\n (click)=\"removeColumn.emit(idx)\"\n title=\"Remove Column\"\n >\n <rpt-icon name=\"trash\" [size]=\"12\" />\n </button>\n </div>\n </div>\n\n <div class=\"col-card-body\">\n <div class=\"col-edit-fields\">\n <div class=\"col-field-item\">\n <span class=\"mini-label\">Field</span>\n <input\n type=\"text\"\n class=\"col-mini-input\"\n placeholder=\"field\"\n [ngModel]=\"col.field\"\n (ngModelChange)=\"onUpdateCol(idx, 'field', $event)\"\n />\n </div>\n <div class=\"col-field-item\">\n <span class=\"mini-label\">Type</span>\n <select\n class=\"col-mini-select\"\n [ngModel]=\"col.dataType || 'string'\"\n (ngModelChange)=\"onUpdateCol(idx, 'dataType', $event)\"\n >\n <option value=\"string\">String</option>\n <option value=\"number\">Number</option>\n <option value=\"currency\">Currency</option>\n <option value=\"date\">Date</option>\n <option value=\"boolean\">Boolean</option>\n </select>\n </div>\n <div class=\"col-field-item\">\n <span class=\"mini-label\">Format</span>\n <input\n type=\"text\"\n class=\"col-mini-input\"\n placeholder=\"e.g. dd/MM/yyyy\"\n [ngModel]=\"col.format\"\n (ngModelChange)=\"onUpdateCol(idx, 'format', $event)\"\n />\n </div>\n <div class=\"col-field-item\">\n <span class=\"mini-label\">Align</span>\n <select\n class=\"col-mini-select\"\n [ngModel]=\"col.align || 'left'\"\n (ngModelChange)=\"onUpdateCol(idx, 'align', $event)\"\n >\n <option value=\"left\">Left</option>\n <option value=\"center\">Center</option>\n <option value=\"right\">Right</option>\n </select>\n </div>\n <div class=\"col-field-item\">\n <span class=\"mini-label\">Aggregate</span>\n <select\n class=\"col-mini-select\"\n [ngModel]=\"col.aggregate || 'none'\"\n (ngModelChange)=\"onUpdateCol(idx, 'aggregate', $event === 'none' ? undefined : $event)\"\n >\n <option value=\"none\">None</option>\n <option value=\"sum\">Sum</option>\n <option value=\"avg\">Average</option>\n <option value=\"count\">Count</option>\n <option value=\"min\">Min</option>\n <option value=\"max\">Max</option>\n </select>\n </div>\n <div class=\"col-field-item\">\n <span class=\"mini-label\">Width</span>\n <input\n type=\"text\"\n class=\"col-mini-input\"\n placeholder=\"auto\"\n [ngModel]=\"col.width || ''\"\n (ngModelChange)=\"onUpdateCol(idx, 'width', $event || undefined)\"\n />\n </div>\n </div>\n\n <div class=\"col-card-footer\">\n <label class=\"col-checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"col.visible !== false\"\n (ngModelChange)=\"onUpdateCol(idx, 'visible', $event)\"\n />\n <span>Visible</span>\n </label>\n <label class=\"col-checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"col.checkboxSelection ?? false\"\n (ngModelChange)=\"onUpdateCol(idx, 'checkboxSelection', $event)\"\n />\n <span>Checkbox</span>\n </label>\n </div>\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Grid Table Options Group -->\n <div class=\"property-group\">\n <div class=\"group-title\">Table Options</div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.sideBar !== false\"\n (ngModelChange)=\"updateOption.emit({ field: 'sideBar', value: $event })\"\n />\n <span>Enable Side Panel (Tool Panels)</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.globalSearch !== false\"\n (ngModelChange)=\"updateOption.emit({ field: 'globalSearch', value: $event })\"\n />\n <span>Global Search Box</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.statusBar !== false\"\n (ngModelChange)=\"updateOption.emit({ field: 'statusBar', value: $event })\"\n />\n <span>Status Bar</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.pagination ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'pagination', value: $event })\"\n />\n <span>Enable Pagination</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.selectable ?? false\"\n (ngModelChange)=\"updateOption.emit({ field: 'selectable', value: $event })\"\n />\n <span>Row Selection</span>\n </label>\n </div>\n\n @if (grid()?.options?.selectable) {\n <div class=\"field-row\">\n <label class=\"field-label\">Selection Mode</label>\n <select\n class=\"field-input\"\n [ngModel]=\"grid()?.options?.selectionMode ?? 'multiple'\"\n (ngModelChange)=\"updateOption.emit({ field: 'selectionMode', value: $event })\"\n >\n <option value=\"multiple\">Multiple Rows</option>\n <option value=\"single\">Single Row</option>\n </select>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.checkboxSelection ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'checkboxSelection', value: $event })\"\n />\n <span>Checkbox Selection</span>\n </label>\n </div>\n\n @if ((grid()?.options?.selectionMode ?? 'multiple') === 'multiple') {\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.headerCheckboxSelection ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'headerCheckboxSelection', value: $event })\"\n />\n <span>Header Select All Checkbox</span>\n </label>\n </div>\n }\n }\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.allowColumnReorder ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'allowColumnReorder', value: $event })\"\n />\n <span>Allow Column Reorder</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.allowExport ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'allowExport', value: $event })\"\n />\n <span>Allow Export</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"field-label\">Page Size</label>\n <input\n type=\"number\"\n class=\"field-input\"\n min=\"5\"\n max=\"100\"\n [ngModel]=\"grid()?.options?.pageSize ?? 10\"\n (ngModelChange)=\"updateOption.emit({ field: 'pageSize', value: +$event })\"\n />\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.showRowNumbers ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'showRowNumbers', value: $event })\"\n />\n <span>Show Row Numbers</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.showFooter ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'showFooter', value: $event })\"\n />\n <span>Show Footer Totals</span>\n </label>\n </div>\n </div>\n\n <!-- Row Click & Drill-Down Action Group -->\n <div class=\"property-group\">\n <div class=\"group-title\">Row Click & Drill-Down</div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"hasRowClickAction()\"\n (ngModelChange)=\"toggleRowClickAction($event)\"\n />\n <span>Enable Row Click Action</span>\n </label>\n </div>\n\n @if (hasRowClickAction()) {\n <div class=\"field-row\">\n <label class=\"field-label\">Action Type</label>\n <select\n class=\"field-input field-select\"\n [ngModel]=\"rowClickAction()?.type || 'open-drawer'\"\n (ngModelChange)=\"updateRowActionField('type', $event)\"\n >\n <option value=\"open-drawer\">Slide-Over Detail Drawer</option>\n <option value=\"drilldown-report\">Drill-Down Report (Host Event)</option>\n <option value=\"navigate\">Navigate to URL</option>\n <option value=\"emit-custom\">Emit Custom Action</option>\n </select>\n </div>\n\n @if (rowClickAction()?.type === 'open-drawer' || rowClickAction()?.type === 'drilldown-report') {\n <div class=\"field-row\">\n <label class=\"field-label\">Target Report ID</label>\n <input\n type=\"text\"\n class=\"field-input\"\n placeholder=\"e.g. order-details\"\n [ngModel]=\"rowClickAction()?.targetReportId || ''\"\n (ngModelChange)=\"updateRowActionField('targetReportId', $event)\"\n />\n </div>\n\n <div class=\"field-row\">\n <label class=\"field-label\">Drawer Title</label>\n <input\n type=\"text\"\n class=\"field-input\"\n placeholder=\"e.g. Order Details\"\n [ngModel]=\"rowClickAction()?.drawerTitle || ''\"\n (ngModelChange)=\"updateRowActionField('drawerTitle', $event)\"\n />\n </div>\n\n <div class=\"field-row\">\n <label class=\"field-label\">Drawer Width</label>\n <input\n type=\"text\"\n class=\"field-input\"\n placeholder=\"e.g. 760px, 80%, 900px\"\n [ngModel]=\"rowClickAction()?.drawerWidth || '760px'\"\n (ngModelChange)=\"updateRowActionField('drawerWidth', $event)\"\n />\n </div>\n }\n\n @if (rowClickAction()?.type === 'navigate') {\n <div class=\"field-row\">\n <label class=\"field-label\">Target URL</label>\n <input\n type=\"text\"\n class=\"field-input\"\n [placeholder]=\"'/orders/{{row.orderId}} or https://...'\"\n [ngModel]=\"rowClickAction()?.targetUrl || ''\"\n (ngModelChange)=\"updateRowActionField('targetUrl', $event)\"\n />\n </div>\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"rowClickAction()?.openNewTab ?? false\"\n (ngModelChange)=\"updateRowActionField('openNewTab', $event)\"\n />\n <span>Open in New Tab</span>\n </label>\n </div>\n }\n\n @if (rowClickAction()?.type === 'emit-custom') {\n <div class=\"field-row\">\n <label class=\"field-label\">Custom Action Name</label>\n <input\n type=\"text\"\n class=\"field-input\"\n placeholder=\"e.g. view-audit-trail\"\n [ngModel]=\"rowClickAction()?.customActionName || ''\"\n (ngModelChange)=\"updateRowActionField('customActionName', $event)\"\n />\n </div>\n }\n\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">Parameter Mapping</label>\n <button\n type=\"button\"\n class=\"btn-add-mapping\"\n (click)=\"addParameterMapping()\"\n >\n + Add Mapping\n </button>\n </div>\n\n @for (mapping of parameterMappings(); track mapping.key; let mIdx = $index) {\n <div class=\"mapping-pair-row\">\n <input\n type=\"text\"\n class=\"col-mini-input\"\n style=\"flex: 1;\"\n placeholder=\"Target (e.g. customerId)\"\n [ngModel]=\"mapping.key\"\n (ngModelChange)=\"updateMappingKey(mIdx, $event)\"\n />\n <span style=\"font-size: 0.75rem; color: #94a3b8;\">\u2190</span>\n <input\n type=\"text\"\n class=\"col-mini-input\"\n style=\"flex: 1;\"\n placeholder=\"Source (e.g. row.id)\"\n [ngModel]=\"mapping.value\"\n (ngModelChange)=\"updateMappingValue(mIdx, $event)\"\n />\n <button\n type=\"button\"\n class=\"col-action-btn btn-delete\"\n (click)=\"removeMapping(mIdx)\"\n title=\"Remove mapping\"\n >\n <rpt-icon name=\"trash\" [size]=\"11\" />\n </button>\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Remove Grid Action -->\n <div class=\"grid-danger-zone\">\n <button type=\"button\" class=\"btn-remove-grid-action\" (click)=\"removeGrid.emit()\">\n <rpt-icon name=\"trash\" [size]=\"13\" />\n <span>Remove Grid from Report</span>\n </button>\n </div>\n </div>\n }", styles: [".property-group{display:flex;flex-direction:column;gap:12px}.group-title{font-size:.7rem;font-weight:700;color:var(--rpt-color-text-muted, #64748b);text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--rpt-color-border, #e2e8f0);padding-bottom:4px}.field-row{display:flex;flex-direction:column;gap:4px}.field-label-row{display:flex;align-items:center;justify-content:space-between}.field-badge-val{font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border:1px solid #bfdbfe;padding:1px 6px;border-radius:9999px}.field-label{font-size:.7rem;font-weight:600;color:var(--rpt-color-text, #0f172a)}.field-input,.field-select,.field-textarea{width:100%;box-sizing:border-box;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius-sm, 4px);padding:6px 10px;font-size:var(--rpt-font-size-sm, .75rem);color:var(--rpt-color-text, #0f172a);background:#fff;outline:none;transition:all var(--rpt-transition-speed, .15s) ease}.field-select{appearance:none;-webkit-appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right 10px center;background-size:13px 13px;padding-right:28px;cursor:pointer}.field-input:focus,.field-select:focus,.field-textarea:focus{border-color:var(--rpt-color-primary, #2563eb);box-shadow:var(--rpt-box-shadow-focus, 0 0 0 3px rgba(37, 99, 235, .25))}.checkbox-toggle{display:inline-flex;align-items:center;gap:8px;font-size:.75rem;font-weight:500;color:var(--rpt-color-text, #0f172a);cursor:pointer}.preset-pill-group{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.preset-pill{background:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;padding:2px 8px;font-size:.65rem;font-weight:600;color:#475569;cursor:pointer;transition:all .12s ease}.preset-pill:hover{background:#e2e8f0;color:#0f172a;border-color:#cbd5e1}.preset-pill-active{background:var(--rpt-color-primary, #2563eb)!important;color:#fff!important;border-color:var(--rpt-color-primary, #2563eb)!important}.preset-pill-reset{color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border-color:#bfdbfe}.preset-pill-reset:hover{background:#dbeafe;color:#1d4ed8;border-color:#93c5fd}\n", ".empty-grid-card{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 16px;text-align:center;border:2px dashed #cbd5e1;border-radius:8px;background:#fff;margin:10px 0}.empty-grid-icon{width:48px;height:48px;border-radius:10px;background:#eff6ff;color:#2563eb;display:flex;align-items:center;justify-content:center;margin-bottom:12px}.empty-grid-title{font-size:.95rem;font-weight:700;color:#0f172a;margin:0 0 6px}.empty-grid-desc{font-size:.775rem;color:#64748b;margin:0 0 18px;line-height:1.4;max-width:280px}.btn-create-grid-action{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;background:#2563eb;color:#fff;font-size:.775rem;font-weight:600;border:none;border-radius:6px;cursor:pointer;box-shadow:0 2px 4px #2563eb33;transition:all .15s ease}.btn-create-grid-action:hover{background:#1d4ed8;box-shadow:0 4px 8px #2563eb40}.grid-danger-zone{margin-top:10px;padding-top:16px;border-top:1px dashed #e2e8f0;display:flex;justify-content:center}.btn-remove-grid-action{display:inline-flex;align-items:center;gap:6px;background:#fee2e2;border:1px solid #fecaca;color:#dc2626;font-size:.75rem;font-weight:600;padding:6px 12px;border-radius:6px;cursor:pointer;transition:all .15s ease}.btn-remove-grid-action:hover{background:#fecaca;border-color:#f87171;color:#b91c1c}.props-form{display:flex;flex-direction:column;gap:20px}.group-header-with-btn{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--rpt-color-border, #e2e8f0);padding-bottom:8px;margin-bottom:4px}.btn-add-col{display:inline-flex;align-items:center;gap:5px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:4px;padding:4px 10px;font-size:.725rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);cursor:pointer;transition:all .15s ease}.btn-add-col:hover{background:var(--rpt-color-primary, #2563eb);color:#fff;border-color:var(--rpt-color-primary, #2563eb)}.columns-editor-list{display:flex;flex-direction:column;gap:10px}.col-edit-card{background:#fff;border:1.5px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius, 6px);box-shadow:var(--rpt-box-shadow, 0 1px 2px rgba(0, 0, 0, .05));box-sizing:border-box;transition:all var(--rpt-transition-speed, .15s) ease;position:relative;overflow:hidden;display:flex;flex-direction:column}.col-edit-card:hover{border-color:var(--rpt-color-primary, #2563eb);box-shadow:0 4px 6px -1px #2563eb1a}.col-card-header{display:flex;align-items:center;justify-content:space-between;gap:6px;padding:5px 8px;background:var(--rpt-color-surface, #f8fafc);border-bottom:1px solid var(--rpt-color-border, #e2e8f0);min-height:32px;box-sizing:border-box}.col-header-title-group{display:flex;align-items:center;gap:5px;min-width:0;flex:1}.col-drag-dots{color:#94a3b8;font-size:.8rem;cursor:grab;-webkit-user-select:none;user-select:none;line-height:1;padding-left:2px}.col-idx-badge{font-size:.65rem;font-weight:700;color:#64748b;background:#e2e8f0;padding:1px 5px;border-radius:3px;letter-spacing:.02em}.col-title-input{flex:1;border:1px solid transparent;background:transparent;font-size:.75rem;font-weight:600;color:var(--rpt-color-text, #0f172a);padding:2px 6px;border-radius:4px;outline:none;transition:all .15s ease}.col-title-input:hover{border-color:#cbd5e1;background:#fff}.col-title-input:focus{border-color:var(--rpt-color-primary, #2563eb);background:#fff;box-shadow:0 0 0 2px #2563eb26}.col-card-actions{display:flex;align-items:center;gap:4px}.col-type-pill{font-size:.625rem;font-weight:700;color:#1e40af;background:#dbeafe;border:1px solid #bfdbfe;padding:1px 5px;border-radius:3px;text-transform:uppercase;letter-spacing:.04em}.col-action-btn{display:inline-flex;align-items:center;justify-content:center;background:none;border:none;color:var(--rpt-color-text-muted, #64748b);cursor:pointer;padding:3px;border-radius:4px;transition:all .15s ease}.col-action-btn.btn-delete:hover{color:var(--rpt-color-danger, #ef4444);background:#fee2e2}.col-card-body{padding:8px 10px;display:flex;flex-direction:column;gap:6px;background:#fff}.col-edit-fields{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px 8px}.col-field-item{display:flex;flex-direction:column;gap:2px;min-width:0}.mini-label{font-size:.625rem;font-weight:600;color:#64748b;letter-spacing:.02em}.col-mini-input,.col-mini-select{width:100%;box-sizing:border-box;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:3px;padding:3px 5px;font-size:.7rem;color:var(--rpt-color-text, #0f172a);background:#fff;outline:none;transition:all .12s ease}.col-mini-select{appearance:none;-webkit-appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right 4px center;background-size:10px 10px;padding-right:16px;cursor:pointer}.col-mini-input:focus,.col-mini-select:focus{border-color:var(--rpt-color-primary, #2563eb);box-shadow:0 0 0 2px #2563eb26}.col-card-footer{display:flex;align-items:center;justify-content:flex-end;gap:12px;padding-top:4px;border-top:1px dashed #e2e8f0;margin-top:2px}.col-checkbox-toggle{display:inline-flex;align-items:center;gap:5px;font-size:.675rem;font-weight:500;color:#475569;cursor:pointer;-webkit-user-select:none;user-select:none}.btn-add-mapping{display:inline-flex;align-items:center;background:#eff6ff;border:1px solid #bfdbfe;color:#2563eb;font-size:.65rem;font-weight:600;padding:2px 6px;border-radius:4px;cursor:pointer;transition:all .12s ease}.btn-add-mapping:hover{background:#dbeafe}.mapping-pair-row{display:flex;align-items:center;gap:6px;margin-top:4px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.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: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: RptIconComponent, selector: "rpt-icon", inputs: ["name", "size", "color", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
10141
|
-
|
|
10142
|
-
|
|
10143
|
-
|
|
10144
|
-
args: [{ selector: 'rpt-property-editor-grid', standalone: true, imports: [CommonModule, FormsModule, RptIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: " @if (!grid()) {\n <div class=\"empty-grid-card\">\n <div class=\"empty-grid-icon\">\n <rpt-icon name=\"table\" [size]=\"28\" />\n </div>\n <h4 class=\"empty-grid-title\">No Result Grid Attached</h4>\n <p class=\"empty-grid-desc\">Attach an interactive Result Data Grid (or UltraGrid) to this report definition to render queries and stored procedure results.</p>\n <button type=\"button\" class=\"btn-create-grid-action\" (click)=\"addGrid.emit()\">\n <rpt-icon name=\"plus\" [size]=\"14\" />\n <span>+ Add Result Grid</span>\n </button>\n </div>\n } @else {\n <div class=\"props-form\">\n <div class=\"property-group\">\n <div class=\"group-header-with-btn\">\n <span class=\"group-title\">Columns ({{ columns().length }})</span>\n <div style=\"display: flex; gap: 4px;\">\n <button\n type=\"button\"\n class=\"btn-add-col\"\n style=\"background: #eff6ff; color: #2563eb; border-color: #bfdbfe;\"\n (click)=\"autoDetectColumns.emit()\"\n title=\"Auto-detect all columns from procedure or data table\"\n >\n <rpt-icon name=\"refresh\" [size]=\"11\" />\n <span>Auto-Detect</span>\n </button>\n <button type=\"button\" class=\"btn-add-col\" (click)=\"addColumn.emit()\">\n <rpt-icon name=\"plus\" [size]=\"12\" />\n <span>Add Column</span>\n </button>\n </div>\n </div>\n\n <div class=\"columns-editor-list\">\n @for (col of columns(); track col.field; let idx = $index) {\n <div class=\"col-edit-card\">\n <div class=\"col-card-header\">\n <div class=\"col-header-title-group\">\n <span class=\"col-drag-dots\">\u22EE\u22EE</span>\n <span class=\"col-idx-badge\">#{{ idx + 1 }}</span>\n <input\n type=\"text\"\n class=\"col-title-input\"\n placeholder=\"Column Label\"\n [ngModel]=\"col.label\"\n (ngModelChange)=\"onUpdateCol(idx, 'label', $event)\"\n />\n </div>\n <div class=\"col-card-actions\">\n <span class=\"col-type-pill\">{{ col.dataType || 'string' }}</span>\n <button\n type=\"button\"\n class=\"col-action-btn btn-delete\"\n (click)=\"removeColumn.emit(idx)\"\n title=\"Remove Column\"\n >\n <rpt-icon name=\"trash\" [size]=\"12\" />\n </button>\n </div>\n </div>\n\n <div class=\"col-card-body\">\n <div class=\"col-edit-fields\">\n <div class=\"col-field-item\">\n <span class=\"mini-label\">Field</span>\n <input\n type=\"text\"\n class=\"col-mini-input\"\n placeholder=\"field\"\n [ngModel]=\"col.field\"\n (ngModelChange)=\"onUpdateCol(idx, 'field', $event)\"\n />\n </div>\n <div class=\"col-field-item\">\n <span class=\"mini-label\">Type</span>\n <select\n class=\"col-mini-select\"\n [ngModel]=\"col.dataType || 'string'\"\n (ngModelChange)=\"onUpdateCol(idx, 'dataType', $event)\"\n >\n <option value=\"string\">String</option>\n <option value=\"number\">Number</option>\n <option value=\"currency\">Currency</option>\n <option value=\"date\">Date</option>\n <option value=\"boolean\">Boolean</option>\n </select>\n </div>\n <div class=\"col-field-item\">\n <span class=\"mini-label\">Format</span>\n <input\n type=\"text\"\n class=\"col-mini-input\"\n placeholder=\"e.g. dd/MM/yyyy\"\n [ngModel]=\"col.format\"\n (ngModelChange)=\"onUpdateCol(idx, 'format', $event)\"\n />\n </div>\n <div class=\"col-field-item\">\n <span class=\"mini-label\">Align</span>\n <select\n class=\"col-mini-select\"\n [ngModel]=\"col.align || 'left'\"\n (ngModelChange)=\"onUpdateCol(idx, 'align', $event)\"\n >\n <option value=\"left\">Left</option>\n <option value=\"center\">Center</option>\n <option value=\"right\">Right</option>\n </select>\n </div>\n <div class=\"col-field-item\">\n <span class=\"mini-label\">Aggregate</span>\n <select\n class=\"col-mini-select\"\n [ngModel]=\"col.aggregate || 'none'\"\n (ngModelChange)=\"onUpdateCol(idx, 'aggregate', $event === 'none' ? undefined : $event)\"\n >\n <option value=\"none\">None</option>\n <option value=\"sum\">Sum</option>\n <option value=\"avg\">Average</option>\n <option value=\"count\">Count</option>\n <option value=\"min\">Min</option>\n <option value=\"max\">Max</option>\n </select>\n </div>\n <div class=\"col-field-item\">\n <span class=\"mini-label\">Width</span>\n <input\n type=\"text\"\n class=\"col-mini-input\"\n placeholder=\"auto\"\n [ngModel]=\"col.width || ''\"\n (ngModelChange)=\"onUpdateCol(idx, 'width', $event || undefined)\"\n />\n </div>\n </div>\n\n <div class=\"col-card-footer\">\n <label class=\"col-checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"col.visible !== false\"\n (ngModelChange)=\"onUpdateCol(idx, 'visible', $event)\"\n />\n <span>Visible</span>\n </label>\n <label class=\"col-checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"col.checkboxSelection ?? false\"\n (ngModelChange)=\"onUpdateCol(idx, 'checkboxSelection', $event)\"\n />\n <span>Checkbox</span>\n </label>\n </div>\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Grid Table Options Group -->\n <div class=\"property-group\">\n <div class=\"group-title\">Table Options</div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.sideBar !== false\"\n (ngModelChange)=\"updateOption.emit({ field: 'sideBar', value: $event })\"\n />\n <span>Enable Side Panel (Tool Panels)</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.globalSearch !== false\"\n (ngModelChange)=\"updateOption.emit({ field: 'globalSearch', value: $event })\"\n />\n <span>Global Search Box</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.statusBar !== false\"\n (ngModelChange)=\"updateOption.emit({ field: 'statusBar', value: $event })\"\n />\n <span>Status Bar</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.pagination ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'pagination', value: $event })\"\n />\n <span>Enable Pagination</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.selectable ?? false\"\n (ngModelChange)=\"updateOption.emit({ field: 'selectable', value: $event })\"\n />\n <span>Row Selection</span>\n </label>\n </div>\n\n @if (grid()?.options?.selectable) {\n <div class=\"field-row\">\n <label class=\"field-label\">Selection Mode</label>\n <select\n class=\"field-input\"\n [ngModel]=\"grid()?.options?.selectionMode ?? 'multiple'\"\n (ngModelChange)=\"updateOption.emit({ field: 'selectionMode', value: $event })\"\n >\n <option value=\"multiple\">Multiple Rows</option>\n <option value=\"single\">Single Row</option>\n </select>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.checkboxSelection ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'checkboxSelection', value: $event })\"\n />\n <span>Checkbox Selection</span>\n </label>\n </div>\n\n @if ((grid()?.options?.selectionMode ?? 'multiple') === 'multiple') {\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.headerCheckboxSelection ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'headerCheckboxSelection', value: $event })\"\n />\n <span>Header Select All Checkbox</span>\n </label>\n </div>\n }\n }\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.allowColumnReorder ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'allowColumnReorder', value: $event })\"\n />\n <span>Allow Column Reorder</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.allowExport ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'allowExport', value: $event })\"\n />\n <span>Allow Export</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"field-label\">Page Size</label>\n <input\n type=\"number\"\n class=\"field-input\"\n min=\"5\"\n max=\"100\"\n [ngModel]=\"grid()?.options?.pageSize ?? 10\"\n (ngModelChange)=\"updateOption.emit({ field: 'pageSize', value: +$event })\"\n />\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.showRowNumbers ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'showRowNumbers', value: $event })\"\n />\n <span>Show Row Numbers</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.showFooter ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'showFooter', value: $event })\"\n />\n <span>Show Footer Totals</span>\n </label>\n </div>\n </div>\n\n <!-- Row Click & Drill-Down Action Group -->\n <div class=\"property-group\">\n <div class=\"group-title\">Row Click & Drill-Down</div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"hasRowClickAction()\"\n (ngModelChange)=\"toggleRowClickAction($event)\"\n />\n <span>Enable Row Click Action</span>\n </label>\n </div>\n\n @if (hasRowClickAction()) {\n <div class=\"field-row\">\n <label class=\"field-label\">Action Type</label>\n <select\n class=\"field-input field-select\"\n [ngModel]=\"rowClickAction()?.type || 'open-drawer'\"\n (ngModelChange)=\"updateRowActionField('type', $event)\"\n >\n <option value=\"open-drawer\">Slide-Over Detail Drawer</option>\n <option value=\"drilldown-report\">Drill-Down Report (Host Event)</option>\n <option value=\"navigate\">Navigate to URL</option>\n <option value=\"emit-custom\">Emit Custom Action</option>\n </select>\n </div>\n\n @if (rowClickAction()?.type === 'open-drawer' || rowClickAction()?.type === 'drilldown-report') {\n <div class=\"field-row\">\n <label class=\"field-label\">Target Report ID</label>\n <input\n type=\"text\"\n class=\"field-input\"\n placeholder=\"e.g. order-details\"\n [ngModel]=\"rowClickAction()?.targetReportId || ''\"\n (ngModelChange)=\"updateRowActionField('targetReportId', $event)\"\n />\n </div>\n\n <div class=\"field-row\">\n <label class=\"field-label\">Drawer Title</label>\n <input\n type=\"text\"\n class=\"field-input\"\n placeholder=\"e.g. Order Details\"\n [ngModel]=\"rowClickAction()?.drawerTitle || ''\"\n (ngModelChange)=\"updateRowActionField('drawerTitle', $event)\"\n />\n </div>\n\n <div class=\"field-row\">\n <label class=\"field-label\">Drawer Width</label>\n <input\n type=\"text\"\n class=\"field-input\"\n placeholder=\"e.g. 760px, 80%, 900px\"\n [ngModel]=\"rowClickAction()?.drawerWidth || '760px'\"\n (ngModelChange)=\"updateRowActionField('drawerWidth', $event)\"\n />\n </div>\n }\n\n @if (rowClickAction()?.type === 'navigate') {\n <div class=\"field-row\">\n <label class=\"field-label\">Target URL</label>\n <input\n type=\"text\"\n class=\"field-input\"\n [placeholder]=\"'/orders/{{row.orderId}} or https://...'\"\n [ngModel]=\"rowClickAction()?.targetUrl || ''\"\n (ngModelChange)=\"updateRowActionField('targetUrl', $event)\"\n />\n </div>\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"rowClickAction()?.openNewTab ?? false\"\n (ngModelChange)=\"updateRowActionField('openNewTab', $event)\"\n />\n <span>Open in New Tab</span>\n </label>\n </div>\n }\n\n @if (rowClickAction()?.type === 'emit-custom') {\n <div class=\"field-row\">\n <label class=\"field-label\">Custom Action Name</label>\n <input\n type=\"text\"\n class=\"field-input\"\n placeholder=\"e.g. view-audit-trail\"\n [ngModel]=\"rowClickAction()?.customActionName || ''\"\n (ngModelChange)=\"updateRowActionField('customActionName', $event)\"\n />\n </div>\n }\n\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">Parameter Mapping</label>\n <button\n type=\"button\"\n class=\"btn-add-mapping\"\n (click)=\"addParameterMapping()\"\n >\n + Add Mapping\n </button>\n </div>\n\n @for (mapping of parameterMappings(); track mapping.key; let mIdx = $index) {\n <div class=\"mapping-pair-row\">\n <input\n type=\"text\"\n class=\"col-mini-input\"\n style=\"flex: 1;\"\n placeholder=\"Target (e.g. customerId)\"\n [ngModel]=\"mapping.key\"\n (ngModelChange)=\"updateMappingKey(mIdx, $event)\"\n />\n <span style=\"font-size: 0.75rem; color: #94a3b8;\">\u2190</span>\n <input\n type=\"text\"\n class=\"col-mini-input\"\n style=\"flex: 1;\"\n placeholder=\"Source (e.g. row.id)\"\n [ngModel]=\"mapping.value\"\n (ngModelChange)=\"updateMappingValue(mIdx, $event)\"\n />\n <button\n type=\"button\"\n class=\"col-action-btn btn-delete\"\n (click)=\"removeMapping(mIdx)\"\n title=\"Remove mapping\"\n >\n <rpt-icon name=\"trash\" [size]=\"11\" />\n </button>\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Remove Grid Action -->\n <div class=\"grid-danger-zone\">\n <button type=\"button\" class=\"btn-remove-grid-action\" (click)=\"removeGrid.emit()\">\n <rpt-icon name=\"trash\" [size]=\"13\" />\n <span>Remove Grid from Report</span>\n </button>\n </div>\n </div>\n }", styles: [".property-group{display:flex;flex-direction:column;gap:12px}.group-title{font-size:.7rem;font-weight:700;color:var(--rpt-color-text-muted, #64748b);text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--rpt-color-border, #e2e8f0);padding-bottom:4px}.field-row{display:flex;flex-direction:column;gap:4px}.field-label-row{display:flex;align-items:center;justify-content:space-between}.field-badge-val{font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border:1px solid #bfdbfe;padding:1px 6px;border-radius:9999px}.field-label{font-size:.7rem;font-weight:600;color:var(--rpt-color-text, #0f172a)}.field-input,.field-select,.field-textarea{width:100%;box-sizing:border-box;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius-sm, 4px);padding:6px 10px;font-size:var(--rpt-font-size-sm, .75rem);color:var(--rpt-color-text, #0f172a);background:#fff;outline:none;transition:all var(--rpt-transition-speed, .15s) ease}.field-select{appearance:none;-webkit-appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right 10px center;background-size:13px 13px;padding-right:28px;cursor:pointer}.field-input:focus,.field-select:focus,.field-textarea:focus{border-color:var(--rpt-color-primary, #2563eb);box-shadow:var(--rpt-box-shadow-focus, 0 0 0 3px rgba(37, 99, 235, .25))}.checkbox-toggle{display:inline-flex;align-items:center;gap:8px;font-size:.75rem;font-weight:500;color:var(--rpt-color-text, #0f172a);cursor:pointer}.preset-pill-group{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.preset-pill{background:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;padding:2px 8px;font-size:.65rem;font-weight:600;color:#475569;cursor:pointer;transition:all .12s ease}.preset-pill:hover{background:#e2e8f0;color:#0f172a;border-color:#cbd5e1}.preset-pill-active{background:var(--rpt-color-primary, #2563eb)!important;color:#fff!important;border-color:var(--rpt-color-primary, #2563eb)!important}.preset-pill-reset{color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border-color:#bfdbfe}.preset-pill-reset:hover{background:#dbeafe;color:#1d4ed8;border-color:#93c5fd}\n", ".empty-grid-card{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 16px;text-align:center;border:2px dashed #cbd5e1;border-radius:8px;background:#fff;margin:10px 0}.empty-grid-icon{width:48px;height:48px;border-radius:10px;background:#eff6ff;color:#2563eb;display:flex;align-items:center;justify-content:center;margin-bottom:12px}.empty-grid-title{font-size:.95rem;font-weight:700;color:#0f172a;margin:0 0 6px}.empty-grid-desc{font-size:.775rem;color:#64748b;margin:0 0 18px;line-height:1.4;max-width:280px}.btn-create-grid-action{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;background:#2563eb;color:#fff;font-size:.775rem;font-weight:600;border:none;border-radius:6px;cursor:pointer;box-shadow:0 2px 4px #2563eb33;transition:all .15s ease}.btn-create-grid-action:hover{background:#1d4ed8;box-shadow:0 4px 8px #2563eb40}.grid-danger-zone{margin-top:10px;padding-top:16px;border-top:1px dashed #e2e8f0;display:flex;justify-content:center}.btn-remove-grid-action{display:inline-flex;align-items:center;gap:6px;background:#fee2e2;border:1px solid #fecaca;color:#dc2626;font-size:.75rem;font-weight:600;padding:6px 12px;border-radius:6px;cursor:pointer;transition:all .15s ease}.btn-remove-grid-action:hover{background:#fecaca;border-color:#f87171;color:#b91c1c}.props-form{display:flex;flex-direction:column;gap:20px}.group-header-with-btn{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--rpt-color-border, #e2e8f0);padding-bottom:8px;margin-bottom:4px}.btn-add-col{display:inline-flex;align-items:center;gap:5px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:4px;padding:4px 10px;font-size:.725rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);cursor:pointer;transition:all .15s ease}.btn-add-col:hover{background:var(--rpt-color-primary, #2563eb);color:#fff;border-color:var(--rpt-color-primary, #2563eb)}.columns-editor-list{display:flex;flex-direction:column;gap:10px}.col-edit-card{background:#fff;border:1.5px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius, 6px);box-shadow:var(--rpt-box-shadow, 0 1px 2px rgba(0, 0, 0, .05));box-sizing:border-box;transition:all var(--rpt-transition-speed, .15s) ease;position:relative;overflow:hidden;display:flex;flex-direction:column}.col-edit-card:hover{border-color:var(--rpt-color-primary, #2563eb);box-shadow:0 4px 6px -1px #2563eb1a}.col-card-header{display:flex;align-items:center;justify-content:space-between;gap:6px;padding:5px 8px;background:var(--rpt-color-surface, #f8fafc);border-bottom:1px solid var(--rpt-color-border, #e2e8f0);min-height:32px;box-sizing:border-box}.col-header-title-group{display:flex;align-items:center;gap:5px;min-width:0;flex:1}.col-drag-dots{color:#94a3b8;font-size:.8rem;cursor:grab;-webkit-user-select:none;user-select:none;line-height:1;padding-left:2px}.col-idx-badge{font-size:.65rem;font-weight:700;color:#64748b;background:#e2e8f0;padding:1px 5px;border-radius:3px;letter-spacing:.02em}.col-title-input{flex:1;border:1px solid transparent;background:transparent;font-size:.75rem;font-weight:600;color:var(--rpt-color-text, #0f172a);padding:2px 6px;border-radius:4px;outline:none;transition:all .15s ease}.col-title-input:hover{border-color:#cbd5e1;background:#fff}.col-title-input:focus{border-color:var(--rpt-color-primary, #2563eb);background:#fff;box-shadow:0 0 0 2px #2563eb26}.col-card-actions{display:flex;align-items:center;gap:4px}.col-type-pill{font-size:.625rem;font-weight:700;color:#1e40af;background:#dbeafe;border:1px solid #bfdbfe;padding:1px 5px;border-radius:3px;text-transform:uppercase;letter-spacing:.04em}.col-action-btn{display:inline-flex;align-items:center;justify-content:center;background:none;border:none;color:var(--rpt-color-text-muted, #64748b);cursor:pointer;padding:3px;border-radius:4px;transition:all .15s ease}.col-action-btn.btn-delete:hover{color:var(--rpt-color-danger, #ef4444);background:#fee2e2}.col-card-body{padding:8px 10px;display:flex;flex-direction:column;gap:6px;background:#fff}.col-edit-fields{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px 8px}.col-field-item{display:flex;flex-direction:column;gap:2px;min-width:0}.mini-label{font-size:.625rem;font-weight:600;color:#64748b;letter-spacing:.02em}.col-mini-input,.col-mini-select{width:100%;box-sizing:border-box;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:3px;padding:3px 5px;font-size:.7rem;color:var(--rpt-color-text, #0f172a);background:#fff;outline:none;transition:all .12s ease}.col-mini-select{appearance:none;-webkit-appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right 4px center;background-size:10px 10px;padding-right:16px;cursor:pointer}.col-mini-input:focus,.col-mini-select:focus{border-color:var(--rpt-color-primary, #2563eb);box-shadow:0 0 0 2px #2563eb26}.col-card-footer{display:flex;align-items:center;justify-content:flex-end;gap:12px;padding-top:4px;border-top:1px dashed #e2e8f0;margin-top:2px}.col-checkbox-toggle{display:inline-flex;align-items:center;gap:5px;font-size:.675rem;font-weight:500;color:#475569;cursor:pointer;-webkit-user-select:none;user-select:none}.btn-add-mapping{display:inline-flex;align-items:center;background:#eff6ff;border:1px solid #bfdbfe;color:#2563eb;font-size:.65rem;font-weight:600;padding:2px 6px;border-radius:4px;cursor:pointer;transition:all .12s ease}.btn-add-mapping:hover{background:#dbeafe}.mapping-pair-row{display:flex;align-items:center;gap:6px;margin-top:4px}\n"] }]
|
|
10145
|
-
|
|
10146
|
-
|
|
10147
|
-
class
|
|
10148
|
-
|
|
10149
|
-
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
<div class="props-form">
|
|
10154
|
-
<div class="property-group">
|
|
10155
|
-
<div class="group-title">Report Definition</div>
|
|
10156
|
-
|
|
10157
|
-
<div class="field-row">
|
|
10158
|
-
<label class="field-label">Report Name</label>
|
|
10159
|
-
<input
|
|
10160
|
-
type="text"
|
|
10161
|
-
class="field-input"
|
|
10162
|
-
[ngModel]="definition().name"
|
|
10163
|
-
(ngModelChange)="updateReport.emit({ field: 'name', value: $event })"
|
|
10164
|
-
/>
|
|
10646
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ParameterBindingEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10647
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ParameterBindingEditorComponent, isStandalone: true, selector: "rpt-parameter-binding-editor", inputs: { param: { classPropertyName: "param", publicName: "param", isSignal: true, isRequired: true, transformFunction: null }, currentValue: { classPropertyName: "currentValue", publicName: "currentValue", isSignal: true, isRequired: false, transformFunction: null }, allComponents: { classPropertyName: "allComponents", publicName: "allComponents", isSignal: true, isRequired: false, transformFunction: null }, currentComponentId: { classPropertyName: "currentComponentId", publicName: "currentComponentId", isSignal: true, isRequired: false, transformFunction: null }, isAdvancedMode: { classPropertyName: "isAdvancedMode", publicName: "isAdvancedMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange", linkedFieldChange: "linkedFieldChange" }, ngImport: i0, template: `
|
|
10648
|
+
<div class="param-card" [class.session-bound]="isAutoSessionDerived()">
|
|
10649
|
+
<!-- Card Header: Title, Technical Name, Type, and Badges -->
|
|
10650
|
+
<div class="param-header">
|
|
10651
|
+
<div class="param-title-wrap">
|
|
10652
|
+
<span class="param-label" [title]="param().name">{{ friendlyLabel() }}</span>
|
|
10653
|
+
<span class="param-tech-badge">{{ param().name }}</span>
|
|
10654
|
+
<span class="param-type-badge">{{ displayType() }}</span>
|
|
10655
|
+
@if (isAutoSessionDerived()) {
|
|
10656
|
+
<span class="session-auto-badge">Auto Session</span>
|
|
10657
|
+
} @else if (param().required && !hasValue()) {
|
|
10658
|
+
<span class="required-warn-badge">Required</span>
|
|
10659
|
+
}
|
|
10165
10660
|
</div>
|
|
10166
10661
|
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10662
|
+
<!-- Binding Mode Segmented Selector -->
|
|
10663
|
+
<div class="binding-mode-tabs" role="tablist">
|
|
10664
|
+
@if (sessionMatch()) {
|
|
10665
|
+
<button
|
|
10666
|
+
type="button"
|
|
10667
|
+
class="mode-btn"
|
|
10668
|
+
[class.active]="activeMode() === 'session'"
|
|
10669
|
+
(click)="setMode('session')"
|
|
10670
|
+
title="Bound to session user/branch context"
|
|
10671
|
+
>
|
|
10672
|
+
<rpt-icon name="user" [size]="11" />
|
|
10673
|
+
<span>Session</span>
|
|
10674
|
+
</button>
|
|
10675
|
+
}
|
|
10676
|
+
<button
|
|
10677
|
+
type="button"
|
|
10678
|
+
class="mode-btn"
|
|
10679
|
+
[class.active]="activeMode() === 'control'"
|
|
10680
|
+
(click)="setMode('control')"
|
|
10681
|
+
title="Link value to a form field on canvas (e.g. Dropdown, DatePicker)"
|
|
10682
|
+
>
|
|
10683
|
+
<rpt-icon name="link" [size]="11" />
|
|
10684
|
+
<span>Form Field</span>
|
|
10685
|
+
</button>
|
|
10686
|
+
<button
|
|
10687
|
+
type="button"
|
|
10688
|
+
class="mode-btn"
|
|
10689
|
+
[class.active]="activeMode() === 'fixed'"
|
|
10690
|
+
(click)="setMode('fixed')"
|
|
10691
|
+
title="Type a manual or fixed value (e.g. 1, 0, 101)"
|
|
10692
|
+
>
|
|
10693
|
+
<rpt-icon name="edit" [size]="11" />
|
|
10694
|
+
<span>Fixed Value</span>
|
|
10695
|
+
</button>
|
|
10696
|
+
<button
|
|
10697
|
+
type="button"
|
|
10698
|
+
class="mode-btn"
|
|
10699
|
+
[class.active]="activeMode() === 'context'"
|
|
10700
|
+
(click)="setMode('context')"
|
|
10701
|
+
title="Use system date presets (e.g. Today, Month Start)"
|
|
10702
|
+
>
|
|
10703
|
+
<rpt-icon name="clock" [size]="11" />
|
|
10704
|
+
<span>Preset</span>
|
|
10705
|
+
</button>
|
|
10706
|
+
@if (isAdvancedMode()) {
|
|
10707
|
+
<button
|
|
10708
|
+
type="button"
|
|
10709
|
+
class="mode-btn"
|
|
10710
|
+
[class.active]="activeMode() === 'custom'"
|
|
10711
|
+
(click)="setMode('custom')"
|
|
10712
|
+
title="Manual template expression"
|
|
10713
|
+
>
|
|
10714
|
+
<rpt-icon name="code" [size]="11" />
|
|
10715
|
+
<span>Expr</span>
|
|
10716
|
+
</button>
|
|
10717
|
+
}
|
|
10175
10718
|
</div>
|
|
10176
10719
|
</div>
|
|
10177
10720
|
|
|
10178
|
-
|
|
10179
|
-
|
|
10721
|
+
<!-- Mode-specific Input Controls -->
|
|
10722
|
+
<div class="param-body">
|
|
10723
|
+
<!-- 0. AUTO SESSION DERIVED -->
|
|
10724
|
+
@if (activeMode() === 'session' && sessionMatch()) {
|
|
10725
|
+
<div class="session-box">
|
|
10726
|
+
<div class="session-box-text">
|
|
10727
|
+
<rpt-icon name="user" [size]="13" class="session-icon" />
|
|
10728
|
+
<span>
|
|
10729
|
+
Derived from <strong>{{ sessionMatch()?.sessionKey }}</strong> ({{ sessionMatch()?.friendlyLabel }})
|
|
10730
|
+
</span>
|
|
10731
|
+
</div>
|
|
10732
|
+
<div class="session-box-actions">
|
|
10733
|
+
<div class="session-val-tag">
|
|
10734
|
+
Current: <strong>{{ sessionMatch()?.value ?? '(none)' }}</strong>
|
|
10735
|
+
</div>
|
|
10736
|
+
<button
|
|
10737
|
+
type="button"
|
|
10738
|
+
class="btn-session-override"
|
|
10739
|
+
(click)="setMode('fixed')"
|
|
10740
|
+
title="Override with manual value"
|
|
10741
|
+
>
|
|
10742
|
+
Override
|
|
10743
|
+
</button>
|
|
10744
|
+
</div>
|
|
10745
|
+
</div>
|
|
10746
|
+
}
|
|
10180
10747
|
|
|
10181
|
-
|
|
10182
|
-
|
|
10183
|
-
<
|
|
10184
|
-
class="
|
|
10185
|
-
|
|
10186
|
-
|
|
10187
|
-
|
|
10188
|
-
|
|
10189
|
-
|
|
10190
|
-
|
|
10191
|
-
|
|
10192
|
-
|
|
10748
|
+
<!-- 1. FORM CONTROL LINKER -->
|
|
10749
|
+
@if (activeMode() === 'control') {
|
|
10750
|
+
<div class="linker-row">
|
|
10751
|
+
<rpt-icon name="link" [size]="13" class="linker-icon" />
|
|
10752
|
+
<select
|
|
10753
|
+
class="field-select linker-select"
|
|
10754
|
+
[ngModel]="selectedControlKey()"
|
|
10755
|
+
(ngModelChange)="onControlSelected($event)"
|
|
10756
|
+
>
|
|
10757
|
+
<option value="">-- Choose Form Field on Canvas --</option>
|
|
10758
|
+
@for (field of compatibleControls(); track field.key || field.id) {
|
|
10759
|
+
<option [value]="field.key || field.id">
|
|
10760
|
+
{{ field.label || field.key }} ({{ field.type }})
|
|
10761
|
+
</option>
|
|
10762
|
+
}
|
|
10763
|
+
</select>
|
|
10764
|
+
</div>
|
|
10765
|
+
@if (!selectedControlKey() && compatibleControls().length === 0) {
|
|
10766
|
+
<div class="empty-hint">No input fields on canvas. Add a dropdown, date picker, or input first.</div>
|
|
10767
|
+
}
|
|
10768
|
+
}
|
|
10193
10769
|
|
|
10194
|
-
|
|
10195
|
-
|
|
10196
|
-
|
|
10197
|
-
|
|
10198
|
-
|
|
10199
|
-
|
|
10200
|
-
|
|
10201
|
-
|
|
10202
|
-
|
|
10203
|
-
|
|
10204
|
-
|
|
10770
|
+
<!-- 2. ENTER FIXED VALUE -->
|
|
10771
|
+
@if (activeMode() === 'fixed') {
|
|
10772
|
+
@switch (param().dataType) {
|
|
10773
|
+
@case ('boolean') {
|
|
10774
|
+
<label class="toggle-switch">
|
|
10775
|
+
<input
|
|
10776
|
+
type="checkbox"
|
|
10777
|
+
[ngModel]="booleanFixedValue()"
|
|
10778
|
+
(ngModelChange)="onBooleanChanged($event)"
|
|
10779
|
+
/>
|
|
10780
|
+
<span class="toggle-text">{{ booleanFixedValue() ? 'Active / Yes (1)' : 'Inactive / No (0)' }}</span>
|
|
10781
|
+
</label>
|
|
10782
|
+
}
|
|
10783
|
+
@case ('number') {
|
|
10784
|
+
<div class="input-wrap">
|
|
10785
|
+
<input
|
|
10786
|
+
type="number"
|
|
10787
|
+
class="field-input"
|
|
10788
|
+
placeholder="Enter number (e.g. 1, 0, 11)"
|
|
10789
|
+
[ngModel]="currentValue()"
|
|
10790
|
+
(ngModelChange)="onFixedValueChanged($event)"
|
|
10791
|
+
/>
|
|
10792
|
+
</div>
|
|
10793
|
+
}
|
|
10794
|
+
@case ('date') {
|
|
10795
|
+
<div class="input-wrap">
|
|
10796
|
+
<input
|
|
10797
|
+
type="date"
|
|
10798
|
+
class="field-input"
|
|
10799
|
+
[ngModel]="currentValue()"
|
|
10800
|
+
(ngModelChange)="onFixedValueChanged($event)"
|
|
10801
|
+
/>
|
|
10802
|
+
</div>
|
|
10803
|
+
}
|
|
10804
|
+
@default {
|
|
10805
|
+
<div class="input-wrap">
|
|
10806
|
+
<input
|
|
10807
|
+
type="text"
|
|
10808
|
+
class="field-input"
|
|
10809
|
+
placeholder="Enter value (e.g. 101, text, code)"
|
|
10810
|
+
[ngModel]="currentValue()"
|
|
10811
|
+
(ngModelChange)="onFixedValueChanged($event)"
|
|
10812
|
+
/>
|
|
10813
|
+
</div>
|
|
10814
|
+
}
|
|
10815
|
+
}
|
|
10816
|
+
@if (paramQuickSuggestions().length > 0) {
|
|
10817
|
+
<div class="quick-chips-row">
|
|
10818
|
+
<span class="chips-label">Quick:</span>
|
|
10819
|
+
@for (chip of paramQuickSuggestions(); track chip.value) {
|
|
10820
|
+
<button
|
|
10821
|
+
type="button"
|
|
10822
|
+
class="chip-btn"
|
|
10823
|
+
[class.active]="isCurrentValue(chip.value)"
|
|
10824
|
+
(click)="onFixedValueChanged(chip.value)"
|
|
10825
|
+
>
|
|
10826
|
+
{{ chip.label }}
|
|
10827
|
+
</button>
|
|
10828
|
+
}
|
|
10829
|
+
</div>
|
|
10830
|
+
}
|
|
10831
|
+
}
|
|
10205
10832
|
|
|
10206
|
-
|
|
10207
|
-
|
|
10208
|
-
<input
|
|
10209
|
-
|
|
10210
|
-
|
|
10211
|
-
|
|
10212
|
-
|
|
10213
|
-
|
|
10214
|
-
|
|
10215
|
-
|
|
10216
|
-
|
|
10833
|
+
<!-- 3. SYSTEM DATE PRESETS -->
|
|
10834
|
+
@if (activeMode() === 'context') {
|
|
10835
|
+
<div class="input-wrap">
|
|
10836
|
+
<select
|
|
10837
|
+
class="field-select"
|
|
10838
|
+
[ngModel]="selectedContextKey()"
|
|
10839
|
+
(ngModelChange)="onContextSelected($event)"
|
|
10840
|
+
>
|
|
10841
|
+
<option value="">-- Choose Date / Context Preset --</option>
|
|
10842
|
+
<option value="{{ '{{' }}context.today{{ '}}' }}">Today's Date</option>
|
|
10843
|
+
<option value="{{ '{{' }}context.startOfMonth{{ '}}' }}">First Day of Current Month</option>
|
|
10844
|
+
<option value="{{ '{{' }}context.endOfMonth{{ '}}' }}">Last Day of Current Month</option>
|
|
10845
|
+
<option value="{{ '{{' }}context.startOfYear{{ '}}' }}">First Day of Current Year</option>
|
|
10846
|
+
<option value="{{ '{{' }}context.yesterday{{ '}}' }}">Yesterday</option>
|
|
10847
|
+
<option value="{{ '{{' }}context.branchId{{ '}}' }}">Current User Branch</option>
|
|
10848
|
+
<option value="{{ '{{' }}context.userId{{ '}}' }}">Current User ID</option>
|
|
10849
|
+
</select>
|
|
10850
|
+
</div>
|
|
10851
|
+
}
|
|
10217
10852
|
|
|
10218
|
-
|
|
10219
|
-
|
|
10220
|
-
<
|
|
10221
|
-
|
|
10222
|
-
|
|
10223
|
-
|
|
10224
|
-
|
|
10225
|
-
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
|
|
10853
|
+
<!-- 4. CUSTOM EXPRESSION -->
|
|
10854
|
+
@if (activeMode() === 'custom') {
|
|
10855
|
+
<div class="input-wrap">
|
|
10856
|
+
<input
|
|
10857
|
+
type="text"
|
|
10858
|
+
class="field-input monospace-input"
|
|
10859
|
+
placeholder="e.g. {{ '{{' }}form.dropdownField ?? 1{{ '}}' }}"
|
|
10860
|
+
[ngModel]="currentValue()"
|
|
10861
|
+
(ngModelChange)="onFixedValueChanged($event)"
|
|
10862
|
+
/>
|
|
10863
|
+
</div>
|
|
10864
|
+
}
|
|
10229
10865
|
</div>
|
|
10866
|
+
|
|
10867
|
+
@if (param().description && isAdvancedMode()) {
|
|
10868
|
+
<div class="param-desc">{{ param().description }}</div>
|
|
10869
|
+
}
|
|
10230
10870
|
</div>
|
|
10231
|
-
`, isInline: true, styles: [".property-group{display:flex;flex-direction:column;gap:12px}.group-title{font-size:.7rem;font-weight:700;color:var(--rpt-color-text-muted, #64748b);text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--rpt-color-border, #e2e8f0);padding-bottom:4px}.field-row{display:flex;flex-direction:column;gap:4px}.field-label-row{display:flex;align-items:center;justify-content:space-between}.field-badge-val{font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border:1px solid #bfdbfe;padding:1px 6px;border-radius:9999px}.field-label{font-size:.7rem;font-weight:600;color:var(--rpt-color-text, #0f172a)}.field-input,.field-select,.field-textarea{width:100%;box-sizing:border-box;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius-sm, 4px);padding:6px 10px;font-size:var(--rpt-font-size-sm, .75rem);color:var(--rpt-color-text, #0f172a);background:#fff;outline:none;transition:all var(--rpt-transition-speed, .15s) ease}.field-select{appearance:none;-webkit-appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right 10px center;background-size:13px 13px;padding-right:28px;cursor:pointer}.field-input:focus,.field-select:focus,.field-textarea:focus{border-color:var(--rpt-color-primary, #2563eb);box-shadow:var(--rpt-box-shadow-focus, 0 0 0 3px rgba(37, 99, 235, .25))}.checkbox-toggle{display:inline-flex;align-items:center;gap:8px;font-size:.75rem;font-weight:500;color:var(--rpt-color-text, #0f172a);cursor:pointer}.preset-pill-group{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.preset-pill{background:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;padding:2px 8px;font-size:.65rem;font-weight:600;color:#475569;cursor:pointer;transition:all .12s ease}.preset-pill:hover{background:#e2e8f0;color:#0f172a;border-color:#cbd5e1}.preset-pill-active{background:var(--rpt-color-primary, #2563eb)!important;color:#fff!important;border-color:var(--rpt-color-primary, #2563eb)!important}.preset-pill-reset{color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border-color:#bfdbfe}.preset-pill-reset:hover{background:#dbeafe;color:#1d4ed8;border-color:#93c5fd}\n", ".
|
|
10871
|
+
`, isInline: true, styles: [".property-group{display:flex;flex-direction:column;gap:12px}.group-title{font-size:.7rem;font-weight:700;color:var(--rpt-color-text-muted, #64748b);text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--rpt-color-border, #e2e8f0);padding-bottom:4px}.field-row{display:flex;flex-direction:column;gap:4px}.field-label-row{display:flex;align-items:center;justify-content:space-between}.field-badge-val{font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border:1px solid #bfdbfe;padding:1px 6px;border-radius:9999px}.field-label{font-size:.7rem;font-weight:600;color:var(--rpt-color-text, #0f172a)}.field-input,.field-select,.field-textarea{width:100%;box-sizing:border-box;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius-sm, 4px);padding:6px 10px;font-size:var(--rpt-font-size-sm, .75rem);color:var(--rpt-color-text, #0f172a);background:#fff;outline:none;transition:all var(--rpt-transition-speed, .15s) ease}.field-select{appearance:none;-webkit-appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right 10px center;background-size:13px 13px;padding-right:28px;cursor:pointer}.field-input:focus,.field-select:focus,.field-textarea:focus{border-color:var(--rpt-color-primary, #2563eb);box-shadow:var(--rpt-box-shadow-focus, 0 0 0 3px rgba(37, 99, 235, .25))}.checkbox-toggle{display:inline-flex;align-items:center;gap:8px;font-size:.75rem;font-weight:500;color:var(--rpt-color-text, #0f172a);cursor:pointer}.preset-pill-group{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.preset-pill{background:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;padding:2px 8px;font-size:.65rem;font-weight:600;color:#475569;cursor:pointer;transition:all .12s ease}.preset-pill:hover{background:#e2e8f0;color:#0f172a;border-color:#cbd5e1}.preset-pill-active{background:var(--rpt-color-primary, #2563eb)!important;color:#fff!important;border-color:var(--rpt-color-primary, #2563eb)!important}.preset-pill-reset{color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border-color:#bfdbfe}.preset-pill-reset:hover{background:#dbeafe;color:#1d4ed8;border-color:#93c5fd}\n", ".param-card{background:#fff;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:8px;box-shadow:0 1px 3px #00000008;box-sizing:border-box;width:100%;min-width:0;transition:all .12s ease}.param-card:hover{border-color:#94a3b8}.param-card.session-bound{background:#f0fdf4;border-color:#bbf7d0}.param-header{display:flex;flex-direction:column;gap:6px;width:100%;box-sizing:border-box}.param-title-wrap{display:flex;align-items:center;justify-content:flex-start;flex-wrap:wrap;gap:6px;width:100%}.param-label{font-size:.775rem;font-weight:700;color:#0f172a;white-space:nowrap}.param-tech-badge{font-size:.65rem;font-family:monospace;padding:1px 6px;border-radius:4px;background:#eff6ff;color:#2563eb;border:1px solid #bfdbfe}.param-type-badge{font-size:.65rem;padding:1px 6px;border-radius:4px;background:#f1f5f9;color:#475569;text-transform:capitalize}.session-auto-badge{font-size:.625rem;font-weight:600;padding:1px 6px;border-radius:4px;background:#dcfce7;color:#15803d;border:1px solid #bbf7d0}.required-warn-badge{font-size:.625rem;font-weight:600;padding:1px 6px;border-radius:4px;background:#fef2f2;color:#b91c1c;border:1px solid #fecaca}.binding-mode-tabs{display:flex;flex-wrap:wrap;width:100%;background:#f1f5f9;border-radius:6px;padding:3px;gap:3px;box-sizing:border-box}.mode-btn{flex:1 1 70px;display:inline-flex;align-items:center;justify-content:center;gap:4px;padding:5px 6px;font-size:.69rem;font-weight:500;background:transparent;color:#64748b;border:none;border-radius:4px;cursor:pointer;transition:all .12s ease;white-space:nowrap}.mode-btn:hover{color:#0f172a;background:#fff9}.mode-btn.active{background:#fff;color:var(--rpt-color-primary, #2563eb);font-weight:600;box-shadow:0 1px 2px #00000014}.param-body{display:flex;flex-direction:column;gap:6px;width:100%;min-width:0;box-sizing:border-box}.linker-row{display:flex;align-items:center;gap:8px;width:100%;min-width:0;box-sizing:border-box}.linker-icon{color:#2563eb;flex-shrink:0}.linker-select{flex:1;width:100%;min-width:0;box-sizing:border-box;border:1px solid #bfdbfe;border-radius:6px;background-color:#eff6ff;font-weight:500;color:#1e40af;padding:6px 10px;font-size:.75rem}.quick-chips-row{display:flex;align-items:center;flex-wrap:wrap;gap:4px;margin-top:4px}.chips-label{font-size:.65rem;font-weight:600;color:#64748b;margin-right:2px}.chip-btn{font-size:.65rem;font-weight:500;padding:2px 7px;border-radius:4px;background:#f1f5f9;color:#334155;border:1px solid #cbd5e1;cursor:pointer;transition:all .12s ease}.chip-btn:hover{background:#e2e8f0;color:#0f172a;border-color:#94a3b8}.chip-btn.active{background:#dbeafe;color:#1e40af;border-color:#60a5fa;font-weight:600}.input-wrap{width:100%;min-width:0;box-sizing:border-box}.mode-switch-hint{font-size:.68rem;color:#64748b;margin-top:1px;display:flex;align-items:center;gap:5px;flex-wrap:wrap}.btn-text-switch{background:none;border:none;padding:0;color:#2563eb;font-weight:600;cursor:pointer;text-decoration:underline;font-size:.68rem}.btn-text-switch:hover{color:#1d4ed8}.session-box{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;background:#fff;border:1px solid #bbf7d0;border-radius:6px;padding:6px 10px;gap:6px;width:100%;box-sizing:border-box}.session-box-text{display:flex;align-items:center;gap:6px;font-size:.72rem;color:#166534;min-width:0}.session-icon{color:#16a34a;flex-shrink:0}.session-box-actions{display:flex;align-items:center;gap:6px;flex-shrink:0}.session-val-tag{font-size:.7rem;color:#15803d;background:#dcfce7;padding:2px 8px;border-radius:4px;white-space:nowrap;flex-shrink:0}.btn-session-override{background:#fff;border:1px solid #86efac;color:#15803d;font-size:.65rem;font-weight:600;padding:2px 6px;border-radius:4px;cursor:pointer;transition:all .12s ease}.btn-session-override:hover{background:#dcfce7}.empty-hint{font-size:.68rem;color:#94a3b8;font-style:italic}.toggle-switch{display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-size:.75rem;color:#334155;font-weight:500}.monospace-input{font-family:monospace;font-size:.72rem}.param-desc{font-size:.675rem;color:#64748b;line-height:1.35}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.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: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: RptIconComponent, selector: "rpt-icon", inputs: ["name", "size", "color", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
10232
10872
|
}
|
|
10233
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type:
|
|
10873
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ParameterBindingEditorComponent, decorators: [{
|
|
10234
10874
|
type: Component,
|
|
10235
|
-
args: [{ selector: 'rpt-
|
|
10236
|
-
<div class="
|
|
10237
|
-
|
|
10238
|
-
|
|
10239
|
-
|
|
10240
|
-
|
|
10241
|
-
<
|
|
10242
|
-
<
|
|
10243
|
-
|
|
10244
|
-
class="
|
|
10245
|
-
|
|
10246
|
-
|
|
10247
|
-
|
|
10875
|
+
args: [{ selector: 'rpt-parameter-binding-editor', standalone: true, imports: [CommonModule, FormsModule, RptIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
10876
|
+
<div class="param-card" [class.session-bound]="isAutoSessionDerived()">
|
|
10877
|
+
<!-- Card Header: Title, Technical Name, Type, and Badges -->
|
|
10878
|
+
<div class="param-header">
|
|
10879
|
+
<div class="param-title-wrap">
|
|
10880
|
+
<span class="param-label" [title]="param().name">{{ friendlyLabel() }}</span>
|
|
10881
|
+
<span class="param-tech-badge">{{ param().name }}</span>
|
|
10882
|
+
<span class="param-type-badge">{{ displayType() }}</span>
|
|
10883
|
+
@if (isAutoSessionDerived()) {
|
|
10884
|
+
<span class="session-auto-badge">Auto Session</span>
|
|
10885
|
+
} @else if (param().required && !hasValue()) {
|
|
10886
|
+
<span class="required-warn-badge">Required</span>
|
|
10887
|
+
}
|
|
10248
10888
|
</div>
|
|
10249
10889
|
|
|
10250
|
-
|
|
10251
|
-
|
|
10252
|
-
|
|
10253
|
-
|
|
10254
|
-
|
|
10255
|
-
|
|
10256
|
-
|
|
10257
|
-
|
|
10890
|
+
<!-- Binding Mode Segmented Selector -->
|
|
10891
|
+
<div class="binding-mode-tabs" role="tablist">
|
|
10892
|
+
@if (sessionMatch()) {
|
|
10893
|
+
<button
|
|
10894
|
+
type="button"
|
|
10895
|
+
class="mode-btn"
|
|
10896
|
+
[class.active]="activeMode() === 'session'"
|
|
10897
|
+
(click)="setMode('session')"
|
|
10898
|
+
title="Bound to session user/branch context"
|
|
10899
|
+
>
|
|
10900
|
+
<rpt-icon name="user" [size]="11" />
|
|
10901
|
+
<span>Session</span>
|
|
10902
|
+
</button>
|
|
10903
|
+
}
|
|
10904
|
+
<button
|
|
10905
|
+
type="button"
|
|
10906
|
+
class="mode-btn"
|
|
10907
|
+
[class.active]="activeMode() === 'control'"
|
|
10908
|
+
(click)="setMode('control')"
|
|
10909
|
+
title="Link value to a form field on canvas (e.g. Dropdown, DatePicker)"
|
|
10910
|
+
>
|
|
10911
|
+
<rpt-icon name="link" [size]="11" />
|
|
10912
|
+
<span>Form Field</span>
|
|
10913
|
+
</button>
|
|
10914
|
+
<button
|
|
10915
|
+
type="button"
|
|
10916
|
+
class="mode-btn"
|
|
10917
|
+
[class.active]="activeMode() === 'fixed'"
|
|
10918
|
+
(click)="setMode('fixed')"
|
|
10919
|
+
title="Type a manual or fixed value (e.g. 1, 0, 101)"
|
|
10920
|
+
>
|
|
10921
|
+
<rpt-icon name="edit" [size]="11" />
|
|
10922
|
+
<span>Fixed Value</span>
|
|
10923
|
+
</button>
|
|
10924
|
+
<button
|
|
10925
|
+
type="button"
|
|
10926
|
+
class="mode-btn"
|
|
10927
|
+
[class.active]="activeMode() === 'context'"
|
|
10928
|
+
(click)="setMode('context')"
|
|
10929
|
+
title="Use system date presets (e.g. Today, Month Start)"
|
|
10930
|
+
>
|
|
10931
|
+
<rpt-icon name="clock" [size]="11" />
|
|
10932
|
+
<span>Preset</span>
|
|
10933
|
+
</button>
|
|
10934
|
+
@if (isAdvancedMode()) {
|
|
10935
|
+
<button
|
|
10936
|
+
type="button"
|
|
10937
|
+
class="mode-btn"
|
|
10938
|
+
[class.active]="activeMode() === 'custom'"
|
|
10939
|
+
(click)="setMode('custom')"
|
|
10940
|
+
title="Manual template expression"
|
|
10941
|
+
>
|
|
10942
|
+
<rpt-icon name="code" [size]="11" />
|
|
10943
|
+
<span>Expr</span>
|
|
10944
|
+
</button>
|
|
10945
|
+
}
|
|
10258
10946
|
</div>
|
|
10259
10947
|
</div>
|
|
10260
10948
|
|
|
10261
|
-
|
|
10262
|
-
|
|
10949
|
+
<!-- Mode-specific Input Controls -->
|
|
10950
|
+
<div class="param-body">
|
|
10951
|
+
<!-- 0. AUTO SESSION DERIVED -->
|
|
10952
|
+
@if (activeMode() === 'session' && sessionMatch()) {
|
|
10953
|
+
<div class="session-box">
|
|
10954
|
+
<div class="session-box-text">
|
|
10955
|
+
<rpt-icon name="user" [size]="13" class="session-icon" />
|
|
10956
|
+
<span>
|
|
10957
|
+
Derived from <strong>{{ sessionMatch()?.sessionKey }}</strong> ({{ sessionMatch()?.friendlyLabel }})
|
|
10958
|
+
</span>
|
|
10959
|
+
</div>
|
|
10960
|
+
<div class="session-box-actions">
|
|
10961
|
+
<div class="session-val-tag">
|
|
10962
|
+
Current: <strong>{{ sessionMatch()?.value ?? '(none)' }}</strong>
|
|
10963
|
+
</div>
|
|
10964
|
+
<button
|
|
10965
|
+
type="button"
|
|
10966
|
+
class="btn-session-override"
|
|
10967
|
+
(click)="setMode('fixed')"
|
|
10968
|
+
title="Override with manual value"
|
|
10969
|
+
>
|
|
10970
|
+
Override
|
|
10971
|
+
</button>
|
|
10972
|
+
</div>
|
|
10973
|
+
</div>
|
|
10974
|
+
}
|
|
10263
10975
|
|
|
10264
|
-
|
|
10265
|
-
|
|
10266
|
-
<
|
|
10267
|
-
class="
|
|
10268
|
-
|
|
10269
|
-
|
|
10270
|
-
|
|
10271
|
-
|
|
10272
|
-
|
|
10273
|
-
|
|
10274
|
-
|
|
10275
|
-
|
|
10976
|
+
<!-- 1. FORM CONTROL LINKER -->
|
|
10977
|
+
@if (activeMode() === 'control') {
|
|
10978
|
+
<div class="linker-row">
|
|
10979
|
+
<rpt-icon name="link" [size]="13" class="linker-icon" />
|
|
10980
|
+
<select
|
|
10981
|
+
class="field-select linker-select"
|
|
10982
|
+
[ngModel]="selectedControlKey()"
|
|
10983
|
+
(ngModelChange)="onControlSelected($event)"
|
|
10984
|
+
>
|
|
10985
|
+
<option value="">-- Choose Form Field on Canvas --</option>
|
|
10986
|
+
@for (field of compatibleControls(); track field.key || field.id) {
|
|
10987
|
+
<option [value]="field.key || field.id">
|
|
10988
|
+
{{ field.label || field.key }} ({{ field.type }})
|
|
10989
|
+
</option>
|
|
10990
|
+
}
|
|
10991
|
+
</select>
|
|
10992
|
+
</div>
|
|
10993
|
+
@if (!selectedControlKey() && compatibleControls().length === 0) {
|
|
10994
|
+
<div class="empty-hint">No input fields on canvas. Add a dropdown, date picker, or input first.</div>
|
|
10995
|
+
}
|
|
10996
|
+
}
|
|
10276
10997
|
|
|
10277
|
-
|
|
10278
|
-
|
|
10279
|
-
|
|
10280
|
-
|
|
10281
|
-
|
|
10282
|
-
|
|
10283
|
-
|
|
10284
|
-
|
|
10285
|
-
|
|
10286
|
-
|
|
10287
|
-
|
|
10998
|
+
<!-- 2. ENTER FIXED VALUE -->
|
|
10999
|
+
@if (activeMode() === 'fixed') {
|
|
11000
|
+
@switch (param().dataType) {
|
|
11001
|
+
@case ('boolean') {
|
|
11002
|
+
<label class="toggle-switch">
|
|
11003
|
+
<input
|
|
11004
|
+
type="checkbox"
|
|
11005
|
+
[ngModel]="booleanFixedValue()"
|
|
11006
|
+
(ngModelChange)="onBooleanChanged($event)"
|
|
11007
|
+
/>
|
|
11008
|
+
<span class="toggle-text">{{ booleanFixedValue() ? 'Active / Yes (1)' : 'Inactive / No (0)' }}</span>
|
|
11009
|
+
</label>
|
|
11010
|
+
}
|
|
11011
|
+
@case ('number') {
|
|
11012
|
+
<div class="input-wrap">
|
|
11013
|
+
<input
|
|
11014
|
+
type="number"
|
|
11015
|
+
class="field-input"
|
|
11016
|
+
placeholder="Enter number (e.g. 1, 0, 11)"
|
|
11017
|
+
[ngModel]="currentValue()"
|
|
11018
|
+
(ngModelChange)="onFixedValueChanged($event)"
|
|
11019
|
+
/>
|
|
11020
|
+
</div>
|
|
11021
|
+
}
|
|
11022
|
+
@case ('date') {
|
|
11023
|
+
<div class="input-wrap">
|
|
11024
|
+
<input
|
|
11025
|
+
type="date"
|
|
11026
|
+
class="field-input"
|
|
11027
|
+
[ngModel]="currentValue()"
|
|
11028
|
+
(ngModelChange)="onFixedValueChanged($event)"
|
|
11029
|
+
/>
|
|
11030
|
+
</div>
|
|
11031
|
+
}
|
|
11032
|
+
@default {
|
|
11033
|
+
<div class="input-wrap">
|
|
11034
|
+
<input
|
|
11035
|
+
type="text"
|
|
11036
|
+
class="field-input"
|
|
11037
|
+
placeholder="Enter value (e.g. 101, text, code)"
|
|
11038
|
+
[ngModel]="currentValue()"
|
|
11039
|
+
(ngModelChange)="onFixedValueChanged($event)"
|
|
11040
|
+
/>
|
|
11041
|
+
</div>
|
|
11042
|
+
}
|
|
11043
|
+
}
|
|
11044
|
+
@if (paramQuickSuggestions().length > 0) {
|
|
11045
|
+
<div class="quick-chips-row">
|
|
11046
|
+
<span class="chips-label">Quick:</span>
|
|
11047
|
+
@for (chip of paramQuickSuggestions(); track chip.value) {
|
|
11048
|
+
<button
|
|
11049
|
+
type="button"
|
|
11050
|
+
class="chip-btn"
|
|
11051
|
+
[class.active]="isCurrentValue(chip.value)"
|
|
11052
|
+
(click)="onFixedValueChanged(chip.value)"
|
|
11053
|
+
>
|
|
11054
|
+
{{ chip.label }}
|
|
11055
|
+
</button>
|
|
11056
|
+
}
|
|
11057
|
+
</div>
|
|
11058
|
+
}
|
|
11059
|
+
}
|
|
10288
11060
|
|
|
10289
|
-
|
|
10290
|
-
|
|
10291
|
-
<input
|
|
10292
|
-
|
|
10293
|
-
|
|
10294
|
-
|
|
10295
|
-
|
|
10296
|
-
|
|
10297
|
-
|
|
10298
|
-
|
|
10299
|
-
|
|
11061
|
+
<!-- 3. SYSTEM DATE PRESETS -->
|
|
11062
|
+
@if (activeMode() === 'context') {
|
|
11063
|
+
<div class="input-wrap">
|
|
11064
|
+
<select
|
|
11065
|
+
class="field-select"
|
|
11066
|
+
[ngModel]="selectedContextKey()"
|
|
11067
|
+
(ngModelChange)="onContextSelected($event)"
|
|
11068
|
+
>
|
|
11069
|
+
<option value="">-- Choose Date / Context Preset --</option>
|
|
11070
|
+
<option value="{{ '{{' }}context.today{{ '}}' }}">Today's Date</option>
|
|
11071
|
+
<option value="{{ '{{' }}context.startOfMonth{{ '}}' }}">First Day of Current Month</option>
|
|
11072
|
+
<option value="{{ '{{' }}context.endOfMonth{{ '}}' }}">Last Day of Current Month</option>
|
|
11073
|
+
<option value="{{ '{{' }}context.startOfYear{{ '}}' }}">First Day of Current Year</option>
|
|
11074
|
+
<option value="{{ '{{' }}context.yesterday{{ '}}' }}">Yesterday</option>
|
|
11075
|
+
<option value="{{ '{{' }}context.branchId{{ '}}' }}">Current User Branch</option>
|
|
11076
|
+
<option value="{{ '{{' }}context.userId{{ '}}' }}">Current User ID</option>
|
|
11077
|
+
</select>
|
|
11078
|
+
</div>
|
|
11079
|
+
}
|
|
10300
11080
|
|
|
10301
|
-
|
|
10302
|
-
|
|
10303
|
-
<
|
|
10304
|
-
|
|
10305
|
-
|
|
10306
|
-
|
|
10307
|
-
|
|
10308
|
-
|
|
10309
|
-
|
|
10310
|
-
|
|
10311
|
-
|
|
11081
|
+
<!-- 4. CUSTOM EXPRESSION -->
|
|
11082
|
+
@if (activeMode() === 'custom') {
|
|
11083
|
+
<div class="input-wrap">
|
|
11084
|
+
<input
|
|
11085
|
+
type="text"
|
|
11086
|
+
class="field-input monospace-input"
|
|
11087
|
+
placeholder="e.g. {{ '{{' }}form.dropdownField ?? 1{{ '}}' }}"
|
|
11088
|
+
[ngModel]="currentValue()"
|
|
11089
|
+
(ngModelChange)="onFixedValueChanged($event)"
|
|
11090
|
+
/>
|
|
11091
|
+
</div>
|
|
11092
|
+
}
|
|
10312
11093
|
</div>
|
|
11094
|
+
|
|
11095
|
+
@if (param().description && isAdvancedMode()) {
|
|
11096
|
+
<div class="param-desc">{{ param().description }}</div>
|
|
11097
|
+
}
|
|
10313
11098
|
</div>
|
|
10314
|
-
`, styles: [".property-group{display:flex;flex-direction:column;gap:12px}.group-title{font-size:.7rem;font-weight:700;color:var(--rpt-color-text-muted, #64748b);text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--rpt-color-border, #e2e8f0);padding-bottom:4px}.field-row{display:flex;flex-direction:column;gap:4px}.field-label-row{display:flex;align-items:center;justify-content:space-between}.field-badge-val{font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border:1px solid #bfdbfe;padding:1px 6px;border-radius:9999px}.field-label{font-size:.7rem;font-weight:600;color:var(--rpt-color-text, #0f172a)}.field-input,.field-select,.field-textarea{width:100%;box-sizing:border-box;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius-sm, 4px);padding:6px 10px;font-size:var(--rpt-font-size-sm, .75rem);color:var(--rpt-color-text, #0f172a);background:#fff;outline:none;transition:all var(--rpt-transition-speed, .15s) ease}.field-select{appearance:none;-webkit-appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right 10px center;background-size:13px 13px;padding-right:28px;cursor:pointer}.field-input:focus,.field-select:focus,.field-textarea:focus{border-color:var(--rpt-color-primary, #2563eb);box-shadow:var(--rpt-box-shadow-focus, 0 0 0 3px rgba(37, 99, 235, .25))}.checkbox-toggle{display:inline-flex;align-items:center;gap:8px;font-size:.75rem;font-weight:500;color:var(--rpt-color-text, #0f172a);cursor:pointer}.preset-pill-group{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.preset-pill{background:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;padding:2px 8px;font-size:.65rem;font-weight:600;color:#475569;cursor:pointer;transition:all .12s ease}.preset-pill:hover{background:#e2e8f0;color:#0f172a;border-color:#cbd5e1}.preset-pill-active{background:var(--rpt-color-primary, #2563eb)!important;color:#fff!important;border-color:var(--rpt-color-primary, #2563eb)!important}.preset-pill-reset{color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border-color:#bfdbfe}.preset-pill-reset:hover{background:#dbeafe;color:#1d4ed8;border-color:#93c5fd}\n", ".
|
|
11099
|
+
`, styles: [".property-group{display:flex;flex-direction:column;gap:12px}.group-title{font-size:.7rem;font-weight:700;color:var(--rpt-color-text-muted, #64748b);text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--rpt-color-border, #e2e8f0);padding-bottom:4px}.field-row{display:flex;flex-direction:column;gap:4px}.field-label-row{display:flex;align-items:center;justify-content:space-between}.field-badge-val{font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border:1px solid #bfdbfe;padding:1px 6px;border-radius:9999px}.field-label{font-size:.7rem;font-weight:600;color:var(--rpt-color-text, #0f172a)}.field-input,.field-select,.field-textarea{width:100%;box-sizing:border-box;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius-sm, 4px);padding:6px 10px;font-size:var(--rpt-font-size-sm, .75rem);color:var(--rpt-color-text, #0f172a);background:#fff;outline:none;transition:all var(--rpt-transition-speed, .15s) ease}.field-select{appearance:none;-webkit-appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right 10px center;background-size:13px 13px;padding-right:28px;cursor:pointer}.field-input:focus,.field-select:focus,.field-textarea:focus{border-color:var(--rpt-color-primary, #2563eb);box-shadow:var(--rpt-box-shadow-focus, 0 0 0 3px rgba(37, 99, 235, .25))}.checkbox-toggle{display:inline-flex;align-items:center;gap:8px;font-size:.75rem;font-weight:500;color:var(--rpt-color-text, #0f172a);cursor:pointer}.preset-pill-group{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.preset-pill{background:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;padding:2px 8px;font-size:.65rem;font-weight:600;color:#475569;cursor:pointer;transition:all .12s ease}.preset-pill:hover{background:#e2e8f0;color:#0f172a;border-color:#cbd5e1}.preset-pill-active{background:var(--rpt-color-primary, #2563eb)!important;color:#fff!important;border-color:var(--rpt-color-primary, #2563eb)!important}.preset-pill-reset{color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border-color:#bfdbfe}.preset-pill-reset:hover{background:#dbeafe;color:#1d4ed8;border-color:#93c5fd}\n", ".param-card{background:#fff;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:8px;box-shadow:0 1px 3px #00000008;box-sizing:border-box;width:100%;min-width:0;transition:all .12s ease}.param-card:hover{border-color:#94a3b8}.param-card.session-bound{background:#f0fdf4;border-color:#bbf7d0}.param-header{display:flex;flex-direction:column;gap:6px;width:100%;box-sizing:border-box}.param-title-wrap{display:flex;align-items:center;justify-content:flex-start;flex-wrap:wrap;gap:6px;width:100%}.param-label{font-size:.775rem;font-weight:700;color:#0f172a;white-space:nowrap}.param-tech-badge{font-size:.65rem;font-family:monospace;padding:1px 6px;border-radius:4px;background:#eff6ff;color:#2563eb;border:1px solid #bfdbfe}.param-type-badge{font-size:.65rem;padding:1px 6px;border-radius:4px;background:#f1f5f9;color:#475569;text-transform:capitalize}.session-auto-badge{font-size:.625rem;font-weight:600;padding:1px 6px;border-radius:4px;background:#dcfce7;color:#15803d;border:1px solid #bbf7d0}.required-warn-badge{font-size:.625rem;font-weight:600;padding:1px 6px;border-radius:4px;background:#fef2f2;color:#b91c1c;border:1px solid #fecaca}.binding-mode-tabs{display:flex;flex-wrap:wrap;width:100%;background:#f1f5f9;border-radius:6px;padding:3px;gap:3px;box-sizing:border-box}.mode-btn{flex:1 1 70px;display:inline-flex;align-items:center;justify-content:center;gap:4px;padding:5px 6px;font-size:.69rem;font-weight:500;background:transparent;color:#64748b;border:none;border-radius:4px;cursor:pointer;transition:all .12s ease;white-space:nowrap}.mode-btn:hover{color:#0f172a;background:#fff9}.mode-btn.active{background:#fff;color:var(--rpt-color-primary, #2563eb);font-weight:600;box-shadow:0 1px 2px #00000014}.param-body{display:flex;flex-direction:column;gap:6px;width:100%;min-width:0;box-sizing:border-box}.linker-row{display:flex;align-items:center;gap:8px;width:100%;min-width:0;box-sizing:border-box}.linker-icon{color:#2563eb;flex-shrink:0}.linker-select{flex:1;width:100%;min-width:0;box-sizing:border-box;border:1px solid #bfdbfe;border-radius:6px;background-color:#eff6ff;font-weight:500;color:#1e40af;padding:6px 10px;font-size:.75rem}.quick-chips-row{display:flex;align-items:center;flex-wrap:wrap;gap:4px;margin-top:4px}.chips-label{font-size:.65rem;font-weight:600;color:#64748b;margin-right:2px}.chip-btn{font-size:.65rem;font-weight:500;padding:2px 7px;border-radius:4px;background:#f1f5f9;color:#334155;border:1px solid #cbd5e1;cursor:pointer;transition:all .12s ease}.chip-btn:hover{background:#e2e8f0;color:#0f172a;border-color:#94a3b8}.chip-btn.active{background:#dbeafe;color:#1e40af;border-color:#60a5fa;font-weight:600}.input-wrap{width:100%;min-width:0;box-sizing:border-box}.mode-switch-hint{font-size:.68rem;color:#64748b;margin-top:1px;display:flex;align-items:center;gap:5px;flex-wrap:wrap}.btn-text-switch{background:none;border:none;padding:0;color:#2563eb;font-weight:600;cursor:pointer;text-decoration:underline;font-size:.68rem}.btn-text-switch:hover{color:#1d4ed8}.session-box{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;background:#fff;border:1px solid #bbf7d0;border-radius:6px;padding:6px 10px;gap:6px;width:100%;box-sizing:border-box}.session-box-text{display:flex;align-items:center;gap:6px;font-size:.72rem;color:#166534;min-width:0}.session-icon{color:#16a34a;flex-shrink:0}.session-box-actions{display:flex;align-items:center;gap:6px;flex-shrink:0}.session-val-tag{font-size:.7rem;color:#15803d;background:#dcfce7;padding:2px 8px;border-radius:4px;white-space:nowrap;flex-shrink:0}.btn-session-override{background:#fff;border:1px solid #86efac;color:#15803d;font-size:.65rem;font-weight:600;padding:2px 6px;border-radius:4px;cursor:pointer;transition:all .12s ease}.btn-session-override:hover{background:#dcfce7}.empty-hint{font-size:.68rem;color:#94a3b8;font-style:italic}.toggle-switch{display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-size:.75rem;color:#334155;font-weight:500}.monospace-input{font-family:monospace;font-size:.72rem}.param-desc{font-size:.675rem;color:#64748b;line-height:1.35}\n"] }]
|
|
10315
11100
|
}] });
|
|
10316
11101
|
|
|
10317
|
-
class
|
|
10318
|
-
|
|
10319
|
-
|
|
10320
|
-
|
|
11102
|
+
class PropertyEditorGridComponent {
|
|
11103
|
+
dataDiscovery = inject(ReportDataDiscoveryService, { optional: true });
|
|
11104
|
+
grid = input();
|
|
11105
|
+
dataSource = input();
|
|
11106
|
+
availableEntities = input([]);
|
|
11107
|
+
procedureParameters = input([]);
|
|
10321
11108
|
allComponents = input([]);
|
|
10322
|
-
|
|
10323
|
-
|
|
10324
|
-
|
|
10325
|
-
|
|
10326
|
-
|
|
10327
|
-
|
|
10328
|
-
|
|
10329
|
-
|
|
10330
|
-
|
|
10331
|
-
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
|
|
10335
|
-
|
|
10336
|
-
|
|
10337
|
-
|
|
10338
|
-
|
|
10339
|
-
|
|
10340
|
-
|
|
11109
|
+
isDetecting = input(false);
|
|
11110
|
+
statusMessage = input(null);
|
|
11111
|
+
statusKind = input(null);
|
|
11112
|
+
addGrid = output();
|
|
11113
|
+
removeGrid = output();
|
|
11114
|
+
addColumn = output();
|
|
11115
|
+
autoDetectColumns = output();
|
|
11116
|
+
removeColumn = output();
|
|
11117
|
+
updateColumn = output();
|
|
11118
|
+
updateOption = output();
|
|
11119
|
+
updateRowClickAction = output();
|
|
11120
|
+
setDataSource = output();
|
|
11121
|
+
updateDataSourceParameters = output();
|
|
11122
|
+
entityChanged = output();
|
|
11123
|
+
internalParameters = signal([]);
|
|
11124
|
+
effectiveParameters = computed(() => {
|
|
11125
|
+
const fromParent = this.procedureParameters();
|
|
11126
|
+
if (fromParent && fromParent.length > 0)
|
|
11127
|
+
return fromParent;
|
|
11128
|
+
return this.internalParameters();
|
|
11129
|
+
});
|
|
11130
|
+
selectedSourceType = signal('stored_procedure');
|
|
11131
|
+
constructor() {
|
|
11132
|
+
effect(() => {
|
|
11133
|
+
const entity = this.activeEntityName();
|
|
11134
|
+
const type = this.currentSourceType();
|
|
11135
|
+
if (entity && type === 'stored_procedure' && this.dataDiscovery) {
|
|
11136
|
+
this.dataDiscovery.getParameters(entity, 'dbo').subscribe({
|
|
11137
|
+
next: (params) => this.internalParameters.set(params || []),
|
|
11138
|
+
error: () => this.internalParameters.set([]),
|
|
11139
|
+
});
|
|
11140
|
+
}
|
|
11141
|
+
else {
|
|
11142
|
+
this.internalParameters.set([]);
|
|
11143
|
+
}
|
|
11144
|
+
}, { allowSignalWrites: true });
|
|
11145
|
+
}
|
|
11146
|
+
currentSourceType = computed(() => {
|
|
11147
|
+
const ds = this.dataSource();
|
|
11148
|
+
if (ds) {
|
|
11149
|
+
return ds.type === 'table' ? 'table' : 'stored_procedure';
|
|
10341
11150
|
}
|
|
11151
|
+
return this.selectedSourceType();
|
|
10342
11152
|
});
|
|
10343
|
-
|
|
10344
|
-
return this.
|
|
11153
|
+
activeEntityName = computed(() => {
|
|
11154
|
+
return this.dataSource()?.name || this.dataSource()?.endpoint || '';
|
|
10345
11155
|
});
|
|
10346
|
-
|
|
10347
|
-
|
|
10348
|
-
|
|
11156
|
+
availableTables = computed(() => {
|
|
11157
|
+
return this.availableEntities().filter((e) => {
|
|
11158
|
+
const t = (e.type || '').toLowerCase();
|
|
11159
|
+
return t === 'table' || t === 'view';
|
|
11160
|
+
});
|
|
10349
11161
|
});
|
|
10350
|
-
|
|
10351
|
-
|
|
10352
|
-
|
|
10353
|
-
|
|
10354
|
-
|
|
10355
|
-
// If matches session context and has no explicit manual override, default to session
|
|
10356
|
-
if (this.sessionMatch() !== null && (!val || val.startsWith('{{session.'))) {
|
|
10357
|
-
return 'session';
|
|
10358
|
-
}
|
|
10359
|
-
if (!val) {
|
|
10360
|
-
return this.compatibleControls().length > 0 ? 'control' : 'fixed';
|
|
10361
|
-
}
|
|
10362
|
-
if (val.startsWith('{{context.')) {
|
|
10363
|
-
return 'context';
|
|
10364
|
-
}
|
|
10365
|
-
if (val.startsWith('{{session.')) {
|
|
10366
|
-
return 'session';
|
|
10367
|
-
}
|
|
10368
|
-
if (val.startsWith('{{') && val.endsWith('}}')) {
|
|
10369
|
-
const inner = val.slice(2, -2).trim();
|
|
10370
|
-
const match = this.compatibleControls().find((c) => (c.key || c.id) === inner);
|
|
10371
|
-
if (match) {
|
|
10372
|
-
return 'control';
|
|
10373
|
-
}
|
|
10374
|
-
return 'custom';
|
|
10375
|
-
}
|
|
10376
|
-
const plainMatch = this.compatibleControls().find((c) => (c.key || c.id) === val);
|
|
10377
|
-
if (plainMatch) {
|
|
10378
|
-
return 'control';
|
|
10379
|
-
}
|
|
10380
|
-
return 'fixed';
|
|
11162
|
+
availableProcedures = computed(() => {
|
|
11163
|
+
return this.availableEntities().filter((e) => {
|
|
11164
|
+
const t = (e.type || '').toLowerCase();
|
|
11165
|
+
return t === 'stored_procedure' || t === 'procedure' || t.includes('procedure');
|
|
11166
|
+
});
|
|
10381
11167
|
});
|
|
10382
|
-
|
|
10383
|
-
return this.
|
|
11168
|
+
activeEntityList = computed(() => {
|
|
11169
|
+
return this.currentSourceType() === 'table' ? this.availableTables() : this.availableProcedures();
|
|
10384
11170
|
});
|
|
10385
|
-
|
|
10386
|
-
|
|
10387
|
-
if (val.startsWith('{{') && val.endsWith('}}')) {
|
|
10388
|
-
return val.slice(2, -2).trim();
|
|
10389
|
-
}
|
|
10390
|
-
return val;
|
|
11171
|
+
columns = computed(() => {
|
|
11172
|
+
return this.grid()?.columns ?? [];
|
|
10391
11173
|
});
|
|
10392
|
-
|
|
10393
|
-
|
|
10394
|
-
if (val.startsWith('{{context.')) {
|
|
10395
|
-
return val;
|
|
10396
|
-
}
|
|
10397
|
-
return '';
|
|
11174
|
+
rowClickAction = computed(() => {
|
|
11175
|
+
return this.grid()?.rowClickAction ?? this.grid()?.options?.rowClickAction;
|
|
10398
11176
|
});
|
|
10399
|
-
|
|
10400
|
-
|
|
10401
|
-
return v === true || v === 'true' || v === 1 || v === '1';
|
|
11177
|
+
hasRowClickAction = computed(() => {
|
|
11178
|
+
return !!this.rowClickAction();
|
|
10402
11179
|
});
|
|
10403
|
-
|
|
10404
|
-
this.
|
|
10405
|
-
if (
|
|
10406
|
-
|
|
10407
|
-
|
|
10408
|
-
|
|
10409
|
-
|
|
10410
|
-
|
|
10411
|
-
|
|
10412
|
-
|
|
10413
|
-
|
|
10414
|
-
});
|
|
11180
|
+
parameterMappings = computed(() => {
|
|
11181
|
+
const mapping = this.rowClickAction()?.parameterMapping;
|
|
11182
|
+
if (!mapping)
|
|
11183
|
+
return [];
|
|
11184
|
+
return Object.entries(mapping).map(([key, value]) => ({ key, value }));
|
|
11185
|
+
});
|
|
11186
|
+
onSourceTypeChange(type) {
|
|
11187
|
+
this.selectedSourceType.set(type);
|
|
11188
|
+
}
|
|
11189
|
+
onSelectEntity(entityName) {
|
|
11190
|
+
if (!entityName)
|
|
10415
11191
|
return;
|
|
10416
|
-
|
|
10417
|
-
|
|
10418
|
-
|
|
10419
|
-
|
|
10420
|
-
this.valueChange.emit(def);
|
|
10421
|
-
}
|
|
10422
|
-
else {
|
|
10423
|
-
this.valueChange.emit('');
|
|
10424
|
-
}
|
|
10425
|
-
this.linkedFieldChange.emit({
|
|
10426
|
-
paramName: this.param().name,
|
|
10427
|
-
fieldKey: '',
|
|
10428
|
-
isLinked: false,
|
|
11192
|
+
if (this.currentSourceType() === 'stored_procedure' && this.dataDiscovery) {
|
|
11193
|
+
this.dataDiscovery.getParameters(entityName, 'dbo').subscribe({
|
|
11194
|
+
next: (params) => this.internalParameters.set(params || []),
|
|
11195
|
+
error: () => this.internalParameters.set([]),
|
|
10429
11196
|
});
|
|
10430
11197
|
}
|
|
11198
|
+
this.entityChanged.emit({ name: entityName, type: this.currentSourceType() });
|
|
10431
11199
|
}
|
|
10432
|
-
|
|
10433
|
-
|
|
10434
|
-
|
|
10435
|
-
|
|
10436
|
-
|
|
10437
|
-
|
|
10438
|
-
fieldKey: key,
|
|
10439
|
-
isLinked: true,
|
|
10440
|
-
});
|
|
11200
|
+
getParameterValue(paramName) {
|
|
11201
|
+
const mappings = this.dataSource()?.parameterMappings || {};
|
|
11202
|
+
const cleanName = paramName.replace(/^@/, '');
|
|
11203
|
+
const val = mappings[paramName] ?? mappings[`@${cleanName}`] ?? mappings[cleanName];
|
|
11204
|
+
if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {
|
|
11205
|
+
return val;
|
|
10441
11206
|
}
|
|
10442
|
-
|
|
10443
|
-
|
|
10444
|
-
|
|
10445
|
-
|
|
10446
|
-
|
|
10447
|
-
|
|
11207
|
+
return '';
|
|
11208
|
+
}
|
|
11209
|
+
onParameterChange(paramName, value) {
|
|
11210
|
+
const current = { ...(this.dataSource()?.parameterMappings || {}) };
|
|
11211
|
+
const cleanName = paramName.replace(/^@/, '');
|
|
11212
|
+
current[paramName] = value;
|
|
11213
|
+
current[`@${cleanName}`] = value;
|
|
11214
|
+
current[cleanName] = value;
|
|
11215
|
+
this.updateDataSourceParameters.emit(current);
|
|
11216
|
+
}
|
|
11217
|
+
onUpdateCol(index, field, value) {
|
|
11218
|
+
this.updateColumn.emit({ index, field, value });
|
|
11219
|
+
}
|
|
11220
|
+
toggleRowClickAction(enabled) {
|
|
11221
|
+
if (enabled) {
|
|
11222
|
+
this.updateRowClickAction.emit({
|
|
11223
|
+
type: 'open-drawer',
|
|
11224
|
+
drawerTitle: 'Details',
|
|
11225
|
+
drawerWidth: '760px',
|
|
10448
11226
|
});
|
|
10449
11227
|
}
|
|
11228
|
+
else {
|
|
11229
|
+
this.updateRowClickAction.emit(undefined);
|
|
11230
|
+
}
|
|
10450
11231
|
}
|
|
10451
|
-
|
|
10452
|
-
this.
|
|
10453
|
-
|
|
10454
|
-
|
|
10455
|
-
|
|
10456
|
-
|
|
11232
|
+
updateRowActionField(field, value) {
|
|
11233
|
+
const current = this.rowClickAction() ?? { type: 'open-drawer' };
|
|
11234
|
+
const next = { ...current, [field]: value };
|
|
11235
|
+
this.updateRowClickAction.emit(next);
|
|
11236
|
+
}
|
|
11237
|
+
addParameterMapping() {
|
|
11238
|
+
const current = this.rowClickAction() ?? { type: 'open-drawer' };
|
|
11239
|
+
const mapping = { ...(current.parameterMapping ?? {}) };
|
|
11240
|
+
let idx = Object.keys(mapping).length + 1;
|
|
11241
|
+
let key = `param${idx}`;
|
|
11242
|
+
while (key in mapping) {
|
|
11243
|
+
idx++;
|
|
11244
|
+
key = `param${idx}`;
|
|
11245
|
+
}
|
|
11246
|
+
mapping[key] = 'row.id';
|
|
11247
|
+
this.updateRowClickAction.emit({ ...current, parameterMapping: mapping });
|
|
11248
|
+
}
|
|
11249
|
+
updateMappingKey(index, newKey) {
|
|
11250
|
+
const pairs = this.parameterMappings();
|
|
11251
|
+
if (!pairs[index])
|
|
11252
|
+
return;
|
|
11253
|
+
const nextMapping = {};
|
|
11254
|
+
pairs.forEach((p, i) => {
|
|
11255
|
+
const k = i === index ? newKey : p.key;
|
|
11256
|
+
if (k.trim()) {
|
|
11257
|
+
nextMapping[k.trim()] = p.value;
|
|
11258
|
+
}
|
|
10457
11259
|
});
|
|
11260
|
+
const current = this.rowClickAction() ?? { type: 'open-drawer' };
|
|
11261
|
+
this.updateRowClickAction.emit({ ...current, parameterMapping: nextMapping });
|
|
10458
11262
|
}
|
|
10459
|
-
|
|
10460
|
-
this.
|
|
10461
|
-
|
|
10462
|
-
|
|
10463
|
-
|
|
10464
|
-
|
|
11263
|
+
updateMappingValue(index, newValue) {
|
|
11264
|
+
const pairs = this.parameterMappings();
|
|
11265
|
+
if (!pairs[index])
|
|
11266
|
+
return;
|
|
11267
|
+
const nextMapping = {};
|
|
11268
|
+
pairs.forEach((p, i) => {
|
|
11269
|
+
const v = i === index ? newValue : p.value;
|
|
11270
|
+
if (p.key.trim()) {
|
|
11271
|
+
nextMapping[p.key.trim()] = v;
|
|
11272
|
+
}
|
|
10465
11273
|
});
|
|
11274
|
+
const current = this.rowClickAction() ?? { type: 'open-drawer' };
|
|
11275
|
+
this.updateRowClickAction.emit({ ...current, parameterMapping: nextMapping });
|
|
10466
11276
|
}
|
|
10467
|
-
|
|
10468
|
-
this.
|
|
10469
|
-
|
|
10470
|
-
|
|
10471
|
-
|
|
10472
|
-
|
|
11277
|
+
removeMapping(index) {
|
|
11278
|
+
const pairs = this.parameterMappings();
|
|
11279
|
+
const nextMapping = {};
|
|
11280
|
+
pairs.forEach((p, i) => {
|
|
11281
|
+
if (i !== index && p.key.trim()) {
|
|
11282
|
+
nextMapping[p.key.trim()] = p.value;
|
|
11283
|
+
}
|
|
10473
11284
|
});
|
|
11285
|
+
const current = this.rowClickAction() ?? { type: 'open-drawer' };
|
|
11286
|
+
this.updateRowClickAction.emit({ ...current, parameterMapping: nextMapping });
|
|
10474
11287
|
}
|
|
10475
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type:
|
|
10476
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ParameterBindingEditorComponent, isStandalone: true, selector: "rpt-parameter-binding-editor", inputs: { param: { classPropertyName: "param", publicName: "param", isSignal: true, isRequired: true, transformFunction: null }, currentValue: { classPropertyName: "currentValue", publicName: "currentValue", isSignal: true, isRequired: false, transformFunction: null }, allComponents: { classPropertyName: "allComponents", publicName: "allComponents", isSignal: true, isRequired: false, transformFunction: null }, currentComponentId: { classPropertyName: "currentComponentId", publicName: "currentComponentId", isSignal: true, isRequired: false, transformFunction: null }, isAdvancedMode: { classPropertyName: "isAdvancedMode", publicName: "isAdvancedMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange", linkedFieldChange: "linkedFieldChange" }, ngImport: i0, template: `
|
|
10477
|
-
|
|
10478
|
-
|
|
10479
|
-
|
|
10480
|
-
<span class="param-label">{{ friendlyLabel() }}</span>
|
|
10481
|
-
|
|
10482
|
-
|
|
10483
|
-
|
|
10484
|
-
|
|
10485
|
-
|
|
10486
|
-
|
|
10487
|
-
|
|
10488
|
-
|
|
10489
|
-
|
|
10490
|
-
|
|
11288
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PropertyEditorGridComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
11289
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: PropertyEditorGridComponent, isStandalone: true, selector: "rpt-property-editor-grid", inputs: { grid: { classPropertyName: "grid", publicName: "grid", isSignal: true, isRequired: false, transformFunction: null }, dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null }, availableEntities: { classPropertyName: "availableEntities", publicName: "availableEntities", isSignal: true, isRequired: false, transformFunction: null }, procedureParameters: { classPropertyName: "procedureParameters", publicName: "procedureParameters", isSignal: true, isRequired: false, transformFunction: null }, allComponents: { classPropertyName: "allComponents", publicName: "allComponents", isSignal: true, isRequired: false, transformFunction: null }, isDetecting: { classPropertyName: "isDetecting", publicName: "isDetecting", isSignal: true, isRequired: false, transformFunction: null }, statusMessage: { classPropertyName: "statusMessage", publicName: "statusMessage", isSignal: true, isRequired: false, transformFunction: null }, statusKind: { classPropertyName: "statusKind", publicName: "statusKind", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { addGrid: "addGrid", removeGrid: "removeGrid", addColumn: "addColumn", autoDetectColumns: "autoDetectColumns", removeColumn: "removeColumn", updateColumn: "updateColumn", updateOption: "updateOption", updateRowClickAction: "updateRowClickAction", setDataSource: "setDataSource", updateDataSourceParameters: "updateDataSourceParameters", entityChanged: "entityChanged" }, ngImport: i0, template: " @if (!grid()) {\n <div class=\"empty-grid-card\">\n <div class=\"empty-grid-icon\">\n <rpt-icon name=\"table\" [size]=\"28\" />\n </div>\n <h4 class=\"empty-grid-title\">No Result Grid Attached</h4>\n <p class=\"empty-grid-desc\">Attach an interactive Result Data Grid (or UltraGrid) to this report definition to render queries and stored procedure results.</p>\n <button type=\"button\" class=\"btn-create-grid-action\" (click)=\"addGrid.emit()\">\n <rpt-icon name=\"plus\" [size]=\"14\" />\n <span>+ Add Result Grid</span>\n </button>\n </div>\n } @else {\n <div class=\"props-form\">\n <!-- 1. Report Data Source & Stored Procedure Configuration -->\n <div class=\"property-group\">\n <div class=\"group-header-row\">\n <div class=\"group-title-wrap\">\n <span class=\"group-title\">Report Data Source</span>\n @if (activeEntityName()) {\n <span class=\"group-badge active-ds-badge\">{{ activeEntityName() }}</span>\n }\n </div>\n </div>\n\n <!-- Source Type Switcher (Stored Procedure vs Table) -->\n <div class=\"source-type-switcher\">\n <button\n type=\"button\"\n class=\"source-tab-btn\"\n [class.active]=\"currentSourceType() === 'stored_procedure'\"\n (click)=\"onSourceTypeChange('stored_procedure')\"\n >\n <rpt-icon name=\"code\" [size]=\"12\" />\n <span>Stored Procedure</span>\n </button>\n <button\n type=\"button\"\n class=\"source-tab-btn\"\n [class.active]=\"currentSourceType() === 'table'\"\n (click)=\"onSourceTypeChange('table')\"\n >\n <rpt-icon name=\"table\" [size]=\"12\" />\n <span>Table / View</span>\n </button>\n </div>\n\n <!-- Entity Selector Dropdown -->\n <div class=\"field-row\">\n <label class=\"field-label\">\n {{ currentSourceType() === 'stored_procedure' ? 'Stored Procedure' : 'Table / View' }}\n </label>\n <select\n class=\"field-select\"\n [ngModel]=\"activeEntityName()\"\n (ngModelChange)=\"onSelectEntity($event)\"\n >\n <option value=\"\">-- Choose Data Source --</option>\n @for (ent of activeEntityList(); track ent.name) {\n <option [value]=\"ent.name\">\n {{ ent.friendlyName || ent.name }} ({{ ent.name }})\n </option>\n }\n </select>\n </div>\n\n <!-- Stored Procedure Parameter Mappings -->\n @if (currentSourceType() === 'stored_procedure' && activeEntityName()) {\n <div class=\"procedure-params-section\">\n <div class=\"params-section-header\">\n <div class=\"params-header-left\">\n <span class=\"params-title\">Procedure Parameters</span>\n <span class=\"params-count-badge\">{{ effectiveParameters().length }}</span>\n </div>\n </div>\n\n @if (effectiveParameters().length > 0) {\n <div class=\"param-cards-list\">\n @for (param of effectiveParameters(); track param.name) {\n <rpt-parameter-binding-editor\n [param]=\"param\"\n [currentValue]=\"getParameterValue(param.name)\"\n [allComponents]=\"allComponents()\"\n (valueChange)=\"onParameterChange(param.name, $event)\"\n />\n }\n </div>\n } @else {\n <div class=\"empty-params-hint\">\n <span>No required parameters for this procedure.</span>\n </div>\n }\n </div>\n }\n </div>\n\n <!-- 2. Result Columns Configuration -->\n <div class=\"property-group\">\n <div class=\"group-header-row\">\n <div class=\"group-title-wrap\">\n <span class=\"group-title\">Columns</span>\n <span class=\"group-badge\">{{ columns().length }}</span>\n </div>\n </div>\n\n <!-- Dedicated Full-Width Action Buttons (No cramping) -->\n <div class=\"columns-action-bar\">\n <button\n type=\"button\"\n class=\"btn-col-action btn-auto-detect\"\n (click)=\"autoDetectColumns.emit()\"\n [disabled]=\"isDetecting()\"\n title=\"Auto-detect all columns from procedure or data table\"\n >\n <rpt-icon name=\"refresh\" [size]=\"12\" [class.rpt-spin]=\"isDetecting()\" />\n <span>{{ isDetecting() ? 'Detecting...' : 'Auto-Detect' }}</span>\n </button>\n <button\n type=\"button\"\n class=\"btn-col-action btn-add-col\"\n (click)=\"addColumn.emit()\"\n title=\"Add a new column manually\"\n >\n <rpt-icon name=\"plus\" [size]=\"12\" />\n <span>+ Add Column</span>\n </button>\n </div>\n\n @if (statusMessage()) {\n <div class=\"col-status-banner\" [class.is-error]=\"statusKind() === 'error'\" [class.is-success]=\"statusKind() === 'success'\">\n <rpt-icon [name]=\"statusKind() === 'error' ? 'close' : 'check'\" [size]=\"12\" />\n <span>{{ statusMessage() }}</span>\n </div>\n }\n\n <div class=\"columns-editor-list\">\n @for (col of columns(); track col.field; let idx = $index) {\n <div class=\"col-edit-card\">\n <div class=\"col-card-header\">\n <div class=\"col-header-title-group\">\n <span class=\"col-drag-dots\">\u22EE\u22EE</span>\n <span class=\"col-idx-badge\">#{{ idx + 1 }}</span>\n <input\n type=\"text\"\n class=\"col-title-input\"\n placeholder=\"Column Label\"\n [ngModel]=\"col.label\"\n (ngModelChange)=\"onUpdateCol(idx, 'label', $event)\"\n />\n </div>\n <div class=\"col-card-actions\">\n <span class=\"col-type-pill\">{{ col.dataType || 'string' }}</span>\n <button\n type=\"button\"\n class=\"col-action-btn btn-delete\"\n (click)=\"removeColumn.emit(idx)\"\n title=\"Remove Column\"\n >\n <rpt-icon name=\"trash\" [size]=\"12\" />\n </button>\n </div>\n </div>\n\n <div class=\"col-card-body\">\n <div class=\"col-edit-fields\">\n <div class=\"col-field-item\">\n <span class=\"mini-label\">Field</span>\n <input\n type=\"text\"\n class=\"col-mini-input\"\n placeholder=\"field\"\n [ngModel]=\"col.field\"\n (ngModelChange)=\"onUpdateCol(idx, 'field', $event)\"\n />\n </div>\n <div class=\"col-field-item\">\n <span class=\"mini-label\">Type</span>\n <select\n class=\"col-mini-select\"\n [ngModel]=\"col.dataType || 'string'\"\n (ngModelChange)=\"onUpdateCol(idx, 'dataType', $event)\"\n >\n <option value=\"string\">String</option>\n <option value=\"number\">Number</option>\n <option value=\"currency\">Currency</option>\n <option value=\"date\">Date</option>\n <option value=\"boolean\">Boolean</option>\n </select>\n </div>\n <div class=\"col-field-item\">\n <span class=\"mini-label\">Format</span>\n <input\n type=\"text\"\n class=\"col-mini-input\"\n placeholder=\"e.g. dd/MM/yyyy\"\n [ngModel]=\"col.format\"\n (ngModelChange)=\"onUpdateCol(idx, 'format', $event)\"\n />\n </div>\n <div class=\"col-field-item\">\n <span class=\"mini-label\">Align</span>\n <select\n class=\"col-mini-select\"\n [ngModel]=\"col.align || 'left'\"\n (ngModelChange)=\"onUpdateCol(idx, 'align', $event)\"\n >\n <option value=\"left\">Left</option>\n <option value=\"center\">Center</option>\n <option value=\"right\">Right</option>\n </select>\n </div>\n <div class=\"col-field-item\">\n <span class=\"mini-label\">Aggregate</span>\n <select\n class=\"col-mini-select\"\n [ngModel]=\"col.aggregate || 'none'\"\n (ngModelChange)=\"onUpdateCol(idx, 'aggregate', $event === 'none' ? undefined : $event)\"\n >\n <option value=\"none\">None</option>\n <option value=\"sum\">Sum</option>\n <option value=\"avg\">Average</option>\n <option value=\"count\">Count</option>\n <option value=\"min\">Min</option>\n <option value=\"max\">Max</option>\n </select>\n </div>\n <div class=\"col-field-item\">\n <span class=\"mini-label\">Width</span>\n <input\n type=\"text\"\n class=\"col-mini-input\"\n placeholder=\"auto\"\n [ngModel]=\"col.width || ''\"\n (ngModelChange)=\"onUpdateCol(idx, 'width', $event || undefined)\"\n />\n </div>\n </div>\n\n <div class=\"col-card-footer\">\n <label class=\"col-checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"col.visible !== false\"\n (ngModelChange)=\"onUpdateCol(idx, 'visible', $event)\"\n />\n <span>Visible</span>\n </label>\n <label class=\"col-checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"col.checkboxSelection ?? false\"\n (ngModelChange)=\"onUpdateCol(idx, 'checkboxSelection', $event)\"\n />\n <span>Checkbox</span>\n </label>\n </div>\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Grid Table Options Group -->\n <div class=\"property-group\">\n <div class=\"group-title\">Table Options</div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.sideBar !== false\"\n (ngModelChange)=\"updateOption.emit({ field: 'sideBar', value: $event })\"\n />\n <span>Enable Side Panel (Tool Panels)</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.globalSearch !== false\"\n (ngModelChange)=\"updateOption.emit({ field: 'globalSearch', value: $event })\"\n />\n <span>Global Search Box</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.statusBar !== false\"\n (ngModelChange)=\"updateOption.emit({ field: 'statusBar', value: $event })\"\n />\n <span>Status Bar</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.pagination ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'pagination', value: $event })\"\n />\n <span>Enable Pagination</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.selectable ?? false\"\n (ngModelChange)=\"updateOption.emit({ field: 'selectable', value: $event })\"\n />\n <span>Row Selection</span>\n </label>\n </div>\n\n @if (grid()?.options?.selectable) {\n <div class=\"field-row\">\n <label class=\"field-label\">Selection Mode</label>\n <select\n class=\"field-input\"\n [ngModel]=\"grid()?.options?.selectionMode ?? 'multiple'\"\n (ngModelChange)=\"updateOption.emit({ field: 'selectionMode', value: $event })\"\n >\n <option value=\"multiple\">Multiple Rows</option>\n <option value=\"single\">Single Row</option>\n </select>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.checkboxSelection ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'checkboxSelection', value: $event })\"\n />\n <span>Checkbox Selection</span>\n </label>\n </div>\n\n @if ((grid()?.options?.selectionMode ?? 'multiple') === 'multiple') {\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.headerCheckboxSelection ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'headerCheckboxSelection', value: $event })\"\n />\n <span>Header Select All Checkbox</span>\n </label>\n </div>\n }\n }\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.allowColumnReorder ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'allowColumnReorder', value: $event })\"\n />\n <span>Allow Column Reorder</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.allowExport ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'allowExport', value: $event })\"\n />\n <span>Allow Export</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"field-label\">Page Size</label>\n <input\n type=\"number\"\n class=\"field-input\"\n min=\"5\"\n max=\"100\"\n [ngModel]=\"grid()?.options?.pageSize ?? 10\"\n (ngModelChange)=\"updateOption.emit({ field: 'pageSize', value: +$event })\"\n />\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.showRowNumbers ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'showRowNumbers', value: $event })\"\n />\n <span>Show Row Numbers</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.showFooter ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'showFooter', value: $event })\"\n />\n <span>Show Footer Totals</span>\n </label>\n </div>\n </div>\n\n <!-- Row Click & Drill-Down Action Group -->\n <div class=\"property-group\">\n <div class=\"group-title\">Row Click & Drill-Down</div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"hasRowClickAction()\"\n (ngModelChange)=\"toggleRowClickAction($event)\"\n />\n <span>Enable Row Click Action</span>\n </label>\n </div>\n\n @if (hasRowClickAction()) {\n <div class=\"field-row\">\n <label class=\"field-label\">Action Type</label>\n <select\n class=\"field-input field-select\"\n [ngModel]=\"rowClickAction()?.type || 'open-drawer'\"\n (ngModelChange)=\"updateRowActionField('type', $event)\"\n >\n <option value=\"open-drawer\">Slide-Over Detail Drawer</option>\n <option value=\"drilldown-report\">Drill-Down Report (Host Event)</option>\n <option value=\"navigate\">Navigate to URL</option>\n <option value=\"emit-custom\">Emit Custom Action</option>\n </select>\n </div>\n\n @if (rowClickAction()?.type === 'open-drawer' || rowClickAction()?.type === 'drilldown-report') {\n <div class=\"field-row\">\n <label class=\"field-label\">Target Report ID</label>\n <input\n type=\"text\"\n class=\"field-input\"\n placeholder=\"e.g. order-details\"\n [ngModel]=\"rowClickAction()?.targetReportId || ''\"\n (ngModelChange)=\"updateRowActionField('targetReportId', $event)\"\n />\n </div>\n\n <div class=\"field-row\">\n <label class=\"field-label\">Drawer Title</label>\n <input\n type=\"text\"\n class=\"field-input\"\n placeholder=\"e.g. Order Details\"\n [ngModel]=\"rowClickAction()?.drawerTitle || ''\"\n (ngModelChange)=\"updateRowActionField('drawerTitle', $event)\"\n />\n </div>\n\n <div class=\"field-row\">\n <label class=\"field-label\">Drawer Width</label>\n <input\n type=\"text\"\n class=\"field-input\"\n placeholder=\"e.g. 760px, 80%, 900px\"\n [ngModel]=\"rowClickAction()?.drawerWidth || '760px'\"\n (ngModelChange)=\"updateRowActionField('drawerWidth', $event)\"\n />\n </div>\n }\n\n @if (rowClickAction()?.type === 'navigate') {\n <div class=\"field-row\">\n <label class=\"field-label\">Target URL</label>\n <input\n type=\"text\"\n class=\"field-input\"\n [placeholder]=\"'/orders/{{row.orderId}} or https://...'\"\n [ngModel]=\"rowClickAction()?.targetUrl || ''\"\n (ngModelChange)=\"updateRowActionField('targetUrl', $event)\"\n />\n </div>\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"rowClickAction()?.openNewTab ?? false\"\n (ngModelChange)=\"updateRowActionField('openNewTab', $event)\"\n />\n <span>Open in New Tab</span>\n </label>\n </div>\n }\n\n @if (rowClickAction()?.type === 'emit-custom') {\n <div class=\"field-row\">\n <label class=\"field-label\">Custom Action Name</label>\n <input\n type=\"text\"\n class=\"field-input\"\n placeholder=\"e.g. view-audit-trail\"\n [ngModel]=\"rowClickAction()?.customActionName || ''\"\n (ngModelChange)=\"updateRowActionField('customActionName', $event)\"\n />\n </div>\n }\n\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">Parameter Mapping</label>\n <button\n type=\"button\"\n class=\"btn-add-mapping\"\n (click)=\"addParameterMapping()\"\n >\n + Add Mapping\n </button>\n </div>\n\n @for (mapping of parameterMappings(); track mapping.key; let mIdx = $index) {\n <div class=\"mapping-pair-row\">\n <input\n type=\"text\"\n class=\"col-mini-input\"\n style=\"flex: 1;\"\n placeholder=\"Target (e.g. customerId)\"\n [ngModel]=\"mapping.key\"\n (ngModelChange)=\"updateMappingKey(mIdx, $event)\"\n />\n <span style=\"font-size: 0.75rem; color: #94a3b8;\">\u2190</span>\n <input\n type=\"text\"\n class=\"col-mini-input\"\n style=\"flex: 1;\"\n placeholder=\"Source (e.g. row.id)\"\n [ngModel]=\"mapping.value\"\n (ngModelChange)=\"updateMappingValue(mIdx, $event)\"\n />\n <button\n type=\"button\"\n class=\"col-action-btn btn-delete\"\n (click)=\"removeMapping(mIdx)\"\n title=\"Remove mapping\"\n >\n <rpt-icon name=\"trash\" [size]=\"11\" />\n </button>\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Remove Grid Action -->\n <div class=\"grid-danger-zone\">\n <button type=\"button\" class=\"btn-remove-grid-action\" (click)=\"removeGrid.emit()\">\n <rpt-icon name=\"trash\" [size]=\"13\" />\n <span>Remove Grid from Report</span>\n </button>\n </div>\n </div>\n }", styles: [".property-group{display:flex;flex-direction:column;gap:12px}.group-title{font-size:.7rem;font-weight:700;color:var(--rpt-color-text-muted, #64748b);text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--rpt-color-border, #e2e8f0);padding-bottom:4px}.field-row{display:flex;flex-direction:column;gap:4px}.field-label-row{display:flex;align-items:center;justify-content:space-between}.field-badge-val{font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border:1px solid #bfdbfe;padding:1px 6px;border-radius:9999px}.field-label{font-size:.7rem;font-weight:600;color:var(--rpt-color-text, #0f172a)}.field-input,.field-select,.field-textarea{width:100%;box-sizing:border-box;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius-sm, 4px);padding:6px 10px;font-size:var(--rpt-font-size-sm, .75rem);color:var(--rpt-color-text, #0f172a);background:#fff;outline:none;transition:all var(--rpt-transition-speed, .15s) ease}.field-select{appearance:none;-webkit-appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right 10px center;background-size:13px 13px;padding-right:28px;cursor:pointer}.field-input:focus,.field-select:focus,.field-textarea:focus{border-color:var(--rpt-color-primary, #2563eb);box-shadow:var(--rpt-box-shadow-focus, 0 0 0 3px rgba(37, 99, 235, .25))}.checkbox-toggle{display:inline-flex;align-items:center;gap:8px;font-size:.75rem;font-weight:500;color:var(--rpt-color-text, #0f172a);cursor:pointer}.preset-pill-group{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.preset-pill{background:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;padding:2px 8px;font-size:.65rem;font-weight:600;color:#475569;cursor:pointer;transition:all .12s ease}.preset-pill:hover{background:#e2e8f0;color:#0f172a;border-color:#cbd5e1}.preset-pill-active{background:var(--rpt-color-primary, #2563eb)!important;color:#fff!important;border-color:var(--rpt-color-primary, #2563eb)!important}.preset-pill-reset{color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border-color:#bfdbfe}.preset-pill-reset:hover{background:#dbeafe;color:#1d4ed8;border-color:#93c5fd}\n", ".empty-grid-card{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 16px;text-align:center;border:2px dashed #cbd5e1;border-radius:8px;background:#fff;margin:10px 0}.empty-grid-icon{width:48px;height:48px;border-radius:10px;background:#eff6ff;color:#2563eb;display:flex;align-items:center;justify-content:center;margin-bottom:12px}.empty-grid-title{font-size:.95rem;font-weight:700;color:#0f172a;margin:0 0 6px}.empty-grid-desc{font-size:.775rem;color:#64748b;margin:0 0 18px;line-height:1.4;max-width:280px}.btn-create-grid-action{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;background:#2563eb;color:#fff;font-size:.775rem;font-weight:600;border:none;border-radius:6px;cursor:pointer;box-shadow:0 2px 4px #2563eb33;transition:all .15s ease}.btn-create-grid-action:hover{background:#1d4ed8;box-shadow:0 4px 8px #2563eb40}.grid-danger-zone{margin-top:10px;padding-top:16px;border-top:1px dashed #e2e8f0;display:flex;justify-content:center}.btn-remove-grid-action{display:inline-flex;align-items:center;gap:6px;background:#fee2e2;border:1px solid #fecaca;color:#dc2626;font-size:.75rem;font-weight:600;padding:6px 12px;border-radius:6px;cursor:pointer;transition:all .15s ease}.btn-remove-grid-action:hover{background:#fecaca;border-color:#f87171;color:#b91c1c}.props-form{display:flex;flex-direction:column;gap:20px}.group-header-row{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--rpt-color-border, #e2e8f0);padding-bottom:6px;margin-bottom:8px}.group-title-wrap{display:flex;align-items:center;gap:6px}.group-badge{font-size:.65rem;font-weight:700;color:#64748b;background:#f1f5f9;border:1px solid #e2e8f0;border-radius:10px;padding:1px 6px}.columns-action-bar{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px}.btn-col-action{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:7px 8px;font-size:.725rem;font-weight:600;border-radius:6px;cursor:pointer;white-space:nowrap;transition:all .12s ease;box-sizing:border-box}.btn-col-action:disabled{opacity:.6;cursor:not-allowed}.btn-auto-detect{background:#eff6ff;border:1px solid #bfdbfe;color:#2563eb}.btn-auto-detect:hover:not(:disabled){background:#2563eb;color:#fff;border-color:#2563eb}.btn-add-col{background:#fff;border:1px solid var(--rpt-color-border, #cbd5e1);color:#334155}.btn-add-col:hover{background:#f8fafc;border-color:#94a3b8;color:#0f172a}.col-status-banner{display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:6px;font-size:.72rem;line-height:1.35;margin-bottom:10px;background:#f1f5f9;color:#334155;border:1px solid #cbd5e1}.col-status-banner.is-success{background:#f0fdf4;color:#166534;border-color:#bbf7d0}.col-status-banner.is-error{background:#fef2f2;color:#991b1b;border-color:#fecaca}@keyframes rpt-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.rpt-spin{animation:rpt-spin 1s linear infinite}.columns-editor-list{display:flex;flex-direction:column;gap:10px}.col-edit-card{background:#fff;border:1.5px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius, 6px);box-shadow:var(--rpt-box-shadow, 0 1px 2px rgba(0, 0, 0, .05));box-sizing:border-box;transition:all var(--rpt-transition-speed, .15s) ease;position:relative;overflow:hidden;display:flex;flex-direction:column}.col-edit-card:hover{border-color:var(--rpt-color-primary, #2563eb);box-shadow:0 4px 6px -1px #2563eb1a}.col-card-header{display:flex;align-items:center;justify-content:space-between;gap:6px;padding:5px 8px;background:var(--rpt-color-surface, #f8fafc);border-bottom:1px solid var(--rpt-color-border, #e2e8f0);min-height:32px;box-sizing:border-box}.col-header-title-group{display:flex;align-items:center;gap:5px;min-width:0;flex:1}.col-drag-dots{color:#94a3b8;font-size:.8rem;cursor:grab;-webkit-user-select:none;user-select:none;line-height:1;padding-left:2px}.col-idx-badge{font-size:.65rem;font-weight:700;color:#64748b;background:#e2e8f0;padding:1px 5px;border-radius:3px;letter-spacing:.02em}.col-title-input{flex:1;border:1px solid transparent;background:transparent;font-size:.75rem;font-weight:600;color:var(--rpt-color-text, #0f172a);padding:2px 6px;border-radius:4px;outline:none;transition:all .15s ease}.col-title-input:hover{border-color:#cbd5e1;background:#fff}.col-title-input:focus{border-color:var(--rpt-color-primary, #2563eb);background:#fff;box-shadow:0 0 0 2px #2563eb26}.col-card-actions{display:flex;align-items:center;gap:4px}.col-type-pill{font-size:.625rem;font-weight:700;color:#1e40af;background:#dbeafe;border:1px solid #bfdbfe;padding:1px 5px;border-radius:3px;text-transform:uppercase;letter-spacing:.04em}.col-action-btn{display:inline-flex;align-items:center;justify-content:center;background:none;border:none;color:var(--rpt-color-text-muted, #64748b);cursor:pointer;padding:3px;border-radius:4px;transition:all .15s ease}.col-action-btn.btn-delete:hover{color:var(--rpt-color-danger, #ef4444);background:#fee2e2}.col-card-body{padding:8px 10px;display:flex;flex-direction:column;gap:6px;background:#fff}.col-edit-fields{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px 8px}.col-field-item{display:flex;flex-direction:column;gap:2px;min-width:0}.mini-label{font-size:.625rem;font-weight:600;color:#64748b;letter-spacing:.02em}.col-mini-input,.col-mini-select{width:100%;box-sizing:border-box;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:3px;padding:3px 5px;font-size:.7rem;color:var(--rpt-color-text, #0f172a);background:#fff;outline:none;transition:all .12s ease}.col-mini-select{appearance:none;-webkit-appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right 4px center;background-size:10px 10px;padding-right:16px;cursor:pointer}.col-mini-input:focus,.col-mini-select:focus{border-color:var(--rpt-color-primary, #2563eb);box-shadow:0 0 0 2px #2563eb26}.col-card-footer{display:flex;align-items:center;justify-content:flex-end;gap:12px;padding-top:4px;border-top:1px dashed #e2e8f0;margin-top:2px}.col-checkbox-toggle{display:inline-flex;align-items:center;gap:5px;font-size:.675rem;font-weight:500;color:#475569;cursor:pointer;-webkit-user-select:none;user-select:none}.btn-add-mapping{display:inline-flex;align-items:center;background:#eff6ff;border:1px solid #bfdbfe;color:#2563eb;font-size:.65rem;font-weight:600;padding:2px 6px;border-radius:4px;cursor:pointer;transition:all .12s ease}.btn-add-mapping:hover{background:#dbeafe}.mapping-pair-row{display:flex;align-items:center;gap:6px;margin-top:4px}.active-ds-badge{background:#eff6ff;color:#2563eb;font-size:.65rem;font-weight:700;padding:1px 6px;border-radius:4px;border:1px solid #bfdbfe;max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.source-type-switcher{display:flex;background:#f1f5f9;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:6px;padding:2px;gap:2px;margin-bottom:12px}.source-tab-btn{flex:1;display:inline-flex;align-items:center;justify-content:center;gap:5px;padding:6px 4px;font-size:.725rem;font-weight:600;color:var(--rpt-color-text-muted, #64748b);background:transparent;border:none;border-radius:4px;cursor:pointer;transition:all .12s ease}.source-tab-btn:hover{color:var(--rpt-color-text, #0f172a)}.source-tab-btn.active{background:#fff;color:var(--rpt-color-primary, #2563eb);box-shadow:0 1px 2px #0000000f}.procedure-params-section{margin-top:14px;padding:0;background:transparent;border:none}.params-section-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;padding-bottom:4px;border-bottom:1px solid var(--rpt-color-border, #e2e8f0)}.params-header-left{display:flex;align-items:center;gap:6px}.params-title{font-size:.725rem;font-weight:700;color:#334155;text-transform:uppercase;letter-spacing:.04em}.params-count-badge{font-size:.625rem;font-weight:700;color:#2563eb;background:#eff6ff;border:1px solid #bfdbfe;border-radius:9999px;padding:0 6px;line-height:16px}.param-cards-list{display:flex;flex-direction:column;gap:10px;width:100%;box-sizing:border-box}.empty-params-hint{font-size:.725rem;color:#64748b;font-style:italic;padding:12px;text-align:center;background:#f8fafc;border:1px dashed #cbd5e1;border-radius:6px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.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: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: RptIconComponent, selector: "rpt-icon", inputs: ["name", "size", "color", "strokeWidth"] }, { kind: "component", type: ParameterBindingEditorComponent, selector: "rpt-parameter-binding-editor", inputs: ["param", "currentValue", "allComponents", "currentComponentId", "isAdvancedMode"], outputs: ["valueChange", "linkedFieldChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
11290
|
+
}
|
|
11291
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PropertyEditorGridComponent, decorators: [{
|
|
11292
|
+
type: Component,
|
|
11293
|
+
args: [{ selector: 'rpt-property-editor-grid', standalone: true, imports: [CommonModule, FormsModule, RptIconComponent, ParameterBindingEditorComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: " @if (!grid()) {\n <div class=\"empty-grid-card\">\n <div class=\"empty-grid-icon\">\n <rpt-icon name=\"table\" [size]=\"28\" />\n </div>\n <h4 class=\"empty-grid-title\">No Result Grid Attached</h4>\n <p class=\"empty-grid-desc\">Attach an interactive Result Data Grid (or UltraGrid) to this report definition to render queries and stored procedure results.</p>\n <button type=\"button\" class=\"btn-create-grid-action\" (click)=\"addGrid.emit()\">\n <rpt-icon name=\"plus\" [size]=\"14\" />\n <span>+ Add Result Grid</span>\n </button>\n </div>\n } @else {\n <div class=\"props-form\">\n <!-- 1. Report Data Source & Stored Procedure Configuration -->\n <div class=\"property-group\">\n <div class=\"group-header-row\">\n <div class=\"group-title-wrap\">\n <span class=\"group-title\">Report Data Source</span>\n @if (activeEntityName()) {\n <span class=\"group-badge active-ds-badge\">{{ activeEntityName() }}</span>\n }\n </div>\n </div>\n\n <!-- Source Type Switcher (Stored Procedure vs Table) -->\n <div class=\"source-type-switcher\">\n <button\n type=\"button\"\n class=\"source-tab-btn\"\n [class.active]=\"currentSourceType() === 'stored_procedure'\"\n (click)=\"onSourceTypeChange('stored_procedure')\"\n >\n <rpt-icon name=\"code\" [size]=\"12\" />\n <span>Stored Procedure</span>\n </button>\n <button\n type=\"button\"\n class=\"source-tab-btn\"\n [class.active]=\"currentSourceType() === 'table'\"\n (click)=\"onSourceTypeChange('table')\"\n >\n <rpt-icon name=\"table\" [size]=\"12\" />\n <span>Table / View</span>\n </button>\n </div>\n\n <!-- Entity Selector Dropdown -->\n <div class=\"field-row\">\n <label class=\"field-label\">\n {{ currentSourceType() === 'stored_procedure' ? 'Stored Procedure' : 'Table / View' }}\n </label>\n <select\n class=\"field-select\"\n [ngModel]=\"activeEntityName()\"\n (ngModelChange)=\"onSelectEntity($event)\"\n >\n <option value=\"\">-- Choose Data Source --</option>\n @for (ent of activeEntityList(); track ent.name) {\n <option [value]=\"ent.name\">\n {{ ent.friendlyName || ent.name }} ({{ ent.name }})\n </option>\n }\n </select>\n </div>\n\n <!-- Stored Procedure Parameter Mappings -->\n @if (currentSourceType() === 'stored_procedure' && activeEntityName()) {\n <div class=\"procedure-params-section\">\n <div class=\"params-section-header\">\n <div class=\"params-header-left\">\n <span class=\"params-title\">Procedure Parameters</span>\n <span class=\"params-count-badge\">{{ effectiveParameters().length }}</span>\n </div>\n </div>\n\n @if (effectiveParameters().length > 0) {\n <div class=\"param-cards-list\">\n @for (param of effectiveParameters(); track param.name) {\n <rpt-parameter-binding-editor\n [param]=\"param\"\n [currentValue]=\"getParameterValue(param.name)\"\n [allComponents]=\"allComponents()\"\n (valueChange)=\"onParameterChange(param.name, $event)\"\n />\n }\n </div>\n } @else {\n <div class=\"empty-params-hint\">\n <span>No required parameters for this procedure.</span>\n </div>\n }\n </div>\n }\n </div>\n\n <!-- 2. Result Columns Configuration -->\n <div class=\"property-group\">\n <div class=\"group-header-row\">\n <div class=\"group-title-wrap\">\n <span class=\"group-title\">Columns</span>\n <span class=\"group-badge\">{{ columns().length }}</span>\n </div>\n </div>\n\n <!-- Dedicated Full-Width Action Buttons (No cramping) -->\n <div class=\"columns-action-bar\">\n <button\n type=\"button\"\n class=\"btn-col-action btn-auto-detect\"\n (click)=\"autoDetectColumns.emit()\"\n [disabled]=\"isDetecting()\"\n title=\"Auto-detect all columns from procedure or data table\"\n >\n <rpt-icon name=\"refresh\" [size]=\"12\" [class.rpt-spin]=\"isDetecting()\" />\n <span>{{ isDetecting() ? 'Detecting...' : 'Auto-Detect' }}</span>\n </button>\n <button\n type=\"button\"\n class=\"btn-col-action btn-add-col\"\n (click)=\"addColumn.emit()\"\n title=\"Add a new column manually\"\n >\n <rpt-icon name=\"plus\" [size]=\"12\" />\n <span>+ Add Column</span>\n </button>\n </div>\n\n @if (statusMessage()) {\n <div class=\"col-status-banner\" [class.is-error]=\"statusKind() === 'error'\" [class.is-success]=\"statusKind() === 'success'\">\n <rpt-icon [name]=\"statusKind() === 'error' ? 'close' : 'check'\" [size]=\"12\" />\n <span>{{ statusMessage() }}</span>\n </div>\n }\n\n <div class=\"columns-editor-list\">\n @for (col of columns(); track col.field; let idx = $index) {\n <div class=\"col-edit-card\">\n <div class=\"col-card-header\">\n <div class=\"col-header-title-group\">\n <span class=\"col-drag-dots\">\u22EE\u22EE</span>\n <span class=\"col-idx-badge\">#{{ idx + 1 }}</span>\n <input\n type=\"text\"\n class=\"col-title-input\"\n placeholder=\"Column Label\"\n [ngModel]=\"col.label\"\n (ngModelChange)=\"onUpdateCol(idx, 'label', $event)\"\n />\n </div>\n <div class=\"col-card-actions\">\n <span class=\"col-type-pill\">{{ col.dataType || 'string' }}</span>\n <button\n type=\"button\"\n class=\"col-action-btn btn-delete\"\n (click)=\"removeColumn.emit(idx)\"\n title=\"Remove Column\"\n >\n <rpt-icon name=\"trash\" [size]=\"12\" />\n </button>\n </div>\n </div>\n\n <div class=\"col-card-body\">\n <div class=\"col-edit-fields\">\n <div class=\"col-field-item\">\n <span class=\"mini-label\">Field</span>\n <input\n type=\"text\"\n class=\"col-mini-input\"\n placeholder=\"field\"\n [ngModel]=\"col.field\"\n (ngModelChange)=\"onUpdateCol(idx, 'field', $event)\"\n />\n </div>\n <div class=\"col-field-item\">\n <span class=\"mini-label\">Type</span>\n <select\n class=\"col-mini-select\"\n [ngModel]=\"col.dataType || 'string'\"\n (ngModelChange)=\"onUpdateCol(idx, 'dataType', $event)\"\n >\n <option value=\"string\">String</option>\n <option value=\"number\">Number</option>\n <option value=\"currency\">Currency</option>\n <option value=\"date\">Date</option>\n <option value=\"boolean\">Boolean</option>\n </select>\n </div>\n <div class=\"col-field-item\">\n <span class=\"mini-label\">Format</span>\n <input\n type=\"text\"\n class=\"col-mini-input\"\n placeholder=\"e.g. dd/MM/yyyy\"\n [ngModel]=\"col.format\"\n (ngModelChange)=\"onUpdateCol(idx, 'format', $event)\"\n />\n </div>\n <div class=\"col-field-item\">\n <span class=\"mini-label\">Align</span>\n <select\n class=\"col-mini-select\"\n [ngModel]=\"col.align || 'left'\"\n (ngModelChange)=\"onUpdateCol(idx, 'align', $event)\"\n >\n <option value=\"left\">Left</option>\n <option value=\"center\">Center</option>\n <option value=\"right\">Right</option>\n </select>\n </div>\n <div class=\"col-field-item\">\n <span class=\"mini-label\">Aggregate</span>\n <select\n class=\"col-mini-select\"\n [ngModel]=\"col.aggregate || 'none'\"\n (ngModelChange)=\"onUpdateCol(idx, 'aggregate', $event === 'none' ? undefined : $event)\"\n >\n <option value=\"none\">None</option>\n <option value=\"sum\">Sum</option>\n <option value=\"avg\">Average</option>\n <option value=\"count\">Count</option>\n <option value=\"min\">Min</option>\n <option value=\"max\">Max</option>\n </select>\n </div>\n <div class=\"col-field-item\">\n <span class=\"mini-label\">Width</span>\n <input\n type=\"text\"\n class=\"col-mini-input\"\n placeholder=\"auto\"\n [ngModel]=\"col.width || ''\"\n (ngModelChange)=\"onUpdateCol(idx, 'width', $event || undefined)\"\n />\n </div>\n </div>\n\n <div class=\"col-card-footer\">\n <label class=\"col-checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"col.visible !== false\"\n (ngModelChange)=\"onUpdateCol(idx, 'visible', $event)\"\n />\n <span>Visible</span>\n </label>\n <label class=\"col-checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"col.checkboxSelection ?? false\"\n (ngModelChange)=\"onUpdateCol(idx, 'checkboxSelection', $event)\"\n />\n <span>Checkbox</span>\n </label>\n </div>\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Grid Table Options Group -->\n <div class=\"property-group\">\n <div class=\"group-title\">Table Options</div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.sideBar !== false\"\n (ngModelChange)=\"updateOption.emit({ field: 'sideBar', value: $event })\"\n />\n <span>Enable Side Panel (Tool Panels)</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.globalSearch !== false\"\n (ngModelChange)=\"updateOption.emit({ field: 'globalSearch', value: $event })\"\n />\n <span>Global Search Box</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.statusBar !== false\"\n (ngModelChange)=\"updateOption.emit({ field: 'statusBar', value: $event })\"\n />\n <span>Status Bar</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.pagination ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'pagination', value: $event })\"\n />\n <span>Enable Pagination</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.selectable ?? false\"\n (ngModelChange)=\"updateOption.emit({ field: 'selectable', value: $event })\"\n />\n <span>Row Selection</span>\n </label>\n </div>\n\n @if (grid()?.options?.selectable) {\n <div class=\"field-row\">\n <label class=\"field-label\">Selection Mode</label>\n <select\n class=\"field-input\"\n [ngModel]=\"grid()?.options?.selectionMode ?? 'multiple'\"\n (ngModelChange)=\"updateOption.emit({ field: 'selectionMode', value: $event })\"\n >\n <option value=\"multiple\">Multiple Rows</option>\n <option value=\"single\">Single Row</option>\n </select>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.checkboxSelection ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'checkboxSelection', value: $event })\"\n />\n <span>Checkbox Selection</span>\n </label>\n </div>\n\n @if ((grid()?.options?.selectionMode ?? 'multiple') === 'multiple') {\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.headerCheckboxSelection ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'headerCheckboxSelection', value: $event })\"\n />\n <span>Header Select All Checkbox</span>\n </label>\n </div>\n }\n }\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.allowColumnReorder ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'allowColumnReorder', value: $event })\"\n />\n <span>Allow Column Reorder</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.allowExport ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'allowExport', value: $event })\"\n />\n <span>Allow Export</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"field-label\">Page Size</label>\n <input\n type=\"number\"\n class=\"field-input\"\n min=\"5\"\n max=\"100\"\n [ngModel]=\"grid()?.options?.pageSize ?? 10\"\n (ngModelChange)=\"updateOption.emit({ field: 'pageSize', value: +$event })\"\n />\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.showRowNumbers ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'showRowNumbers', value: $event })\"\n />\n <span>Show Row Numbers</span>\n </label>\n </div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"grid()?.options?.showFooter ?? true\"\n (ngModelChange)=\"updateOption.emit({ field: 'showFooter', value: $event })\"\n />\n <span>Show Footer Totals</span>\n </label>\n </div>\n </div>\n\n <!-- Row Click & Drill-Down Action Group -->\n <div class=\"property-group\">\n <div class=\"group-title\">Row Click & Drill-Down</div>\n\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"hasRowClickAction()\"\n (ngModelChange)=\"toggleRowClickAction($event)\"\n />\n <span>Enable Row Click Action</span>\n </label>\n </div>\n\n @if (hasRowClickAction()) {\n <div class=\"field-row\">\n <label class=\"field-label\">Action Type</label>\n <select\n class=\"field-input field-select\"\n [ngModel]=\"rowClickAction()?.type || 'open-drawer'\"\n (ngModelChange)=\"updateRowActionField('type', $event)\"\n >\n <option value=\"open-drawer\">Slide-Over Detail Drawer</option>\n <option value=\"drilldown-report\">Drill-Down Report (Host Event)</option>\n <option value=\"navigate\">Navigate to URL</option>\n <option value=\"emit-custom\">Emit Custom Action</option>\n </select>\n </div>\n\n @if (rowClickAction()?.type === 'open-drawer' || rowClickAction()?.type === 'drilldown-report') {\n <div class=\"field-row\">\n <label class=\"field-label\">Target Report ID</label>\n <input\n type=\"text\"\n class=\"field-input\"\n placeholder=\"e.g. order-details\"\n [ngModel]=\"rowClickAction()?.targetReportId || ''\"\n (ngModelChange)=\"updateRowActionField('targetReportId', $event)\"\n />\n </div>\n\n <div class=\"field-row\">\n <label class=\"field-label\">Drawer Title</label>\n <input\n type=\"text\"\n class=\"field-input\"\n placeholder=\"e.g. Order Details\"\n [ngModel]=\"rowClickAction()?.drawerTitle || ''\"\n (ngModelChange)=\"updateRowActionField('drawerTitle', $event)\"\n />\n </div>\n\n <div class=\"field-row\">\n <label class=\"field-label\">Drawer Width</label>\n <input\n type=\"text\"\n class=\"field-input\"\n placeholder=\"e.g. 760px, 80%, 900px\"\n [ngModel]=\"rowClickAction()?.drawerWidth || '760px'\"\n (ngModelChange)=\"updateRowActionField('drawerWidth', $event)\"\n />\n </div>\n }\n\n @if (rowClickAction()?.type === 'navigate') {\n <div class=\"field-row\">\n <label class=\"field-label\">Target URL</label>\n <input\n type=\"text\"\n class=\"field-input\"\n [placeholder]=\"'/orders/{{row.orderId}} or https://...'\"\n [ngModel]=\"rowClickAction()?.targetUrl || ''\"\n (ngModelChange)=\"updateRowActionField('targetUrl', $event)\"\n />\n </div>\n <div class=\"field-row\">\n <label class=\"checkbox-toggle\">\n <input\n type=\"checkbox\"\n [ngModel]=\"rowClickAction()?.openNewTab ?? false\"\n (ngModelChange)=\"updateRowActionField('openNewTab', $event)\"\n />\n <span>Open in New Tab</span>\n </label>\n </div>\n }\n\n @if (rowClickAction()?.type === 'emit-custom') {\n <div class=\"field-row\">\n <label class=\"field-label\">Custom Action Name</label>\n <input\n type=\"text\"\n class=\"field-input\"\n placeholder=\"e.g. view-audit-trail\"\n [ngModel]=\"rowClickAction()?.customActionName || ''\"\n (ngModelChange)=\"updateRowActionField('customActionName', $event)\"\n />\n </div>\n }\n\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">Parameter Mapping</label>\n <button\n type=\"button\"\n class=\"btn-add-mapping\"\n (click)=\"addParameterMapping()\"\n >\n + Add Mapping\n </button>\n </div>\n\n @for (mapping of parameterMappings(); track mapping.key; let mIdx = $index) {\n <div class=\"mapping-pair-row\">\n <input\n type=\"text\"\n class=\"col-mini-input\"\n style=\"flex: 1;\"\n placeholder=\"Target (e.g. customerId)\"\n [ngModel]=\"mapping.key\"\n (ngModelChange)=\"updateMappingKey(mIdx, $event)\"\n />\n <span style=\"font-size: 0.75rem; color: #94a3b8;\">\u2190</span>\n <input\n type=\"text\"\n class=\"col-mini-input\"\n style=\"flex: 1;\"\n placeholder=\"Source (e.g. row.id)\"\n [ngModel]=\"mapping.value\"\n (ngModelChange)=\"updateMappingValue(mIdx, $event)\"\n />\n <button\n type=\"button\"\n class=\"col-action-btn btn-delete\"\n (click)=\"removeMapping(mIdx)\"\n title=\"Remove mapping\"\n >\n <rpt-icon name=\"trash\" [size]=\"11\" />\n </button>\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Remove Grid Action -->\n <div class=\"grid-danger-zone\">\n <button type=\"button\" class=\"btn-remove-grid-action\" (click)=\"removeGrid.emit()\">\n <rpt-icon name=\"trash\" [size]=\"13\" />\n <span>Remove Grid from Report</span>\n </button>\n </div>\n </div>\n }", styles: [".property-group{display:flex;flex-direction:column;gap:12px}.group-title{font-size:.7rem;font-weight:700;color:var(--rpt-color-text-muted, #64748b);text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--rpt-color-border, #e2e8f0);padding-bottom:4px}.field-row{display:flex;flex-direction:column;gap:4px}.field-label-row{display:flex;align-items:center;justify-content:space-between}.field-badge-val{font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border:1px solid #bfdbfe;padding:1px 6px;border-radius:9999px}.field-label{font-size:.7rem;font-weight:600;color:var(--rpt-color-text, #0f172a)}.field-input,.field-select,.field-textarea{width:100%;box-sizing:border-box;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius-sm, 4px);padding:6px 10px;font-size:var(--rpt-font-size-sm, .75rem);color:var(--rpt-color-text, #0f172a);background:#fff;outline:none;transition:all var(--rpt-transition-speed, .15s) ease}.field-select{appearance:none;-webkit-appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right 10px center;background-size:13px 13px;padding-right:28px;cursor:pointer}.field-input:focus,.field-select:focus,.field-textarea:focus{border-color:var(--rpt-color-primary, #2563eb);box-shadow:var(--rpt-box-shadow-focus, 0 0 0 3px rgba(37, 99, 235, .25))}.checkbox-toggle{display:inline-flex;align-items:center;gap:8px;font-size:.75rem;font-weight:500;color:var(--rpt-color-text, #0f172a);cursor:pointer}.preset-pill-group{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.preset-pill{background:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;padding:2px 8px;font-size:.65rem;font-weight:600;color:#475569;cursor:pointer;transition:all .12s ease}.preset-pill:hover{background:#e2e8f0;color:#0f172a;border-color:#cbd5e1}.preset-pill-active{background:var(--rpt-color-primary, #2563eb)!important;color:#fff!important;border-color:var(--rpt-color-primary, #2563eb)!important}.preset-pill-reset{color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border-color:#bfdbfe}.preset-pill-reset:hover{background:#dbeafe;color:#1d4ed8;border-color:#93c5fd}\n", ".empty-grid-card{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 16px;text-align:center;border:2px dashed #cbd5e1;border-radius:8px;background:#fff;margin:10px 0}.empty-grid-icon{width:48px;height:48px;border-radius:10px;background:#eff6ff;color:#2563eb;display:flex;align-items:center;justify-content:center;margin-bottom:12px}.empty-grid-title{font-size:.95rem;font-weight:700;color:#0f172a;margin:0 0 6px}.empty-grid-desc{font-size:.775rem;color:#64748b;margin:0 0 18px;line-height:1.4;max-width:280px}.btn-create-grid-action{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;background:#2563eb;color:#fff;font-size:.775rem;font-weight:600;border:none;border-radius:6px;cursor:pointer;box-shadow:0 2px 4px #2563eb33;transition:all .15s ease}.btn-create-grid-action:hover{background:#1d4ed8;box-shadow:0 4px 8px #2563eb40}.grid-danger-zone{margin-top:10px;padding-top:16px;border-top:1px dashed #e2e8f0;display:flex;justify-content:center}.btn-remove-grid-action{display:inline-flex;align-items:center;gap:6px;background:#fee2e2;border:1px solid #fecaca;color:#dc2626;font-size:.75rem;font-weight:600;padding:6px 12px;border-radius:6px;cursor:pointer;transition:all .15s ease}.btn-remove-grid-action:hover{background:#fecaca;border-color:#f87171;color:#b91c1c}.props-form{display:flex;flex-direction:column;gap:20px}.group-header-row{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--rpt-color-border, #e2e8f0);padding-bottom:6px;margin-bottom:8px}.group-title-wrap{display:flex;align-items:center;gap:6px}.group-badge{font-size:.65rem;font-weight:700;color:#64748b;background:#f1f5f9;border:1px solid #e2e8f0;border-radius:10px;padding:1px 6px}.columns-action-bar{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px}.btn-col-action{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:7px 8px;font-size:.725rem;font-weight:600;border-radius:6px;cursor:pointer;white-space:nowrap;transition:all .12s ease;box-sizing:border-box}.btn-col-action:disabled{opacity:.6;cursor:not-allowed}.btn-auto-detect{background:#eff6ff;border:1px solid #bfdbfe;color:#2563eb}.btn-auto-detect:hover:not(:disabled){background:#2563eb;color:#fff;border-color:#2563eb}.btn-add-col{background:#fff;border:1px solid var(--rpt-color-border, #cbd5e1);color:#334155}.btn-add-col:hover{background:#f8fafc;border-color:#94a3b8;color:#0f172a}.col-status-banner{display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:6px;font-size:.72rem;line-height:1.35;margin-bottom:10px;background:#f1f5f9;color:#334155;border:1px solid #cbd5e1}.col-status-banner.is-success{background:#f0fdf4;color:#166534;border-color:#bbf7d0}.col-status-banner.is-error{background:#fef2f2;color:#991b1b;border-color:#fecaca}@keyframes rpt-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.rpt-spin{animation:rpt-spin 1s linear infinite}.columns-editor-list{display:flex;flex-direction:column;gap:10px}.col-edit-card{background:#fff;border:1.5px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius, 6px);box-shadow:var(--rpt-box-shadow, 0 1px 2px rgba(0, 0, 0, .05));box-sizing:border-box;transition:all var(--rpt-transition-speed, .15s) ease;position:relative;overflow:hidden;display:flex;flex-direction:column}.col-edit-card:hover{border-color:var(--rpt-color-primary, #2563eb);box-shadow:0 4px 6px -1px #2563eb1a}.col-card-header{display:flex;align-items:center;justify-content:space-between;gap:6px;padding:5px 8px;background:var(--rpt-color-surface, #f8fafc);border-bottom:1px solid var(--rpt-color-border, #e2e8f0);min-height:32px;box-sizing:border-box}.col-header-title-group{display:flex;align-items:center;gap:5px;min-width:0;flex:1}.col-drag-dots{color:#94a3b8;font-size:.8rem;cursor:grab;-webkit-user-select:none;user-select:none;line-height:1;padding-left:2px}.col-idx-badge{font-size:.65rem;font-weight:700;color:#64748b;background:#e2e8f0;padding:1px 5px;border-radius:3px;letter-spacing:.02em}.col-title-input{flex:1;border:1px solid transparent;background:transparent;font-size:.75rem;font-weight:600;color:var(--rpt-color-text, #0f172a);padding:2px 6px;border-radius:4px;outline:none;transition:all .15s ease}.col-title-input:hover{border-color:#cbd5e1;background:#fff}.col-title-input:focus{border-color:var(--rpt-color-primary, #2563eb);background:#fff;box-shadow:0 0 0 2px #2563eb26}.col-card-actions{display:flex;align-items:center;gap:4px}.col-type-pill{font-size:.625rem;font-weight:700;color:#1e40af;background:#dbeafe;border:1px solid #bfdbfe;padding:1px 5px;border-radius:3px;text-transform:uppercase;letter-spacing:.04em}.col-action-btn{display:inline-flex;align-items:center;justify-content:center;background:none;border:none;color:var(--rpt-color-text-muted, #64748b);cursor:pointer;padding:3px;border-radius:4px;transition:all .15s ease}.col-action-btn.btn-delete:hover{color:var(--rpt-color-danger, #ef4444);background:#fee2e2}.col-card-body{padding:8px 10px;display:flex;flex-direction:column;gap:6px;background:#fff}.col-edit-fields{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px 8px}.col-field-item{display:flex;flex-direction:column;gap:2px;min-width:0}.mini-label{font-size:.625rem;font-weight:600;color:#64748b;letter-spacing:.02em}.col-mini-input,.col-mini-select{width:100%;box-sizing:border-box;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:3px;padding:3px 5px;font-size:.7rem;color:var(--rpt-color-text, #0f172a);background:#fff;outline:none;transition:all .12s ease}.col-mini-select{appearance:none;-webkit-appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right 4px center;background-size:10px 10px;padding-right:16px;cursor:pointer}.col-mini-input:focus,.col-mini-select:focus{border-color:var(--rpt-color-primary, #2563eb);box-shadow:0 0 0 2px #2563eb26}.col-card-footer{display:flex;align-items:center;justify-content:flex-end;gap:12px;padding-top:4px;border-top:1px dashed #e2e8f0;margin-top:2px}.col-checkbox-toggle{display:inline-flex;align-items:center;gap:5px;font-size:.675rem;font-weight:500;color:#475569;cursor:pointer;-webkit-user-select:none;user-select:none}.btn-add-mapping{display:inline-flex;align-items:center;background:#eff6ff;border:1px solid #bfdbfe;color:#2563eb;font-size:.65rem;font-weight:600;padding:2px 6px;border-radius:4px;cursor:pointer;transition:all .12s ease}.btn-add-mapping:hover{background:#dbeafe}.mapping-pair-row{display:flex;align-items:center;gap:6px;margin-top:4px}.active-ds-badge{background:#eff6ff;color:#2563eb;font-size:.65rem;font-weight:700;padding:1px 6px;border-radius:4px;border:1px solid #bfdbfe;max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.source-type-switcher{display:flex;background:#f1f5f9;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:6px;padding:2px;gap:2px;margin-bottom:12px}.source-tab-btn{flex:1;display:inline-flex;align-items:center;justify-content:center;gap:5px;padding:6px 4px;font-size:.725rem;font-weight:600;color:var(--rpt-color-text-muted, #64748b);background:transparent;border:none;border-radius:4px;cursor:pointer;transition:all .12s ease}.source-tab-btn:hover{color:var(--rpt-color-text, #0f172a)}.source-tab-btn.active{background:#fff;color:var(--rpt-color-primary, #2563eb);box-shadow:0 1px 2px #0000000f}.procedure-params-section{margin-top:14px;padding:0;background:transparent;border:none}.params-section-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;padding-bottom:4px;border-bottom:1px solid var(--rpt-color-border, #e2e8f0)}.params-header-left{display:flex;align-items:center;gap:6px}.params-title{font-size:.725rem;font-weight:700;color:#334155;text-transform:uppercase;letter-spacing:.04em}.params-count-badge{font-size:.625rem;font-weight:700;color:#2563eb;background:#eff6ff;border:1px solid #bfdbfe;border-radius:9999px;padding:0 6px;line-height:16px}.param-cards-list{display:flex;flex-direction:column;gap:10px;width:100%;box-sizing:border-box}.empty-params-hint{font-size:.725rem;color:#64748b;font-style:italic;padding:12px;text-align:center;background:#f8fafc;border:1px dashed #cbd5e1;border-radius:6px}\n"] }]
|
|
11294
|
+
}], ctorParameters: () => [] });
|
|
11295
|
+
|
|
11296
|
+
class PropertyEditorReportComponent {
|
|
11297
|
+
definition = input.required();
|
|
11298
|
+
updateReport = output();
|
|
11299
|
+
updateLayout = output();
|
|
11300
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PropertyEditorReportComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
11301
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "18.2.14", type: PropertyEditorReportComponent, isStandalone: true, selector: "rpt-property-editor-report", inputs: { definition: { classPropertyName: "definition", publicName: "definition", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { updateReport: "updateReport", updateLayout: "updateLayout" }, ngImport: i0, template: `
|
|
11302
|
+
<div class="props-form">
|
|
11303
|
+
<div class="property-group">
|
|
11304
|
+
<div class="group-title">Report Definition</div>
|
|
11305
|
+
|
|
11306
|
+
<div class="field-row">
|
|
11307
|
+
<label class="field-label">Report Name</label>
|
|
11308
|
+
<input
|
|
11309
|
+
type="text"
|
|
11310
|
+
class="field-input"
|
|
11311
|
+
[ngModel]="definition().name"
|
|
11312
|
+
(ngModelChange)="updateReport.emit({ field: 'name', value: $event })"
|
|
11313
|
+
/>
|
|
10491
11314
|
</div>
|
|
10492
11315
|
|
|
10493
|
-
|
|
10494
|
-
|
|
10495
|
-
<
|
|
10496
|
-
|
|
10497
|
-
|
|
10498
|
-
|
|
10499
|
-
|
|
10500
|
-
|
|
10501
|
-
|
|
10502
|
-
title="Auto-bound to host session context"
|
|
10503
|
-
>
|
|
10504
|
-
<rpt-icon name="user" [size]="10" />
|
|
10505
|
-
<span>Session</span>
|
|
10506
|
-
</button>
|
|
10507
|
-
}
|
|
10508
|
-
<button
|
|
10509
|
-
type="button"
|
|
10510
|
-
class="mode-btn"
|
|
10511
|
-
[class.active]="activeMode() === 'control'"
|
|
10512
|
-
(click)="setMode('control')"
|
|
10513
|
-
title="Link value to another field on canvas (e.g. Branch dropdown)"
|
|
10514
|
-
>
|
|
10515
|
-
<rpt-icon name="link" [size]="10" />
|
|
10516
|
-
<span>Form Field</span>
|
|
10517
|
-
</button>
|
|
10518
|
-
<button
|
|
10519
|
-
type="button"
|
|
10520
|
-
class="mode-btn"
|
|
10521
|
-
[class.active]="activeMode() === 'fixed'"
|
|
10522
|
-
(click)="setMode('fixed')"
|
|
10523
|
-
title="Use a static fixed value"
|
|
10524
|
-
>
|
|
10525
|
-
<rpt-icon name="edit" [size]="10" />
|
|
10526
|
-
<span>Fixed</span>
|
|
10527
|
-
</button>
|
|
10528
|
-
<button
|
|
10529
|
-
type="button"
|
|
10530
|
-
class="mode-btn"
|
|
10531
|
-
[class.active]="activeMode() === 'context'"
|
|
10532
|
-
(click)="setMode('context')"
|
|
10533
|
-
title="Use system context presets (e.g. Today, Month Start)"
|
|
10534
|
-
>
|
|
10535
|
-
<rpt-icon name="clock" [size]="10" />
|
|
10536
|
-
<span>Preset</span>
|
|
10537
|
-
</button>
|
|
10538
|
-
@if (isAdvancedMode()) {
|
|
10539
|
-
<button
|
|
10540
|
-
type="button"
|
|
10541
|
-
class="mode-btn"
|
|
10542
|
-
[class.active]="activeMode() === 'custom'"
|
|
10543
|
-
(click)="setMode('custom')"
|
|
10544
|
-
title="Manual expression with {{ '{{' }}mustache{{ '}}' }}"
|
|
10545
|
-
>
|
|
10546
|
-
<rpt-icon name="code" [size]="10" />
|
|
10547
|
-
<span>Expr</span>
|
|
10548
|
-
</button>
|
|
10549
|
-
}
|
|
10550
|
-
</div>
|
|
10551
|
-
}
|
|
11316
|
+
<div class="field-row">
|
|
11317
|
+
<label class="field-label">Description</label>
|
|
11318
|
+
<textarea
|
|
11319
|
+
class="field-textarea"
|
|
11320
|
+
rows="3"
|
|
11321
|
+
[ngModel]="definition().description"
|
|
11322
|
+
(ngModelChange)="updateReport.emit({ field: 'description', value: $event })"
|
|
11323
|
+
></textarea>
|
|
11324
|
+
</div>
|
|
10552
11325
|
</div>
|
|
10553
11326
|
|
|
10554
|
-
|
|
10555
|
-
|
|
10556
|
-
<!-- 0. AUTO SESSION DERIVED (Simple Mode default or Session mode) -->
|
|
10557
|
-
@if (activeMode() === 'session' && sessionMatch()) {
|
|
10558
|
-
<div class="session-box">
|
|
10559
|
-
<div class="session-box-text">
|
|
10560
|
-
<rpt-icon name="user" [size]="12" class="session-icon" />
|
|
10561
|
-
<span>
|
|
10562
|
-
Derived from <strong>{{ sessionMatch()?.sessionKey }}</strong> ({{ sessionMatch()?.friendlyLabel }})
|
|
10563
|
-
</span>
|
|
10564
|
-
</div>
|
|
10565
|
-
<div class="session-val-tag">
|
|
10566
|
-
Current: <strong>{{ sessionMatch()?.value ?? '(none)' }}</strong>
|
|
10567
|
-
</div>
|
|
10568
|
-
</div>
|
|
10569
|
-
}
|
|
11327
|
+
<div class="property-group">
|
|
11328
|
+
<div class="group-title">Page Grid Layout</div>
|
|
10570
11329
|
|
|
10571
|
-
|
|
10572
|
-
|
|
10573
|
-
<
|
|
10574
|
-
|
|
10575
|
-
|
|
10576
|
-
|
|
10577
|
-
|
|
10578
|
-
|
|
10579
|
-
>
|
|
10580
|
-
|
|
10581
|
-
|
|
10582
|
-
|
|
10583
|
-
{{ field.label || field.key }} ({{ field.type }})
|
|
10584
|
-
</option>
|
|
10585
|
-
}
|
|
10586
|
-
</select>
|
|
10587
|
-
</div>
|
|
10588
|
-
@if (!selectedControlKey() && compatibleControls().length === 0) {
|
|
10589
|
-
<div class="empty-hint">No other controls on canvas. Add a dropdown or input first.</div>
|
|
10590
|
-
}
|
|
10591
|
-
}
|
|
11330
|
+
<div class="field-row">
|
|
11331
|
+
<label class="field-label">Total Columns</label>
|
|
11332
|
+
<select
|
|
11333
|
+
class="field-select"
|
|
11334
|
+
[ngModel]="definition().layout.columns"
|
|
11335
|
+
(ngModelChange)="updateLayout.emit({ field: 'columns', value: +$event })"
|
|
11336
|
+
>
|
|
11337
|
+
<option [value]="12">12 Columns (Standard)</option>
|
|
11338
|
+
<option [value]="16">16 Columns (Dense)</option>
|
|
11339
|
+
<option [value]="24">24 Columns (Ultra-wide)</option>
|
|
11340
|
+
</select>
|
|
11341
|
+
</div>
|
|
10592
11342
|
|
|
10593
|
-
|
|
10594
|
-
|
|
10595
|
-
|
|
10596
|
-
|
|
10597
|
-
|
|
10598
|
-
|
|
10599
|
-
|
|
10600
|
-
|
|
10601
|
-
|
|
10602
|
-
|
|
10603
|
-
|
|
10604
|
-
</label>
|
|
10605
|
-
}
|
|
10606
|
-
@case ('number') {
|
|
10607
|
-
<input
|
|
10608
|
-
type="number"
|
|
10609
|
-
class="field-input"
|
|
10610
|
-
placeholder="Enter number (e.g. 100)"
|
|
10611
|
-
[ngModel]="currentValue()"
|
|
10612
|
-
(ngModelChange)="onFixedValueChanged($event)"
|
|
10613
|
-
/>
|
|
10614
|
-
}
|
|
10615
|
-
@case ('date') {
|
|
10616
|
-
<input
|
|
10617
|
-
type="date"
|
|
10618
|
-
class="field-input"
|
|
10619
|
-
[ngModel]="currentValue()"
|
|
10620
|
-
(ngModelChange)="onFixedValueChanged($event)"
|
|
10621
|
-
/>
|
|
10622
|
-
}
|
|
10623
|
-
@default {
|
|
10624
|
-
<input
|
|
10625
|
-
type="text"
|
|
10626
|
-
class="field-input"
|
|
10627
|
-
placeholder="Enter fixed value"
|
|
10628
|
-
[ngModel]="currentValue()"
|
|
10629
|
-
(ngModelChange)="onFixedValueChanged($event)"
|
|
10630
|
-
/>
|
|
10631
|
-
}
|
|
10632
|
-
}
|
|
10633
|
-
}
|
|
11343
|
+
<div class="field-row">
|
|
11344
|
+
<label class="field-label">Cell Gap (px)</label>
|
|
11345
|
+
<input
|
|
11346
|
+
type="number"
|
|
11347
|
+
class="field-input"
|
|
11348
|
+
min="0"
|
|
11349
|
+
max="64"
|
|
11350
|
+
[ngModel]="definition().layout.gap ?? 16"
|
|
11351
|
+
(ngModelChange)="updateLayout.emit({ field: 'gap', value: +$event })"
|
|
11352
|
+
/>
|
|
11353
|
+
</div>
|
|
10634
11354
|
|
|
10635
|
-
|
|
10636
|
-
|
|
11355
|
+
<div class="field-row">
|
|
11356
|
+
<label class="field-label">Padding (px)</label>
|
|
11357
|
+
<input
|
|
11358
|
+
type="number"
|
|
11359
|
+
class="field-input"
|
|
11360
|
+
min="0"
|
|
11361
|
+
max="64"
|
|
11362
|
+
[ngModel]="definition().layout.padding ?? 24"
|
|
11363
|
+
(ngModelChange)="updateLayout.emit({ field: 'padding', value: +$event })"
|
|
11364
|
+
/>
|
|
11365
|
+
</div>
|
|
11366
|
+
|
|
11367
|
+
<div class="field-row">
|
|
11368
|
+
<label class="field-label">Direction</label>
|
|
10637
11369
|
<select
|
|
10638
11370
|
class="field-select"
|
|
10639
|
-
[ngModel]="
|
|
10640
|
-
(ngModelChange)="
|
|
11371
|
+
[ngModel]="definition().layout.direction ?? 'ltr'"
|
|
11372
|
+
(ngModelChange)="updateLayout.emit({ field: 'direction', value: $event })"
|
|
10641
11373
|
>
|
|
10642
|
-
<option value=""
|
|
10643
|
-
<option value="
|
|
10644
|
-
<option value="{{ '{{' }}context.userId{{ '}}' }}">Current User ID</option>
|
|
10645
|
-
<option value="{{ '{{' }}context.tenantId{{ '}}' }}">Current Company / Tenant</option>
|
|
10646
|
-
<option value="{{ '{{' }}context.today{{ '}}' }}">Today's Date</option>
|
|
10647
|
-
<option value="{{ '{{' }}context.startOfMonth{{ '}}' }}">First Day of Current Month</option>
|
|
11374
|
+
<option value="ltr">Left to Right (LTR)</option>
|
|
11375
|
+
<option value="rtl">Right to Left (RTL)</option>
|
|
10648
11376
|
</select>
|
|
10649
|
-
|
|
11377
|
+
</div>
|
|
11378
|
+
</div>
|
|
11379
|
+
</div>
|
|
11380
|
+
`, isInline: true, styles: [".property-group{display:flex;flex-direction:column;gap:12px}.group-title{font-size:.7rem;font-weight:700;color:var(--rpt-color-text-muted, #64748b);text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--rpt-color-border, #e2e8f0);padding-bottom:4px}.field-row{display:flex;flex-direction:column;gap:4px}.field-label-row{display:flex;align-items:center;justify-content:space-between}.field-badge-val{font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border:1px solid #bfdbfe;padding:1px 6px;border-radius:9999px}.field-label{font-size:.7rem;font-weight:600;color:var(--rpt-color-text, #0f172a)}.field-input,.field-select,.field-textarea{width:100%;box-sizing:border-box;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius-sm, 4px);padding:6px 10px;font-size:var(--rpt-font-size-sm, .75rem);color:var(--rpt-color-text, #0f172a);background:#fff;outline:none;transition:all var(--rpt-transition-speed, .15s) ease}.field-select{appearance:none;-webkit-appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right 10px center;background-size:13px 13px;padding-right:28px;cursor:pointer}.field-input:focus,.field-select:focus,.field-textarea:focus{border-color:var(--rpt-color-primary, #2563eb);box-shadow:var(--rpt-box-shadow-focus, 0 0 0 3px rgba(37, 99, 235, .25))}.checkbox-toggle{display:inline-flex;align-items:center;gap:8px;font-size:.75rem;font-weight:500;color:var(--rpt-color-text, #0f172a);cursor:pointer}.preset-pill-group{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.preset-pill{background:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;padding:2px 8px;font-size:.65rem;font-weight:600;color:#475569;cursor:pointer;transition:all .12s ease}.preset-pill:hover{background:#e2e8f0;color:#0f172a;border-color:#cbd5e1}.preset-pill-active{background:var(--rpt-color-primary, #2563eb)!important;color:#fff!important;border-color:var(--rpt-color-primary, #2563eb)!important}.preset-pill-reset{color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border-color:#bfdbfe}.preset-pill-reset:hover{background:#dbeafe;color:#1d4ed8;border-color:#93c5fd}\n", ".props-form{display:flex;flex-direction:column;gap:20px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.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: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
11381
|
+
}
|
|
11382
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PropertyEditorReportComponent, decorators: [{
|
|
11383
|
+
type: Component,
|
|
11384
|
+
args: [{ selector: 'rpt-property-editor-report', standalone: true, imports: [CommonModule, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
11385
|
+
<div class="props-form">
|
|
11386
|
+
<div class="property-group">
|
|
11387
|
+
<div class="group-title">Report Definition</div>
|
|
10650
11388
|
|
|
10651
|
-
|
|
10652
|
-
|
|
11389
|
+
<div class="field-row">
|
|
11390
|
+
<label class="field-label">Report Name</label>
|
|
10653
11391
|
<input
|
|
10654
11392
|
type="text"
|
|
10655
|
-
class="field-input
|
|
10656
|
-
|
|
10657
|
-
|
|
10658
|
-
(ngModelChange)="onFixedValueChanged($event)"
|
|
11393
|
+
class="field-input"
|
|
11394
|
+
[ngModel]="definition().name"
|
|
11395
|
+
(ngModelChange)="updateReport.emit({ field: 'name', value: $event })"
|
|
10659
11396
|
/>
|
|
10660
|
-
}
|
|
10661
|
-
</div>
|
|
10662
|
-
|
|
10663
|
-
@if (param().description && isAdvancedMode()) {
|
|
10664
|
-
<div class="param-desc">{{ param().description }}</div>
|
|
10665
|
-
}
|
|
10666
|
-
</div>
|
|
10667
|
-
`, isInline: true, styles: [".param-card{background:#f8fafc;border:1px solid var(--rpt-color-border, #e2e8f0);border-radius:6px;padding:8px 10px;display:flex;flex-direction:column;gap:6px;transition:all .12s ease}.param-card:hover{border-color:#cbd5e1}.param-card.session-bound{background:#f0fdf4;border-color:#bbf7d0}.param-header{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap}.param-title-wrap{display:flex;align-items:center;gap:6px}.param-label{font-size:.76rem;font-weight:600;color:#1e293b}.param-type-badge{font-size:.65rem;padding:1px 5px;border-radius:4px;background:#e2e8f0;color:#475569;text-transform:capitalize}.session-auto-badge{font-size:.62rem;font-weight:600;padding:1px 5px;border-radius:4px;background:#dcfce7;color:#15803d;border:1px solid #bbf7d0}.required-warn-badge{font-size:.62rem;font-weight:600;padding:1px 5px;border-radius:4px;background:#fef2f2;color:#b91c1c;border:1px solid #fecaca}.session-box{display:flex;align-items:center;justify-content:space-between;background:#fff;border:1px solid #bbf7d0;border-radius:5px;padding:5px 8px;gap:8px}.session-box-text{display:flex;align-items:center;gap:5px;font-size:.7rem;color:#166534}.session-icon{color:#16a34a}.session-val-tag{font-size:.68rem;color:#15803d;background:#dcfce7;padding:1px 6px;border-radius:4px}.param-tech-badge{font-size:.65rem;font-family:monospace;padding:1px 5px;border-radius:4px;background:#eff6ff;color:#2563eb}.binding-mode-tabs{display:inline-flex;background:#e2e8f0;border-radius:4px;padding:2px;gap:2px}.mode-btn{display:inline-flex;align-items:center;gap:3px;padding:3px 6px;font-size:.65rem;font-weight:500;background:transparent;color:#64748b;border:none;border-radius:3px;cursor:pointer;transition:all .1s ease}.mode-btn:hover{color:#0f172a}.mode-btn.active{background:#fff;color:var(--rpt-color-primary, #2563eb);box-shadow:0 1px 2px #0000000d;font-weight:600}.param-body{display:flex;flex-direction:column;gap:4px}.linker-row{display:flex;align-items:center;gap:6px;position:relative}.linker-icon{color:#2563eb;flex-shrink:0}.linker-select{flex:1;border-color:#bfdbfe;background-color:#eff6ff;font-weight:500;color:#1e40af}.empty-hint{font-size:.68rem;color:#94a3b8;font-style:italic}.toggle-switch{display:inline-flex;align-items:center;gap:6px;cursor:pointer;font-size:.72rem;color:#334155}.monospace-input{font-family:monospace;font-size:.72rem}.param-desc{font-size:.65rem;color:#64748b}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.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: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: RptIconComponent, selector: "rpt-icon", inputs: ["name", "size", "color", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
10668
|
-
}
|
|
10669
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ParameterBindingEditorComponent, decorators: [{
|
|
10670
|
-
type: Component,
|
|
10671
|
-
args: [{ selector: 'rpt-parameter-binding-editor', standalone: true, imports: [CommonModule, FormsModule, RptIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
10672
|
-
<div class="param-card" [class.session-bound]="isAutoSessionDerived()">
|
|
10673
|
-
<div class="param-header">
|
|
10674
|
-
<div class="param-title-wrap">
|
|
10675
|
-
<span class="param-label">{{ friendlyLabel() }}</span>
|
|
10676
|
-
@if (isAdvancedMode()) {
|
|
10677
|
-
<span class="param-tech-badge">{{ param().name }} ({{ param().sqlType }})</span>
|
|
10678
|
-
} @else {
|
|
10679
|
-
<span class="param-type-badge">{{ displayType() }}</span>
|
|
10680
|
-
}
|
|
10681
|
-
@if (isAutoSessionDerived()) {
|
|
10682
|
-
<span class="session-auto-badge">Auto Session</span>
|
|
10683
|
-
} @else if (param().required && !hasValue()) {
|
|
10684
|
-
<span class="required-warn-badge">Required</span>
|
|
10685
|
-
}
|
|
10686
11397
|
</div>
|
|
10687
11398
|
|
|
10688
|
-
|
|
10689
|
-
|
|
10690
|
-
<
|
|
10691
|
-
|
|
10692
|
-
|
|
10693
|
-
|
|
10694
|
-
|
|
10695
|
-
|
|
10696
|
-
|
|
10697
|
-
title="Auto-bound to host session context"
|
|
10698
|
-
>
|
|
10699
|
-
<rpt-icon name="user" [size]="10" />
|
|
10700
|
-
<span>Session</span>
|
|
10701
|
-
</button>
|
|
10702
|
-
}
|
|
10703
|
-
<button
|
|
10704
|
-
type="button"
|
|
10705
|
-
class="mode-btn"
|
|
10706
|
-
[class.active]="activeMode() === 'control'"
|
|
10707
|
-
(click)="setMode('control')"
|
|
10708
|
-
title="Link value to another field on canvas (e.g. Branch dropdown)"
|
|
10709
|
-
>
|
|
10710
|
-
<rpt-icon name="link" [size]="10" />
|
|
10711
|
-
<span>Form Field</span>
|
|
10712
|
-
</button>
|
|
10713
|
-
<button
|
|
10714
|
-
type="button"
|
|
10715
|
-
class="mode-btn"
|
|
10716
|
-
[class.active]="activeMode() === 'fixed'"
|
|
10717
|
-
(click)="setMode('fixed')"
|
|
10718
|
-
title="Use a static fixed value"
|
|
10719
|
-
>
|
|
10720
|
-
<rpt-icon name="edit" [size]="10" />
|
|
10721
|
-
<span>Fixed</span>
|
|
10722
|
-
</button>
|
|
10723
|
-
<button
|
|
10724
|
-
type="button"
|
|
10725
|
-
class="mode-btn"
|
|
10726
|
-
[class.active]="activeMode() === 'context'"
|
|
10727
|
-
(click)="setMode('context')"
|
|
10728
|
-
title="Use system context presets (e.g. Today, Month Start)"
|
|
10729
|
-
>
|
|
10730
|
-
<rpt-icon name="clock" [size]="10" />
|
|
10731
|
-
<span>Preset</span>
|
|
10732
|
-
</button>
|
|
10733
|
-
@if (isAdvancedMode()) {
|
|
10734
|
-
<button
|
|
10735
|
-
type="button"
|
|
10736
|
-
class="mode-btn"
|
|
10737
|
-
[class.active]="activeMode() === 'custom'"
|
|
10738
|
-
(click)="setMode('custom')"
|
|
10739
|
-
title="Manual expression with {{ '{{' }}mustache{{ '}}' }}"
|
|
10740
|
-
>
|
|
10741
|
-
<rpt-icon name="code" [size]="10" />
|
|
10742
|
-
<span>Expr</span>
|
|
10743
|
-
</button>
|
|
10744
|
-
}
|
|
10745
|
-
</div>
|
|
10746
|
-
}
|
|
11399
|
+
<div class="field-row">
|
|
11400
|
+
<label class="field-label">Description</label>
|
|
11401
|
+
<textarea
|
|
11402
|
+
class="field-textarea"
|
|
11403
|
+
rows="3"
|
|
11404
|
+
[ngModel]="definition().description"
|
|
11405
|
+
(ngModelChange)="updateReport.emit({ field: 'description', value: $event })"
|
|
11406
|
+
></textarea>
|
|
11407
|
+
</div>
|
|
10747
11408
|
</div>
|
|
10748
11409
|
|
|
10749
|
-
|
|
10750
|
-
|
|
10751
|
-
<!-- 0. AUTO SESSION DERIVED (Simple Mode default or Session mode) -->
|
|
10752
|
-
@if (activeMode() === 'session' && sessionMatch()) {
|
|
10753
|
-
<div class="session-box">
|
|
10754
|
-
<div class="session-box-text">
|
|
10755
|
-
<rpt-icon name="user" [size]="12" class="session-icon" />
|
|
10756
|
-
<span>
|
|
10757
|
-
Derived from <strong>{{ sessionMatch()?.sessionKey }}</strong> ({{ sessionMatch()?.friendlyLabel }})
|
|
10758
|
-
</span>
|
|
10759
|
-
</div>
|
|
10760
|
-
<div class="session-val-tag">
|
|
10761
|
-
Current: <strong>{{ sessionMatch()?.value ?? '(none)' }}</strong>
|
|
10762
|
-
</div>
|
|
10763
|
-
</div>
|
|
10764
|
-
}
|
|
10765
|
-
|
|
10766
|
-
<!-- 1. FORM CONTROL LINKER -->
|
|
10767
|
-
@if (activeMode() === 'control') {
|
|
10768
|
-
<div class="linker-row">
|
|
10769
|
-
<rpt-icon name="link" [size]="12" class="linker-icon" />
|
|
10770
|
-
<select
|
|
10771
|
-
class="field-select linker-select"
|
|
10772
|
-
[ngModel]="selectedControlKey()"
|
|
10773
|
-
(ngModelChange)="onControlSelected($event)"
|
|
10774
|
-
>
|
|
10775
|
-
<option value="">-- Choose Form Field on Canvas --</option>
|
|
10776
|
-
@for (field of compatibleControls(); track field.key || field.id) {
|
|
10777
|
-
<option [value]="field.key || field.id">
|
|
10778
|
-
{{ field.label || field.key }} ({{ field.type }})
|
|
10779
|
-
</option>
|
|
10780
|
-
}
|
|
10781
|
-
</select>
|
|
10782
|
-
</div>
|
|
10783
|
-
@if (!selectedControlKey() && compatibleControls().length === 0) {
|
|
10784
|
-
<div class="empty-hint">No other controls on canvas. Add a dropdown or input first.</div>
|
|
10785
|
-
}
|
|
10786
|
-
}
|
|
10787
|
-
|
|
10788
|
-
<!-- 2. FIXED VALUE -->
|
|
10789
|
-
@if (activeMode() === 'fixed') {
|
|
10790
|
-
@switch (param().dataType) {
|
|
10791
|
-
@case ('boolean') {
|
|
10792
|
-
<label class="toggle-switch">
|
|
10793
|
-
<input
|
|
10794
|
-
type="checkbox"
|
|
10795
|
-
[ngModel]="booleanFixedValue()"
|
|
10796
|
-
(ngModelChange)="onBooleanChanged($event)"
|
|
10797
|
-
/>
|
|
10798
|
-
<span class="toggle-text">{{ booleanFixedValue() ? 'Active / Yes' : 'Inactive / No' }}</span>
|
|
10799
|
-
</label>
|
|
10800
|
-
}
|
|
10801
|
-
@case ('number') {
|
|
10802
|
-
<input
|
|
10803
|
-
type="number"
|
|
10804
|
-
class="field-input"
|
|
10805
|
-
placeholder="Enter number (e.g. 100)"
|
|
10806
|
-
[ngModel]="currentValue()"
|
|
10807
|
-
(ngModelChange)="onFixedValueChanged($event)"
|
|
10808
|
-
/>
|
|
10809
|
-
}
|
|
10810
|
-
@case ('date') {
|
|
10811
|
-
<input
|
|
10812
|
-
type="date"
|
|
10813
|
-
class="field-input"
|
|
10814
|
-
[ngModel]="currentValue()"
|
|
10815
|
-
(ngModelChange)="onFixedValueChanged($event)"
|
|
10816
|
-
/>
|
|
10817
|
-
}
|
|
10818
|
-
@default {
|
|
10819
|
-
<input
|
|
10820
|
-
type="text"
|
|
10821
|
-
class="field-input"
|
|
10822
|
-
placeholder="Enter fixed value"
|
|
10823
|
-
[ngModel]="currentValue()"
|
|
10824
|
-
(ngModelChange)="onFixedValueChanged($event)"
|
|
10825
|
-
/>
|
|
10826
|
-
}
|
|
10827
|
-
}
|
|
10828
|
-
}
|
|
11410
|
+
<div class="property-group">
|
|
11411
|
+
<div class="group-title">Page Grid Layout</div>
|
|
10829
11412
|
|
|
10830
|
-
|
|
10831
|
-
|
|
11413
|
+
<div class="field-row">
|
|
11414
|
+
<label class="field-label">Total Columns</label>
|
|
10832
11415
|
<select
|
|
10833
11416
|
class="field-select"
|
|
10834
|
-
[ngModel]="
|
|
10835
|
-
(ngModelChange)="
|
|
11417
|
+
[ngModel]="definition().layout.columns"
|
|
11418
|
+
(ngModelChange)="updateLayout.emit({ field: 'columns', value: +$event })"
|
|
10836
11419
|
>
|
|
10837
|
-
<option value=""
|
|
10838
|
-
<option value="
|
|
10839
|
-
<option value="
|
|
10840
|
-
<option value="{{ '{{' }}context.tenantId{{ '}}' }}">Current Company / Tenant</option>
|
|
10841
|
-
<option value="{{ '{{' }}context.today{{ '}}' }}">Today's Date</option>
|
|
10842
|
-
<option value="{{ '{{' }}context.startOfMonth{{ '}}' }}">First Day of Current Month</option>
|
|
11420
|
+
<option [value]="12">12 Columns (Standard)</option>
|
|
11421
|
+
<option [value]="16">16 Columns (Dense)</option>
|
|
11422
|
+
<option [value]="24">24 Columns (Ultra-wide)</option>
|
|
10843
11423
|
</select>
|
|
10844
|
-
|
|
11424
|
+
</div>
|
|
10845
11425
|
|
|
10846
|
-
|
|
10847
|
-
|
|
11426
|
+
<div class="field-row">
|
|
11427
|
+
<label class="field-label">Cell Gap (px)</label>
|
|
10848
11428
|
<input
|
|
10849
|
-
type="
|
|
10850
|
-
class="field-input
|
|
10851
|
-
|
|
10852
|
-
|
|
10853
|
-
|
|
11429
|
+
type="number"
|
|
11430
|
+
class="field-input"
|
|
11431
|
+
min="0"
|
|
11432
|
+
max="64"
|
|
11433
|
+
[ngModel]="definition().layout.gap ?? 16"
|
|
11434
|
+
(ngModelChange)="updateLayout.emit({ field: 'gap', value: +$event })"
|
|
10854
11435
|
/>
|
|
10855
|
-
|
|
10856
|
-
</div>
|
|
11436
|
+
</div>
|
|
10857
11437
|
|
|
10858
|
-
|
|
10859
|
-
|
|
10860
|
-
|
|
11438
|
+
<div class="field-row">
|
|
11439
|
+
<label class="field-label">Padding (px)</label>
|
|
11440
|
+
<input
|
|
11441
|
+
type="number"
|
|
11442
|
+
class="field-input"
|
|
11443
|
+
min="0"
|
|
11444
|
+
max="64"
|
|
11445
|
+
[ngModel]="definition().layout.padding ?? 24"
|
|
11446
|
+
(ngModelChange)="updateLayout.emit({ field: 'padding', value: +$event })"
|
|
11447
|
+
/>
|
|
11448
|
+
</div>
|
|
11449
|
+
|
|
11450
|
+
<div class="field-row">
|
|
11451
|
+
<label class="field-label">Direction</label>
|
|
11452
|
+
<select
|
|
11453
|
+
class="field-select"
|
|
11454
|
+
[ngModel]="definition().layout.direction ?? 'ltr'"
|
|
11455
|
+
(ngModelChange)="updateLayout.emit({ field: 'direction', value: $event })"
|
|
11456
|
+
>
|
|
11457
|
+
<option value="ltr">Left to Right (LTR)</option>
|
|
11458
|
+
<option value="rtl">Right to Left (RTL)</option>
|
|
11459
|
+
</select>
|
|
11460
|
+
</div>
|
|
11461
|
+
</div>
|
|
10861
11462
|
</div>
|
|
10862
|
-
`, styles: [".
|
|
11463
|
+
`, styles: [".property-group{display:flex;flex-direction:column;gap:12px}.group-title{font-size:.7rem;font-weight:700;color:var(--rpt-color-text-muted, #64748b);text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--rpt-color-border, #e2e8f0);padding-bottom:4px}.field-row{display:flex;flex-direction:column;gap:4px}.field-label-row{display:flex;align-items:center;justify-content:space-between}.field-badge-val{font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border:1px solid #bfdbfe;padding:1px 6px;border-radius:9999px}.field-label{font-size:.7rem;font-weight:600;color:var(--rpt-color-text, #0f172a)}.field-input,.field-select,.field-textarea{width:100%;box-sizing:border-box;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius-sm, 4px);padding:6px 10px;font-size:var(--rpt-font-size-sm, .75rem);color:var(--rpt-color-text, #0f172a);background:#fff;outline:none;transition:all var(--rpt-transition-speed, .15s) ease}.field-select{appearance:none;-webkit-appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right 10px center;background-size:13px 13px;padding-right:28px;cursor:pointer}.field-input:focus,.field-select:focus,.field-textarea:focus{border-color:var(--rpt-color-primary, #2563eb);box-shadow:var(--rpt-box-shadow-focus, 0 0 0 3px rgba(37, 99, 235, .25))}.checkbox-toggle{display:inline-flex;align-items:center;gap:8px;font-size:.75rem;font-weight:500;color:var(--rpt-color-text, #0f172a);cursor:pointer}.preset-pill-group{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.preset-pill{background:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;padding:2px 8px;font-size:.65rem;font-weight:600;color:#475569;cursor:pointer;transition:all .12s ease}.preset-pill:hover{background:#e2e8f0;color:#0f172a;border-color:#cbd5e1}.preset-pill-active{background:var(--rpt-color-primary, #2563eb)!important;color:#fff!important;border-color:var(--rpt-color-primary, #2563eb)!important}.preset-pill-reset{color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border-color:#bfdbfe}.preset-pill-reset:hover{background:#dbeafe;color:#1d4ed8;border-color:#93c5fd}\n", ".props-form{display:flex;flex-direction:column;gap:20px}\n"] }]
|
|
10863
11464
|
}] });
|
|
10864
11465
|
|
|
10865
11466
|
class PropertyEditorDatasourceComponent {
|
|
@@ -10993,8 +11594,22 @@ class PropertyEditorDatasourceComponent {
|
|
|
10993
11594
|
const params = paramsList || this.procedureParameters();
|
|
10994
11595
|
if (!params || params.length === 0)
|
|
10995
11596
|
return;
|
|
11597
|
+
const allowed = new Set();
|
|
11598
|
+
for (const param of params) {
|
|
11599
|
+
const clean = param.name.replace(/^@/, '').toLowerCase();
|
|
11600
|
+
allowed.add(clean);
|
|
11601
|
+
allowed.add(`@${clean}`);
|
|
11602
|
+
allowed.add(param.name.toLowerCase());
|
|
11603
|
+
}
|
|
10996
11604
|
const currentMappings = { ...(this.comp().dataSource?.parameterMappings ?? {}) };
|
|
10997
11605
|
let changed = false;
|
|
11606
|
+
// Prune orphaned keys that do not belong to this stored procedure
|
|
11607
|
+
for (const key of Object.keys(currentMappings)) {
|
|
11608
|
+
if (!allowed.has(key.toLowerCase())) {
|
|
11609
|
+
delete currentMappings[key];
|
|
11610
|
+
changed = true;
|
|
11611
|
+
}
|
|
11612
|
+
}
|
|
10998
11613
|
for (const param of params) {
|
|
10999
11614
|
const clean = param.name.replace(/^@/, '');
|
|
11000
11615
|
const existing = currentMappings[param.name] ?? currentMappings[clean];
|
|
@@ -11039,8 +11654,22 @@ class PropertyEditorDatasourceComponent {
|
|
|
11039
11654
|
const params = paramsList || this.procedureParameters();
|
|
11040
11655
|
if (!params || params.length === 0)
|
|
11041
11656
|
return;
|
|
11657
|
+
const allowed = new Set();
|
|
11658
|
+
for (const param of params) {
|
|
11659
|
+
const clean = param.name.replace(/^@/, '').toLowerCase();
|
|
11660
|
+
allowed.add(clean);
|
|
11661
|
+
allowed.add(`@${clean}`);
|
|
11662
|
+
allowed.add(param.name.toLowerCase());
|
|
11663
|
+
}
|
|
11042
11664
|
const currentMappings = { ...(this.comp().dataSource?.parameterMappings ?? {}) };
|
|
11043
11665
|
let changed = false;
|
|
11666
|
+
// Prune orphaned keys that do not belong to this stored procedure
|
|
11667
|
+
for (const key of Object.keys(currentMappings)) {
|
|
11668
|
+
if (!allowed.has(key.toLowerCase())) {
|
|
11669
|
+
delete currentMappings[key];
|
|
11670
|
+
changed = true;
|
|
11671
|
+
}
|
|
11672
|
+
}
|
|
11044
11673
|
for (const param of params) {
|
|
11045
11674
|
const clean = param.name.replace(/^@/, '');
|
|
11046
11675
|
const existing = currentMappings[param.name] ?? currentMappings[clean];
|
|
@@ -11118,6 +11747,7 @@ class PropertyEditorDatasourceComponent {
|
|
|
11118
11747
|
sourceType: 'static',
|
|
11119
11748
|
displayField: '',
|
|
11120
11749
|
valueField: '',
|
|
11750
|
+
parameterMappings: {},
|
|
11121
11751
|
});
|
|
11122
11752
|
return;
|
|
11123
11753
|
}
|
|
@@ -11134,9 +11764,11 @@ class PropertyEditorDatasourceComponent {
|
|
|
11134
11764
|
schema: 'dbo',
|
|
11135
11765
|
displayField: '',
|
|
11136
11766
|
valueField: '',
|
|
11767
|
+
parameterMappings: isValidForType ? (this.comp().dataSource?.parameterMappings ?? {}) : {},
|
|
11137
11768
|
});
|
|
11138
11769
|
}
|
|
11139
11770
|
onEntitySelected(entityName) {
|
|
11771
|
+
const isDifferent = entityName !== this.comp().dataSource?.entityName;
|
|
11140
11772
|
this.updateDataSource.emit({
|
|
11141
11773
|
...(this.comp().dataSource ?? { displayField: '', valueField: '' }),
|
|
11142
11774
|
sourceType: this.currentSourceType(),
|
|
@@ -11144,6 +11776,7 @@ class PropertyEditorDatasourceComponent {
|
|
|
11144
11776
|
schema: 'dbo',
|
|
11145
11777
|
valueField: '',
|
|
11146
11778
|
displayField: '',
|
|
11779
|
+
parameterMappings: isDifferent ? {} : (this.comp().dataSource?.parameterMappings ?? {}),
|
|
11147
11780
|
});
|
|
11148
11781
|
}
|
|
11149
11782
|
onValueFieldChanged(valueField) {
|
|
@@ -11212,11 +11845,11 @@ class PropertyEditorDatasourceComponent {
|
|
|
11212
11845
|
this.updateProperty.emit({ key: 'options', value: current });
|
|
11213
11846
|
}
|
|
11214
11847
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PropertyEditorDatasourceComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
11215
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: PropertyEditorDatasourceComponent, isStandalone: true, selector: "rpt-property-editor-datasource", inputs: { comp: { classPropertyName: "comp", publicName: "comp", isSignal: true, isRequired: true, transformFunction: null }, allComponents: { classPropertyName: "allComponents", publicName: "allComponents", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { updateDataSource: "updateDataSource", updateProperty: "updateProperty" }, ngImport: i0, template: " <div class=\"property-group\">\n <!-- 1. Header with Simple / Advanced Mode Toggle -->\n <div class=\"datasource-header-bar\">\n <div class=\"group-title\">Options Data Source</div>\n <div class=\"mode-toggle-pill\">\n <button\n type=\"button\"\n class=\"pill-btn\"\n [class.active]=\"!isAdvancedMode()\"\n (click)=\"isAdvancedMode.set(false)\"\n title=\"Simplified view for business users with friendly names\"\n >\n Simple\n </button>\n <button\n type=\"button\"\n class=\"pill-btn\"\n [class.active]=\"isAdvancedMode()\"\n (click)=\"isAdvancedMode.set(true)\"\n title=\"Technical view with SQL types and expressions\"\n >\n Advanced\n </button>\n </div>\n </div>\n\n <!-- 2. Source Type Segmented Switcher -->\n <div class=\"source-type-switcher\">\n <button\n type=\"button\"\n class=\"source-tab-btn\"\n [class.active]=\"currentSourceType() === 'table'\"\n (click)=\"setSourceType('table')\"\n >\n <rpt-icon name=\"table\" [size]=\"12\" />\n <span>{{ isAdvancedMode() ? 'Table / View' : 'Database Table' }}</span>\n </button>\n <button\n type=\"button\"\n class=\"source-tab-btn\"\n [class.active]=\"currentSourceType() === 'procedure'\"\n (click)=\"setSourceType('procedure')\"\n >\n <rpt-icon name=\"code\" [size]=\"12\" />\n <span>{{ isAdvancedMode() ? 'Stored Proc' : 'Data Query' }}</span>\n </button>\n <button\n type=\"button\"\n class=\"source-tab-btn\"\n [class.active]=\"currentSourceType() === 'static'\"\n (click)=\"setSourceType('static')\"\n >\n <rpt-icon name=\"edit\" [size]=\"12\" />\n <span>Static List</span>\n </button>\n </div>\n\n <!-- 3A. DATABASE TABLE / VIEW CONFIGURATION -->\n @if (currentSourceType() === 'table') {\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Database Table / View' : 'Select Data Source' }}\n </label>\n <button\n type=\"button\"\n class=\"btn-refresh-entities\"\n (click)=\"loadEntities()\"\n title=\"Refresh available tables from backend\"\n >\n <rpt-icon name=\"refresh\" [size]=\"11\" />\n <span>Refresh</span>\n </button>\n </div>\n <select\n class=\"field-select\"\n [ngModel]=\"selectedEntity()\"\n (ngModelChange)=\"onEntitySelected($event)\"\n [disabled]=\"isLoadingEntities()\"\n >\n @if (isLoadingEntities()) {\n <option value=\"\">Loading tables from database...</option>\n } @else if (availableTables().length === 0) {\n <option value=\"\">-- No tables found (Click Refresh) --</option>\n } @else {\n <option value=\"\">-- Choose Data Table --</option>\n }\n @for (tbl of availableTables(); track tbl.name) {\n <option [value]=\"tbl.name\">\n @if (isAdvancedMode()) {\n {{ tbl.schema }}.{{ tbl.name }} ({{ tbl.type }})\n } @else {\n {{ tbl.displayName }}{{ tbl.category ? ' \u2022 ' + tbl.category : '' }}\n }\n </option>\n }\n </select>\n </div>\n\n @if (selectedEntity()) {\n <!-- Display Field (Visible text) -->\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Display Field (Visible Text)' : 'Text Shown in Dropdown' }}\n </label>\n @if (!isAdvancedMode()) {\n <span class=\"auto-badge\" title=\"Auto-detected column for text label\">Auto-detected</span>\n }\n </div>\n <select\n class=\"field-select\"\n [ngModel]=\"selectedDisplayField()\"\n (ngModelChange)=\"onDisplayFieldChanged($event)\"\n >\n <option value=\"\">-- Select Display Column --</option>\n @for (col of entityColumns(); track col.field) {\n <option [value]=\"col.field\">\n {{ col.label }} {{ isAdvancedMode() ? '(' + col.dataType + ')' : '' }}\n </option>\n }\n </select>\n </div>\n\n <!-- Value Field (Stored key) -->\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Value Field (Stored Key)' : 'Internal Value / Key' }}\n </label>\n @if (!isAdvancedMode()) {\n <span class=\"auto-badge\" title=\"Auto-detected column for primary ID\">Auto-detected</span>\n }\n </div>\n <select\n class=\"field-select\"\n [ngModel]=\"selectedValueField()\"\n (ngModelChange)=\"onValueFieldChanged($event)\"\n >\n <option value=\"\">-- Select Key Column --</option>\n @for (col of entityColumns(); track col.field) {\n <option [value]=\"col.field\">\n {{ col.label }} {{ isAdvancedMode() ? '(' + col.dataType + ')' : '' }}\n </option>\n }\n </select>\n </div>\n\n <!-- Cascading Dropdown Dependency -->\n <div class=\"field-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Dependent On (Cascading Parent Field)' : 'Filter by Another Field' }}\n </label>\n <select\n class=\"field-select\"\n [ngModel]=\"selectedDependsOn()\"\n (ngModelChange)=\"onDependsOnChanged($event)\"\n >\n <option value=\"\">-- None (Independent) --</option>\n @for (other of availableParentFields(); track other.key || other.id) {\n <option [value]=\"other.key || other.id\">{{ other.label || other.key }}</option>\n }\n </select>\n <span class=\"field-hint\">\n {{ isAdvancedMode() ? 'Automatically re-queries options when parent field changes.' : 'Only loads options when the selected field changes.' }}\n </span>\n </div>\n }\n }\n\n <!-- 3B. STORED PROCEDURE CONFIGURATION -->\n @if (currentSourceType() === 'procedure') {\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Stored Procedure' : 'Select Data Procedure' }}\n </label>\n <button\n type=\"button\"\n class=\"btn-refresh-entities\"\n (click)=\"loadEntities()\"\n title=\"Refresh available procedures from backend\"\n >\n <rpt-icon name=\"refresh\" [size]=\"11\" />\n <span>Refresh</span>\n </button>\n </div>\n <select\n class=\"field-select\"\n [ngModel]=\"selectedEntity()\"\n (ngModelChange)=\"onEntitySelected($event)\"\n [disabled]=\"isLoadingEntities()\"\n >\n @if (isLoadingEntities()) {\n <option value=\"\">Loading stored procedures from database...</option>\n } @else if (availableProcedures().length === 0) {\n <option value=\"\">-- No stored procedures found (Click Refresh) --</option>\n } @else {\n <option value=\"\">-- Choose Data Query / SP --</option>\n }\n @for (sp of availableProcedures(); track sp.name) {\n <option [value]=\"sp.name\">\n @if (isAdvancedMode()) {\n {{ sp.schema }}.{{ sp.name }}\n } @else {\n {{ sp.displayName }}{{ sp.category ? ' \u2022 ' + sp.category : '' }}\n }\n </option>\n }\n </select>\n </div>\n\n @if (selectedEntity()) {\n <!-- Visual Parameter Linkers -->\n @if (procedureParameters().length > 0) {\n <div class=\"sp-params-group\">\n <div class=\"params-header-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Procedure Input Parameters' : 'Filter Inputs & Parameters' }}\n </label>\n <button\n type=\"button\"\n class=\"btn-auto-params\"\n (click)=\"autoWireAllParameters()\"\n title=\"Auto-detect session parameters and add missing filter controls to the canvas\"\n >\n <rpt-icon name=\"check\" [size]=\"11\" />\n <span>Auto-Wire Canvas Inputs</span>\n </button>\n </div>\n\n @for (param of procedureParameters(); track param.name) {\n <rpt-parameter-binding-editor\n [param]=\"param\"\n [currentValue]=\"getParamValue(param.name)\"\n [allComponents]=\"allComponents()\"\n [currentComponentId]=\"comp().id\"\n [isAdvancedMode]=\"isAdvancedMode()\"\n (valueChange)=\"setParamValue(param.name, $event)\"\n (linkedFieldChange)=\"onParamLinkedFieldChange($event)\"\n />\n }\n </div>\n }\n\n <!-- Display Field (Visible text) -->\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Display Field (from SP result)' : 'Text Shown in Dropdown' }}\n </label>\n @if (!isAdvancedMode()) {\n <span class=\"auto-badge\">Auto-detected</span>\n }\n </div>\n <select\n class=\"field-select\"\n [ngModel]=\"selectedDisplayField()\"\n (ngModelChange)=\"onDisplayFieldChanged($event)\"\n >\n <option value=\"\">-- Select Display Column --</option>\n @for (col of entityColumns(); track col.field) {\n <option [value]=\"col.field\">\n {{ col.label }} {{ isAdvancedMode() ? '(' + col.dataType + ')' : '' }}\n </option>\n }\n </select>\n </div>\n\n <!-- Value Field (Stored key) -->\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Value Field (from SP result)' : 'Internal Value / Key' }}\n </label>\n @if (!isAdvancedMode()) {\n <span class=\"auto-badge\">Auto-detected</span>\n }\n </div>\n <select\n class=\"field-select\"\n [ngModel]=\"selectedValueField()\"\n (ngModelChange)=\"onValueFieldChanged($event)\"\n >\n <option value=\"\">-- Select Key Column --</option>\n @for (col of entityColumns(); track col.field) {\n <option [value]=\"col.field\">\n {{ col.label }} {{ isAdvancedMode() ? '(' + col.dataType + ')' : '' }}\n </option>\n }\n </select>\n </div>\n }\n }\n\n <!-- 3C. STATIC OPTIONS EDITOR -->\n @if (currentSourceType() === 'static') {\n <div class=\"static-options-wrapper\">\n <div class=\"static-options-header\">\n <span class=\"field-label\">Static Options</span>\n <button type=\"button\" class=\"btn-add-option\" (click)=\"addStaticOption()\">\n <rpt-icon name=\"plus\" [size]=\"11\" />\n <span>Add Option</span>\n </button>\n </div>\n\n <div class=\"static-options-list\">\n @for (opt of staticOptions(); track $index) {\n <div class=\"static-option-row\">\n <input\n type=\"text\"\n class=\"field-input opt-key\"\n placeholder=\"Key\"\n [ngModel]=\"opt.key\"\n (ngModelChange)=\"updateStaticOption($index, 'key', $event)\"\n />\n <input\n type=\"text\"\n class=\"field-input opt-label\"\n placeholder=\"Label\"\n [ngModel]=\"opt.label\"\n (ngModelChange)=\"updateStaticOption($index, 'label', $event)\"\n />\n <button\n type=\"button\"\n class=\"btn-remove-option\"\n (click)=\"removeStaticOption($index)\"\n title=\"Remove option\"\n >\n <rpt-icon name=\"trash\" [size]=\"12\" />\n </button>\n </div>\n }\n </div>\n </div>\n }\n </div>", styles: [".property-group{display:flex;flex-direction:column;gap:12px}.group-title{font-size:.7rem;font-weight:700;color:var(--rpt-color-text-muted, #64748b);text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--rpt-color-border, #e2e8f0);padding-bottom:4px}.field-row{display:flex;flex-direction:column;gap:4px}.field-label-row{display:flex;align-items:center;justify-content:space-between}.field-badge-val{font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border:1px solid #bfdbfe;padding:1px 6px;border-radius:9999px}.field-label{font-size:.7rem;font-weight:600;color:var(--rpt-color-text, #0f172a)}.field-input,.field-select,.field-textarea{width:100%;box-sizing:border-box;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius-sm, 4px);padding:6px 10px;font-size:var(--rpt-font-size-sm, .75rem);color:var(--rpt-color-text, #0f172a);background:#fff;outline:none;transition:all var(--rpt-transition-speed, .15s) ease}.field-select{appearance:none;-webkit-appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right 10px center;background-size:13px 13px;padding-right:28px;cursor:pointer}.field-input:focus,.field-select:focus,.field-textarea:focus{border-color:var(--rpt-color-primary, #2563eb);box-shadow:var(--rpt-box-shadow-focus, 0 0 0 3px rgba(37, 99, 235, .25))}.checkbox-toggle{display:inline-flex;align-items:center;gap:8px;font-size:.75rem;font-weight:500;color:var(--rpt-color-text, #0f172a);cursor:pointer}.preset-pill-group{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.preset-pill{background:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;padding:2px 8px;font-size:.65rem;font-weight:600;color:#475569;cursor:pointer;transition:all .12s ease}.preset-pill:hover{background:#e2e8f0;color:#0f172a;border-color:#cbd5e1}.preset-pill-active{background:var(--rpt-color-primary, #2563eb)!important;color:#fff!important;border-color:var(--rpt-color-primary, #2563eb)!important}.preset-pill-reset{color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border-color:#bfdbfe}.preset-pill-reset:hover{background:#dbeafe;color:#1d4ed8;border-color:#93c5fd}\n", ".datasource-header-bar{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.mode-toggle-pill{display:inline-flex;background:#e2e8f0;border-radius:12px;padding:2px;gap:2px}.pill-btn{border:none;background:transparent;padding:2px 8px;font-size:.65rem;font-weight:500;color:#64748b;border-radius:10px;cursor:pointer;transition:all .12s ease}.pill-btn:hover{color:#0f172a}.pill-btn.active{background:#fff;color:var(--rpt-color-primary, #2563eb);font-weight:700;box-shadow:0 1px 2px #00000014}.source-type-switcher{display:flex;background:#f1f5f9;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:6px;padding:2px;gap:2px;margin-bottom:6px}.source-tab-btn{flex:1;display:inline-flex;align-items:center;justify-content:center;gap:5px;padding:6px 4px;font-size:.7rem;font-weight:600;color:var(--rpt-color-text-muted, #64748b);background:transparent;border:none;border-radius:4px;cursor:pointer;transition:all .12s ease}.source-tab-btn:hover{color:var(--rpt-color-text, #0f172a)}.source-tab-btn.active{background:#fff;color:var(--rpt-color-primary, #2563eb);box-shadow:0 1px 2px #0000000d}.btn-refresh-entities{display:inline-flex;align-items:center;gap:4px;border:none;background:transparent;color:var(--rpt-color-primary, #2563eb);font-size:.65rem;font-weight:600;cursor:pointer;padding:0}.btn-refresh-entities:hover{text-decoration:underline}.btn-auto-params{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;font-size:.65rem;font-weight:600;color:#2563eb;background:#eff6ff;border:1px solid #bfdbfe;border-radius:4px;cursor:pointer;transition:all .12s ease}.btn-auto-params:hover{background:#2563eb;color:#fff}.auto-badge{font-size:.62rem;font-weight:600;background:#dcfce7;color:#166534;padding:1px 5px;border-radius:4px}.field-hint{font-size:.65rem;color:var(--rpt-color-text-muted, #94a3b8);margin-top:2px}.sp-params-group{display:flex;flex-direction:column;gap:6px;background:#f8fafc;padding:8px;border-radius:6px;border:1px solid #e2e8f0;margin-bottom:6px}.params-header-row{display:flex;align-items:center;justify-content:space-between}.params-hint{font-size:.64rem;color:#64748b;font-style:italic}.static-options-wrapper{display:flex;flex-direction:column;gap:8px}.static-options-header{display:flex;align-items:center;justify-content:space-between}.btn-add-option{display:inline-flex;align-items:center;gap:4px;border:1px solid var(--rpt-color-border, #cbd5e1);background:#fff;border-radius:4px;padding:3px 8px;font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);cursor:pointer;transition:all .12s ease}.btn-add-option:hover{background:#eff6ff}.static-options-list{display:flex;flex-direction:column;gap:6px}.static-option-row{display:flex;align-items:center;gap:6px}.opt-key{flex:1}.opt-label{flex:1.5}.btn-remove-option{width:24px;height:24px;border:none;background:transparent;color:#94a3b8;border-radius:4px;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;flex-shrink:0;padding:0;transition:all .12s ease}.btn-remove-option:hover{background:#fee2e2;color:#ef4444}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.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: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: RptIconComponent, selector: "rpt-icon", inputs: ["name", "size", "color", "strokeWidth"] }, { kind: "component", type: ParameterBindingEditorComponent, selector: "rpt-parameter-binding-editor", inputs: ["param", "currentValue", "allComponents", "currentComponentId", "isAdvancedMode"], outputs: ["valueChange", "linkedFieldChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
11848
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: PropertyEditorDatasourceComponent, isStandalone: true, selector: "rpt-property-editor-datasource", inputs: { comp: { classPropertyName: "comp", publicName: "comp", isSignal: true, isRequired: true, transformFunction: null }, allComponents: { classPropertyName: "allComponents", publicName: "allComponents", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { updateDataSource: "updateDataSource", updateProperty: "updateProperty" }, ngImport: i0, template: " <div class=\"property-group\">\n <!-- 1. Header with Simple / Advanced Mode Toggle -->\n <div class=\"datasource-header-bar\">\n <div class=\"group-title\">Options Data Source</div>\n <div class=\"mode-toggle-pill\">\n <button\n type=\"button\"\n class=\"pill-btn\"\n [class.active]=\"!isAdvancedMode()\"\n (click)=\"isAdvancedMode.set(false)\"\n title=\"Simplified view for business users with friendly names\"\n >\n Simple\n </button>\n <button\n type=\"button\"\n class=\"pill-btn\"\n [class.active]=\"isAdvancedMode()\"\n (click)=\"isAdvancedMode.set(true)\"\n title=\"Technical view with SQL types and expressions\"\n >\n Advanced\n </button>\n </div>\n </div>\n\n <div class=\"datasource-guidance-card\">\n <rpt-icon name=\"info\" [size]=\"12\" />\n <span>Populates selectable choices in this dropdown filter. For the report's main data and result grid, use the <strong>Grid</strong> tab.</span>\n </div>\n\n <!-- 2. Source Type Segmented Switcher -->\n <div class=\"source-type-switcher\">\n <button\n type=\"button\"\n class=\"source-tab-btn\"\n [class.active]=\"currentSourceType() === 'table'\"\n (click)=\"setSourceType('table')\"\n >\n <rpt-icon name=\"table\" [size]=\"12\" />\n <span>{{ isAdvancedMode() ? 'Table / View' : 'Database Table' }}</span>\n </button>\n <button\n type=\"button\"\n class=\"source-tab-btn\"\n [class.active]=\"currentSourceType() === 'procedure'\"\n (click)=\"setSourceType('procedure')\"\n >\n <rpt-icon name=\"code\" [size]=\"12\" />\n <span>{{ isAdvancedMode() ? 'Stored Proc' : 'Data Query' }}</span>\n </button>\n <button\n type=\"button\"\n class=\"source-tab-btn\"\n [class.active]=\"currentSourceType() === 'static'\"\n (click)=\"setSourceType('static')\"\n >\n <rpt-icon name=\"edit\" [size]=\"12\" />\n <span>Static List</span>\n </button>\n </div>\n\n <!-- 3A. DATABASE TABLE / VIEW CONFIGURATION -->\n @if (currentSourceType() === 'table') {\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Database Table / View' : 'Select Data Source' }}\n </label>\n <button\n type=\"button\"\n class=\"btn-refresh-entities\"\n (click)=\"loadEntities()\"\n title=\"Refresh available tables from backend\"\n >\n <rpt-icon name=\"refresh\" [size]=\"11\" />\n <span>Refresh</span>\n </button>\n </div>\n <select\n class=\"field-select\"\n [ngModel]=\"selectedEntity()\"\n (ngModelChange)=\"onEntitySelected($event)\"\n [disabled]=\"isLoadingEntities()\"\n >\n @if (isLoadingEntities()) {\n <option value=\"\">Loading tables from database...</option>\n } @else if (availableTables().length === 0) {\n <option value=\"\">-- No tables found (Click Refresh) --</option>\n } @else {\n <option value=\"\">-- Choose Data Table --</option>\n }\n @for (tbl of availableTables(); track tbl.name) {\n <option [value]=\"tbl.name\">\n @if (isAdvancedMode()) {\n {{ tbl.schema }}.{{ tbl.name }} ({{ tbl.type }})\n } @else {\n {{ tbl.displayName }}{{ tbl.category ? ' \u2022 ' + tbl.category : '' }}\n }\n </option>\n }\n </select>\n </div>\n\n @if (selectedEntity()) {\n <!-- Display Field (Visible text) -->\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Display Field (Visible Text)' : 'Text Shown in Dropdown' }}\n </label>\n @if (!isAdvancedMode()) {\n <span class=\"auto-badge\" title=\"Auto-detected column for text label\">Auto-detected</span>\n }\n </div>\n <select\n class=\"field-select\"\n [ngModel]=\"selectedDisplayField()\"\n (ngModelChange)=\"onDisplayFieldChanged($event)\"\n >\n <option value=\"\">-- Select Display Column --</option>\n @for (col of entityColumns(); track col.field) {\n <option [value]=\"col.field\">\n {{ col.label }} {{ isAdvancedMode() ? '(' + col.dataType + ')' : '' }}\n </option>\n }\n </select>\n </div>\n\n <!-- Value Field (Stored key) -->\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Value Field (Stored Key)' : 'Internal Value / Key' }}\n </label>\n @if (!isAdvancedMode()) {\n <span class=\"auto-badge\" title=\"Auto-detected column for primary ID\">Auto-detected</span>\n }\n </div>\n <select\n class=\"field-select\"\n [ngModel]=\"selectedValueField()\"\n (ngModelChange)=\"onValueFieldChanged($event)\"\n >\n <option value=\"\">-- Select Key Column --</option>\n @for (col of entityColumns(); track col.field) {\n <option [value]=\"col.field\">\n {{ col.label }} {{ isAdvancedMode() ? '(' + col.dataType + ')' : '' }}\n </option>\n }\n </select>\n </div>\n\n <!-- Cascading Dropdown Dependency -->\n <div class=\"field-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Dependent On (Cascading Parent Field)' : 'Filter by Another Field' }}\n </label>\n <select\n class=\"field-select\"\n [ngModel]=\"selectedDependsOn()\"\n (ngModelChange)=\"onDependsOnChanged($event)\"\n >\n <option value=\"\">-- None (Independent) --</option>\n @for (other of availableParentFields(); track other.key || other.id) {\n <option [value]=\"other.key || other.id\">{{ other.label || other.key }}</option>\n }\n </select>\n <span class=\"field-hint\">\n {{ isAdvancedMode() ? 'Automatically re-queries options when parent field changes.' : 'Only loads options when the selected field changes.' }}\n </span>\n </div>\n }\n }\n\n <!-- 3B. STORED PROCEDURE CONFIGURATION -->\n @if (currentSourceType() === 'procedure') {\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Stored Procedure' : 'Select Data Procedure' }}\n </label>\n <button\n type=\"button\"\n class=\"btn-refresh-entities\"\n (click)=\"loadEntities()\"\n title=\"Refresh available procedures from backend\"\n >\n <rpt-icon name=\"refresh\" [size]=\"11\" />\n <span>Refresh</span>\n </button>\n </div>\n <select\n class=\"field-select\"\n [ngModel]=\"selectedEntity()\"\n (ngModelChange)=\"onEntitySelected($event)\"\n [disabled]=\"isLoadingEntities()\"\n >\n @if (isLoadingEntities()) {\n <option value=\"\">Loading stored procedures from database...</option>\n } @else if (availableProcedures().length === 0) {\n <option value=\"\">-- No stored procedures found (Click Refresh) --</option>\n } @else {\n <option value=\"\">-- Choose Data Query / SP --</option>\n }\n @for (sp of availableProcedures(); track sp.name) {\n <option [value]=\"sp.name\">\n @if (isAdvancedMode()) {\n {{ sp.schema }}.{{ sp.name }}\n } @else {\n {{ sp.displayName }}{{ sp.category ? ' \u2022 ' + sp.category : '' }}\n }\n </option>\n }\n </select>\n </div>\n\n @if (selectedEntity()) {\n <!-- Visual Parameter Linkers -->\n @if (procedureParameters().length > 0) {\n <div class=\"sp-params-group\">\n <div class=\"params-header-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Procedure Input Parameters' : 'Filter Inputs & Parameters' }}\n </label>\n <button\n type=\"button\"\n class=\"btn-auto-params\"\n (click)=\"autoWireAllParameters()\"\n title=\"Auto-detect session parameters and add missing filter controls to the canvas\"\n >\n <rpt-icon name=\"check\" [size]=\"11\" />\n <span>Auto-Wire Canvas Inputs</span>\n </button>\n </div>\n\n @for (param of procedureParameters(); track param.name) {\n <rpt-parameter-binding-editor\n [param]=\"param\"\n [currentValue]=\"getParamValue(param.name)\"\n [allComponents]=\"allComponents()\"\n [currentComponentId]=\"comp().id\"\n [isAdvancedMode]=\"isAdvancedMode()\"\n (valueChange)=\"setParamValue(param.name, $event)\"\n (linkedFieldChange)=\"onParamLinkedFieldChange($event)\"\n />\n }\n </div>\n }\n\n <!-- Display Field (Visible text) -->\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Display Field (from SP result)' : 'Text Shown in Dropdown' }}\n </label>\n @if (!isAdvancedMode()) {\n <span class=\"auto-badge\">Auto-detected</span>\n }\n </div>\n @if (entityColumns().length > 0) {\n <select\n class=\"field-select\"\n [ngModel]=\"selectedDisplayField()\"\n (ngModelChange)=\"onDisplayFieldChanged($event)\"\n >\n <option value=\"\">-- Select Display Column --</option>\n @for (col of entityColumns(); track col.field) {\n <option [value]=\"col.field\">\n {{ col.label }} {{ isAdvancedMode() ? '(' + col.dataType + ')' : '' }}\n </option>\n }\n </select>\n } @else {\n <input\n type=\"text\"\n class=\"field-input\"\n placeholder=\"Column name (leave blank for auto)\"\n [ngModel]=\"selectedDisplayField()\"\n (ngModelChange)=\"onDisplayFieldChanged($event)\"\n />\n }\n </div>\n\n <!-- Value Field (Stored key) -->\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Value Field (from SP result)' : 'Internal Value / Key' }}\n </label>\n @if (!isAdvancedMode()) {\n <span class=\"auto-badge\">Auto-detected</span>\n }\n </div>\n @if (entityColumns().length > 0) {\n <select\n class=\"field-select\"\n [ngModel]=\"selectedValueField()\"\n (ngModelChange)=\"onValueFieldChanged($event)\"\n >\n <option value=\"\">-- Select Key Column --</option>\n @for (col of entityColumns(); track col.field) {\n <option [value]=\"col.field\">\n {{ col.label }} {{ isAdvancedMode() ? '(' + col.dataType + ')' : '' }}\n </option>\n }\n </select>\n } @else {\n <input\n type=\"text\"\n class=\"field-input\"\n placeholder=\"Column name (leave blank for auto)\"\n [ngModel]=\"selectedValueField()\"\n (ngModelChange)=\"onValueFieldChanged($event)\"\n />\n }\n </div>\n }\n }\n\n <!-- 3C. STATIC OPTIONS EDITOR -->\n @if (currentSourceType() === 'static') {\n <div class=\"static-options-wrapper\">\n <div class=\"static-options-header\">\n <span class=\"field-label\">Static Options</span>\n <button type=\"button\" class=\"btn-add-option\" (click)=\"addStaticOption()\">\n <rpt-icon name=\"plus\" [size]=\"11\" />\n <span>Add Option</span>\n </button>\n </div>\n\n <div class=\"static-options-list\">\n @for (opt of staticOptions(); track $index) {\n <div class=\"static-option-row\">\n <input\n type=\"text\"\n class=\"field-input opt-key\"\n placeholder=\"Key\"\n [ngModel]=\"opt.key\"\n (ngModelChange)=\"updateStaticOption($index, 'key', $event)\"\n />\n <input\n type=\"text\"\n class=\"field-input opt-label\"\n placeholder=\"Label\"\n [ngModel]=\"opt.label\"\n (ngModelChange)=\"updateStaticOption($index, 'label', $event)\"\n />\n <button\n type=\"button\"\n class=\"btn-remove-option\"\n (click)=\"removeStaticOption($index)\"\n title=\"Remove option\"\n >\n <rpt-icon name=\"trash\" [size]=\"12\" />\n </button>\n </div>\n }\n </div>\n </div>\n }\n </div>", styles: [".property-group{display:flex;flex-direction:column;gap:12px}.group-title{font-size:.7rem;font-weight:700;color:var(--rpt-color-text-muted, #64748b);text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--rpt-color-border, #e2e8f0);padding-bottom:4px}.field-row{display:flex;flex-direction:column;gap:4px}.field-label-row{display:flex;align-items:center;justify-content:space-between}.field-badge-val{font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border:1px solid #bfdbfe;padding:1px 6px;border-radius:9999px}.field-label{font-size:.7rem;font-weight:600;color:var(--rpt-color-text, #0f172a)}.field-input,.field-select,.field-textarea{width:100%;box-sizing:border-box;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius-sm, 4px);padding:6px 10px;font-size:var(--rpt-font-size-sm, .75rem);color:var(--rpt-color-text, #0f172a);background:#fff;outline:none;transition:all var(--rpt-transition-speed, .15s) ease}.field-select{appearance:none;-webkit-appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right 10px center;background-size:13px 13px;padding-right:28px;cursor:pointer}.field-input:focus,.field-select:focus,.field-textarea:focus{border-color:var(--rpt-color-primary, #2563eb);box-shadow:var(--rpt-box-shadow-focus, 0 0 0 3px rgba(37, 99, 235, .25))}.checkbox-toggle{display:inline-flex;align-items:center;gap:8px;font-size:.75rem;font-weight:500;color:var(--rpt-color-text, #0f172a);cursor:pointer}.preset-pill-group{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.preset-pill{background:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;padding:2px 8px;font-size:.65rem;font-weight:600;color:#475569;cursor:pointer;transition:all .12s ease}.preset-pill:hover{background:#e2e8f0;color:#0f172a;border-color:#cbd5e1}.preset-pill-active{background:var(--rpt-color-primary, #2563eb)!important;color:#fff!important;border-color:var(--rpt-color-primary, #2563eb)!important}.preset-pill-reset{color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border-color:#bfdbfe}.preset-pill-reset:hover{background:#dbeafe;color:#1d4ed8;border-color:#93c5fd}\n", ".datasource-header-bar{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.mode-toggle-pill{display:inline-flex;background:#e2e8f0;border-radius:12px;padding:2px;gap:2px}.pill-btn{border:none;background:transparent;padding:2px 8px;font-size:.65rem;font-weight:500;color:#64748b;border-radius:10px;cursor:pointer;transition:all .12s ease}.pill-btn:hover{color:#0f172a}.pill-btn.active{background:#fff;color:var(--rpt-color-primary, #2563eb);font-weight:700;box-shadow:0 1px 2px #00000014}.datasource-guidance-card{display:flex;align-items:center;gap:6px;background:#eff6ff;border:1px solid #bfdbfe;color:#1e40af;font-size:.675rem;padding:6px 8px;border-radius:6px;margin-bottom:8px;line-height:1.35}.source-type-switcher{display:flex;background:#f1f5f9;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:6px;padding:2px;gap:2px;margin-bottom:6px}.source-tab-btn{flex:1;display:inline-flex;align-items:center;justify-content:center;gap:5px;padding:6px 4px;font-size:.7rem;font-weight:600;color:var(--rpt-color-text-muted, #64748b);background:transparent;border:none;border-radius:4px;cursor:pointer;transition:all .12s ease}.source-tab-btn:hover{color:var(--rpt-color-text, #0f172a)}.source-tab-btn.active{background:#fff;color:var(--rpt-color-primary, #2563eb);box-shadow:0 1px 2px #0000000d}.btn-refresh-entities{display:inline-flex;align-items:center;gap:4px;border:none;background:transparent;color:var(--rpt-color-primary, #2563eb);font-size:.65rem;font-weight:600;cursor:pointer;padding:0}.btn-refresh-entities:hover{text-decoration:underline}.btn-auto-params{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;font-size:.65rem;font-weight:600;color:#2563eb;background:#eff6ff;border:1px solid #bfdbfe;border-radius:4px;cursor:pointer;transition:all .12s ease}.btn-auto-params:hover{background:#2563eb;color:#fff}.auto-badge{font-size:.62rem;font-weight:600;background:#dcfce7;color:#166534;padding:1px 5px;border-radius:4px}.field-hint{font-size:.65rem;color:var(--rpt-color-text-muted, #94a3b8);margin-top:2px}.sp-params-group{display:flex;flex-direction:column;gap:6px;background:#f8fafc;padding:8px;border-radius:6px;border:1px solid #e2e8f0;margin-bottom:6px}.params-header-row{display:flex;align-items:center;justify-content:space-between}.params-hint{font-size:.64rem;color:#64748b;font-style:italic}.static-options-wrapper{display:flex;flex-direction:column;gap:8px}.static-options-header{display:flex;align-items:center;justify-content:space-between}.btn-add-option{display:inline-flex;align-items:center;gap:4px;border:1px solid var(--rpt-color-border, #cbd5e1);background:#fff;border-radius:4px;padding:3px 8px;font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);cursor:pointer;transition:all .12s ease}.btn-add-option:hover{background:#eff6ff}.static-options-list{display:flex;flex-direction:column;gap:6px}.static-option-row{display:flex;align-items:center;gap:6px}.opt-key{flex:1}.opt-label{flex:1.5}.btn-remove-option{width:24px;height:24px;border:none;background:transparent;color:#94a3b8;border-radius:4px;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;flex-shrink:0;padding:0;transition:all .12s ease}.btn-remove-option:hover{background:#fee2e2;color:#ef4444}.field-input{width:100%;padding:6px 8px;font-size:.75rem;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius, 6px);background:var(--rpt-color-surface, #ffffff);color:var(--rpt-color-text, #0f172a);box-sizing:border-box;outline:none;transition:border-color .12s ease}.field-input:focus{border-color:var(--rpt-color-primary, #2563eb)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.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: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: RptIconComponent, selector: "rpt-icon", inputs: ["name", "size", "color", "strokeWidth"] }, { kind: "component", type: ParameterBindingEditorComponent, selector: "rpt-parameter-binding-editor", inputs: ["param", "currentValue", "allComponents", "currentComponentId", "isAdvancedMode"], outputs: ["valueChange", "linkedFieldChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
11216
11849
|
}
|
|
11217
11850
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PropertyEditorDatasourceComponent, decorators: [{
|
|
11218
11851
|
type: Component,
|
|
11219
|
-
args: [{ selector: 'rpt-property-editor-datasource', standalone: true, imports: [CommonModule, FormsModule, RptIconComponent, ParameterBindingEditorComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: " <div class=\"property-group\">\n <!-- 1. Header with Simple / Advanced Mode Toggle -->\n <div class=\"datasource-header-bar\">\n <div class=\"group-title\">Options Data Source</div>\n <div class=\"mode-toggle-pill\">\n <button\n type=\"button\"\n class=\"pill-btn\"\n [class.active]=\"!isAdvancedMode()\"\n (click)=\"isAdvancedMode.set(false)\"\n title=\"Simplified view for business users with friendly names\"\n >\n Simple\n </button>\n <button\n type=\"button\"\n class=\"pill-btn\"\n [class.active]=\"isAdvancedMode()\"\n (click)=\"isAdvancedMode.set(true)\"\n title=\"Technical view with SQL types and expressions\"\n >\n Advanced\n </button>\n </div>\n </div>\n\n <!-- 2. Source Type Segmented Switcher -->\n <div class=\"source-type-switcher\">\n <button\n type=\"button\"\n class=\"source-tab-btn\"\n [class.active]=\"currentSourceType() === 'table'\"\n (click)=\"setSourceType('table')\"\n >\n <rpt-icon name=\"table\" [size]=\"12\" />\n <span>{{ isAdvancedMode() ? 'Table / View' : 'Database Table' }}</span>\n </button>\n <button\n type=\"button\"\n class=\"source-tab-btn\"\n [class.active]=\"currentSourceType() === 'procedure'\"\n (click)=\"setSourceType('procedure')\"\n >\n <rpt-icon name=\"code\" [size]=\"12\" />\n <span>{{ isAdvancedMode() ? 'Stored Proc' : 'Data Query' }}</span>\n </button>\n <button\n type=\"button\"\n class=\"source-tab-btn\"\n [class.active]=\"currentSourceType() === 'static'\"\n (click)=\"setSourceType('static')\"\n >\n <rpt-icon name=\"edit\" [size]=\"12\" />\n <span>Static List</span>\n </button>\n </div>\n\n <!-- 3A. DATABASE TABLE / VIEW CONFIGURATION -->\n @if (currentSourceType() === 'table') {\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Database Table / View' : 'Select Data Source' }}\n </label>\n <button\n type=\"button\"\n class=\"btn-refresh-entities\"\n (click)=\"loadEntities()\"\n title=\"Refresh available tables from backend\"\n >\n <rpt-icon name=\"refresh\" [size]=\"11\" />\n <span>Refresh</span>\n </button>\n </div>\n <select\n class=\"field-select\"\n [ngModel]=\"selectedEntity()\"\n (ngModelChange)=\"onEntitySelected($event)\"\n [disabled]=\"isLoadingEntities()\"\n >\n @if (isLoadingEntities()) {\n <option value=\"\">Loading tables from database...</option>\n } @else if (availableTables().length === 0) {\n <option value=\"\">-- No tables found (Click Refresh) --</option>\n } @else {\n <option value=\"\">-- Choose Data Table --</option>\n }\n @for (tbl of availableTables(); track tbl.name) {\n <option [value]=\"tbl.name\">\n @if (isAdvancedMode()) {\n {{ tbl.schema }}.{{ tbl.name }} ({{ tbl.type }})\n } @else {\n {{ tbl.displayName }}{{ tbl.category ? ' \u2022 ' + tbl.category : '' }}\n }\n </option>\n }\n </select>\n </div>\n\n @if (selectedEntity()) {\n <!-- Display Field (Visible text) -->\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Display Field (Visible Text)' : 'Text Shown in Dropdown' }}\n </label>\n @if (!isAdvancedMode()) {\n <span class=\"auto-badge\" title=\"Auto-detected column for text label\">Auto-detected</span>\n }\n </div>\n <select\n class=\"field-select\"\n [ngModel]=\"selectedDisplayField()\"\n (ngModelChange)=\"onDisplayFieldChanged($event)\"\n >\n <option value=\"\">-- Select Display Column --</option>\n @for (col of entityColumns(); track col.field) {\n <option [value]=\"col.field\">\n {{ col.label }} {{ isAdvancedMode() ? '(' + col.dataType + ')' : '' }}\n </option>\n }\n </select>\n </div>\n\n <!-- Value Field (Stored key) -->\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Value Field (Stored Key)' : 'Internal Value / Key' }}\n </label>\n @if (!isAdvancedMode()) {\n <span class=\"auto-badge\" title=\"Auto-detected column for primary ID\">Auto-detected</span>\n }\n </div>\n <select\n class=\"field-select\"\n [ngModel]=\"selectedValueField()\"\n (ngModelChange)=\"onValueFieldChanged($event)\"\n >\n <option value=\"\">-- Select Key Column --</option>\n @for (col of entityColumns(); track col.field) {\n <option [value]=\"col.field\">\n {{ col.label }} {{ isAdvancedMode() ? '(' + col.dataType + ')' : '' }}\n </option>\n }\n </select>\n </div>\n\n <!-- Cascading Dropdown Dependency -->\n <div class=\"field-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Dependent On (Cascading Parent Field)' : 'Filter by Another Field' }}\n </label>\n <select\n class=\"field-select\"\n [ngModel]=\"selectedDependsOn()\"\n (ngModelChange)=\"onDependsOnChanged($event)\"\n >\n <option value=\"\">-- None (Independent) --</option>\n @for (other of availableParentFields(); track other.key || other.id) {\n <option [value]=\"other.key || other.id\">{{ other.label || other.key }}</option>\n }\n </select>\n <span class=\"field-hint\">\n {{ isAdvancedMode() ? 'Automatically re-queries options when parent field changes.' : 'Only loads options when the selected field changes.' }}\n </span>\n </div>\n }\n }\n\n <!-- 3B. STORED PROCEDURE CONFIGURATION -->\n @if (currentSourceType() === 'procedure') {\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Stored Procedure' : 'Select Data Procedure' }}\n </label>\n <button\n type=\"button\"\n class=\"btn-refresh-entities\"\n (click)=\"loadEntities()\"\n title=\"Refresh available procedures from backend\"\n >\n <rpt-icon name=\"refresh\" [size]=\"11\" />\n <span>Refresh</span>\n </button>\n </div>\n <select\n class=\"field-select\"\n [ngModel]=\"selectedEntity()\"\n (ngModelChange)=\"onEntitySelected($event)\"\n [disabled]=\"isLoadingEntities()\"\n >\n @if (isLoadingEntities()) {\n <option value=\"\">Loading stored procedures from database...</option>\n } @else if (availableProcedures().length === 0) {\n <option value=\"\">-- No stored procedures found (Click Refresh) --</option>\n } @else {\n <option value=\"\">-- Choose Data Query / SP --</option>\n }\n @for (sp of availableProcedures(); track sp.name) {\n <option [value]=\"sp.name\">\n @if (isAdvancedMode()) {\n {{ sp.schema }}.{{ sp.name }}\n } @else {\n {{ sp.displayName }}{{ sp.category ? ' \u2022 ' + sp.category : '' }}\n }\n </option>\n }\n </select>\n </div>\n\n @if (selectedEntity()) {\n <!-- Visual Parameter Linkers -->\n @if (procedureParameters().length > 0) {\n <div class=\"sp-params-group\">\n <div class=\"params-header-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Procedure Input Parameters' : 'Filter Inputs & Parameters' }}\n </label>\n <button\n type=\"button\"\n class=\"btn-auto-params\"\n (click)=\"autoWireAllParameters()\"\n title=\"Auto-detect session parameters and add missing filter controls to the canvas\"\n >\n <rpt-icon name=\"check\" [size]=\"11\" />\n <span>Auto-Wire Canvas Inputs</span>\n </button>\n </div>\n\n @for (param of procedureParameters(); track param.name) {\n <rpt-parameter-binding-editor\n [param]=\"param\"\n [currentValue]=\"getParamValue(param.name)\"\n [allComponents]=\"allComponents()\"\n [currentComponentId]=\"comp().id\"\n [isAdvancedMode]=\"isAdvancedMode()\"\n (valueChange)=\"setParamValue(param.name, $event)\"\n (linkedFieldChange)=\"onParamLinkedFieldChange($event)\"\n />\n }\n </div>\n }\n\n <!-- Display Field (Visible text) -->\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Display Field (from SP result)' : 'Text Shown in Dropdown' }}\n </label>\n @if (!isAdvancedMode()) {\n <span class=\"auto-badge\">Auto-detected</span>\n }\n </div>\n <select\n class=\"field-select\"\n [ngModel]=\"selectedDisplayField()\"\n (ngModelChange)=\"onDisplayFieldChanged($event)\"\n >\n <option value=\"\">-- Select Display Column --</option>\n @for (col of entityColumns(); track col.field) {\n <option [value]=\"col.field\">\n {{ col.label }} {{ isAdvancedMode() ? '(' + col.dataType + ')' : '' }}\n </option>\n }\n </select>\n </div>\n\n <!-- Value Field (Stored key) -->\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Value Field (from SP result)' : 'Internal Value / Key' }}\n </label>\n @if (!isAdvancedMode()) {\n <span class=\"auto-badge\">Auto-detected</span>\n }\n </div>\n <select\n class=\"field-select\"\n [ngModel]=\"selectedValueField()\"\n (ngModelChange)=\"onValueFieldChanged($event)\"\n >\n <option value=\"\">-- Select Key Column --</option>\n @for (col of entityColumns(); track col.field) {\n <option [value]=\"col.field\">\n {{ col.label }} {{ isAdvancedMode() ? '(' + col.dataType + ')' : '' }}\n </option>\n }\n </select>\n </div>\n }\n }\n\n <!-- 3C. STATIC OPTIONS EDITOR -->\n @if (currentSourceType() === 'static') {\n <div class=\"static-options-wrapper\">\n <div class=\"static-options-header\">\n <span class=\"field-label\">Static Options</span>\n <button type=\"button\" class=\"btn-add-option\" (click)=\"addStaticOption()\">\n <rpt-icon name=\"plus\" [size]=\"11\" />\n <span>Add Option</span>\n </button>\n </div>\n\n <div class=\"static-options-list\">\n @for (opt of staticOptions(); track $index) {\n <div class=\"static-option-row\">\n <input\n type=\"text\"\n class=\"field-input opt-key\"\n placeholder=\"Key\"\n [ngModel]=\"opt.key\"\n (ngModelChange)=\"updateStaticOption($index, 'key', $event)\"\n />\n <input\n type=\"text\"\n class=\"field-input opt-label\"\n placeholder=\"Label\"\n [ngModel]=\"opt.label\"\n (ngModelChange)=\"updateStaticOption($index, 'label', $event)\"\n />\n <button\n type=\"button\"\n class=\"btn-remove-option\"\n (click)=\"removeStaticOption($index)\"\n title=\"Remove option\"\n >\n <rpt-icon name=\"trash\" [size]=\"12\" />\n </button>\n </div>\n }\n </div>\n </div>\n }\n </div>", styles: [".property-group{display:flex;flex-direction:column;gap:12px}.group-title{font-size:.7rem;font-weight:700;color:var(--rpt-color-text-muted, #64748b);text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--rpt-color-border, #e2e8f0);padding-bottom:4px}.field-row{display:flex;flex-direction:column;gap:4px}.field-label-row{display:flex;align-items:center;justify-content:space-between}.field-badge-val{font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border:1px solid #bfdbfe;padding:1px 6px;border-radius:9999px}.field-label{font-size:.7rem;font-weight:600;color:var(--rpt-color-text, #0f172a)}.field-input,.field-select,.field-textarea{width:100%;box-sizing:border-box;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius-sm, 4px);padding:6px 10px;font-size:var(--rpt-font-size-sm, .75rem);color:var(--rpt-color-text, #0f172a);background:#fff;outline:none;transition:all var(--rpt-transition-speed, .15s) ease}.field-select{appearance:none;-webkit-appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right 10px center;background-size:13px 13px;padding-right:28px;cursor:pointer}.field-input:focus,.field-select:focus,.field-textarea:focus{border-color:var(--rpt-color-primary, #2563eb);box-shadow:var(--rpt-box-shadow-focus, 0 0 0 3px rgba(37, 99, 235, .25))}.checkbox-toggle{display:inline-flex;align-items:center;gap:8px;font-size:.75rem;font-weight:500;color:var(--rpt-color-text, #0f172a);cursor:pointer}.preset-pill-group{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.preset-pill{background:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;padding:2px 8px;font-size:.65rem;font-weight:600;color:#475569;cursor:pointer;transition:all .12s ease}.preset-pill:hover{background:#e2e8f0;color:#0f172a;border-color:#cbd5e1}.preset-pill-active{background:var(--rpt-color-primary, #2563eb)!important;color:#fff!important;border-color:var(--rpt-color-primary, #2563eb)!important}.preset-pill-reset{color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border-color:#bfdbfe}.preset-pill-reset:hover{background:#dbeafe;color:#1d4ed8;border-color:#93c5fd}\n", ".datasource-header-bar{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.mode-toggle-pill{display:inline-flex;background:#e2e8f0;border-radius:12px;padding:2px;gap:2px}.pill-btn{border:none;background:transparent;padding:2px 8px;font-size:.65rem;font-weight:500;color:#64748b;border-radius:10px;cursor:pointer;transition:all .12s ease}.pill-btn:hover{color:#0f172a}.pill-btn.active{background:#fff;color:var(--rpt-color-primary, #2563eb);font-weight:700;box-shadow:0 1px 2px #00000014}.source-type-switcher{display:flex;background:#f1f5f9;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:6px;padding:2px;gap:2px;margin-bottom:6px}.source-tab-btn{flex:1;display:inline-flex;align-items:center;justify-content:center;gap:5px;padding:6px 4px;font-size:.7rem;font-weight:600;color:var(--rpt-color-text-muted, #64748b);background:transparent;border:none;border-radius:4px;cursor:pointer;transition:all .12s ease}.source-tab-btn:hover{color:var(--rpt-color-text, #0f172a)}.source-tab-btn.active{background:#fff;color:var(--rpt-color-primary, #2563eb);box-shadow:0 1px 2px #0000000d}.btn-refresh-entities{display:inline-flex;align-items:center;gap:4px;border:none;background:transparent;color:var(--rpt-color-primary, #2563eb);font-size:.65rem;font-weight:600;cursor:pointer;padding:0}.btn-refresh-entities:hover{text-decoration:underline}.btn-auto-params{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;font-size:.65rem;font-weight:600;color:#2563eb;background:#eff6ff;border:1px solid #bfdbfe;border-radius:4px;cursor:pointer;transition:all .12s ease}.btn-auto-params:hover{background:#2563eb;color:#fff}.auto-badge{font-size:.62rem;font-weight:600;background:#dcfce7;color:#166534;padding:1px 5px;border-radius:4px}.field-hint{font-size:.65rem;color:var(--rpt-color-text-muted, #94a3b8);margin-top:2px}.sp-params-group{display:flex;flex-direction:column;gap:6px;background:#f8fafc;padding:8px;border-radius:6px;border:1px solid #e2e8f0;margin-bottom:6px}.params-header-row{display:flex;align-items:center;justify-content:space-between}.params-hint{font-size:.64rem;color:#64748b;font-style:italic}.static-options-wrapper{display:flex;flex-direction:column;gap:8px}.static-options-header{display:flex;align-items:center;justify-content:space-between}.btn-add-option{display:inline-flex;align-items:center;gap:4px;border:1px solid var(--rpt-color-border, #cbd5e1);background:#fff;border-radius:4px;padding:3px 8px;font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);cursor:pointer;transition:all .12s ease}.btn-add-option:hover{background:#eff6ff}.static-options-list{display:flex;flex-direction:column;gap:6px}.static-option-row{display:flex;align-items:center;gap:6px}.opt-key{flex:1}.opt-label{flex:1.5}.btn-remove-option{width:24px;height:24px;border:none;background:transparent;color:#94a3b8;border-radius:4px;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;flex-shrink:0;padding:0;transition:all .12s ease}.btn-remove-option:hover{background:#fee2e2;color:#ef4444}\n"] }]
|
|
11852
|
+
args: [{ selector: 'rpt-property-editor-datasource', standalone: true, imports: [CommonModule, FormsModule, RptIconComponent, ParameterBindingEditorComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: " <div class=\"property-group\">\n <!-- 1. Header with Simple / Advanced Mode Toggle -->\n <div class=\"datasource-header-bar\">\n <div class=\"group-title\">Options Data Source</div>\n <div class=\"mode-toggle-pill\">\n <button\n type=\"button\"\n class=\"pill-btn\"\n [class.active]=\"!isAdvancedMode()\"\n (click)=\"isAdvancedMode.set(false)\"\n title=\"Simplified view for business users with friendly names\"\n >\n Simple\n </button>\n <button\n type=\"button\"\n class=\"pill-btn\"\n [class.active]=\"isAdvancedMode()\"\n (click)=\"isAdvancedMode.set(true)\"\n title=\"Technical view with SQL types and expressions\"\n >\n Advanced\n </button>\n </div>\n </div>\n\n <div class=\"datasource-guidance-card\">\n <rpt-icon name=\"info\" [size]=\"12\" />\n <span>Populates selectable choices in this dropdown filter. For the report's main data and result grid, use the <strong>Grid</strong> tab.</span>\n </div>\n\n <!-- 2. Source Type Segmented Switcher -->\n <div class=\"source-type-switcher\">\n <button\n type=\"button\"\n class=\"source-tab-btn\"\n [class.active]=\"currentSourceType() === 'table'\"\n (click)=\"setSourceType('table')\"\n >\n <rpt-icon name=\"table\" [size]=\"12\" />\n <span>{{ isAdvancedMode() ? 'Table / View' : 'Database Table' }}</span>\n </button>\n <button\n type=\"button\"\n class=\"source-tab-btn\"\n [class.active]=\"currentSourceType() === 'procedure'\"\n (click)=\"setSourceType('procedure')\"\n >\n <rpt-icon name=\"code\" [size]=\"12\" />\n <span>{{ isAdvancedMode() ? 'Stored Proc' : 'Data Query' }}</span>\n </button>\n <button\n type=\"button\"\n class=\"source-tab-btn\"\n [class.active]=\"currentSourceType() === 'static'\"\n (click)=\"setSourceType('static')\"\n >\n <rpt-icon name=\"edit\" [size]=\"12\" />\n <span>Static List</span>\n </button>\n </div>\n\n <!-- 3A. DATABASE TABLE / VIEW CONFIGURATION -->\n @if (currentSourceType() === 'table') {\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Database Table / View' : 'Select Data Source' }}\n </label>\n <button\n type=\"button\"\n class=\"btn-refresh-entities\"\n (click)=\"loadEntities()\"\n title=\"Refresh available tables from backend\"\n >\n <rpt-icon name=\"refresh\" [size]=\"11\" />\n <span>Refresh</span>\n </button>\n </div>\n <select\n class=\"field-select\"\n [ngModel]=\"selectedEntity()\"\n (ngModelChange)=\"onEntitySelected($event)\"\n [disabled]=\"isLoadingEntities()\"\n >\n @if (isLoadingEntities()) {\n <option value=\"\">Loading tables from database...</option>\n } @else if (availableTables().length === 0) {\n <option value=\"\">-- No tables found (Click Refresh) --</option>\n } @else {\n <option value=\"\">-- Choose Data Table --</option>\n }\n @for (tbl of availableTables(); track tbl.name) {\n <option [value]=\"tbl.name\">\n @if (isAdvancedMode()) {\n {{ tbl.schema }}.{{ tbl.name }} ({{ tbl.type }})\n } @else {\n {{ tbl.displayName }}{{ tbl.category ? ' \u2022 ' + tbl.category : '' }}\n }\n </option>\n }\n </select>\n </div>\n\n @if (selectedEntity()) {\n <!-- Display Field (Visible text) -->\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Display Field (Visible Text)' : 'Text Shown in Dropdown' }}\n </label>\n @if (!isAdvancedMode()) {\n <span class=\"auto-badge\" title=\"Auto-detected column for text label\">Auto-detected</span>\n }\n </div>\n <select\n class=\"field-select\"\n [ngModel]=\"selectedDisplayField()\"\n (ngModelChange)=\"onDisplayFieldChanged($event)\"\n >\n <option value=\"\">-- Select Display Column --</option>\n @for (col of entityColumns(); track col.field) {\n <option [value]=\"col.field\">\n {{ col.label }} {{ isAdvancedMode() ? '(' + col.dataType + ')' : '' }}\n </option>\n }\n </select>\n </div>\n\n <!-- Value Field (Stored key) -->\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Value Field (Stored Key)' : 'Internal Value / Key' }}\n </label>\n @if (!isAdvancedMode()) {\n <span class=\"auto-badge\" title=\"Auto-detected column for primary ID\">Auto-detected</span>\n }\n </div>\n <select\n class=\"field-select\"\n [ngModel]=\"selectedValueField()\"\n (ngModelChange)=\"onValueFieldChanged($event)\"\n >\n <option value=\"\">-- Select Key Column --</option>\n @for (col of entityColumns(); track col.field) {\n <option [value]=\"col.field\">\n {{ col.label }} {{ isAdvancedMode() ? '(' + col.dataType + ')' : '' }}\n </option>\n }\n </select>\n </div>\n\n <!-- Cascading Dropdown Dependency -->\n <div class=\"field-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Dependent On (Cascading Parent Field)' : 'Filter by Another Field' }}\n </label>\n <select\n class=\"field-select\"\n [ngModel]=\"selectedDependsOn()\"\n (ngModelChange)=\"onDependsOnChanged($event)\"\n >\n <option value=\"\">-- None (Independent) --</option>\n @for (other of availableParentFields(); track other.key || other.id) {\n <option [value]=\"other.key || other.id\">{{ other.label || other.key }}</option>\n }\n </select>\n <span class=\"field-hint\">\n {{ isAdvancedMode() ? 'Automatically re-queries options when parent field changes.' : 'Only loads options when the selected field changes.' }}\n </span>\n </div>\n }\n }\n\n <!-- 3B. STORED PROCEDURE CONFIGURATION -->\n @if (currentSourceType() === 'procedure') {\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Stored Procedure' : 'Select Data Procedure' }}\n </label>\n <button\n type=\"button\"\n class=\"btn-refresh-entities\"\n (click)=\"loadEntities()\"\n title=\"Refresh available procedures from backend\"\n >\n <rpt-icon name=\"refresh\" [size]=\"11\" />\n <span>Refresh</span>\n </button>\n </div>\n <select\n class=\"field-select\"\n [ngModel]=\"selectedEntity()\"\n (ngModelChange)=\"onEntitySelected($event)\"\n [disabled]=\"isLoadingEntities()\"\n >\n @if (isLoadingEntities()) {\n <option value=\"\">Loading stored procedures from database...</option>\n } @else if (availableProcedures().length === 0) {\n <option value=\"\">-- No stored procedures found (Click Refresh) --</option>\n } @else {\n <option value=\"\">-- Choose Data Query / SP --</option>\n }\n @for (sp of availableProcedures(); track sp.name) {\n <option [value]=\"sp.name\">\n @if (isAdvancedMode()) {\n {{ sp.schema }}.{{ sp.name }}\n } @else {\n {{ sp.displayName }}{{ sp.category ? ' \u2022 ' + sp.category : '' }}\n }\n </option>\n }\n </select>\n </div>\n\n @if (selectedEntity()) {\n <!-- Visual Parameter Linkers -->\n @if (procedureParameters().length > 0) {\n <div class=\"sp-params-group\">\n <div class=\"params-header-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Procedure Input Parameters' : 'Filter Inputs & Parameters' }}\n </label>\n <button\n type=\"button\"\n class=\"btn-auto-params\"\n (click)=\"autoWireAllParameters()\"\n title=\"Auto-detect session parameters and add missing filter controls to the canvas\"\n >\n <rpt-icon name=\"check\" [size]=\"11\" />\n <span>Auto-Wire Canvas Inputs</span>\n </button>\n </div>\n\n @for (param of procedureParameters(); track param.name) {\n <rpt-parameter-binding-editor\n [param]=\"param\"\n [currentValue]=\"getParamValue(param.name)\"\n [allComponents]=\"allComponents()\"\n [currentComponentId]=\"comp().id\"\n [isAdvancedMode]=\"isAdvancedMode()\"\n (valueChange)=\"setParamValue(param.name, $event)\"\n (linkedFieldChange)=\"onParamLinkedFieldChange($event)\"\n />\n }\n </div>\n }\n\n <!-- Display Field (Visible text) -->\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Display Field (from SP result)' : 'Text Shown in Dropdown' }}\n </label>\n @if (!isAdvancedMode()) {\n <span class=\"auto-badge\">Auto-detected</span>\n }\n </div>\n @if (entityColumns().length > 0) {\n <select\n class=\"field-select\"\n [ngModel]=\"selectedDisplayField()\"\n (ngModelChange)=\"onDisplayFieldChanged($event)\"\n >\n <option value=\"\">-- Select Display Column --</option>\n @for (col of entityColumns(); track col.field) {\n <option [value]=\"col.field\">\n {{ col.label }} {{ isAdvancedMode() ? '(' + col.dataType + ')' : '' }}\n </option>\n }\n </select>\n } @else {\n <input\n type=\"text\"\n class=\"field-input\"\n placeholder=\"Column name (leave blank for auto)\"\n [ngModel]=\"selectedDisplayField()\"\n (ngModelChange)=\"onDisplayFieldChanged($event)\"\n />\n }\n </div>\n\n <!-- Value Field (Stored key) -->\n <div class=\"field-row\">\n <div class=\"field-label-row\">\n <label class=\"field-label\">\n {{ isAdvancedMode() ? 'Value Field (from SP result)' : 'Internal Value / Key' }}\n </label>\n @if (!isAdvancedMode()) {\n <span class=\"auto-badge\">Auto-detected</span>\n }\n </div>\n @if (entityColumns().length > 0) {\n <select\n class=\"field-select\"\n [ngModel]=\"selectedValueField()\"\n (ngModelChange)=\"onValueFieldChanged($event)\"\n >\n <option value=\"\">-- Select Key Column --</option>\n @for (col of entityColumns(); track col.field) {\n <option [value]=\"col.field\">\n {{ col.label }} {{ isAdvancedMode() ? '(' + col.dataType + ')' : '' }}\n </option>\n }\n </select>\n } @else {\n <input\n type=\"text\"\n class=\"field-input\"\n placeholder=\"Column name (leave blank for auto)\"\n [ngModel]=\"selectedValueField()\"\n (ngModelChange)=\"onValueFieldChanged($event)\"\n />\n }\n </div>\n }\n }\n\n <!-- 3C. STATIC OPTIONS EDITOR -->\n @if (currentSourceType() === 'static') {\n <div class=\"static-options-wrapper\">\n <div class=\"static-options-header\">\n <span class=\"field-label\">Static Options</span>\n <button type=\"button\" class=\"btn-add-option\" (click)=\"addStaticOption()\">\n <rpt-icon name=\"plus\" [size]=\"11\" />\n <span>Add Option</span>\n </button>\n </div>\n\n <div class=\"static-options-list\">\n @for (opt of staticOptions(); track $index) {\n <div class=\"static-option-row\">\n <input\n type=\"text\"\n class=\"field-input opt-key\"\n placeholder=\"Key\"\n [ngModel]=\"opt.key\"\n (ngModelChange)=\"updateStaticOption($index, 'key', $event)\"\n />\n <input\n type=\"text\"\n class=\"field-input opt-label\"\n placeholder=\"Label\"\n [ngModel]=\"opt.label\"\n (ngModelChange)=\"updateStaticOption($index, 'label', $event)\"\n />\n <button\n type=\"button\"\n class=\"btn-remove-option\"\n (click)=\"removeStaticOption($index)\"\n title=\"Remove option\"\n >\n <rpt-icon name=\"trash\" [size]=\"12\" />\n </button>\n </div>\n }\n </div>\n </div>\n }\n </div>", styles: [".property-group{display:flex;flex-direction:column;gap:12px}.group-title{font-size:.7rem;font-weight:700;color:var(--rpt-color-text-muted, #64748b);text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--rpt-color-border, #e2e8f0);padding-bottom:4px}.field-row{display:flex;flex-direction:column;gap:4px}.field-label-row{display:flex;align-items:center;justify-content:space-between}.field-badge-val{font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border:1px solid #bfdbfe;padding:1px 6px;border-radius:9999px}.field-label{font-size:.7rem;font-weight:600;color:var(--rpt-color-text, #0f172a)}.field-input,.field-select,.field-textarea{width:100%;box-sizing:border-box;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius-sm, 4px);padding:6px 10px;font-size:var(--rpt-font-size-sm, .75rem);color:var(--rpt-color-text, #0f172a);background:#fff;outline:none;transition:all var(--rpt-transition-speed, .15s) ease}.field-select{appearance:none;-webkit-appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right 10px center;background-size:13px 13px;padding-right:28px;cursor:pointer}.field-input:focus,.field-select:focus,.field-textarea:focus{border-color:var(--rpt-color-primary, #2563eb);box-shadow:var(--rpt-box-shadow-focus, 0 0 0 3px rgba(37, 99, 235, .25))}.checkbox-toggle{display:inline-flex;align-items:center;gap:8px;font-size:.75rem;font-weight:500;color:var(--rpt-color-text, #0f172a);cursor:pointer}.preset-pill-group{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.preset-pill{background:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;padding:2px 8px;font-size:.65rem;font-weight:600;color:#475569;cursor:pointer;transition:all .12s ease}.preset-pill:hover{background:#e2e8f0;color:#0f172a;border-color:#cbd5e1}.preset-pill-active{background:var(--rpt-color-primary, #2563eb)!important;color:#fff!important;border-color:var(--rpt-color-primary, #2563eb)!important}.preset-pill-reset{color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border-color:#bfdbfe}.preset-pill-reset:hover{background:#dbeafe;color:#1d4ed8;border-color:#93c5fd}\n", ".datasource-header-bar{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.mode-toggle-pill{display:inline-flex;background:#e2e8f0;border-radius:12px;padding:2px;gap:2px}.pill-btn{border:none;background:transparent;padding:2px 8px;font-size:.65rem;font-weight:500;color:#64748b;border-radius:10px;cursor:pointer;transition:all .12s ease}.pill-btn:hover{color:#0f172a}.pill-btn.active{background:#fff;color:var(--rpt-color-primary, #2563eb);font-weight:700;box-shadow:0 1px 2px #00000014}.datasource-guidance-card{display:flex;align-items:center;gap:6px;background:#eff6ff;border:1px solid #bfdbfe;color:#1e40af;font-size:.675rem;padding:6px 8px;border-radius:6px;margin-bottom:8px;line-height:1.35}.source-type-switcher{display:flex;background:#f1f5f9;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:6px;padding:2px;gap:2px;margin-bottom:6px}.source-tab-btn{flex:1;display:inline-flex;align-items:center;justify-content:center;gap:5px;padding:6px 4px;font-size:.7rem;font-weight:600;color:var(--rpt-color-text-muted, #64748b);background:transparent;border:none;border-radius:4px;cursor:pointer;transition:all .12s ease}.source-tab-btn:hover{color:var(--rpt-color-text, #0f172a)}.source-tab-btn.active{background:#fff;color:var(--rpt-color-primary, #2563eb);box-shadow:0 1px 2px #0000000d}.btn-refresh-entities{display:inline-flex;align-items:center;gap:4px;border:none;background:transparent;color:var(--rpt-color-primary, #2563eb);font-size:.65rem;font-weight:600;cursor:pointer;padding:0}.btn-refresh-entities:hover{text-decoration:underline}.btn-auto-params{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;font-size:.65rem;font-weight:600;color:#2563eb;background:#eff6ff;border:1px solid #bfdbfe;border-radius:4px;cursor:pointer;transition:all .12s ease}.btn-auto-params:hover{background:#2563eb;color:#fff}.auto-badge{font-size:.62rem;font-weight:600;background:#dcfce7;color:#166534;padding:1px 5px;border-radius:4px}.field-hint{font-size:.65rem;color:var(--rpt-color-text-muted, #94a3b8);margin-top:2px}.sp-params-group{display:flex;flex-direction:column;gap:6px;background:#f8fafc;padding:8px;border-radius:6px;border:1px solid #e2e8f0;margin-bottom:6px}.params-header-row{display:flex;align-items:center;justify-content:space-between}.params-hint{font-size:.64rem;color:#64748b;font-style:italic}.static-options-wrapper{display:flex;flex-direction:column;gap:8px}.static-options-header{display:flex;align-items:center;justify-content:space-between}.btn-add-option{display:inline-flex;align-items:center;gap:4px;border:1px solid var(--rpt-color-border, #cbd5e1);background:#fff;border-radius:4px;padding:3px 8px;font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);cursor:pointer;transition:all .12s ease}.btn-add-option:hover{background:#eff6ff}.static-options-list{display:flex;flex-direction:column;gap:6px}.static-option-row{display:flex;align-items:center;gap:6px}.opt-key{flex:1}.opt-label{flex:1.5}.btn-remove-option{width:24px;height:24px;border:none;background:transparent;color:#94a3b8;border-radius:4px;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;flex-shrink:0;padding:0;transition:all .12s ease}.btn-remove-option:hover{background:#fee2e2;color:#ef4444}.field-input{width:100%;padding:6px 8px;font-size:.75rem;border:1px solid var(--rpt-color-border, #cbd5e1);border-radius:var(--rpt-border-radius, 6px);background:var(--rpt-color-surface, #ffffff);color:var(--rpt-color-text, #0f172a);box-sizing:border-box;outline:none;transition:border-color .12s ease}.field-input:focus{border-color:var(--rpt-color-primary, #2563eb)}\n"] }]
|
|
11220
11853
|
}], ctorParameters: () => [] });
|
|
11221
11854
|
|
|
11222
11855
|
/**
|
|
@@ -11227,6 +11860,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
11227
11860
|
class PropertyEditorActionComponent {
|
|
11228
11861
|
designerState = inject(DesignerStateService, { optional: true });
|
|
11229
11862
|
discoveryAdapter = inject(ReportSchemaDiscoveryAdapter, { optional: true });
|
|
11863
|
+
lookupAdapter = inject(ReportLookupAdapter, { optional: true });
|
|
11864
|
+
sessionService = inject(ReportSessionContextService, { optional: true });
|
|
11865
|
+
dataDiscovery = inject(ReportDataDiscoveryService, { optional: true });
|
|
11230
11866
|
comp = input.required();
|
|
11231
11867
|
updateProperty = output();
|
|
11232
11868
|
actionOptions = [
|
|
@@ -11317,42 +11953,44 @@ class PropertyEditorActionComponent {
|
|
|
11317
11953
|
}
|
|
11318
11954
|
}
|
|
11319
11955
|
const dsId = targetDs?.id || entityName || 'default';
|
|
11320
|
-
|
|
11321
|
-
|
|
11956
|
+
const mappings = targetDs?.parameterMappings || this.comp().dataSource?.parameterMappings || def.components.find((c) => c.dataSource?.entityName === entityName)?.dataSource?.parameterMappings;
|
|
11957
|
+
const applyGridCols = (cols) => {
|
|
11958
|
+
const newGrid = {
|
|
11959
|
+
dataSourceId: dsId,
|
|
11960
|
+
columns: cols.length > 0 ? cols : [
|
|
11961
|
+
{ field: 'id', label: 'ID', dataType: 'number', visible: true, sortable: true },
|
|
11962
|
+
{ field: 'name', label: 'Name', dataType: 'string', visible: true, sortable: true },
|
|
11963
|
+
],
|
|
11964
|
+
options: {
|
|
11965
|
+
pagination: true,
|
|
11966
|
+
pageSize: 25,
|
|
11967
|
+
showRowNumbers: true,
|
|
11968
|
+
showFooter: true,
|
|
11969
|
+
allowColumnReorder: true,
|
|
11970
|
+
allowExport: true,
|
|
11971
|
+
},
|
|
11972
|
+
};
|
|
11973
|
+
this.designerState?.updateGridSettings(newGrid);
|
|
11974
|
+
};
|
|
11975
|
+
if (entityName && this.dataDiscovery) {
|
|
11976
|
+
this.dataDiscovery
|
|
11977
|
+
.autoDetectColumns(entityName, entityType, mappings || {}, targetDs?.schema || 'dbo')
|
|
11978
|
+
.subscribe({
|
|
11322
11979
|
next: (cols) => {
|
|
11323
|
-
|
|
11324
|
-
|
|
11325
|
-
|
|
11326
|
-
|
|
11327
|
-
|
|
11328
|
-
|
|
11329
|
-
filterable: true,
|
|
11330
|
-
}));
|
|
11331
|
-
const newGrid = {
|
|
11332
|
-
dataSourceId: dsId,
|
|
11333
|
-
columns: gridCols.length > 0 ? gridCols : [
|
|
11334
|
-
{ field: 'id', label: 'ID', dataType: 'number', visible: true, sortable: true },
|
|
11335
|
-
{ field: 'name', label: 'Name', dataType: 'string', visible: true, sortable: true },
|
|
11336
|
-
],
|
|
11337
|
-
options: {
|
|
11338
|
-
pagination: true,
|
|
11339
|
-
pageSize: 25,
|
|
11340
|
-
showRowNumbers: true,
|
|
11341
|
-
showFooter: true,
|
|
11342
|
-
allowColumnReorder: true,
|
|
11343
|
-
allowExport: true,
|
|
11344
|
-
},
|
|
11345
|
-
};
|
|
11346
|
-
this.designerState?.updateGridSettings(newGrid);
|
|
11980
|
+
if (cols && cols.length > 0) {
|
|
11981
|
+
applyGridCols(cols);
|
|
11982
|
+
}
|
|
11983
|
+
else {
|
|
11984
|
+
this.fallbackGridCreation(dsId);
|
|
11985
|
+
}
|
|
11347
11986
|
},
|
|
11348
|
-
error: (
|
|
11349
|
-
|
|
11350
|
-
this.fallbackGridCreation(entityName);
|
|
11987
|
+
error: () => {
|
|
11988
|
+
this.fallbackGridCreation(dsId);
|
|
11351
11989
|
},
|
|
11352
11990
|
});
|
|
11353
11991
|
}
|
|
11354
11992
|
else {
|
|
11355
|
-
this.fallbackGridCreation(
|
|
11993
|
+
this.fallbackGridCreation(dsId);
|
|
11356
11994
|
}
|
|
11357
11995
|
}
|
|
11358
11996
|
fallbackGridCreation(dsId) {
|
|
@@ -11813,12 +12451,42 @@ class PropertyEditorComponent {
|
|
|
11813
12451
|
designerState = inject(DesignerStateService);
|
|
11814
12452
|
registry = inject(ComponentRegistry);
|
|
11815
12453
|
discoveryAdapter = inject(ReportSchemaDiscoveryAdapter, { optional: true });
|
|
12454
|
+
lookupAdapter = inject(ReportLookupAdapter, { optional: true });
|
|
12455
|
+
sessionService = inject(ReportSessionContextService, { optional: true });
|
|
12456
|
+
dataDiscovery = inject(ReportDataDiscoveryService);
|
|
12457
|
+
isDetectingColumns = signal(false);
|
|
12458
|
+
gridDetectionStatus = signal(null);
|
|
12459
|
+
availableEntities = signal([]);
|
|
12460
|
+
gridProcedureParameters = signal([]);
|
|
11816
12461
|
definition = this.designerState.definition;
|
|
11817
12462
|
activeTab = this.designerState.activeTab;
|
|
11818
12463
|
selectedComponent = this.designerState.selectedComponent;
|
|
11819
12464
|
collapseRequested = output();
|
|
11820
12465
|
maxColumns = computed(() => this.definition().layout.columns || 12);
|
|
11821
12466
|
allComponents = computed(() => this.definition().components);
|
|
12467
|
+
gridDataSource = computed(() => {
|
|
12468
|
+
const def = this.definition();
|
|
12469
|
+
const dsId = def.grid?.dataSourceId;
|
|
12470
|
+
if (!dsId)
|
|
12471
|
+
return def.dataSources?.[0];
|
|
12472
|
+
return def.dataSources?.find((d) => d.id === dsId || d.name === dsId) ?? def.dataSources?.[0];
|
|
12473
|
+
});
|
|
12474
|
+
constructor() {
|
|
12475
|
+
this.dataDiscovery.getEntities().subscribe((entities) => {
|
|
12476
|
+
this.availableEntities.set(entities);
|
|
12477
|
+
});
|
|
12478
|
+
effect(() => {
|
|
12479
|
+
const ds = this.gridDataSource();
|
|
12480
|
+
if (ds && ds.type === 'stored_procedure' && ds.name) {
|
|
12481
|
+
this.dataDiscovery.getParameters(ds.name, ds.schema || 'dbo').subscribe((params) => {
|
|
12482
|
+
this.gridProcedureParameters.set(params);
|
|
12483
|
+
});
|
|
12484
|
+
}
|
|
12485
|
+
else {
|
|
12486
|
+
this.gridProcedureParameters.set([]);
|
|
12487
|
+
}
|
|
12488
|
+
}, { allowSignalWrites: true });
|
|
12489
|
+
}
|
|
11822
12490
|
propertySchema = computed(() => {
|
|
11823
12491
|
const comp = this.selectedComponent();
|
|
11824
12492
|
if (!comp)
|
|
@@ -11978,53 +12646,94 @@ class PropertyEditorComponent {
|
|
|
11978
12646
|
rowClickAction: action,
|
|
11979
12647
|
});
|
|
11980
12648
|
}
|
|
11981
|
-
|
|
11982
|
-
|
|
11983
|
-
|
|
11984
|
-
const
|
|
11985
|
-
|
|
11986
|
-
|
|
11987
|
-
|
|
11988
|
-
|
|
11989
|
-
|
|
11990
|
-
|
|
11991
|
-
|
|
11992
|
-
|
|
12649
|
+
onGridEntityChanged(event) {
|
|
12650
|
+
const isTable = event.type === 'table';
|
|
12651
|
+
const dsId = `ds-${event.name}`;
|
|
12652
|
+
const newDs = {
|
|
12653
|
+
id: dsId,
|
|
12654
|
+
name: event.name,
|
|
12655
|
+
endpoint: event.name,
|
|
12656
|
+
schema: 'dbo',
|
|
12657
|
+
type: isTable ? 'table' : 'stored_procedure',
|
|
12658
|
+
method: isTable ? 'GET' : 'STORED_PROCEDURE',
|
|
12659
|
+
parameterMappings: {},
|
|
12660
|
+
};
|
|
12661
|
+
if (!isTable) {
|
|
12662
|
+
this.dataDiscovery.getParameters(event.name, 'dbo').subscribe({
|
|
12663
|
+
next: (params) => this.gridProcedureParameters.set(params || []),
|
|
12664
|
+
error: () => this.gridProcedureParameters.set([]),
|
|
12665
|
+
});
|
|
11993
12666
|
}
|
|
11994
12667
|
else {
|
|
12668
|
+
this.gridProcedureParameters.set([]);
|
|
12669
|
+
}
|
|
12670
|
+
this.designerState.setGridDataSource(newDs);
|
|
12671
|
+
}
|
|
12672
|
+
onGridDataSourceParametersChanged(mappings) {
|
|
12673
|
+
this.designerState.updateGridDataSourceParameters(mappings);
|
|
12674
|
+
}
|
|
12675
|
+
autoDetectGridColumns() {
|
|
12676
|
+
const currentGrid = this.definition().grid;
|
|
12677
|
+
const gridDs = this.gridDataSource();
|
|
12678
|
+
let targetEntity = gridDs?.endpoint || gridDs?.name || '';
|
|
12679
|
+
let targetType = gridDs?.type === 'table' ? 'table' : 'stored_procedure';
|
|
12680
|
+
let targetMappings = gridDs?.parameterMappings || {};
|
|
12681
|
+
let schema = gridDs?.schema || 'dbo';
|
|
12682
|
+
if (!targetEntity) {
|
|
11995
12683
|
const compWithEntity = this.definition().components.find((c) => c.dataSource?.entityName);
|
|
11996
12684
|
if (compWithEntity?.dataSource?.entityName) {
|
|
11997
12685
|
targetEntity = compWithEntity.dataSource.entityName;
|
|
12686
|
+
targetMappings = compWithEntity.dataSource.parameterMappings || {};
|
|
11998
12687
|
if (compWithEntity.dataSource.sourceType === 'table') {
|
|
11999
12688
|
targetType = 'table';
|
|
12000
12689
|
}
|
|
12690
|
+
schema = compWithEntity.dataSource.schema || 'dbo';
|
|
12001
12691
|
}
|
|
12002
12692
|
}
|
|
12003
12693
|
if (!targetEntity) {
|
|
12004
|
-
|
|
12694
|
+
this.gridDetectionStatus.set({
|
|
12695
|
+
message: 'No data source configured. Choose a Stored Procedure or Table in the Grid Data Source section above.',
|
|
12696
|
+
kind: 'error',
|
|
12697
|
+
});
|
|
12005
12698
|
return;
|
|
12006
12699
|
}
|
|
12007
|
-
this.
|
|
12008
|
-
|
|
12009
|
-
|
|
12010
|
-
|
|
12011
|
-
|
|
12012
|
-
|
|
12013
|
-
|
|
12014
|
-
|
|
12015
|
-
|
|
12016
|
-
|
|
12017
|
-
|
|
12018
|
-
|
|
12019
|
-
|
|
12020
|
-
|
|
12021
|
-
|
|
12022
|
-
|
|
12023
|
-
|
|
12024
|
-
|
|
12700
|
+
this.isDetectingColumns.set(true);
|
|
12701
|
+
this.gridDetectionStatus.set({
|
|
12702
|
+
message: `Detecting columns for ${targetEntity}...`,
|
|
12703
|
+
kind: 'info',
|
|
12704
|
+
});
|
|
12705
|
+
this.dataDiscovery.autoDetectColumns(targetEntity, targetType, targetMappings, schema).subscribe({
|
|
12706
|
+
next: (mappedCols) => {
|
|
12707
|
+
if (mappedCols && mappedCols.length > 0) {
|
|
12708
|
+
this.designerState.updateGridSettings({
|
|
12709
|
+
...(currentGrid ?? {
|
|
12710
|
+
dataSourceId: gridDs?.id || `ds-${targetEntity}`,
|
|
12711
|
+
options: { pagination: true, pageSize: 25, showRowNumbers: true, showFooter: true },
|
|
12712
|
+
}),
|
|
12713
|
+
columns: mappedCols,
|
|
12714
|
+
});
|
|
12715
|
+
this.isDetectingColumns.set(false);
|
|
12716
|
+
this.gridDetectionStatus.set({
|
|
12717
|
+
message: `Successfully detected ${mappedCols.length} columns from ${targetEntity}!`,
|
|
12718
|
+
kind: 'success',
|
|
12719
|
+
});
|
|
12720
|
+
setTimeout(() => this.gridDetectionStatus.set(null), 4000);
|
|
12721
|
+
}
|
|
12722
|
+
else {
|
|
12723
|
+
this.isDetectingColumns.set(false);
|
|
12724
|
+
this.gridDetectionStatus.set({
|
|
12725
|
+
message: `No columns detected for ${targetEntity}. Stored procedure returned 0 rows. Verify parameter values or add columns manually.`,
|
|
12726
|
+
kind: 'error',
|
|
12727
|
+
});
|
|
12728
|
+
}
|
|
12729
|
+
},
|
|
12730
|
+
error: (err) => {
|
|
12731
|
+
this.isDetectingColumns.set(false);
|
|
12732
|
+
this.gridDetectionStatus.set({
|
|
12733
|
+
message: `Failed to detect columns: ${err?.message || 'Database error'}. You can add columns manually.`,
|
|
12734
|
+
kind: 'error',
|
|
12025
12735
|
});
|
|
12026
12736
|
},
|
|
12027
|
-
error: (err) => console.error('[PropertyEditor] Auto-detect columns failed:', err),
|
|
12028
12737
|
});
|
|
12029
12738
|
}
|
|
12030
12739
|
expandedSections = signal({
|
|
@@ -12084,7 +12793,7 @@ class PropertyEditorComponent {
|
|
|
12084
12793
|
return comp.validation?.required ? 'Required' : 'Optional';
|
|
12085
12794
|
}
|
|
12086
12795
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PropertyEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
12087
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: PropertyEditorComponent, isStandalone: true, selector: "rpt-property-editor", outputs: { collapseRequested: "collapseRequested" }, ngImport: i0, template: " <div class=\"editor-container\">\n <!-- Editor Header & Tabs -->\n <div class=\"editor-tabs\">\n <button\n type=\"button\"\n class=\"tab-btn\"\n [class.active]=\"activeTab() === 'properties'\"\n (click)=\"setTab('properties')\"\n [disabled]=\"!selectedComponent()\"\n >\n <rpt-icon name=\"edit\" [size]=\"13\" />\n <span>Properties</span>\n </button>\n <button\n type=\"button\"\n class=\"tab-btn\"\n [class.active]=\"activeTab() === 'report'\"\n (click)=\"setTab('report')\"\n >\n <rpt-icon name=\"design\" [size]=\"13\" />\n <span>Layout</span>\n </button>\n <button\n type=\"button\"\n class=\"tab-btn\"\n [class.active]=\"activeTab() === 'grid'\"\n (click)=\"setTab('grid')\"\n >\n <rpt-icon name=\"table\" [size]=\"13\" />\n <span>Grid</span>\n </button>\n\n <div style=\"flex: 1;\"></div>\n\n <button\n type=\"button\"\n class=\"tab-btn btn-collapse-inspector\"\n (click)=\"collapseRequested.emit()\"\n title=\"Collapse Property Editor\"\n aria-label=\"Collapse Property Editor\"\n >\n <rpt-icon name=\"chevron-right\" [size]=\"13\" />\n </button>\n </div>\n\n <!-- Editor Body -->\n <div class=\"editor-body\">\n <!-- 1. COMPONENT PROPERTIES TAB -->\n @if (activeTab() === 'properties') {\n @if (selectedComponent(); as comp) {\n <div class=\"component-sections-wrapper\">\n <!-- Component Identifier Header (Pinned at Top) -->\n <div class=\"comp-badge-header\">\n <div class=\"badge-title\">\n <span class=\"badge-type\">{{ comp.type }}</span>\n <span class=\"badge-id\">{{ comp.id }}</span>\n </div>\n <div class=\"badge-header-actions\">\n <button\n type=\"button\"\n class=\"btn-badge-action\"\n (click)=\"duplicateComponent(comp.id)\"\n title=\"Duplicate component\"\n >\n <rpt-icon name=\"copy\" [size]=\"13\" />\n </button>\n <button\n type=\"button\"\n class=\"btn-badge-action btn-badge-delete\"\n (click)=\"deleteComponent(comp.id)\"\n title=\"Delete component\"\n >\n <rpt-icon name=\"trash\" [size]=\"13\" />\n </button>\n <button\n type=\"button\"\n class=\"btn-deselect\"\n (click)=\"deselect()\"\n title=\"Deselect component\"\n >\n <rpt-icon name=\"close\" [size]=\"13\" />\n </button>\n </div>\n </div>\n\n <!-- Accordion Container -->\n <div class=\"property-accordion\">\n <!-- 1. OPTIONS DATA SOURCE ACCORDION (FIRST for Dropdown) -->\n @if (comp.type === 'dropdown') {\n <div class=\"accordion-item\" [class.is-expanded]=\"isSectionExpanded('datasource')\">\n <button\n type=\"button\"\n class=\"accordion-header\"\n (click)=\"toggleSection('datasource')\"\n title=\"Toggle Options Data Source section\"\n >\n <div class=\"header-left\">\n <rpt-icon name=\"database\" [size]=\"14\" />\n <span class=\"header-title\">Options Data Source</span>\n <span class=\"header-badge\">{{ getDatasourceBadge(comp) }}</span>\n </div>\n <div class=\"header-right\">\n <rpt-icon name=\"chevron-down\" [size]=\"13\" class=\"accordion-chevron\" />\n </div>\n </button>\n @if (isSectionExpanded('datasource')) {\n <div class=\"accordion-content\">\n <rpt-property-editor-datasource\n [comp]=\"comp\"\n [allComponents]=\"allComponents()\"\n (updateDataSource)=\"updateCompDataSource(comp.id, $event)\"\n (updateProperty)=\"updateProp(comp.id, $event.key, $event.value)\"\n />\n </div>\n }\n </div>\n }\n\n <!-- BUTTON ACTION ACCORDION (FIRST for Button) -->\n @if (comp.type === 'button') {\n <div class=\"accordion-item\" [class.is-expanded]=\"isSectionExpanded('action')\">\n <button\n type=\"button\"\n class=\"accordion-header\"\n (click)=\"toggleSection('action')\"\n title=\"Toggle Button Action section\"\n >\n <div class=\"header-left\">\n <rpt-icon name=\"lightning\" [size]=\"14\" />\n <span class=\"header-title\">Button Action</span>\n <span class=\"header-badge\">{{ getActionBadge(comp) }}</span>\n </div>\n <div class=\"header-right\">\n <rpt-icon name=\"chevron-down\" [size]=\"13\" class=\"accordion-chevron\" />\n </div>\n </button>\n @if (isSectionExpanded('action')) {\n <div class=\"accordion-content\">\n <rpt-property-editor-action\n [comp]=\"comp\"\n (updateProperty)=\"updateProp(comp.id, $event.key, $event.value)\"\n />\n </div>\n }\n </div>\n }\n\n <!-- 2. GENERAL PROPERTIES ACCORDION -->\n <div class=\"accordion-item\" [class.is-expanded]=\"isSectionExpanded('general')\">\n <button\n type=\"button\"\n class=\"accordion-header\"\n (click)=\"toggleSection('general')\"\n title=\"Toggle General Properties section\"\n >\n <div class=\"header-left\">\n <rpt-icon name=\"edit\" [size]=\"14\" />\n <span class=\"header-title\">General Properties</span>\n @if (comp.label) {\n <span class=\"header-badge\">{{ comp.label }}</span>\n }\n </div>\n <div class=\"header-right\">\n <rpt-icon name=\"chevron-down\" [size]=\"13\" class=\"accordion-chevron\" />\n </div>\n </button>\n @if (isSectionExpanded('general')) {\n <div class=\"accordion-content\">\n <rpt-property-editor-general\n [comp]=\"comp\"\n [propertySchema]=\"propertySchema()\"\n (update)=\"updateComp($event.field, $event.value)\"\n (updateProperty)=\"updateProp(comp.id, $event.key, $event.value)\"\n (deselect)=\"deselect()\"\n />\n </div>\n }\n </div>\n\n <!-- 3. LAYOUT & GRID ACCORDION -->\n <div class=\"accordion-item\" [class.is-expanded]=\"isSectionExpanded('layout')\">\n <button\n type=\"button\"\n class=\"accordion-header\"\n (click)=\"toggleSection('layout')\"\n title=\"Toggle Layout & Dimensions section\"\n >\n <div class=\"header-left\">\n <rpt-icon name=\"design\" [size]=\"14\" />\n <span class=\"header-title\">Layout & Grid</span>\n <span class=\"header-badge\">{{ getLayoutBadge(comp) }}</span>\n </div>\n <div class=\"header-right\">\n <rpt-icon name=\"chevron-down\" [size]=\"13\" class=\"accordion-chevron\" />\n </div>\n </button>\n @if (isSectionExpanded('layout')) {\n <div class=\"accordion-content\">\n <rpt-property-editor-layout\n [comp]=\"comp\"\n [maxColumns]=\"maxColumns()\"\n (updateLayout)=\"updateLayout($event.key, $event.value)\"\n (updateHeight)=\"updateComponentHeight($event)\"\n (resetDimensions)=\"resetComponentDimensions(comp.id, comp.type)\"\n />\n </div>\n }\n </div>\n\n <!-- 4. VALIDATION ACCORDION -->\n <div class=\"accordion-item\" [class.is-expanded]=\"isSectionExpanded('validation')\">\n <button\n type=\"button\"\n class=\"accordion-header\"\n (click)=\"toggleSection('validation')\"\n title=\"Toggle Validation Rules section\"\n >\n <div class=\"header-left\">\n <rpt-icon name=\"check\" [size]=\"14\" />\n <span class=\"header-title\">Validation Rules</span>\n <span class=\"header-badge\">{{ getValidationBadge(comp) }}</span>\n </div>\n <div class=\"header-right\">\n <rpt-icon name=\"chevron-down\" [size]=\"13\" class=\"accordion-chevron\" />\n </div>\n </button>\n @if (isSectionExpanded('validation')) {\n <div class=\"accordion-content\">\n <rpt-property-editor-validation\n [comp]=\"comp\"\n (updateValidation)=\"updateValidation($event.key, $event.value)\"\n />\n </div>\n }\n </div>\n </div>\n </div>\n } @else {\n <div class=\"empty-selection\">\n <div class=\"empty-icon-wrap\">\n <rpt-icon name=\"cursor\" [size]=\"24\" color=\"#64748b\" />\n </div>\n <p class=\"empty-text\">Select a component on the canvas to configure its properties.</p>\n </div>\n }\n }\n\n <!-- 2. REPORT SETTINGS TAB -->\n @if (activeTab() === 'report') {\n <rpt-property-editor-report\n [definition]=\"definition()\"\n (updateReport)=\"updateReport($event.field, $event.value)\"\n (updateLayout)=\"updateLayoutProp($event.field, $event.value)\"\n />\n }\n\n <!-- 3. RESULT GRID SETTINGS TAB -->\n @if (activeTab() === 'grid') {\n <rpt-property-editor-grid\n [grid]=\"definition().grid\"\n (addGrid)=\"addResultGrid()\"\n (removeGrid)=\"removeResultGrid()\"\n (addColumn)=\"addColumn()\"\n (autoDetectColumns)=\"autoDetectGridColumns()\"\n (removeColumn)=\"removeColumn($event)\"\n (updateColumn)=\"updateColumn($event.index, $event.field, $event.value)\"\n (updateOption)=\"updateGridOption($event.field, $event.value)\"\n (updateRowClickAction)=\"updateGridRowClickAction($event)\"\n />\n }\n </div>\n </div>", styles: [":host{display:block;height:100%;background:#fff;border-left:1px solid var(--rpt-color-border, #cbd5e1);box-sizing:border-box;font-family:var(--rpt-font-family, inherit)}.editor-container{display:flex;flex-direction:column;height:100%}.editor-tabs{display:flex;flex-shrink:0;border-bottom:1px solid var(--rpt-color-border, #cbd5e1);background:var(--rpt-color-surface, #f8fafc)}.tab-btn{flex:1;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:12px 8px;font-size:var(--rpt-font-size-sm, .75rem);font-weight:600;color:var(--rpt-color-text-muted, #64748b);background:transparent;border:none;border-bottom:2px solid transparent;cursor:pointer;transition:all var(--rpt-transition-speed, .15s) ease}.tab-btn:hover:not(:disabled){color:var(--rpt-color-text, #0f172a)}.tab-btn.active{color:var(--rpt-color-primary, #2563eb);border-bottom-color:var(--rpt-color-primary, #2563eb);background:#fff}.tab-btn:disabled{opacity:.4;cursor:not-allowed}.editor-body{flex:1;overflow-y:auto;padding:12px 14px}.component-sections-wrapper{display:flex;flex-direction:column;gap:12px}.comp-badge-header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:var(--rpt-color-surface, #f8fafc);border-radius:6px;border:1px solid var(--rpt-color-border, #e2e8f0)}.badge-title{display:flex;align-items:center}.badge-type{font-size:.75rem;font-weight:700;color:var(--rpt-color-primary, #2563eb);text-transform:uppercase;margin-right:8px}.badge-id{font-size:.7rem;color:var(--rpt-color-text-muted, #94a3b8);font-family:monospace}.badge-header-actions{display:flex;align-items:center;gap:4px}.btn-badge-action{background:#fff;border:1px solid var(--rpt-color-border, #cbd5e1);display:inline-flex;align-items:center;justify-content:center;color:var(--rpt-color-text-muted, #64748b);cursor:pointer;width:24px;height:24px;border-radius:4px;padding:0;transition:all .12s ease}.btn-badge-action:hover{background:var(--rpt-color-surface-hover, #f1f5f9);color:var(--rpt-color-primary, #2563eb);border-color:var(--rpt-color-primary, #2563eb)}.btn-badge-action.btn-badge-delete:hover{background:#fee2e2;color:#ef4444;border-color:#fca5a5}.btn-deselect{background:none;border:1px solid transparent;display:inline-flex;align-items:center;justify-content:center;color:var(--rpt-color-text-muted, #64748b);cursor:pointer;width:24px;height:24px;border-radius:4px;padding:0;transition:all .15s ease}.btn-deselect:hover{color:var(--rpt-color-danger, #ef4444);background:#fee2e2;border-color:#fca5a5}.property-accordion{display:flex;flex-direction:column;gap:10px}.accordion-item{border:1px solid var(--rpt-color-border, #e2e8f0);border-radius:6px;background:#fff;overflow:hidden;transition:all .15s ease}.accordion-item.is-expanded{border-color:#cbd5e1;box-shadow:0 1px 3px #0000000a}.accordion-header{width:100%;display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:var(--rpt-color-surface, #f8fafc);border:none;cursor:pointer;text-align:left;-webkit-user-select:none;user-select:none;transition:background .15s ease}.accordion-header:hover{background:#f1f5f9}.accordion-item.is-expanded .accordion-header{background:#f8fafc;border-bottom:1px solid var(--rpt-color-border, #e2e8f0)}.header-left{display:flex;align-items:center;gap:8px;min-width:0;flex:1}.header-title{font-size:.72rem;font-weight:700;color:var(--rpt-color-text, #0f172a);text-transform:uppercase;letter-spacing:.03em;white-space:nowrap}.header-badge{font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border:1px solid #bfdbfe;padding:1px 6px;border-radius:9999px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:120px}.header-right{display:flex;align-items:center;color:var(--rpt-color-text-muted, #64748b);margin-left:8px}.accordion-chevron{transition:transform .2s ease}.accordion-item.is-expanded .accordion-chevron{transform:rotate(180deg)}.accordion-content{padding:12px;display:flex;flex-direction:column;gap:12px}::ng-deep .accordion-content .group-title{display:none}::ng-deep .accordion-content .comp-badge-header{display:none}.empty-selection{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:60px 20px;text-align:center;box-sizing:border-box}.empty-icon-wrap{display:inline-flex;align-items:center;justify-content:center;width:52px;height:52px;border-radius:50%;background:#f1f5f9;margin-bottom:16px;box-shadow:0 1px 3px #0000000a}.empty-text{font-size:.8125rem;line-height:1.5;max-width:220px;margin:0;color:var(--rpt-color-text-muted, #64748b)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: RptIconComponent, selector: "rpt-icon", inputs: ["name", "size", "color", "strokeWidth"] }, { kind: "component", type: PropertyEditorGeneralComponent, selector: "rpt-property-editor-general", inputs: ["comp", "propertySchema"], outputs: ["update", "updateProperty", "deselect"] }, { kind: "component", type: PropertyEditorLayoutComponent, selector: "rpt-property-editor-layout", inputs: ["comp", "maxColumns"], outputs: ["updateLayout", "updateHeight", "resetDimensions"] }, { kind: "component", type: PropertyEditorValidationComponent, selector: "rpt-property-editor-validation", inputs: ["comp"], outputs: ["updateValidation"] }, { kind: "component", type: PropertyEditorGridComponent, selector: "rpt-property-editor-grid", inputs: ["grid"], outputs: ["addGrid", "removeGrid", "addColumn", "autoDetectColumns", "removeColumn", "updateColumn", "updateOption", "updateRowClickAction"] }, { kind: "component", type: PropertyEditorReportComponent, selector: "rpt-property-editor-report", inputs: ["definition"], outputs: ["updateReport", "updateLayout"] }, { kind: "component", type: PropertyEditorDatasourceComponent, selector: "rpt-property-editor-datasource", inputs: ["comp", "allComponents"], outputs: ["updateDataSource", "updateProperty"] }, { kind: "component", type: PropertyEditorActionComponent, selector: "rpt-property-editor-action", inputs: ["comp"], outputs: ["updateProperty"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
12796
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: PropertyEditorComponent, isStandalone: true, selector: "rpt-property-editor", outputs: { collapseRequested: "collapseRequested" }, ngImport: i0, template: " <div class=\"editor-container\">\n <!-- Editor Header & Tabs -->\n <div class=\"editor-tabs\">\n <button\n type=\"button\"\n class=\"tab-btn\"\n [class.active]=\"activeTab() === 'properties'\"\n (click)=\"setTab('properties')\"\n [disabled]=\"!selectedComponent()\"\n >\n <rpt-icon name=\"edit\" [size]=\"13\" />\n <span>Properties</span>\n </button>\n <button\n type=\"button\"\n class=\"tab-btn\"\n [class.active]=\"activeTab() === 'report'\"\n (click)=\"setTab('report')\"\n >\n <rpt-icon name=\"design\" [size]=\"13\" />\n <span>Layout</span>\n </button>\n <button\n type=\"button\"\n class=\"tab-btn\"\n [class.active]=\"activeTab() === 'grid'\"\n (click)=\"setTab('grid')\"\n >\n <rpt-icon name=\"table\" [size]=\"13\" />\n <span>Grid</span>\n </button>\n\n <div style=\"flex: 1;\"></div>\n\n <button\n type=\"button\"\n class=\"tab-btn btn-collapse-inspector\"\n (click)=\"collapseRequested.emit()\"\n title=\"Collapse Property Editor\"\n aria-label=\"Collapse Property Editor\"\n >\n <rpt-icon name=\"chevron-right\" [size]=\"13\" />\n </button>\n </div>\n\n <!-- Editor Body -->\n <div class=\"editor-body\">\n <!-- 1. COMPONENT PROPERTIES TAB -->\n @if (activeTab() === 'properties') {\n @if (selectedComponent(); as comp) {\n <div class=\"component-sections-wrapper\">\n <!-- Component Identifier Header (Pinned at Top) -->\n <div class=\"comp-badge-header\">\n <div class=\"badge-title\">\n <span class=\"badge-type\">{{ comp.type }}</span>\n <span class=\"badge-id\">{{ comp.id }}</span>\n </div>\n <div class=\"badge-header-actions\">\n <button\n type=\"button\"\n class=\"btn-badge-action\"\n (click)=\"duplicateComponent(comp.id)\"\n title=\"Duplicate component\"\n >\n <rpt-icon name=\"copy\" [size]=\"13\" />\n </button>\n <button\n type=\"button\"\n class=\"btn-badge-action btn-badge-delete\"\n (click)=\"deleteComponent(comp.id)\"\n title=\"Delete component\"\n >\n <rpt-icon name=\"trash\" [size]=\"13\" />\n </button>\n <button\n type=\"button\"\n class=\"btn-deselect\"\n (click)=\"deselect()\"\n title=\"Deselect component\"\n >\n <rpt-icon name=\"close\" [size]=\"13\" />\n </button>\n </div>\n </div>\n\n <!-- Accordion Container -->\n <div class=\"property-accordion\">\n <!-- 1. OPTIONS DATA SOURCE ACCORDION (FIRST for Dropdown) -->\n @if (comp.type === 'dropdown') {\n <div class=\"accordion-item\" [class.is-expanded]=\"isSectionExpanded('datasource')\">\n <button\n type=\"button\"\n class=\"accordion-header\"\n (click)=\"toggleSection('datasource')\"\n title=\"Toggle Options Data Source section\"\n >\n <div class=\"header-left\">\n <rpt-icon name=\"database\" [size]=\"14\" />\n <span class=\"header-title\">Options Data Source</span>\n <span class=\"header-badge\">{{ getDatasourceBadge(comp) }}</span>\n </div>\n <div class=\"header-right\">\n <rpt-icon name=\"chevron-down\" [size]=\"13\" class=\"accordion-chevron\" />\n </div>\n </button>\n @if (isSectionExpanded('datasource')) {\n <div class=\"accordion-content\">\n <rpt-property-editor-datasource\n [comp]=\"comp\"\n [allComponents]=\"allComponents()\"\n (updateDataSource)=\"updateCompDataSource(comp.id, $event)\"\n (updateProperty)=\"updateProp(comp.id, $event.key, $event.value)\"\n />\n </div>\n }\n </div>\n }\n\n <!-- BUTTON ACTION ACCORDION (FIRST for Button) -->\n @if (comp.type === 'button') {\n <div class=\"accordion-item\" [class.is-expanded]=\"isSectionExpanded('action')\">\n <button\n type=\"button\"\n class=\"accordion-header\"\n (click)=\"toggleSection('action')\"\n title=\"Toggle Button Action section\"\n >\n <div class=\"header-left\">\n <rpt-icon name=\"lightning\" [size]=\"14\" />\n <span class=\"header-title\">Button Action</span>\n <span class=\"header-badge\">{{ getActionBadge(comp) }}</span>\n </div>\n <div class=\"header-right\">\n <rpt-icon name=\"chevron-down\" [size]=\"13\" class=\"accordion-chevron\" />\n </div>\n </button>\n @if (isSectionExpanded('action')) {\n <div class=\"accordion-content\">\n <rpt-property-editor-action\n [comp]=\"comp\"\n (updateProperty)=\"updateProp(comp.id, $event.key, $event.value)\"\n />\n </div>\n }\n </div>\n }\n\n <!-- 2. GENERAL PROPERTIES ACCORDION -->\n <div class=\"accordion-item\" [class.is-expanded]=\"isSectionExpanded('general')\">\n <button\n type=\"button\"\n class=\"accordion-header\"\n (click)=\"toggleSection('general')\"\n title=\"Toggle General Properties section\"\n >\n <div class=\"header-left\">\n <rpt-icon name=\"edit\" [size]=\"14\" />\n <span class=\"header-title\">General Properties</span>\n @if (comp.label) {\n <span class=\"header-badge\">{{ comp.label }}</span>\n }\n </div>\n <div class=\"header-right\">\n <rpt-icon name=\"chevron-down\" [size]=\"13\" class=\"accordion-chevron\" />\n </div>\n </button>\n @if (isSectionExpanded('general')) {\n <div class=\"accordion-content\">\n <rpt-property-editor-general\n [comp]=\"comp\"\n [propertySchema]=\"propertySchema()\"\n (update)=\"updateComp($event.field, $event.value)\"\n (updateProperty)=\"updateProp(comp.id, $event.key, $event.value)\"\n (deselect)=\"deselect()\"\n />\n </div>\n }\n </div>\n\n <!-- 3. LAYOUT & GRID ACCORDION -->\n <div class=\"accordion-item\" [class.is-expanded]=\"isSectionExpanded('layout')\">\n <button\n type=\"button\"\n class=\"accordion-header\"\n (click)=\"toggleSection('layout')\"\n title=\"Toggle Layout & Dimensions section\"\n >\n <div class=\"header-left\">\n <rpt-icon name=\"design\" [size]=\"14\" />\n <span class=\"header-title\">Layout & Grid</span>\n <span class=\"header-badge\">{{ getLayoutBadge(comp) }}</span>\n </div>\n <div class=\"header-right\">\n <rpt-icon name=\"chevron-down\" [size]=\"13\" class=\"accordion-chevron\" />\n </div>\n </button>\n @if (isSectionExpanded('layout')) {\n <div class=\"accordion-content\">\n <rpt-property-editor-layout\n [comp]=\"comp\"\n [maxColumns]=\"maxColumns()\"\n (updateLayout)=\"updateLayout($event.key, $event.value)\"\n (updateHeight)=\"updateComponentHeight($event)\"\n (resetDimensions)=\"resetComponentDimensions(comp.id, comp.type)\"\n />\n </div>\n }\n </div>\n\n <!-- 4. VALIDATION ACCORDION -->\n <div class=\"accordion-item\" [class.is-expanded]=\"isSectionExpanded('validation')\">\n <button\n type=\"button\"\n class=\"accordion-header\"\n (click)=\"toggleSection('validation')\"\n title=\"Toggle Validation Rules section\"\n >\n <div class=\"header-left\">\n <rpt-icon name=\"check\" [size]=\"14\" />\n <span class=\"header-title\">Validation Rules</span>\n <span class=\"header-badge\">{{ getValidationBadge(comp) }}</span>\n </div>\n <div class=\"header-right\">\n <rpt-icon name=\"chevron-down\" [size]=\"13\" class=\"accordion-chevron\" />\n </div>\n </button>\n @if (isSectionExpanded('validation')) {\n <div class=\"accordion-content\">\n <rpt-property-editor-validation\n [comp]=\"comp\"\n (updateValidation)=\"updateValidation($event.key, $event.value)\"\n />\n </div>\n }\n </div>\n </div>\n </div>\n } @else {\n <div class=\"empty-selection\">\n <div class=\"empty-icon-wrap\">\n <rpt-icon name=\"cursor\" [size]=\"24\" color=\"#64748b\" />\n </div>\n <p class=\"empty-text\">Select a component on the canvas to configure its properties.</p>\n </div>\n }\n }\n\n <!-- 2. REPORT SETTINGS TAB -->\n @if (activeTab() === 'report') {\n <rpt-property-editor-report\n [definition]=\"definition()\"\n (updateReport)=\"updateReport($event.field, $event.value)\"\n (updateLayout)=\"updateLayoutProp($event.field, $event.value)\"\n />\n }\n\n <!-- 3. RESULT GRID SETTINGS TAB -->\n @if (activeTab() === 'grid') {\n <rpt-property-editor-grid\n [grid]=\"definition().grid\"\n [dataSource]=\"gridDataSource()\"\n [availableEntities]=\"availableEntities()\"\n [procedureParameters]=\"gridProcedureParameters()\"\n [allComponents]=\"allComponents()\"\n [isDetecting]=\"isDetectingColumns()\"\n [statusMessage]=\"gridDetectionStatus()?.message ?? null\"\n [statusKind]=\"gridDetectionStatus()?.kind ?? null\"\n (entityChanged)=\"onGridEntityChanged($event)\"\n (updateDataSourceParameters)=\"onGridDataSourceParametersChanged($event)\"\n (addGrid)=\"addResultGrid()\"\n (removeGrid)=\"removeResultGrid()\"\n (addColumn)=\"addColumn()\"\n (autoDetectColumns)=\"autoDetectGridColumns()\"\n (removeColumn)=\"removeColumn($event)\"\n (updateColumn)=\"updateColumn($event.index, $event.field, $event.value)\"\n (updateOption)=\"updateGridOption($event.field, $event.value)\"\n (updateRowClickAction)=\"updateGridRowClickAction($event)\"\n />\n }\n </div>\n </div>", styles: [":host{display:block;height:100%;background:#fff;border-left:1px solid var(--rpt-color-border, #cbd5e1);box-sizing:border-box;font-family:var(--rpt-font-family, inherit)}.editor-container{display:flex;flex-direction:column;height:100%}.editor-tabs{display:flex;flex-shrink:0;border-bottom:1px solid var(--rpt-color-border, #cbd5e1);background:var(--rpt-color-surface, #f8fafc)}.tab-btn{flex:1;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:12px 8px;font-size:var(--rpt-font-size-sm, .75rem);font-weight:600;color:var(--rpt-color-text-muted, #64748b);background:transparent;border:none;border-bottom:2px solid transparent;cursor:pointer;transition:all var(--rpt-transition-speed, .15s) ease}.tab-btn:hover:not(:disabled){color:var(--rpt-color-text, #0f172a)}.tab-btn.active{color:var(--rpt-color-primary, #2563eb);border-bottom-color:var(--rpt-color-primary, #2563eb);background:#fff}.tab-btn:disabled{opacity:.4;cursor:not-allowed}.editor-body{flex:1;overflow-y:auto;padding:12px 14px}.component-sections-wrapper{display:flex;flex-direction:column;gap:12px}.comp-badge-header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:var(--rpt-color-surface, #f8fafc);border-radius:6px;border:1px solid var(--rpt-color-border, #e2e8f0)}.badge-title{display:flex;align-items:center}.badge-type{font-size:.75rem;font-weight:700;color:var(--rpt-color-primary, #2563eb);text-transform:uppercase;margin-right:8px}.badge-id{font-size:.7rem;color:var(--rpt-color-text-muted, #94a3b8);font-family:monospace}.badge-header-actions{display:flex;align-items:center;gap:4px}.btn-badge-action{background:#fff;border:1px solid var(--rpt-color-border, #cbd5e1);display:inline-flex;align-items:center;justify-content:center;color:var(--rpt-color-text-muted, #64748b);cursor:pointer;width:24px;height:24px;border-radius:4px;padding:0;transition:all .12s ease}.btn-badge-action:hover{background:var(--rpt-color-surface-hover, #f1f5f9);color:var(--rpt-color-primary, #2563eb);border-color:var(--rpt-color-primary, #2563eb)}.btn-badge-action.btn-badge-delete:hover{background:#fee2e2;color:#ef4444;border-color:#fca5a5}.btn-deselect{background:none;border:1px solid transparent;display:inline-flex;align-items:center;justify-content:center;color:var(--rpt-color-text-muted, #64748b);cursor:pointer;width:24px;height:24px;border-radius:4px;padding:0;transition:all .15s ease}.btn-deselect:hover{color:var(--rpt-color-danger, #ef4444);background:#fee2e2;border-color:#fca5a5}.property-accordion{display:flex;flex-direction:column;gap:10px}.accordion-item{border:1px solid var(--rpt-color-border, #e2e8f0);border-radius:6px;background:#fff;overflow:hidden;transition:all .15s ease}.accordion-item.is-expanded{border-color:#cbd5e1;box-shadow:0 1px 3px #0000000a}.accordion-header{width:100%;display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:var(--rpt-color-surface, #f8fafc);border:none;cursor:pointer;text-align:left;-webkit-user-select:none;user-select:none;transition:background .15s ease}.accordion-header:hover{background:#f1f5f9}.accordion-item.is-expanded .accordion-header{background:#f8fafc;border-bottom:1px solid var(--rpt-color-border, #e2e8f0)}.header-left{display:flex;align-items:center;gap:8px;min-width:0;flex:1}.header-title{font-size:.72rem;font-weight:700;color:var(--rpt-color-text, #0f172a);text-transform:uppercase;letter-spacing:.03em;white-space:nowrap}.header-badge{font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border:1px solid #bfdbfe;padding:1px 6px;border-radius:9999px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:120px}.header-right{display:flex;align-items:center;color:var(--rpt-color-text-muted, #64748b);margin-left:8px}.accordion-chevron{transition:transform .2s ease}.accordion-item.is-expanded .accordion-chevron{transform:rotate(180deg)}.accordion-content{padding:12px;display:flex;flex-direction:column;gap:12px}::ng-deep .accordion-content .group-title{display:none}::ng-deep .accordion-content .comp-badge-header{display:none}.empty-selection{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:60px 20px;text-align:center;box-sizing:border-box}.empty-icon-wrap{display:inline-flex;align-items:center;justify-content:center;width:52px;height:52px;border-radius:50%;background:#f1f5f9;margin-bottom:16px;box-shadow:0 1px 3px #0000000a}.empty-text{font-size:.8125rem;line-height:1.5;max-width:220px;margin:0;color:var(--rpt-color-text-muted, #64748b)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: RptIconComponent, selector: "rpt-icon", inputs: ["name", "size", "color", "strokeWidth"] }, { kind: "component", type: PropertyEditorGeneralComponent, selector: "rpt-property-editor-general", inputs: ["comp", "propertySchema"], outputs: ["update", "updateProperty", "deselect"] }, { kind: "component", type: PropertyEditorLayoutComponent, selector: "rpt-property-editor-layout", inputs: ["comp", "maxColumns"], outputs: ["updateLayout", "updateHeight", "resetDimensions"] }, { kind: "component", type: PropertyEditorValidationComponent, selector: "rpt-property-editor-validation", inputs: ["comp"], outputs: ["updateValidation"] }, { kind: "component", type: PropertyEditorGridComponent, selector: "rpt-property-editor-grid", inputs: ["grid", "dataSource", "availableEntities", "procedureParameters", "allComponents", "isDetecting", "statusMessage", "statusKind"], outputs: ["addGrid", "removeGrid", "addColumn", "autoDetectColumns", "removeColumn", "updateColumn", "updateOption", "updateRowClickAction", "setDataSource", "updateDataSourceParameters", "entityChanged"] }, { kind: "component", type: PropertyEditorReportComponent, selector: "rpt-property-editor-report", inputs: ["definition"], outputs: ["updateReport", "updateLayout"] }, { kind: "component", type: PropertyEditorDatasourceComponent, selector: "rpt-property-editor-datasource", inputs: ["comp", "allComponents"], outputs: ["updateDataSource", "updateProperty"] }, { kind: "component", type: PropertyEditorActionComponent, selector: "rpt-property-editor-action", inputs: ["comp"], outputs: ["updateProperty"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
12088
12797
|
}
|
|
12089
12798
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PropertyEditorComponent, decorators: [{
|
|
12090
12799
|
type: Component,
|
|
@@ -12098,8 +12807,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
12098
12807
|
PropertyEditorReportComponent,
|
|
12099
12808
|
PropertyEditorDatasourceComponent,
|
|
12100
12809
|
PropertyEditorActionComponent,
|
|
12101
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, template: " <div class=\"editor-container\">\n <!-- Editor Header & Tabs -->\n <div class=\"editor-tabs\">\n <button\n type=\"button\"\n class=\"tab-btn\"\n [class.active]=\"activeTab() === 'properties'\"\n (click)=\"setTab('properties')\"\n [disabled]=\"!selectedComponent()\"\n >\n <rpt-icon name=\"edit\" [size]=\"13\" />\n <span>Properties</span>\n </button>\n <button\n type=\"button\"\n class=\"tab-btn\"\n [class.active]=\"activeTab() === 'report'\"\n (click)=\"setTab('report')\"\n >\n <rpt-icon name=\"design\" [size]=\"13\" />\n <span>Layout</span>\n </button>\n <button\n type=\"button\"\n class=\"tab-btn\"\n [class.active]=\"activeTab() === 'grid'\"\n (click)=\"setTab('grid')\"\n >\n <rpt-icon name=\"table\" [size]=\"13\" />\n <span>Grid</span>\n </button>\n\n <div style=\"flex: 1;\"></div>\n\n <button\n type=\"button\"\n class=\"tab-btn btn-collapse-inspector\"\n (click)=\"collapseRequested.emit()\"\n title=\"Collapse Property Editor\"\n aria-label=\"Collapse Property Editor\"\n >\n <rpt-icon name=\"chevron-right\" [size]=\"13\" />\n </button>\n </div>\n\n <!-- Editor Body -->\n <div class=\"editor-body\">\n <!-- 1. COMPONENT PROPERTIES TAB -->\n @if (activeTab() === 'properties') {\n @if (selectedComponent(); as comp) {\n <div class=\"component-sections-wrapper\">\n <!-- Component Identifier Header (Pinned at Top) -->\n <div class=\"comp-badge-header\">\n <div class=\"badge-title\">\n <span class=\"badge-type\">{{ comp.type }}</span>\n <span class=\"badge-id\">{{ comp.id }}</span>\n </div>\n <div class=\"badge-header-actions\">\n <button\n type=\"button\"\n class=\"btn-badge-action\"\n (click)=\"duplicateComponent(comp.id)\"\n title=\"Duplicate component\"\n >\n <rpt-icon name=\"copy\" [size]=\"13\" />\n </button>\n <button\n type=\"button\"\n class=\"btn-badge-action btn-badge-delete\"\n (click)=\"deleteComponent(comp.id)\"\n title=\"Delete component\"\n >\n <rpt-icon name=\"trash\" [size]=\"13\" />\n </button>\n <button\n type=\"button\"\n class=\"btn-deselect\"\n (click)=\"deselect()\"\n title=\"Deselect component\"\n >\n <rpt-icon name=\"close\" [size]=\"13\" />\n </button>\n </div>\n </div>\n\n <!-- Accordion Container -->\n <div class=\"property-accordion\">\n <!-- 1. OPTIONS DATA SOURCE ACCORDION (FIRST for Dropdown) -->\n @if (comp.type === 'dropdown') {\n <div class=\"accordion-item\" [class.is-expanded]=\"isSectionExpanded('datasource')\">\n <button\n type=\"button\"\n class=\"accordion-header\"\n (click)=\"toggleSection('datasource')\"\n title=\"Toggle Options Data Source section\"\n >\n <div class=\"header-left\">\n <rpt-icon name=\"database\" [size]=\"14\" />\n <span class=\"header-title\">Options Data Source</span>\n <span class=\"header-badge\">{{ getDatasourceBadge(comp) }}</span>\n </div>\n <div class=\"header-right\">\n <rpt-icon name=\"chevron-down\" [size]=\"13\" class=\"accordion-chevron\" />\n </div>\n </button>\n @if (isSectionExpanded('datasource')) {\n <div class=\"accordion-content\">\n <rpt-property-editor-datasource\n [comp]=\"comp\"\n [allComponents]=\"allComponents()\"\n (updateDataSource)=\"updateCompDataSource(comp.id, $event)\"\n (updateProperty)=\"updateProp(comp.id, $event.key, $event.value)\"\n />\n </div>\n }\n </div>\n }\n\n <!-- BUTTON ACTION ACCORDION (FIRST for Button) -->\n @if (comp.type === 'button') {\n <div class=\"accordion-item\" [class.is-expanded]=\"isSectionExpanded('action')\">\n <button\n type=\"button\"\n class=\"accordion-header\"\n (click)=\"toggleSection('action')\"\n title=\"Toggle Button Action section\"\n >\n <div class=\"header-left\">\n <rpt-icon name=\"lightning\" [size]=\"14\" />\n <span class=\"header-title\">Button Action</span>\n <span class=\"header-badge\">{{ getActionBadge(comp) }}</span>\n </div>\n <div class=\"header-right\">\n <rpt-icon name=\"chevron-down\" [size]=\"13\" class=\"accordion-chevron\" />\n </div>\n </button>\n @if (isSectionExpanded('action')) {\n <div class=\"accordion-content\">\n <rpt-property-editor-action\n [comp]=\"comp\"\n (updateProperty)=\"updateProp(comp.id, $event.key, $event.value)\"\n />\n </div>\n }\n </div>\n }\n\n <!-- 2. GENERAL PROPERTIES ACCORDION -->\n <div class=\"accordion-item\" [class.is-expanded]=\"isSectionExpanded('general')\">\n <button\n type=\"button\"\n class=\"accordion-header\"\n (click)=\"toggleSection('general')\"\n title=\"Toggle General Properties section\"\n >\n <div class=\"header-left\">\n <rpt-icon name=\"edit\" [size]=\"14\" />\n <span class=\"header-title\">General Properties</span>\n @if (comp.label) {\n <span class=\"header-badge\">{{ comp.label }}</span>\n }\n </div>\n <div class=\"header-right\">\n <rpt-icon name=\"chevron-down\" [size]=\"13\" class=\"accordion-chevron\" />\n </div>\n </button>\n @if (isSectionExpanded('general')) {\n <div class=\"accordion-content\">\n <rpt-property-editor-general\n [comp]=\"comp\"\n [propertySchema]=\"propertySchema()\"\n (update)=\"updateComp($event.field, $event.value)\"\n (updateProperty)=\"updateProp(comp.id, $event.key, $event.value)\"\n (deselect)=\"deselect()\"\n />\n </div>\n }\n </div>\n\n <!-- 3. LAYOUT & GRID ACCORDION -->\n <div class=\"accordion-item\" [class.is-expanded]=\"isSectionExpanded('layout')\">\n <button\n type=\"button\"\n class=\"accordion-header\"\n (click)=\"toggleSection('layout')\"\n title=\"Toggle Layout & Dimensions section\"\n >\n <div class=\"header-left\">\n <rpt-icon name=\"design\" [size]=\"14\" />\n <span class=\"header-title\">Layout & Grid</span>\n <span class=\"header-badge\">{{ getLayoutBadge(comp) }}</span>\n </div>\n <div class=\"header-right\">\n <rpt-icon name=\"chevron-down\" [size]=\"13\" class=\"accordion-chevron\" />\n </div>\n </button>\n @if (isSectionExpanded('layout')) {\n <div class=\"accordion-content\">\n <rpt-property-editor-layout\n [comp]=\"comp\"\n [maxColumns]=\"maxColumns()\"\n (updateLayout)=\"updateLayout($event.key, $event.value)\"\n (updateHeight)=\"updateComponentHeight($event)\"\n (resetDimensions)=\"resetComponentDimensions(comp.id, comp.type)\"\n />\n </div>\n }\n </div>\n\n <!-- 4. VALIDATION ACCORDION -->\n <div class=\"accordion-item\" [class.is-expanded]=\"isSectionExpanded('validation')\">\n <button\n type=\"button\"\n class=\"accordion-header\"\n (click)=\"toggleSection('validation')\"\n title=\"Toggle Validation Rules section\"\n >\n <div class=\"header-left\">\n <rpt-icon name=\"check\" [size]=\"14\" />\n <span class=\"header-title\">Validation Rules</span>\n <span class=\"header-badge\">{{ getValidationBadge(comp) }}</span>\n </div>\n <div class=\"header-right\">\n <rpt-icon name=\"chevron-down\" [size]=\"13\" class=\"accordion-chevron\" />\n </div>\n </button>\n @if (isSectionExpanded('validation')) {\n <div class=\"accordion-content\">\n <rpt-property-editor-validation\n [comp]=\"comp\"\n (updateValidation)=\"updateValidation($event.key, $event.value)\"\n />\n </div>\n }\n </div>\n </div>\n </div>\n } @else {\n <div class=\"empty-selection\">\n <div class=\"empty-icon-wrap\">\n <rpt-icon name=\"cursor\" [size]=\"24\" color=\"#64748b\" />\n </div>\n <p class=\"empty-text\">Select a component on the canvas to configure its properties.</p>\n </div>\n }\n }\n\n <!-- 2. REPORT SETTINGS TAB -->\n @if (activeTab() === 'report') {\n <rpt-property-editor-report\n [definition]=\"definition()\"\n (updateReport)=\"updateReport($event.field, $event.value)\"\n (updateLayout)=\"updateLayoutProp($event.field, $event.value)\"\n />\n }\n\n <!-- 3. RESULT GRID SETTINGS TAB -->\n @if (activeTab() === 'grid') {\n <rpt-property-editor-grid\n [grid]=\"definition().grid\"\n (addGrid)=\"addResultGrid()\"\n (removeGrid)=\"removeResultGrid()\"\n (addColumn)=\"addColumn()\"\n (autoDetectColumns)=\"autoDetectGridColumns()\"\n (removeColumn)=\"removeColumn($event)\"\n (updateColumn)=\"updateColumn($event.index, $event.field, $event.value)\"\n (updateOption)=\"updateGridOption($event.field, $event.value)\"\n (updateRowClickAction)=\"updateGridRowClickAction($event)\"\n />\n }\n </div>\n </div>", styles: [":host{display:block;height:100%;background:#fff;border-left:1px solid var(--rpt-color-border, #cbd5e1);box-sizing:border-box;font-family:var(--rpt-font-family, inherit)}.editor-container{display:flex;flex-direction:column;height:100%}.editor-tabs{display:flex;flex-shrink:0;border-bottom:1px solid var(--rpt-color-border, #cbd5e1);background:var(--rpt-color-surface, #f8fafc)}.tab-btn{flex:1;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:12px 8px;font-size:var(--rpt-font-size-sm, .75rem);font-weight:600;color:var(--rpt-color-text-muted, #64748b);background:transparent;border:none;border-bottom:2px solid transparent;cursor:pointer;transition:all var(--rpt-transition-speed, .15s) ease}.tab-btn:hover:not(:disabled){color:var(--rpt-color-text, #0f172a)}.tab-btn.active{color:var(--rpt-color-primary, #2563eb);border-bottom-color:var(--rpt-color-primary, #2563eb);background:#fff}.tab-btn:disabled{opacity:.4;cursor:not-allowed}.editor-body{flex:1;overflow-y:auto;padding:12px 14px}.component-sections-wrapper{display:flex;flex-direction:column;gap:12px}.comp-badge-header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:var(--rpt-color-surface, #f8fafc);border-radius:6px;border:1px solid var(--rpt-color-border, #e2e8f0)}.badge-title{display:flex;align-items:center}.badge-type{font-size:.75rem;font-weight:700;color:var(--rpt-color-primary, #2563eb);text-transform:uppercase;margin-right:8px}.badge-id{font-size:.7rem;color:var(--rpt-color-text-muted, #94a3b8);font-family:monospace}.badge-header-actions{display:flex;align-items:center;gap:4px}.btn-badge-action{background:#fff;border:1px solid var(--rpt-color-border, #cbd5e1);display:inline-flex;align-items:center;justify-content:center;color:var(--rpt-color-text-muted, #64748b);cursor:pointer;width:24px;height:24px;border-radius:4px;padding:0;transition:all .12s ease}.btn-badge-action:hover{background:var(--rpt-color-surface-hover, #f1f5f9);color:var(--rpt-color-primary, #2563eb);border-color:var(--rpt-color-primary, #2563eb)}.btn-badge-action.btn-badge-delete:hover{background:#fee2e2;color:#ef4444;border-color:#fca5a5}.btn-deselect{background:none;border:1px solid transparent;display:inline-flex;align-items:center;justify-content:center;color:var(--rpt-color-text-muted, #64748b);cursor:pointer;width:24px;height:24px;border-radius:4px;padding:0;transition:all .15s ease}.btn-deselect:hover{color:var(--rpt-color-danger, #ef4444);background:#fee2e2;border-color:#fca5a5}.property-accordion{display:flex;flex-direction:column;gap:10px}.accordion-item{border:1px solid var(--rpt-color-border, #e2e8f0);border-radius:6px;background:#fff;overflow:hidden;transition:all .15s ease}.accordion-item.is-expanded{border-color:#cbd5e1;box-shadow:0 1px 3px #0000000a}.accordion-header{width:100%;display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:var(--rpt-color-surface, #f8fafc);border:none;cursor:pointer;text-align:left;-webkit-user-select:none;user-select:none;transition:background .15s ease}.accordion-header:hover{background:#f1f5f9}.accordion-item.is-expanded .accordion-header{background:#f8fafc;border-bottom:1px solid var(--rpt-color-border, #e2e8f0)}.header-left{display:flex;align-items:center;gap:8px;min-width:0;flex:1}.header-title{font-size:.72rem;font-weight:700;color:var(--rpt-color-text, #0f172a);text-transform:uppercase;letter-spacing:.03em;white-space:nowrap}.header-badge{font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border:1px solid #bfdbfe;padding:1px 6px;border-radius:9999px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:120px}.header-right{display:flex;align-items:center;color:var(--rpt-color-text-muted, #64748b);margin-left:8px}.accordion-chevron{transition:transform .2s ease}.accordion-item.is-expanded .accordion-chevron{transform:rotate(180deg)}.accordion-content{padding:12px;display:flex;flex-direction:column;gap:12px}::ng-deep .accordion-content .group-title{display:none}::ng-deep .accordion-content .comp-badge-header{display:none}.empty-selection{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:60px 20px;text-align:center;box-sizing:border-box}.empty-icon-wrap{display:inline-flex;align-items:center;justify-content:center;width:52px;height:52px;border-radius:50%;background:#f1f5f9;margin-bottom:16px;box-shadow:0 1px 3px #0000000a}.empty-text{font-size:.8125rem;line-height:1.5;max-width:220px;margin:0;color:var(--rpt-color-text-muted, #64748b)}\n"] }]
|
|
12102
|
-
}] });
|
|
12810
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: " <div class=\"editor-container\">\n <!-- Editor Header & Tabs -->\n <div class=\"editor-tabs\">\n <button\n type=\"button\"\n class=\"tab-btn\"\n [class.active]=\"activeTab() === 'properties'\"\n (click)=\"setTab('properties')\"\n [disabled]=\"!selectedComponent()\"\n >\n <rpt-icon name=\"edit\" [size]=\"13\" />\n <span>Properties</span>\n </button>\n <button\n type=\"button\"\n class=\"tab-btn\"\n [class.active]=\"activeTab() === 'report'\"\n (click)=\"setTab('report')\"\n >\n <rpt-icon name=\"design\" [size]=\"13\" />\n <span>Layout</span>\n </button>\n <button\n type=\"button\"\n class=\"tab-btn\"\n [class.active]=\"activeTab() === 'grid'\"\n (click)=\"setTab('grid')\"\n >\n <rpt-icon name=\"table\" [size]=\"13\" />\n <span>Grid</span>\n </button>\n\n <div style=\"flex: 1;\"></div>\n\n <button\n type=\"button\"\n class=\"tab-btn btn-collapse-inspector\"\n (click)=\"collapseRequested.emit()\"\n title=\"Collapse Property Editor\"\n aria-label=\"Collapse Property Editor\"\n >\n <rpt-icon name=\"chevron-right\" [size]=\"13\" />\n </button>\n </div>\n\n <!-- Editor Body -->\n <div class=\"editor-body\">\n <!-- 1. COMPONENT PROPERTIES TAB -->\n @if (activeTab() === 'properties') {\n @if (selectedComponent(); as comp) {\n <div class=\"component-sections-wrapper\">\n <!-- Component Identifier Header (Pinned at Top) -->\n <div class=\"comp-badge-header\">\n <div class=\"badge-title\">\n <span class=\"badge-type\">{{ comp.type }}</span>\n <span class=\"badge-id\">{{ comp.id }}</span>\n </div>\n <div class=\"badge-header-actions\">\n <button\n type=\"button\"\n class=\"btn-badge-action\"\n (click)=\"duplicateComponent(comp.id)\"\n title=\"Duplicate component\"\n >\n <rpt-icon name=\"copy\" [size]=\"13\" />\n </button>\n <button\n type=\"button\"\n class=\"btn-badge-action btn-badge-delete\"\n (click)=\"deleteComponent(comp.id)\"\n title=\"Delete component\"\n >\n <rpt-icon name=\"trash\" [size]=\"13\" />\n </button>\n <button\n type=\"button\"\n class=\"btn-deselect\"\n (click)=\"deselect()\"\n title=\"Deselect component\"\n >\n <rpt-icon name=\"close\" [size]=\"13\" />\n </button>\n </div>\n </div>\n\n <!-- Accordion Container -->\n <div class=\"property-accordion\">\n <!-- 1. OPTIONS DATA SOURCE ACCORDION (FIRST for Dropdown) -->\n @if (comp.type === 'dropdown') {\n <div class=\"accordion-item\" [class.is-expanded]=\"isSectionExpanded('datasource')\">\n <button\n type=\"button\"\n class=\"accordion-header\"\n (click)=\"toggleSection('datasource')\"\n title=\"Toggle Options Data Source section\"\n >\n <div class=\"header-left\">\n <rpt-icon name=\"database\" [size]=\"14\" />\n <span class=\"header-title\">Options Data Source</span>\n <span class=\"header-badge\">{{ getDatasourceBadge(comp) }}</span>\n </div>\n <div class=\"header-right\">\n <rpt-icon name=\"chevron-down\" [size]=\"13\" class=\"accordion-chevron\" />\n </div>\n </button>\n @if (isSectionExpanded('datasource')) {\n <div class=\"accordion-content\">\n <rpt-property-editor-datasource\n [comp]=\"comp\"\n [allComponents]=\"allComponents()\"\n (updateDataSource)=\"updateCompDataSource(comp.id, $event)\"\n (updateProperty)=\"updateProp(comp.id, $event.key, $event.value)\"\n />\n </div>\n }\n </div>\n }\n\n <!-- BUTTON ACTION ACCORDION (FIRST for Button) -->\n @if (comp.type === 'button') {\n <div class=\"accordion-item\" [class.is-expanded]=\"isSectionExpanded('action')\">\n <button\n type=\"button\"\n class=\"accordion-header\"\n (click)=\"toggleSection('action')\"\n title=\"Toggle Button Action section\"\n >\n <div class=\"header-left\">\n <rpt-icon name=\"lightning\" [size]=\"14\" />\n <span class=\"header-title\">Button Action</span>\n <span class=\"header-badge\">{{ getActionBadge(comp) }}</span>\n </div>\n <div class=\"header-right\">\n <rpt-icon name=\"chevron-down\" [size]=\"13\" class=\"accordion-chevron\" />\n </div>\n </button>\n @if (isSectionExpanded('action')) {\n <div class=\"accordion-content\">\n <rpt-property-editor-action\n [comp]=\"comp\"\n (updateProperty)=\"updateProp(comp.id, $event.key, $event.value)\"\n />\n </div>\n }\n </div>\n }\n\n <!-- 2. GENERAL PROPERTIES ACCORDION -->\n <div class=\"accordion-item\" [class.is-expanded]=\"isSectionExpanded('general')\">\n <button\n type=\"button\"\n class=\"accordion-header\"\n (click)=\"toggleSection('general')\"\n title=\"Toggle General Properties section\"\n >\n <div class=\"header-left\">\n <rpt-icon name=\"edit\" [size]=\"14\" />\n <span class=\"header-title\">General Properties</span>\n @if (comp.label) {\n <span class=\"header-badge\">{{ comp.label }}</span>\n }\n </div>\n <div class=\"header-right\">\n <rpt-icon name=\"chevron-down\" [size]=\"13\" class=\"accordion-chevron\" />\n </div>\n </button>\n @if (isSectionExpanded('general')) {\n <div class=\"accordion-content\">\n <rpt-property-editor-general\n [comp]=\"comp\"\n [propertySchema]=\"propertySchema()\"\n (update)=\"updateComp($event.field, $event.value)\"\n (updateProperty)=\"updateProp(comp.id, $event.key, $event.value)\"\n (deselect)=\"deselect()\"\n />\n </div>\n }\n </div>\n\n <!-- 3. LAYOUT & GRID ACCORDION -->\n <div class=\"accordion-item\" [class.is-expanded]=\"isSectionExpanded('layout')\">\n <button\n type=\"button\"\n class=\"accordion-header\"\n (click)=\"toggleSection('layout')\"\n title=\"Toggle Layout & Dimensions section\"\n >\n <div class=\"header-left\">\n <rpt-icon name=\"design\" [size]=\"14\" />\n <span class=\"header-title\">Layout & Grid</span>\n <span class=\"header-badge\">{{ getLayoutBadge(comp) }}</span>\n </div>\n <div class=\"header-right\">\n <rpt-icon name=\"chevron-down\" [size]=\"13\" class=\"accordion-chevron\" />\n </div>\n </button>\n @if (isSectionExpanded('layout')) {\n <div class=\"accordion-content\">\n <rpt-property-editor-layout\n [comp]=\"comp\"\n [maxColumns]=\"maxColumns()\"\n (updateLayout)=\"updateLayout($event.key, $event.value)\"\n (updateHeight)=\"updateComponentHeight($event)\"\n (resetDimensions)=\"resetComponentDimensions(comp.id, comp.type)\"\n />\n </div>\n }\n </div>\n\n <!-- 4. VALIDATION ACCORDION -->\n <div class=\"accordion-item\" [class.is-expanded]=\"isSectionExpanded('validation')\">\n <button\n type=\"button\"\n class=\"accordion-header\"\n (click)=\"toggleSection('validation')\"\n title=\"Toggle Validation Rules section\"\n >\n <div class=\"header-left\">\n <rpt-icon name=\"check\" [size]=\"14\" />\n <span class=\"header-title\">Validation Rules</span>\n <span class=\"header-badge\">{{ getValidationBadge(comp) }}</span>\n </div>\n <div class=\"header-right\">\n <rpt-icon name=\"chevron-down\" [size]=\"13\" class=\"accordion-chevron\" />\n </div>\n </button>\n @if (isSectionExpanded('validation')) {\n <div class=\"accordion-content\">\n <rpt-property-editor-validation\n [comp]=\"comp\"\n (updateValidation)=\"updateValidation($event.key, $event.value)\"\n />\n </div>\n }\n </div>\n </div>\n </div>\n } @else {\n <div class=\"empty-selection\">\n <div class=\"empty-icon-wrap\">\n <rpt-icon name=\"cursor\" [size]=\"24\" color=\"#64748b\" />\n </div>\n <p class=\"empty-text\">Select a component on the canvas to configure its properties.</p>\n </div>\n }\n }\n\n <!-- 2. REPORT SETTINGS TAB -->\n @if (activeTab() === 'report') {\n <rpt-property-editor-report\n [definition]=\"definition()\"\n (updateReport)=\"updateReport($event.field, $event.value)\"\n (updateLayout)=\"updateLayoutProp($event.field, $event.value)\"\n />\n }\n\n <!-- 3. RESULT GRID SETTINGS TAB -->\n @if (activeTab() === 'grid') {\n <rpt-property-editor-grid\n [grid]=\"definition().grid\"\n [dataSource]=\"gridDataSource()\"\n [availableEntities]=\"availableEntities()\"\n [procedureParameters]=\"gridProcedureParameters()\"\n [allComponents]=\"allComponents()\"\n [isDetecting]=\"isDetectingColumns()\"\n [statusMessage]=\"gridDetectionStatus()?.message ?? null\"\n [statusKind]=\"gridDetectionStatus()?.kind ?? null\"\n (entityChanged)=\"onGridEntityChanged($event)\"\n (updateDataSourceParameters)=\"onGridDataSourceParametersChanged($event)\"\n (addGrid)=\"addResultGrid()\"\n (removeGrid)=\"removeResultGrid()\"\n (addColumn)=\"addColumn()\"\n (autoDetectColumns)=\"autoDetectGridColumns()\"\n (removeColumn)=\"removeColumn($event)\"\n (updateColumn)=\"updateColumn($event.index, $event.field, $event.value)\"\n (updateOption)=\"updateGridOption($event.field, $event.value)\"\n (updateRowClickAction)=\"updateGridRowClickAction($event)\"\n />\n }\n </div>\n </div>", styles: [":host{display:block;height:100%;background:#fff;border-left:1px solid var(--rpt-color-border, #cbd5e1);box-sizing:border-box;font-family:var(--rpt-font-family, inherit)}.editor-container{display:flex;flex-direction:column;height:100%}.editor-tabs{display:flex;flex-shrink:0;border-bottom:1px solid var(--rpt-color-border, #cbd5e1);background:var(--rpt-color-surface, #f8fafc)}.tab-btn{flex:1;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:12px 8px;font-size:var(--rpt-font-size-sm, .75rem);font-weight:600;color:var(--rpt-color-text-muted, #64748b);background:transparent;border:none;border-bottom:2px solid transparent;cursor:pointer;transition:all var(--rpt-transition-speed, .15s) ease}.tab-btn:hover:not(:disabled){color:var(--rpt-color-text, #0f172a)}.tab-btn.active{color:var(--rpt-color-primary, #2563eb);border-bottom-color:var(--rpt-color-primary, #2563eb);background:#fff}.tab-btn:disabled{opacity:.4;cursor:not-allowed}.editor-body{flex:1;overflow-y:auto;padding:12px 14px}.component-sections-wrapper{display:flex;flex-direction:column;gap:12px}.comp-badge-header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:var(--rpt-color-surface, #f8fafc);border-radius:6px;border:1px solid var(--rpt-color-border, #e2e8f0)}.badge-title{display:flex;align-items:center}.badge-type{font-size:.75rem;font-weight:700;color:var(--rpt-color-primary, #2563eb);text-transform:uppercase;margin-right:8px}.badge-id{font-size:.7rem;color:var(--rpt-color-text-muted, #94a3b8);font-family:monospace}.badge-header-actions{display:flex;align-items:center;gap:4px}.btn-badge-action{background:#fff;border:1px solid var(--rpt-color-border, #cbd5e1);display:inline-flex;align-items:center;justify-content:center;color:var(--rpt-color-text-muted, #64748b);cursor:pointer;width:24px;height:24px;border-radius:4px;padding:0;transition:all .12s ease}.btn-badge-action:hover{background:var(--rpt-color-surface-hover, #f1f5f9);color:var(--rpt-color-primary, #2563eb);border-color:var(--rpt-color-primary, #2563eb)}.btn-badge-action.btn-badge-delete:hover{background:#fee2e2;color:#ef4444;border-color:#fca5a5}.btn-deselect{background:none;border:1px solid transparent;display:inline-flex;align-items:center;justify-content:center;color:var(--rpt-color-text-muted, #64748b);cursor:pointer;width:24px;height:24px;border-radius:4px;padding:0;transition:all .15s ease}.btn-deselect:hover{color:var(--rpt-color-danger, #ef4444);background:#fee2e2;border-color:#fca5a5}.property-accordion{display:flex;flex-direction:column;gap:10px}.accordion-item{border:1px solid var(--rpt-color-border, #e2e8f0);border-radius:6px;background:#fff;overflow:hidden;transition:all .15s ease}.accordion-item.is-expanded{border-color:#cbd5e1;box-shadow:0 1px 3px #0000000a}.accordion-header{width:100%;display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:var(--rpt-color-surface, #f8fafc);border:none;cursor:pointer;text-align:left;-webkit-user-select:none;user-select:none;transition:background .15s ease}.accordion-header:hover{background:#f1f5f9}.accordion-item.is-expanded .accordion-header{background:#f8fafc;border-bottom:1px solid var(--rpt-color-border, #e2e8f0)}.header-left{display:flex;align-items:center;gap:8px;min-width:0;flex:1}.header-title{font-size:.72rem;font-weight:700;color:var(--rpt-color-text, #0f172a);text-transform:uppercase;letter-spacing:.03em;white-space:nowrap}.header-badge{font-size:.65rem;font-weight:600;color:var(--rpt-color-primary, #2563eb);background:#eff6ff;border:1px solid #bfdbfe;padding:1px 6px;border-radius:9999px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:120px}.header-right{display:flex;align-items:center;color:var(--rpt-color-text-muted, #64748b);margin-left:8px}.accordion-chevron{transition:transform .2s ease}.accordion-item.is-expanded .accordion-chevron{transform:rotate(180deg)}.accordion-content{padding:12px;display:flex;flex-direction:column;gap:12px}::ng-deep .accordion-content .group-title{display:none}::ng-deep .accordion-content .comp-badge-header{display:none}.empty-selection{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:60px 20px;text-align:center;box-sizing:border-box}.empty-icon-wrap{display:inline-flex;align-items:center;justify-content:center;width:52px;height:52px;border-radius:50%;background:#f1f5f9;margin-bottom:16px;box-shadow:0 1px 3px #0000000a}.empty-text{font-size:.8125rem;line-height:1.5;max-width:220px;margin:0;color:var(--rpt-color-text-muted, #64748b)}\n"] }]
|
|
12811
|
+
}], ctorParameters: () => [] });
|
|
12103
12812
|
|
|
12104
12813
|
class DesignerComponent {
|
|
12105
12814
|
designerState = inject(DesignerStateService);
|
|
@@ -12439,7 +13148,7 @@ class DesignerComponent {
|
|
|
12439
13148
|
</div>
|
|
12440
13149
|
}
|
|
12441
13150
|
</div>
|
|
12442
|
-
`, isInline: true, styles: [":host{display:block;width:100%;height:100%;min-height:400px;font-family:var(--rpt-font-family, inherit);box-sizing:border-box}.designer-shell{display:flex;flex-direction:column;height:100%;background-color:#f8fafc;position:relative;overflow:hidden}.designer-workbench{display:flex;flex:1;overflow:hidden;position:relative;min-height:0}.workbench-left-container{display:flex;position:relative;width:250px;flex-shrink:0;height:100%;transition:width var(--rpt-transition-speed-smooth, .25s) cubic-bezier(.16,1,.3,1);z-index:20}.workbench-left-container.is-collapsed{width:0!important}.workbench-left-panel{width:100%;height:100%;overflow:hidden;border-right:1px solid var(--rpt-color-border, #cbd5e1);background:#fff;box-sizing:border-box}.workbench-left-container.is-collapsed .workbench-left-panel{visibility:hidden;border-right:none}.workbench-center-panel{flex:1;height:100%;overflow:hidden;display:flex;flex-direction:column;min-width:0;position:relative}.workbench-right-container{display:flex;position:relative;width:
|
|
13151
|
+
`, isInline: true, styles: [":host{display:block;width:100%;height:100%;min-height:400px;font-family:var(--rpt-font-family, inherit);box-sizing:border-box}.designer-shell{display:flex;flex-direction:column;height:100%;background-color:#f8fafc;position:relative;overflow:hidden}.designer-workbench{display:flex;flex:1;overflow:hidden;position:relative;min-height:0}.workbench-left-container{display:flex;position:relative;width:250px;flex-shrink:0;height:100%;transition:width var(--rpt-transition-speed-smooth, .25s) cubic-bezier(.16,1,.3,1);z-index:20}.workbench-left-container.is-collapsed{width:0!important}.workbench-left-panel{width:100%;height:100%;overflow:hidden;border-right:1px solid var(--rpt-color-border, #cbd5e1);background:#fff;box-sizing:border-box}.workbench-left-container.is-collapsed .workbench-left-panel{visibility:hidden;border-right:none}.workbench-center-panel{flex:1;height:100%;overflow:hidden;display:flex;flex-direction:column;min-width:0;position:relative}.workbench-right-container{display:flex;position:relative;width:420px;flex-shrink:0;height:100%;transition:width var(--rpt-transition-speed-smooth, .25s) cubic-bezier(.16,1,.3,1);z-index:20}.workbench-right-container.is-collapsed{width:0!important}.workbench-right-panel{width:100%;height:100%;overflow:hidden;border-left:1px solid var(--rpt-color-border, #cbd5e1);background:#fff;box-sizing:border-box}.workbench-right-container.is-collapsed .workbench-right-panel{visibility:hidden;border-left:none}.compact-workbench-bar,.workbench-drawer-backdrop{display:none}@media (max-width: 900px){.compact-workbench-bar{display:flex;align-items:center;justify-content:space-around;height:48px;background:var(--rpt-color-surface, #ffffff);border-top:1px solid var(--rpt-color-border, #cbd5e1);padding:0 12px;gap:8px;z-index:45;flex-shrink:0;box-shadow:0 -2px 8px #0000000a;font-family:var(--rpt-font-family, inherit)}.compact-nav-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:6px 14px;border-radius:var(--rpt-border-radius, 6px);border:1px solid transparent;background:transparent;font-family:var(--rpt-font-family, inherit);font-size:var(--rpt-font-size-sm, .75rem);font-weight:var(--rpt-font-weight-medium, 500);letter-spacing:-.01em;line-height:1.4;color:var(--rpt-color-text-secondary, #334155);cursor:pointer;-webkit-user-select:none;user-select:none;transition:all var(--rpt-transition-speed, .15s) ease;white-space:nowrap}.compact-nav-btn rpt-icon{display:inline-flex;align-items:center;justify-content:center;color:var(--rpt-color-text-muted, #64748b);transition:color var(--rpt-transition-speed, .15s) ease}.compact-nav-btn:hover{background:var(--rpt-color-surface-hover, #f1f5f9);color:var(--rpt-color-text, #0f172a);border-color:var(--rpt-color-border-subtle, #e2e8f0)}.compact-nav-btn:hover rpt-icon{color:var(--rpt-color-text, #0f172a)}.compact-nav-btn.active{background:var(--rpt-color-primary-light, #eff6ff);color:var(--rpt-color-primary, #2563eb);border-color:#2563eb40;font-weight:var(--rpt-font-weight-semibold, 600);box-shadow:0 1px 2px #2563eb14}.compact-nav-btn.active rpt-icon{color:var(--rpt-color-primary, #2563eb)}.compact-nav-btn:focus-visible{outline:2px solid var(--rpt-color-primary, #2563eb);outline-offset:1px}.workbench-drawer-backdrop{display:block;position:absolute;inset:0;background:#0f172a59;backdrop-filter:blur(1px);z-index:50}.workbench-left-container{position:absolute;left:0;top:0;bottom:0;width:280px;max-width:85vw;z-index:60;box-shadow:4px 0 24px #00000026;transform:translate(0);transition:transform .25s cubic-bezier(.16,1,.3,1)}.workbench-left-container.is-collapsed{transform:translate(-100%);width:280px!important}.workbench-right-container{position:absolute;right:0;top:0;bottom:0;width:380px;max-width:95vw;z-index:60;box-shadow:-4px 0 24px #00000026;transform:translate(0);transition:transform .25s cubic-bezier(.16,1,.3,1)}.workbench-right-container.is-collapsed{transform:translate(100%);width:380px!important}}.preview-mode-container{flex:1;overflow-y:auto;padding:16px;background-color:#f1f5f9;box-sizing:border-box}.preview-banner{max-width:1400px;margin:0 auto 16px;background:#eff6ff;color:#1e40af;border:1px solid #bfdbfe;padding:10px 18px;border-radius:var(--rpt-border-radius-lg, 8px);font-size:var(--rpt-font-size-sm, .8rem);font-weight:600;display:flex;align-items:center;gap:8px;box-shadow:var(--rpt-box-shadow-xs, 0 1px 2px rgba(0, 0, 0, .04));box-sizing:border-box}.preview-content{max-width:1400px;margin:0 auto;background:#fff;border-radius:var(--rpt-border-radius-lg, 8px);border:1px solid var(--rpt-color-border, #cbd5e1);box-shadow:0 4px 6px -1px #0000000d;padding:0;min-height:480px;box-sizing:border-box}.json-mode-container{flex:1;overflow-y:auto;padding:24px;background-color:#0f172a;display:flex;flex-direction:column}.json-banner{display:flex;align-items:center;justify-content:space-between;color:#94a3b8;font-size:.8rem;font-weight:600;margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid #1e293b}.btn-copy-json{background:#ffffff1a;color:#fff;border:1px solid rgba(255,255,255,.2);border-radius:4px;padding:4px 12px;font-size:.75rem;cursor:pointer;transition:all .15s ease}.btn-copy-json:hover{background:#fff3}.json-display{flex:1;margin:0;padding:20px;background:#090d16;border-radius:8px;border:1px solid #1e293b;color:#38bdf8;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.85rem;line-height:1.5;overflow-x:auto}.save-toast{position:absolute;bottom:24px;right:24px;background:#10b981;color:#fff;padding:10px 20px;border-radius:6px;font-size:.85rem;font-weight:600;box-shadow:0 10px 15px -3px #0000001a;z-index:100;animation:rpt-fade-in .2s ease}@keyframes rpt-fade-in{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$2.CdkDropListGroup, selector: "[cdkDropListGroup]", inputs: ["cdkDropListGroupDisabled"], exportAs: ["cdkDropListGroup"] }, { kind: "component", type: DesignerToolbarComponent, selector: "rpt-designer-toolbar", inputs: ["isLeftPanelCollapsed", "isRightPanelCollapsed"], outputs: ["toggleLeftPanel", "toggleRightPanel", "saveRequested"] }, { kind: "component", type: ComponentPaletteComponent, selector: "rpt-component-palette", outputs: ["collapseRequested"] }, { kind: "component", type: DesignCanvasComponent, selector: "rpt-design-canvas" }, { kind: "component", type: PropertyEditorComponent, selector: "rpt-property-editor", outputs: ["collapseRequested"] }, { kind: "component", type: DynamicRendererComponent, selector: "rpt-dynamic-renderer", inputs: ["definition", "initialValues", "resultData"], outputs: ["executeRequested", "resetRequested", "exportRequested", "printRequested", "customActionTriggered", "gridStateChanged", "columnDefsChanged", "selectionChange", "selectionChanged", "rowActionTriggered"] }, { kind: "component", type: RptIconComponent, selector: "rpt-icon", inputs: ["name", "size", "color", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
12443
13152
|
}
|
|
12444
13153
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DesignerComponent, decorators: [{
|
|
12445
13154
|
type: Component,
|
|
@@ -12579,7 +13288,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
12579
13288
|
</div>
|
|
12580
13289
|
}
|
|
12581
13290
|
</div>
|
|
12582
|
-
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;width:100%;height:100%;min-height:400px;font-family:var(--rpt-font-family, inherit);box-sizing:border-box}.designer-shell{display:flex;flex-direction:column;height:100%;background-color:#f8fafc;position:relative;overflow:hidden}.designer-workbench{display:flex;flex:1;overflow:hidden;position:relative;min-height:0}.workbench-left-container{display:flex;position:relative;width:250px;flex-shrink:0;height:100%;transition:width var(--rpt-transition-speed-smooth, .25s) cubic-bezier(.16,1,.3,1);z-index:20}.workbench-left-container.is-collapsed{width:0!important}.workbench-left-panel{width:100%;height:100%;overflow:hidden;border-right:1px solid var(--rpt-color-border, #cbd5e1);background:#fff;box-sizing:border-box}.workbench-left-container.is-collapsed .workbench-left-panel{visibility:hidden;border-right:none}.workbench-center-panel{flex:1;height:100%;overflow:hidden;display:flex;flex-direction:column;min-width:0;position:relative}.workbench-right-container{display:flex;position:relative;width:
|
|
13291
|
+
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;width:100%;height:100%;min-height:400px;font-family:var(--rpt-font-family, inherit);box-sizing:border-box}.designer-shell{display:flex;flex-direction:column;height:100%;background-color:#f8fafc;position:relative;overflow:hidden}.designer-workbench{display:flex;flex:1;overflow:hidden;position:relative;min-height:0}.workbench-left-container{display:flex;position:relative;width:250px;flex-shrink:0;height:100%;transition:width var(--rpt-transition-speed-smooth, .25s) cubic-bezier(.16,1,.3,1);z-index:20}.workbench-left-container.is-collapsed{width:0!important}.workbench-left-panel{width:100%;height:100%;overflow:hidden;border-right:1px solid var(--rpt-color-border, #cbd5e1);background:#fff;box-sizing:border-box}.workbench-left-container.is-collapsed .workbench-left-panel{visibility:hidden;border-right:none}.workbench-center-panel{flex:1;height:100%;overflow:hidden;display:flex;flex-direction:column;min-width:0;position:relative}.workbench-right-container{display:flex;position:relative;width:420px;flex-shrink:0;height:100%;transition:width var(--rpt-transition-speed-smooth, .25s) cubic-bezier(.16,1,.3,1);z-index:20}.workbench-right-container.is-collapsed{width:0!important}.workbench-right-panel{width:100%;height:100%;overflow:hidden;border-left:1px solid var(--rpt-color-border, #cbd5e1);background:#fff;box-sizing:border-box}.workbench-right-container.is-collapsed .workbench-right-panel{visibility:hidden;border-left:none}.compact-workbench-bar,.workbench-drawer-backdrop{display:none}@media (max-width: 900px){.compact-workbench-bar{display:flex;align-items:center;justify-content:space-around;height:48px;background:var(--rpt-color-surface, #ffffff);border-top:1px solid var(--rpt-color-border, #cbd5e1);padding:0 12px;gap:8px;z-index:45;flex-shrink:0;box-shadow:0 -2px 8px #0000000a;font-family:var(--rpt-font-family, inherit)}.compact-nav-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:6px 14px;border-radius:var(--rpt-border-radius, 6px);border:1px solid transparent;background:transparent;font-family:var(--rpt-font-family, inherit);font-size:var(--rpt-font-size-sm, .75rem);font-weight:var(--rpt-font-weight-medium, 500);letter-spacing:-.01em;line-height:1.4;color:var(--rpt-color-text-secondary, #334155);cursor:pointer;-webkit-user-select:none;user-select:none;transition:all var(--rpt-transition-speed, .15s) ease;white-space:nowrap}.compact-nav-btn rpt-icon{display:inline-flex;align-items:center;justify-content:center;color:var(--rpt-color-text-muted, #64748b);transition:color var(--rpt-transition-speed, .15s) ease}.compact-nav-btn:hover{background:var(--rpt-color-surface-hover, #f1f5f9);color:var(--rpt-color-text, #0f172a);border-color:var(--rpt-color-border-subtle, #e2e8f0)}.compact-nav-btn:hover rpt-icon{color:var(--rpt-color-text, #0f172a)}.compact-nav-btn.active{background:var(--rpt-color-primary-light, #eff6ff);color:var(--rpt-color-primary, #2563eb);border-color:#2563eb40;font-weight:var(--rpt-font-weight-semibold, 600);box-shadow:0 1px 2px #2563eb14}.compact-nav-btn.active rpt-icon{color:var(--rpt-color-primary, #2563eb)}.compact-nav-btn:focus-visible{outline:2px solid var(--rpt-color-primary, #2563eb);outline-offset:1px}.workbench-drawer-backdrop{display:block;position:absolute;inset:0;background:#0f172a59;backdrop-filter:blur(1px);z-index:50}.workbench-left-container{position:absolute;left:0;top:0;bottom:0;width:280px;max-width:85vw;z-index:60;box-shadow:4px 0 24px #00000026;transform:translate(0);transition:transform .25s cubic-bezier(.16,1,.3,1)}.workbench-left-container.is-collapsed{transform:translate(-100%);width:280px!important}.workbench-right-container{position:absolute;right:0;top:0;bottom:0;width:380px;max-width:95vw;z-index:60;box-shadow:-4px 0 24px #00000026;transform:translate(0);transition:transform .25s cubic-bezier(.16,1,.3,1)}.workbench-right-container.is-collapsed{transform:translate(100%);width:380px!important}}.preview-mode-container{flex:1;overflow-y:auto;padding:16px;background-color:#f1f5f9;box-sizing:border-box}.preview-banner{max-width:1400px;margin:0 auto 16px;background:#eff6ff;color:#1e40af;border:1px solid #bfdbfe;padding:10px 18px;border-radius:var(--rpt-border-radius-lg, 8px);font-size:var(--rpt-font-size-sm, .8rem);font-weight:600;display:flex;align-items:center;gap:8px;box-shadow:var(--rpt-box-shadow-xs, 0 1px 2px rgba(0, 0, 0, .04));box-sizing:border-box}.preview-content{max-width:1400px;margin:0 auto;background:#fff;border-radius:var(--rpt-border-radius-lg, 8px);border:1px solid var(--rpt-color-border, #cbd5e1);box-shadow:0 4px 6px -1px #0000000d;padding:0;min-height:480px;box-sizing:border-box}.json-mode-container{flex:1;overflow-y:auto;padding:24px;background-color:#0f172a;display:flex;flex-direction:column}.json-banner{display:flex;align-items:center;justify-content:space-between;color:#94a3b8;font-size:.8rem;font-weight:600;margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid #1e293b}.btn-copy-json{background:#ffffff1a;color:#fff;border:1px solid rgba(255,255,255,.2);border-radius:4px;padding:4px 12px;font-size:.75rem;cursor:pointer;transition:all .15s ease}.btn-copy-json:hover{background:#fff3}.json-display{flex:1;margin:0;padding:20px;background:#090d16;border-radius:8px;border:1px solid #1e293b;color:#38bdf8;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.85rem;line-height:1.5;overflow-x:auto}.save-toast{position:absolute;bottom:24px;right:24px;background:#10b981;color:#fff;padding:10px 20px;border-radius:6px;font-size:.85rem;font-weight:600;box-shadow:0 10px 15px -3px #0000001a;z-index:100;animation:rpt-fade-in .2s ease}@keyframes rpt-fade-in{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}\n"] }]
|
|
12583
13292
|
}], ctorParameters: () => [], propDecorators: { onResize: [{
|
|
12584
13293
|
type: HostListener,
|
|
12585
13294
|
args: ['window:resize']
|
|
@@ -13491,6 +14200,7 @@ const DEFAULT_CONFIG = {
|
|
|
13491
14200
|
dateFormat: 'dd/MM/yyyy',
|
|
13492
14201
|
enableDesigner: false,
|
|
13493
14202
|
schemaVersion: 1,
|
|
14203
|
+
lookupTimeoutMs: 300000,
|
|
13494
14204
|
};
|
|
13495
14205
|
/**
|
|
13496
14206
|
* Kinds of reporting features available in `provideBwaysReport`.
|
|
@@ -13520,10 +14230,17 @@ function withHttpAdapters(options) {
|
|
|
13520
14230
|
const cleanUrl = options.apiBaseUrl.replace(/\/+$/, '');
|
|
13521
14231
|
return {
|
|
13522
14232
|
ɵkind: ReportingFeatureKind.HttpAdapters,
|
|
14233
|
+
ɵconfig: {
|
|
14234
|
+
apiBaseUrl: cleanUrl,
|
|
14235
|
+
...(options.lookupTimeoutMs ? { lookupTimeoutMs: options.lookupTimeoutMs } : {}),
|
|
14236
|
+
},
|
|
13523
14237
|
ɵproviders: [
|
|
13524
14238
|
{
|
|
13525
14239
|
provide: REPORTING_CONFIG,
|
|
13526
|
-
useValue: {
|
|
14240
|
+
useValue: {
|
|
14241
|
+
apiBaseUrl: cleanUrl,
|
|
14242
|
+
...(options.lookupTimeoutMs ? { lookupTimeoutMs: options.lookupTimeoutMs } : {}),
|
|
14243
|
+
},
|
|
13527
14244
|
},
|
|
13528
14245
|
{
|
|
13529
14246
|
provide: ReportDataSourceAdapter,
|
|
@@ -13590,11 +14307,15 @@ const withMockAdapters = withLocalStorage;
|
|
|
13590
14307
|
*/
|
|
13591
14308
|
function provideBwaysReport(options = {}, ...features) {
|
|
13592
14309
|
const config = { ...DEFAULT_CONFIG, ...options };
|
|
14310
|
+
for (const feature of features) {
|
|
14311
|
+
if (feature.ɵconfig) {
|
|
14312
|
+
Object.assign(config, feature.ɵconfig);
|
|
14313
|
+
}
|
|
14314
|
+
}
|
|
13593
14315
|
if (config.apiBaseUrl) {
|
|
13594
14316
|
config.apiBaseUrl = config.apiBaseUrl.replace(/\/+$/, '');
|
|
13595
14317
|
}
|
|
13596
14318
|
const providers = [
|
|
13597
|
-
{ provide: REPORTING_CONFIG, useValue: config },
|
|
13598
14319
|
{
|
|
13599
14320
|
provide: ENVIRONMENT_INITIALIZER,
|
|
13600
14321
|
multi: true,
|
|
@@ -13606,10 +14327,13 @@ function provideBwaysReport(options = {}, ...features) {
|
|
|
13606
14327
|
];
|
|
13607
14328
|
const hasHttpFeature = features.some((f) => f.ɵkind === ReportingFeatureKind.HttpAdapters);
|
|
13608
14329
|
const hasLocalFeature = features.some((f) => f.ɵkind === ReportingFeatureKind.LocalStorage);
|
|
13609
|
-
// Append feature providers
|
|
14330
|
+
// Append feature providers (excluding standalone REPORTING_CONFIG so merged config wins)
|
|
13610
14331
|
for (const feature of features) {
|
|
13611
|
-
providers.
|
|
14332
|
+
const nonConfigProviders = feature.ɵproviders.filter((p) => !(typeof p === 'object' && p !== null && 'provide' in p && p.provide === REPORTING_CONFIG));
|
|
14333
|
+
providers.push(...nonConfigProviders);
|
|
13612
14334
|
}
|
|
14335
|
+
// Unified merged configuration
|
|
14336
|
+
providers.push({ provide: REPORTING_CONFIG, useValue: config });
|
|
13613
14337
|
// Auto-provision HTTP adapters if apiBaseUrl was supplied as a 1-line option
|
|
13614
14338
|
if (!hasHttpFeature && !hasLocalFeature && options.apiBaseUrl) {
|
|
13615
14339
|
providers.push({ provide: ReportDataSourceAdapter, useClass: options.dataSourceAdapter ?? HttpReportDataSourceAdapter }, { provide: ReportPersistenceAdapter, useClass: options.persistenceAdapter ?? HttpReportPersistenceAdapter }, { provide: MasterDataProvider, useClass: options.masterDataProvider ?? HttpMasterDataAdapter }, { provide: ReportSchemaDiscoveryAdapter, useClass: options.schemaDiscoveryAdapter ?? HttpReportSchemaDiscoveryAdapter }, { provide: ReportLookupAdapter, useClass: options.lookupAdapter ?? HttpReportLookupAdapter });
|
|
@@ -13690,5 +14414,5 @@ const bwaysReportProviders = provideBwaysReport;
|
|
|
13690
14414
|
* Generated bundle index. Do not edit.
|
|
13691
14415
|
*/
|
|
13692
14416
|
|
|
13693
|
-
export { AdapterNotProvidedError, BrowserReportStorage, CHART_COMPONENT, ComponentNotRegisteredError, ComponentPaletteComponent, ComponentRegistry, DataSourceError, DesignCanvasComponent, DesignerComponent, DesignerStateService, DesignerToolbarComponent, DynamicFormBuilder, DynamicRendererComponent, GRID_COMPONENT, HttpDataSourceAdapter, HttpLookupAdapter, HttpMasterDataAdapter, HttpPersistenceAdapter, HttpReportDataSourceAdapter, HttpReportLookupAdapter, HttpReportPersistenceAdapter, HttpReportSchemaDiscoveryAdapter, HttpSchemaDiscoveryAdapter, LocalStorageReportPersistenceAdapter, MasterDataProvider, MemoryReportStorage, MockReportDataSourceAdapter, PropertyEditorActionComponent, PropertyEditorComponent, REPORTING_CONFIG, REPORT_SESSION_CONTEXT, REPORT_SESSION_PROVIDER, REPORT_STORAGE, ReportChartBridgeComponent, ReportDataSourceAdapter, ReportDefinitionError, ReportExportService, ReportGridBridgeComponent, ReportLookupAdapter, ReportMenuService, ReportPersistenceAdapter, ReportRuntimeComponent, ReportSchemaDiscoveryAdapter, ReportSessionContextService, ReportStorageService, ReportStoreService, ReportTemplateService, ReportTranslationAdapter, ReportVersioningService, ReportingFeatureKind, RoutedReportDesignerComponent, RoutedReportRuntimeComponent, RptButtonComponent, RptCheckboxComponent, RptDateComponent, RptDateRangeComponent, RptDropdownComponent, RptIconComponent, RptInputComponent, RptKpiCardComponent, RptNumberComponent, RptReportComponent, RptReportDesignerComponent, SchemaValidatorService, SchemaVersionError, SessionContextProvider, bwaysReportProviders, createReportRoutes, createValidators, extractRowArray, findMissingCanvasFields, formatFilterSummary, generateCsvContent, generateExcelXml, generateHtmlContent, generateId, getFieldCaseInsensitive, getValidationMessage, groupReportsByCategory, migrateDefinition, provideBwaysReport, provideBwaysReporting, provideReporting, registerBuiltinComponents, reportResolver, resolveCanvasFieldValue, resolveContextPreset, resolveExecutionDataSource, resolveParameterMappings, resolveParameterValue, sanitizeCsvCell, toFieldKey, validateReportSchema, withHttpAdapters, withLocalStorage, withMockAdapters };
|
|
14417
|
+
export { AdapterNotProvidedError, BrowserReportStorage, CHART_COMPONENT, ComponentNotRegisteredError, ComponentPaletteComponent, ComponentRegistry, DataSourceError, DesignCanvasComponent, DesignerComponent, DesignerStateService, DesignerToolbarComponent, DynamicFormBuilder, DynamicRendererComponent, GRID_COMPONENT, HttpDataSourceAdapter, HttpLookupAdapter, HttpMasterDataAdapter, HttpPersistenceAdapter, HttpReportDataSourceAdapter, HttpReportLookupAdapter, HttpReportPersistenceAdapter, HttpReportSchemaDiscoveryAdapter, HttpSchemaDiscoveryAdapter, LocalStorageReportPersistenceAdapter, MasterDataProvider, MemoryReportStorage, MockReportDataSourceAdapter, PropertyEditorActionComponent, PropertyEditorComponent, REPORTING_CONFIG, REPORT_SESSION_CONTEXT, REPORT_SESSION_PROVIDER, REPORT_STORAGE, ReportChartBridgeComponent, ReportDataDiscoveryService, ReportDataSourceAdapter, ReportDefinitionError, ReportExportService, ReportGridBridgeComponent, ReportLookupAdapter, ReportMenuService, ReportPersistenceAdapter, ReportRuntimeComponent, ReportSchemaDiscoveryAdapter, ReportSessionContextService, ReportStorageService, ReportStoreService, ReportTemplateService, ReportTranslationAdapter, ReportVersioningService, ReportingFeatureKind, RoutedReportDesignerComponent, RoutedReportRuntimeComponent, RptButtonComponent, RptCheckboxComponent, RptDateComponent, RptDateRangeComponent, RptDropdownComponent, RptIconComponent, RptInputComponent, RptKpiCardComponent, RptNumberComponent, RptReportComponent, RptReportDesignerComponent, SchemaValidatorService, SchemaVersionError, SessionContextProvider, bwaysReportProviders, createReportRoutes, createValidators, extractRowArray, findMissingCanvasFields, formatFilterSummary, generateCsvContent, generateExcelXml, generateHtmlContent, generateId, getFieldCaseInsensitive, getValidationMessage, groupReportsByCategory, migrateDefinition, provideBwaysReport, provideBwaysReporting, provideReporting, registerBuiltinComponents, reportResolver, resolveCanvasFieldValue, resolveContextPreset, resolveExecutionDataSource, resolveParameterMappings, resolveParameterValue, sanitizeCsvCell, toFieldKey, validateReportSchema, withHttpAdapters, withLocalStorage, withMockAdapters };
|
|
13694
14418
|
//# sourceMappingURL=bways-report.mjs.map
|