@richardmcquiston01/dashboard-widgets-toolkit 0.4.1 → 0.6.0
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/README.md +3 -370
- package/dist/core.cjs +42 -9
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +2 -2
- package/dist/core.d.ts +2 -2
- package/dist/core.js +41 -10
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +42 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +41 -10
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +589 -470
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +19 -9
- package/dist/react.d.ts +19 -9
- package/dist/react.js +589 -470
- package/dist/react.js.map +1 -1
- package/dist/{scale-dWDGE1zW.d.cts → scale-D51ZGIF4.d.cts} +30 -1
- package/dist/{scale-dWDGE1zW.d.ts → scale-D51ZGIF4.d.ts} +30 -1
- package/dist/styles.css +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Dashboard Widgets Toolkit
|
|
2
2
|
|
|
3
|
-
- Author:
|
|
3
|
+
- Author: Richard McQuiston
|
|
4
4
|
- Website: https://richardmcquiston.com/
|
|
5
|
+
- Demo: https://dashboard-widgets-toolkit-demo.vercel.app/
|
|
5
6
|
|
|
6
7
|
## Overview
|
|
7
8
|
|
|
@@ -42,375 +43,7 @@ synced yet.' }`.
|
|
|
42
43
|
|
|
43
44
|
## Getting Started
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
- Node.js 18+ or Bun 1.3+ (or any modern browser bundler).
|
|
48
|
-
- React 18 or later, only if you use `/react`.
|
|
49
|
-
- TypeScript is optional; type declarations are included.
|
|
50
|
-
|
|
51
|
-
### Installation
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
bun add @richardmcquiston01/dashboard-widgets-toolkit
|
|
55
|
-
# or
|
|
56
|
-
npm install @richardmcquiston01/dashboard-widgets-toolkit
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
Both `import` and `require` work, for every entry:
|
|
60
|
-
|
|
61
|
-
```js
|
|
62
|
-
const { resolveWidgets } = require('@richardmcquiston01/dashboard-widgets-toolkit/core');
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### Usage
|
|
66
|
-
|
|
67
|
-
**Core: define widgets, register providers, resolve.** Each provider gets
|
|
68
|
-
your context and returns a payload (or throws). One failing provider never
|
|
69
|
-
breaks the dashboard: it comes back as an `error` widget with the reason.
|
|
70
|
-
|
|
71
|
-
```ts
|
|
72
|
-
import {
|
|
73
|
-
defineWidget,
|
|
74
|
-
emptyWidget,
|
|
75
|
-
resolveWidgets,
|
|
76
|
-
type WidgetContext,
|
|
77
|
-
type WidgetProviders,
|
|
78
|
-
} from '@richardmcquiston01/dashboard-widgets-toolkit';
|
|
79
|
-
|
|
80
|
-
interface ShopContext extends WidgetContext {
|
|
81
|
-
shopId: string;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const widgets = [
|
|
85
|
-
defineWidget({ key: 'revenue', title: 'Revenue', kind: 'KPI', sortOrder: 10 }),
|
|
86
|
-
defineWidget({ key: 'low-stock', title: 'Low stock', kind: 'ALERT_LIST', sortOrder: 20 }),
|
|
87
|
-
defineWidget({
|
|
88
|
-
key: 'monthly',
|
|
89
|
-
title: 'Revenue by month',
|
|
90
|
-
kind: 'GRAPH',
|
|
91
|
-
sortOrder: 30,
|
|
92
|
-
defaultSize: 'large',
|
|
93
|
-
}),
|
|
94
|
-
defineWidget({ key: 'audit', title: 'Audit log', kind: 'TABLE', roles: ['admin'] }),
|
|
95
|
-
];
|
|
96
|
-
|
|
97
|
-
const providers: WidgetProviders<ShopContext> = {
|
|
98
|
-
revenue: async ({ shopId }) => {
|
|
99
|
-
const { current, previous } = await loadRevenue(shopId); // your code
|
|
100
|
-
return { kind: 'KPI', value: current, previous, format: 'currency', label: 'Revenue' };
|
|
101
|
-
},
|
|
102
|
-
'low-stock': async ({ shopId }) => {
|
|
103
|
-
const rows = await loadLowStock(shopId);
|
|
104
|
-
return {
|
|
105
|
-
kind: 'ALERT_LIST',
|
|
106
|
-
items: rows.slice(0, 5).map((row) => ({
|
|
107
|
-
title: row.title,
|
|
108
|
-
href: row.url,
|
|
109
|
-
thumbnailUrl: row.thumbnailUrl,
|
|
110
|
-
valueLabel: `${row.quantity} left`,
|
|
111
|
-
})),
|
|
112
|
-
total: rows.length,
|
|
113
|
-
emptyText: 'Nothing low on stock.',
|
|
114
|
-
};
|
|
115
|
-
},
|
|
116
|
-
monthly: async ({ shopId }) => {
|
|
117
|
-
const months = await loadMonthly(shopId);
|
|
118
|
-
if (months.length === 0) return emptyWidget('No orders synced yet.');
|
|
119
|
-
return {
|
|
120
|
-
kind: 'GRAPH',
|
|
121
|
-
chartType: 'bar',
|
|
122
|
-
valueFormat: 'currency',
|
|
123
|
-
xLabel: 'Month',
|
|
124
|
-
series: [{ name: 'Revenue', points: months.map((m) => ({ label: m.label, value: m.revenue })) }],
|
|
125
|
-
};
|
|
126
|
-
},
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
const resolved = await resolveWidgets(widgets, providers, {
|
|
130
|
-
shopId: 'shop-1',
|
|
131
|
-
roles: ['owner'], // "audit" is skipped: it needs "admin"
|
|
132
|
-
});
|
|
133
|
-
// [{ definition, status: 'ok', data }, { definition, status: 'empty', emptyText }, ...]
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
Payloads are validated by default (turn it off with `{ validate: false }`
|
|
137
|
-
for trusted, typed providers). In a client, check JSON from your API the same
|
|
138
|
-
way:
|
|
139
|
-
|
|
140
|
-
```ts
|
|
141
|
-
import { resolvePayload, validateWidgetData } from '@richardmcquiston01/dashboard-widgets-toolkit';
|
|
142
|
-
|
|
143
|
-
const result = validateWidgetData(json, { widgetKey: 'revenue', expectedKind: 'KPI' });
|
|
144
|
-
if (!result.ok) console.warn(result.error);
|
|
145
|
-
// 'Widget "revenue" (KPI): previous must be a finite number, got string.'
|
|
146
|
-
|
|
147
|
-
const widget = resolvePayload(definition, json); // ok, empty, or error with that message
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
**React: render a dashboard with the viewer's layout.**
|
|
151
|
-
|
|
152
|
-
```tsx
|
|
153
|
-
import { useState } from 'react';
|
|
154
|
-
import {
|
|
155
|
-
parseLayout,
|
|
156
|
-
serializeLayout,
|
|
157
|
-
type ResolvedWidget,
|
|
158
|
-
} from '@richardmcquiston01/dashboard-widgets-toolkit';
|
|
159
|
-
import { Dashboard } from '@richardmcquiston01/dashboard-widgets-toolkit/react';
|
|
160
|
-
import '@richardmcquiston01/dashboard-widgets-toolkit/styles.css'; // optional
|
|
161
|
-
|
|
162
|
-
export function ShopDashboard({ widgets }: { widgets: ResolvedWidget[] }) {
|
|
163
|
-
const [layout, setLayout] = useState(() => parseLayout(localStorage.getItem('layout')));
|
|
164
|
-
return (
|
|
165
|
-
<Dashboard
|
|
166
|
-
widgets={widgets}
|
|
167
|
-
layout={layout}
|
|
168
|
-
onLayoutChange={(next) => {
|
|
169
|
-
setLayout(next);
|
|
170
|
-
localStorage.setItem('layout', serializeLayout(next));
|
|
171
|
-
}}
|
|
172
|
-
locale="en-US"
|
|
173
|
-
linkTarget="_blank"
|
|
174
|
-
onRetry={(key) => refetch(key)}
|
|
175
|
-
/>
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
`Dashboard` orders widgets by the saved layout (saved order first, new widgets
|
|
181
|
-
appended by `sortOrder`, hidden ones removed), and gives each card move
|
|
182
|
-
earlier/later, hide and minimize buttons, plus a "Hidden:" bar to bring
|
|
183
|
-
widgets back. A minimized widget leaves the grid (so it stops taking space)
|
|
184
|
-
and waits in a "Minimized:" bar until the viewer expands it. Without `onLayoutChange` it is read-only. Use `loadingWidgets`
|
|
185
|
-
for placeholders while data loads, and `WidgetGrid` for a plain grid with no
|
|
186
|
-
layout controls.
|
|
187
|
-
|
|
188
|
-
Links open in the same tab unless you set `linkTarget` (for example
|
|
189
|
-
`"_blank"`); links that open a new tab get a small external-link icon and
|
|
190
|
-
"(opens in a new tab)" for screen readers (label `opensInNewTab`).
|
|
191
|
-
|
|
192
|
-
### Examples
|
|
193
|
-
|
|
194
|
-
**Layout functions on their own** (pure; the input is never mutated):
|
|
195
|
-
|
|
196
|
-
```ts
|
|
197
|
-
import {
|
|
198
|
-
EMPTY_LAYOUT,
|
|
199
|
-
hideWidget,
|
|
200
|
-
moveWidgetUp,
|
|
201
|
-
toggleMinimized,
|
|
202
|
-
visibleWidgets,
|
|
203
|
-
} from '@richardmcquiston01/dashboard-widgets-toolkit';
|
|
204
|
-
|
|
205
|
-
const definitions = [
|
|
206
|
-
{ key: 'revenue', sortOrder: 10 },
|
|
207
|
-
{ key: 'low-stock', sortOrder: 20 },
|
|
208
|
-
{ key: 'monthly', sortOrder: 30 },
|
|
209
|
-
];
|
|
210
|
-
let layout = moveWidgetUp(definitions, EMPTY_LAYOUT, 'monthly');
|
|
211
|
-
layout = hideWidget(layout, 'low-stock');
|
|
212
|
-
layout = toggleMinimized(layout, 'revenue');
|
|
213
|
-
visibleWidgets(definitions, layout).map((d) => d.key); // ['revenue', 'monthly']
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
**KPI deltas and formatting** with an explicit locale:
|
|
217
|
-
|
|
218
|
-
```ts
|
|
219
|
-
import { formatValue, kpiDelta } from '@richardmcquiston01/dashboard-widgets-toolkit';
|
|
220
|
-
|
|
221
|
-
formatValue(1234.5, 'currency', { locale: 'en-GB', currency: 'GBP' }); // "£1,234.50"
|
|
222
|
-
kpiDelta(120, 100, { locale: 'en-US' });
|
|
223
|
-
// { percent: 20, direction: 'up', sentiment: 'good', text: '+20%', description: '+20% vs previous period' }
|
|
224
|
-
kpiDelta(120, 100, { higherIsBetter: false })?.sentiment; // 'bad' (e.g. refunds)
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
**Styling with Tailwind (or any classes).** Components keep their `dwt-*`
|
|
228
|
-
classes and append yours per slot. Skip `styles.css` entirely, or keep it and
|
|
229
|
-
override its custom properties:
|
|
230
|
-
|
|
231
|
-
```tsx
|
|
232
|
-
<Dashboard
|
|
233
|
-
widgets={widgets}
|
|
234
|
-
classNames={{ card: 'rounded-lg shadow-sm', cardTitle: 'text-sm font-semibold' }}
|
|
235
|
-
/>
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
```css
|
|
239
|
-
.dwt-dashboard {
|
|
240
|
-
--dwt-series-1: #567d62; /* your brand color for single-series charts */
|
|
241
|
-
--dwt-radius: 4px;
|
|
242
|
-
}
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
Dark mode follows the OS setting unless the page sets `data-theme="light"`;
|
|
246
|
-
`data-theme="dark"` or a `dark` class on `<html>` forces it.
|
|
247
|
-
|
|
248
|
-
**Widget width.** Give a widget a `width` to say how much of the row it
|
|
249
|
-
takes, in twelfths (like a 12-column grid): an integer from 2 to 12. A table
|
|
250
|
-
with many columns might want half the row:
|
|
251
|
-
|
|
252
|
-
```ts
|
|
253
|
-
defineWidget({ key: 'top-products', title: 'Most popular products', kind: 'TABLE', width: 8 });
|
|
254
|
-
defineWidget({ key: 'ratings', title: 'Rating breakdown', kind: 'BAR_LIST', width: 4 });
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
When any widget in a grid sets `width`, the grid becomes 12 columns
|
|
258
|
-
(`dwt-grid--twelve`) and each card spans `--dwt-width` columns. Widgets
|
|
259
|
-
without a `width` use one from their `defaultSize` (small 3, medium 4, large
|
|
260
|
-
6, full 12). Narrow grids give widgets more room: under 900px a widget gets
|
|
261
|
-
twice its width, under 560px it takes the whole row. `fill: 'width'` still
|
|
262
|
-
works, sharing the twelfths left over in a row. The validator rejects
|
|
263
|
-
anything but an integer from 2 to 12.
|
|
264
|
-
|
|
265
|
-
**Detail view.** Set `detail: true` (or `{ title, pageSize, mode }`) on a
|
|
266
|
-
definition and `Dashboard` adds a "View" eye button to the card (the title is
|
|
267
|
-
clickable too). It opens a modal dialog with search, per-column filters,
|
|
268
|
-
sortable headers and paging. Where the data comes from:
|
|
269
|
-
|
|
270
|
-
```tsx
|
|
271
|
-
<Dashboard
|
|
272
|
-
widgets={widgets}
|
|
273
|
-
loadDetail={(definition, { signal, query }) => fetchAllRows(definition.key, signal)}
|
|
274
|
-
/>
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
- `loadDetail` (close over your own context; the package never fetches)
|
|
278
|
-
returns a `DetailData` table: columns with a `key`, rows of cells with
|
|
279
|
-
`text` and an optional `value` used for sorting and filtering.
|
|
280
|
-
- Without it, a TABLE with no `footer` and a BAR_LIST show their own card data;
|
|
281
|
-
other widgets get no View button until you pass `loadDetail`.
|
|
282
|
-
- `onOpenDetail={(key) => navigate(...)}` replaces the built-in dialog so you
|
|
283
|
-
can open your own page.
|
|
284
|
-
- The pieces are exported for your own layout: `WidgetDetail` (controlled by
|
|
285
|
-
a `DetailQuery`), `WidgetDetailDialog` and `useDetailData`. In the core,
|
|
286
|
-
`queryRows` does the filtering, sorting and paging (case- and
|
|
287
|
-
accent-insensitive, numeric-aware), `serializeDetailQuery` /
|
|
288
|
-
`parseDetailQuery` keep the query in a URL, and `validateDetailData` checks
|
|
289
|
-
what your loader returns.
|
|
290
|
-
|
|
291
|
-
Server-side paging and URL deep links are next; see
|
|
292
|
-
[docs/design/detail-view.md](./docs/design/detail-view.md).
|
|
293
|
-
|
|
294
|
-
**Filling space.** By default each card is only as big as its content, so a
|
|
295
|
-
short card beside a tall one leaves a gap. Set `fill` on a widget definition
|
|
296
|
-
to have it use the free space in its grid row:
|
|
297
|
-
|
|
298
|
-
```ts
|
|
299
|
-
defineWidget({ key: 'countries', title: 'Orders by country', kind: 'BAR_LIST', fill: 'height' });
|
|
300
|
-
defineWidget({ key: 'notes', title: 'Notes', kind: 'TEXT', fill: 'width' });
|
|
301
|
-
defineWidget({ key: 'trend', title: 'Trend', kind: 'GRAPH', defaultSize: 'large', fill: 'both' });
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
| `fill` | Effect |
|
|
305
|
-
| -------- | ------------------------------------------------------------------- |
|
|
306
|
-
| `height` | Stretches the card to the height of its row (`dwt-card--fill-height`). |
|
|
307
|
-
| `width` | Widens the card to take the columns left over in its row (`dwt-card--fill-width`). |
|
|
308
|
-
| `both` | Both. |
|
|
309
|
-
|
|
310
|
-
`Dashboard` and `WidgetGrid` work out the spans from the rendered grid (so it
|
|
311
|
-
follows your CSS and the container width) and set `grid-column: span N` on
|
|
312
|
-
width-filling cards; on the server, and before the first measurement, the
|
|
313
|
-
grid simply lays out normally. The same helper is exported from the core as
|
|
314
|
-
`fillColumnSpans(items, columns)` for custom layouts, and `WidgetCard` takes
|
|
315
|
-
`fill` and `columnSpan` props.
|
|
316
|
-
|
|
317
|
-
**Loading data after the page renders.** Render the dashboard first and let
|
|
318
|
-
each widget's data arrive on its own. `useWidgets` returns placeholders
|
|
319
|
-
immediately (so first paint and server rendering never wait), starts every
|
|
320
|
-
provider after mount, and replaces each card as its own data arrives:
|
|
321
|
-
|
|
322
|
-
```tsx
|
|
323
|
-
import { Dashboard, useWidgets } from '@richardmcquiston01/dashboard-widgets-toolkit/react';
|
|
324
|
-
|
|
325
|
-
const { widgets, refresh } = useWidgets(definitions, providers, context, {
|
|
326
|
-
timeoutMs: 8000, // a provider that takes longer becomes an error card
|
|
327
|
-
refreshMs: 60_000, // optional polling; skips widgets still loading
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
return <Dashboard widgets={widgets} onRetry={(key) => refresh(key)} />;
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
Providers receive an `AbortSignal` as a third argument (`(context, definition,
|
|
334
|
-
{ signal })`); pass it to `fetch` so a timeout, a refresh or leaving the page
|
|
335
|
-
stops the request. Existing two-argument providers keep working.
|
|
336
|
-
|
|
337
|
-
- `loadWhen: 'visible'` also waits until a card is near the viewport. Attach
|
|
338
|
-
the returned `gridRef` to an element around the dashboard.
|
|
339
|
-
- `cache` (your own `{ get, set }` storage: memory, `localStorage`, ...) shows
|
|
340
|
-
the last payload instantly as `stale` while a fresh load runs. Cached
|
|
341
|
-
payloads are validated again, and a failing cache never fails a widget.
|
|
342
|
-
- Keep `definitions`, `providers`, `context` and `cache` referentially stable
|
|
343
|
-
(module constants or `useMemo`); a changed value restarts loading.
|
|
344
|
-
- Not using React? `createWidgetLoader` (core) is the same logic with
|
|
345
|
-
`subscribe`/`getSnapshot`, `load`, `refresh` and `dispose`; `resolveWidget`
|
|
346
|
-
and `resolveWidgets` accept `timeoutMs`, `signal` and `cache` too.
|
|
347
|
-
|
|
348
|
-
**Charts.** `GRAPH` widgets render as inline SVG with `role="img"`, a
|
|
349
|
-
`<title>` and a `<desc>` summarizing the series (latest, high and low), a
|
|
350
|
-
legend when there are two or more series, a hover and keyboard (arrow keys)
|
|
351
|
-
tooltip, and a collapsible "View as table" with every value. Colors are CSS
|
|
352
|
-
custom properties (`--dwt-series-1` … `--dwt-series-8`) with built-in
|
|
353
|
-
defaults from a palette checked for color-vision deficiency in light and
|
|
354
|
-
dark modes; a graph may have at most eight series.
|
|
355
|
-
|
|
356
|
-
## Maker Toolkit compatibility
|
|
357
|
-
|
|
358
|
-
The kind names are upper case, as in Maker Toolkit's `WidgetViewType`, and
|
|
359
|
-
the definition mirrors its `widget` table:
|
|
360
|
-
|
|
361
|
-
| Maker Toolkit (`widget` table / API) | This package |
|
|
362
|
-
| ------------------------------------ | ------------------------------ |
|
|
363
|
-
| `widget_key` / `widgetKey` | `key` |
|
|
364
|
-
| `title`, `description` | `title`, `description` |
|
|
365
|
-
| `view_type` / `viewType` | `kind` |
|
|
366
|
-
| `sort_order` / `sortOrder` | `sortOrder` |
|
|
367
|
-
| `roles` (empty = everyone) | `roles` (absent/empty = everyone) |
|
|
368
|
-
| `active` | `active` |
|
|
369
|
-
| providers keyed by `widget_key` | `WidgetProviders` keyed by `key` |
|
|
370
|
-
|
|
371
|
-
- `TEXT` and `GAUGE` payloads are unchanged.
|
|
372
|
-
- `TABLE` columns and cells became objects (`{label, numeric?}`,
|
|
373
|
-
`{text, href?}`), and `GRAPH` became multi-series with a chart type and
|
|
374
|
-
value format. `upgradeLegacyWidgetData()` converts the older
|
|
375
|
-
`{columns: string[], rows: string[][]}` and `{points}` shapes, and
|
|
376
|
-
`fromLegacyDefinition()` maps a served definition.
|
|
377
|
-
- `KPI`, `BAR_LIST` and `ALERT_LIST` are new kinds. Add them to the
|
|
378
|
-
`WidgetViewType` enum before seeding widgets that use them.
|
|
379
|
-
- The desktop app's `dashboard-layout.ts` (order, hidden, minimized) is
|
|
380
|
-
`layout.ts` here, with the same JSON shape, so saved layouts carry over.
|
|
381
|
-
It now appends new widgets by `sortOrder` and tolerates corrupt values per
|
|
382
|
-
field.
|
|
383
|
-
|
|
384
|
-
```ts
|
|
385
|
-
import {
|
|
386
|
-
fromLegacyDefinition,
|
|
387
|
-
upgradeLegacyWidgetData,
|
|
388
|
-
validateWidgetData,
|
|
389
|
-
} from '@richardmcquiston01/dashboard-widgets-toolkit';
|
|
390
|
-
|
|
391
|
-
const result = validateWidgetData(upgradeLegacyWidgetData(apiPayload));
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
## Development
|
|
395
|
-
|
|
396
|
-
```bash
|
|
397
|
-
bun install
|
|
398
|
-
bun run typecheck
|
|
399
|
-
bun run lint
|
|
400
|
-
bun run format:check
|
|
401
|
-
bun run test
|
|
402
|
-
bun run build # dist/: ESM (.js), CommonJS (.cjs), .d.ts/.d.cts, styles.css
|
|
403
|
-
```
|
|
404
|
-
|
|
405
|
-
Branches: `dev` is the integration branch. Branch features off `dev` and open
|
|
406
|
-
pull requests into `dev`; `dev` is merged to `main` for a release. Add a
|
|
407
|
-
changeset (`bun run changeset`) with each user-facing change.
|
|
408
|
-
|
|
409
|
-
Releases are published to npm by GitHub Actions when a `vX.Y.Z` tag matching
|
|
410
|
-
`package.json` is pushed to `main`. **The `NPM_TOKEN` repository secret (an
|
|
411
|
-
npm automation token) must be added under Settings → Secrets and variables →
|
|
412
|
-
Actions** before the first release. See [CLAUDE.md](./CLAUDE.md) for the
|
|
413
|
-
full flow.
|
|
46
|
+
See [GETTING_STARTED.md](./GETTING_STARTED.md)
|
|
414
47
|
|
|
415
48
|
## Buy Me a Coffee
|
|
416
49
|
|
package/dist/core.cjs
CHANGED
|
@@ -252,6 +252,16 @@ function resolveDetailOptions(definition) {
|
|
|
252
252
|
mode: options.mode ?? "client"
|
|
253
253
|
};
|
|
254
254
|
}
|
|
255
|
+
function resolveTableControls(definition) {
|
|
256
|
+
const setting = definition.tableControls;
|
|
257
|
+
if (setting === void 0 || setting === false) return void 0;
|
|
258
|
+
const options = setting === true ? {} : setting;
|
|
259
|
+
const resolved = {
|
|
260
|
+
search: options.search ?? true,
|
|
261
|
+
sort: options.sort ?? true
|
|
262
|
+
};
|
|
263
|
+
return resolved.search || resolved.sort ? resolved : void 0;
|
|
264
|
+
}
|
|
255
265
|
function defaultDetailQuery(pageSize = DEFAULT_DETAIL_PAGE_SIZE) {
|
|
256
266
|
return { page: 1, pageSize };
|
|
257
267
|
}
|
|
@@ -526,17 +536,19 @@ function validateDetailData(data, subject = "Detail data") {
|
|
|
526
536
|
}
|
|
527
537
|
return ok(data);
|
|
528
538
|
}
|
|
539
|
+
function tableDetailData(data) {
|
|
540
|
+
return {
|
|
541
|
+
columns: data.columns.map((column, index) => ({
|
|
542
|
+
key: `c${index}`,
|
|
543
|
+
label: column.label,
|
|
544
|
+
...column.numeric === void 0 ? {} : { numeric: column.numeric }
|
|
545
|
+
})),
|
|
546
|
+
rows: data.rows
|
|
547
|
+
};
|
|
548
|
+
}
|
|
529
549
|
function deriveDetailData(data, locale) {
|
|
530
550
|
if (data.kind === "TABLE") {
|
|
531
|
-
|
|
532
|
-
return {
|
|
533
|
-
columns: data.columns.map((column, index) => ({
|
|
534
|
-
key: `c${index}`,
|
|
535
|
-
label: column.label,
|
|
536
|
-
...column.numeric === void 0 ? {} : { numeric: column.numeric }
|
|
537
|
-
})),
|
|
538
|
-
rows: data.rows
|
|
539
|
-
};
|
|
551
|
+
return data.footer === void 0 ? tableDetailData(data) : void 0;
|
|
540
552
|
}
|
|
541
553
|
if (data.kind === "BAR_LIST") {
|
|
542
554
|
return {
|
|
@@ -1021,6 +1033,24 @@ function checkDetailOption(detail, problems) {
|
|
|
1021
1033
|
requireOneOf(detail, "mode", DETAIL_MODES, "detail", problems);
|
|
1022
1034
|
}
|
|
1023
1035
|
}
|
|
1036
|
+
function checkTableControlsOption(controls, kind, problems) {
|
|
1037
|
+
if (controls === void 0 || controls === false) return;
|
|
1038
|
+
if (typeof controls !== "boolean" && !isRecord(controls)) {
|
|
1039
|
+
problems.push(
|
|
1040
|
+
`tableControls must be true, false or an options object, got ${describeType2(controls)}.`
|
|
1041
|
+
);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
if (isRecord(controls)) {
|
|
1045
|
+
optionalBoolean(controls, "search", "tableControls", problems);
|
|
1046
|
+
optionalBoolean(controls, "sort", "tableControls", problems);
|
|
1047
|
+
}
|
|
1048
|
+
if (typeof kind === "string" && kind !== "TABLE") {
|
|
1049
|
+
problems.push(
|
|
1050
|
+
`tableControls only applies to TABLE widgets, but kind is "${kind}".`
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1024
1054
|
function formatProblems(prefix, problems) {
|
|
1025
1055
|
const shown = problems.slice(0, MAX_REPORTED_PROBLEMS);
|
|
1026
1056
|
const more = problems.length - shown.length;
|
|
@@ -1093,6 +1123,7 @@ function validateWidgetDefinition(value) {
|
|
|
1093
1123
|
);
|
|
1094
1124
|
}
|
|
1095
1125
|
checkDetailOption(value["detail"], problems);
|
|
1126
|
+
checkTableControlsOption(value["tableControls"], value["kind"], problems);
|
|
1096
1127
|
if (value["fill"] !== void 0) {
|
|
1097
1128
|
requireOneOf(value, "fill", WIDGET_FILLS, "", problems);
|
|
1098
1129
|
}
|
|
@@ -1440,6 +1471,7 @@ exports.pruneLayout = pruneLayout;
|
|
|
1440
1471
|
exports.queryRows = queryRows;
|
|
1441
1472
|
exports.resolveDetailOptions = resolveDetailOptions;
|
|
1442
1473
|
exports.resolvePayload = resolvePayload;
|
|
1474
|
+
exports.resolveTableControls = resolveTableControls;
|
|
1443
1475
|
exports.resolveWidget = resolveWidget;
|
|
1444
1476
|
exports.resolveWidgets = resolveWidgets;
|
|
1445
1477
|
exports.restoreWidget = restoreWidget;
|
|
@@ -1448,6 +1480,7 @@ exports.serializeLayout = serializeLayout;
|
|
|
1448
1480
|
exports.shareOf = shareOf;
|
|
1449
1481
|
exports.showWidget = showWidget;
|
|
1450
1482
|
exports.sortDefinitions = sortDefinitions;
|
|
1483
|
+
exports.tableDetailData = tableDetailData;
|
|
1451
1484
|
exports.toggleHidden = toggleHidden;
|
|
1452
1485
|
exports.toggleMinimized = toggleMinimized;
|
|
1453
1486
|
exports.upgradeLegacyWidgetData = upgradeLegacyWidgetData;
|