@zerotal/admin 1.3.0 → 1.5.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/CHANGELOG.md +35 -1
- package/README.md +3 -1
- package/package.json +10 -10
- package/src/Panel.ts +4 -1
- package/src/actions/Action.ts +4 -1
- package/src/actions/csv.ts +3 -1
- package/src/actions/transfer.ts +6 -1
- package/src/config.ts +6 -0
- package/src/dashboardLayout.ts +12 -2
- package/src/form/ResourceForm.ts +1 -0
- package/src/form/Section.ts +13 -3
- package/src/history.ts +4 -1
- package/src/impersonation.ts +4 -1
- package/src/media.ts +5 -1
- package/src/pages/RecordViewPage.tsx +15 -9
- package/src/pages/ResourceListPage.tsx +0 -2
- package/src/renderHooks.ts +2 -0
- package/src/roles.ts +4 -1
- package/src/savedViews.ts +10 -2
- package/src/table/Filter.ts +10 -2
- package/src/theme.ts +4 -1
- package/src/widgets/Widget.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,10 +4,44 @@ All notable changes to this package are documented here. The format is
|
|
|
4
4
|
based on [Keep a Changelog](https://keepachangelog.com/); this package
|
|
5
5
|
follows the Zerotal monorepo's unified versioning.
|
|
6
6
|
|
|
7
|
-
**Maturity: `
|
|
7
|
+
**Maturity: `stable`**
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.5.0] — 2026-08-15
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **The public surface is now an explicit decision rather than whatever happened to be
|
|
16
|
+
exported.** 36 exports are marked `@internal` and leave the compatibility promise:
|
|
17
|
+
layout plumbing (`applyLayout`, `reconcile`, `moveKey`), schema normalisation
|
|
18
|
+
(`toFormLayout`, `toFormSections`, `flattenFields`, `flattenActions`), rule-tree
|
|
19
|
+
internals (`parseRuleTree`, `describeRuleTree`, `ruleTreeIsEmpty`), predicates
|
|
20
|
+
(`isImage`, `isUpload`, `isFormSection`), and the session/storage keys and defaults
|
|
21
|
+
(`IMPERSONATOR_KEY`, `THEME_STORAGE_KEY`, `VIEW_PARAMS`, `DEFAULT_ADMIN_CONFIG`, …).
|
|
22
|
+
|
|
23
|
+
None of these were ever meant to be API; they were exported because something else
|
|
24
|
+
needed them. Deciding that **now** matters, because after `stable` narrowing the
|
|
25
|
+
surface is itself a breaking change. The promise drops from 182 callables to 146.
|
|
26
|
+
|
|
27
|
+
- **Every remaining export is documented.** The reference gained sections for
|
|
28
|
+
configuration and middleware, the row and bulk actions the guide had not reached
|
|
29
|
+
(`restoreAction`, `forceDeleteAction`, `bulkEditAction`, `bulkRestoreAction`,
|
|
30
|
+
`bulkForceDeleteAction`, `textFilter`), impersonation, the media helpers behind
|
|
31
|
+
`mediaPicker`, import/export, history and permissions — each with its real signature
|
|
32
|
+
read off the API surface rather than paraphrased. Coverage of the promised surface
|
|
33
|
+
went from 58% to 100%.
|
|
34
|
+
|
|
35
|
+
- **Maturity is now `stable`** — the public API follows SemVer strictly for the rest of
|
|
36
|
+
the 1.x line. Every gate is closed: `@zerotal/flow` and `@zerotal/flow-ui` are stable
|
|
37
|
+
underneath it, the internal/public boundary is explicit, and the documented surface is
|
|
38
|
+
complete. The guard and ability middleware — the panel's security boundary — were
|
|
39
|
+
already covered by tests, alongside 222 tests overall.
|
|
40
|
+
|
|
41
|
+
This remains the largest surface in the monorepo at 146 callables, which is exactly
|
|
42
|
+
why the `@internal` triage came first: freezing a smaller, deliberate surface is a
|
|
43
|
+
promise worth making, where freezing everything that happened to be exported is not.
|
|
44
|
+
|
|
11
45
|
## [1.0.3] — 2026-08-07
|
|
12
46
|
|
|
13
47
|
### Changed
|
package/README.md
CHANGED
|
@@ -12,7 +12,9 @@ The default UI ships with **light + dark mode out of the box**. Styling currentl
|
|
|
12
12
|
loads Tailwind via the Play CDN (configured in `theme.ts`); swapping to a real
|
|
13
13
|
Tailwind build later only touches that one file.
|
|
14
14
|
|
|
15
|
-
> **Status:**
|
|
15
|
+
> **Status:** stable — the public API follows SemVer strictly for the rest of the 1.x
|
|
16
|
+
> line. Anything importable without an `@internal` marker is covered, and the surface is
|
|
17
|
+
> snapshotted in `api-surface.md`, which CI diffs on every change. You get navigation, list pages (search / sort /
|
|
16
18
|
> paginate), a read-only **View** page (infolists), reactive **Create / Edit**
|
|
17
19
|
> forms with validation, a **Delete** row action, and a configurable auth
|
|
18
20
|
> **guard**. The previous config-driven admin has been retired; it remains
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerotal/admin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"maturity": "
|
|
5
|
+
"maturity": "stable",
|
|
6
6
|
"private": false,
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "./src/index.ts",
|
|
@@ -31,16 +31,16 @@
|
|
|
31
31
|
"typecheck": "tsc --noEmit"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@zerotal/cache": "1.
|
|
35
|
-
"@zerotal/core": "1.
|
|
36
|
-
"@zerotal/flow": "1.
|
|
37
|
-
"@zerotal/flow-ui": "1.
|
|
38
|
-
"@zerotal/validator": "1.
|
|
34
|
+
"@zerotal/cache": "1.5.0",
|
|
35
|
+
"@zerotal/core": "1.5.0",
|
|
36
|
+
"@zerotal/flow": "1.5.0",
|
|
37
|
+
"@zerotal/flow-ui": "1.5.0",
|
|
38
|
+
"@zerotal/validator": "1.5.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@zerotal/orm": "1.
|
|
42
|
-
"@zerotal/auth": "1.
|
|
43
|
-
"@zerotal/queue": "1.
|
|
41
|
+
"@zerotal/orm": "1.5.0",
|
|
42
|
+
"@zerotal/auth": "1.5.0",
|
|
43
|
+
"@zerotal/queue": "1.5.0"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@zerotal/orm": "^1.0.0",
|
package/src/Panel.ts
CHANGED
|
@@ -47,7 +47,10 @@ import type {
|
|
|
47
47
|
export { PanelInstance } from "./PanelInstance.ts";
|
|
48
48
|
export type { ResourceClass, NavItem, NavGroup, PanelPage } from "./PanelInstance.ts";
|
|
49
49
|
|
|
50
|
-
/** The id given to the panel every application starts with.
|
|
50
|
+
/** The id given to the panel every application starts with.
|
|
51
|
+
*
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
51
54
|
export const DEFAULT_PANEL_ID = "admin";
|
|
52
55
|
|
|
53
56
|
export class Panel {
|
package/src/actions/Action.ts
CHANGED
|
@@ -366,7 +366,10 @@ export function actionGroup(actions: Action[]): ActionGroup {
|
|
|
366
366
|
/** Either a single action or a group of them — what a resource may return. */
|
|
367
367
|
export type ActionItem = Action | ActionGroup;
|
|
368
368
|
|
|
369
|
-
/** Flatten groups so callers that need plain actions (dispatch, lookup) get them.
|
|
369
|
+
/** Flatten groups so callers that need plain actions (dispatch, lookup) get them.
|
|
370
|
+
*
|
|
371
|
+
* @internal
|
|
372
|
+
*/
|
|
370
373
|
export function flattenActions(items: ActionItem[]): Action[] {
|
|
371
374
|
return items.flatMap((item) => (item instanceof ActionGroup ? item._actions : [item]));
|
|
372
375
|
}
|
package/src/actions/csv.ts
CHANGED
|
@@ -45,7 +45,7 @@ export function toCsv(rows: Record<string, unknown>[], columns: Column[]): strin
|
|
|
45
45
|
* newlines. Returns an empty array for empty input rather than a phantom row.
|
|
46
46
|
*/
|
|
47
47
|
export function parseCsv(text: string): string[][] {
|
|
48
|
-
const source = text.replace(
|
|
48
|
+
const source = text.replace(/^\uFEFF/, ""); // strip a spreadsheet's byte-order mark
|
|
49
49
|
if (source.trim() === "") return [];
|
|
50
50
|
|
|
51
51
|
const rows: string[][] = [];
|
|
@@ -102,6 +102,8 @@ export function parseCsv(text: string): string[][] {
|
|
|
102
102
|
* Matching is forgiving about the ways a label and a key differ in practice —
|
|
103
103
|
* case, spaces, underscores, hyphens — so a file exported from the panel
|
|
104
104
|
* re-imports without the user mapping a single column by hand.
|
|
105
|
+
*
|
|
106
|
+
* @internal
|
|
105
107
|
*/
|
|
106
108
|
export function guessColumnMapping(
|
|
107
109
|
headers: string[],
|
package/src/actions/transfer.ts
CHANGED
|
@@ -29,6 +29,8 @@ import { DEFAULT_PANEL_ID } from "../Panel.ts";
|
|
|
29
29
|
* A synchronous import holds a WebSocket round-trip open, so a hundred-thousand
|
|
30
30
|
* row file would look like a hang and time out half-written. Files beyond this
|
|
31
31
|
* belong on a queue; the action says so rather than trying and failing.
|
|
32
|
+
*
|
|
33
|
+
* @internal
|
|
32
34
|
*/
|
|
33
35
|
export const IMPORT_ROW_LIMIT = 2000;
|
|
34
36
|
|
|
@@ -202,7 +204,10 @@ export async function importCsv(
|
|
|
202
204
|
return result;
|
|
203
205
|
}
|
|
204
206
|
|
|
205
|
-
/** Field key a mapping select writes to, for CSV column `index`.
|
|
207
|
+
/** Field key a mapping select writes to, for CSV column `index`.
|
|
208
|
+
*
|
|
209
|
+
* @internal
|
|
210
|
+
*/
|
|
206
211
|
export const MAPPING_FIELD_PREFIX = "map_";
|
|
207
212
|
|
|
208
213
|
/**
|
package/src/config.ts
CHANGED
|
@@ -99,6 +99,12 @@ export interface AdminConfigShape {
|
|
|
99
99
|
plugins?: Record<string, boolean>;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* The raw defaults `AdminConfig()` merges onto. Apps configure the panel through
|
|
104
|
+
* that factory rather than reaching for this object.
|
|
105
|
+
*
|
|
106
|
+
* @internal
|
|
107
|
+
*/
|
|
102
108
|
export const DEFAULT_ADMIN_CONFIG: AdminConfigShape = {
|
|
103
109
|
path: "/admin",
|
|
104
110
|
brand: "Zerotal",
|
package/src/dashboardLayout.ts
CHANGED
|
@@ -39,7 +39,10 @@ export interface DashboardLayoutStore {
|
|
|
39
39
|
save(layout: DashboardLayout): Promise<void> | void;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/** An empty layout — the state before anyone has arranged anything.
|
|
42
|
+
/** An empty layout — the state before anyone has arranged anything.
|
|
43
|
+
*
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
43
46
|
export const EMPTY_LAYOUT: DashboardLayout = { order: [], hidden: [] };
|
|
44
47
|
|
|
45
48
|
/**
|
|
@@ -49,6 +52,8 @@ export const EMPTY_LAYOUT: DashboardLayout = { order: [], hidden: [] };
|
|
|
49
52
|
* only says how to arrange them. So a widget added since the layout was saved
|
|
50
53
|
* still appears (at the end, where it is noticeable rather than lost), and a
|
|
51
54
|
* widget since removed does not resurrect because a stale key mentions it.
|
|
55
|
+
*
|
|
56
|
+
* @internal
|
|
52
57
|
*/
|
|
53
58
|
export function applyLayout<T>(
|
|
54
59
|
widgets: T[],
|
|
@@ -75,7 +80,10 @@ export function applyLayout<T>(
|
|
|
75
80
|
};
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
/** Move one key up or down, returning the new order.
|
|
83
|
+
/** Move one key up or down, returning the new order.
|
|
84
|
+
*
|
|
85
|
+
* @internal
|
|
86
|
+
*/
|
|
79
87
|
export function moveKey(order: string[], key: string, direction: -1 | 1): string[] {
|
|
80
88
|
const from = order.indexOf(key);
|
|
81
89
|
if (from === -1) return order;
|
|
@@ -92,6 +100,8 @@ export function moveKey(order: string[], key: string, direction: -1 | 1): string
|
|
|
92
100
|
*
|
|
93
101
|
* A layout saved when there were three widgets cannot reorder a fourth, so the
|
|
94
102
|
* order is reconciled against the current keys before anything is moved.
|
|
103
|
+
*
|
|
104
|
+
* @internal
|
|
95
105
|
*/
|
|
96
106
|
export function reconcile(layout: DashboardLayout | null, keys: string[]): DashboardLayout {
|
|
97
107
|
const known = new Set(keys);
|
package/src/form/ResourceForm.ts
CHANGED
package/src/form/Section.ts
CHANGED
|
@@ -271,11 +271,15 @@ export function formSection(heading?: string): FormSection {
|
|
|
271
271
|
return new FormSection(heading);
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
/** @internal */
|
|
274
275
|
export function isFormSection(c: FormComponent): c is FormSection {
|
|
275
276
|
return c instanceof FormSection;
|
|
276
277
|
}
|
|
277
278
|
|
|
278
|
-
/** Collect every field across sections, tab groups, + loose fields, in order.
|
|
279
|
+
/** Collect every field across sections, tab groups, + loose fields, in order.
|
|
280
|
+
*
|
|
281
|
+
* @internal
|
|
282
|
+
*/
|
|
279
283
|
export function flattenFields(components: FormComponent[]): Field[] {
|
|
280
284
|
const out: Field[] = [];
|
|
281
285
|
for (const c of components) {
|
|
@@ -295,7 +299,10 @@ export function flattenFields(components: FormComponent[]): Field[] {
|
|
|
295
299
|
return out;
|
|
296
300
|
}
|
|
297
301
|
|
|
298
|
-
/** Normalize components into sections, wrapping loose fields in a default section.
|
|
302
|
+
/** Normalize components into sections, wrapping loose fields in a default section.
|
|
303
|
+
*
|
|
304
|
+
* @internal
|
|
305
|
+
*/
|
|
299
306
|
export function toFormSections(components: FormComponent[]): FormSection[] {
|
|
300
307
|
const sections: FormSection[] = [];
|
|
301
308
|
let loose: Field[] = [];
|
|
@@ -326,7 +333,10 @@ export type FormBlock =
|
|
|
326
333
|
| { kind: "callout"; callout: Callout }
|
|
327
334
|
| { kind: "prime"; prime: Prime };
|
|
328
335
|
|
|
329
|
-
/** Normalize the schema into ordered layout blocks (loose fields → a section).
|
|
336
|
+
/** Normalize the schema into ordered layout blocks (loose fields → a section).
|
|
337
|
+
*
|
|
338
|
+
* @internal
|
|
339
|
+
*/
|
|
330
340
|
export function toFormLayout(components: FormComponent[]): FormBlock[] {
|
|
331
341
|
const blocks: FormBlock[] = [];
|
|
332
342
|
let loose: Field[] = [];
|
package/src/history.ts
CHANGED
|
@@ -136,7 +136,10 @@ function diff(
|
|
|
136
136
|
return changes;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
/** The values that would put a record back to how it was before an entry.
|
|
139
|
+
/** The values that would put a record back to how it was before an entry.
|
|
140
|
+
*
|
|
141
|
+
* @internal
|
|
142
|
+
*/
|
|
140
143
|
export function revertPayload(entry: HistoryEntry): Record<string, unknown> {
|
|
141
144
|
const payload: Record<string, unknown> = {};
|
|
142
145
|
for (const change of entry.changes) payload[change.field] = change.from;
|
package/src/impersonation.ts
CHANGED
|
@@ -19,7 +19,10 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { frameworkLog } from "@zerotal/core/logger";
|
|
21
21
|
|
|
22
|
-
/** Session key holding the original user's id while impersonating.
|
|
22
|
+
/** Session key holding the original user's id while impersonating.
|
|
23
|
+
*
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
23
26
|
export const IMPERSONATOR_KEY = "admin.impersonator";
|
|
24
27
|
|
|
25
28
|
/** The session surface this needs — kept structural so `@zerotal/session` stays optional. */
|
package/src/media.ts
CHANGED
|
@@ -64,6 +64,7 @@ export interface MediaProvider {
|
|
|
64
64
|
/** Image types the picker shows a thumbnail for rather than an icon. */
|
|
65
65
|
const IMAGE_TYPES = /^image\/(png|jpe?g|gif|webp|avif|svg\+xml)$/i;
|
|
66
66
|
|
|
67
|
+
/** @internal */
|
|
67
68
|
export function isImage(item: MediaItem): boolean {
|
|
68
69
|
return IMAGE_TYPES.test(item.mime);
|
|
69
70
|
}
|
|
@@ -203,7 +204,10 @@ export interface UploadedFileLike {
|
|
|
203
204
|
store(directory: string, disk?: string, filename?: string): Promise<string>;
|
|
204
205
|
}
|
|
205
206
|
|
|
206
|
-
/** Whether a bound value is an upload rather than an already-stored path.
|
|
207
|
+
/** Whether a bound value is an upload rather than an already-stored path.
|
|
208
|
+
*
|
|
209
|
+
* @internal
|
|
210
|
+
*/
|
|
207
211
|
export function isUpload(value: unknown): value is UploadedFileLike {
|
|
208
212
|
return (
|
|
209
213
|
value != null &&
|
|
@@ -82,6 +82,19 @@ const COLS_CLASS = [
|
|
|
82
82
|
];
|
|
83
83
|
const SPAN_CLASS = ["", "", "sm:col-span-2", "sm:col-span-3", "sm:col-span-4"];
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* The pivot object a BelongsToMany relation method returns when called without
|
|
87
|
+
* eager-loading. Reached by name off a model instance, so the methods are
|
|
88
|
+
* optional: the page checks each one before calling it, and a relation that is
|
|
89
|
+
* not many-to-many has none of them.
|
|
90
|
+
*/
|
|
91
|
+
interface PivotProxy {
|
|
92
|
+
attach?: (id: unknown) => unknown;
|
|
93
|
+
detach?: (id: unknown) => unknown;
|
|
94
|
+
get?: () => unknown;
|
|
95
|
+
all?: () => unknown;
|
|
96
|
+
}
|
|
97
|
+
|
|
85
98
|
export class RecordViewPage extends Component {
|
|
86
99
|
static layout = AdminLayout;
|
|
87
100
|
/** Set by each generated subclass. */
|
|
@@ -459,9 +472,7 @@ export class RecordViewPage extends Component {
|
|
|
459
472
|
@expose attachDraft: Record<string, string> = {};
|
|
460
473
|
|
|
461
474
|
/** Resolve a parent model *instance* and its relation object by method name. */
|
|
462
|
-
private async _relation(
|
|
463
|
-
relationName: string,
|
|
464
|
-
): Promise<{ attach?: Function; detach?: Function; get?: Function; all?: Function } | null> {
|
|
475
|
+
private async _relation(relationName: string): Promise<PivotProxy | null> {
|
|
465
476
|
const model = this._resource.model as unknown as {
|
|
466
477
|
find?: (id: unknown) => Promise<Record<string, unknown> | null>;
|
|
467
478
|
};
|
|
@@ -469,12 +480,7 @@ export class RecordViewPage extends Component {
|
|
|
469
480
|
const parent = (await model.find(this.recordId)) as Record<string, unknown> | null;
|
|
470
481
|
const fn = parent && (parent as Record<string, unknown>)[relationName];
|
|
471
482
|
if (typeof fn !== "function") return null;
|
|
472
|
-
return (fn as () => unknown).call(parent) as
|
|
473
|
-
attach?: Function;
|
|
474
|
-
detach?: Function;
|
|
475
|
-
get?: Function;
|
|
476
|
-
all?: Function;
|
|
477
|
-
};
|
|
483
|
+
return (fn as () => unknown).call(parent) as PivotProxy;
|
|
478
484
|
}
|
|
479
485
|
|
|
480
486
|
/** Load the rows currently attached through a BelongsToMany relation. */
|
|
@@ -22,7 +22,6 @@ import type { ResourceClass } from "../Panel.ts";
|
|
|
22
22
|
import { Panel } from "../Panel.ts";
|
|
23
23
|
import type { PanelInstance } from "../PanelInstance.ts";
|
|
24
24
|
import type { Column, BadgeTone } from "../table/Column.ts";
|
|
25
|
-
import type { Group } from "../table/Group.ts";
|
|
26
25
|
import type { Filter, QueryRule } from "../table/Filter.ts";
|
|
27
26
|
import {
|
|
28
27
|
parseRuleTree,
|
|
@@ -1824,7 +1823,6 @@ export class ResourceListPage extends Component {
|
|
|
1824
1823
|
|
|
1825
1824
|
override async render(): Promise<HtmlNode> {
|
|
1826
1825
|
const R = this._resource;
|
|
1827
|
-
const base = this._panel.base();
|
|
1828
1826
|
const currentPage = Math.max(1, parseInt(this.page, 10) || 1);
|
|
1829
1827
|
|
|
1830
1828
|
// Resolve the active tab (defaults to the first) and its query scope.
|
package/src/renderHooks.ts
CHANGED
|
@@ -68,6 +68,8 @@ export type RenderHook = (context: RenderHookContext) => HtmlNode | string | nul
|
|
|
68
68
|
*
|
|
69
69
|
* A hook is decoration: it must not be able to take down the page it decorates,
|
|
70
70
|
* so a throwing hook is logged and skipped rather than propagated.
|
|
71
|
+
*
|
|
72
|
+
* @internal
|
|
71
73
|
*/
|
|
72
74
|
export function resolveRenderHooks(
|
|
73
75
|
hooks: RenderHook[],
|
package/src/roles.ts
CHANGED
|
@@ -151,7 +151,10 @@ export function panelPermissions(panel: PanelInstance): Permission[] {
|
|
|
151
151
|
return out;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
/** Group a flat permission list for rendering, preserving order.
|
|
154
|
+
/** Group a flat permission list for rendering, preserving order.
|
|
155
|
+
*
|
|
156
|
+
* @internal
|
|
157
|
+
*/
|
|
155
158
|
export function groupPermissions(
|
|
156
159
|
permissions: Permission[],
|
|
157
160
|
): { group: string; items: Permission[] }[] {
|
package/src/savedViews.ts
CHANGED
|
@@ -48,6 +48,8 @@ export interface SavedViewProvider {
|
|
|
48
48
|
* Deliberately explicit rather than "everything in the URL": a saved view should
|
|
49
49
|
* restore how the list was *shaped*, not which page of it happened to be open,
|
|
50
50
|
* so `page` is excluded and a restored view always starts at the top.
|
|
51
|
+
*
|
|
52
|
+
* @internal
|
|
51
53
|
*/
|
|
52
54
|
export const VIEW_PARAMS = [
|
|
53
55
|
"search",
|
|
@@ -62,7 +64,10 @@ export const VIEW_PARAMS = [
|
|
|
62
64
|
"group",
|
|
63
65
|
] as const;
|
|
64
66
|
|
|
65
|
-
/** Reduce a full query string to just the parts a view restores.
|
|
67
|
+
/** Reduce a full query string to just the parts a view restores.
|
|
68
|
+
*
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
66
71
|
export function viewQuery(params: URLSearchParams | string): string {
|
|
67
72
|
const source = typeof params === "string" ? new URLSearchParams(params) : params;
|
|
68
73
|
const out = new URLSearchParams();
|
|
@@ -73,7 +78,10 @@ export function viewQuery(params: URLSearchParams | string): string {
|
|
|
73
78
|
return out.toString();
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
/** Whether a view matches the list's current state — used to mark the active one.
|
|
81
|
+
/** Whether a view matches the list's current state — used to mark the active one.
|
|
82
|
+
*
|
|
83
|
+
* @internal
|
|
84
|
+
*/
|
|
77
85
|
export function viewIsActive(view: SavedView, current: URLSearchParams | string): boolean {
|
|
78
86
|
return viewQuery(view.query) === viewQuery(current);
|
|
79
87
|
}
|
package/src/table/Filter.ts
CHANGED
|
@@ -22,7 +22,10 @@ export type QueryRule =
|
|
|
22
22
|
| { type: "rule"; constraint: string; operator: string; value?: string }
|
|
23
23
|
| { type: "group"; operator: Conjunction; rules: QueryRule[] };
|
|
24
24
|
|
|
25
|
-
/** Parse the JSON a query-builder filter stores in the URL. Invalid input filters nothing.
|
|
25
|
+
/** Parse the JSON a query-builder filter stores in the URL. Invalid input filters nothing.
|
|
26
|
+
*
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
26
29
|
export function parseRuleTree(value: string): QueryRule | null {
|
|
27
30
|
if (!value) return null;
|
|
28
31
|
try {
|
|
@@ -48,6 +51,8 @@ function isRule(node: unknown): node is QueryRule {
|
|
|
48
51
|
* "Advanced filter: 3 rules" tells someone their list is narrowed and by roughly
|
|
49
52
|
* how much; the builder itself shows the detail. One rule gets named outright,
|
|
50
53
|
* since that is the common case and the label fits.
|
|
54
|
+
*
|
|
55
|
+
* @internal
|
|
51
56
|
*/
|
|
52
57
|
export function describeRuleTree(
|
|
53
58
|
node: QueryRule | null,
|
|
@@ -73,7 +78,10 @@ export function describeRuleTree(
|
|
|
73
78
|
return `${rules.length} rules`;
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
/** Does this tree actually constrain anything? An empty group is a no-op.
|
|
81
|
+
/** Does this tree actually constrain anything? An empty group is a no-op.
|
|
82
|
+
*
|
|
83
|
+
* @internal
|
|
84
|
+
*/
|
|
77
85
|
export function ruleTreeIsEmpty(node: QueryRule | null): boolean {
|
|
78
86
|
if (!node) return true;
|
|
79
87
|
if (node.type === "rule") return false;
|
package/src/theme.ts
CHANGED
|
@@ -20,7 +20,10 @@ import {
|
|
|
20
20
|
THEME_TOGGLE_SCRIPT as FLOW_THEME_TOGGLE_SCRIPT,
|
|
21
21
|
} from "@zerotal/flow-ui";
|
|
22
22
|
|
|
23
|
-
/** localStorage key the toggle and the no-flash script share.
|
|
23
|
+
/** localStorage key the toggle and the no-flash script share.
|
|
24
|
+
*
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
24
27
|
export const THEME_STORAGE_KEY = FLOW_THEME_STORAGE_KEY;
|
|
25
28
|
|
|
26
29
|
/** Client helpers (theme toggle + copy-to-clipboard), eval-free. */
|
package/src/widgets/Widget.ts
CHANGED
|
@@ -226,6 +226,8 @@ export type DashboardWidget = StatsWidget | ChartWidget | TableWidget;
|
|
|
226
226
|
/**
|
|
227
227
|
* The shortest poll interval among these widgets, or `undefined` when none
|
|
228
228
|
* polls. The dashboard polls as one unit, so the keenest widget sets the pace.
|
|
229
|
+
*
|
|
230
|
+
* @internal
|
|
229
231
|
*/
|
|
230
232
|
export function widgetPollInterval(widgets: DashboardWidget[]): string | undefined {
|
|
231
233
|
const intervals = widgets.map((w) => w._poll).filter((i): i is string => Boolean(i));
|