@zestl/zvolv-widgets 0.0.2 → 0.0.3
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 +25 -0
- package/MIRROR_NOTES.md +10 -0
- package/README.md +118 -5
- package/SKILL.md +14 -2
- package/dist/index.cjs +4234 -3789
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +238 -9
- package/dist/index.d.ts +238 -9
- package/dist/index.js +4224 -3790
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.3
|
|
4
|
+
|
|
5
|
+
These are all client-side enhancements that run after the query; none changes
|
|
6
|
+
what is queried. Existing configs give byte-identical queries, rows and default
|
|
7
|
+
markup, which `test/compat.mjs` checks against snapshots captured from 0.0.2.
|
|
8
|
+
There is nothing for ZAI-Dashboard or the server to mirror.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **KPI card layout.**
|
|
12
|
+
- `config.layout: "card"` (or `cardConfig.layout`) renders every item inside one card, through a `"KPI_CARD:card"` template override or the default `KpiCard`.
|
|
13
|
+
- `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.
|
|
14
|
+
- `KpiContentConfig.role` (`"main" | "sub" | "chip" | "series"`) places an item in the card; `resolveKpiRoles` does this sorting.
|
|
15
|
+
- `KpiSparkline` gains `width`, `height`, `area` and `scale: "minmax" | "zero"`.
|
|
16
|
+
- **`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`.
|
|
17
|
+
- **`ranking.order: "none"`.** `RankingBarWidget` keeps the incoming row order.
|
|
18
|
+
- **`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`.
|
|
19
|
+
- **`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`.
|
|
20
|
+
- **`FilterPanelWidget` options from a data source.**
|
|
21
|
+
- `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`).
|
|
22
|
+
- Values restored from URL params match numeric options (`matchFilterOption`).
|
|
23
|
+
- `onFiltersChange(WidgetFilter[])` emits `{ field, operator: "IN", value: [v] }`, or nothing for "All".
|
|
24
|
+
- The `renderFilter` slot receives `{ control, options, value, setValue, loading, tokens }`.
|
|
25
|
+
- Pure helpers: `filterPanelToWidgetFilters`, `rowsToFilterOptions`.
|
|
26
|
+
- **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.
|
|
27
|
+
|
|
3
28
|
## 0.0.2
|
|
4
29
|
|
|
5
30
|
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
|
@@ -545,6 +545,117 @@ KPI card with 20 numbers on one form is one call. `pivot()`, `formSource()` and
|
|
|
545
545
|
`combine: "AND"`. Stored `pivot.filters` entries without that marker keep the
|
|
546
546
|
original rule: the last entry per metric wins.
|
|
547
547
|
|
|
548
|
+
### Reshaping rows after the query (0.0.3)
|
|
549
|
+
|
|
550
|
+
These run in the browser after the query returns. They never change what is
|
|
551
|
+
queried.
|
|
552
|
+
|
|
553
|
+
**One wide row → one row per category (`unpivot`).** A multi-number source
|
|
554
|
+
returns one row such as `{ Longlist: 4, Contacted: 4, … }`. Turn it into one
|
|
555
|
+
row per column, in your order:
|
|
556
|
+
|
|
557
|
+
```tsx
|
|
558
|
+
const STAGES = ["Longlist", "Contacted", "Interviewed", "Offered", "Placed"];
|
|
559
|
+
|
|
560
|
+
<RankingBarWidget
|
|
561
|
+
dataSource={FUNNEL_ONE}
|
|
562
|
+
unpivot={{ columns: STAGES, extra: { "Contacted %": "step" } }}
|
|
563
|
+
configurations={{ ranking: { order: "none" } }} // keep stage order; ratio = value / max
|
|
564
|
+
/>
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
- Output rows are `{ label, value }`. Rename the keys with `labelField` / `valueField`.
|
|
568
|
+
- A missing column gives 0.
|
|
569
|
+
- `unpivot` runs after `postOperations`, so a `derive()` column can be a stage.
|
|
570
|
+
- With no declared `series`, x and y become `labelField` and `valueField`.
|
|
571
|
+
- `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.
|
|
572
|
+
|
|
573
|
+
**All categories, in dropdown order (`categoryGap`).** ES returns a bucket only
|
|
574
|
+
for values that have data. `categoryGap` adds a zero row for every missing
|
|
575
|
+
category and orders the rows:
|
|
576
|
+
|
|
577
|
+
```tsx
|
|
578
|
+
<BarChartWidget
|
|
579
|
+
dataSource={MINI_FUNNEL}
|
|
580
|
+
categoryGap={{ field: "Pipeline Stage", values: "fromForm" }} // or an explicit list
|
|
581
|
+
widgetFilters={[{ field: "Assignment", operator: "IN", value: [title] }]}
|
|
582
|
+
render={({ rows }) => <MiniBars rows={rows} />}
|
|
583
|
+
/>
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
- `field` is the chart's argumentField.
|
|
587
|
+
- `"fromForm"` reads that field's dropdown options from the form schema, once per form; it reuses the widget's form lookup.
|
|
588
|
+
- Every series `valueField` is 0 in the added rows.
|
|
589
|
+
- Values that are not in the list are kept, after the listed ones.
|
|
590
|
+
- It runs after `postOperations` and widget filters, and before local search, column filters and sort.
|
|
591
|
+
|
|
592
|
+
### Funnel
|
|
593
|
+
|
|
594
|
+
```tsx
|
|
595
|
+
<FunnelWidget
|
|
596
|
+
dataSource={FUNNEL_ONE}
|
|
597
|
+
unpivot={{ columns: STAGES }}
|
|
598
|
+
onItemClick={(row) => setStage(row.label)}
|
|
599
|
+
/>
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
Each stage resolves to `{ label, value, ratioToFirst, stepPct, isFirst, index, raw }`:
|
|
603
|
+
- `stepPct` is value ÷ previous stage × 100. It is `null` for the first stage, and after a stage of 0.
|
|
604
|
+
- `ratioToFirst` is value ÷ first stage. It is 0 when the first stage is 0.
|
|
605
|
+
|
|
606
|
+
Stages are never re-sorted. `render` gets `{ rows, first, tokens, config, toLabel, onItemClick }`.
|
|
607
|
+
|
|
608
|
+
### One KPI card from several numbers
|
|
609
|
+
|
|
610
|
+
```tsx
|
|
611
|
+
<KpiWidget dataSource={cardSource} configurations={{ layout: "card", contents: [
|
|
612
|
+
{ dataPoint: "value", role: "main", title: "Open mandates", template: "chart", seriesPoints: P1toP12 },
|
|
613
|
+
{ dataPoint: "total", role: "sub", title: "total" },
|
|
614
|
+
{ dataPoint: "atRisk", role: "sub", title: "at risk" },
|
|
615
|
+
{ dataPoint: "P12", role: "chip", template: "trend", deltaSource: "datapoint", deltaPoint: "P8",
|
|
616
|
+
deltaMode: "absolute", subtitle: "vs 4 wk" },
|
|
617
|
+
]}} />
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
The default card puts the items in slots by `role`:
|
|
621
|
+
- `main` is the big value. Without a `main` role, it is the first item with no role.
|
|
622
|
+
- `sub` items are small inline values under it.
|
|
623
|
+
- `chip` is the change badge (its `delta`).
|
|
624
|
+
- `series` supplies the sparkline. Without it, the main item's series is used.
|
|
625
|
+
|
|
626
|
+
Card mode turns on only with `layout: "card"`, set either at the config root
|
|
627
|
+
or in `cardConfig`, where platform KPI payloads keep their items. To build the
|
|
628
|
+
card yourself, pass `renderCard={({ vms, byKey, roles, state, tokens }) => …}`;
|
|
629
|
+
`render` also works, but likewise only in card mode, so a `render` a host
|
|
630
|
+
passes to every widget never changes a KPI. `byKey` maps each `dataPoint` to
|
|
631
|
+
its resolved item; the first item wins when two share one. You can also
|
|
632
|
+
register a `"KPI_CARD:card"` template. `KpiSparkline` accepts
|
|
633
|
+
`width`, `height`, `area` (fill under the line) and `scale: "minmax" | "zero"`.
|
|
634
|
+
|
|
635
|
+
### Filter options from another form
|
|
636
|
+
|
|
637
|
+
```tsx
|
|
638
|
+
<FilterPanelWidget
|
|
639
|
+
configurations={{ filters: [{
|
|
640
|
+
key: "scope", field: "Assignment", type: "select",
|
|
641
|
+
optionsSource: {
|
|
642
|
+
dataSource: OPEN_MANDATES, // e.g. a formSource on Assignment with Status IN Open
|
|
643
|
+
labelField: "Assignment Title", valueField: "Assignment Title",
|
|
644
|
+
sort: "asc", allLabel: "Firm-wide",
|
|
645
|
+
},
|
|
646
|
+
}] }}
|
|
647
|
+
onFiltersChange={(filters) => setScopeFilters(filters)} // → other widgets' widgetFilters
|
|
648
|
+
renderFilter={({ options, value, setValue, loading }) => <MySelect … />}
|
|
649
|
+
/>
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
- **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.
|
|
653
|
+
- **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.
|
|
654
|
+
- **URL values:** values restored from URL params (strings) match numeric options, and the emitted filter carries the option's own typed value.
|
|
655
|
+
- **Output:** choosing a value emits `[{ field, operator: "IN", value: [v] }]`; "All" emits `[]`.
|
|
656
|
+
- **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**.
|
|
657
|
+
- **Without a slot:** the panel renders a plain `<select>` for these controls.
|
|
658
|
+
|
|
548
659
|
---
|
|
549
660
|
|
|
550
661
|
## 5. Controlling the UI
|
|
@@ -600,9 +711,10 @@ Never re-derive values from `vm.raw`.
|
|
|
600
711
|
|
|
601
712
|
| Family | Components | Type strings | `render` receives |
|
|
602
713
|
| --- | --- | --- | --- |
|
|
603
|
-
| KPI | `KpiWidget` | `KPI` `KPI_CARD` `KPI_CARD_V2` | templates get `{ vm, index }` |
|
|
714
|
+
| KPI | `KpiWidget` | `KPI` `KPI_CARD` `KPI_CARD_V2` | templates get `{ vm, index }`; `render` / `"KPI_CARD:card"` get `{ vms, byKey, roles, state, tokens, config }` |
|
|
604
715
|
| 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, … }` |
|
|
716
|
+
| Ranking | `RankingBarWidget` | `RANKING_BAR` | `{ rows: {rank,label,value,ratio,raw}[], max, … }` (`ranking.order: "none"` keeps row order) |
|
|
717
|
+
| Funnel | `FunnelWidget` | `FUNNEL` | `{ rows: {label,value,ratioToFirst,stepPct,isFirst,index,raw}[], first, tokens, config, toLabel, onItemClick }` |
|
|
606
718
|
| Gauge | `GaugeMeterWidget` | `GAUGE_METER` | `{ value, min, max, ratio, color, band, thresholds, rows }` |
|
|
607
719
|
| Heat map | `HeatMapWidget` | `HEAT_MAP` | `{ cells: {zone,count,color,band}[], grid }` |
|
|
608
720
|
| 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 +733,10 @@ and HTML sanitization stay in the host app. The package never calls
|
|
|
621
733
|
`dangerouslySetInnerHTML`.
|
|
622
734
|
|
|
623
735
|
**`FilterPanelWidget` vs widget filters.** `FilterPanelWidget` is a
|
|
624
|
-
*dashboard-level* control. It is controlled (`value` / `onChange`) and
|
|
625
|
-
queries
|
|
626
|
-
|
|
736
|
+
*dashboard-level* control. It is controlled (`value` / `onChange`), and it
|
|
737
|
+
issues no queries except one per `optionsSource`. Feed its values into other
|
|
738
|
+
widgets' `dashFilters`, or pass `onFiltersChange` output to their
|
|
739
|
+
`widgetFilters`. A widget filter (§3) narrows only its own widget.
|
|
627
740
|
|
|
628
741
|
---
|
|
629
742
|
|
package/SKILL.md
CHANGED
|
@@ -191,9 +191,10 @@ KPI templates: `standard` `trend` `comparison` `progress` `financial` `status` `
|
|
|
191
191
|
|
|
192
192
|
| Family | Components | `render` args |
|
|
193
193
|
| --- | --- | --- |
|
|
194
|
-
| KPI | `KpiWidget` | templates get `{ vm, index }`. `vm` is fully resolved; never read `vm.raw` |
|
|
194
|
+
| 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
195
|
| 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 }` |
|
|
196
|
+
| Ranking | `RankingBarWidget` | `{ rows: {rank,label,value,ratio}[], max }`; `ranking.order: "none"` keeps row order |
|
|
197
|
+
| Funnel | `FunnelWidget` (`FUNNEL`) | `{ rows: {label,value,ratioToFirst,stepPct,isFirst}[], first, tokens }`; `onItemClick(row)` |
|
|
197
198
|
| Gauge | `GaugeMeterWidget` | `{ value, min, max, ratio, color, band }` |
|
|
198
199
|
| Heat map | `HeatMapWidget` | `{ cells: {zone,count,color,band}[], grid }` |
|
|
199
200
|
| Tables | `DashboardTableWidget` `DatabaseTableWidget` `TaskTrackerTableWidget` `ChildTableWidget` `PivotGridWidget` | `{ rows, columns, renderCell, toLabel, interactions, headerControls(col), getColumnOptions(key), enableSort, showColumnFilter, mode }` |
|
|
@@ -203,6 +204,17 @@ KPI templates: `standard` `trend` `comparison` `progress` `financial` `status` `
|
|
|
203
204
|
| Display | `HtmlWidget` `RawHtmlWidget` `IframeWidget` `GoogleMapWidget` `TreeWidget` `ActionCardWidget` | see types |
|
|
204
205
|
| Containers | `TabWidget` `FormWidget` `FilterPanelWidget` `ZaiChatWidget` | see types |
|
|
205
206
|
|
|
207
|
+
Reshaping after the query (0.0.3). Use these instead of re-shaping rows in React:
|
|
208
|
+
|
|
209
|
+
```tsx
|
|
210
|
+
unpivot={{ columns: STAGES, extra: { "Contacted %": "step" } }} // one wide row → { label, value } per column, in order; missing → 0
|
|
211
|
+
categoryGap={{ field: "Pipeline Stage", values: "fromForm" }} // every category in dropdown order, 0 where no bucket
|
|
212
|
+
<FunnelWidget dataSource={ONE_ROW} unpivot={{ columns: STAGES }} onItemClick={(r) => …} />
|
|
213
|
+
<FilterPanelWidget configurations={{ filters: [{ key, field: "Assignment", type: "select",
|
|
214
|
+
optionsSource: { dataSource, labelField, valueField, allLabel: "Firm-wide" } }] }}
|
|
215
|
+
onFiltersChange={(f) => setWidgetFilters(f)} /> // valueField = the lookup's ViewLabel field for FORM_SEARCH targets
|
|
216
|
+
```
|
|
217
|
+
|
|
206
218
|
**`render` is REQUIRED** for `ZaiChatWidget`, `GoogleMapWidget` and `HtmlWidget` markup. `FormWidget` renders `<ZvolvForm>` by default (see Forms). The package never calls `dangerouslySetInnerHTML`.
|
|
207
219
|
|
|
208
220
|
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)`.
|