@gridengine/angular-datagrid-enterprise 0.5.0 → 0.7.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.
|
@@ -1723,6 +1723,684 @@ class RowLockEngine {
|
|
|
1723
1723
|
}
|
|
1724
1724
|
}
|
|
1725
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
|
+
|
|
1984
|
+
/**
|
|
1985
|
+
* Parse delimited text into a matrix of string cells. Handles quoted fields
|
|
1986
|
+
* containing the delimiter, newlines, and escaped quotes (`""`), plus `\n` and
|
|
1987
|
+
* `\r\n` line endings.
|
|
1988
|
+
*/
|
|
1989
|
+
function parseCSV(text, delimiter = ',') {
|
|
1990
|
+
const rows = [];
|
|
1991
|
+
let row = [];
|
|
1992
|
+
let cell = '';
|
|
1993
|
+
let inQuotes = false;
|
|
1994
|
+
let i = 0;
|
|
1995
|
+
const pushCell = () => {
|
|
1996
|
+
row.push(cell);
|
|
1997
|
+
cell = '';
|
|
1998
|
+
};
|
|
1999
|
+
const pushRow = () => {
|
|
2000
|
+
pushCell();
|
|
2001
|
+
rows.push(row);
|
|
2002
|
+
row = [];
|
|
2003
|
+
};
|
|
2004
|
+
while (i < text.length) {
|
|
2005
|
+
const ch = text[i];
|
|
2006
|
+
if (inQuotes) {
|
|
2007
|
+
if (ch === '"') {
|
|
2008
|
+
if (text[i + 1] === '"') {
|
|
2009
|
+
cell += '"';
|
|
2010
|
+
i += 2;
|
|
2011
|
+
continue;
|
|
2012
|
+
}
|
|
2013
|
+
inQuotes = false;
|
|
2014
|
+
i++;
|
|
2015
|
+
continue;
|
|
2016
|
+
}
|
|
2017
|
+
cell += ch;
|
|
2018
|
+
i++;
|
|
2019
|
+
continue;
|
|
2020
|
+
}
|
|
2021
|
+
if (ch === '"') {
|
|
2022
|
+
inQuotes = true;
|
|
2023
|
+
i++;
|
|
2024
|
+
continue;
|
|
2025
|
+
}
|
|
2026
|
+
if (ch === delimiter) {
|
|
2027
|
+
pushCell();
|
|
2028
|
+
i++;
|
|
2029
|
+
continue;
|
|
2030
|
+
}
|
|
2031
|
+
if (ch === '\r') {
|
|
2032
|
+
pushRow();
|
|
2033
|
+
if (text[i + 1] === '\n')
|
|
2034
|
+
i++;
|
|
2035
|
+
i++;
|
|
2036
|
+
continue;
|
|
2037
|
+
}
|
|
2038
|
+
if (ch === '\n') {
|
|
2039
|
+
pushRow();
|
|
2040
|
+
i++;
|
|
2041
|
+
continue;
|
|
2042
|
+
}
|
|
2043
|
+
cell += ch;
|
|
2044
|
+
i++;
|
|
2045
|
+
}
|
|
2046
|
+
if (cell.length > 0 || row.length > 0) {
|
|
2047
|
+
pushRow();
|
|
2048
|
+
}
|
|
2049
|
+
return rows;
|
|
2050
|
+
}
|
|
2051
|
+
function normalizeHeader(h) {
|
|
2052
|
+
return h.trim().toLowerCase().replace(/[\s_-]+/g, '');
|
|
2053
|
+
}
|
|
2054
|
+
function coerce(raw, type) {
|
|
2055
|
+
const trimmed = raw.trim();
|
|
2056
|
+
switch (type) {
|
|
2057
|
+
case 'number': {
|
|
2058
|
+
if (trimmed === '')
|
|
2059
|
+
return { ok: true, value: null };
|
|
2060
|
+
const n = Number(trimmed.replace(/,/g, ''));
|
|
2061
|
+
if (Number.isNaN(n))
|
|
2062
|
+
return { ok: false, value: raw, message: `"${raw}" is not a number` };
|
|
2063
|
+
return { ok: true, value: n };
|
|
2064
|
+
}
|
|
2065
|
+
case 'boolean': {
|
|
2066
|
+
const t = trimmed.toLowerCase();
|
|
2067
|
+
if (['true', '1', 'yes', 'y'].includes(t))
|
|
2068
|
+
return { ok: true, value: true };
|
|
2069
|
+
if (['false', '0', 'no', 'n', ''].includes(t))
|
|
2070
|
+
return { ok: true, value: false };
|
|
2071
|
+
return { ok: false, value: raw, message: `"${raw}" is not a boolean` };
|
|
2072
|
+
}
|
|
2073
|
+
case 'date': {
|
|
2074
|
+
if (trimmed === '')
|
|
2075
|
+
return { ok: true, value: null };
|
|
2076
|
+
const ts = Date.parse(trimmed);
|
|
2077
|
+
if (Number.isNaN(ts))
|
|
2078
|
+
return { ok: false, value: raw, message: `"${raw}" is not a valid date` };
|
|
2079
|
+
return { ok: true, value: new Date(ts).toISOString() };
|
|
2080
|
+
}
|
|
2081
|
+
case 'string':
|
|
2082
|
+
default:
|
|
2083
|
+
return { ok: true, value: raw };
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
class ExcelImportEngine {
|
|
2087
|
+
_columns;
|
|
2088
|
+
_headers = [];
|
|
2089
|
+
_dataRows = [];
|
|
2090
|
+
/** sourceHeader → targetField (null = unmapped). */
|
|
2091
|
+
_mapping = new Map();
|
|
2092
|
+
constructor(options) {
|
|
2093
|
+
this._columns = options.columns;
|
|
2094
|
+
if (options.columnMapping) {
|
|
2095
|
+
for (const [header, field] of Object.entries(options.columnMapping)) {
|
|
2096
|
+
this._mapping.set(header, field);
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
/**
|
|
2101
|
+
* Load a matrix of raw cells.
|
|
2102
|
+
* @param matrix Full sheet, including the header row when hasHeaderRow.
|
|
2103
|
+
* @param hasHeaderRow When true (default) the first row is treated as headers.
|
|
2104
|
+
*/
|
|
2105
|
+
loadMatrix(matrix, hasHeaderRow = true) {
|
|
2106
|
+
if (matrix.length === 0) {
|
|
2107
|
+
this._headers = [];
|
|
2108
|
+
this._dataRows = [];
|
|
2109
|
+
return;
|
|
2110
|
+
}
|
|
2111
|
+
if (hasHeaderRow) {
|
|
2112
|
+
this._headers = matrix[0].map((h) => String(h ?? ''));
|
|
2113
|
+
this._dataRows = matrix.slice(1);
|
|
2114
|
+
}
|
|
2115
|
+
else {
|
|
2116
|
+
this._headers = matrix[0].map((_, idx) => `Col${idx + 1}`);
|
|
2117
|
+
this._dataRows = matrix;
|
|
2118
|
+
}
|
|
2119
|
+
this._autoMap();
|
|
2120
|
+
}
|
|
2121
|
+
/** Parse delimited text and load it as the source matrix. */
|
|
2122
|
+
loadCSV(text, delimiter = ',', hasHeaderRow = true) {
|
|
2123
|
+
this.loadMatrix(parseCSV(text, delimiter), hasHeaderRow);
|
|
2124
|
+
}
|
|
2125
|
+
_autoMap() {
|
|
2126
|
+
const fieldByNorm = new Map();
|
|
2127
|
+
for (const col of this._columns) {
|
|
2128
|
+
fieldByNorm.set(normalizeHeader(col.headerName), col.field);
|
|
2129
|
+
fieldByNorm.set(normalizeHeader(col.field), col.field);
|
|
2130
|
+
}
|
|
2131
|
+
for (const header of this._headers) {
|
|
2132
|
+
if (this._mapping.has(header))
|
|
2133
|
+
continue; // respect explicit mapping
|
|
2134
|
+
const match = fieldByNorm.get(normalizeHeader(header));
|
|
2135
|
+
this._mapping.set(header, match ?? null);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
getHeaders() {
|
|
2139
|
+
return this._headers;
|
|
2140
|
+
}
|
|
2141
|
+
/** Current source-header → target-field mapping. */
|
|
2142
|
+
getMapping() {
|
|
2143
|
+
const out = {};
|
|
2144
|
+
for (const header of this._headers) {
|
|
2145
|
+
out[header] = this._mapping.get(header) ?? null;
|
|
2146
|
+
}
|
|
2147
|
+
return out;
|
|
2148
|
+
}
|
|
2149
|
+
/** Manually map (or unmap with null) a source header to a target field. */
|
|
2150
|
+
setMapping(sourceHeader, field) {
|
|
2151
|
+
this._mapping.set(sourceHeader, field);
|
|
2152
|
+
}
|
|
2153
|
+
/** Source headers not yet mapped to a target field. */
|
|
2154
|
+
getUnmappedHeaders() {
|
|
2155
|
+
return this._headers.filter((h) => !this._mapping.get(h));
|
|
2156
|
+
}
|
|
2157
|
+
/** Target fields that have no source column mapped to them. */
|
|
2158
|
+
getUnmappedFields() {
|
|
2159
|
+
const mapped = new Set(this._headers.map((h) => this._mapping.get(h)).filter((f) => !!f));
|
|
2160
|
+
return this._columns.map((c) => c.field).filter((f) => !mapped.has(f));
|
|
2161
|
+
}
|
|
2162
|
+
/** Validate all data rows against the mapped column types. */
|
|
2163
|
+
validate() {
|
|
2164
|
+
return this._process().errors;
|
|
2165
|
+
}
|
|
2166
|
+
/** Build validated rows (rows with errors are still returned, best-effort). */
|
|
2167
|
+
buildRows() {
|
|
2168
|
+
return this._process().rows;
|
|
2169
|
+
}
|
|
2170
|
+
/** Full preview: rows plus any validation errors. */
|
|
2171
|
+
preview() {
|
|
2172
|
+
return this._process();
|
|
2173
|
+
}
|
|
2174
|
+
_process() {
|
|
2175
|
+
const rows = [];
|
|
2176
|
+
const errors = [];
|
|
2177
|
+
const fieldToSourceIndex = new Map();
|
|
2178
|
+
this._headers.forEach((header, idx) => {
|
|
2179
|
+
const field = this._mapping.get(header);
|
|
2180
|
+
if (field)
|
|
2181
|
+
fieldToSourceIndex.set(field, idx);
|
|
2182
|
+
});
|
|
2183
|
+
this._dataRows.forEach((sourceRow, rowIndex) => {
|
|
2184
|
+
const row = {};
|
|
2185
|
+
for (const col of this._columns) {
|
|
2186
|
+
const srcIdx = fieldToSourceIndex.get(col.field);
|
|
2187
|
+
const raw = srcIdx === undefined ? '' : String(sourceRow[srcIdx] ?? '');
|
|
2188
|
+
const type = col.type ?? 'string';
|
|
2189
|
+
if (col.required && raw.trim() === '') {
|
|
2190
|
+
errors.push({ rowIndex, field: col.field, value: raw, message: `${col.headerName} is required` });
|
|
2191
|
+
}
|
|
2192
|
+
const result = coerce(raw, type);
|
|
2193
|
+
if (!result.ok) {
|
|
2194
|
+
errors.push({ rowIndex, field: col.field, value: raw, message: result.message ?? `Invalid ${type}` });
|
|
2195
|
+
row[col.field] = raw; // keep raw for user correction
|
|
2196
|
+
}
|
|
2197
|
+
else {
|
|
2198
|
+
row[col.field] = result.value;
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
rows.push(row);
|
|
2202
|
+
});
|
|
2203
|
+
return { rows, errors };
|
|
2204
|
+
}
|
|
2205
|
+
/** Number of data rows currently loaded (excludes the header row). */
|
|
2206
|
+
get rowCount() {
|
|
2207
|
+
return this._dataRows.length;
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
const DEFAULTS = {
|
|
2212
|
+
pageSize: 'A4',
|
|
2213
|
+
orientation: 'landscape',
|
|
2214
|
+
rowsPerPage: 25,
|
|
2215
|
+
};
|
|
2216
|
+
class PDFExportEngine {
|
|
2217
|
+
_columns;
|
|
2218
|
+
_options;
|
|
2219
|
+
_now;
|
|
2220
|
+
constructor(options) {
|
|
2221
|
+
const { columns, now, ...pdfOptions } = options;
|
|
2222
|
+
this._columns = columns;
|
|
2223
|
+
this._options = pdfOptions;
|
|
2224
|
+
this._now = now ?? (() => new Date());
|
|
2225
|
+
}
|
|
2226
|
+
/** Build the paginated document model from the given (already-visible) rows. */
|
|
2227
|
+
build(rows) {
|
|
2228
|
+
const rowsPerPage = Math.max(1, this._options.rowsPerPage ?? DEFAULTS.rowsPerPage);
|
|
2229
|
+
const pageSize = this._options.pageSize ?? DEFAULTS.pageSize;
|
|
2230
|
+
const orientation = this._options.orientation ?? DEFAULTS.orientation;
|
|
2231
|
+
const generatedAt = this._now().toISOString();
|
|
2232
|
+
const columnHeaders = this._columns.map((c) => c.headerName);
|
|
2233
|
+
const pageCount = Math.max(1, Math.ceil(rows.length / rowsPerPage));
|
|
2234
|
+
const pages = [];
|
|
2235
|
+
for (let p = 0; p < pageCount; p++) {
|
|
2236
|
+
const slice = rows.slice(p * rowsPerPage, (p + 1) * rowsPerPage);
|
|
2237
|
+
pages.push({
|
|
2238
|
+
header: { title: this._options.title, logo: this._options.logo, generatedAt },
|
|
2239
|
+
footer: { text: this._options.footer, pageNumber: p + 1, pageCount },
|
|
2240
|
+
columnHeaders,
|
|
2241
|
+
rows: slice.map((row) => this._formatRow(row)),
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
return { pageSize, orientation, pages };
|
|
2245
|
+
}
|
|
2246
|
+
_formatRow(row) {
|
|
2247
|
+
return this._columns.map((col) => {
|
|
2248
|
+
const value = row[col.field];
|
|
2249
|
+
if (col.format)
|
|
2250
|
+
return col.format(value, row);
|
|
2251
|
+
return value === null || value === undefined ? '' : String(value);
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
class FormEditorEngine {
|
|
2257
|
+
_rows;
|
|
2258
|
+
_rowIdField;
|
|
2259
|
+
_onSave;
|
|
2260
|
+
_onDelete;
|
|
2261
|
+
_index = -1; // -1 = panel closed
|
|
2262
|
+
_draft = null;
|
|
2263
|
+
_onChange;
|
|
2264
|
+
constructor(options) {
|
|
2265
|
+
this._rows = [...options.rows];
|
|
2266
|
+
this._rowIdField = options.rowIdField ?? 'id';
|
|
2267
|
+
this._onSave = options.onSave;
|
|
2268
|
+
this._onDelete = options.onDelete;
|
|
2269
|
+
}
|
|
2270
|
+
subscribe(listener) {
|
|
2271
|
+
this._onChange = listener;
|
|
2272
|
+
return () => {
|
|
2273
|
+
if (this._onChange === listener)
|
|
2274
|
+
this._onChange = undefined;
|
|
2275
|
+
};
|
|
2276
|
+
}
|
|
2277
|
+
/** Replace the row set; keeps the panel on the same rowId if still present. */
|
|
2278
|
+
setRows(rows) {
|
|
2279
|
+
const currentId = this.isOpen ? this._rowId(this._rows[this._index]) : undefined;
|
|
2280
|
+
this._rows = [...rows];
|
|
2281
|
+
if (currentId !== undefined) {
|
|
2282
|
+
const idx = this._rows.findIndex((r) => this._rowId(r) === currentId);
|
|
2283
|
+
if (idx === -1) {
|
|
2284
|
+
this.close();
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
this._index = idx;
|
|
2288
|
+
this._draft = { ...this._rows[idx] };
|
|
2289
|
+
}
|
|
2290
|
+
this._notify();
|
|
2291
|
+
}
|
|
2292
|
+
get isOpen() {
|
|
2293
|
+
return this._index >= 0 && this._index < this._rows.length;
|
|
2294
|
+
}
|
|
2295
|
+
get index() {
|
|
2296
|
+
return this._index;
|
|
2297
|
+
}
|
|
2298
|
+
/** Open the panel on a row by its ID. */
|
|
2299
|
+
openById(rowId) {
|
|
2300
|
+
const idx = this._rows.findIndex((r) => this._rowId(r) === rowId);
|
|
2301
|
+
if (idx === -1)
|
|
2302
|
+
throw new Error(`FormEditorEngine: no row with id '${rowId}'`);
|
|
2303
|
+
this._openAt(idx);
|
|
2304
|
+
}
|
|
2305
|
+
/** Open the panel on a row by index. */
|
|
2306
|
+
openAt(index) {
|
|
2307
|
+
if (index < 0 || index >= this._rows.length) {
|
|
2308
|
+
throw new Error(`FormEditorEngine: index ${index} out of range`);
|
|
2309
|
+
}
|
|
2310
|
+
this._openAt(index);
|
|
2311
|
+
}
|
|
2312
|
+
close() {
|
|
2313
|
+
this._index = -1;
|
|
2314
|
+
this._draft = null;
|
|
2315
|
+
this._notify();
|
|
2316
|
+
}
|
|
2317
|
+
_openAt(index) {
|
|
2318
|
+
this._index = index;
|
|
2319
|
+
this._draft = { ...this._rows[index] };
|
|
2320
|
+
this._notify();
|
|
2321
|
+
}
|
|
2322
|
+
/** The current editable draft (a copy of the row plus unsaved edits). */
|
|
2323
|
+
getDraft() {
|
|
2324
|
+
return this._draft ? { ...this._draft } : null;
|
|
2325
|
+
}
|
|
2326
|
+
/** The original (unedited) row currently open. */
|
|
2327
|
+
getOriginal() {
|
|
2328
|
+
return this.isOpen ? this._rows[this._index] : null;
|
|
2329
|
+
}
|
|
2330
|
+
setFieldValue(field, value) {
|
|
2331
|
+
if (!this._draft)
|
|
2332
|
+
throw new Error('FormEditorEngine: no row open');
|
|
2333
|
+
this._draft[field] = value;
|
|
2334
|
+
this._notify();
|
|
2335
|
+
}
|
|
2336
|
+
/** True when the draft differs from the original row. */
|
|
2337
|
+
get isDirty() {
|
|
2338
|
+
if (!this._draft || !this.isOpen)
|
|
2339
|
+
return false;
|
|
2340
|
+
const original = this._rows[this._index];
|
|
2341
|
+
const keys = new Set([...Object.keys(original), ...Object.keys(this._draft)]);
|
|
2342
|
+
for (const key of keys) {
|
|
2343
|
+
if (!Object.is(original[key], this._draft[key]))
|
|
2344
|
+
return true;
|
|
2345
|
+
}
|
|
2346
|
+
return false;
|
|
2347
|
+
}
|
|
2348
|
+
/** Revert unsaved edits. */
|
|
2349
|
+
revert() {
|
|
2350
|
+
if (!this.isOpen)
|
|
2351
|
+
return;
|
|
2352
|
+
this._draft = { ...this._rows[this._index] };
|
|
2353
|
+
this._notify();
|
|
2354
|
+
}
|
|
2355
|
+
get canGoPrev() {
|
|
2356
|
+
return this._index > 0;
|
|
2357
|
+
}
|
|
2358
|
+
get canGoNext() {
|
|
2359
|
+
return this._index >= 0 && this._index < this._rows.length - 1;
|
|
2360
|
+
}
|
|
2361
|
+
next() {
|
|
2362
|
+
if (this.canGoNext)
|
|
2363
|
+
this._openAt(this._index + 1);
|
|
2364
|
+
}
|
|
2365
|
+
prev() {
|
|
2366
|
+
if (this.canGoPrev)
|
|
2367
|
+
this._openAt(this._index - 1);
|
|
2368
|
+
}
|
|
2369
|
+
/** Save the draft: commits it into the row set and calls onSave. */
|
|
2370
|
+
async save() {
|
|
2371
|
+
if (!this._draft || !this.isOpen)
|
|
2372
|
+
throw new Error('FormEditorEngine: no row open');
|
|
2373
|
+
const saved = { ...this._draft };
|
|
2374
|
+
this._rows[this._index] = saved;
|
|
2375
|
+
await this._onSave?.(saved);
|
|
2376
|
+
this._notify();
|
|
2377
|
+
}
|
|
2378
|
+
/** Delete the open row: removes it and keeps the panel on the next row. */
|
|
2379
|
+
async delete() {
|
|
2380
|
+
if (!this.isOpen)
|
|
2381
|
+
throw new Error('FormEditorEngine: no row open');
|
|
2382
|
+
const row = this._rows[this._index];
|
|
2383
|
+
this._rows = this._rows.filter((_, i) => i !== this._index);
|
|
2384
|
+
await this._onDelete?.(row);
|
|
2385
|
+
if (this._rows.length === 0) {
|
|
2386
|
+
this.close();
|
|
2387
|
+
}
|
|
2388
|
+
else {
|
|
2389
|
+
this._openAt(Math.min(this._index, this._rows.length - 1));
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
_rowId(row) {
|
|
2393
|
+
const id = row[this._rowIdField];
|
|
2394
|
+
if (id === undefined || id === null) {
|
|
2395
|
+
throw new Error(`FormEditorEngine: row has no '${this._rowIdField}' field.`);
|
|
2396
|
+
}
|
|
2397
|
+
return id;
|
|
2398
|
+
}
|
|
2399
|
+
_notify() {
|
|
2400
|
+
this._onChange?.();
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
|
|
1726
2404
|
/*
|
|
1727
2405
|
* Public API Surface of @gridengine/angular-datagrid-enterprise
|
|
1728
2406
|
*
|
|
@@ -1735,5 +2413,5 @@ class RowLockEngine {
|
|
|
1735
2413
|
* Generated bundle index. Do not edit.
|
|
1736
2414
|
*/
|
|
1737
2415
|
|
|
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 };
|
|
2416
|
+
export { AuditTrailEngine, CellPermissionEngine, ClipboardEngine, DEFAULT_MASK, DataGridPro, ExcelImportEngine, FillHandleEngine, FilterPresetEngine, FormEditorEngine, FormulaEngine, GridLicenseWatermark, LicenseManager, MasterDetailEngine, PDFExportEngine, PRODUCT_ID, PURCHASE_URL, RangeSelectionEngine, RowLockEngine, SSRMEngine, SavedViewsEngine, TransactionEngine, UndoRedoManager, deserializeFilter, evaluateFilter, parseCSV, parseTSV, provideGridEngineLicense, serializeFilter, toNumber, toTimestamp };
|
|
1739
2417
|
//# sourceMappingURL=gridengine-angular-datagrid-enterprise.mjs.map
|