@savvifi/meridian-web-react 0.2.0 → 0.3.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/package.json +3 -3
- package/src/index.d.ts +1 -0
- package/src/index.js +1 -0
- package/src/pagination.d.ts +42 -0
- package/src/pagination.js +169 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@savvifi/meridian-web-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "React adapter for the meridian WebRenderer seam: a kit-agnostic core (MeridianProvider, PanelRenderer, ComponentKit, reactWebRenderer) that renders meridian.ui.v1 PanelDescriptors through a swappable ComponentKit (MUI, shadcn, \u2026). The premium web renderer tier of the meridian renderer family.",
|
|
6
6
|
"publishConfig": {
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"src/**/*.d.ts"
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@savvifi/meridian-proto-ts": "^0.
|
|
27
|
-
"@savvifi/meridian-schemas": "^0.
|
|
26
|
+
"@savvifi/meridian-proto-ts": "^0.3.0",
|
|
27
|
+
"@savvifi/meridian-schemas": "^0.3.0",
|
|
28
28
|
"@bufbuild/protobuf": "2.12.1"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
package/src/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export type { ComponentKit, ShapeProps, TablePanelProps, PromptPanelProps, LroPa
|
|
|
2
2
|
export { MeridianProvider, useMeridian, useMeridianTheme, useRpcInvoker, useComponentKit, useAdhocHandler, type MeridianContextValue, type MeridianProviderProps, type ReactAdhocFactory, } from "./provider.js";
|
|
3
3
|
export { PanelRenderer } from "./panel_renderer.js";
|
|
4
4
|
export { ViewRenderer } from "./view_renderer.js";
|
|
5
|
+
export { usePagedRows, resolvePagination, buildPageRequest, readPage, PaginationMode, type PagedTable, } from "./pagination.js";
|
|
5
6
|
export { reactWebRenderer } from "./react_web_renderer.js";
|
|
6
7
|
export { htmlKit } from "./html_kit.js";
|
|
7
8
|
export { shadcnKit } from "./shadcn_kit.js";
|
package/src/index.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
export { MeridianProvider, useMeridian, useMeridianTheme, useRpcInvoker, useComponentKit, useAdhocHandler, } from "./provider.js";
|
|
8
8
|
export { PanelRenderer } from "./panel_renderer.js";
|
|
9
9
|
export { ViewRenderer } from "./view_renderer.js";
|
|
10
|
+
export { usePagedRows, resolvePagination, buildPageRequest, readPage, PaginationMode, } from "./pagination.js";
|
|
10
11
|
export { reactWebRenderer } from "./react_web_renderer.js";
|
|
11
12
|
export { htmlKit } from "./html_kit.js";
|
|
12
13
|
export { shadcnKit } from "./shadcn_kit.js";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { PaginationMode, type Pagination, type TablePanel } from "@savvifi/meridian-proto-ts/proto/table_pb.js";
|
|
2
|
+
import type { RpcInvoker } from "@savvifi/meridian-schemas/uiview";
|
|
3
|
+
export { PaginationMode } from "@savvifi/meridian-proto-ts/proto/table_pb.js";
|
|
4
|
+
type Row = Record<string, unknown>;
|
|
5
|
+
/** Effective mode (UNSPECIFIED ⇒ CLIENT) + page size (0 ⇒ default) for a panel. */
|
|
6
|
+
export declare function resolvePagination(panel: TablePanel): {
|
|
7
|
+
pagination?: Pagination;
|
|
8
|
+
mode: PaginationMode;
|
|
9
|
+
pageSize: number;
|
|
10
|
+
};
|
|
11
|
+
/** Build the `populate` request for a page. CLIENT ⇒ {}. Pure. */
|
|
12
|
+
export declare function buildPageRequest(pagination: Pagination | undefined, opts: {
|
|
13
|
+
page?: number;
|
|
14
|
+
cursor?: string;
|
|
15
|
+
}): Row;
|
|
16
|
+
/** Read a page (rows + total + next cursor) out of a `populate` response. Pure. */
|
|
17
|
+
export declare function readPage(pagination: Pagination | undefined, response: unknown, rowsField: string): {
|
|
18
|
+
rows: Row[];
|
|
19
|
+
total?: number;
|
|
20
|
+
nextCursor?: string;
|
|
21
|
+
};
|
|
22
|
+
export interface PagedTable {
|
|
23
|
+
mode: PaginationMode;
|
|
24
|
+
pageSize: number;
|
|
25
|
+
rows: Row[];
|
|
26
|
+
loading: boolean;
|
|
27
|
+
/** 0-based page index (OFFSET / CURSOR). */
|
|
28
|
+
page: number;
|
|
29
|
+
/** Total rows (OFFSET; CLIENT reports the fetched count). */
|
|
30
|
+
total?: number;
|
|
31
|
+
hasPrev: boolean;
|
|
32
|
+
hasNext: boolean;
|
|
33
|
+
goPrev: () => void;
|
|
34
|
+
goNext: () => void;
|
|
35
|
+
/** Jump to a page (OFFSET only; no-op otherwise). */
|
|
36
|
+
setPage: (page: number) => void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Fetch + page a TablePanel's rows through the invoker. CLIENT returns all rows
|
|
40
|
+
* (the kit's table paginates locally); OFFSET / CURSOR fetch one page at a time.
|
|
41
|
+
*/
|
|
42
|
+
export declare function usePagedRows(panel: TablePanel, invoker: RpcInvoker): PagedTable;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Pagination — the kit-agnostic brain that turns a meridian.ui.v1 TablePanel's
|
|
2
|
+
// `Pagination` into fetched rows + page controls, via the RpcInvoker.
|
|
3
|
+
//
|
|
4
|
+
// Every kit's Table component can drive its own pager off `usePagedRows`, so the
|
|
5
|
+
// three modes behave identically regardless of the component library:
|
|
6
|
+
// - CLIENT fetch once (no page params); return ALL rows — the kit's table
|
|
7
|
+
// paginates them locally (e.g. MUI TablePagination). Good for small lists.
|
|
8
|
+
// - OFFSET re-invoke `populate` per page with offset/limit request fields; read
|
|
9
|
+
// the total-count response for page count.
|
|
10
|
+
// - CURSOR re-invoke `populate` with a cursor request field; read the next-cursor
|
|
11
|
+
// response (empty ⇒ end). Page-at-a-time (a visited-cursor stack backs prev).
|
|
12
|
+
//
|
|
13
|
+
// The request/response field wiring is pure + separately testable
|
|
14
|
+
// (`buildPageRequest` / `readPage`); the hook is thin glue over them.
|
|
15
|
+
import { useEffect, useState } from "react";
|
|
16
|
+
import { PaginationMode, } from "@savvifi/meridian-proto-ts/proto/table_pb.js";
|
|
17
|
+
export { PaginationMode } from "@savvifi/meridian-proto-ts/proto/table_pb.js";
|
|
18
|
+
const DEFAULT_PAGE_SIZE = 20;
|
|
19
|
+
/** Follow a dotted path (e.g. "page.offset") into a value. */
|
|
20
|
+
function getNested(source, path) {
|
|
21
|
+
if (!path)
|
|
22
|
+
return undefined;
|
|
23
|
+
return path.split(".").reduce((acc, key) => {
|
|
24
|
+
if (acc && typeof acc === "object")
|
|
25
|
+
return acc[key];
|
|
26
|
+
return undefined;
|
|
27
|
+
}, source);
|
|
28
|
+
}
|
|
29
|
+
/** Set a dotted path (e.g. "page.offset") on a request object, creating parents. */
|
|
30
|
+
function setNested(target, path, value) {
|
|
31
|
+
if (!path)
|
|
32
|
+
return;
|
|
33
|
+
const keys = path.split(".");
|
|
34
|
+
let cursor = target;
|
|
35
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
36
|
+
const key = keys[i];
|
|
37
|
+
if (typeof cursor[key] !== "object" || cursor[key] === null)
|
|
38
|
+
cursor[key] = {};
|
|
39
|
+
cursor = cursor[key];
|
|
40
|
+
}
|
|
41
|
+
cursor[keys[keys.length - 1]] = value;
|
|
42
|
+
}
|
|
43
|
+
/** Effective mode (UNSPECIFIED ⇒ CLIENT) + page size (0 ⇒ default) for a panel. */
|
|
44
|
+
export function resolvePagination(panel) {
|
|
45
|
+
const pagination = panel.pagination;
|
|
46
|
+
let mode = pagination?.mode ?? PaginationMode.CLIENT;
|
|
47
|
+
if (mode === PaginationMode.UNSPECIFIED)
|
|
48
|
+
mode = PaginationMode.CLIENT;
|
|
49
|
+
const pageSize = pagination?.pageSize && pagination.pageSize > 0
|
|
50
|
+
? pagination.pageSize
|
|
51
|
+
: DEFAULT_PAGE_SIZE;
|
|
52
|
+
return { pagination, mode, pageSize };
|
|
53
|
+
}
|
|
54
|
+
/** Build the `populate` request for a page. CLIENT ⇒ {}. Pure. */
|
|
55
|
+
export function buildPageRequest(pagination, opts) {
|
|
56
|
+
const request = {};
|
|
57
|
+
if (!pagination)
|
|
58
|
+
return request;
|
|
59
|
+
if (pagination.mode === PaginationMode.OFFSET) {
|
|
60
|
+
const page = opts.page ?? 0;
|
|
61
|
+
const pageSize = pagination.pageSize > 0 ? pagination.pageSize : DEFAULT_PAGE_SIZE;
|
|
62
|
+
if (pagination.offsetRequestField)
|
|
63
|
+
setNested(request, pagination.offsetRequestField, page * pageSize);
|
|
64
|
+
if (pagination.limitRequestField)
|
|
65
|
+
setNested(request, pagination.limitRequestField, pageSize);
|
|
66
|
+
}
|
|
67
|
+
else if (pagination.mode === PaginationMode.CURSOR) {
|
|
68
|
+
if (pagination.cursorRequestField && opts.cursor)
|
|
69
|
+
setNested(request, pagination.cursorRequestField, opts.cursor);
|
|
70
|
+
}
|
|
71
|
+
return request;
|
|
72
|
+
}
|
|
73
|
+
/** Read a page (rows + total + next cursor) out of a `populate` response. Pure. */
|
|
74
|
+
export function readPage(pagination, response, rowsField) {
|
|
75
|
+
const list = getNested(response, rowsField);
|
|
76
|
+
const rows = Array.isArray(list) ? list : [];
|
|
77
|
+
let total;
|
|
78
|
+
let nextCursor;
|
|
79
|
+
if (pagination?.totalField) {
|
|
80
|
+
const value = getNested(response, pagination.totalField);
|
|
81
|
+
total = typeof value === "number" ? value : Number(value) || undefined;
|
|
82
|
+
}
|
|
83
|
+
if (pagination?.nextCursorField) {
|
|
84
|
+
const value = getNested(response, pagination.nextCursorField);
|
|
85
|
+
nextCursor = typeof value === "string" ? value : undefined;
|
|
86
|
+
}
|
|
87
|
+
return { rows, total, nextCursor };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Fetch + page a TablePanel's rows through the invoker. CLIENT returns all rows
|
|
91
|
+
* (the kit's table paginates locally); OFFSET / CURSOR fetch one page at a time.
|
|
92
|
+
*/
|
|
93
|
+
export function usePagedRows(panel, invoker) {
|
|
94
|
+
const { pagination, mode, pageSize } = resolvePagination(panel);
|
|
95
|
+
const [page, setPageState] = useState(0);
|
|
96
|
+
// Cursor visited for each page index (CURSOR mode); index 0 = "" (first page).
|
|
97
|
+
const [cursorStack, setCursorStack] = useState([""]);
|
|
98
|
+
const [rows, setRows] = useState([]);
|
|
99
|
+
const [total, setTotal] = useState(undefined);
|
|
100
|
+
const [nextCursor, setNextCursor] = useState(undefined);
|
|
101
|
+
const [loading, setLoading] = useState(Boolean(panel.populate));
|
|
102
|
+
// Reset paging when the panel identity changes.
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
setPageState(0);
|
|
105
|
+
setCursorStack([""]);
|
|
106
|
+
}, [panel]);
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (!panel.populate) {
|
|
109
|
+
setLoading(false);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
let cancelled = false;
|
|
113
|
+
setLoading(true);
|
|
114
|
+
const cursor = mode === PaginationMode.CURSOR ? cursorStack[page] ?? "" : undefined;
|
|
115
|
+
const request = mode === PaginationMode.CLIENT
|
|
116
|
+
? {}
|
|
117
|
+
: buildPageRequest(pagination, { page, cursor });
|
|
118
|
+
invoker
|
|
119
|
+
.invoke(panel.populate.service, panel.populate.method, request)
|
|
120
|
+
.then((response) => {
|
|
121
|
+
if (cancelled)
|
|
122
|
+
return;
|
|
123
|
+
const read = readPage(pagination, response, panel.rowsField);
|
|
124
|
+
setRows(read.rows);
|
|
125
|
+
setTotal(mode === PaginationMode.CLIENT ? read.rows.length : read.total);
|
|
126
|
+
setNextCursor(read.nextCursor);
|
|
127
|
+
})
|
|
128
|
+
.catch(() => {
|
|
129
|
+
if (!cancelled)
|
|
130
|
+
setRows([]);
|
|
131
|
+
})
|
|
132
|
+
.finally(() => {
|
|
133
|
+
if (!cancelled)
|
|
134
|
+
setLoading(false);
|
|
135
|
+
});
|
|
136
|
+
return () => {
|
|
137
|
+
cancelled = true;
|
|
138
|
+
};
|
|
139
|
+
// cursorStack is intentionally omitted: goNext updates it together with page,
|
|
140
|
+
// so the page change drives the refetch.
|
|
141
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
142
|
+
}, [panel, invoker, page, mode]);
|
|
143
|
+
const hasPrev = page > 0;
|
|
144
|
+
const hasNext = mode === PaginationMode.OFFSET
|
|
145
|
+
? total !== undefined
|
|
146
|
+
? (page + 1) * pageSize < total
|
|
147
|
+
: rows.length === pageSize
|
|
148
|
+
: mode === PaginationMode.CURSOR
|
|
149
|
+
? Boolean(nextCursor)
|
|
150
|
+
: false; // CLIENT: the kit's own pager decides
|
|
151
|
+
const goNext = () => {
|
|
152
|
+
if (mode === PaginationMode.CURSOR) {
|
|
153
|
+
if (!nextCursor)
|
|
154
|
+
return;
|
|
155
|
+
setCursorStack((stack) => {
|
|
156
|
+
const next = [...stack];
|
|
157
|
+
next[page + 1] = nextCursor;
|
|
158
|
+
return next;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
setPageState((current) => current + 1);
|
|
162
|
+
};
|
|
163
|
+
const goPrev = () => setPageState((current) => (current > 0 ? current - 1 : current));
|
|
164
|
+
const setPage = (target) => {
|
|
165
|
+
if (mode === PaginationMode.OFFSET)
|
|
166
|
+
setPageState(Math.max(0, target));
|
|
167
|
+
};
|
|
168
|
+
return { mode, pageSize, rows, loading, page, total, hasPrev, hasNext, goNext, goPrev, setPage };
|
|
169
|
+
}
|