@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvifi/meridian-web-react",
3
- "version": "0.2.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.2.0",
27
- "@savvifi/meridian-schemas": "^0.2.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
+ }