@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.
Files changed (270) hide show
  1. package/README.md +1 -0
  2. package/dist/SvAlertRuleEditor.svelte +16 -5
  3. package/dist/SvGridAlerts.svelte +8 -0
  4. package/dist/SvGridAlerts.svelte.d.ts +4 -0
  5. package/dist/SvGridBoard.svelte +69 -10
  6. package/dist/SvGridBulkEditDrawer.svelte +13 -5
  7. package/dist/SvGridGallery.svelte +286 -0
  8. package/dist/SvGridGallery.svelte.d.ts +30 -0
  9. package/dist/SvGridScheduler.svelte +33 -11
  10. package/dist/SvGridSelectionBar.svelte +47 -0
  11. package/dist/advanced-filter/expr-columns-from-grid.d.ts +1 -0
  12. package/dist/advanced-filter/expr-columns-from-grid.js +11 -7
  13. package/dist/advanced-filter-enable.d.ts +6 -0
  14. package/dist/advanced-filter-enable.js +8 -0
  15. package/dist/alerts/alert-engine.d.ts +7 -0
  16. package/dist/alerts/alert-engine.js +5 -2
  17. package/dist/alerts/condition-editor.d.ts +26 -0
  18. package/dist/alerts/condition-editor.js +17 -0
  19. package/dist/base/index.d.ts +20 -0
  20. package/dist/base/index.js +20 -0
  21. package/dist/bulk-edit.d.ts +11 -0
  22. package/dist/bulk-edit.js +18 -1
  23. package/dist/cdn/svgrid-enterprise.svelte-external.js +11375 -10805
  24. package/dist/designer/assets/GridFooter-Cu8_yBlQ.js +2 -0
  25. package/dist/designer/assets/GridMenus-BUjkkAU2.js +6 -0
  26. package/dist/designer/assets/PinSplitter-BgTpgh1d.css +1 -0
  27. package/dist/designer/assets/PinSplitter-C5AwA1xV.js +1 -0
  28. package/dist/designer/assets/SvGrid.helpers-CIrmIo9U.js +1 -0
  29. package/dist/designer/assets/SvGridCellEditor-CJxThKL_.js +4 -0
  30. package/dist/designer/assets/SvListBox-CS7oUAiX.js +2 -0
  31. package/dist/designer/assets/conditional-formatting-MDWbF8_Y.js +1 -0
  32. package/dist/designer/assets/grid-extension.svelte-BZ7AqyJI.js +1 -0
  33. package/dist/designer/assets/{index-DtBIaQno.js → index-BPU2KNc5.js} +396 -395
  34. package/dist/designer/assets/index-BmvG99kR.css +1 -0
  35. package/dist/designer/assets/sparkline-C-NcnB2i.js +1 -0
  36. package/dist/designer/assets/src-C_U1NKOY.js +58 -0
  37. package/dist/designer/assets/sv-grid-scrollbar-B9N6Wuj0.js +109 -0
  38. package/dist/designer/assets/validate-H5u2HEQh.js +1 -0
  39. package/dist/designer/index.html +11 -7
  40. package/dist/expressions/compile.js +10 -0
  41. package/dist/expressions/evaluate.js +18 -0
  42. package/dist/expressions/expression-types.d.ts +10 -0
  43. package/dist/expressions/kinds.d.ts +28 -0
  44. package/dist/expressions/kinds.js +19 -0
  45. package/dist/expressions/parse.js +9 -1
  46. package/dist/fields/AttachmentCell.svelte +242 -0
  47. package/dist/fields/AttachmentCell.svelte.d.ts +5 -0
  48. package/dist/fields/AttachmentDialog.svelte +357 -0
  49. package/dist/fields/AttachmentDialog.svelte.d.ts +13 -0
  50. package/dist/fields/AttachmentEditor.svelte +41 -0
  51. package/dist/fields/AttachmentEditor.svelte.d.ts +11 -0
  52. package/dist/fields/AttachmentPreview.svelte +72 -0
  53. package/dist/fields/AttachmentPreview.svelte.d.ts +8 -0
  54. package/dist/fields/BarcodeCell.svelte +20 -0
  55. package/dist/fields/BarcodeCell.svelte.d.ts +5 -0
  56. package/dist/fields/BaseAutomations.svelte +558 -0
  57. package/dist/fields/BaseAutomations.svelte.d.ts +18 -0
  58. package/dist/fields/BaseChartView.svelte +47 -0
  59. package/dist/fields/BaseChartView.svelte.d.ts +8 -0
  60. package/dist/fields/BaseTableView.svelte +185 -0
  61. package/dist/fields/BaseTableView.svelte.d.ts +37 -0
  62. package/dist/fields/BaseTabs.svelte +308 -0
  63. package/dist/fields/BaseTabs.svelte.d.ts +21 -0
  64. package/dist/fields/ButtonCell.svelte +47 -0
  65. package/dist/fields/ButtonCell.svelte.d.ts +20 -0
  66. package/dist/fields/CheckboxCell.svelte +46 -0
  67. package/dist/fields/CheckboxCell.svelte.d.ts +14 -0
  68. package/dist/fields/DeleteFieldConfirm.svelte +80 -0
  69. package/dist/fields/DeleteFieldConfirm.svelte.d.ts +8 -0
  70. package/dist/fields/DeleteRecordConfirm.svelte +48 -0
  71. package/dist/fields/DeleteRecordConfirm.svelte.d.ts +9 -0
  72. package/dist/fields/FieldCell.svelte +80 -0
  73. package/dist/fields/FieldCell.svelte.d.ts +9 -0
  74. package/dist/fields/FieldEditor.svelte +321 -0
  75. package/dist/fields/FieldEditor.svelte.d.ts +10 -0
  76. package/dist/fields/FieldInput.svelte +188 -0
  77. package/dist/fields/FieldInput.svelte.d.ts +29 -0
  78. package/dist/fields/FieldsHeaderEnd.svelte +146 -0
  79. package/dist/fields/FieldsHeaderEnd.svelte.d.ts +6 -0
  80. package/dist/fields/FootRowAdder.svelte +8 -0
  81. package/dist/fields/FootRowAdder.svelte.d.ts +6 -0
  82. package/dist/fields/GroupHeader.svelte +127 -0
  83. package/dist/fields/GroupHeader.svelte.d.ts +24 -0
  84. package/dist/fields/LinkCell.svelte +46 -0
  85. package/dist/fields/LinkCell.svelte.d.ts +9 -0
  86. package/dist/fields/LinkPickerEditor.svelte +172 -0
  87. package/dist/fields/LinkPickerEditor.svelte.d.ts +9 -0
  88. package/dist/fields/LinkPreview.svelte +60 -0
  89. package/dist/fields/LinkPreview.svelte.d.ts +7 -0
  90. package/dist/fields/LinkedRecordView.svelte +94 -0
  91. package/dist/fields/LinkedRecordView.svelte.d.ts +9 -0
  92. package/dist/fields/LinkedRecordsCell.svelte +59 -0
  93. package/dist/fields/LinkedRecordsCell.svelte.d.ts +11 -0
  94. package/dist/fields/LongTextEditor.svelte +157 -0
  95. package/dist/fields/LongTextEditor.svelte.d.ts +11 -0
  96. package/dist/fields/NumberEditor.svelte +309 -0
  97. package/dist/fields/NumberEditor.svelte.d.ts +12 -0
  98. package/dist/fields/OptionsEditor.svelte +117 -0
  99. package/dist/fields/OptionsEditor.svelte.d.ts +11 -0
  100. package/dist/fields/PercentCell.svelte +21 -0
  101. package/dist/fields/PercentCell.svelte.d.ts +8 -0
  102. package/dist/fields/PropertyInput.svelte +204 -0
  103. package/dist/fields/PropertyInput.svelte.d.ts +11 -0
  104. package/dist/fields/RatingCell.svelte +75 -0
  105. package/dist/fields/RatingCell.svelte.d.ts +16 -0
  106. package/dist/fields/RatingEditor.svelte +82 -0
  107. package/dist/fields/RatingEditor.svelte.d.ts +9 -0
  108. package/dist/fields/RecordView.svelte +310 -0
  109. package/dist/fields/RecordView.svelte.d.ts +11 -0
  110. package/dist/fields/RichTextCell.svelte +30 -0
  111. package/dist/fields/RichTextCell.svelte.d.ts +5 -0
  112. package/dist/fields/RowAdder.svelte +93 -0
  113. package/dist/fields/RowAdder.svelte.d.ts +7 -0
  114. package/dist/fields/RowHeader.svelte +99 -0
  115. package/dist/fields/RowHeader.svelte.d.ts +11 -0
  116. package/dist/fields/SelectCell.svelte +35 -0
  117. package/dist/fields/SelectCell.svelte.d.ts +15 -0
  118. package/dist/fields/SelectEditor.svelte +350 -0
  119. package/dist/fields/SelectEditor.svelte.d.ts +17 -0
  120. package/dist/fields/SummaryCell.svelte +160 -0
  121. package/dist/fields/SummaryCell.svelte.d.ts +11 -0
  122. package/dist/fields/SvBase.svelte +545 -0
  123. package/dist/fields/SvBase.svelte.d.ts +30 -0
  124. package/dist/fields/SvBaseForm.svelte +295 -0
  125. package/dist/fields/SvBaseForm.svelte.d.ts +22 -0
  126. package/dist/fields/SvBaseViewBar.svelte +644 -0
  127. package/dist/fields/SvBaseViewBar.svelte.d.ts +26 -0
  128. package/dist/fields/UserCell.svelte +39 -0
  129. package/dist/fields/UserCell.svelte.d.ts +9 -0
  130. package/dist/fields/UserFace.svelte +30 -0
  131. package/dist/fields/UserFace.svelte.d.ts +12 -0
  132. package/dist/fields/activity.svelte.d.ts +50 -0
  133. package/dist/fields/activity.svelte.js +110 -0
  134. package/dist/fields/attachments.d.ts +95 -0
  135. package/dist/fields/attachments.js +145 -0
  136. package/dist/fields/automation-outbound.d.ts +46 -0
  137. package/dist/fields/automation-outbound.js +61 -0
  138. package/dist/fields/automation-server.d.ts +53 -0
  139. package/dist/fields/automation-server.js +333 -0
  140. package/dist/fields/automations.svelte.d.ts +208 -0
  141. package/dist/fields/automations.svelte.js +437 -0
  142. package/dist/fields/bar-request.svelte.d.ts +16 -0
  143. package/dist/fields/bar-request.svelte.js +15 -0
  144. package/dist/fields/base-alerts.d.ts +24 -0
  145. package/dist/fields/base-alerts.js +64 -0
  146. package/dist/fields/base-connect.d.ts +82 -0
  147. package/dist/fields/base-connect.js +271 -0
  148. package/dist/fields/base-schema.d.ts +104 -0
  149. package/dist/fields/base-schema.js +266 -0
  150. package/dist/fields/base-sql.d.ts +62 -0
  151. package/dist/fields/base-sql.js +251 -0
  152. package/dist/fields/base-sync.d.ts +142 -0
  153. package/dist/fields/base-sync.js +499 -0
  154. package/dist/fields/base-tables.d.ts +89 -0
  155. package/dist/fields/base-tables.js +349 -0
  156. package/dist/fields/builtin-types.d.ts +13 -0
  157. package/dist/fields/builtin-types.js +773 -0
  158. package/dist/fields/button.d.ts +2 -0
  159. package/dist/fields/button.js +17 -0
  160. package/dist/fields/chart-view.d.ts +35 -0
  161. package/dist/fields/chart-view.js +154 -0
  162. package/dist/fields/column-order.d.ts +6 -0
  163. package/dist/fields/column-order.js +12 -0
  164. package/dist/fields/convert.d.ts +16 -0
  165. package/dist/fields/convert.js +176 -0
  166. package/dist/fields/copy-text.d.ts +13 -0
  167. package/dist/fields/copy-text.js +45 -0
  168. package/dist/fields/editor-request.svelte.d.ts +71 -0
  169. package/dist/fields/editor-request.svelte.js +52 -0
  170. package/dist/fields/field-ops.d.ts +72 -0
  171. package/dist/fields/field-ops.js +334 -0
  172. package/dist/fields/filter-model.d.ts +84 -0
  173. package/dist/fields/filter-model.js +446 -0
  174. package/dist/fields/format.d.ts +109 -0
  175. package/dist/fields/format.js +288 -0
  176. package/dist/fields/formula.d.ts +50 -0
  177. package/dist/fields/formula.js +530 -0
  178. package/dist/fields/grid-access.d.ts +60 -0
  179. package/dist/fields/grid-access.js +120 -0
  180. package/dist/fields/index.d.ts +34 -0
  181. package/dist/fields/index.js +312 -0
  182. package/dist/fields/linked-fields.d.ts +8 -0
  183. package/dist/fields/linked-fields.js +40 -0
  184. package/dist/fields/links.d.ts +10 -0
  185. package/dist/fields/links.js +46 -0
  186. package/dist/fields/name-list.d.ts +16 -0
  187. package/dist/fields/name-list.js +49 -0
  188. package/dist/fields/palette.d.ts +47 -0
  189. package/dist/fields/palette.js +69 -0
  190. package/dist/fields/record-delete.svelte.d.ts +12 -0
  191. package/dist/fields/record-delete.svelte.js +5 -0
  192. package/dist/fields/record-menu.d.ts +9 -0
  193. package/dist/fields/record-menu.js +78 -0
  194. package/dist/fields/registry.svelte.d.ts +176 -0
  195. package/dist/fields/registry.svelte.js +51 -0
  196. package/dist/fields/rich-text.d.ts +23 -0
  197. package/dist/fields/rich-text.js +48 -0
  198. package/dist/fields/row-color-choice.svelte.d.ts +31 -0
  199. package/dist/fields/row-color-choice.svelte.js +25 -0
  200. package/dist/fields/row-colors.d.ts +36 -0
  201. package/dist/fields/row-colors.js +90 -0
  202. package/dist/fields/row-height.d.ts +28 -0
  203. package/dist/fields/row-height.js +50 -0
  204. package/dist/fields/sources.svelte.d.ts +80 -0
  205. package/dist/fields/sources.svelte.js +131 -0
  206. package/dist/fields/summaries.d.ts +18 -0
  207. package/dist/fields/summaries.js +138 -0
  208. package/dist/fields/view-model.d.ts +129 -0
  209. package/dist/fields/view-model.js +492 -0
  210. package/dist/fields/viewbar/ChartPanel.svelte +61 -0
  211. package/dist/fields/viewbar/ChartPanel.svelte.d.ts +9 -0
  212. package/dist/fields/viewbar/ColorPanel.svelte +112 -0
  213. package/dist/fields/viewbar/ColorPanel.svelte.d.ts +9 -0
  214. package/dist/fields/viewbar/ConditionValue.svelte +153 -0
  215. package/dist/fields/viewbar/ConditionValue.svelte.d.ts +15 -0
  216. package/dist/fields/viewbar/FieldIcon.svelte +18 -0
  217. package/dist/fields/viewbar/FieldIcon.svelte.d.ts +9 -0
  218. package/dist/fields/viewbar/FilterPanel.svelte +192 -0
  219. package/dist/fields/viewbar/FilterPanel.svelte.d.ts +13 -0
  220. package/dist/fields/viewbar/GanttPanel.svelte +60 -0
  221. package/dist/fields/viewbar/GanttPanel.svelte.d.ts +10 -0
  222. package/dist/fields/viewbar/HideFieldsPanel.svelte +104 -0
  223. package/dist/fields/viewbar/HideFieldsPanel.svelte.d.ts +7 -0
  224. package/dist/fields/viewbar/SortPanel.svelte +85 -0
  225. package/dist/fields/viewbar/SortPanel.svelte.d.ts +8 -0
  226. package/dist/fields/viewbar/ViewsMenu.svelte +250 -0
  227. package/dist/fields/viewbar/ViewsMenu.svelte.d.ts +15 -0
  228. package/dist/fields/viewbar/panel-focus.d.ts +3 -0
  229. package/dist/fields/viewbar/panel-focus.js +22 -0
  230. package/dist/fields/viewbar/sort-state.d.ts +20 -0
  231. package/dist/fields/viewbar/sort-state.js +24 -0
  232. package/dist/gallery.d.ts +7 -0
  233. package/dist/gallery.js +36 -0
  234. package/dist/gantt/SvGridGantt.svelte +88 -16
  235. package/dist/gantt/gantt-config.d.ts +28 -0
  236. package/dist/index.d.ts +3 -0
  237. package/dist/index.js +6 -0
  238. package/dist/install.js +3 -0
  239. package/dist/license-core.d.ts +3 -3
  240. package/dist/license.js +1 -0
  241. package/dist/node/studio.js +141 -20
  242. package/dist/scheduler-config.d.ts +11 -0
  243. package/dist/sources/introspect-supabase.js +4 -1
  244. package/dist/sources/rest.js +6 -2
  245. package/dist/studio/emit-project.js +25 -6
  246. package/dist/studio/sveltekit3.js +13 -7
  247. package/dist/studio/ui-components.generated.js +94 -2
  248. package/dist/upgrade-prompt.d.ts +1 -1
  249. package/dist/version.d.ts +2 -2
  250. package/dist/version.js +2 -2
  251. package/dist/wc/GridFooter-f5K0aLe_.js +145 -0
  252. package/dist/wc/GridMenus-FtSN_8t9.js +1369 -0
  253. package/dist/wc/PinSplitter-D-qtDhSM.js +132 -0
  254. package/dist/wc/{SvGrid.helpers-zBkDTryR.js → SvGrid.helpers-B2GEKrxS.js} +84 -36
  255. package/dist/wc/{SvGridCellEditor-CzUKEbkI.js → SvGridCellEditor-B_cLJrsp.js} +20 -9
  256. package/dist/wc/conditional-formatting-BXis_oP1.js +189 -0
  257. package/dist/wc/grid-extension.svelte-Bufq4B4C.js +20 -0
  258. package/dist/wc/sv-sheet-element.js +7561 -7618
  259. package/dist/wc/svelte-virtualizer.svelte-ehwOupl-.js +360 -0
  260. package/dist/wc/{validate-43C03DLP.js → validate-BaMO8UPE.js} +1 -1
  261. package/package.json +14 -3
  262. package/dist/designer/assets/GridMenus-CvcASlKv.js +0 -8
  263. package/dist/designer/assets/SvGrid.helpers-DE5mmORW.js +0 -1
  264. package/dist/designer/assets/SvGridCellEditor-C7NWcnNC.js +0 -4
  265. package/dist/designer/assets/SvListBox-BlmmzYcU.js +0 -110
  266. package/dist/designer/assets/index-KmSEY7Lb.css +0 -1
  267. package/dist/designer/assets/src-DqHCqori.js +0 -58
  268. package/dist/designer/assets/validate-B71dE1Cs.js +0 -1
  269. package/dist/wc/GridMenus-fWbBUiRH.js +0 -1088
  270. package/dist/wc/svelte-virtualizer.svelte-Dntdh8Ms.js +0 -581
@@ -0,0 +1,558 @@
1
+ <script lang="ts">
2
+ /**
3
+ * A table's automations, in a dialog: the list on the left (each can be
4
+ * turned off), the one picked on the right as "When", "Only for records
5
+ * where" and "Do", and the history of runs. Every change is kept at once
6
+ * (`onChange`), as the view bar keeps a view's settings.
7
+ */
8
+ import { SvModal, SvSwitchButton } from '@svgrid/grid'
9
+ import FilterPanel from './viewbar/FilterPanel.svelte'
10
+ import { CRON_PRESETS } from '../scheduling'
11
+ import { primaryOf, rowIdOf, type BaseTable } from './base-tables'
12
+ import { getFieldUsers } from './sources.svelte'
13
+ import {
14
+ actionLabel,
15
+ listAutomationActions,
16
+ tableFields,
17
+ triggerText,
18
+ type AutomationAction,
19
+ type AutomationRun,
20
+ type AutomationRunner,
21
+ type AutomationTrigger,
22
+ type BaseAutomation,
23
+ } from './automations.svelte'
24
+ import type { FieldFilter } from './filter-model'
25
+ import type { FieldView } from './view-model'
26
+
27
+ let {
28
+ open = $bindable(false),
29
+ table,
30
+ tables,
31
+ ctrl,
32
+ views = [],
33
+ runner,
34
+ serverRuns,
35
+ onChange,
36
+ }: {
37
+ open?: boolean
38
+ table: BaseTable
39
+ tables: BaseTable[]
40
+ /** The grid of the table, for the condition's fields. */
41
+ ctrl: any
42
+ views?: FieldView[]
43
+ runner: AutomationRunner
44
+ /** The server runner's latest runs (an endpoint returning `server.runs`). */
45
+ serverRuns?: () => Promise<ReadonlyArray<AutomationRun>>
46
+ onChange: (automations: BaseAutomation[]) => void
47
+ } = $props()
48
+
49
+ // svelte-ignore state_referenced_locally
50
+ let list = $state<BaseAutomation[]>($state.snapshot(table.automations ?? []) as BaseAutomation[])
51
+ // svelte-ignore state_referenced_locally
52
+ let selectedId = $state<string | null>(list[0]?.id ?? null)
53
+ let tab = $state<'edit' | 'runs'>('edit')
54
+ const selected = $derived(list.find((a) => a.id === selectedId) ?? null)
55
+
56
+ const fields = $derived([...tableFields(table).values()])
57
+ const fieldsOf = (t: BaseTable | undefined) => (t ? [...tableFields(t).values()] : [])
58
+ const COMPUTED = new Set(['formula', 'lookup', 'rollup', 'count', 'button', 'autoNumber', 'createdTime', 'lastModifiedTime', 'createdBy', 'lastModifiedBy'])
59
+ const writable = (t: BaseTable | undefined) => fieldsOf(t).filter((c) => !COMPUTED.has(String(c.fieldType ?? '')))
60
+ const forms = $derived(views.filter((v) => v.type === 'form'))
61
+ const buttons = $derived(fields.filter((c) => c.fieldType === 'button'))
62
+ const appActions = listAutomationActions()
63
+
64
+ function save(next: BaseAutomation[]) {
65
+ list = next
66
+ onChange($state.snapshot(next) as BaseAutomation[])
67
+ }
68
+ function patch(change: Partial<BaseAutomation>) {
69
+ if (!selected) return
70
+ save(list.map((a) => (a.id === selected.id ? { ...a, ...change } : a)))
71
+ }
72
+ function add() {
73
+ const primary = fields.find((c) => c.id === primaryOf(table)) ?? fields[0]
74
+ const a: BaseAutomation = {
75
+ id: `auto-${Date.now().toString(36)}`,
76
+ name: 'New automation',
77
+ enabled: true,
78
+ trigger: { type: 'recordCreated' },
79
+ actions: [{ type: 'notify', message: `New record: {${primary?.header ?? 'Name'}}` }],
80
+ }
81
+ save([...list, a])
82
+ selectedId = a.id
83
+ tab = 'edit'
84
+ }
85
+ let deleting = $state<string | null>(null)
86
+ function remove(id: string) {
87
+ const next = list.filter((a) => a.id !== id)
88
+ save(next)
89
+ selectedId = next[0]?.id ?? null
90
+ }
91
+
92
+ // ---- When ----
93
+ const TRIGGERS: Array<{ value: AutomationTrigger['type']; label: string }> = [
94
+ { value: 'recordCreated', label: 'A record is created' },
95
+ { value: 'recordUpdated', label: 'A record changes' },
96
+ { value: 'recordMatches', label: 'A record matches a condition' },
97
+ { value: 'formSubmitted', label: 'A form is sent' },
98
+ { value: 'button', label: 'A button is pressed' },
99
+ { value: 'scheduled', label: 'At a scheduled time' },
100
+ ]
101
+ function setTrigger(type: AutomationTrigger['type']) {
102
+ const trigger: AutomationTrigger =
103
+ type === 'recordUpdated' ? { type, fields: [] }
104
+ : type === 'formSubmitted' ? { type }
105
+ : type === 'button' ? { type, field: buttons[0]?.id ?? '' }
106
+ : type === 'scheduled' ? { type, cron: CRON_PRESETS[3]!.cron }
107
+ : { type }
108
+ patch({ trigger })
109
+ }
110
+ function toggleWatched(id: string) {
111
+ if (selected?.trigger.type !== 'recordUpdated') return
112
+ const now = new Set(selected.trigger.fields ?? [])
113
+ if (now.has(id)) now.delete(id)
114
+ else now.add(id)
115
+ patch({ trigger: { type: 'recordUpdated', fields: [...now] } })
116
+ }
117
+
118
+ // ---- Only for records where ----
119
+ const EMPTY: FieldFilter = { kind: 'fieldFilter', conjunction: 'and', conditions: [] }
120
+ function setCondition(f: FieldFilter) {
121
+ patch({ condition: f.conditions.length ? f : undefined })
122
+ }
123
+
124
+ // ---- Do ----
125
+ const ACTIONS: Array<{ value: AutomationAction['type'] | `run:${string}`; label: string }> = [
126
+ { value: 'updateRecord', label: 'Update the record' },
127
+ { value: 'createRecord', label: 'Create a record' },
128
+ { value: 'notify', label: 'Show a notice' },
129
+ { value: 'highlight', label: 'Color the record while it matches' },
130
+ { value: 'webhook', label: 'Call a webhook' },
131
+ { value: 'email', label: 'Send an email' },
132
+ ...appActions.map((a) => ({ value: `run:${a.id}` as const, label: a.label })),
133
+ ]
134
+ function newAction(value: string): AutomationAction {
135
+ if (value.startsWith('run:')) return { type: 'run', action: value.slice(4) }
136
+ switch (value) {
137
+ case 'updateRecord':
138
+ return { type: 'updateRecord', values: {} }
139
+ case 'createRecord':
140
+ return { type: 'createRecord', table: table.id, values: {} }
141
+ case 'highlight':
142
+ return { type: 'highlight', background: '#fee2e2', color: '#991b1b' }
143
+ case 'webhook':
144
+ return { type: 'webhook', url: 'https://', body: '' }
145
+ case 'email':
146
+ return { type: 'email', to: '', subject: '', body: '' }
147
+ default:
148
+ return { type: 'notify', message: '', severity: 'info' }
149
+ }
150
+ }
151
+ const actionKey = (a: AutomationAction) => (a.type === 'run' ? `run:${a.action}` : a.type)
152
+ function setAction(i: number, next: AutomationAction | null) {
153
+ if (!selected) return
154
+ const actions = [...selected.actions]
155
+ if (next) actions[i] = next
156
+ else actions.splice(i, 1)
157
+ patch({ actions })
158
+ }
159
+ function moveAction(i: number, dir: -1 | 1) {
160
+ if (!selected) return
161
+ const actions = [...selected.actions]
162
+ const j = i + dir
163
+ if (j < 0 || j >= actions.length) return
164
+ ;[actions[i], actions[j]] = [actions[j]!, actions[i]!]
165
+ patch({ actions })
166
+ }
167
+ function setValue(i: number, values: Record<string, unknown>, id: string, value: unknown) {
168
+ const a = selected?.actions[i]
169
+ if (!a || (a.type !== 'updateRecord' && a.type !== 'createRecord')) return
170
+ setAction(i, { ...a, values: { ...values, [id]: value } })
171
+ }
172
+ function dropValue(i: number, id: string) {
173
+ const a = selected?.actions[i]
174
+ if (!a || (a.type !== 'updateRecord' && a.type !== 'createRecord')) return
175
+ const { [id]: _gone, ...values } = a.values
176
+ setAction(i, { ...a, values })
177
+ }
178
+
179
+ /** What a field's value box suggests: its options, people, record names, dates, tokens. */
180
+ function suggestions(t: BaseTable | undefined, c: { fieldType?: string; def: Record<string, any> }): string[] {
181
+ const tokens = fields.map((f) => `{${f.header}}`)
182
+ switch (c.fieldType) {
183
+ case 'singleSelect':
184
+ case 'multiSelect':
185
+ return ((c.def.fieldProps?.options ?? []) as Array<{ value: string }>).map((o) => o.value)
186
+ case 'user':
187
+ return getFieldUsers().map((u) => u.name)
188
+ case 'checkbox':
189
+ return ['true', 'false']
190
+ case 'date':
191
+ return ['today', 'today+1', 'today+7', 'today-1', 'now']
192
+ case 'link': {
193
+ const target = tables.find((x) => x.id === c.def.fieldProps?.table)
194
+ const names = target ? target.data.slice(0, 50).map((r) => String(r?.[primaryOf(target)] ?? '')).filter(Boolean) : []
195
+ return [...(target?.id === table.id || c.def.fieldProps?.table === table.id ? ['{record}'] : []), ...names]
196
+ }
197
+ default:
198
+ return t ? tokens : []
199
+ }
200
+ }
201
+ const PLACEHOLDER: Record<string, string> = { date: 'today+7', checkbox: 'true', number: '0, or 100% for a percent', link: 'a record name, or {record}', user: 'a name' }
202
+
203
+ // ---- Try it ----
204
+ let tryRecord = $state('')
205
+ const records = $derived(table.data.filter(Boolean).slice(0, 200).map((r) => ({ id: rowIdOf(table, r), name: String(r[primaryOf(table)] ?? '') || '(no name)' })))
206
+ function tryNow() {
207
+ if (!selected) return
208
+ void runner.runNow($state.snapshot(selected) as BaseAutomation, table.id, tryRecord || null)
209
+ tab = 'runs'
210
+ }
211
+
212
+ // ---- Runs: this page's and, when the app hands them over, the server's ----
213
+ let fromServer = $state.raw<ReadonlyArray<AutomationRun>>([])
214
+ let serverError = $state('')
215
+ function showRuns() {
216
+ tab = 'runs'
217
+ if (!serverRuns) return
218
+ serverRuns().then(
219
+ (r) => {
220
+ fromServer = r
221
+ serverError = ''
222
+ },
223
+ (e) => (serverError = e instanceof Error ? e.message : String(e)),
224
+ )
225
+ }
226
+ const allRuns = $derived(
227
+ [...runner.runs.map((r) => ({ ...r, side: 'browser' as const })), ...fromServer.map((r) => ({ ...r, side: 'server' as const }))].sort((x, y) => y.at - x.at),
228
+ )
229
+ /** `Name: value` lines, as a webhook's headers. */
230
+ function parseHeaders(text: string): Record<string, string> {
231
+ const out: Record<string, string> = {}
232
+ for (const line of text.split('\n')) {
233
+ const at = line.indexOf(':')
234
+ if (at > 0) out[line.slice(0, at).trim()] = line.slice(at + 1).trim()
235
+ }
236
+ return out
237
+ }
238
+ const headerText = (h: Record<string, string> | undefined) => Object.entries(h ?? {}).map(([k, v]) => `${k}: ${v}`).join('\n')
239
+
240
+ const time = (at: number) => new Date(at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })
241
+ </script>
242
+
243
+ <SvModal bind:open title={`Automations in ${table.name}`} width={1000} resizable>
244
+ <div class="sv-bauto">
245
+ <aside class="sv-bauto-list" aria-label="Automations">
246
+ {#each list as a (a.id)}
247
+ <div class="sv-bauto-item" class:is-on={a.id === selectedId && tab === 'edit'}>
248
+ <button type="button" class="sv-bauto-pick" onclick={() => { selectedId = a.id; tab = 'edit' }}>
249
+ <span class="sv-bauto-name">{a.name || 'Untitled'}</span>
250
+ <span class="sv-bauto-when">{triggerText(table, a)}</span>
251
+ </button>
252
+ <SvSwitchButton
253
+ checked={a.enabled}
254
+ ariaLabel={`${a.name} on`}
255
+ onChange={(on: boolean) => save(list.map((x) => (x.id === a.id ? { ...x, enabled: on } : x)))}
256
+ />
257
+ </div>
258
+ {:else}
259
+ <p class="sv-bauto-empty">No automations yet. An automation does something when a record is added, changes or matches a condition.</p>
260
+ {/each}
261
+ <div class="sv-bauto-list-foot">
262
+ <button type="button" class="sv-fvb-text-btn" onclick={add}>+ New automation</button>
263
+ <button type="button" class="sv-bauto-runs-btn" class:is-on={tab === 'runs'} onclick={showRuns}>
264
+ Runs{#if allRuns.length}<span class="sv-bauto-count">{allRuns.length}</span>{/if}
265
+ </button>
266
+ </div>
267
+ </aside>
268
+
269
+ <section class="sv-bauto-main">
270
+ {#if tab === 'runs'}
271
+ <header class="sv-bauto-runs-head">
272
+ <h3>Runs</h3>
273
+ {#if runner.runs.length}<button type="button" class="sv-fvb-text-btn" onclick={() => runner.clearRuns()}>Clear this page's runs</button>{/if}
274
+ </header>
275
+ {#if serverError}<p class="sv-bauto-hint sv-bauto-bad">The server's runs did not load: {serverError}</p>{/if}
276
+ {#each allRuns as run (`${run.side}-${run.id}`)}
277
+ <article class="sv-bauto-run" data-status={run.status}>
278
+ <div class="sv-bauto-run-head">
279
+ <span class="sv-bauto-status">{run.status === 'ok' ? 'Done' : run.status === 'error' ? 'Failed' : 'Stopped'}</span>
280
+ <strong>{run.name}</strong>
281
+ {#if run.record}<span class="sv-bauto-muted">on {run.record}</span>{/if}
282
+ {#if run.table !== table.name}<span class="sv-bauto-muted">in {run.table}</span>{/if}
283
+ {#if run.side === 'server'}<span class="sv-bauto-tag">Server</span>{/if}
284
+ <span class="sv-bauto-muted sv-bauto-time">{time(run.at)}</span>
285
+ </div>
286
+ <ul>
287
+ {#each run.steps as s, i (i)}
288
+ <li class:is-error={!!s.error}><span>{s.action}</span> {s.error ?? s.detail}</li>
289
+ {/each}
290
+ </ul>
291
+ </article>
292
+ {:else}
293
+ <p class="sv-bauto-empty">Nothing has run yet. Runs show here with what each action did.</p>
294
+ {/each}
295
+ {:else if selected}
296
+ {@const a = selected}
297
+ <div class="sv-bauto-head">
298
+ <label class="sv-bauto-field">
299
+ <span>Name</span>
300
+ <input class="sv-fvb-input" value={a.name} onchange={(e) => patch({ name: e.currentTarget.value })} />
301
+ </label>
302
+ <label class="sv-bauto-field">
303
+ <span>Runs in</span>
304
+ <select class="sv-fvb-select" value={a.runOn ?? 'browser'} onchange={(e) => patch({ runOn: e.currentTarget.value === 'server' ? 'server' : undefined })}>
305
+ <option value="browser">This browser</option>
306
+ <option value="server">The server</option>
307
+ </select>
308
+ </label>
309
+ </div>
310
+ <p class="sv-bauto-hint">
311
+ {a.runOn === 'server'
312
+ ? 'Your server runs it (createAutomationServer): it hears changes from the database, whoever made them, reads secrets and sends email.'
313
+ : 'It runs in the browser of the person who makes the change, right away.'}
314
+ </p>
315
+
316
+ <div class="sv-bauto-step">
317
+ <h4><span class="sv-bauto-badge">When</span></h4>
318
+ <select class="sv-fvb-select" aria-label="Trigger" value={a.trigger.type} onchange={(e) => setTrigger(e.currentTarget.value as AutomationTrigger['type'])}>
319
+ {#each TRIGGERS as t (t.value)}<option value={t.value}>{t.label}</option>{/each}
320
+ </select>
321
+ {#if a.trigger.type === 'recordUpdated'}
322
+ {@const watched = new Set(a.trigger.fields ?? [])}
323
+ <p class="sv-bauto-hint">{watched.size ? 'Only when one of these fields changes:' : 'Any field. Pick fields to watch only those:'}</p>
324
+ <div class="sv-bauto-chips">
325
+ {#each writable(table) as c (c.id)}
326
+ <button type="button" class="sv-bauto-chip" class:is-on={watched.has(c.id)} aria-pressed={watched.has(c.id)} onclick={() => toggleWatched(c.id)}>{c.header}</button>
327
+ {/each}
328
+ </div>
329
+ {:else if a.trigger.type === 'formSubmitted'}
330
+ <select class="sv-fvb-select" aria-label="Form" value={a.trigger.view ?? ''} onchange={(e) => patch({ trigger: { type: 'formSubmitted', ...(e.currentTarget.value ? { view: e.currentTarget.value } : {}) } })}>
331
+ <option value="">Any form of this table</option>
332
+ {#each forms as v (v.id)}<option value={v.id}>{v.name}</option>{/each}
333
+ </select>
334
+ {:else if a.trigger.type === 'button'}
335
+ {#if buttons.length}
336
+ <select class="sv-fvb-select" aria-label="Button field" value={a.trigger.field} onchange={(e) => patch({ trigger: { type: 'button', field: e.currentTarget.value } })}>
337
+ {#each buttons as c (c.id)}<option value={c.id}>{c.header}</option>{/each}
338
+ </select>
339
+ {:else}
340
+ <p class="sv-bauto-hint">Add a Button field to the table first.</p>
341
+ {/if}
342
+ {:else if a.trigger.type === 'scheduled'}
343
+ {@const cron = a.trigger.cron}
344
+ <select class="sv-fvb-select" aria-label="Schedule" value={CRON_PRESETS.some((p) => p.cron === cron) ? cron : ''} onchange={(e) => e.currentTarget.value && patch({ trigger: { type: 'scheduled', cron: e.currentTarget.value } })}>
345
+ {#each CRON_PRESETS as p (p.cron)}<option value={p.cron}>{p.label}</option>{/each}
346
+ <option value="">Custom</option>
347
+ </select>
348
+ <input class="sv-fvb-input sv-bauto-mono sv-bauto-cron" aria-label="Cron expression" value={cron} onchange={(e) => patch({ trigger: { type: 'scheduled', cron: e.currentTarget.value.trim() } })} />
349
+ <p class="sv-bauto-hint">Runs while the base is open in a browser. With a condition, it runs once for each record that matches at that time.</p>
350
+ {/if}
351
+ </div>
352
+
353
+ <div class="sv-bauto-step">
354
+ <h4><span class="sv-bauto-badge">{a.trigger.type === 'recordMatches' ? 'Condition' : 'Only for records where'}</span></h4>
355
+ <FilterPanel
356
+ {ctrl}
357
+ value={a.condition ?? EMPTY}
358
+ onChange={setCondition}
359
+ title={a.trigger.type === 'recordMatches' ? 'Runs when a record starts to match' : ''}
360
+ empty={a.trigger.type === 'recordMatches' ? 'Add the condition a record has to start matching.' : 'Every record. Add a condition to narrow it.'}
361
+ />
362
+ </div>
363
+
364
+ <div class="sv-bauto-step">
365
+ <h4><span class="sv-bauto-badge">Do</span></h4>
366
+ {#each a.actions as action, i (i)}
367
+ <div class="sv-bauto-action">
368
+ <div class="sv-bauto-action-head">
369
+ <span class="sv-bauto-n">{i + 1}</span>
370
+ <select class="sv-fvb-select" aria-label="Action" value={actionKey(action)} onchange={(e) => setAction(i, newAction(e.currentTarget.value))}>
371
+ {#each ACTIONS as o (o.value)}<option value={o.value}>{o.label}</option>{/each}
372
+ </select>
373
+ <button type="button" class="sv-fvb-icon-btn" aria-label="Move up" title="Move up" disabled={i === 0} onclick={() => moveAction(i, -1)}>↑</button>
374
+ <button type="button" class="sv-fvb-icon-btn" aria-label="Move down" title="Move down" disabled={i === a.actions.length - 1} onclick={() => moveAction(i, 1)}>↓</button>
375
+ <button type="button" class="sv-fvb-icon-btn" aria-label="Remove action" title="Remove action" onclick={() => setAction(i, null)}>×</button>
376
+ </div>
377
+
378
+ {#if action.type === 'notify'}
379
+ <div class="sv-bauto-row">
380
+ <input class="sv-fvb-input" aria-label="Message" value={action.message} placeholder={`{${fields[0]?.header ?? 'Name'}} needs a look`} onchange={(e) => setAction(i, { ...action, message: e.currentTarget.value })} />
381
+ <select class="sv-fvb-select sv-bauto-narrow" aria-label="Kind" value={action.severity ?? 'info'} onchange={(e) => setAction(i, { ...action, severity: e.currentTarget.value as 'info' })}>
382
+ <option value="info">Info</option><option value="success">Success</option><option value="warning">Warning</option><option value="error">Error</option>
383
+ </select>
384
+ </div>
385
+ <p class="sv-bauto-hint">Name a field in braces for its value: {fields.slice(0, 3).map((c) => `{${c.header}}`).join(', ')}.</p>
386
+ {:else if action.type === 'highlight'}
387
+ {#if a.trigger.type !== 'recordMatches' && !a.condition}
388
+ <p class="sv-bauto-hint">Colors records while they match the condition above; add one.</p>
389
+ {/if}
390
+ <div class="sv-bauto-row">
391
+ <label class="sv-bauto-color">Fill <input type="color" value={action.background} onchange={(e) => setAction(i, { ...action, background: e.currentTarget.value })} /></label>
392
+ <label class="sv-bauto-color">Text <input type="color" value={action.color ?? '#0f172a'} onchange={(e) => setAction(i, { ...action, color: e.currentTarget.value })} /></label>
393
+ <span class="sv-bauto-sample" style:background={action.background} style:color={action.color}>Sample</span>
394
+ </div>
395
+ <div class="sv-bauto-chips">
396
+ <button type="button" class="sv-bauto-chip" class:is-on={!action.fields?.length} onclick={() => setAction(i, { ...action, fields: [] })}>Whole row</button>
397
+ {#each fields as c (c.id)}
398
+ {@const on = action.fields?.includes(c.id) ?? false}
399
+ <button type="button" class="sv-bauto-chip" class:is-on={on} aria-pressed={on} onclick={() => setAction(i, { ...action, fields: on ? (action.fields ?? []).filter((x) => x !== c.id) : [...(action.fields ?? []), c.id] })}>{c.header}</button>
400
+ {/each}
401
+ </div>
402
+ {:else if action.type === 'updateRecord' || action.type === 'createRecord'}
403
+ {@const target = action.type === 'createRecord' ? tables.find((x) => x.id === action.table) : table}
404
+ {#if action.type === 'createRecord'}
405
+ <div class="sv-bauto-row">
406
+ <span class="sv-bauto-label">In</span>
407
+ <select class="sv-fvb-select" aria-label="Table" value={action.table} onchange={(e) => setAction(i, { type: 'createRecord', table: e.currentTarget.value, values: {} })}>
408
+ {#each tables as x (x.id)}<option value={x.id}>{x.name}</option>{/each}
409
+ </select>
410
+ </div>
411
+ {@const links = fieldsOf(target).filter((c) => c.fieldType === 'link' && c.def.fieldProps?.table === table.id)}
412
+ {#if links.length}
413
+ <div class="sv-bauto-row">
414
+ <span class="sv-bauto-label">Link it back</span>
415
+ <select class="sv-fvb-select" aria-label="Link field" value={action.linkField ?? ''} onchange={(e) => setAction(i, { ...action, linkField: e.currentTarget.value || undefined })}>
416
+ <option value="">No link</option>
417
+ {#each links as c (c.id)}<option value={c.id}>through {c.header}</option>{/each}
418
+ </select>
419
+ </div>
420
+ {/if}
421
+ {/if}
422
+ {#each Object.entries(action.values) as [id, value] (id)}
423
+ {@const c = tableFields(target ?? table).get(id)}
424
+ {#if c}
425
+ <div class="sv-bauto-row">
426
+ <span class="sv-bauto-label">{c.header}</span>
427
+ <input
428
+ class="sv-fvb-input"
429
+ aria-label={`${c.header} value`}
430
+ list={`sv-bauto-${i}-${id}`}
431
+ value={typeof value === 'string' ? value : JSON.stringify(value)}
432
+ placeholder={PLACEHOLDER[String(c.fieldType)] ?? 'a value, or {Field}'}
433
+ onchange={(e) => setValue(i, action.values, id, e.currentTarget.value)}
434
+ />
435
+ <datalist id={`sv-bauto-${i}-${id}`}>{#each suggestions(target, c) as s (s)}<option value={s}></option>{/each}</datalist>
436
+ <button type="button" class="sv-fvb-icon-btn" aria-label={`Do not set ${c.header}`} title="Remove" onclick={() => dropValue(i, id)}>×</button>
437
+ </div>
438
+ {/if}
439
+ {/each}
440
+ {@const free = writable(target).filter((c) => !(c.id in action.values) && !(action.type === 'createRecord' && c.id === action.linkField))}
441
+ {#if free.length}
442
+ <select class="sv-fvb-select sv-bauto-add" aria-label="Set a field" value="" onchange={(e) => { if (e.currentTarget.value) setValue(i, action.values, e.currentTarget.value, '') }}>
443
+ <option value="">+ Set a field</option>
444
+ {#each free as c (c.id)}<option value={c.id}>{c.header}</option>{/each}
445
+ </select>
446
+ {/if}
447
+ {:else if action.type === 'run'}
448
+ <p class="sv-bauto-hint">Runs <code>{action.action}</code>, an action the app registered.</p>
449
+ {:else if action.type === 'webhook'}
450
+ <div class="sv-bauto-row">
451
+ <select class="sv-fvb-select sv-bauto-narrow" aria-label="Method" value={action.method ?? 'POST'} onchange={(e) => setAction(i, { ...action, method: e.currentTarget.value as 'POST' })}>
452
+ <option value="POST">POST</option><option value="PUT">PUT</option><option value="PATCH">PATCH</option>
453
+ </select>
454
+ <input class="sv-fvb-input sv-bauto-mono" aria-label="URL" value={action.url} placeholder={'https://hooks.example.com/{secret:HOOK_ID}'} onchange={(e) => setAction(i, { ...action, url: e.currentTarget.value.trim() })} />
455
+ </div>
456
+ <textarea class="sv-fvb-input sv-bauto-text sv-bauto-mono" aria-label="Headers" rows="2" placeholder={'Authorization: Bearer {secret:API_KEY}'} value={headerText(action.headers)} onchange={(e) => setAction(i, { ...action, headers: parseHeaders(e.currentTarget.value) })}></textarea>
457
+ <textarea class="sv-fvb-input sv-bauto-text sv-bauto-mono" aria-label="Body" rows="3" placeholder={'{"text": "{' + (fields[0]?.header ?? 'Name') + '} needs a look"}'} value={action.body ?? ''} onchange={(e) => setAction(i, { ...action, body: e.currentTarget.value })}></textarea>
458
+ <p class="sv-bauto-hint">Empty body: the record as JSON. <code>{'{secret:NAME}'}</code> is read on the server only{a.runOn === 'server' ? '' : '; set Runs in to The server to use one'}.</p>
459
+ {:else if action.type === 'email'}
460
+ {#if a.runOn !== 'server'}<p class="sv-bauto-hint sv-bauto-bad">Email is sent by the server: set Runs in to The server.</p>{/if}
461
+ <div class="sv-bauto-row"><span class="sv-bauto-label">To</span><input class="sv-fvb-input" aria-label="To" value={action.to} placeholder="team@example.com, {'{'}Owner email{'}'}" onchange={(e) => setAction(i, { ...action, to: e.currentTarget.value })} /></div>
462
+ <div class="sv-bauto-row"><span class="sv-bauto-label">Subject</span><input class="sv-fvb-input" aria-label="Subject" value={action.subject} onchange={(e) => setAction(i, { ...action, subject: e.currentTarget.value })} /></div>
463
+ <textarea class="sv-fvb-input sv-bauto-text" aria-label="Email text" rows="3" value={action.body} onchange={(e) => setAction(i, { ...action, body: e.currentTarget.value })}></textarea>
464
+ {/if}
465
+ </div>
466
+ {/each}
467
+ <select class="sv-fvb-select sv-bauto-add" aria-label="Add an action" value="" onchange={(e) => { if (e.currentTarget.value) patch({ actions: [...a.actions, newAction(e.currentTarget.value)] }) }}>
468
+ <option value="">+ Add an action</option>
469
+ {#each ACTIONS as o (o.value)}<option value={o.value}>{o.label}</option>{/each}
470
+ </select>
471
+ </div>
472
+
473
+ <footer class="sv-bauto-foot">
474
+ {#if deleting === a.id}
475
+ <span class="sv-bauto-hint">Delete it?</span>
476
+ <button type="button" class="sv-bauto-danger" onclick={() => { deleting = null; remove(a.id) }}>Delete</button>
477
+ <button type="button" class="sv-fvb-text-btn" onclick={() => (deleting = null)}>Keep</button>
478
+ {:else}
479
+ <button type="button" class="sv-bauto-danger" onclick={() => (deleting = a.id)}>Delete automation</button>
480
+ {/if}
481
+ <span class="sv-bauto-spacer"></span>
482
+ {#if a.runOn === 'server'}
483
+ <span class="sv-bauto-hint">Runs on your server when its trigger happens.</span>
484
+ {:else}
485
+ <select class="sv-fvb-select sv-bauto-try" aria-label="Record to try it on" bind:value={tryRecord}>
486
+ {#if a.trigger.type === 'scheduled'}<option value="">No record</option>{/if}
487
+ {#each records as r (r.id)}<option value={r.id}>{r.name}</option>{/each}
488
+ </select>
489
+ <button type="button" class="sv-fvb-solid-btn" onclick={tryNow}>Run now</button>
490
+ {/if}
491
+ </footer>
492
+ {:else}
493
+ <p class="sv-bauto-empty">Pick an automation, or add one.</p>
494
+ {/if}
495
+ </section>
496
+ </div>
497
+ </SvModal>
498
+
499
+ <style>
500
+ .sv-bauto { display: grid; grid-template-columns: 270px minmax(0, 1fr); min-height: 520px; max-height: 72vh; font-size: 13px; }
501
+ @media (max-width: 760px) { .sv-bauto { grid-template-columns: 1fr; } }
502
+ .sv-bauto-list { display: flex; flex-direction: column; gap: 2px; padding: 10px; overflow: auto; border-right: 1px solid var(--sg-border, #e2e8f0); background: var(--sg-header-bg, transparent); }
503
+ .sv-bauto-item { display: flex; align-items: center; gap: 6px; padding: 4px 6px 4px 4px; border-radius: 8px; }
504
+ .sv-bauto-item.is-on { background: color-mix(in srgb, var(--sg-accent, #2563eb) 12%, transparent); }
505
+ .sv-bauto-pick { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; padding: 6px; border: 0; background: none; color: inherit; font: inherit; text-align: left; cursor: pointer; border-radius: 6px; }
506
+ .sv-bauto-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
507
+ .sv-bauto-when { font-size: 12px; color: var(--sg-muted, #64748b); display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
508
+ .sv-bauto-list-foot { margin-top: auto; padding-top: 10px; display: flex; align-items: center; justify-content: space-between; gap: 8px; border-top: 1px solid var(--sg-border, #e2e8f0); }
509
+ .sv-bauto-runs-btn { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 10px; border: 1px solid var(--sg-border, #cbd5e1); border-radius: 6px; background: none; color: inherit; font: inherit; cursor: pointer; }
510
+ .sv-bauto-runs-btn.is-on { border-color: var(--sg-accent, #2563eb); color: var(--sg-accent, #2563eb); }
511
+ .sv-bauto-count { min-width: 18px; padding: 0 5px; border-radius: 999px; font-size: 11px; font-weight: 700; background: color-mix(in srgb, var(--sg-fg, #0f172a) 10%, transparent); text-align: center; }
512
+ .sv-bauto-main { display: flex; flex-direction: column; gap: 14px; padding: 16px 18px; overflow: auto; min-width: 0; }
513
+ .sv-bauto-head { display: grid; grid-template-columns: minmax(0, 1fr) 180px; gap: 12px; max-width: 620px; }
514
+ .sv-bauto-field { display: grid; gap: 4px; }
515
+ .sv-bauto-text { width: 100%; height: auto; padding: 6px; resize: vertical; line-height: 1.4; }
516
+ .sv-bauto-bad { color: var(--sg-danger, #dc2626); }
517
+ .sv-bauto-tag { font-size: 11px; font-weight: 600; padding: 0 6px; border-radius: 4px; border: 1px solid var(--sg-border, #cbd5e1); color: var(--sg-muted, #64748b); }
518
+ .sv-bauto-field > span, .sv-bauto-label { color: var(--sg-muted, #64748b); font-size: 12px; }
519
+ .sv-bauto-step { display: grid; gap: 8px; padding: 12px 14px; border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px; }
520
+ .sv-bauto-step h4 { margin: 0; }
521
+ .sv-bauto-badge { font-size: 11px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--sg-muted, #64748b); }
522
+ .sv-bauto-step > .sv-fvb-select { max-width: 340px; }
523
+ .sv-bauto-hint { margin: 0; font-size: 12px; color: var(--sg-muted, #64748b); }
524
+ .sv-bauto-chips { display: flex; flex-wrap: wrap; gap: 6px; }
525
+ .sv-bauto-chip { height: 26px; padding: 0 10px; border: 1px solid var(--sg-border, #cbd5e1); border-radius: 999px; background: none; color: inherit; font: inherit; font-size: 12px; cursor: pointer; }
526
+ .sv-bauto-chip.is-on { border-color: var(--sg-accent, #2563eb); background: color-mix(in srgb, var(--sg-accent, #2563eb) 14%, transparent); }
527
+ .sv-bauto-action { display: grid; gap: 8px; padding: 10px; border-radius: 8px; background: color-mix(in srgb, var(--sg-fg, #0f172a) 3%, transparent); }
528
+ .sv-bauto-action-head { display: grid; grid-template-columns: 22px minmax(0, 300px) 26px 26px 26px; gap: 6px; align-items: center; }
529
+ .sv-bauto-n { display: grid; place-items: center; width: 22px; height: 22px; border-radius: 50%; font-size: 11px; font-weight: 700; background: var(--sg-fg, #0f172a); color: var(--sg-bg, #fff); }
530
+ .sv-bauto-row { display: flex; align-items: center; gap: 8px; }
531
+ .sv-bauto-row > .sv-bauto-label { flex: 0 0 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
532
+ .sv-bauto-row > .sv-fvb-input { flex: 1; }
533
+ .sv-bauto-narrow { flex: 0 0 110px; width: 110px; }
534
+ .sv-bauto-add { max-width: 220px; }
535
+ .sv-bauto-color { display: inline-flex; align-items: center; gap: 6px; color: var(--sg-muted, #64748b); }
536
+ .sv-bauto-color input { width: 32px; height: 24px; padding: 0; border: 1px solid var(--sg-border, #cbd5e1); border-radius: 4px; background: none; }
537
+ .sv-bauto-sample { padding: 2px 10px; border-radius: 4px; font-size: 12px; }
538
+ .sv-bauto-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
539
+ .sv-bauto-cron { max-width: 220px; }
540
+ .sv-bauto-foot { display: flex; align-items: center; gap: 8px; padding-top: 4px; }
541
+ .sv-bauto-spacer { flex: 1; }
542
+ .sv-bauto-try { width: 220px; }
543
+ .sv-bauto-danger { border: 0; background: none; padding: 0; font: inherit; color: var(--sg-danger, #dc2626); cursor: pointer; }
544
+ .sv-bauto-danger:hover { text-decoration: underline; }
545
+ .sv-bauto-empty { margin: 8px 4px; color: var(--sg-muted, #64748b); }
546
+ .sv-bauto-runs-head { display: flex; align-items: center; justify-content: space-between; }
547
+ .sv-bauto-runs-head h3 { margin: 0; font-size: 15px; }
548
+ .sv-bauto-run { padding: 10px 12px; border: 1px solid var(--sg-border, #e2e8f0); border-radius: 8px; }
549
+ .sv-bauto-run-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
550
+ .sv-bauto-status { font-size: 11px; font-weight: 700; padding: 1px 7px; border-radius: 999px; color: #fff; background: var(--sg-success, #16a34a); }
551
+ .sv-bauto-run[data-status='error'] .sv-bauto-status { background: var(--sg-danger, #dc2626); }
552
+ .sv-bauto-run[data-status='stopped'] .sv-bauto-status { background: var(--sg-warning, #d97706); }
553
+ .sv-bauto-muted { color: var(--sg-muted, #64748b); }
554
+ .sv-bauto-time { margin-left: auto; font-variant-numeric: tabular-nums; }
555
+ .sv-bauto-run ul { margin: 6px 0 0; padding-left: 18px; }
556
+ .sv-bauto-run li > span { font-weight: 600; }
557
+ .sv-bauto-run li.is-error { color: var(--sg-danger, #dc2626); }
558
+ </style>
@@ -0,0 +1,18 @@
1
+ import { type BaseTable } from './base-tables.js';
2
+ import { type AutomationRun, type AutomationRunner, type BaseAutomation } from './automations.svelte.js';
3
+ import type { FieldView } from './view-model.js';
4
+ type $$ComponentProps = {
5
+ open?: boolean;
6
+ table: BaseTable;
7
+ tables: BaseTable[];
8
+ /** The grid of the table, for the condition's fields. */
9
+ ctrl: any;
10
+ views?: FieldView[];
11
+ runner: AutomationRunner;
12
+ /** The server runner's latest runs (an endpoint returning `server.runs`). */
13
+ serverRuns?: () => Promise<ReadonlyArray<AutomationRun>>;
14
+ onChange: (automations: BaseAutomation[]) => void;
15
+ };
16
+ declare const BaseAutomations: import("svelte").Component<$$ComponentProps, {}, "open">;
17
+ type BaseAutomations = ReturnType<typeof BaseAutomations>;
18
+ export default BaseAutomations;
@@ -0,0 +1,47 @@
1
+ <script lang="ts">
2
+ /**
3
+ * A chart view: the records the view shows (its filter applies), grouped
4
+ * and counted or added up as its setting says. The grid stays mounted
5
+ * under it, hidden: it is where the records, the filter and the sort live.
6
+ */
7
+ import { SvChart } from '@svgrid/grid'
8
+ import { dataRows, gridOf } from './grid-access'
9
+ import { chartFormatter, chartSpec, type ChartSetting } from './chart-view'
10
+
11
+ let { api, setting }: { api: any; setting: ChartSetting | undefined } = $props()
12
+
13
+ const ctrl = $derived(api ? gridOf(api) : null)
14
+ const rows = $derived(ctrl ? dataRows(ctrl).map((r) => r.original) : [])
15
+ const formatValue = $derived(ctrl && setting ? chartFormatter(ctrl, setting) : undefined)
16
+ const spec = $derived.by(() => {
17
+ const s = ctrl && setting ? chartSpec(ctrl, setting, rows) : null
18
+ if (!s || !formatValue || s.type === 'pie' || s.orientation === 'horizontal') return s
19
+ // The value axis gutter fits the widest tick as the field writes it
20
+ // ("$1,000,000"); the chart's own default fits its short "1M" style.
21
+ const max = Math.max(0, ...s.series.flatMap((x) => x.values))
22
+ const top = max > 0 ? 10 ** Math.ceil(Math.log10(max)) : 1
23
+ const width = Math.max(48, Math.round(formatValue(top).length * 6.4 + 14))
24
+ return { ...s, yAxis: { width } }
25
+ })
26
+ const isPie = $derived(setting?.kind === 'pie' || setting?.kind === 'donut')
27
+ </script>
28
+
29
+ <div class="sv-fchart">
30
+ {#if spec && spec.categories.length}
31
+ <div class="sv-fchart-plot">
32
+ <SvChart {spec} {formatValue} autosize legend={isPie} dataLabels={isPie} />
33
+ </div>
34
+ {:else}
35
+ <p class="sv-fchart-empty">{spec ? 'No records to chart. The view\'s filter may hide them all.' : 'Choose a field to chart in Customize chart.'}</p>
36
+ {/if}
37
+ </div>
38
+
39
+ <style>
40
+ .sv-fchart {
41
+ position: absolute; inset: 0; display: flex; flex-direction: column; box-sizing: border-box; padding: 16px 20px 12px;
42
+ background: var(--sg-bg, #fff); font-family: var(--sg-font, inherit);
43
+ }
44
+ .sv-fchart-plot { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
45
+ .sv-fchart-plot > :global(*) { flex: 1 1 auto; min-height: 0; }
46
+ .sv-fchart-empty { margin: auto; font-size: 14px; color: var(--sg-muted, #64748b); }
47
+ </style>
@@ -0,0 +1,8 @@
1
+ import { type ChartSetting } from './chart-view.js';
2
+ type $$ComponentProps = {
3
+ api: any;
4
+ setting: ChartSetting | undefined;
5
+ };
6
+ declare const BaseChartView: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type BaseChartView = ReturnType<typeof BaseChartView>;
8
+ export default BaseChartView;