@gridengine/angular-datagrid-enterprise 0.6.0 → 0.8.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.
@@ -1981,6 +1981,479 @@ class SavedViewsEngine {
1981
1981
  }
1982
1982
  }
1983
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
+
2404
+ /**
2405
+ * Return a copy of `columnDefs` whose cells honour a `CellPermissionEngine`:
2406
+ * unreadable cells resolve to the engine's mask value (so they never reach
2407
+ * renderers, sorting, filtering, or export), and non-editable cells become
2408
+ * non-editable regardless of the column's own `editable`. Columns without a
2409
+ * `field` (e.g. a checkbox-selection column) pass through unchanged.
2410
+ */
2411
+ function applyCellPermissions(columnDefs, engine) {
2412
+ return columnDefs.map((col) => {
2413
+ const field = col.field;
2414
+ if (!field)
2415
+ return col;
2416
+ const originalGetter = col.valueGetter;
2417
+ const originalEditable = col.editable;
2418
+ return {
2419
+ ...col,
2420
+ valueGetter: (data) => {
2421
+ const row = data;
2422
+ if (!engine.canRead(row, field))
2423
+ return engine.maskValue;
2424
+ return originalGetter ? originalGetter(data) : row[field];
2425
+ },
2426
+ editable: (row) => {
2427
+ if (!engine.canEdit(row, field))
2428
+ return false;
2429
+ return typeof originalEditable === 'function'
2430
+ ? originalEditable(row)
2431
+ : (originalEditable ?? false);
2432
+ },
2433
+ };
2434
+ });
2435
+ }
2436
+ /**
2437
+ * Convert open-source grid column defs into `PdfColumn`s for `PDFExportEngine`,
2438
+ * carrying each column's `headerName` and `valueFormatter`. Hidden columns and
2439
+ * columns without a `field` are omitted.
2440
+ */
2441
+ function toPdfColumns(columnDefs) {
2442
+ const out = [];
2443
+ for (const col of columnDefs) {
2444
+ if (!col.field || col.hide)
2445
+ continue;
2446
+ const field = col.field;
2447
+ const fmt = col.valueFormatter;
2448
+ out.push({
2449
+ field,
2450
+ headerName: col.headerName ?? field,
2451
+ format: fmt ? (value) => fmt(value) : undefined,
2452
+ });
2453
+ }
2454
+ return out;
2455
+ }
2456
+
1984
2457
  /*
1985
2458
  * Public API Surface of @gridengine/angular-datagrid-enterprise
1986
2459
  *
@@ -1993,5 +2466,5 @@ class SavedViewsEngine {
1993
2466
  * Generated bundle index. Do not edit.
1994
2467
  */
1995
2468
 
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 };
2469
+ 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, applyCellPermissions, deserializeFilter, evaluateFilter, parseCSV, parseTSV, provideGridEngineLicense, serializeFilter, toNumber, toPdfColumns, toTimestamp };
1997
2470
  //# sourceMappingURL=gridengine-angular-datagrid-enterprise.mjs.map