@gridengine/angular-datagrid-enterprise 0.4.0 → 0.6.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.
@@ -1487,6 +1487,500 @@ class MasterDetailEngine {
1487
1487
  }
1488
1488
  }
1489
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
+
1726
+ function toComparable(value) {
1727
+ if (value === null || value === undefined)
1728
+ return null;
1729
+ if (typeof value === 'number')
1730
+ return value;
1731
+ if (typeof value === 'boolean')
1732
+ return value ? 1 : 0;
1733
+ if (value instanceof Date)
1734
+ return value.getTime();
1735
+ return String(value);
1736
+ }
1737
+ function evaluateCondition(cond, row) {
1738
+ const raw = row[cond.field];
1739
+ const op = cond.operator;
1740
+ const isEmpty = raw === null || raw === undefined || raw === '';
1741
+ if (op === 'isEmpty')
1742
+ return isEmpty;
1743
+ if (op === 'isNotEmpty')
1744
+ return !isEmpty;
1745
+ const target = cond.value;
1746
+ switch (op) {
1747
+ case 'equals':
1748
+ return toComparable(raw) === toComparable(target);
1749
+ case 'notEquals':
1750
+ return toComparable(raw) !== toComparable(target);
1751
+ case 'contains':
1752
+ return String(raw ?? '')
1753
+ .toLowerCase()
1754
+ .includes(String(target ?? '').toLowerCase());
1755
+ case 'notContains':
1756
+ return !String(raw ?? '')
1757
+ .toLowerCase()
1758
+ .includes(String(target ?? '').toLowerCase());
1759
+ case 'startsWith':
1760
+ return String(raw ?? '')
1761
+ .toLowerCase()
1762
+ .startsWith(String(target ?? '').toLowerCase());
1763
+ case 'endsWith':
1764
+ return String(raw ?? '')
1765
+ .toLowerCase()
1766
+ .endsWith(String(target ?? '').toLowerCase());
1767
+ case 'greaterThan':
1768
+ case 'greaterThanOrEqual':
1769
+ case 'lessThan':
1770
+ case 'lessThanOrEqual': {
1771
+ const a = toComparable(raw);
1772
+ const b = toComparable(target);
1773
+ if (a === null || b === null || typeof a !== typeof b)
1774
+ return false;
1775
+ if (op === 'greaterThan')
1776
+ return a > b;
1777
+ if (op === 'greaterThanOrEqual')
1778
+ return a >= b;
1779
+ if (op === 'lessThan')
1780
+ return a < b;
1781
+ return a <= b;
1782
+ }
1783
+ default:
1784
+ return false;
1785
+ }
1786
+ }
1787
+ /** Evaluate a filter node against a row. */
1788
+ function evaluateFilter(node, row) {
1789
+ if (node.kind === 'condition') {
1790
+ return evaluateCondition(node, row);
1791
+ }
1792
+ const group = node;
1793
+ let result;
1794
+ if (group.children.length === 0) {
1795
+ result = true; // an empty group matches everything
1796
+ }
1797
+ else if (group.combinator === 'and') {
1798
+ result = group.children.every((child) => evaluateFilter(child, row));
1799
+ }
1800
+ else {
1801
+ result = group.children.some((child) => evaluateFilter(child, row));
1802
+ }
1803
+ return group.not ? !result : result;
1804
+ }
1805
+ function toUrlSafe(b64) {
1806
+ return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
1807
+ }
1808
+ function fromUrlSafe(s) {
1809
+ const b64 = s.replace(/-/g, '+').replace(/_/g, '/');
1810
+ const pad = b64.length % 4 === 0 ? '' : '='.repeat(4 - (b64.length % 4));
1811
+ return b64 + pad;
1812
+ }
1813
+ /** Serialize a filter tree to a URL-safe string. */
1814
+ function serializeFilter(node) {
1815
+ return toUrlSafe(btoa(JSON.stringify(node)));
1816
+ }
1817
+ /** Deserialize a URL-safe string back into a filter tree. Throws if invalid. */
1818
+ function deserializeFilter(encoded) {
1819
+ const parsed = JSON.parse(atob(fromUrlSafe(encoded)));
1820
+ if (!parsed || (parsed.kind !== 'condition' && parsed.kind !== 'group')) {
1821
+ throw new Error('Invalid filter payload');
1822
+ }
1823
+ return parsed;
1824
+ }
1825
+ class FilterPresetEngine {
1826
+ _onSave;
1827
+ _generateId;
1828
+ _presets;
1829
+ _seq = 0;
1830
+ _onChange;
1831
+ constructor(options = {}) {
1832
+ this._onSave = options.onSave;
1833
+ this._generateId = options.generateId ?? (() => `preset-${++this._seq}`);
1834
+ this._presets = [...(options.presets ?? [])];
1835
+ }
1836
+ subscribe(listener) {
1837
+ this._onChange = listener;
1838
+ return () => {
1839
+ if (this._onChange === listener)
1840
+ this._onChange = undefined;
1841
+ };
1842
+ }
1843
+ getPresets() {
1844
+ return this._presets;
1845
+ }
1846
+ getPreset(id) {
1847
+ return this._presets.find((p) => p.id === id);
1848
+ }
1849
+ /** Save a new preset (or replace one with the same name). Returns it. */
1850
+ savePreset(name, filter, isShared = false) {
1851
+ const existing = this._presets.find((p) => p.name === name);
1852
+ const preset = existing
1853
+ ? { ...existing, filter, isShared }
1854
+ : { id: this._generateId(), name, filter, isShared };
1855
+ if (existing) {
1856
+ this._presets = this._presets.map((p) => (p.id === existing.id ? preset : p));
1857
+ }
1858
+ else {
1859
+ this._presets = [...this._presets, preset];
1860
+ }
1861
+ this._onSave?.(preset);
1862
+ this._notify();
1863
+ return preset;
1864
+ }
1865
+ deletePreset(id) {
1866
+ const next = this._presets.filter((p) => p.id !== id);
1867
+ if (next.length !== this._presets.length) {
1868
+ this._presets = next;
1869
+ this._notify();
1870
+ }
1871
+ }
1872
+ /** Filter an array of rows through a filter tree. */
1873
+ applyFilter(node, rows) {
1874
+ return rows.filter((row) => evaluateFilter(node, row));
1875
+ }
1876
+ _notify() {
1877
+ this._onChange?.();
1878
+ }
1879
+ }
1880
+
1881
+ /**
1882
+ * SavedViewsEngine — manages named grid views (column layout, sort, filter,
1883
+ * group, density, etc.) across personal + admin-shared tiers. Server sync is
1884
+ * delegated to async callbacks so the engine stays pure and Node-testable.
1885
+ */
1886
+ class SavedViewsEngine {
1887
+ _getSavedViews;
1888
+ _onSaveView;
1889
+ _onDeleteView;
1890
+ _onViewChange;
1891
+ _generateId;
1892
+ _views;
1893
+ _activeViewId = null;
1894
+ _seq = 0;
1895
+ _onChange;
1896
+ constructor(options = {}) {
1897
+ this._getSavedViews = options.getSavedViews;
1898
+ this._onSaveView = options.onSaveView;
1899
+ this._onDeleteView = options.onDeleteView;
1900
+ this._onViewChange = options.onViewChange;
1901
+ this._generateId = options.generateId ?? (() => `view-${++this._seq}`);
1902
+ this._views = [...(options.views ?? [])];
1903
+ }
1904
+ subscribe(listener) {
1905
+ this._onChange = listener;
1906
+ return () => {
1907
+ if (this._onChange === listener)
1908
+ this._onChange = undefined;
1909
+ };
1910
+ }
1911
+ /** Load views from the async source, replacing local state. */
1912
+ async load() {
1913
+ if (!this._getSavedViews)
1914
+ return;
1915
+ this._views = await this._getSavedViews();
1916
+ this._notify();
1917
+ }
1918
+ getViews() {
1919
+ return this._views;
1920
+ }
1921
+ getView(id) {
1922
+ return this._views.find((v) => v.id === id);
1923
+ }
1924
+ /** Personal (non-shared) views. */
1925
+ getPersonalViews() {
1926
+ return this._views.filter((v) => !v.isShared);
1927
+ }
1928
+ /** Admin-shared views. */
1929
+ getSharedViews() {
1930
+ return this._views.filter((v) => v.isShared);
1931
+ }
1932
+ getActiveView() {
1933
+ if (this._activeViewId === null)
1934
+ return null;
1935
+ return this.getView(this._activeViewId) ?? null;
1936
+ }
1937
+ /** Create a new view or update an existing one by name. Returns the view. */
1938
+ async saveView(name, layout, options = {}) {
1939
+ const existing = options.id
1940
+ ? this._views.find((v) => v.id === options.id)
1941
+ : this._views.find((v) => v.name === name);
1942
+ const view = existing
1943
+ ? { ...existing, name, layout, isShared: options.isShared ?? existing.isShared }
1944
+ : { id: this._generateId(), name, layout, isShared: options.isShared };
1945
+ if (existing) {
1946
+ this._views = this._views.map((v) => (v.id === existing.id ? view : v));
1947
+ }
1948
+ else {
1949
+ this._views = [...this._views, view];
1950
+ }
1951
+ await this._onSaveView?.(view);
1952
+ this._notify();
1953
+ return view;
1954
+ }
1955
+ /** Delete a view. If it was active, the active view is cleared. */
1956
+ async deleteView(id) {
1957
+ const next = this._views.filter((v) => v.id !== id);
1958
+ if (next.length === this._views.length)
1959
+ return;
1960
+ this._views = next;
1961
+ if (this._activeViewId === id) {
1962
+ this._activeViewId = null;
1963
+ this._onViewChange?.(null);
1964
+ }
1965
+ await this._onDeleteView?.(id);
1966
+ this._notify();
1967
+ }
1968
+ /** Activate a view (or clear with null). Fires onViewChange. */
1969
+ setActiveView(id) {
1970
+ if (id !== null && !this.getView(id)) {
1971
+ throw new Error(`SavedViewsEngine: unknown view '${id}'`);
1972
+ }
1973
+ if (this._activeViewId === id)
1974
+ return;
1975
+ this._activeViewId = id;
1976
+ this._onViewChange?.(this.getActiveView());
1977
+ this._notify();
1978
+ }
1979
+ _notify() {
1980
+ this._onChange?.();
1981
+ }
1982
+ }
1983
+
1490
1984
  /*
1491
1985
  * Public API Surface of @gridengine/angular-datagrid-enterprise
1492
1986
  *
@@ -1499,5 +1993,5 @@ class MasterDetailEngine {
1499
1993
  * Generated bundle index. Do not edit.
1500
1994
  */
1501
1995
 
1502
- export { ClipboardEngine, DataGridPro, FillHandleEngine, FormulaEngine, GridLicenseWatermark, LicenseManager, MasterDetailEngine, PRODUCT_ID, PURCHASE_URL, RangeSelectionEngine, SSRMEngine, TransactionEngine, UndoRedoManager, parseTSV, provideGridEngineLicense, toNumber, toTimestamp };
1996
+ export { AuditTrailEngine, CellPermissionEngine, ClipboardEngine, DEFAULT_MASK, DataGridPro, FillHandleEngine, FilterPresetEngine, FormulaEngine, GridLicenseWatermark, LicenseManager, MasterDetailEngine, PRODUCT_ID, PURCHASE_URL, RangeSelectionEngine, RowLockEngine, SSRMEngine, SavedViewsEngine, TransactionEngine, UndoRedoManager, deserializeFilter, evaluateFilter, parseTSV, provideGridEngineLicense, serializeFilter, toNumber, toTimestamp };
1503
1997
  //# sourceMappingURL=gridengine-angular-datagrid-enterprise.mjs.map