@zestl/zvolv-widgets 0.0.3 → 0.0.5

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 CHANGED
@@ -1,5 +1,15 @@
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
+
3
13
  ## 0.0.3
4
14
 
5
15
  These are all client-side enhancements that run after the query; none changes
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
 
@@ -839,6 +840,8 @@ same rules ZAI-Dashboard applies:
839
840
  | `dateRangeFor(token)`, `dashboardDefaultDateToken(ws)` | date presets, and the DATE_FILTER widget's default |
840
841
  | `cdateFiltersFor(w, ws)` | the per-widget `cdateFilters` (the fields a date range lands on) |
841
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 |
842
845
 
843
846
  ```tsx
844
847
  const w = normalizeWidget(stored);
@@ -913,6 +916,7 @@ writes widget code. Without it, Claude tends to write its own data fetching.
913
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.
914
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.
915
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.
916
920
  - **Widget filters are AND-ed.** For OR logic, pass a host ZFilters payload through `localFilters`.
917
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`.
918
922
  - **Calendar / MeetingScheduler are simplified.** There is no drag-to-resize, month-grid layout or virtual scrolling. For full parity, wrap FullCalendar in `render`.
@@ -1085,3 +1089,64 @@ Platform rules these helpers follow:
1085
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.
1086
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.
1087
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
 
@@ -269,6 +272,10 @@ When asked to build a dashboard that captures data:
269
272
  - `width` is a percentage, placed at the element root and in `attributes.Width`.
270
273
  - Rules go on the driving field; each target starts `hidden: true`.
271
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
+
272
279
  ## Checklist before you finish
273
280
 
274
281
  - [ ] Every plotted, grouped, filtered, searched or sorted name exists as a `dataSource.fields` header, and series names equal the pivot titles/values.
@@ -276,3 +283,4 @@ When asked to build a dashboard that captures data:
276
283
  - [ ] Controlled interaction props each have their `on*Change`.
277
284
  - [ ] No hard-coded colours. `render` is supplied for ZaiChat, GoogleMap and Html markup, and a FormKit is provided when forms are shown.
278
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.
package/dist/index.cjs CHANGED
@@ -9334,6 +9334,20 @@ function buildDashboardTree(elements, isAdmin = false) {
9334
9334
  });
9335
9335
  return list.filter((d) => !childIds.has(String(d.ReportID)) && visible(d)).sort(seq).map(node);
9336
9336
  }
9337
+ function canViewDashboard(d, viewer = {}) {
9338
+ if (!d) return false;
9339
+ if (viewer.isAdmin) return true;
9340
+ const perms = Array.isArray(d.permissions) ? d.permissions : null;
9341
+ if (d.HideInMobileApp === "YES") return !!perms?.includes("EDIT_PERMISSION");
9342
+ if (perms) return perms.includes("VIEW_PERMISSION") || perms.includes("EDIT_PERMISSION");
9343
+ const view = Array.isArray(d.ViewPerm) ? d.ViewPerm.map(Number) : null;
9344
+ if (view && view.length && viewer.groupId != null && !view.includes(-1)) return view.includes(Number(viewer.groupId));
9345
+ return true;
9346
+ }
9347
+ function visibleDashboardTree(elements, viewer = {}) {
9348
+ const list = (Array.isArray(elements) ? elements : []).filter((d) => canViewDashboard(d, viewer));
9349
+ return buildDashboardTree(list, !!viewer.isAdmin);
9350
+ }
9337
9351
  var text = (v) => {
9338
9352
  if (v === null || v === void 0 || v === "") return "";
9339
9353
  if (Array.isArray(v)) return v.map(text).filter(Boolean).join(", ");
@@ -9762,6 +9776,7 @@ exports.buildSubmissionElements = buildSubmissionElements;
9762
9776
  exports.buildTree = buildTree;
9763
9777
  exports.buildTriggerBody = buildTriggerBody;
9764
9778
  exports.buildWidgetFilterClauses = buildWidgetFilterClauses;
9779
+ exports.canViewDashboard = canViewDashboard;
9765
9780
  exports.cascade = cascade;
9766
9781
  exports.cdateFiltersFor = cdateFiltersFor;
9767
9782
  exports.childIdsOf = childIdsOf;
@@ -9896,6 +9911,7 @@ exports.useZvolvRuntime = useZvolvRuntime;
9896
9911
  exports.usesFormFramework = usesFormFramework;
9897
9912
  exports.validateField = validateField;
9898
9913
  exports.validateForm = validateForm;
9914
+ exports.visibleDashboardTree = visibleDashboardTree;
9899
9915
  exports.when = when;
9900
9916
  exports.where = where;
9901
9917
  //# sourceMappingURL=index.cjs.map