@zestl/zvolv-widgets 0.0.2 → 0.0.4
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/CHANGELOG.md +35 -0
- package/MIRROR_NOTES.md +10 -0
- package/README.md +183 -5
- package/SKILL.md +22 -2
- package/dist/index.cjs +4249 -3788
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +246 -9
- package/dist/index.d.ts +246 -9
- package/dist/index.js +4237 -3789
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.4
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **`canViewDashboard(d, { isAdmin, groupId })`** — ZAI-Dashboard's dashboard view rule (admins all; `permissions` VIEW/EDIT_PERMISSION; `HideInMobileApp` needs EDIT_PERMISSION; `ViewPerm` fallback).
|
|
7
|
+
- **`visibleDashboardTree(elements, viewer)`** — `canViewDashboard` + `buildDashboardTree`, for an app's sidebar.
|
|
8
|
+
- README §12 and SKILL.md: apps on stored dashboards (pages, access, per-page loading, per-widget refresh) and writing records with `fetchSubmission` / `updateSubmission` / `createSubmission`.
|
|
9
|
+
|
|
10
|
+
### Documented
|
|
11
|
+
- A TABLE widget with `configuration.columns` returns only `id` + those columns (`_source`); store every field a host reads or writes back as a column.
|
|
12
|
+
|
|
13
|
+
## 0.0.3
|
|
14
|
+
|
|
15
|
+
These are all client-side enhancements that run after the query; none changes
|
|
16
|
+
what is queried. Existing configs give byte-identical queries, rows and default
|
|
17
|
+
markup, which `test/compat.mjs` checks against snapshots captured from 0.0.2.
|
|
18
|
+
There is nothing for ZAI-Dashboard or the server to mirror.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **KPI card layout.**
|
|
22
|
+
- `config.layout: "card"` (or `cardConfig.layout`) renders every item inside one card, through a `"KPI_CARD:card"` template override or the default `KpiCard`.
|
|
23
|
+
- `KpiWidget` takes `renderCard` (or `render`), which receives `{ vms, byKey, roles, state, tokens, config, onItemClick }`. Both are used only in card mode, so a generic `render` a host passes to every widget leaves KPIs unchanged.
|
|
24
|
+
- `KpiContentConfig.role` (`"main" | "sub" | "chip" | "series"`) places an item in the card; `resolveKpiRoles` does this sorting.
|
|
25
|
+
- `KpiSparkline` gains `width`, `height`, `area` and `scale: "minmax" | "zero"`.
|
|
26
|
+
- **`unpivot`.** On `useWidgetData` and every data widget. It turns row 0's columns into one `{ label, value }` row per column, in the given order. A missing column gives 0. `extra` copies more columns onto the output rows. It runs after postOperations. With no declared series, x and y become `labelField` and `valueField`. Pure helper: `unpivotRows`.
|
|
27
|
+
- **`ranking.order: "none"`.** `RankingBarWidget` keeps the incoming row order.
|
|
28
|
+
- **`FunnelWidget` (type `FUNNEL`).** Each stage resolves to `{ label, value, ratioToFirst, stepPct, isFirst, index, raw }`, with `onItemClick(row)` for drill-down. `render` gets `{ rows, first, tokens, config, toLabel, onItemClick }`. Pure helper: `computeFunnelRows`.
|
|
29
|
+
- **`categoryGap`.** On `useWidgetData` and every data widget. It gives one row per category in the given order, or in the form's dropdown order with `"fromForm"`. Missing buckets get 0 for every series valueField, and values that are not listed are kept at the end. It runs after postOperations and widget filters, and before local search, column filters and sort. Pure helpers: `fillCategoryGaps`, `elementOptionValues`.
|
|
30
|
+
- **`FilterPanelWidget` options from a data source.**
|
|
31
|
+
- `filters[].optionsSource = { dataSource, labelField, valueField, sort?, allLabel? }` fetches the options in one request: a distinct-values aggregation over the whole index, with the source's filters applied server-side (`optionsDataSource`).
|
|
32
|
+
- Values restored from URL params match numeric options (`matchFilterOption`).
|
|
33
|
+
- `onFiltersChange(WidgetFilter[])` emits `{ field, operator: "IN", value: [v] }`, or nothing for "All".
|
|
34
|
+
- The `renderFilter` slot receives `{ control, options, value, setValue, loading, tokens }`.
|
|
35
|
+
- Pure helpers: `filterPanelToWidgetFilters`, `rowsToFilterOptions`.
|
|
36
|
+
- **New tests:** `kpiCard`, `unpivot`, `funnel`, `categoryGap`, `filterOptions`. `compat` now also snapshots rows and default markup. A missing snapshot now fails the test instead of being regenerated.
|
|
37
|
+
|
|
3
38
|
## 0.0.2
|
|
4
39
|
|
|
5
40
|
Every new feature is opt-in. Configs that existed before this release produce
|
package/MIRROR_NOTES.md
CHANGED
|
@@ -144,3 +144,13 @@ item below is what the client expects. Verify each one against a live index.
|
|
|
144
144
|
- `dateDiff()`.
|
|
145
145
|
- `aggregate(f, type, name, { percents, where })`.
|
|
146
146
|
- `pivot()`, `formSource()` and `taskSource()` move `where` conditions into `pivot.filters` with `combine: "AND"`.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 0.0.3: nothing to mirror
|
|
151
|
+
|
|
152
|
+
`unpivot`, `categoryGap`, the KPI card layout, `FunnelWidget` and filter
|
|
153
|
+
`optionsSource` all run in the browser after the query. They add no query
|
|
154
|
+
shapes and need no server change. `categoryGap: "fromForm"` reads the form
|
|
155
|
+
through the same `getSingleForm` call the widget's access check already makes.
|
|
156
|
+
|
package/README.md
CHANGED
|
@@ -36,6 +36,7 @@ visual decision can be overridden.
|
|
|
36
36
|
9. [Install, link, test](#9-install-link-test)
|
|
37
37
|
10. [Rules and known limitations](#10-rules-and-known-limitations)
|
|
38
38
|
11. [Forms](#11-forms)
|
|
39
|
+
12. [Apps on stored dashboards: pages, access, writes](#12-apps-on-stored-dashboards-pages-access-writes)
|
|
39
40
|
|
|
40
41
|
---
|
|
41
42
|
|
|
@@ -545,6 +546,117 @@ KPI card with 20 numbers on one form is one call. `pivot()`, `formSource()` and
|
|
|
545
546
|
`combine: "AND"`. Stored `pivot.filters` entries without that marker keep the
|
|
546
547
|
original rule: the last entry per metric wins.
|
|
547
548
|
|
|
549
|
+
### Reshaping rows after the query (0.0.3)
|
|
550
|
+
|
|
551
|
+
These run in the browser after the query returns. They never change what is
|
|
552
|
+
queried.
|
|
553
|
+
|
|
554
|
+
**One wide row → one row per category (`unpivot`).** A multi-number source
|
|
555
|
+
returns one row such as `{ Longlist: 4, Contacted: 4, … }`. Turn it into one
|
|
556
|
+
row per column, in your order:
|
|
557
|
+
|
|
558
|
+
```tsx
|
|
559
|
+
const STAGES = ["Longlist", "Contacted", "Interviewed", "Offered", "Placed"];
|
|
560
|
+
|
|
561
|
+
<RankingBarWidget
|
|
562
|
+
dataSource={FUNNEL_ONE}
|
|
563
|
+
unpivot={{ columns: STAGES, extra: { "Contacted %": "step" } }}
|
|
564
|
+
configurations={{ ranking: { order: "none" } }} // keep stage order; ratio = value / max
|
|
565
|
+
/>
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
- Output rows are `{ label, value }`. Rename the keys with `labelField` / `valueField`.
|
|
569
|
+
- A missing column gives 0.
|
|
570
|
+
- `unpivot` runs after `postOperations`, so a `derive()` column can be a stage.
|
|
571
|
+
- With no declared `series`, x and y become `labelField` and `valueField`.
|
|
572
|
+
- `extra` copies a source column onto the stage row whose name it starts with. `"Contacted %"` lands on the "Contacted" row as `step`. A source column that matches no stage is copied onto every row.
|
|
573
|
+
|
|
574
|
+
**All categories, in dropdown order (`categoryGap`).** ES returns a bucket only
|
|
575
|
+
for values that have data. `categoryGap` adds a zero row for every missing
|
|
576
|
+
category and orders the rows:
|
|
577
|
+
|
|
578
|
+
```tsx
|
|
579
|
+
<BarChartWidget
|
|
580
|
+
dataSource={MINI_FUNNEL}
|
|
581
|
+
categoryGap={{ field: "Pipeline Stage", values: "fromForm" }} // or an explicit list
|
|
582
|
+
widgetFilters={[{ field: "Assignment", operator: "IN", value: [title] }]}
|
|
583
|
+
render={({ rows }) => <MiniBars rows={rows} />}
|
|
584
|
+
/>
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
- `field` is the chart's argumentField.
|
|
588
|
+
- `"fromForm"` reads that field's dropdown options from the form schema, once per form; it reuses the widget's form lookup.
|
|
589
|
+
- Every series `valueField` is 0 in the added rows.
|
|
590
|
+
- Values that are not in the list are kept, after the listed ones.
|
|
591
|
+
- It runs after `postOperations` and widget filters, and before local search, column filters and sort.
|
|
592
|
+
|
|
593
|
+
### Funnel
|
|
594
|
+
|
|
595
|
+
```tsx
|
|
596
|
+
<FunnelWidget
|
|
597
|
+
dataSource={FUNNEL_ONE}
|
|
598
|
+
unpivot={{ columns: STAGES }}
|
|
599
|
+
onItemClick={(row) => setStage(row.label)}
|
|
600
|
+
/>
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
Each stage resolves to `{ label, value, ratioToFirst, stepPct, isFirst, index, raw }`:
|
|
604
|
+
- `stepPct` is value ÷ previous stage × 100. It is `null` for the first stage, and after a stage of 0.
|
|
605
|
+
- `ratioToFirst` is value ÷ first stage. It is 0 when the first stage is 0.
|
|
606
|
+
|
|
607
|
+
Stages are never re-sorted. `render` gets `{ rows, first, tokens, config, toLabel, onItemClick }`.
|
|
608
|
+
|
|
609
|
+
### One KPI card from several numbers
|
|
610
|
+
|
|
611
|
+
```tsx
|
|
612
|
+
<KpiWidget dataSource={cardSource} configurations={{ layout: "card", contents: [
|
|
613
|
+
{ dataPoint: "value", role: "main", title: "Open mandates", template: "chart", seriesPoints: P1toP12 },
|
|
614
|
+
{ dataPoint: "total", role: "sub", title: "total" },
|
|
615
|
+
{ dataPoint: "atRisk", role: "sub", title: "at risk" },
|
|
616
|
+
{ dataPoint: "P12", role: "chip", template: "trend", deltaSource: "datapoint", deltaPoint: "P8",
|
|
617
|
+
deltaMode: "absolute", subtitle: "vs 4 wk" },
|
|
618
|
+
]}} />
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
The default card puts the items in slots by `role`:
|
|
622
|
+
- `main` is the big value. Without a `main` role, it is the first item with no role.
|
|
623
|
+
- `sub` items are small inline values under it.
|
|
624
|
+
- `chip` is the change badge (its `delta`).
|
|
625
|
+
- `series` supplies the sparkline. Without it, the main item's series is used.
|
|
626
|
+
|
|
627
|
+
Card mode turns on only with `layout: "card"`, set either at the config root
|
|
628
|
+
or in `cardConfig`, where platform KPI payloads keep their items. To build the
|
|
629
|
+
card yourself, pass `renderCard={({ vms, byKey, roles, state, tokens }) => …}`;
|
|
630
|
+
`render` also works, but likewise only in card mode, so a `render` a host
|
|
631
|
+
passes to every widget never changes a KPI. `byKey` maps each `dataPoint` to
|
|
632
|
+
its resolved item; the first item wins when two share one. You can also
|
|
633
|
+
register a `"KPI_CARD:card"` template. `KpiSparkline` accepts
|
|
634
|
+
`width`, `height`, `area` (fill under the line) and `scale: "minmax" | "zero"`.
|
|
635
|
+
|
|
636
|
+
### Filter options from another form
|
|
637
|
+
|
|
638
|
+
```tsx
|
|
639
|
+
<FilterPanelWidget
|
|
640
|
+
configurations={{ filters: [{
|
|
641
|
+
key: "scope", field: "Assignment", type: "select",
|
|
642
|
+
optionsSource: {
|
|
643
|
+
dataSource: OPEN_MANDATES, // e.g. a formSource on Assignment with Status IN Open
|
|
644
|
+
labelField: "Assignment Title", valueField: "Assignment Title",
|
|
645
|
+
sort: "asc", allLabel: "Firm-wide",
|
|
646
|
+
},
|
|
647
|
+
}] }}
|
|
648
|
+
onFiltersChange={(filters) => setScopeFilters(filters)} // → other widgets' widgetFilters
|
|
649
|
+
renderFilter={({ options, value, setValue, loading }) => <MySelect … />}
|
|
650
|
+
/>
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
- **Fetch:** the options come from one request: a distinct-values (composite terms) aggregation over the **whole index**, with the source's own filters applied by Elasticsearch. It is not built from a page of rows. The cap is the composite size (50,000 distinct values, or `displayRowLimit`). Declare `labelField` and `valueField` in the source's `fields` so text fields aggregate on `.keyword`. A source that already has a `pivot` is used as is. The panel refetches only when `optionsSource.dataSource` changes.
|
|
654
|
+
- **Where the filter runs:** the emitted filter is applied in the target widget's ES query, so over the whole index. Only automation-row and legacy widget-id sources, which have no query, filter their loaded rows.
|
|
655
|
+
- **URL values:** values restored from URL params (strings) match numeric options, and the emitted filter carries the option's own typed value.
|
|
656
|
+
- **Output:** choosing a value emits `[{ field, operator: "IN", value: [v] }]`; "All" emits `[]`.
|
|
657
|
+
- **Lookup targets:** when `field` is a FORM_SEARCH lookup on the target form, the filter matches the lookup's `.view` value. So `valueField` must be the field named as the lookup's **ViewLabel**.
|
|
658
|
+
- **Without a slot:** the panel renders a plain `<select>` for these controls.
|
|
659
|
+
|
|
548
660
|
---
|
|
549
661
|
|
|
550
662
|
## 5. Controlling the UI
|
|
@@ -600,9 +712,10 @@ Never re-derive values from `vm.raw`.
|
|
|
600
712
|
|
|
601
713
|
| Family | Components | Type strings | `render` receives |
|
|
602
714
|
| --- | --- | --- | --- |
|
|
603
|
-
| KPI | `KpiWidget` | `KPI` `KPI_CARD` `KPI_CARD_V2` | templates get `{ vm, index }` |
|
|
715
|
+
| KPI | `KpiWidget` | `KPI` `KPI_CARD` `KPI_CARD_V2` | templates get `{ vm, index }`; `render` / `"KPI_CARD:card"` get `{ vms, byKey, roles, state, tokens, config }` |
|
|
604
716
|
| Charts | `BarChartWidget` `LineChartWidget` `AreaChartWidget` `PieChartWidget` `DoughnutChartWidget` `ScatterChartWidget` | `BAR(_CHART)` `LINE(_CHART)` `AREA_CHART` `PIE(_CHART)` `DOUGHNUT(_CHART)` `SCATTER_CHART` | `{ rows, xKey, series, colorFor, toLabel, template, config, tokens, meta }` |
|
|
605
|
-
| Ranking | `RankingBarWidget` | `RANKING_BAR` | `{ rows: {rank,label,value,ratio,raw}[], max, … }` |
|
|
717
|
+
| Ranking | `RankingBarWidget` | `RANKING_BAR` | `{ rows: {rank,label,value,ratio,raw}[], max, … }` (`ranking.order: "none"` keeps row order) |
|
|
718
|
+
| Funnel | `FunnelWidget` | `FUNNEL` | `{ rows: {label,value,ratioToFirst,stepPct,isFirst,index,raw}[], first, tokens, config, toLabel, onItemClick }` |
|
|
606
719
|
| Gauge | `GaugeMeterWidget` | `GAUGE_METER` | `{ value, min, max, ratio, color, band, thresholds, rows }` |
|
|
607
720
|
| Heat map | `HeatMapWidget` | `HEAT_MAP` | `{ cells: {zone,count,color,band}[], grid }` |
|
|
608
721
|
| Tables (columns from `config.columns[]` as `{ label, dataPoint, type }`, the stored shape) | `DashboardTableWidget` `DatabaseTableWidget` `TaskTrackerTableWidget` `ChildTableWidget` `PivotGridWidget` | `TABLE` `DATABASE_TABLE` `TASK_TRACKER_TABLE` `CHILD_TABLE` `CHILD_DATABASE_TABLE` `PIVOTGRID` | `{ rows, columns, renderCell, toLabel, interactions, headerControls, getColumnOptions, enableSort, showColumnFilter, mode }` (+ `rowFields`, `columnFields`, `valueFields` for pivot) |
|
|
@@ -621,9 +734,10 @@ and HTML sanitization stay in the host app. The package never calls
|
|
|
621
734
|
`dangerouslySetInnerHTML`.
|
|
622
735
|
|
|
623
736
|
**`FilterPanelWidget` vs widget filters.** `FilterPanelWidget` is a
|
|
624
|
-
*dashboard-level* control. It is controlled (`value` / `onChange`) and
|
|
625
|
-
queries
|
|
626
|
-
|
|
737
|
+
*dashboard-level* control. It is controlled (`value` / `onChange`), and it
|
|
738
|
+
issues no queries except one per `optionsSource`. Feed its values into other
|
|
739
|
+
widgets' `dashFilters`, or pass `onFiltersChange` output to their
|
|
740
|
+
`widgetFilters`. A widget filter (§3) narrows only its own widget.
|
|
627
741
|
|
|
628
742
|
---
|
|
629
743
|
|
|
@@ -726,6 +840,8 @@ same rules ZAI-Dashboard applies:
|
|
|
726
840
|
| `dateRangeFor(token)`, `dashboardDefaultDateToken(ws)` | date presets, and the DATE_FILTER widget's default |
|
|
727
841
|
| `cdateFiltersFor(w, ws)` | the per-widget `cdateFilters` (the fields a date range lands on) |
|
|
728
842
|
| `buildDashboardTree(elements, isAdmin)` | `getDashboards()` → a sorted tree (`ChildReportIDs` nest), with hidden/mobile-only removed |
|
|
843
|
+
| `canViewDashboard(d, { isAdmin, groupId })` | ZAI-Dashboard's view rule: admins see all; otherwise `d.permissions` must hold `VIEW_PERMISSION` / `EDIT_PERMISSION` (`HideInMobileApp` needs `EDIT_PERMISSION`); without `permissions`, `ViewPerm` (group ids, `-1` = everyone) is checked |
|
|
844
|
+
| `visibleDashboardTree(elements, viewer)` | `canViewDashboard` filter + `buildDashboardTree`: the sidebar of an app |
|
|
729
845
|
|
|
730
846
|
```tsx
|
|
731
847
|
const w = normalizeWidget(stored);
|
|
@@ -800,6 +916,7 @@ writes widget code. Without it, Claude tends to write its own data fetching.
|
|
|
800
916
|
- **Live backend:** `zvolv-insights` reaches the live API through `createZvolvRuntime` (workspace init and login verified). Check real dashboards against ZAI-Dashboard before relying on the numbers.
|
|
801
917
|
- **Tables fetch up to `rowLimit` rows (default 1000).** There is no server pagination yet. Server-side search, filters and sort run over the full dataset, but only the first `rowLimit` matches are shown.
|
|
802
918
|
- **Column-filter options** come from the rows loaded so far. They accumulate across fetches, so options do not vanish once one is ticked. They are not a full distinct-values query.
|
|
919
|
+
- **A TABLE widget returns only its columns.** When `configuration.columns` is non-empty, the query asks Elasticsearch for `_source: ["id", ...columns.map(c => c.dataPoint)]` (`["uuid", …]` for tasks). A field that is not a column is not in the rows, even if it is in `dataSource.fields`. Store every field a host reads or writes back as a column (`hidden: true` hides it from the default table), or leave `columns` empty to get every field.
|
|
803
920
|
- **Widget filters are AND-ed.** For OR logic, pass a host ZFilters payload through `localFilters`.
|
|
804
921
|
- **0.0.2 query features need server and host support.** `interval` grouping, `dateDiff` metrics, percentiles and AND-ed per-metric filters produce ES shapes that the server's aggregation flattener and ZAI-Dashboard must also handle. See `MIRROR_NOTES.md`.
|
|
805
922
|
- **Calendar / MeetingScheduler are simplified.** There is no drag-to-resize, month-grid layout or virtual scrolling. For full parity, wrap FullCalendar in `render`.
|
|
@@ -972,3 +1089,64 @@ Platform rules these helpers follow:
|
|
|
972
1089
|
- **Do not use `AVG`**: the platform lists it but does not implement it. Every write to the form then fails with a 502. Write the average out instead, e.g. `DIVI(ADD(A, ADD(B, C)), 3)`. `ADD`, `SUB`, `MUL`, `DIVI` and `IF` are verified working.
|
|
973
1090
|
- The `create_form` validator only accepts formulas with **bare, single-word labels**, e.g. `MUL(Quantity, Rate)`. It rejects `{Label}` and labels containing spaces, even though the KB documents `{Label}`. The engine evaluates both styles.
|
|
974
1091
|
- Row height is 21px. A tile of `h` rows is `h × 21 − 16` px tall, so size form tiles to their fields; forms scroll inside the tile with the footer pinned.
|
|
1092
|
+
|
|
1093
|
+
---
|
|
1094
|
+
|
|
1095
|
+
## 12. Apps on stored dashboards: pages, access, writes
|
|
1096
|
+
|
|
1097
|
+
A custom app (e.g. a zapp app at `/v6/<app>/zapp/<id>`) can use the zapp's dashboards as its pages.
|
|
1098
|
+
The pattern below is what the `zvolv-zapp-app` skill's template does.
|
|
1099
|
+
|
|
1100
|
+
### Pages = the dashboards the user may open
|
|
1101
|
+
|
|
1102
|
+
```ts
|
|
1103
|
+
const res = await client.workflow.getDashboards(String(zappId));
|
|
1104
|
+
const elements = res?.data?.elements ?? res?.elements ?? [];
|
|
1105
|
+
const tree = visibleDashboardTree(elements, { isAdmin, groupId }); // sidebar, in SequenceID order, titles from Zvolv
|
|
1106
|
+
```
|
|
1107
|
+
|
|
1108
|
+
- Tag each dashboard's Description (e.g. `app:screen:orders`) to pick the screen that draws it; draw an
|
|
1109
|
+
untagged one generically with `normalizeWidget` + `resolveDashboardLayout` + `<ZvolvWidget>`.
|
|
1110
|
+
- Edit rights on the zapp: `client.workflow.checkPermissions(zappId, ["EDIT_WORKFLOW_TYPE"])` returns the
|
|
1111
|
+
granted ops (ZAI-Dashboard's `hasDashboardEditAccess`).
|
|
1112
|
+
- `isAdmin`: the signed-in user's `profile.role_details.role === "admin"` (decrypt the session first:
|
|
1113
|
+
`decryptAuthObject(client.auth.userInstance)`). `groupId`: `userGroupId` on the same object.
|
|
1114
|
+
|
|
1115
|
+
### Load per page, refresh per widget
|
|
1116
|
+
|
|
1117
|
+
On each visit to a page, call `getDashboard(dashId, zappId)` once and run only that page's widgets
|
|
1118
|
+
(`useWidgetData({ widgetId, widgetType, dataSource, configurations })` per widget, from
|
|
1119
|
+
`normalizeWidget`). Nothing is fetched for pages that are not open. A card's refresh button calls that
|
|
1120
|
+
widget's `refresh()`; nothing else refetches.
|
|
1121
|
+
|
|
1122
|
+
### Writing records
|
|
1123
|
+
|
|
1124
|
+
The submission engine `<ZvolvForm>` uses is exported for in-place edits (a status select, a drag to
|
|
1125
|
+
another stage, an owner pick):
|
|
1126
|
+
|
|
1127
|
+
```ts
|
|
1128
|
+
import { fetchForm, fetchSubmission, updateSubmission, createSubmission, submissionIdOf } from "@zestl/zvolv-widgets";
|
|
1129
|
+
|
|
1130
|
+
// update a few fields of one record, keeping every other stored value
|
|
1131
|
+
const { form, values } = await fetchSubmission(client, row.id); // row.id = the submission id
|
|
1132
|
+
const def = form ?? (await fetchForm(client, formId));
|
|
1133
|
+
const byId = { ...values };
|
|
1134
|
+
for (const [label, v] of Object.entries(patch)) byId[def.elements.find((e) => e.label === label)!.elementId] = v;
|
|
1135
|
+
await updateSubmission(client, def, row.id, def.elements, byId);
|
|
1136
|
+
|
|
1137
|
+
// create one
|
|
1138
|
+
const res = await createSubmission(client, def, def.elements, { [elementIdOf("Status")]: "Open" });
|
|
1139
|
+
const id = submissionIdOf(res);
|
|
1140
|
+
```
|
|
1141
|
+
|
|
1142
|
+
| Field type | Value |
|
|
1143
|
+
| --- | --- |
|
|
1144
|
+
| text, number, date (`YYYY-MM-DD`), SPINNER | as is (the option text for SPINNER) |
|
|
1145
|
+
| CHECK_BOX | `true` / `false` |
|
|
1146
|
+
| AUTO_COMPLETE people picker | `[userId]` |
|
|
1147
|
+
| FORM_SEARCH / INPUT_TAG lookup | `{ view, select, extraLabels: [] }` |
|
|
1148
|
+
|
|
1149
|
+
- A row's identity is `row.id` (the submission id). Autonumber fields come back only if they are columns
|
|
1150
|
+
(see §10), so never key records on them.
|
|
1151
|
+
- After a write, refetch the widgets that read that form (`refresh()`), rather than patching rows locally.
|
|
1152
|
+
- The server re-evaluates formulas and enforces form permissions; show its error when a write is refused.
|
package/SKILL.md
CHANGED
|
@@ -18,6 +18,8 @@ design tokens, render props and control slots. You never write data logic.
|
|
|
18
18
|
6. **No colour/font/shadow literals** in templates or slots. Use `tokens` / `useTokens()`.
|
|
19
19
|
7. Pass stored `dataSource` / `configurations` through **untouched**. Give widgets a sized parent.
|
|
20
20
|
8. Never call `getZvolvClient()` at module scope. The runtime's `client` is a getter.
|
|
21
|
+
9. **A TABLE returns only its columns.** Non-empty `configuration.columns` limits the query to `["id", ...columns]`. Store every field you read or write back as a column (`hidden: true` to hide it), or leave `columns` empty.
|
|
22
|
+
10. **Rows are keyed by `row.id`** (the submission id). Edits go through `fetchSubmission` + `updateSubmission` (or `<ZvolvForm mode="EDIT">`), creates through `createSubmission` / `<ZvolvForm>`. Never keep edited records only in app state.
|
|
21
23
|
|
|
22
24
|
## Setup
|
|
23
25
|
|
|
@@ -123,6 +125,7 @@ For `getDashboard(dashId, zappId).elements[0].widgets`, never parse or lay out b
|
|
|
123
125
|
`dashboardFilterDefs(ws)` + `buildDashFilters(values, range)` → `dashFilters` ·
|
|
124
126
|
`cdateFiltersFor(w, ws)` → `cdateFilters` · `dateRangeFor(token)` · `buildDashboardTree(getDashboards().elements, isAdmin)`.
|
|
125
127
|
Stored table columns are `{ label, dataPoint, type }` and are read as-is.
|
|
128
|
+
Sidebar of an app: `visibleDashboardTree(getDashboards().elements, { isAdmin, groupId })` — the access rule is `canViewDashboard` (admins all; else `permissions` has VIEW/EDIT_PERMISSION; else `ViewPerm` holds the group). Load a dashboard's widgets when its page opens, not before; a card refresh = that widget's `refresh()`. Zapp edit right: `workflow.checkPermissions(zappId, ["EDIT_WORKFLOW_TYPE"])`. Full pattern: README §12.
|
|
126
129
|
|
|
127
130
|
## Authoring a data source
|
|
128
131
|
|
|
@@ -191,9 +194,10 @@ KPI templates: `standard` `trend` `comparison` `progress` `financial` `status` `
|
|
|
191
194
|
|
|
192
195
|
| Family | Components | `render` args |
|
|
193
196
|
| --- | --- | --- |
|
|
194
|
-
| KPI | `KpiWidget` | templates get `{ vm, index }`. `vm` is fully resolved; never read `vm.raw` |
|
|
197
|
+
| KPI | `KpiWidget` | templates get `{ vm, index }`. `vm` is fully resolved; never read `vm.raw`. Card: `layout: "card"` (root or `cardConfig`) + `contents[].role` (`main`/`sub`/`chip`/`series`); custom card via `renderCard` (only in card mode) |
|
|
195
198
|
| Charts | `BarChartWidget` `LineChartWidget` `AreaChartWidget` `PieChartWidget` `DoughnutChartWidget` `ScatterChartWidget` | `{ rows, xKey, series, colorFor, toLabel, template, config, tokens, meta }` |
|
|
196
|
-
| Ranking | `RankingBarWidget` | `{ rows: {rank,label,value,ratio}[], max }` |
|
|
199
|
+
| Ranking | `RankingBarWidget` | `{ rows: {rank,label,value,ratio}[], max }`; `ranking.order: "none"` keeps row order |
|
|
200
|
+
| Funnel | `FunnelWidget` (`FUNNEL`) | `{ rows: {label,value,ratioToFirst,stepPct,isFirst}[], first, tokens }`; `onItemClick(row)` |
|
|
197
201
|
| Gauge | `GaugeMeterWidget` | `{ value, min, max, ratio, color, band }` |
|
|
198
202
|
| Heat map | `HeatMapWidget` | `{ cells: {zone,count,color,band}[], grid }` |
|
|
199
203
|
| Tables | `DashboardTableWidget` `DatabaseTableWidget` `TaskTrackerTableWidget` `ChildTableWidget` `PivotGridWidget` | `{ rows, columns, renderCell, toLabel, interactions, headerControls(col), getColumnOptions(key), enableSort, showColumnFilter, mode }` |
|
|
@@ -203,6 +207,17 @@ KPI templates: `standard` `trend` `comparison` `progress` `financial` `status` `
|
|
|
203
207
|
| Display | `HtmlWidget` `RawHtmlWidget` `IframeWidget` `GoogleMapWidget` `TreeWidget` `ActionCardWidget` | see types |
|
|
204
208
|
| Containers | `TabWidget` `FormWidget` `FilterPanelWidget` `ZaiChatWidget` | see types |
|
|
205
209
|
|
|
210
|
+
Reshaping after the query (0.0.3). Use these instead of re-shaping rows in React:
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
unpivot={{ columns: STAGES, extra: { "Contacted %": "step" } }} // one wide row → { label, value } per column, in order; missing → 0
|
|
214
|
+
categoryGap={{ field: "Pipeline Stage", values: "fromForm" }} // every category in dropdown order, 0 where no bucket
|
|
215
|
+
<FunnelWidget dataSource={ONE_ROW} unpivot={{ columns: STAGES }} onItemClick={(r) => …} />
|
|
216
|
+
<FilterPanelWidget configurations={{ filters: [{ key, field: "Assignment", type: "select",
|
|
217
|
+
optionsSource: { dataSource, labelField, valueField, allLabel: "Firm-wide" } }] }}
|
|
218
|
+
onFiltersChange={(f) => setWidgetFilters(f)} /> // valueField = the lookup's ViewLabel field for FORM_SEARCH targets
|
|
219
|
+
```
|
|
220
|
+
|
|
206
221
|
**`render` is REQUIRED** for `ZaiChatWidget`, `GoogleMapWidget` and `HtmlWidget` markup. `FormWidget` renders `<ZvolvForm>` by default (see Forms). The package never calls `dangerouslySetInnerHTML`.
|
|
207
222
|
|
|
208
223
|
In chart `render`, always use `xKey` and `series[].valueField` from the args, pass axis and user values through `toLabel`, and colour with `colorFor(i, s)`.
|
|
@@ -257,6 +272,10 @@ When asked to build a dashboard that captures data:
|
|
|
257
272
|
- `width` is a percentage, placed at the element root and in `attributes.Width`.
|
|
258
273
|
- Rules go on the driving field; each target starts `hidden: true`.
|
|
259
274
|
|
|
275
|
+
## Writing records
|
|
276
|
+
|
|
277
|
+
`fetchSubmission(client, row.id)` → `updateSubmission(client, form, row.id, form.elements, valuesByElementId)` keeps every other stored value. `createSubmission(client, form, form.elements, values)` + `submissionIdOf(res)` for a create. Values: people `[userId]`, lookups `{ view, select, extraLabels: [] }`, checkbox `true/false`, dates `YYYY-MM-DD`. Refetch the widgets of that form after a write. See README §12.
|
|
278
|
+
|
|
260
279
|
## Checklist before you finish
|
|
261
280
|
|
|
262
281
|
- [ ] Every plotted, grouped, filtered, searched or sorted name exists as a `dataSource.fields` header, and series names equal the pivot titles/values.
|
|
@@ -264,3 +283,4 @@ When asked to build a dashboard that captures data:
|
|
|
264
283
|
- [ ] Controlled interaction props each have their `on*Change`.
|
|
265
284
|
- [ ] No hard-coded colours. `render` is supplied for ZaiChat, GoogleMap and Html markup, and a FormKit is provided when forms are shown.
|
|
266
285
|
- [ ] Each widget sits in a sized container, and `ChildTableWidget` is width-clamped.
|
|
286
|
+
- [ ] Every change a user makes is written with `ZvolvForm` / `updateSubmission` / `createSubmission`; no records live only in app state or browser storage.
|