@svgrid/enterprise 3.1.7 → 3.1.9
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 +1 -0
- package/dist/SvAlertRuleEditor.svelte +16 -5
- package/dist/SvGridAlerts.svelte +8 -0
- package/dist/SvGridAlerts.svelte.d.ts +4 -0
- package/dist/SvGridBoard.svelte +69 -10
- package/dist/SvGridBulkEditDrawer.svelte +13 -5
- package/dist/SvGridGallery.svelte +286 -0
- package/dist/SvGridGallery.svelte.d.ts +30 -0
- package/dist/SvGridScheduler.svelte +33 -11
- package/dist/SvGridSelectionBar.svelte +47 -0
- package/dist/advanced-filter/expr-columns-from-grid.d.ts +1 -0
- package/dist/advanced-filter/expr-columns-from-grid.js +11 -7
- package/dist/advanced-filter-enable.d.ts +6 -0
- package/dist/advanced-filter-enable.js +8 -0
- package/dist/alerts/alert-engine.d.ts +7 -0
- package/dist/alerts/alert-engine.js +5 -2
- package/dist/alerts/condition-editor.d.ts +26 -0
- package/dist/alerts/condition-editor.js +17 -0
- package/dist/base/index.d.ts +20 -0
- package/dist/base/index.js +20 -0
- package/dist/bulk-edit.d.ts +11 -0
- package/dist/bulk-edit.js +18 -1
- package/dist/cdn/svgrid-enterprise.svelte-external.js +11375 -10805
- package/dist/designer/assets/GridFooter-Cu8_yBlQ.js +2 -0
- package/dist/designer/assets/GridMenus-BUjkkAU2.js +6 -0
- package/dist/designer/assets/PinSplitter-BgTpgh1d.css +1 -0
- package/dist/designer/assets/PinSplitter-C5AwA1xV.js +1 -0
- package/dist/designer/assets/SvGrid.helpers-CIrmIo9U.js +1 -0
- package/dist/designer/assets/SvGridCellEditor-CJxThKL_.js +4 -0
- package/dist/designer/assets/SvListBox-CS7oUAiX.js +2 -0
- package/dist/designer/assets/conditional-formatting-MDWbF8_Y.js +1 -0
- package/dist/designer/assets/grid-extension.svelte-BZ7AqyJI.js +1 -0
- package/dist/designer/assets/{index-DtBIaQno.js → index-BPU2KNc5.js} +396 -395
- package/dist/designer/assets/index-BmvG99kR.css +1 -0
- package/dist/designer/assets/sparkline-C-NcnB2i.js +1 -0
- package/dist/designer/assets/src-C_U1NKOY.js +58 -0
- package/dist/designer/assets/sv-grid-scrollbar-B9N6Wuj0.js +109 -0
- package/dist/designer/assets/validate-H5u2HEQh.js +1 -0
- package/dist/designer/index.html +11 -7
- package/dist/expressions/compile.js +10 -0
- package/dist/expressions/evaluate.js +18 -0
- package/dist/expressions/expression-types.d.ts +10 -0
- package/dist/expressions/kinds.d.ts +28 -0
- package/dist/expressions/kinds.js +19 -0
- package/dist/expressions/parse.js +9 -1
- package/dist/fields/AttachmentCell.svelte +242 -0
- package/dist/fields/AttachmentCell.svelte.d.ts +5 -0
- package/dist/fields/AttachmentDialog.svelte +357 -0
- package/dist/fields/AttachmentDialog.svelte.d.ts +13 -0
- package/dist/fields/AttachmentEditor.svelte +41 -0
- package/dist/fields/AttachmentEditor.svelte.d.ts +11 -0
- package/dist/fields/AttachmentPreview.svelte +72 -0
- package/dist/fields/AttachmentPreview.svelte.d.ts +8 -0
- package/dist/fields/BarcodeCell.svelte +20 -0
- package/dist/fields/BarcodeCell.svelte.d.ts +5 -0
- package/dist/fields/BaseAutomations.svelte +558 -0
- package/dist/fields/BaseAutomations.svelte.d.ts +18 -0
- package/dist/fields/BaseChartView.svelte +47 -0
- package/dist/fields/BaseChartView.svelte.d.ts +8 -0
- package/dist/fields/BaseTableView.svelte +185 -0
- package/dist/fields/BaseTableView.svelte.d.ts +37 -0
- package/dist/fields/BaseTabs.svelte +308 -0
- package/dist/fields/BaseTabs.svelte.d.ts +21 -0
- package/dist/fields/ButtonCell.svelte +47 -0
- package/dist/fields/ButtonCell.svelte.d.ts +20 -0
- package/dist/fields/CheckboxCell.svelte +46 -0
- package/dist/fields/CheckboxCell.svelte.d.ts +14 -0
- package/dist/fields/DeleteFieldConfirm.svelte +80 -0
- package/dist/fields/DeleteFieldConfirm.svelte.d.ts +8 -0
- package/dist/fields/DeleteRecordConfirm.svelte +48 -0
- package/dist/fields/DeleteRecordConfirm.svelte.d.ts +9 -0
- package/dist/fields/FieldCell.svelte +80 -0
- package/dist/fields/FieldCell.svelte.d.ts +9 -0
- package/dist/fields/FieldEditor.svelte +321 -0
- package/dist/fields/FieldEditor.svelte.d.ts +10 -0
- package/dist/fields/FieldInput.svelte +188 -0
- package/dist/fields/FieldInput.svelte.d.ts +29 -0
- package/dist/fields/FieldsHeaderEnd.svelte +146 -0
- package/dist/fields/FieldsHeaderEnd.svelte.d.ts +6 -0
- package/dist/fields/FootRowAdder.svelte +8 -0
- package/dist/fields/FootRowAdder.svelte.d.ts +6 -0
- package/dist/fields/GroupHeader.svelte +127 -0
- package/dist/fields/GroupHeader.svelte.d.ts +24 -0
- package/dist/fields/LinkCell.svelte +46 -0
- package/dist/fields/LinkCell.svelte.d.ts +9 -0
- package/dist/fields/LinkPickerEditor.svelte +172 -0
- package/dist/fields/LinkPickerEditor.svelte.d.ts +9 -0
- package/dist/fields/LinkPreview.svelte +60 -0
- package/dist/fields/LinkPreview.svelte.d.ts +7 -0
- package/dist/fields/LinkedRecordView.svelte +94 -0
- package/dist/fields/LinkedRecordView.svelte.d.ts +9 -0
- package/dist/fields/LinkedRecordsCell.svelte +59 -0
- package/dist/fields/LinkedRecordsCell.svelte.d.ts +11 -0
- package/dist/fields/LongTextEditor.svelte +157 -0
- package/dist/fields/LongTextEditor.svelte.d.ts +11 -0
- package/dist/fields/NumberEditor.svelte +309 -0
- package/dist/fields/NumberEditor.svelte.d.ts +12 -0
- package/dist/fields/OptionsEditor.svelte +117 -0
- package/dist/fields/OptionsEditor.svelte.d.ts +11 -0
- package/dist/fields/PercentCell.svelte +21 -0
- package/dist/fields/PercentCell.svelte.d.ts +8 -0
- package/dist/fields/PropertyInput.svelte +204 -0
- package/dist/fields/PropertyInput.svelte.d.ts +11 -0
- package/dist/fields/RatingCell.svelte +75 -0
- package/dist/fields/RatingCell.svelte.d.ts +16 -0
- package/dist/fields/RatingEditor.svelte +82 -0
- package/dist/fields/RatingEditor.svelte.d.ts +9 -0
- package/dist/fields/RecordView.svelte +310 -0
- package/dist/fields/RecordView.svelte.d.ts +11 -0
- package/dist/fields/RichTextCell.svelte +30 -0
- package/dist/fields/RichTextCell.svelte.d.ts +5 -0
- package/dist/fields/RowAdder.svelte +93 -0
- package/dist/fields/RowAdder.svelte.d.ts +7 -0
- package/dist/fields/RowHeader.svelte +99 -0
- package/dist/fields/RowHeader.svelte.d.ts +11 -0
- package/dist/fields/SelectCell.svelte +35 -0
- package/dist/fields/SelectCell.svelte.d.ts +15 -0
- package/dist/fields/SelectEditor.svelte +350 -0
- package/dist/fields/SelectEditor.svelte.d.ts +17 -0
- package/dist/fields/SummaryCell.svelte +160 -0
- package/dist/fields/SummaryCell.svelte.d.ts +11 -0
- package/dist/fields/SvBase.svelte +545 -0
- package/dist/fields/SvBase.svelte.d.ts +30 -0
- package/dist/fields/SvBaseForm.svelte +295 -0
- package/dist/fields/SvBaseForm.svelte.d.ts +22 -0
- package/dist/fields/SvBaseViewBar.svelte +644 -0
- package/dist/fields/SvBaseViewBar.svelte.d.ts +26 -0
- package/dist/fields/UserCell.svelte +39 -0
- package/dist/fields/UserCell.svelte.d.ts +9 -0
- package/dist/fields/UserFace.svelte +30 -0
- package/dist/fields/UserFace.svelte.d.ts +12 -0
- package/dist/fields/activity.svelte.d.ts +50 -0
- package/dist/fields/activity.svelte.js +110 -0
- package/dist/fields/attachments.d.ts +95 -0
- package/dist/fields/attachments.js +145 -0
- package/dist/fields/automation-outbound.d.ts +46 -0
- package/dist/fields/automation-outbound.js +61 -0
- package/dist/fields/automation-server.d.ts +53 -0
- package/dist/fields/automation-server.js +333 -0
- package/dist/fields/automations.svelte.d.ts +208 -0
- package/dist/fields/automations.svelte.js +437 -0
- package/dist/fields/bar-request.svelte.d.ts +16 -0
- package/dist/fields/bar-request.svelte.js +15 -0
- package/dist/fields/base-alerts.d.ts +24 -0
- package/dist/fields/base-alerts.js +64 -0
- package/dist/fields/base-connect.d.ts +82 -0
- package/dist/fields/base-connect.js +271 -0
- package/dist/fields/base-schema.d.ts +104 -0
- package/dist/fields/base-schema.js +266 -0
- package/dist/fields/base-sql.d.ts +62 -0
- package/dist/fields/base-sql.js +251 -0
- package/dist/fields/base-sync.d.ts +142 -0
- package/dist/fields/base-sync.js +499 -0
- package/dist/fields/base-tables.d.ts +89 -0
- package/dist/fields/base-tables.js +349 -0
- package/dist/fields/builtin-types.d.ts +13 -0
- package/dist/fields/builtin-types.js +773 -0
- package/dist/fields/button.d.ts +2 -0
- package/dist/fields/button.js +17 -0
- package/dist/fields/chart-view.d.ts +35 -0
- package/dist/fields/chart-view.js +154 -0
- package/dist/fields/column-order.d.ts +6 -0
- package/dist/fields/column-order.js +12 -0
- package/dist/fields/convert.d.ts +16 -0
- package/dist/fields/convert.js +176 -0
- package/dist/fields/copy-text.d.ts +13 -0
- package/dist/fields/copy-text.js +45 -0
- package/dist/fields/editor-request.svelte.d.ts +71 -0
- package/dist/fields/editor-request.svelte.js +52 -0
- package/dist/fields/field-ops.d.ts +72 -0
- package/dist/fields/field-ops.js +334 -0
- package/dist/fields/filter-model.d.ts +84 -0
- package/dist/fields/filter-model.js +446 -0
- package/dist/fields/format.d.ts +109 -0
- package/dist/fields/format.js +288 -0
- package/dist/fields/formula.d.ts +50 -0
- package/dist/fields/formula.js +530 -0
- package/dist/fields/grid-access.d.ts +60 -0
- package/dist/fields/grid-access.js +120 -0
- package/dist/fields/index.d.ts +34 -0
- package/dist/fields/index.js +312 -0
- package/dist/fields/linked-fields.d.ts +8 -0
- package/dist/fields/linked-fields.js +40 -0
- package/dist/fields/links.d.ts +10 -0
- package/dist/fields/links.js +46 -0
- package/dist/fields/name-list.d.ts +16 -0
- package/dist/fields/name-list.js +49 -0
- package/dist/fields/palette.d.ts +47 -0
- package/dist/fields/palette.js +69 -0
- package/dist/fields/record-delete.svelte.d.ts +12 -0
- package/dist/fields/record-delete.svelte.js +5 -0
- package/dist/fields/record-menu.d.ts +9 -0
- package/dist/fields/record-menu.js +78 -0
- package/dist/fields/registry.svelte.d.ts +176 -0
- package/dist/fields/registry.svelte.js +51 -0
- package/dist/fields/rich-text.d.ts +23 -0
- package/dist/fields/rich-text.js +48 -0
- package/dist/fields/row-color-choice.svelte.d.ts +31 -0
- package/dist/fields/row-color-choice.svelte.js +25 -0
- package/dist/fields/row-colors.d.ts +36 -0
- package/dist/fields/row-colors.js +90 -0
- package/dist/fields/row-height.d.ts +28 -0
- package/dist/fields/row-height.js +50 -0
- package/dist/fields/sources.svelte.d.ts +80 -0
- package/dist/fields/sources.svelte.js +131 -0
- package/dist/fields/summaries.d.ts +18 -0
- package/dist/fields/summaries.js +138 -0
- package/dist/fields/view-model.d.ts +129 -0
- package/dist/fields/view-model.js +492 -0
- package/dist/fields/viewbar/ChartPanel.svelte +61 -0
- package/dist/fields/viewbar/ChartPanel.svelte.d.ts +9 -0
- package/dist/fields/viewbar/ColorPanel.svelte +112 -0
- package/dist/fields/viewbar/ColorPanel.svelte.d.ts +9 -0
- package/dist/fields/viewbar/ConditionValue.svelte +153 -0
- package/dist/fields/viewbar/ConditionValue.svelte.d.ts +15 -0
- package/dist/fields/viewbar/FieldIcon.svelte +18 -0
- package/dist/fields/viewbar/FieldIcon.svelte.d.ts +9 -0
- package/dist/fields/viewbar/FilterPanel.svelte +192 -0
- package/dist/fields/viewbar/FilterPanel.svelte.d.ts +13 -0
- package/dist/fields/viewbar/GanttPanel.svelte +60 -0
- package/dist/fields/viewbar/GanttPanel.svelte.d.ts +10 -0
- package/dist/fields/viewbar/HideFieldsPanel.svelte +104 -0
- package/dist/fields/viewbar/HideFieldsPanel.svelte.d.ts +7 -0
- package/dist/fields/viewbar/SortPanel.svelte +85 -0
- package/dist/fields/viewbar/SortPanel.svelte.d.ts +8 -0
- package/dist/fields/viewbar/ViewsMenu.svelte +250 -0
- package/dist/fields/viewbar/ViewsMenu.svelte.d.ts +15 -0
- package/dist/fields/viewbar/panel-focus.d.ts +3 -0
- package/dist/fields/viewbar/panel-focus.js +22 -0
- package/dist/fields/viewbar/sort-state.d.ts +20 -0
- package/dist/fields/viewbar/sort-state.js +24 -0
- package/dist/gallery.d.ts +7 -0
- package/dist/gallery.js +36 -0
- package/dist/gantt/SvGridGantt.svelte +88 -16
- package/dist/gantt/gantt-config.d.ts +28 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/dist/install.js +3 -0
- package/dist/license-core.d.ts +3 -3
- package/dist/license.js +1 -0
- package/dist/node/studio.js +141 -20
- package/dist/scheduler-config.d.ts +11 -0
- package/dist/sources/introspect-supabase.js +4 -1
- package/dist/sources/rest.js +6 -2
- package/dist/studio/emit-project.js +25 -6
- package/dist/studio/sveltekit3.js +13 -7
- package/dist/studio/ui-components.generated.js +94 -2
- package/dist/upgrade-prompt.d.ts +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/wc/GridFooter-f5K0aLe_.js +145 -0
- package/dist/wc/GridMenus-FtSN_8t9.js +1369 -0
- package/dist/wc/PinSplitter-D-qtDhSM.js +132 -0
- package/dist/wc/{SvGrid.helpers-zBkDTryR.js → SvGrid.helpers-B2GEKrxS.js} +84 -36
- package/dist/wc/{SvGridCellEditor-CzUKEbkI.js → SvGridCellEditor-B_cLJrsp.js} +20 -9
- package/dist/wc/conditional-formatting-BXis_oP1.js +189 -0
- package/dist/wc/grid-extension.svelte-Bufq4B4C.js +20 -0
- package/dist/wc/sv-sheet-element.js +7561 -7618
- package/dist/wc/svelte-virtualizer.svelte-ehwOupl-.js +360 -0
- package/dist/wc/{validate-43C03DLP.js → validate-BaMO8UPE.js} +1 -1
- package/package.json +14 -3
- package/dist/designer/assets/GridMenus-CvcASlKv.js +0 -8
- package/dist/designer/assets/SvGrid.helpers-DE5mmORW.js +0 -1
- package/dist/designer/assets/SvGridCellEditor-C7NWcnNC.js +0 -4
- package/dist/designer/assets/SvListBox-BlmmzYcU.js +0 -110
- package/dist/designer/assets/index-KmSEY7Lb.css +0 -1
- package/dist/designer/assets/src-DqHCqori.js +0 -58
- package/dist/designer/assets/validate-B71dE1Cs.js +0 -1
- package/dist/wc/GridMenus-fWbBUiRH.js +0 -1088
- package/dist/wc/svelte-virtualizer.svelte-Dntdh8Ms.js +0 -581
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The formula field's language: `{Field name}` references, numbers, "text",
|
|
3
|
+
* + - * / and & (join), = != < > <= >=, parentheses and functions such as
|
|
4
|
+
* IF, AND, CONCATENATE, ROUND, DATETIME_DIFF. Parsed once into a closure tree;
|
|
5
|
+
* nothing is ever passed to eval or Function. Errors carry a message the field
|
|
6
|
+
* editor shows under the formula.
|
|
7
|
+
*/
|
|
8
|
+
import { toDate } from './format.js';
|
|
9
|
+
export class FormulaError extends Error {
|
|
10
|
+
}
|
|
11
|
+
function tokenize(src) {
|
|
12
|
+
const out = [];
|
|
13
|
+
let i = 0;
|
|
14
|
+
while (i < src.length) {
|
|
15
|
+
const c = src[i];
|
|
16
|
+
if (/\s/.test(c)) {
|
|
17
|
+
i++;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (c === '{') {
|
|
21
|
+
const end = src.indexOf('}', i);
|
|
22
|
+
if (end < 0)
|
|
23
|
+
throw new FormulaError(`Unclosed "{" at ${i + 1}`);
|
|
24
|
+
out.push({ k: 'ref', v: src.slice(i + 1, end).trim(), at: i });
|
|
25
|
+
i = end + 1;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (c === '"' || c === "'") {
|
|
29
|
+
let j = i + 1;
|
|
30
|
+
let s = '';
|
|
31
|
+
while (j < src.length && src[j] !== c) {
|
|
32
|
+
if (src[j] === '\\' && j + 1 < src.length) {
|
|
33
|
+
s += src[j + 1];
|
|
34
|
+
j += 2;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
s += src[j++];
|
|
38
|
+
}
|
|
39
|
+
if (j >= src.length)
|
|
40
|
+
throw new FormulaError(`Unclosed text at ${i + 1}`);
|
|
41
|
+
out.push({ k: 'str', v: s, at: i });
|
|
42
|
+
i = j + 1;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const num = /^\d+(\.\d+)?|^\.\d+/.exec(src.slice(i));
|
|
46
|
+
if (num) {
|
|
47
|
+
out.push({ k: 'num', v: num[0], at: i });
|
|
48
|
+
i += num[0].length;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const id = /^[A-Za-z_][A-Za-z0-9_]*/.exec(src.slice(i));
|
|
52
|
+
if (id) {
|
|
53
|
+
out.push({ k: 'id', v: id[0], at: i });
|
|
54
|
+
i += id[0].length;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const op = /^(!=|<=|>=|<>|[-+*/&=<>])/.exec(src.slice(i));
|
|
58
|
+
if (op) {
|
|
59
|
+
out.push({ k: 'op', v: op[0] === '<>' ? '!=' : op[0], at: i });
|
|
60
|
+
i += op[0].length;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (c === '(' || c === ')' || c === ',') {
|
|
64
|
+
out.push({ k: c, v: c, at: i });
|
|
65
|
+
i++;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
throw new FormulaError(`Unexpected "${c}" at ${i + 1}`);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
const isBlank = (v) => v == null || v === '' || (Array.isArray(v) && v.length === 0);
|
|
73
|
+
const toNum = (v) => {
|
|
74
|
+
if (typeof v === 'number')
|
|
75
|
+
return v;
|
|
76
|
+
if (typeof v === 'boolean')
|
|
77
|
+
return v ? 1 : 0;
|
|
78
|
+
if (v instanceof Date)
|
|
79
|
+
return v.getTime();
|
|
80
|
+
if (v == null || v === '')
|
|
81
|
+
return 0;
|
|
82
|
+
if (Array.isArray(v))
|
|
83
|
+
return v.reduce((s, x) => s + toNum(x), 0);
|
|
84
|
+
const n = Number(v);
|
|
85
|
+
return Number.isFinite(n) ? n : NaN;
|
|
86
|
+
};
|
|
87
|
+
const toText = (v) => v == null ? '' : v instanceof Date ? v.toISOString() : Array.isArray(v) ? v.map(toText).join(', ') : String(v);
|
|
88
|
+
const truthy = (v) => !isBlank(v) && v !== false && v !== 0;
|
|
89
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
|
|
90
|
+
/**
|
|
91
|
+
* Text written the way a date field or a timestamp stores it (2026-12-04,
|
|
92
|
+
* 2026-12-04T10:30:00Z) as that date; any other text is not a date. A bare
|
|
93
|
+
* YYYY-MM-DD is that day at local midnight, as format.ts reads it.
|
|
94
|
+
*/
|
|
95
|
+
export function isoDate(text) {
|
|
96
|
+
const s = text.trim();
|
|
97
|
+
return ISO_DATE.test(s) ? toDate(s) : null;
|
|
98
|
+
}
|
|
99
|
+
// A bare YYYY-MM-DD (what a date field stores) is that day at local midnight,
|
|
100
|
+
// as format.ts reads it and as TODAY() is, not midnight UTC. Other text is a
|
|
101
|
+
// date only with a four-digit year and a separator ("October 5, 2026",
|
|
102
|
+
// "10/05/2026"), so "1" or "12" is not read as a year.
|
|
103
|
+
const toDateV = (v) => {
|
|
104
|
+
if (v instanceof Date)
|
|
105
|
+
return Number.isNaN(v.getTime()) ? null : v;
|
|
106
|
+
if (v == null || v === '' || typeof v === 'boolean')
|
|
107
|
+
return null;
|
|
108
|
+
if (typeof v === 'number')
|
|
109
|
+
return toDate(v);
|
|
110
|
+
const s = toText(v).trim();
|
|
111
|
+
return isoDate(s) ?? (/\d{4}/.test(s) && /[/\s,.-]/.test(s) ? toDate(s) : null);
|
|
112
|
+
};
|
|
113
|
+
const flat = (args) => args.flatMap((a) => (Array.isArray(a) ? a : [a]));
|
|
114
|
+
const pad2 = (n) => String(n).padStart(2, '0');
|
|
115
|
+
/** Trim float noise: 2.1 / 0.1 is 21.000000000000004. */
|
|
116
|
+
const clean = (n) => Number(n.toPrecision(12));
|
|
117
|
+
/** A unit name as DATEADD and DATETIME_DIFF read it. "m" is minutes; months are "months". */
|
|
118
|
+
const UNITS = {
|
|
119
|
+
milliseconds: 'ms', millisecond: 'ms', ms: 'ms',
|
|
120
|
+
seconds: 's', second: 's', s: 's',
|
|
121
|
+
minutes: 'm', minute: 'm', m: 'm',
|
|
122
|
+
hours: 'h', hour: 'h', h: 'h',
|
|
123
|
+
days: 'd', day: 'd', d: 'd',
|
|
124
|
+
weeks: 'w', week: 'w', w: 'w',
|
|
125
|
+
months: 'mo', month: 'mo',
|
|
126
|
+
years: 'y', year: 'y', y: 'y',
|
|
127
|
+
};
|
|
128
|
+
const UNIT_MS = { ms: 1, s: 1e3, m: 6e4, h: 36e5 };
|
|
129
|
+
const unitOf = (v) => UNITS[toText(v).trim().toLowerCase()] ?? null;
|
|
130
|
+
/** `d` plus `n` months, kept in the month: January 31 plus one month is February 28. */
|
|
131
|
+
function addMonths(d, n) {
|
|
132
|
+
const r = new Date(d);
|
|
133
|
+
const day = r.getDate();
|
|
134
|
+
r.setDate(1);
|
|
135
|
+
r.setMonth(r.getMonth() + n);
|
|
136
|
+
r.setDate(Math.min(day, new Date(r.getFullYear(), r.getMonth() + 1, 0).getDate()));
|
|
137
|
+
return r;
|
|
138
|
+
}
|
|
139
|
+
function dateAdd(d, n, unit) {
|
|
140
|
+
if (UNIT_MS[unit])
|
|
141
|
+
return new Date(d.getTime() + n * UNIT_MS[unit]);
|
|
142
|
+
if (unit === 'mo' || unit === 'y')
|
|
143
|
+
return addMonths(d, Math.trunc(n) * (unit === 'y' ? 12 : 1));
|
|
144
|
+
// Days and weeks move the calendar day, so a daylight-saving change on the
|
|
145
|
+
// way does not land the result on 23:00 the day before.
|
|
146
|
+
const days = n * (unit === 'w' ? 7 : 1);
|
|
147
|
+
const r = new Date(d);
|
|
148
|
+
r.setDate(r.getDate() + Math.trunc(days));
|
|
149
|
+
return new Date(r.getTime() + (days - Math.trunc(days)) * 864e5);
|
|
150
|
+
}
|
|
151
|
+
/** Local wall-clock time as a number, so a daylight-saving hour does not cut a day short. */
|
|
152
|
+
const wall = (d) => d.getTime() - d.getTimezoneOffset() * 6e4;
|
|
153
|
+
function dateDiff(x, y, unit) {
|
|
154
|
+
if (UNIT_MS[unit])
|
|
155
|
+
return Math.trunc((x.getTime() - y.getTime()) / UNIT_MS[unit]);
|
|
156
|
+
if (unit === 'd' || unit === 'w')
|
|
157
|
+
return Math.trunc((wall(x) - wall(y)) / (unit === 'w' ? 6048e5 : 864e5));
|
|
158
|
+
let months = (x.getFullYear() - y.getFullYear()) * 12 + x.getMonth() - y.getMonth();
|
|
159
|
+
// Not a whole month yet: September 20 to October 5 is 0 months.
|
|
160
|
+
const at = addMonths(y, months).getTime();
|
|
161
|
+
if (months > 0 && at > x.getTime())
|
|
162
|
+
months -= 1;
|
|
163
|
+
else if (months < 0 && at < x.getTime())
|
|
164
|
+
months += 1;
|
|
165
|
+
return unit === 'y' ? Math.trunc(months / 12) : months;
|
|
166
|
+
}
|
|
167
|
+
const FUNCTIONS = {
|
|
168
|
+
// logic
|
|
169
|
+
IF: (a) => (truthy(a[0] ?? null) ? (a[1] ?? null) : (a[2] ?? null)),
|
|
170
|
+
AND: (a) => flat(a).every(truthy),
|
|
171
|
+
OR: (a) => flat(a).some(truthy),
|
|
172
|
+
NOT: (a) => !truthy(a[0] ?? null),
|
|
173
|
+
XOR: (a) => flat(a).filter(truthy).length % 2 === 1,
|
|
174
|
+
BLANK: () => null,
|
|
175
|
+
// The parser runs SWITCH lazily; this is its function list entry.
|
|
176
|
+
SWITCH: (a) => {
|
|
177
|
+
for (let i = 1; i + 1 < a.length; i += 2)
|
|
178
|
+
if (toText(a[0] ?? null) === toText(a[i] ?? null))
|
|
179
|
+
return a[i + 1] ?? null;
|
|
180
|
+
return a.length % 2 === 0 ? (a[a.length - 1] ?? null) : null;
|
|
181
|
+
},
|
|
182
|
+
// text
|
|
183
|
+
CONCATENATE: (a) => a.map(toText).join(''),
|
|
184
|
+
LEN: (a) => toText(a[0] ?? null).length,
|
|
185
|
+
UPPER: (a) => toText(a[0] ?? null).toUpperCase(),
|
|
186
|
+
LOWER: (a) => toText(a[0] ?? null).toLowerCase(),
|
|
187
|
+
TRIM: (a) => toText(a[0] ?? null).trim(),
|
|
188
|
+
LEFT: (a) => toText(a[0] ?? null).slice(0, toNum(a[1] ?? 1)),
|
|
189
|
+
RIGHT: (a) => { const s = toText(a[0] ?? null); const n = toNum(a[1] ?? 1); return n <= 0 ? '' : s.slice(-n); },
|
|
190
|
+
// Without a count, the rest of the text.
|
|
191
|
+
MID: (a) => {
|
|
192
|
+
const s = toText(a[0] ?? null), from = Math.max(0, toNum(a[1] ?? 1) - 1);
|
|
193
|
+
return a.length > 2 ? s.slice(from, from + Math.max(0, toNum(a[2] ?? 0))) : s.slice(from);
|
|
194
|
+
},
|
|
195
|
+
FIND: (a) => toText(a[1] ?? null).indexOf(toText(a[0] ?? null), Math.max(0, toNum(a[2] ?? 1) - 1)) + 1,
|
|
196
|
+
SUBSTITUTE: (a) => toText(a[0] ?? null).split(toText(a[1] ?? null)).join(toText(a[2] ?? null)),
|
|
197
|
+
REPT: (a) => toText(a[0] ?? null).repeat(Math.max(0, Math.min(1000, toNum(a[1] ?? 0)))),
|
|
198
|
+
T: (a) => (typeof a[0] === 'string' ? a[0] : ''),
|
|
199
|
+
// Text with no digit has no value: empty, not 0 (Number('') would say 0).
|
|
200
|
+
VALUE: (a) => { const t = toText(a[0] ?? null).replace(/[^0-9.eE+-]/g, ''); const n = /\d/.test(t) ? Number(t) : NaN; return Number.isFinite(n) ? n : null; },
|
|
201
|
+
ARRAYJOIN: (a) => flat([a[0] ?? null]).map(toText).filter(Boolean).join(a.length > 1 ? toText(a[1] ?? null) : ', '),
|
|
202
|
+
// numbers
|
|
203
|
+
SUM: (a) => flat(a).reduce((s, x) => s + toNum(x), 0),
|
|
204
|
+
AVERAGE: (a) => { const v = flat(a).filter((x) => !isBlank(x)); return v.length ? v.reduce((s, x) => s + toNum(x), 0) / v.length : null; },
|
|
205
|
+
MAX: (a) => { const v = flat(a).filter((x) => !isBlank(x)).map(toNum); return v.length ? Math.max(...v) : null; },
|
|
206
|
+
MIN: (a) => { const v = flat(a).filter((x) => !isBlank(x)).map(toNum); return v.length ? Math.min(...v) : null; },
|
|
207
|
+
COUNT: (a) => flat(a).filter((x) => typeof x === 'number').length,
|
|
208
|
+
COUNTA: (a) => flat(a).filter((x) => !isBlank(x)).length,
|
|
209
|
+
ROUND: (a) => { const p = 10 ** toNum(a[1] ?? 0); return Math.round(toNum(a[0] ?? null) * p) / p; },
|
|
210
|
+
ROUNDUP: (a) => { const p = 10 ** toNum(a[1] ?? 0); return Math.ceil(toNum(a[0] ?? null) * p) / p; },
|
|
211
|
+
ROUNDDOWN: (a) => { const p = 10 ** toNum(a[1] ?? 0); return Math.floor(toNum(a[0] ?? null) * p) / p; },
|
|
212
|
+
INT: (a) => Math.floor(toNum(a[0] ?? null)),
|
|
213
|
+
ABS: (a) => Math.abs(toNum(a[0] ?? null)),
|
|
214
|
+
MOD: (a) => toNum(a[0] ?? null) % toNum(a[1] ?? 1),
|
|
215
|
+
POWER: (a) => toNum(a[0] ?? null) ** toNum(a[1] ?? 1),
|
|
216
|
+
SQRT: (a) => Math.sqrt(toNum(a[0] ?? null)),
|
|
217
|
+
// Up or down to a multiple of the significance (default 1): CEILING(7, 5) is 10.
|
|
218
|
+
CEILING: (a) => { const s = toNum(a[1] ?? 1); return s === 0 ? 0 : clean(Math.ceil(clean(toNum(a[0] ?? null) / s)) * s); },
|
|
219
|
+
FLOOR: (a) => { const s = toNum(a[1] ?? 1); return s === 0 ? 0 : clean(Math.floor(clean(toNum(a[0] ?? null) / s)) * s); },
|
|
220
|
+
// dates
|
|
221
|
+
TODAY: () => { const d = new Date(); d.setHours(0, 0, 0, 0); return d; },
|
|
222
|
+
NOW: () => new Date(),
|
|
223
|
+
YEAR: (a) => toDateV(a[0] ?? null)?.getFullYear() ?? null,
|
|
224
|
+
MONTH: (a) => { const d = toDateV(a[0] ?? null); return d ? d.getMonth() + 1 : null; },
|
|
225
|
+
DAY: (a) => toDateV(a[0] ?? null)?.getDate() ?? null,
|
|
226
|
+
WEEKDAY: (a) => toDateV(a[0] ?? null)?.getDay() ?? null,
|
|
227
|
+
// An unknown unit is empty here; one written as text is a compile error (see the parser).
|
|
228
|
+
DATEADD: (a) => {
|
|
229
|
+
const d = toDateV(a[0] ?? null), unit = a.length > 2 ? unitOf(a[2] ?? null) : 'd';
|
|
230
|
+
return d && unit ? dateAdd(d, toNum(a[1] ?? 0), unit) : null;
|
|
231
|
+
},
|
|
232
|
+
DATETIME_DIFF: (a) => {
|
|
233
|
+
const x = toDateV(a[0] ?? null), y = toDateV(a[1] ?? null), unit = a.length > 2 ? unitOf(a[2] ?? null) : 's';
|
|
234
|
+
return x && y && unit ? dateDiff(x, y, unit) : null;
|
|
235
|
+
},
|
|
236
|
+
IS_BEFORE: (a) => { const x = toDateV(a[0] ?? null), y = toDateV(a[1] ?? null); return !!x && !!y && x < y; },
|
|
237
|
+
IS_AFTER: (a) => { const x = toDateV(a[0] ?? null), y = toDateV(a[1] ?? null); return !!x && !!y && x > y; },
|
|
238
|
+
// The local calendar day, the same day TODAY() and a date field mean.
|
|
239
|
+
DATESTR: (a) => { const d = toDateV(a[0] ?? null); return d ? `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}` : null; },
|
|
240
|
+
};
|
|
241
|
+
function cmp(op, x, y) {
|
|
242
|
+
switch (op) {
|
|
243
|
+
case '=': return x === y;
|
|
244
|
+
case '!=': return x !== y;
|
|
245
|
+
case '<': return x < y;
|
|
246
|
+
case '>': return x > y;
|
|
247
|
+
case '<=': return x <= y;
|
|
248
|
+
default: return x >= y;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/** Functions whose third argument is a unit: a unit written as text is checked when the formula compiles. */
|
|
252
|
+
const UNIT_ARG = new Set(['DATEADD', 'DATETIME_DIFF']);
|
|
253
|
+
/** The functions a formula may call, for the editor's help line. */
|
|
254
|
+
export const FORMULA_FUNCTIONS = Object.keys(FUNCTIONS).sort();
|
|
255
|
+
/** The message for a formula that names the field it belongs to. */
|
|
256
|
+
export const selfReferenceError = (name) => new FormulaError(`A formula cannot read its own field {${name.trim()}}`);
|
|
257
|
+
/**
|
|
258
|
+
* A resolver over columns: a name matches a header or a field key, ignoring
|
|
259
|
+
* case and outer spaces. `self` lists the names of the formula's own field
|
|
260
|
+
* (its key, its header, a header being typed): naming one is an error.
|
|
261
|
+
*/
|
|
262
|
+
export function columnResolver(columns, self = []) {
|
|
263
|
+
const own = new Set(self.filter((s) => s != null && String(s).trim() !== '').map((s) => String(s).trim().toLowerCase()));
|
|
264
|
+
return (name) => {
|
|
265
|
+
const n = name.trim().toLowerCase();
|
|
266
|
+
if (own.has(n))
|
|
267
|
+
throw selfReferenceError(name);
|
|
268
|
+
const hit = columns.find((c) => String(c.header ?? '').trim().toLowerCase() === n || String(c.field ?? '').toLowerCase() === n);
|
|
269
|
+
return hit ? String(hit.field ?? hit.id) : null;
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
class Parser {
|
|
273
|
+
constructor(toks, resolve, refs) {
|
|
274
|
+
this.toks = toks;
|
|
275
|
+
this.resolve = resolve;
|
|
276
|
+
this.refs = refs;
|
|
277
|
+
this.i = 0;
|
|
278
|
+
}
|
|
279
|
+
peek() { return this.toks[this.i]; }
|
|
280
|
+
next() { return this.toks[this.i++]; }
|
|
281
|
+
expect(k, v) {
|
|
282
|
+
const t = this.next();
|
|
283
|
+
if (!t || t.k !== k || (v && t.v !== v))
|
|
284
|
+
throw new FormulaError(`Expected "${v ?? k}"${t ? ` at ${t.at + 1}` : ' at the end'}`);
|
|
285
|
+
return t;
|
|
286
|
+
}
|
|
287
|
+
parse() {
|
|
288
|
+
const n = this.compare();
|
|
289
|
+
if (this.peek())
|
|
290
|
+
throw new FormulaError(`Unexpected "${this.peek().v}" at ${this.peek().at + 1}`);
|
|
291
|
+
return n;
|
|
292
|
+
}
|
|
293
|
+
compare() {
|
|
294
|
+
let left = this.concat();
|
|
295
|
+
for (let t = this.peek(); t && t.k === 'op' && ['=', '!=', '<', '>', '<=', '>='].includes(t.v); t = this.peek()) {
|
|
296
|
+
this.next();
|
|
297
|
+
const op = t.v, l = left, r = this.concat();
|
|
298
|
+
left = (row) => {
|
|
299
|
+
const a = l(row), b = r(row);
|
|
300
|
+
// A date against a stored date (text) or another date compares as dates.
|
|
301
|
+
// An empty or unreadable side matches nothing: only != is true.
|
|
302
|
+
if (a instanceof Date || b instanceof Date) {
|
|
303
|
+
const x = toDateV(a), y = toDateV(b);
|
|
304
|
+
if (!x || !y)
|
|
305
|
+
return op === '!=';
|
|
306
|
+
return cmp(op, x.getTime(), y.getTime());
|
|
307
|
+
}
|
|
308
|
+
const both = typeof a === 'number' || typeof b === 'number';
|
|
309
|
+
const x = both ? toNum(a) : toText(a);
|
|
310
|
+
const y = both ? toNum(b) : toText(b);
|
|
311
|
+
return cmp(op, x, y);
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
return left;
|
|
315
|
+
}
|
|
316
|
+
concat() {
|
|
317
|
+
let left = this.add();
|
|
318
|
+
while (this.peek()?.k === 'op' && this.peek().v === '&') {
|
|
319
|
+
this.next();
|
|
320
|
+
const l = left, r = this.add();
|
|
321
|
+
left = (row) => toText(l(row)) + toText(r(row));
|
|
322
|
+
}
|
|
323
|
+
return left;
|
|
324
|
+
}
|
|
325
|
+
add() {
|
|
326
|
+
let left = this.mul();
|
|
327
|
+
for (let t = this.peek(); t && t.k === 'op' && (t.v === '+' || t.v === '-'); t = this.peek()) {
|
|
328
|
+
this.next();
|
|
329
|
+
const l = left, r = this.mul(), plus = t.v === '+';
|
|
330
|
+
left = (row) => (plus ? toNum(l(row)) + toNum(r(row)) : toNum(l(row)) - toNum(r(row)));
|
|
331
|
+
}
|
|
332
|
+
return left;
|
|
333
|
+
}
|
|
334
|
+
mul() {
|
|
335
|
+
let left = this.unary();
|
|
336
|
+
for (let t = this.peek(); t && t.k === 'op' && (t.v === '*' || t.v === '/'); t = this.peek()) {
|
|
337
|
+
this.next();
|
|
338
|
+
const l = left, r = this.unary(), times = t.v === '*';
|
|
339
|
+
left = (row) => {
|
|
340
|
+
if (times)
|
|
341
|
+
return toNum(l(row)) * toNum(r(row));
|
|
342
|
+
const d = toNum(r(row));
|
|
343
|
+
return d === 0 ? null : toNum(l(row)) / d;
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return left;
|
|
347
|
+
}
|
|
348
|
+
unary() {
|
|
349
|
+
const t = this.peek();
|
|
350
|
+
if (t?.k === 'op' && t.v === '-') {
|
|
351
|
+
this.next();
|
|
352
|
+
const n = this.unary();
|
|
353
|
+
return (row) => -toNum(n(row));
|
|
354
|
+
}
|
|
355
|
+
return this.primary();
|
|
356
|
+
}
|
|
357
|
+
primary() {
|
|
358
|
+
const t = this.next();
|
|
359
|
+
if (!t)
|
|
360
|
+
throw new FormulaError('The formula ends too early');
|
|
361
|
+
if (t.k === 'num') {
|
|
362
|
+
const n = Number(t.v);
|
|
363
|
+
return () => n;
|
|
364
|
+
}
|
|
365
|
+
if (t.k === 'str') {
|
|
366
|
+
const s = t.v;
|
|
367
|
+
return () => s;
|
|
368
|
+
}
|
|
369
|
+
if (t.k === 'ref') {
|
|
370
|
+
const key = this.resolve(t.v);
|
|
371
|
+
if (key == null)
|
|
372
|
+
throw new FormulaError(`No field named "${t.v}"`);
|
|
373
|
+
this.refs.add(key);
|
|
374
|
+
return (row) => (row?.[key] ?? null);
|
|
375
|
+
}
|
|
376
|
+
if (t.k === '(') {
|
|
377
|
+
const n = this.compare();
|
|
378
|
+
this.expect(')');
|
|
379
|
+
return n;
|
|
380
|
+
}
|
|
381
|
+
if (t.k === 'id') {
|
|
382
|
+
const upper = t.v.toUpperCase();
|
|
383
|
+
if (upper === 'TRUE' && this.peek()?.k !== '(')
|
|
384
|
+
return () => true;
|
|
385
|
+
if (upper === 'FALSE' && this.peek()?.k !== '(')
|
|
386
|
+
return () => false;
|
|
387
|
+
const fn = FUNCTIONS[upper];
|
|
388
|
+
if (!fn)
|
|
389
|
+
throw new FormulaError(`Unknown function ${t.v}`);
|
|
390
|
+
this.expect('(');
|
|
391
|
+
const args = [];
|
|
392
|
+
// Each argument that is a bare text literal, for checking a unit now.
|
|
393
|
+
const literals = [];
|
|
394
|
+
const arg = () => {
|
|
395
|
+
const start = this.i;
|
|
396
|
+
args.push(this.compare());
|
|
397
|
+
const only = this.i === start + 1 ? this.toks[start] : undefined;
|
|
398
|
+
literals.push(only?.k === 'str' ? only.v : undefined);
|
|
399
|
+
};
|
|
400
|
+
if (this.peek()?.k !== ')') {
|
|
401
|
+
arg();
|
|
402
|
+
while (this.peek()?.k === ',') {
|
|
403
|
+
this.next();
|
|
404
|
+
arg();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
this.expect(')');
|
|
408
|
+
const unit = literals[2];
|
|
409
|
+
if (UNIT_ARG.has(upper) && unit !== undefined && !unitOf(unit)) {
|
|
410
|
+
throw new FormulaError(`Unknown unit "${unit}" in ${upper}: use ms, seconds, minutes, hours, days, weeks, months or years`);
|
|
411
|
+
}
|
|
412
|
+
// IF and SWITCH evaluate lazily, so IF({x} = 0, 0, 1 / {x}) does not divide by zero.
|
|
413
|
+
if (upper === 'IF')
|
|
414
|
+
return (row) => (truthy(args[0]?.(row) ?? null) ? (args[1]?.(row) ?? null) : (args[2]?.(row) ?? null));
|
|
415
|
+
if (upper === 'SWITCH') {
|
|
416
|
+
return (row) => {
|
|
417
|
+
if (!args.length)
|
|
418
|
+
return null;
|
|
419
|
+
const v = toText(args[0](row));
|
|
420
|
+
for (let i = 1; i + 1 < args.length; i += 2)
|
|
421
|
+
if (v === toText(args[i](row)))
|
|
422
|
+
return args[i + 1](row);
|
|
423
|
+
return args.length % 2 === 0 ? args[args.length - 1](row) : null;
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
return (row) => fn(args.map((a) => a(row)));
|
|
427
|
+
}
|
|
428
|
+
throw new FormulaError(`Unexpected "${t.v}" at ${t.at + 1}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/** Compile a formula. Throws FormulaError with a message for the user. */
|
|
432
|
+
export function compileFormula(src, resolve) {
|
|
433
|
+
if (!src.trim())
|
|
434
|
+
throw new FormulaError('The formula is empty');
|
|
435
|
+
const refs = new Set();
|
|
436
|
+
const node = new Parser(tokenize(src), resolve, refs).parse();
|
|
437
|
+
return {
|
|
438
|
+
fn: (row) => {
|
|
439
|
+
try {
|
|
440
|
+
const v = node(row);
|
|
441
|
+
return typeof v === 'number' && !Number.isFinite(v) ? null : v;
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
refs: [...refs],
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* The formula with each `{name}` that `rename` gives a new name for written
|
|
452
|
+
* as `{new name}`. Braces inside text ("{x}") are left alone; a formula that
|
|
453
|
+
* does not tokenize comes back as it was.
|
|
454
|
+
*/
|
|
455
|
+
export function renameFormulaRefs(src, rename) {
|
|
456
|
+
let toks;
|
|
457
|
+
try {
|
|
458
|
+
toks = tokenize(src);
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
return src;
|
|
462
|
+
}
|
|
463
|
+
let out = '';
|
|
464
|
+
let from = 0;
|
|
465
|
+
for (const t of toks) {
|
|
466
|
+
if (t.k !== 'ref')
|
|
467
|
+
continue;
|
|
468
|
+
const next = rename(t.v);
|
|
469
|
+
if (next == null)
|
|
470
|
+
continue;
|
|
471
|
+
const end = src.indexOf('}', t.at);
|
|
472
|
+
out += `${src.slice(from, t.at)}{${next}}`;
|
|
473
|
+
from = end + 1;
|
|
474
|
+
}
|
|
475
|
+
return from ? out + src.slice(from) : src;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* The keys of the fields a formula names, as `resolve` reads them. Unlike
|
|
479
|
+
* `compileFormula` it does not stop at a name no field has or at a mistake
|
|
480
|
+
* elsewhere in the formula; a formula that does not tokenize names nothing.
|
|
481
|
+
*/
|
|
482
|
+
export function formulaRefs(src, resolve) {
|
|
483
|
+
let toks;
|
|
484
|
+
try {
|
|
485
|
+
toks = tokenize(src);
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
return [];
|
|
489
|
+
}
|
|
490
|
+
const keys = new Set();
|
|
491
|
+
for (const t of toks) {
|
|
492
|
+
if (t.k !== 'ref')
|
|
493
|
+
continue;
|
|
494
|
+
try {
|
|
495
|
+
const key = resolve(t.v);
|
|
496
|
+
if (key != null)
|
|
497
|
+
keys.add(key);
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
/* a name the resolver refuses reads nothing */
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return [...keys];
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* The formula fields among `columns` that read the field `field` (its key or
|
|
507
|
+
* id): deleting it turns their cells to #ERROR. Names resolve against
|
|
508
|
+
* `columns`, as the field editor resolves them.
|
|
509
|
+
*/
|
|
510
|
+
export function formulaReaders(columns, field) {
|
|
511
|
+
const keys = new Set([field.field, field.id].filter((k) => k != null && k !== '').map(String));
|
|
512
|
+
if (!keys.size)
|
|
513
|
+
return [];
|
|
514
|
+
const resolve = columnResolver(columns);
|
|
515
|
+
return columns.filter((c) => {
|
|
516
|
+
if (c.fieldType !== 'formula' || keys.has(String(c.field ?? c.id ?? '')))
|
|
517
|
+
return false;
|
|
518
|
+
return formulaRefs(String(c.fieldProps?.formula ?? ''), resolve).some((k) => keys.has(k));
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
/** The formula's message if it does not compile, else null. */
|
|
522
|
+
export function formulaError(src, resolve) {
|
|
523
|
+
try {
|
|
524
|
+
compileFormula(src, resolve);
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
catch (e) {
|
|
528
|
+
return e instanceof FormulaError ? e.message : 'The formula is not valid';
|
|
529
|
+
}
|
|
530
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the fields module's own surfaces (the view bar, the gallery and form
|
|
3
|
+
* views, a board view's moves) need from a grid: the grid behind an api, the
|
|
4
|
+
* data columns with their types, the primary field, and a way to write a
|
|
5
|
+
* value that goes through the grid's edit path.
|
|
6
|
+
*/
|
|
7
|
+
/** A data column as the fields surfaces see it. */
|
|
8
|
+
export type FieldColumn = {
|
|
9
|
+
id: string;
|
|
10
|
+
field: string;
|
|
11
|
+
header: string;
|
|
12
|
+
fieldType?: string;
|
|
13
|
+
/** The column as filled in (formatter, editor, fieldProps). */
|
|
14
|
+
def: Record<string, any>;
|
|
15
|
+
visible: boolean;
|
|
16
|
+
};
|
|
17
|
+
/** Called on every draw of a grid with fields turned on. */
|
|
18
|
+
export declare function rememberGrid(ctrl: any): void;
|
|
19
|
+
/** The grid an api belongs to; undefined until that grid has drawn once. */
|
|
20
|
+
export declare function gridOf(api: unknown): any;
|
|
21
|
+
export declare const columnIdOf: (def: Record<string, any>) => string;
|
|
22
|
+
/** Every data column (declared ones with a field), in screen order, hidden ones last. */
|
|
23
|
+
export declare function fieldColumns(ctrl: any): FieldColumn[];
|
|
24
|
+
/** The primary field's column id: the `primaryField` prop, matched by id or field. */
|
|
25
|
+
export declare function primaryIdOf(ctrl: any): string | null;
|
|
26
|
+
/**
|
|
27
|
+
* Write a value the way an edit in the cell does (undo, validation,
|
|
28
|
+
* onCellValueChange, last-modified fields). False when the row is not shown
|
|
29
|
+
* or the cell cannot be edited.
|
|
30
|
+
*/
|
|
31
|
+
export declare function writeCell(ctrl: any, rowId: string, columnId: string, value: unknown): boolean;
|
|
32
|
+
/** A row's value for a column, through the grid's edit overlay (as the cells read it). */
|
|
33
|
+
export declare function cellValue(ctrl: any, row: {
|
|
34
|
+
id: string;
|
|
35
|
+
getCellValueByColumnId?: (id: string) => unknown;
|
|
36
|
+
original?: any;
|
|
37
|
+
}, column: FieldColumn): unknown;
|
|
38
|
+
/** A value as its cell writes it as text (the column's formatter), for titles and search. */
|
|
39
|
+
export declare function cellText(column: FieldColumn, value: unknown, row?: unknown): string;
|
|
40
|
+
export type GridRow = {
|
|
41
|
+
id: string;
|
|
42
|
+
original: Record<string, unknown>;
|
|
43
|
+
getCellValueByColumnId?: (id: string) => unknown;
|
|
44
|
+
};
|
|
45
|
+
/** The rows the grid shows, filtered and sorted, without group header rows. */
|
|
46
|
+
export declare function dataRows(ctrl: any): GridRow[];
|
|
47
|
+
type TreeRow = {
|
|
48
|
+
subRows?: TreeRow[];
|
|
49
|
+
getCanExpand?: () => boolean;
|
|
50
|
+
__treeRow?: boolean;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* The grid's advanced filter as a test of one record, or null when nothing
|
|
54
|
+
* filters. The grid filters after it groups, so a group's own rows still hold
|
|
55
|
+
* the records the filter hides; this is how a group header leaves them out.
|
|
56
|
+
*/
|
|
57
|
+
export declare function leafFilter(ctrl: any): ((row: unknown) => boolean) | null;
|
|
58
|
+
/** The records under a group row, those the filter keeps. */
|
|
59
|
+
export declare function groupLeaves<TRow extends TreeRow>(row: TRow, keep: ((row: unknown) => boolean) | null): TRow[];
|
|
60
|
+
export {};
|