@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,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automations: "when this happens to a record, do these things", per table
|
|
3
|
+
* of a base.
|
|
4
|
+
*
|
|
5
|
+
* A trigger names the moment (a record created, a field changed, a record
|
|
6
|
+
* that starts matching a condition, a form sent, a button pressed, a time of
|
|
7
|
+
* day). An optional condition, written with the view bar's filter, narrows it
|
|
8
|
+
* to some records. The actions then run in order: change the record, create a
|
|
9
|
+
* record in any table (linked back to this one if you like), show a notice,
|
|
10
|
+
* color the record while it matches, or run a function the app registered.
|
|
11
|
+
*
|
|
12
|
+
* Automations run in the page of the person who made the change, once: a
|
|
13
|
+
* change that arrives from another person (realtime) does not set them off
|
|
14
|
+
* here. A change an automation makes can set off other automations, up to
|
|
15
|
+
* `MAX_DEPTH` deep, so two that answer each other stop instead of looping.
|
|
16
|
+
* Every run is kept in a short history, with what each action did.
|
|
17
|
+
*/
|
|
18
|
+
import { toast } from '@svgrid/grid';
|
|
19
|
+
import { createScheduler } from '../scheduling.js';
|
|
20
|
+
import { colId, primaryOf, rowIdOf, syncAllInverses } from './base-tables.js';
|
|
21
|
+
import { compileFieldFilter, describeFieldFilter } from './filter-model.js';
|
|
22
|
+
import { fieldPropsOf, fillFieldColumn } from './registry.svelte.js';
|
|
23
|
+
import { noteChange } from './activity.svelte.js';
|
|
24
|
+
import { getFieldUsers } from './sources.svelte.js';
|
|
25
|
+
import { newRecordId } from './field-ops.js';
|
|
26
|
+
import { fieldText, fieldValue, linkNames } from './base-alerts.js';
|
|
27
|
+
import { callWebhook, sendEmailAction } from './automation-outbound.js';
|
|
28
|
+
const appActions = new Map();
|
|
29
|
+
/**
|
|
30
|
+
* Add an action automations can run: a webhook, an email, a message to chat.
|
|
31
|
+
* It runs in the page; keep secrets on your server and call it from here.
|
|
32
|
+
*/
|
|
33
|
+
export function registerAutomationAction(id, action) {
|
|
34
|
+
appActions.set(id, action);
|
|
35
|
+
}
|
|
36
|
+
export function listAutomationActions() {
|
|
37
|
+
return [...appActions].map(([id, a]) => ({ id, label: a.label }));
|
|
38
|
+
}
|
|
39
|
+
// ---- Reading a table outside its grid ----------------------------------------------
|
|
40
|
+
const readers = new WeakMap();
|
|
41
|
+
/** A table's fields as filled in (formulas, lookups, formats), by column id. */
|
|
42
|
+
export function tableFields(t) {
|
|
43
|
+
const hit = readers.get(t.columns);
|
|
44
|
+
if (hit)
|
|
45
|
+
return hit;
|
|
46
|
+
const all = t.columns.map((d) => ({ ...d, id: colId(d) }));
|
|
47
|
+
const map = new Map();
|
|
48
|
+
for (const d of t.columns) {
|
|
49
|
+
if (d.field == null)
|
|
50
|
+
continue;
|
|
51
|
+
const def = fillFieldColumn(d, all);
|
|
52
|
+
map.set(colId(d), { id: colId(d), field: String(d.field), header: String(d.header ?? d.field), fieldType: d.fieldType, def, visible: true });
|
|
53
|
+
}
|
|
54
|
+
readers.set(t.columns, map);
|
|
55
|
+
return map;
|
|
56
|
+
}
|
|
57
|
+
/** A field by a header, its key or its id, ignoring case and outer spaces, as a formula's `{name}` matches one. */
|
|
58
|
+
const byName = (t, name) => {
|
|
59
|
+
const lower = name.trim().toLowerCase();
|
|
60
|
+
for (const c of tableFields(t).values()) {
|
|
61
|
+
if (c.header.trim().toLowerCase() === lower || c.field.toLowerCase() === lower || c.id.toLowerCase() === lower)
|
|
62
|
+
return c;
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
};
|
|
66
|
+
/** `{Field name}` tokens of a template, filled from a record. */
|
|
67
|
+
export function fillTokens(t, record, template) {
|
|
68
|
+
return template.replace(/\{([^{}\n]+)\}/g, (m, name) => {
|
|
69
|
+
if (!record)
|
|
70
|
+
return m;
|
|
71
|
+
const c = byName(t, name);
|
|
72
|
+
return c ? fieldText(c, record) : m;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/** Whether a record matches a condition (no condition matches every record). */
|
|
76
|
+
export function matches(t, condition, record) {
|
|
77
|
+
if (!condition)
|
|
78
|
+
return true;
|
|
79
|
+
const fields = tableFields(t);
|
|
80
|
+
const test = compileFieldFilter(condition, (row, id) => {
|
|
81
|
+
const c = fields.get(id);
|
|
82
|
+
return c ? fieldValue(c, row) : row[id];
|
|
83
|
+
}, linkNames);
|
|
84
|
+
return test ? test(record) : true;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Last modified time fields of a record an automation changed, as an edit
|
|
88
|
+
* stamps them. Last modified by names a person, so it keeps its author.
|
|
89
|
+
*/
|
|
90
|
+
function stampModifiedTime(t, row, changedIds, at) {
|
|
91
|
+
for (const d of t.columns) {
|
|
92
|
+
if (d.fieldType !== 'lastModifiedTime' || d.field == null || changedIds.includes(colId(d)))
|
|
93
|
+
continue;
|
|
94
|
+
const p = fieldPropsOf(d);
|
|
95
|
+
if (p.watch === 'specific' && !changedIds.some((id) => (p.watchFields ?? []).includes(id)))
|
|
96
|
+
continue;
|
|
97
|
+
row[String(d.field)] = at.toISOString();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// ---- Values -----------------------------------------------------------------------
|
|
101
|
+
const DAY = 86400000;
|
|
102
|
+
const isoDay = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
103
|
+
/** What a value written in an action becomes in a field of `t`. */
|
|
104
|
+
export function fieldValueFrom(t, def, spec, ctx) {
|
|
105
|
+
const type = def.fieldType;
|
|
106
|
+
if (typeof spec !== 'string')
|
|
107
|
+
return spec;
|
|
108
|
+
const raw = spec.includes('{record}') ? spec : fillTokens(ctx.from, ctx.record, spec);
|
|
109
|
+
const text = raw.trim();
|
|
110
|
+
if (text === '')
|
|
111
|
+
return type === 'checkbox' ? false : type === 'multiSelect' || type === 'link' || type === 'attachment' ? [] : null;
|
|
112
|
+
switch (type) {
|
|
113
|
+
case 'checkbox':
|
|
114
|
+
return /^(true|yes|1|checked|on)$/i.test(text);
|
|
115
|
+
case 'number':
|
|
116
|
+
case 'rating':
|
|
117
|
+
case 'duration':
|
|
118
|
+
case 'currency':
|
|
119
|
+
case 'percent': {
|
|
120
|
+
const n = Number(text.replace(/[^0-9.+-eE]/g, ''));
|
|
121
|
+
// "100%" in a percent field is 1, as the field keeps it.
|
|
122
|
+
return Number.isFinite(n) ? (text.endsWith('%') ? n / 100 : n) : null;
|
|
123
|
+
}
|
|
124
|
+
case 'multiSelect':
|
|
125
|
+
return text.split(/\s*,\s*/).filter(Boolean);
|
|
126
|
+
case 'user': {
|
|
127
|
+
const found = getFieldUsers().find((u) => u.id === text || u.name.toLocaleLowerCase() === text.toLocaleLowerCase());
|
|
128
|
+
return found ? found.id : text;
|
|
129
|
+
}
|
|
130
|
+
case 'date': {
|
|
131
|
+
const m = /^(today|now)\s*(?:([+-])\s*(\d+))?$/i.exec(text);
|
|
132
|
+
if (!m)
|
|
133
|
+
return text;
|
|
134
|
+
const shift = m[2] ? (m[2] === '-' ? -1 : 1) * Number(m[3]) * DAY : 0;
|
|
135
|
+
const at = new Date(ctx.now.getTime() + shift);
|
|
136
|
+
return m[1].toLowerCase() === 'now' || def.fieldProps?.time ? at.toISOString() : isoDay(at);
|
|
137
|
+
}
|
|
138
|
+
case 'link': {
|
|
139
|
+
const target = ctx.tables.find((x) => x.id === def.fieldProps?.table);
|
|
140
|
+
const out = [];
|
|
141
|
+
for (const part of text.split(/\s*,\s*/)) {
|
|
142
|
+
if (part === '{record}') {
|
|
143
|
+
if (ctx.record && ctx.from.id === target?.id)
|
|
144
|
+
out.push(rowIdOf(ctx.from, ctx.record));
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const name = fillTokens(ctx.from, ctx.record, part).toLocaleLowerCase();
|
|
148
|
+
const hit = target?.data.find((r) => String(r?.[primaryOf(target)] ?? '').toLocaleLowerCase() === name);
|
|
149
|
+
if (hit && target)
|
|
150
|
+
out.push(rowIdOf(target, hit));
|
|
151
|
+
}
|
|
152
|
+
return def.fieldProps?.multiple === false ? out.slice(0, 1) : out;
|
|
153
|
+
}
|
|
154
|
+
default:
|
|
155
|
+
return raw;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** The longest chain of automations setting each other off. */
|
|
159
|
+
export const MAX_DEPTH = 4;
|
|
160
|
+
let runSeq = 0;
|
|
161
|
+
export function createAutomationRunner(options) {
|
|
162
|
+
const now = options.now ?? (() => new Date());
|
|
163
|
+
let runs = $state.raw([]);
|
|
164
|
+
let scheduler = null;
|
|
165
|
+
let chain = Promise.resolve();
|
|
166
|
+
const record = (run) => (runs = [run, ...runs].slice(0, 100));
|
|
167
|
+
const automationsOf = (t) => (t.automations ?? []).filter((a) => a.enabled && a.runOn !== 'server');
|
|
168
|
+
const tableOf = (id) => options.getTables()?.find((t) => t.id === id);
|
|
169
|
+
const recordNow = (t, id) => tableOf(t.id)?.data.find((r) => r && rowIdOf(t, r) === id) ?? null;
|
|
170
|
+
const setOff = (t, e) => automationsFor(t, e, 'browser');
|
|
171
|
+
/** Hear of something that happened to a record; the automations it sets off run in order. */
|
|
172
|
+
function handle(e) {
|
|
173
|
+
const t = tableOf(e.table);
|
|
174
|
+
if (!t)
|
|
175
|
+
return chain;
|
|
176
|
+
const list = setOff(t, e);
|
|
177
|
+
const depth = 'depth' in e ? (e.depth ?? 0) : 0;
|
|
178
|
+
for (const a of list)
|
|
179
|
+
chain = chain.then(() => runOne(a, t.id, rowIdOf(t, e.record), depth));
|
|
180
|
+
return chain;
|
|
181
|
+
}
|
|
182
|
+
async function runOne(a, tableId, recordId, depth) {
|
|
183
|
+
const t = tableOf(tableId);
|
|
184
|
+
if (!t)
|
|
185
|
+
return;
|
|
186
|
+
const rec = recordId == null ? null : recordNow(t, recordId);
|
|
187
|
+
const run = { id: ++runSeq, automationId: a.id, name: a.name, table: t.name, record: rec ? String(rec[primaryOf(t)] ?? recordId) : null, at: now().getTime(), status: 'ok', steps: [] };
|
|
188
|
+
if (depth >= MAX_DEPTH) {
|
|
189
|
+
run.status = 'stopped';
|
|
190
|
+
run.steps.push({ action: 'Stopped', detail: `Automations set each other off ${MAX_DEPTH} times in a row; this one did not run, so they cannot loop.` });
|
|
191
|
+
record(run);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
for (const action of a.actions) {
|
|
195
|
+
try {
|
|
196
|
+
const step = await act(a, action, tableId, recordId, depth);
|
|
197
|
+
if (step)
|
|
198
|
+
run.steps.push(step);
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
run.status = 'error';
|
|
202
|
+
run.steps.push({ action: actionLabel(action), detail: '', error: e instanceof Error ? e.message : String(e) });
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
record(run);
|
|
207
|
+
}
|
|
208
|
+
async function act(a, action, tableId, recordId, depth) {
|
|
209
|
+
const tables = options.getTables() ?? [];
|
|
210
|
+
const t = tables.find((x) => x.id === tableId);
|
|
211
|
+
const rec = recordId == null ? null : (t.data.find((r) => r && rowIdOf(t, r) === recordId) ?? null);
|
|
212
|
+
const ctx = { tables, from: t, record: rec, now: now() };
|
|
213
|
+
switch (action.type) {
|
|
214
|
+
case 'notify': {
|
|
215
|
+
const message = fillTokens(t, rec, action.message);
|
|
216
|
+
const sev = action.severity ?? 'info';
|
|
217
|
+
toast[sev](message);
|
|
218
|
+
return { action: 'Notice', detail: message };
|
|
219
|
+
}
|
|
220
|
+
case 'highlight':
|
|
221
|
+
// Drawn by the grid while the record matches; nothing to do now.
|
|
222
|
+
return null;
|
|
223
|
+
case 'updateRecord': {
|
|
224
|
+
if (!rec || recordId == null)
|
|
225
|
+
return { action: 'Update record', detail: 'No record to update' };
|
|
226
|
+
const fields = tableFields(t);
|
|
227
|
+
const patch = {};
|
|
228
|
+
for (const [id, spec] of Object.entries(action.values)) {
|
|
229
|
+
const c = fields.get(id);
|
|
230
|
+
if (c)
|
|
231
|
+
patch[c.field] = fieldValueFrom(t, c.def, spec, ctx);
|
|
232
|
+
}
|
|
233
|
+
const before = { ...rec };
|
|
234
|
+
const after = { ...rec, ...patch };
|
|
235
|
+
const changed = Object.keys(patch).filter((f) => JSON.stringify(before[f] ?? null) !== JSON.stringify(after[f] ?? null));
|
|
236
|
+
if (!changed.length)
|
|
237
|
+
return { action: 'Update record', detail: 'Already set' };
|
|
238
|
+
const changedIds = changed.map((f) => [...fields.values()].find((c) => c.field === f)?.id ?? f);
|
|
239
|
+
stampModifiedTime(t, after, changedIds, ctx.now);
|
|
240
|
+
let next = tables.map((x) => (x.id === t.id ? { ...x, data: x.data.map((r) => (r === rec ? after : r)) } : x));
|
|
241
|
+
next = syncAllInverses(next, t.id);
|
|
242
|
+
options.apply(next, depth + 1, t.id);
|
|
243
|
+
// The record's activity says the automation made the change.
|
|
244
|
+
changed.forEach((f, i) => noteChange({ base: options.scope, tableId: t.id }, recordId, changedIds[i], after[f], a.name));
|
|
245
|
+
// What it changed can set off other automations, one level deeper.
|
|
246
|
+
void handle({ type: 'updated', table: t.id, record: after, before, fields: changedIds, depth: depth + 1 });
|
|
247
|
+
return { action: 'Update record', detail: changed.map((f) => said([...fields.values()].find((c) => c.field === f), f, after)).join(', ') };
|
|
248
|
+
}
|
|
249
|
+
case 'createRecord': {
|
|
250
|
+
const target = tables.find((x) => x.id === action.table);
|
|
251
|
+
if (!target)
|
|
252
|
+
throw new Error(`No table "${action.table}"`);
|
|
253
|
+
const fields = tableFields(target);
|
|
254
|
+
const row = { id: newRecordId() };
|
|
255
|
+
for (const [id, spec] of Object.entries(action.values)) {
|
|
256
|
+
const c = fields.get(id);
|
|
257
|
+
if (c)
|
|
258
|
+
row[c.field] = fieldValueFrom(target, c.def, spec, ctx);
|
|
259
|
+
}
|
|
260
|
+
if (action.linkField && rec) {
|
|
261
|
+
const c = fields.get(action.linkField);
|
|
262
|
+
if (c)
|
|
263
|
+
row[c.field] = [rowIdOf(t, rec)];
|
|
264
|
+
}
|
|
265
|
+
let next = tables.map((x) => (x.id === target.id ? { ...x, data: [...x.data, row] } : x));
|
|
266
|
+
next = syncAllInverses(next, target.id);
|
|
267
|
+
options.apply(next, depth + 1, target.id);
|
|
268
|
+
void handle({ type: 'created', table: target.id, record: row, depth: depth + 1 });
|
|
269
|
+
const name = String(row[primaryOf(target)] ?? '') || 'a record';
|
|
270
|
+
return { action: 'Create record', detail: `${name} in ${target.name}` };
|
|
271
|
+
}
|
|
272
|
+
case 'run':
|
|
273
|
+
return runAppAction(a, action, t, rec);
|
|
274
|
+
case 'webhook':
|
|
275
|
+
return { action: 'Webhook', detail: await callWebhook(action, outbound(a, t, rec)) };
|
|
276
|
+
case 'email':
|
|
277
|
+
// The browser has no mail sender: this fails and says to run it on the server.
|
|
278
|
+
return { action: 'Email', detail: await sendEmailAction(action, outbound(a, t, rec), undefined) };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/** Run the scheduled automations of every table again from now on (after a change to them). */
|
|
282
|
+
function schedule() {
|
|
283
|
+
scheduler?.stop();
|
|
284
|
+
scheduler = null;
|
|
285
|
+
const list = options.getTables() ?? [];
|
|
286
|
+
const due = [];
|
|
287
|
+
for (const t of list)
|
|
288
|
+
for (const a of automationsOf(t))
|
|
289
|
+
if (a.trigger.type === 'scheduled')
|
|
290
|
+
due.push({ id: `${t.id}:${a.id}`, cron: a.trigger.cron, table: t.id, automation: a });
|
|
291
|
+
if (!due.length)
|
|
292
|
+
return;
|
|
293
|
+
scheduler = createScheduler({
|
|
294
|
+
schedules: due,
|
|
295
|
+
now,
|
|
296
|
+
onFire: (s) => void runScheduled(s.id),
|
|
297
|
+
});
|
|
298
|
+
scheduler.start();
|
|
299
|
+
}
|
|
300
|
+
/** A scheduled automation, now: once per record matching its condition, or once with no record. */
|
|
301
|
+
function runScheduled(key) {
|
|
302
|
+
const [tableId, automationId] = key.split(':');
|
|
303
|
+
const t = tableOf(tableId);
|
|
304
|
+
const a = t?.automations?.find((x) => x.id === automationId);
|
|
305
|
+
if (!t || !a)
|
|
306
|
+
return chain;
|
|
307
|
+
if (!a.condition)
|
|
308
|
+
chain = chain.then(() => runOne(a, t.id, null, 0));
|
|
309
|
+
else
|
|
310
|
+
for (const r of t.data)
|
|
311
|
+
if (r && matches(t, a.condition, r))
|
|
312
|
+
chain = chain.then(() => runOne(a, t.id, rowIdOf(t, r), 0));
|
|
313
|
+
return chain;
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
handle,
|
|
317
|
+
schedule,
|
|
318
|
+
runScheduled,
|
|
319
|
+
/** Run one automation now for a record (or none), as its trigger would. */
|
|
320
|
+
/** Run one automation now for a record (or none), as its trigger would; a record that does not match its condition is reported, not run. */
|
|
321
|
+
runNow: (a, tableId, recordId) => (chain = chain.then(() => {
|
|
322
|
+
const t = tableOf(tableId);
|
|
323
|
+
const rec = t && recordId != null ? recordNow(t, recordId) : null;
|
|
324
|
+
if (t && rec && a.condition && !matches(t, a.condition, rec)) {
|
|
325
|
+
record({ id: ++runSeq, automationId: a.id, name: a.name, table: t.name, record: String(rec[primaryOf(t)] ?? recordId), at: now().getTime(), status: 'stopped', steps: [{ action: 'Not run', detail: 'The record does not match the condition' }] });
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
return runOne(a, tableId, recordId, 0);
|
|
329
|
+
})),
|
|
330
|
+
get runs() {
|
|
331
|
+
return runs;
|
|
332
|
+
},
|
|
333
|
+
clearRuns: () => (runs = []),
|
|
334
|
+
settled: () => chain,
|
|
335
|
+
stop: () => scheduler?.stop(),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
// ---- Words --------------------------------------------------------------------------
|
|
339
|
+
/** "Status: Done", "Done: checked": a field of a record as a run lists it. */
|
|
340
|
+
function said(c, field, record) {
|
|
341
|
+
if (!c)
|
|
342
|
+
return `${field}: ${String(record[field] ?? '')}`;
|
|
343
|
+
if (c.fieldType === 'checkbox')
|
|
344
|
+
return `${c.header}: ${record[c.field] ? 'checked' : 'unchecked'}`;
|
|
345
|
+
return `${c.header}: ${fieldText(c, record) || 'empty'}`;
|
|
346
|
+
}
|
|
347
|
+
/** Which of a table's automations an event sets off, for the runner on one side. */
|
|
348
|
+
export function automationsFor(t, e, side) {
|
|
349
|
+
return (t.automations ?? []).filter((a) => {
|
|
350
|
+
if (!a.enabled || (a.runOn === 'server') !== (side === 'server'))
|
|
351
|
+
return false;
|
|
352
|
+
const tr = a.trigger;
|
|
353
|
+
switch (tr.type) {
|
|
354
|
+
case 'recordCreated':
|
|
355
|
+
return e.type === 'created' && matches(t, a.condition, e.record);
|
|
356
|
+
case 'recordUpdated':
|
|
357
|
+
return e.type === 'updated' && (!tr.fields?.length || tr.fields.some((f) => e.fields.includes(f))) && matches(t, a.condition, e.record);
|
|
358
|
+
case 'recordMatches':
|
|
359
|
+
if (!a.condition)
|
|
360
|
+
return false;
|
|
361
|
+
if (e.type === 'created')
|
|
362
|
+
return matches(t, a.condition, e.record);
|
|
363
|
+
if (e.type === 'updated')
|
|
364
|
+
return matches(t, a.condition, e.record) && !matches(t, a.condition, e.before);
|
|
365
|
+
return false;
|
|
366
|
+
case 'formSubmitted':
|
|
367
|
+
return e.type === 'form' && (!tr.view || tr.view === e.view) && matches(t, a.condition, e.record);
|
|
368
|
+
case 'button':
|
|
369
|
+
return e.type === 'button' && e.field === tr.field && matches(t, a.condition, e.record);
|
|
370
|
+
default:
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
/** What a webhook or an email reads of the record. */
|
|
376
|
+
export function outbound(a, t, rec, extra = {}) {
|
|
377
|
+
return {
|
|
378
|
+
automation: a.name,
|
|
379
|
+
table: t.name,
|
|
380
|
+
record: rec,
|
|
381
|
+
fill: (template) => fillTokens(t, rec, template),
|
|
382
|
+
fields: () => {
|
|
383
|
+
const out = {};
|
|
384
|
+
if (rec)
|
|
385
|
+
for (const c of tableFields(t).values())
|
|
386
|
+
out[c.header] = c.fieldType === 'checkbox' ? String(!!rec[c.field]) : fieldText(c, rec);
|
|
387
|
+
return out;
|
|
388
|
+
},
|
|
389
|
+
...extra,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
/** An action the app registered, run on a record. */
|
|
393
|
+
export async function runAppAction(a, action, t, rec) {
|
|
394
|
+
const app = appActions.get(action.action);
|
|
395
|
+
if (!app)
|
|
396
|
+
throw new Error(`No action "${action.action}" is registered`);
|
|
397
|
+
const out = await app.run({ automation: a, table: t, record: rec, text: (name) => fillTokens(t, rec, `{${name}}`), options: action.options ?? {} });
|
|
398
|
+
return { action: app.label, detail: typeof out === 'string' ? out : 'Done' };
|
|
399
|
+
}
|
|
400
|
+
export function actionLabel(a) {
|
|
401
|
+
switch (a.type) {
|
|
402
|
+
case 'updateRecord':
|
|
403
|
+
return 'Update record';
|
|
404
|
+
case 'createRecord':
|
|
405
|
+
return 'Create record';
|
|
406
|
+
case 'notify':
|
|
407
|
+
return 'Show a notice';
|
|
408
|
+
case 'highlight':
|
|
409
|
+
return 'Color the record';
|
|
410
|
+
case 'run':
|
|
411
|
+
return appActions.get(a.action)?.label ?? a.action;
|
|
412
|
+
case 'webhook':
|
|
413
|
+
return 'Call a webhook';
|
|
414
|
+
case 'email':
|
|
415
|
+
return 'Send an email';
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
/** "When a record matches: Status is Blocked", for the list of automations. */
|
|
419
|
+
export function triggerText(t, a) {
|
|
420
|
+
const name = (id) => (t ? (tableFields(t).get(id)?.header ?? id) : id);
|
|
421
|
+
const cond = a.condition ? describeFieldFilter(a.condition, name) : '';
|
|
422
|
+
const tr = a.trigger;
|
|
423
|
+
switch (tr.type) {
|
|
424
|
+
case 'recordCreated':
|
|
425
|
+
return `When a record is created${cond && cond !== 'every record' ? `, where ${cond}` : ''}`;
|
|
426
|
+
case 'recordUpdated':
|
|
427
|
+
return `When ${tr.fields?.length ? tr.fields.map(name).join(' or ') : 'a record'} changes${cond && cond !== 'every record' ? `, where ${cond}` : ''}`;
|
|
428
|
+
case 'recordMatches':
|
|
429
|
+
return `When a record matches: ${cond || 'no condition yet'}`;
|
|
430
|
+
case 'formSubmitted':
|
|
431
|
+
return `When a form is sent${cond && cond !== 'every record' ? `, where ${cond}` : ''}`;
|
|
432
|
+
case 'button':
|
|
433
|
+
return `When ${name(tr.field)} is pressed`;
|
|
434
|
+
case 'scheduled':
|
|
435
|
+
return `On a schedule (${tr.cron})${cond && cond !== 'every record' ? `, for records where ${cond}` : ''}`;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which view bars are mounted, and a request for one of them to open a
|
|
3
|
+
* panel. The column menu's "Filter by this field" adds a condition and asks
|
|
4
|
+
* the grid's bar to show its Filter panel; without a bar the item is not
|
|
5
|
+
* offered.
|
|
6
|
+
*/
|
|
7
|
+
export type BarPanel = 'hide' | 'filter' | 'group' | 'sort' | 'color' | 'chart' | 'gantt';
|
|
8
|
+
/** A bar mounted over this grid; the returned function unregisters it. */
|
|
9
|
+
export declare function registerBar(ctrl: object): () => void;
|
|
10
|
+
export declare const hasBar: (ctrl: unknown) => boolean;
|
|
11
|
+
export declare const getBarRequest: () => {
|
|
12
|
+
ctrl: unknown;
|
|
13
|
+
panel: BarPanel;
|
|
14
|
+
} | null;
|
|
15
|
+
export declare function openBarPanel(ctrl: unknown, panel: BarPanel): void;
|
|
16
|
+
export declare function clearBarRequest(): void;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const bars = new WeakSet();
|
|
2
|
+
let request = $state.raw(null);
|
|
3
|
+
/** A bar mounted over this grid; the returned function unregisters it. */
|
|
4
|
+
export function registerBar(ctrl) {
|
|
5
|
+
bars.add(ctrl);
|
|
6
|
+
return () => bars.delete(ctrl);
|
|
7
|
+
}
|
|
8
|
+
export const hasBar = (ctrl) => !!ctrl && typeof ctrl === 'object' && bars.has(ctrl);
|
|
9
|
+
export const getBarRequest = () => request;
|
|
10
|
+
export function openBarPanel(ctrl, panel) {
|
|
11
|
+
request = { ctrl, panel };
|
|
12
|
+
}
|
|
13
|
+
export function clearBarRequest() {
|
|
14
|
+
request = null;
|
|
15
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { PredicateExpr } from '../expressions/expression-types.js';
|
|
2
|
+
import { type FieldFilter, type LinkNames } from './filter-model.js';
|
|
3
|
+
import type { FieldColumn } from './grid-access.js';
|
|
4
|
+
/** A link value's records by name, for a condition on a link field. */
|
|
5
|
+
export declare const linkNames: LinkNames;
|
|
6
|
+
/**
|
|
7
|
+
* A rule condition written as a base filter.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* predicate: baseCondition({ conjunction: 'and', conditions: [
|
|
11
|
+
* { column: 'due', kind: 'date', op: 'isBefore', value: { mode: 'today' } },
|
|
12
|
+
* { column: 'status', kind: 'select', op: 'isNot', value: 'Done' },
|
|
13
|
+
* ] })
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare function baseCondition(filter: Omit<FieldFilter, 'kind'> | FieldFilter): PredicateExpr;
|
|
17
|
+
/** The base filter inside a predicate, or null when the predicate is something else. */
|
|
18
|
+
export declare function baseFilterOf(expr: PredicateExpr | null | undefined): FieldFilter | null;
|
|
19
|
+
/** Teach the expression language the base filter (once; `enableFields` calls it). */
|
|
20
|
+
export declare function registerBaseCondition(): void;
|
|
21
|
+
/** A field's value in a record: computed for a formula, lookup, rollup or count. */
|
|
22
|
+
export declare function fieldValue(column: FieldColumn, row: Record<string, unknown>): unknown;
|
|
23
|
+
/** A field's value as its cell shows it, as text. */
|
|
24
|
+
export declare function fieldText(column: FieldColumn, row: Record<string, unknown>): string;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A base condition in the expression language: the view bar's filter, kept as
|
|
3
|
+
* `{ kind: 'ext', type: 'fieldFilter', payload }`, so an alert rule or an
|
|
4
|
+
* advanced filter reads a select's option, a person, a linked record's name
|
|
5
|
+
* or "within the past week" the way the view bar does. Also the readers that
|
|
6
|
+
* give a field's value and its text outside the grid.
|
|
7
|
+
*/
|
|
8
|
+
import { registerPredicateKind } from '../expressions/kinds.js';
|
|
9
|
+
import { compileFieldFilter, describeFieldFilter, fieldFilterColumns } from './filter-model.js';
|
|
10
|
+
import { getLinkedTable, linkedRows } from './sources.svelte.js';
|
|
11
|
+
/** A link value's records by name, for a condition on a link field. */
|
|
12
|
+
export const linkNames = (table, value) => {
|
|
13
|
+
const t = getLinkedTable(table);
|
|
14
|
+
return t ? linkedRows(t, value).map((r) => String(r[t.primaryField] ?? '')) : [];
|
|
15
|
+
};
|
|
16
|
+
const EMPTY = { kind: 'fieldFilter', conjunction: 'and', conditions: [] };
|
|
17
|
+
const asFilter = (payload) => {
|
|
18
|
+
const f = payload;
|
|
19
|
+
return f && Array.isArray(f.conditions) ? { ...EMPTY, ...f, kind: 'fieldFilter' } : EMPTY;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* A rule condition written as a base filter.
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* predicate: baseCondition({ conjunction: 'and', conditions: [
|
|
26
|
+
* { column: 'due', kind: 'date', op: 'isBefore', value: { mode: 'today' } },
|
|
27
|
+
* { column: 'status', kind: 'select', op: 'isNot', value: 'Done' },
|
|
28
|
+
* ] })
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function baseCondition(filter) {
|
|
32
|
+
return { kind: 'ext', type: 'fieldFilter', payload: { ...filter, kind: 'fieldFilter' } };
|
|
33
|
+
}
|
|
34
|
+
/** The base filter inside a predicate, or null when the predicate is something else. */
|
|
35
|
+
export function baseFilterOf(expr) {
|
|
36
|
+
return expr?.kind === 'ext' && expr.type === 'fieldFilter' ? asFilter(expr.payload) : null;
|
|
37
|
+
}
|
|
38
|
+
let registered = false;
|
|
39
|
+
/** Teach the expression language the base filter (once; `enableFields` calls it). */
|
|
40
|
+
export function registerBaseCondition() {
|
|
41
|
+
if (registered)
|
|
42
|
+
return;
|
|
43
|
+
registered = true;
|
|
44
|
+
registerPredicateKind('fieldFilter', {
|
|
45
|
+
compile: (payload, ctx) => compileFieldFilter(asFilter(payload), ctx.getValue, linkNames),
|
|
46
|
+
describe: (payload, name) => describeFieldFilter(asFilter(payload), name),
|
|
47
|
+
columns: (payload) => fieldFilterColumns(asFilter(payload)),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/** A field's value in a record: computed for a formula, lookup, rollup or count. */
|
|
51
|
+
export function fieldValue(column, row) {
|
|
52
|
+
const fn = column.def.fieldFn;
|
|
53
|
+
return fn ? fn(row) : row?.[column.field];
|
|
54
|
+
}
|
|
55
|
+
/** A field's value as its cell shows it, as text. */
|
|
56
|
+
export function fieldText(column, row) {
|
|
57
|
+
const value = fieldValue(column, row);
|
|
58
|
+
const fmt = column.def.formatter;
|
|
59
|
+
if (fmt)
|
|
60
|
+
return String(fmt({ value, row, column: { id: column.id, columnDef: column.def } }) ?? '');
|
|
61
|
+
if (Array.isArray(value))
|
|
62
|
+
return value.join(', ');
|
|
63
|
+
return value == null ? '' : String(value);
|
|
64
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type DbExecute, type SqlDialectName } from '../studio/introspect-db.js';
|
|
2
|
+
import { type KitHandlerOptions } from '../sveltekit/transport.js';
|
|
3
|
+
import { type SupabaseClientLike } from '../sources/supabase.js';
|
|
4
|
+
import { type SupabaseRealtimeClientLike } from '../sources/realtime-supabase.js';
|
|
5
|
+
import { type BaseTableSpec } from './base-schema.js';
|
|
6
|
+
import type { BaseTable } from './base-tables.js';
|
|
7
|
+
export type DatabaseOptions = {
|
|
8
|
+
/** Runs a query: `(sql, params) => rows`. With node-postgres: `async (sql, params) => (await pool.query(sql, params)).rows`. */
|
|
9
|
+
execute: DbExecute;
|
|
10
|
+
/** Default `'postgres'`. */
|
|
11
|
+
dialect?: SqlDialectName;
|
|
12
|
+
/** The tables to show, in tab order. Default: every table. */
|
|
13
|
+
tables?: string[];
|
|
14
|
+
/** Make text columns that repeat a few values single selects (default true). */
|
|
15
|
+
choices?: boolean;
|
|
16
|
+
};
|
|
17
|
+
/** The tables of a database, described as a base: plain JSON (what an endpoint sends). */
|
|
18
|
+
export declare function describeDatabase(options: DatabaseOptions): Promise<BaseTableSpec[]>;
|
|
19
|
+
/**
|
|
20
|
+
* A base on a SQL database, from its tables as they are.
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* const tables = await baseFromDatabase({ execute: (sql, params) => db.query(sql, params).then((r) => r.rows) })
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function baseFromDatabase(options: DatabaseOptions): Promise<BaseTable[]>;
|
|
27
|
+
export type BaseEndpointOptions = DatabaseOptions & {
|
|
28
|
+
/** Who may read and write a table; see `createKitHandlers`' `authorize`. */
|
|
29
|
+
authorize?: (table: string) => KitHandlerOptions<Record<string, unknown>>['authorize'];
|
|
30
|
+
/** Each table's audit hook: `(table) => automations.audit(table)` runs server automations. */
|
|
31
|
+
audit?: (table: string) => KitHandlerOptions<Record<string, unknown>>['audit'];
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* The server half of `baseFromEndpoint`, as a SvelteKit route:
|
|
35
|
+
*
|
|
36
|
+
* ```ts
|
|
37
|
+
* // src/routes/api/base/+server.ts
|
|
38
|
+
* export const { GET, POST } = createBaseEndpoint({ execute: (sql, params) => pool.query(sql, params).then((r) => r.rows) })
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* `GET` answers with the tables described as a base; `POST ?table=tasks`
|
|
42
|
+
* reads and writes that table. Only the tables it described are reachable.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createBaseEndpoint(options: BaseEndpointOptions): {
|
|
45
|
+
GET(): Promise<Response>;
|
|
46
|
+
POST(event: {
|
|
47
|
+
request: Request;
|
|
48
|
+
locals?: Record<string, unknown>;
|
|
49
|
+
}): Promise<Response>;
|
|
50
|
+
/** Read the database's tables again (after a migration). */
|
|
51
|
+
refresh(): void;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* A base on your server's `createBaseEndpoint`.
|
|
55
|
+
*
|
|
56
|
+
* ```svelte
|
|
57
|
+
* {#await baseFromEndpoint('/api/base') then tables}<SvBase {tables} editable />{/await}
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export declare function baseFromEndpoint(url: string, options?: {
|
|
61
|
+
fetch?: typeof fetch;
|
|
62
|
+
}): Promise<BaseTable[]>;
|
|
63
|
+
export type SupabaseBaseOptions = {
|
|
64
|
+
client: SupabaseClientLike & SupabaseRealtimeClientLike;
|
|
65
|
+
/** The project URL and anon key, for reading the tables' columns. */
|
|
66
|
+
url: string;
|
|
67
|
+
key: string;
|
|
68
|
+
/** The tables to show, in tab order. Default: every table the key can read. */
|
|
69
|
+
tables?: string[];
|
|
70
|
+
/** Hear other people's changes through Supabase Realtime (default true). */
|
|
71
|
+
realtime?: boolean;
|
|
72
|
+
choices?: boolean;
|
|
73
|
+
fetch?: typeof fetch;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* A base on a Supabase project.
|
|
77
|
+
*
|
|
78
|
+
* ```ts
|
|
79
|
+
* const tables = await baseFromSupabase({ client: supabase, url: PUBLIC_SUPABASE_URL, key: PUBLIC_SUPABASE_ANON_KEY })
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export declare function baseFromSupabase(options: SupabaseBaseOptions): Promise<BaseTable[]>;
|