@arcforgelabs/flow 2026.10.51 → 2026.10.53-1

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 (43) hide show
  1. package/npm-shrinkwrap.json +2 -2
  2. package/package.json +1 -1
  3. package/server/api/captures.js +26 -4
  4. package/server/api/invoicing.js +1 -1
  5. package/server/app.js +2 -0
  6. package/server/auth/people.js +3 -3
  7. package/server/core/ai.js +2 -1
  8. package/server/core/capture-match.js +158 -0
  9. package/server/core/captures.js +141 -7
  10. package/server/core/invoice-rules.js +16 -5
  11. package/server/core/reader.js +1 -10
  12. package/server/core/sending.js +7 -3
  13. package/server/db/migrations/0024_rule_icon.sql +5 -0
  14. package/server/db/migrations/meta/0024_snapshot.json +4703 -0
  15. package/server/db/migrations/meta/_journal.json +8 -1
  16. package/server/db/schema.js +3 -0
  17. package/server/integrations/seazona/history/backfill.js +39 -9
  18. package/server/integrations/seazona/index.js +1 -12
  19. package/server/integrations/seazona/lanes.js +44 -27
  20. package/server/integrations/seazona/stage-moves.js +3 -13
  21. package/server/integrations/sentry/index.js +6 -6
  22. package/server/integrations/setup.js +1 -0
  23. package/server/integrations/xero/drafts.js +27 -13
  24. package/server/ui/app-page.js +1 -1
  25. package/server/ui/app.js +61 -20
  26. package/server/ui/capture-fields.js +17 -0
  27. package/server/ui/captures.js +289 -0
  28. package/server/ui/columns.js +68 -3
  29. package/server/ui/custom-fields.js +3 -9
  30. package/server/ui/flow.css +94 -18
  31. package/server/ui/invoice-rules.js +10 -2
  32. package/server/ui/invoicing-settings.js +2 -8
  33. package/server/ui/invoicing.js +12 -8
  34. package/server/ui/job-view.js +6 -3
  35. package/server/ui/jobs.js +40 -117
  36. package/server/ui/kit.js +86 -1
  37. package/server/ui/list-views.js +47 -3
  38. package/server/ui/mobile.js +45 -5
  39. package/server/ui/nav-pins.js +4 -2
  40. package/server/ui/price-list.js +138 -69
  41. package/server/ui/routes.js +4 -1
  42. package/server/ui/saved-views.js +2 -0
  43. package/server/ui/view-bar.js +61 -2
@@ -1,6 +1,6 @@
1
1
  // The view bar over a list: chips that open a small panel (Group, Sort,
2
2
  // Filter, Columns), as on ClickUp. Every list page builds its chips here, so
3
- // they look and work the same: Jobs (jobs.js) and Reconcile (reconcile.js).
3
+ // they look and work the same: Jobs, Invoices, Reconcile and the Price list.
4
4
  //
5
5
  // viewBar(h) answers the parts:
6
6
  // menu(icon, build, { rows, clearLabel }) a chip, its × and its panel.
@@ -9,6 +9,7 @@
9
9
  // option(text, checked, onclick, role, icon) a row in a panel, with a tick.
10
10
  // title, choose, bin, addButton, rowsFoot, clearAll the parts of a rows
11
11
  // panel: a title, rows of selects with a bin, a foot with Add and Clear.
12
+ // groupMenu(config), sortMenu(config) the Group and Sort chips (below).
12
13
  // filterMenu(config) the Filter chip, as ClickUp's: groups of rows of
13
14
  // field, operator and value, And / Or in a group and between groups,
14
15
  // "Add nested filter", "Add filter" and "Clear all" (job-view.js has
@@ -161,6 +162,64 @@ export function viewBar(h) {
161
162
  pop.style.left = `${Math.round(left)}px`;
162
163
  }
163
164
 
165
+ // ── Group by ──
166
+ // One row: what to group by, the group order, and a bin that takes the
167
+ // groups away when `clearable()` (Jobs' board always has lanes). The page
168
+ // names the chip with set(), as only it knows its group's name.
169
+ // options() [{ value, label } | { group, options }]
170
+ // view() { group, groupDir }; group 'none' is no groups
171
+ function groupMenu({ options, view, change, clearable = () => true }) {
172
+ return menu(ICONS.group, (redraw) => {
173
+ const { group, groupDir } = view();
174
+ const none = group === 'none';
175
+ return [
176
+ title('Group by'),
177
+ h('div', { class: 'fx-rows' }, h('div', { class: 'fx-rows__row fx-rows__row--group' },
178
+ choose('Group by', none ? '' : group, [...(none ? [{ value: '', label: 'Choose…', disabled: true }] : []), ...options()], (value) => { change({ group: value }); redraw(); }),
179
+ choose('Group order', groupDir, [{ value: 'asc', label: 'Ascending' }, { value: 'desc', label: 'Descending' }], (value) => { change({ groupDir: value }); redraw(); }, { disabled: none }),
180
+ clearable() && !none ? bin('Remove the grouping', () => { change({ group: 'none' }); redraw(); }) : h('span', { class: 'fx-rows__gap' }))),
181
+ ];
182
+ }, { rows: true, clearLabel: 'Remove the grouping' });
183
+ }
184
+
185
+ // ── Sort ──
186
+ // Rows of sorts, each applied in turn; a list's column heads sort too.
187
+ // options() [{ value, label } | { group, options }]
188
+ // view() { sorts: [{ by, dir }] }
189
+ // dirWords(by) the two directions' words, as ['A to Z', 'Z to A']
190
+ // nameOf(by) the chip's name for a sort
191
+ function sortMenu({ options, view, change, dirWords, nameOf, empty = 'No sort.' }) {
192
+ const setSorts = (sorts) => change({ sorts });
193
+ const control = menu(ICONS.sort, (redraw) => {
194
+ const { sorts } = view();
195
+ const unused = options().flatMap((o) => o.options ?? [o]).filter((o) => !sorts.some((s) => s.by === o.value));
196
+ return [
197
+ title('Sort'),
198
+ sorts.length ? h('div', { class: 'fx-rows' }, sorts.map((s, i) => {
199
+ const [up, down] = dirWords(s.by);
200
+ const others = sorts.filter((_, n) => n !== i).map((x) => x.by);
201
+ const shown = options().map((o) => (o.group ? { ...o, options: o.options.filter((x) => !others.includes(x.value)) } : o)).filter((o) => o.group || !others.includes(o.value));
202
+ return h('div', { class: 'fx-rows__row fx-rows__row--sort' },
203
+ h('span', { class: 'fx-rows__lead' }, i ? 'then' : 'Sort by'),
204
+ choose('Sort by', s.by, shown, (by) => { setSorts(sorts.map((x, n) => (n === i ? { by, dir: x.dir } : x))); redraw(); }),
205
+ choose('Order', s.dir, [{ value: 'asc', label: up }, { value: 'desc', label: down }], (dir) => { setSorts(sorts.map((x, n) => (n === i ? { ...x, dir } : x))); redraw(); }),
206
+ bin('Remove this sort', () => { setSorts(sorts.filter((_, n) => n !== i)); redraw(); }));
207
+ })) : h('p', { class: 'fx-rows__empty' }, empty),
208
+ rowsFoot(
209
+ sorts.length < 5 && unused.length ? addButton('Add sort', () => { setSorts([...sorts, { by: unused[0].value, dir: 'asc' }]); redraw(); }) : h('span'),
210
+ sorts.length ? clearAll(() => { setSorts([]); redraw(); }) : null),
211
+ ];
212
+ }, { rows: true, clearLabel: 'Clear the sort' });
213
+ // The chip names the sort that is on.
214
+ const draw = () => {
215
+ const { sorts } = view();
216
+ const [first] = sorts;
217
+ control.set(sorts.length > 1 ? `Sort · ${sorts.length}` : first ? `Sort: ${nameOf(first.by)} ${first.dir === 'desc' ? '↓' : '↑'}` : 'Sort', sorts.length > 0,
218
+ sorts.length ? () => setSorts([]) : null);
219
+ };
220
+ return { ...control, draw };
221
+ }
222
+
164
223
  // ── Filters ──
165
224
  // As ClickUp's: groups of rows of field, operator and value, with a bin on
166
225
  // each row. A group's second row has And / Or for the rows in it; the
@@ -329,7 +388,7 @@ export function viewBar(h) {
329
388
  return { ...control, draw };
330
389
  }
331
390
 
332
- return { menu, option, title, choose, bin, addButton, rowsFoot, clearAll, filterMenu };
391
+ return { menu, option, title, choose, bin, addButton, rowsFoot, clearAll, groupMenu, sortMenu, filterMenu };
333
392
  }
334
393
 
335
394
  // A list's own search: a round search icon in the view bar that opens into a