@gridengine/angular-datagrid-enterprise 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1083,6 +1083,646 @@ class FormulaEngine {
|
|
|
1083
1083
|
}
|
|
1084
1084
|
}
|
|
1085
1085
|
|
|
1086
|
+
class SSRMEngine {
|
|
1087
|
+
_dataSource;
|
|
1088
|
+
blockSize;
|
|
1089
|
+
maxBlocksInCache;
|
|
1090
|
+
_blocks = new Map();
|
|
1091
|
+
_accessClock = 0;
|
|
1092
|
+
_totalCount = null;
|
|
1093
|
+
_filterModel = {};
|
|
1094
|
+
_sortModel = [];
|
|
1095
|
+
_groupKeys = [];
|
|
1096
|
+
_onChange;
|
|
1097
|
+
constructor(options) {
|
|
1098
|
+
this._dataSource = options.dataSource;
|
|
1099
|
+
this.blockSize = options.blockSize ?? 100;
|
|
1100
|
+
this.maxBlocksInCache = options.maxBlocksInCache ?? 10;
|
|
1101
|
+
}
|
|
1102
|
+
subscribe(listener) {
|
|
1103
|
+
this._onChange = listener;
|
|
1104
|
+
return () => {
|
|
1105
|
+
if (this._onChange === listener)
|
|
1106
|
+
this._onChange = undefined;
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
setFilterModel(model) {
|
|
1110
|
+
this._filterModel = model;
|
|
1111
|
+
this._invalidateAll();
|
|
1112
|
+
}
|
|
1113
|
+
setSortModel(model) {
|
|
1114
|
+
this._sortModel = model;
|
|
1115
|
+
this._invalidateAll();
|
|
1116
|
+
}
|
|
1117
|
+
setGroupKeys(keys) {
|
|
1118
|
+
this._groupKeys = keys;
|
|
1119
|
+
this._invalidateAll();
|
|
1120
|
+
}
|
|
1121
|
+
/** Total row count from the last server response (null = not yet known). */
|
|
1122
|
+
getTotalCount() {
|
|
1123
|
+
return this._totalCount;
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Get the row at an absolute zero-based index, fetching the containing block
|
|
1127
|
+
* if needed. Returns null while the block is loading.
|
|
1128
|
+
*/
|
|
1129
|
+
getRow(rowIndex) {
|
|
1130
|
+
const blockIndex = Math.floor(rowIndex / this.blockSize);
|
|
1131
|
+
const block = this._getOrCreateBlock(blockIndex);
|
|
1132
|
+
if (block.state === 'loaded') {
|
|
1133
|
+
const localIndex = rowIndex - blockIndex * this.blockSize;
|
|
1134
|
+
return block.rows[localIndex] ?? null;
|
|
1135
|
+
}
|
|
1136
|
+
return null;
|
|
1137
|
+
}
|
|
1138
|
+
/** Rows for the viewport [startRow, endRow); missing slots are null. */
|
|
1139
|
+
getRowSlice(startRow, endRow) {
|
|
1140
|
+
const result = [];
|
|
1141
|
+
for (let i = startRow; i < endRow; i++) {
|
|
1142
|
+
result.push(this.getRow(i));
|
|
1143
|
+
}
|
|
1144
|
+
return result;
|
|
1145
|
+
}
|
|
1146
|
+
getBlockState(blockIndex) {
|
|
1147
|
+
return this._blocks.get(blockIndex)?.state ?? 'idle';
|
|
1148
|
+
}
|
|
1149
|
+
getBlocks() {
|
|
1150
|
+
return this._blocks;
|
|
1151
|
+
}
|
|
1152
|
+
/** Evict all cached blocks and reset total count. */
|
|
1153
|
+
invalidateAll() {
|
|
1154
|
+
this._invalidateAll();
|
|
1155
|
+
}
|
|
1156
|
+
/** Evict a specific block (e.g. after a row mutation on that page). */
|
|
1157
|
+
invalidateBlock(blockIndex) {
|
|
1158
|
+
this._blocks.delete(blockIndex);
|
|
1159
|
+
this._notify();
|
|
1160
|
+
}
|
|
1161
|
+
_getOrCreateBlock(blockIndex) {
|
|
1162
|
+
let block = this._blocks.get(blockIndex);
|
|
1163
|
+
if (!block) {
|
|
1164
|
+
if (this._blocks.size >= this.maxBlocksInCache) {
|
|
1165
|
+
this._evictOldest();
|
|
1166
|
+
}
|
|
1167
|
+
block = { index: blockIndex, state: 'idle', rows: [], lastAccessed: ++this._accessClock };
|
|
1168
|
+
this._blocks.set(blockIndex, block);
|
|
1169
|
+
}
|
|
1170
|
+
block.lastAccessed = ++this._accessClock;
|
|
1171
|
+
if (block.state === 'idle' || block.state === 'error') {
|
|
1172
|
+
this._fetchBlock(block);
|
|
1173
|
+
}
|
|
1174
|
+
return block;
|
|
1175
|
+
}
|
|
1176
|
+
_fetchBlock(block, retryCount = 0) {
|
|
1177
|
+
block.state = 'loading';
|
|
1178
|
+
this._notify();
|
|
1179
|
+
const startRow = block.index * this.blockSize;
|
|
1180
|
+
const params = {
|
|
1181
|
+
startRow,
|
|
1182
|
+
endRow: startRow + this.blockSize,
|
|
1183
|
+
filterModel: this._filterModel,
|
|
1184
|
+
sortModel: this._sortModel,
|
|
1185
|
+
groupKeys: this._groupKeys,
|
|
1186
|
+
};
|
|
1187
|
+
this._dataSource
|
|
1188
|
+
.getRows(params)
|
|
1189
|
+
.then((result) => {
|
|
1190
|
+
block.state = 'loaded';
|
|
1191
|
+
block.rows = result.rows;
|
|
1192
|
+
block.error = undefined;
|
|
1193
|
+
this._totalCount = result.totalCount;
|
|
1194
|
+
this._notify();
|
|
1195
|
+
})
|
|
1196
|
+
.catch((err) => {
|
|
1197
|
+
if (retryCount < 3) {
|
|
1198
|
+
const delay = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s
|
|
1199
|
+
setTimeout(() => this._fetchBlock(block, retryCount + 1), delay);
|
|
1200
|
+
}
|
|
1201
|
+
else {
|
|
1202
|
+
block.state = 'error';
|
|
1203
|
+
block.error = err;
|
|
1204
|
+
this._notify();
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
_evictOldest() {
|
|
1209
|
+
let oldest = null;
|
|
1210
|
+
for (const block of this._blocks.values()) {
|
|
1211
|
+
if (!oldest || block.lastAccessed < oldest.lastAccessed) {
|
|
1212
|
+
oldest = block;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
if (oldest) {
|
|
1216
|
+
this._blocks.delete(oldest.index);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
_invalidateAll() {
|
|
1220
|
+
this._blocks.clear();
|
|
1221
|
+
this._totalCount = null;
|
|
1222
|
+
this._notify();
|
|
1223
|
+
}
|
|
1224
|
+
_notify() {
|
|
1225
|
+
this._onChange?.();
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
class TransactionEngine {
|
|
1230
|
+
_rowIdField;
|
|
1231
|
+
_onCommit;
|
|
1232
|
+
_onRollback;
|
|
1233
|
+
_baseRows;
|
|
1234
|
+
_dirty = new Map();
|
|
1235
|
+
constructor(opts) {
|
|
1236
|
+
this._rowIdField = opts.rowIdField ?? 'id';
|
|
1237
|
+
this._onCommit = opts.onTransactionCommit;
|
|
1238
|
+
this._onRollback = opts.onTransactionRollback;
|
|
1239
|
+
this._baseRows = [...opts.initialRows];
|
|
1240
|
+
}
|
|
1241
|
+
/** Stage new rows; an existing ID is treated as an update instead. */
|
|
1242
|
+
addRows(rows) {
|
|
1243
|
+
for (const row of rows) {
|
|
1244
|
+
const id = this._rowId(row);
|
|
1245
|
+
const existing = this._findBase(id);
|
|
1246
|
+
if (existing !== null) {
|
|
1247
|
+
this._stageUpdate(id, existing, row);
|
|
1248
|
+
}
|
|
1249
|
+
else {
|
|
1250
|
+
this._dirty.set(id, { state: 'added', original: null, current: { ...row } });
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
/** Stage updates for existing rows; unknown IDs are ignored with a warning. */
|
|
1255
|
+
updateRows(rows) {
|
|
1256
|
+
for (const row of rows) {
|
|
1257
|
+
const id = this._rowId(row);
|
|
1258
|
+
const existing = this._dirty.get(id);
|
|
1259
|
+
if (existing?.state === 'added') {
|
|
1260
|
+
existing.current = { ...existing.current, ...row };
|
|
1261
|
+
}
|
|
1262
|
+
else {
|
|
1263
|
+
const base = this._findBase(id);
|
|
1264
|
+
if (base === null) {
|
|
1265
|
+
console.warn(`TransactionEngine.updateRows: row with id '${id}' not found.`);
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
this._stageUpdate(id, existing?.original ?? base, row);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
/** Stage rows for removal by ID; a still-staged add is simply cancelled. */
|
|
1273
|
+
removeRows(ids) {
|
|
1274
|
+
for (const id of ids) {
|
|
1275
|
+
const existing = this._dirty.get(id);
|
|
1276
|
+
if (existing?.state === 'added') {
|
|
1277
|
+
this._dirty.delete(id);
|
|
1278
|
+
continue;
|
|
1279
|
+
}
|
|
1280
|
+
const base = this._findBase(id);
|
|
1281
|
+
if (base === null) {
|
|
1282
|
+
console.warn(`TransactionEngine.removeRows: row with id '${id}' not found.`);
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
this._dirty.set(id, { state: 'removed', original: existing?.original ?? base, current: base });
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
commitTransaction() {
|
|
1289
|
+
const delta = this._buildDelta();
|
|
1290
|
+
for (const [id, entry] of this._dirty) {
|
|
1291
|
+
if (entry.state === 'added') {
|
|
1292
|
+
this._baseRows.push(entry.current);
|
|
1293
|
+
}
|
|
1294
|
+
else if (entry.state === 'updated') {
|
|
1295
|
+
const idx = this._baseRows.findIndex((r) => this._rowId(r) === id);
|
|
1296
|
+
if (idx !== -1)
|
|
1297
|
+
this._baseRows[idx] = entry.current;
|
|
1298
|
+
}
|
|
1299
|
+
else if (entry.state === 'removed') {
|
|
1300
|
+
this._baseRows = this._baseRows.filter((r) => this._rowId(r) !== id);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
this._dirty.clear();
|
|
1304
|
+
this._onCommit?.(delta);
|
|
1305
|
+
}
|
|
1306
|
+
rollbackTransaction() {
|
|
1307
|
+
this._dirty.clear();
|
|
1308
|
+
this._onRollback?.();
|
|
1309
|
+
}
|
|
1310
|
+
/** True if there are any staged (uncommitted) changes. */
|
|
1311
|
+
isDirty() {
|
|
1312
|
+
return this._dirty.size > 0;
|
|
1313
|
+
}
|
|
1314
|
+
/** All current staged changes, grouped by kind. */
|
|
1315
|
+
getDirtyRows() {
|
|
1316
|
+
const added = [];
|
|
1317
|
+
const updated = [];
|
|
1318
|
+
const removed = [];
|
|
1319
|
+
for (const entry of this._dirty.values()) {
|
|
1320
|
+
if (entry.state === 'added')
|
|
1321
|
+
added.push(entry.current);
|
|
1322
|
+
else if (entry.state === 'updated')
|
|
1323
|
+
updated.push(entry.current);
|
|
1324
|
+
else if (entry.state === 'removed' && entry.original !== null)
|
|
1325
|
+
removed.push(entry.original);
|
|
1326
|
+
}
|
|
1327
|
+
return { added, updated, removed };
|
|
1328
|
+
}
|
|
1329
|
+
/** The merged row list: base rows with staged changes applied. */
|
|
1330
|
+
getDisplayRows() {
|
|
1331
|
+
const rows = [];
|
|
1332
|
+
for (const baseRow of this._baseRows) {
|
|
1333
|
+
const id = this._rowId(baseRow);
|
|
1334
|
+
const entry = this._dirty.get(id);
|
|
1335
|
+
if (!entry) {
|
|
1336
|
+
rows.push(baseRow);
|
|
1337
|
+
}
|
|
1338
|
+
else if (entry.state === 'updated') {
|
|
1339
|
+
rows.push(entry.current);
|
|
1340
|
+
}
|
|
1341
|
+
// 'removed' rows are omitted; 'added' rows are appended below.
|
|
1342
|
+
}
|
|
1343
|
+
for (const entry of this._dirty.values()) {
|
|
1344
|
+
if (entry.state === 'added')
|
|
1345
|
+
rows.push(entry.current);
|
|
1346
|
+
}
|
|
1347
|
+
return rows;
|
|
1348
|
+
}
|
|
1349
|
+
/** Replace the base rows (e.g. after a refresh); staged changes are kept. */
|
|
1350
|
+
resetRows(rows) {
|
|
1351
|
+
this._baseRows = [...rows];
|
|
1352
|
+
}
|
|
1353
|
+
_rowId(row) {
|
|
1354
|
+
const id = row[this._rowIdField];
|
|
1355
|
+
if (id === undefined || id === null) {
|
|
1356
|
+
throw new Error(`TransactionEngine: row has no '${this._rowIdField}' field. Set rowIdField to match your data.`);
|
|
1357
|
+
}
|
|
1358
|
+
return id;
|
|
1359
|
+
}
|
|
1360
|
+
_findBase(id) {
|
|
1361
|
+
return this._baseRows.find((r) => this._rowId(r) === id) ?? null;
|
|
1362
|
+
}
|
|
1363
|
+
_stageUpdate(id, original, next) {
|
|
1364
|
+
this._dirty.set(id, { state: 'updated', original, current: { ...original, ...next } });
|
|
1365
|
+
}
|
|
1366
|
+
_buildDelta() {
|
|
1367
|
+
const added = [];
|
|
1368
|
+
const updated = [];
|
|
1369
|
+
const removedIds = [];
|
|
1370
|
+
for (const [id, entry] of this._dirty) {
|
|
1371
|
+
if (entry.state === 'added')
|
|
1372
|
+
added.push(entry.current);
|
|
1373
|
+
else if (entry.state === 'updated')
|
|
1374
|
+
updated.push(entry.current);
|
|
1375
|
+
else if (entry.state === 'removed')
|
|
1376
|
+
removedIds.push(id);
|
|
1377
|
+
}
|
|
1378
|
+
return { added, updated, removedIds };
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
class MasterDetailEngine {
|
|
1383
|
+
_getDetailRowData;
|
|
1384
|
+
_onExpand;
|
|
1385
|
+
_rowIdField;
|
|
1386
|
+
_expanded = new Set();
|
|
1387
|
+
_cache = new Map();
|
|
1388
|
+
_onChange;
|
|
1389
|
+
constructor(options) {
|
|
1390
|
+
this._getDetailRowData = options.getDetailRowData;
|
|
1391
|
+
this._onExpand = options.onExpand;
|
|
1392
|
+
this._rowIdField = options.rowIdField ?? 'id';
|
|
1393
|
+
}
|
|
1394
|
+
/** Subscribe to state changes. Returns an unsubscribe function. */
|
|
1395
|
+
subscribe(listener) {
|
|
1396
|
+
this._onChange = listener;
|
|
1397
|
+
return () => {
|
|
1398
|
+
if (this._onChange === listener)
|
|
1399
|
+
this._onChange = undefined;
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
isExpanded(rowId) {
|
|
1403
|
+
return this._expanded.has(rowId);
|
|
1404
|
+
}
|
|
1405
|
+
getLoadState(rowId) {
|
|
1406
|
+
return this._cache.get(rowId)?.state ?? 'idle';
|
|
1407
|
+
}
|
|
1408
|
+
/** Cached detail rows, or an empty array if not yet loaded. */
|
|
1409
|
+
getDetailData(rowId) {
|
|
1410
|
+
return this._cache.get(rowId)?.data ?? [];
|
|
1411
|
+
}
|
|
1412
|
+
/** Expand a master row, triggering a load if not already cached. */
|
|
1413
|
+
expand(masterRow) {
|
|
1414
|
+
const id = this._rowId(masterRow);
|
|
1415
|
+
if (this._expanded.has(id))
|
|
1416
|
+
return;
|
|
1417
|
+
this._expanded.add(id);
|
|
1418
|
+
this._onExpand?.(masterRow, true);
|
|
1419
|
+
this._notify();
|
|
1420
|
+
this._ensureLoaded(masterRow, id);
|
|
1421
|
+
}
|
|
1422
|
+
/** Collapse a master row; cached data is retained for re-expansion. */
|
|
1423
|
+
collapse(masterRow) {
|
|
1424
|
+
const id = this._rowId(masterRow);
|
|
1425
|
+
if (!this._expanded.has(id))
|
|
1426
|
+
return;
|
|
1427
|
+
this._expanded.delete(id);
|
|
1428
|
+
this._onExpand?.(masterRow, false);
|
|
1429
|
+
this._notify();
|
|
1430
|
+
}
|
|
1431
|
+
toggle(masterRow) {
|
|
1432
|
+
if (this.isExpanded(this._rowId(masterRow))) {
|
|
1433
|
+
this.collapse(masterRow);
|
|
1434
|
+
}
|
|
1435
|
+
else {
|
|
1436
|
+
this.expand(masterRow);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
/** Pre-fetch detail data without expanding (e.g. hover prefetch). */
|
|
1440
|
+
prefetch(masterRow) {
|
|
1441
|
+
this._ensureLoaded(masterRow, this._rowId(masterRow));
|
|
1442
|
+
}
|
|
1443
|
+
/** Invalidate one row's cache, or the entire cache when rowId is omitted. */
|
|
1444
|
+
invalidateCache(rowId) {
|
|
1445
|
+
if (rowId !== undefined) {
|
|
1446
|
+
this._cache.delete(rowId);
|
|
1447
|
+
}
|
|
1448
|
+
else {
|
|
1449
|
+
this._cache.clear();
|
|
1450
|
+
}
|
|
1451
|
+
this._notify();
|
|
1452
|
+
}
|
|
1453
|
+
/** Collapse all expanded rows. Cache is preserved. */
|
|
1454
|
+
collapseAll() {
|
|
1455
|
+
this._expanded.clear();
|
|
1456
|
+
this._notify();
|
|
1457
|
+
}
|
|
1458
|
+
/** A read-only snapshot of currently expanded row IDs. */
|
|
1459
|
+
getExpandedIds() {
|
|
1460
|
+
return this._expanded;
|
|
1461
|
+
}
|
|
1462
|
+
_rowId(row) {
|
|
1463
|
+
const id = row[this._rowIdField];
|
|
1464
|
+
if (id === undefined || id === null) {
|
|
1465
|
+
throw new Error(`MasterDetailEngine: row has no '${this._rowIdField}' field. Set rowIdField to the correct field name.`);
|
|
1466
|
+
}
|
|
1467
|
+
return id;
|
|
1468
|
+
}
|
|
1469
|
+
_ensureLoaded(masterRow, id) {
|
|
1470
|
+
const existing = this._cache.get(id);
|
|
1471
|
+
if (existing && (existing.state === 'loading' || existing.state === 'loaded'))
|
|
1472
|
+
return;
|
|
1473
|
+
this._cache.set(id, { state: 'loading', data: [] });
|
|
1474
|
+
this._notify();
|
|
1475
|
+
this._getDetailRowData(masterRow)
|
|
1476
|
+
.then((data) => {
|
|
1477
|
+
this._cache.set(id, { state: 'loaded', data });
|
|
1478
|
+
this._notify();
|
|
1479
|
+
})
|
|
1480
|
+
.catch((error) => {
|
|
1481
|
+
this._cache.set(id, { state: 'error', data: [], error });
|
|
1482
|
+
this._notify();
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
_notify() {
|
|
1486
|
+
this._onChange?.();
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
/** Default mask string used for unreadable cells. */
|
|
1491
|
+
const DEFAULT_MASK = '●●●';
|
|
1492
|
+
class CellPermissionEngine {
|
|
1493
|
+
_policy;
|
|
1494
|
+
_rowIdField;
|
|
1495
|
+
_maskValue;
|
|
1496
|
+
_cache = new Map();
|
|
1497
|
+
constructor(options) {
|
|
1498
|
+
this._policy = options.policy;
|
|
1499
|
+
this._rowIdField = options.rowIdField ?? 'id';
|
|
1500
|
+
this._maskValue = options.maskValue ?? DEFAULT_MASK;
|
|
1501
|
+
}
|
|
1502
|
+
/** The resolved permission for a cell (cached). */
|
|
1503
|
+
getPermission(row, field) {
|
|
1504
|
+
const key = this._key(row, field);
|
|
1505
|
+
let perm = this._cache.get(key);
|
|
1506
|
+
if (!perm) {
|
|
1507
|
+
perm = this._policy(row, field);
|
|
1508
|
+
this._cache.set(key, perm);
|
|
1509
|
+
}
|
|
1510
|
+
return perm;
|
|
1511
|
+
}
|
|
1512
|
+
canRead(row, field) {
|
|
1513
|
+
return this.getPermission(row, field).canRead;
|
|
1514
|
+
}
|
|
1515
|
+
canEdit(row, field) {
|
|
1516
|
+
const perm = this.getPermission(row, field);
|
|
1517
|
+
return perm.canRead && perm.canEdit; // an unreadable cell can never be edited
|
|
1518
|
+
}
|
|
1519
|
+
/** The display value for a cell, masked when not readable. */
|
|
1520
|
+
getDisplayValue(row, field) {
|
|
1521
|
+
return this.canRead(row, field) ? row[field] : this._maskValue;
|
|
1522
|
+
}
|
|
1523
|
+
/** The value for copy/export; unreadable cells return '' so data never leaks. */
|
|
1524
|
+
getExportValue(row, field) {
|
|
1525
|
+
return this.canRead(row, field) ? row[field] : '';
|
|
1526
|
+
}
|
|
1527
|
+
/** True if the given value is the mask placeholder. */
|
|
1528
|
+
isMasked(value) {
|
|
1529
|
+
return value === this._maskValue;
|
|
1530
|
+
}
|
|
1531
|
+
get maskValue() {
|
|
1532
|
+
return this._maskValue;
|
|
1533
|
+
}
|
|
1534
|
+
/** Clear one row's cached permissions, or the whole cache when omitted. */
|
|
1535
|
+
clearCache(rowId) {
|
|
1536
|
+
if (rowId === undefined) {
|
|
1537
|
+
this._cache.clear();
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
const prefix = `${rowId}\u0000`;
|
|
1541
|
+
for (const key of this._cache.keys()) {
|
|
1542
|
+
if (key.startsWith(prefix)) {
|
|
1543
|
+
this._cache.delete(key);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
_key(row, field) {
|
|
1548
|
+
const id = row[this._rowIdField];
|
|
1549
|
+
if (id === undefined || id === null) {
|
|
1550
|
+
throw new Error(`CellPermissionEngine: row has no '${this._rowIdField}' field. Set rowIdField to match your data.`);
|
|
1551
|
+
}
|
|
1552
|
+
return `${id}\u0000${field}`;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
/**
|
|
1557
|
+
* AuditTrailEngine — records an immutable log of every cell edit (who/what/when
|
|
1558
|
+
* + prev/next values) in a bounded in-memory ring buffer, emitting each entry
|
|
1559
|
+
* via `onEntry` for persistence. Pure logic.
|
|
1560
|
+
*/
|
|
1561
|
+
class AuditTrailEngine {
|
|
1562
|
+
_userId;
|
|
1563
|
+
_onEntry;
|
|
1564
|
+
_maxEntries;
|
|
1565
|
+
_now;
|
|
1566
|
+
_entries = [];
|
|
1567
|
+
_onChange;
|
|
1568
|
+
constructor(options = {}) {
|
|
1569
|
+
this._userId = options.userId;
|
|
1570
|
+
this._onEntry = options.onEntry;
|
|
1571
|
+
this._maxEntries = options.maxEntries ?? 1000;
|
|
1572
|
+
this._now = options.now ?? Date.now;
|
|
1573
|
+
}
|
|
1574
|
+
subscribe(listener) {
|
|
1575
|
+
this._onChange = listener;
|
|
1576
|
+
return () => {
|
|
1577
|
+
if (this._onChange === listener)
|
|
1578
|
+
this._onChange = undefined;
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
/** Record a cell edit; no-op edits (prev === next) return null. */
|
|
1582
|
+
record(rowId, field, prevValue, nextValue) {
|
|
1583
|
+
if (Object.is(prevValue, nextValue))
|
|
1584
|
+
return null;
|
|
1585
|
+
const entry = {
|
|
1586
|
+
timestamp: this._now(),
|
|
1587
|
+
userId: this._userId,
|
|
1588
|
+
rowId,
|
|
1589
|
+
field,
|
|
1590
|
+
prevValue,
|
|
1591
|
+
nextValue,
|
|
1592
|
+
};
|
|
1593
|
+
this._entries.push(entry);
|
|
1594
|
+
if (this._entries.length > this._maxEntries) {
|
|
1595
|
+
this._entries.shift();
|
|
1596
|
+
}
|
|
1597
|
+
this._onEntry?.(entry);
|
|
1598
|
+
this._notify();
|
|
1599
|
+
return entry;
|
|
1600
|
+
}
|
|
1601
|
+
/** All entries, oldest first (read-only snapshot). */
|
|
1602
|
+
getEntries() {
|
|
1603
|
+
return this._entries;
|
|
1604
|
+
}
|
|
1605
|
+
/** Entries for a specific row, oldest first. */
|
|
1606
|
+
getEntriesForRow(rowId) {
|
|
1607
|
+
return this._entries.filter((e) => e.rowId === rowId);
|
|
1608
|
+
}
|
|
1609
|
+
/** Entries for a specific cell (row + field), oldest first. */
|
|
1610
|
+
getEntriesForCell(rowId, field) {
|
|
1611
|
+
return this._entries.filter((e) => e.rowId === rowId && e.field === field);
|
|
1612
|
+
}
|
|
1613
|
+
get size() {
|
|
1614
|
+
return this._entries.length;
|
|
1615
|
+
}
|
|
1616
|
+
/** Discard all entries. */
|
|
1617
|
+
clear() {
|
|
1618
|
+
this._entries = [];
|
|
1619
|
+
this._notify();
|
|
1620
|
+
}
|
|
1621
|
+
_notify() {
|
|
1622
|
+
this._onChange?.();
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
class RowLockEngine {
|
|
1627
|
+
_currentUserId;
|
|
1628
|
+
_getLockedRows;
|
|
1629
|
+
_onLockConflict;
|
|
1630
|
+
_rowIdField;
|
|
1631
|
+
_now;
|
|
1632
|
+
_locks = new Map();
|
|
1633
|
+
_onChange;
|
|
1634
|
+
constructor(options = {}) {
|
|
1635
|
+
this._currentUserId = options.currentUserId;
|
|
1636
|
+
this._getLockedRows = options.getLockedRows;
|
|
1637
|
+
this._onLockConflict = options.onLockConflict;
|
|
1638
|
+
this._rowIdField = options.rowIdField ?? 'id';
|
|
1639
|
+
this._now = options.now ?? Date.now;
|
|
1640
|
+
}
|
|
1641
|
+
subscribe(listener) {
|
|
1642
|
+
this._onChange = listener;
|
|
1643
|
+
return () => {
|
|
1644
|
+
if (this._onChange === listener)
|
|
1645
|
+
this._onChange = undefined;
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
/** Load locks from the async source and replace local state. */
|
|
1649
|
+
async refresh() {
|
|
1650
|
+
if (!this._getLockedRows)
|
|
1651
|
+
return;
|
|
1652
|
+
const locks = await this._getLockedRows();
|
|
1653
|
+
this._locks = new Map(locks.map((l) => [l.rowId, l]));
|
|
1654
|
+
this._notify();
|
|
1655
|
+
}
|
|
1656
|
+
/** Optimistically lock a row for a user. */
|
|
1657
|
+
lockRow(rowId, userId, displayName) {
|
|
1658
|
+
this._locks.set(rowId, {
|
|
1659
|
+
rowId,
|
|
1660
|
+
lockedByUserId: userId,
|
|
1661
|
+
lockedByDisplayName: displayName,
|
|
1662
|
+
lockedAt: this._now(),
|
|
1663
|
+
});
|
|
1664
|
+
this._notify();
|
|
1665
|
+
}
|
|
1666
|
+
/** Remove a lock. */
|
|
1667
|
+
unlockRow(rowId) {
|
|
1668
|
+
if (this._locks.delete(rowId))
|
|
1669
|
+
this._notify();
|
|
1670
|
+
}
|
|
1671
|
+
/** Remove all locks. */
|
|
1672
|
+
clear() {
|
|
1673
|
+
if (this._locks.size === 0)
|
|
1674
|
+
return;
|
|
1675
|
+
this._locks.clear();
|
|
1676
|
+
this._notify();
|
|
1677
|
+
}
|
|
1678
|
+
/** The lock for a row, or undefined if unlocked. */
|
|
1679
|
+
getLock(rowId) {
|
|
1680
|
+
return this._locks.get(rowId);
|
|
1681
|
+
}
|
|
1682
|
+
/** True if the row is locked by anyone. */
|
|
1683
|
+
isLocked(rowId) {
|
|
1684
|
+
return this._locks.has(rowId);
|
|
1685
|
+
}
|
|
1686
|
+
/** True if the row is locked by someone OTHER than the current user. */
|
|
1687
|
+
isLockedByOther(rowId) {
|
|
1688
|
+
const lock = this._locks.get(rowId);
|
|
1689
|
+
if (!lock)
|
|
1690
|
+
return false;
|
|
1691
|
+
return lock.lockedByUserId !== this._currentUserId;
|
|
1692
|
+
}
|
|
1693
|
+
/** All current locks. */
|
|
1694
|
+
getLocks() {
|
|
1695
|
+
return Array.from(this._locks.values());
|
|
1696
|
+
}
|
|
1697
|
+
get size() {
|
|
1698
|
+
return this._locks.size;
|
|
1699
|
+
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Whether the row may be edited by the current user. If it's locked by
|
|
1702
|
+
* another user, fires `onLockConflict` and returns false.
|
|
1703
|
+
*/
|
|
1704
|
+
canEditRow(row) {
|
|
1705
|
+
const rowId = this._rowId(row);
|
|
1706
|
+
const lock = this._locks.get(rowId);
|
|
1707
|
+
if (!lock)
|
|
1708
|
+
return true;
|
|
1709
|
+
if (lock.lockedByUserId === this._currentUserId)
|
|
1710
|
+
return true;
|
|
1711
|
+
this._onLockConflict?.(row, lock.lockedByDisplayName ?? lock.lockedByUserId);
|
|
1712
|
+
return false;
|
|
1713
|
+
}
|
|
1714
|
+
_rowId(row) {
|
|
1715
|
+
const id = row[this._rowIdField];
|
|
1716
|
+
if (id === undefined || id === null) {
|
|
1717
|
+
throw new Error(`RowLockEngine: row has no '${this._rowIdField}' field. Set rowIdField to match your data.`);
|
|
1718
|
+
}
|
|
1719
|
+
return id;
|
|
1720
|
+
}
|
|
1721
|
+
_notify() {
|
|
1722
|
+
this._onChange?.();
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1086
1726
|
/*
|
|
1087
1727
|
* Public API Surface of @gridengine/angular-datagrid-enterprise
|
|
1088
1728
|
*
|
|
@@ -1095,5 +1735,5 @@ class FormulaEngine {
|
|
|
1095
1735
|
* Generated bundle index. Do not edit.
|
|
1096
1736
|
*/
|
|
1097
1737
|
|
|
1098
|
-
export { ClipboardEngine, DataGridPro, FillHandleEngine, FormulaEngine, GridLicenseWatermark, LicenseManager, PRODUCT_ID, PURCHASE_URL, RangeSelectionEngine, UndoRedoManager, parseTSV, provideGridEngineLicense, toNumber, toTimestamp };
|
|
1738
|
+
export { AuditTrailEngine, CellPermissionEngine, ClipboardEngine, DEFAULT_MASK, DataGridPro, FillHandleEngine, FormulaEngine, GridLicenseWatermark, LicenseManager, MasterDetailEngine, PRODUCT_ID, PURCHASE_URL, RangeSelectionEngine, RowLockEngine, SSRMEngine, TransactionEngine, UndoRedoManager, parseTSV, provideGridEngineLicense, toNumber, toTimestamp };
|
|
1099
1739
|
//# sourceMappingURL=gridengine-angular-datagrid-enterprise.mjs.map
|