@savvifi/meridian-aion-web 0.3.1 → 0.4.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/dist/adhoc-bridge.d.ts +8 -0
- package/dist/adhoc-bridge.js +42 -0
- package/dist/render-client.js +2 -1
- package/dist/render-server.d.ts +15 -0
- package/dist/render-server.js +57 -9
- package/package.json +5 -5
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ReactAdhocFactory } from "@savvifi/meridian-web-react";
|
|
2
|
+
/**
|
|
3
|
+
* An adhoc registry that resolves EVERY handler_id to the neutral placeholder, so
|
|
4
|
+
* any bespoke (or newly-added) detail widget degrades gracefully rather than
|
|
5
|
+
* surfacing a warning. Pass as `adhoc={neutralAdhocRegistry}` to MeridianMuiProvider
|
|
6
|
+
* on both the server and client render paths.
|
|
7
|
+
*/
|
|
8
|
+
export declare const neutralAdhocRegistry: Record<string, ReactAdhocFactory>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// The graceful-degrade bridge for bespoke detail widgets (a document file viewer,
|
|
2
|
+
// a task discussion thread, a team members panel, …) that don't yet have a
|
|
3
|
+
// purpose-built meridian panel. The projection emits these as AdhocPanels; without
|
|
4
|
+
// a host handler the kit would draw a "unsupported panel shape" WARNING — a visible
|
|
5
|
+
// layout error. This registry resolves ANY handler_id to a neutral, low-emphasis
|
|
6
|
+
// placeholder instead, so the detail page degrades cleanly until each widget gets a
|
|
7
|
+
// real renderer. Kit-neutral (plain elements + --mer-* theme vars) so this package
|
|
8
|
+
// takes no MUI dependency.
|
|
9
|
+
import { createElement } from "react";
|
|
10
|
+
/** "tasks-detail-discussion" → "Tasks Detail Discussion". */
|
|
11
|
+
function humanize(id) {
|
|
12
|
+
return id
|
|
13
|
+
.replace(/[-_.]+/g, " ")
|
|
14
|
+
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
15
|
+
.trim();
|
|
16
|
+
}
|
|
17
|
+
/** A muted placeholder card for a not-yet-ported bespoke widget — never a warning. */
|
|
18
|
+
function NeutralAdhoc({ descriptor }) {
|
|
19
|
+
const handlerId = descriptor.body.case === "adhoc" ? descriptor.body.value.handlerId : "";
|
|
20
|
+
const label = descriptor.title || humanize(handlerId) || "Section";
|
|
21
|
+
return createElement("section", {
|
|
22
|
+
className: "mer-adhoc-placeholder",
|
|
23
|
+
"data-handler": handlerId,
|
|
24
|
+
style: {
|
|
25
|
+
border: "1px solid var(--mer-border, #e0e0e0)",
|
|
26
|
+
borderRadius: 8,
|
|
27
|
+
padding: "16px 20px",
|
|
28
|
+
color: "var(--mer-fg, inherit)",
|
|
29
|
+
background: "var(--mer-surface, transparent)",
|
|
30
|
+
},
|
|
31
|
+
}, createElement("div", { style: { fontSize: 14, fontWeight: 600, marginBottom: 4 } }, label), createElement("div", { style: { fontSize: 13, opacity: 0.6 } }, "This section isn't available in this view yet."));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* An adhoc registry that resolves EVERY handler_id to the neutral placeholder, so
|
|
35
|
+
* any bespoke (or newly-added) detail widget degrades gracefully rather than
|
|
36
|
+
* surfacing a warning. Pass as `adhoc={neutralAdhocRegistry}` to MeridianMuiProvider
|
|
37
|
+
* on both the server and client render paths.
|
|
38
|
+
*/
|
|
39
|
+
export const neutralAdhocRegistry = new Proxy({}, {
|
|
40
|
+
get: () => NeutralAdhoc,
|
|
41
|
+
has: () => true,
|
|
42
|
+
});
|
package/dist/render-client.js
CHANGED
|
@@ -9,7 +9,8 @@ import { ViewDescriptorSchema } from "@savvifi/meridian-proto-ts/proto/view_pb.j
|
|
|
9
9
|
import { ViewRenderer, } from "@savvifi/meridian-web-react";
|
|
10
10
|
import { MeridianMuiProvider } from "@savvifi/meridian-mui-kit";
|
|
11
11
|
import { savviSkin } from "./savvi-skin.js";
|
|
12
|
+
import { neutralAdhocRegistry } from "./adhoc-bridge.js";
|
|
12
13
|
export function MeridianEntityView({ viewJson, initialData, invoker, mode = "light", onAction, resolveHref, }) {
|
|
13
14
|
const view = fromJson(ViewDescriptorSchema, viewJson);
|
|
14
|
-
return createElement(MeridianMuiProvider, { invoker, theme: savviSkin, mode, onAction, resolveHref }, createElement(ViewRenderer, { view, initialData }));
|
|
15
|
+
return createElement(MeridianMuiProvider, { invoker, theme: savviSkin, mode, onAction, resolveHref, adhoc: neutralAdhocRegistry }, createElement(ViewRenderer, { view, initialData }));
|
|
15
16
|
}
|
package/dist/render-server.d.ts
CHANGED
|
@@ -32,6 +32,21 @@ export interface RenderMeridianEntityViewResult {
|
|
|
32
32
|
node: ReactNode | null;
|
|
33
33
|
dehydrated: MeridianDehydratedView | null;
|
|
34
34
|
}
|
|
35
|
+
/** The command-palette seed: list compositions (as an array) + dialog compositions
|
|
36
|
+
* keyed by their id. Values are the aion @aion/ui-contracts composition shapes;
|
|
37
|
+
* typed loosely here so this seam stays free of the studio-UI type surface —
|
|
38
|
+
* the host casts at the RootContainer boundary. */
|
|
39
|
+
export interface CommandPaletteData {
|
|
40
|
+
/** Installed list compositions (carry the create-form fields). */
|
|
41
|
+
compositions: unknown[];
|
|
42
|
+
/** Installed dialog compositions, keyed by composition id. */
|
|
43
|
+
dialogCompositions: Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
/** Load the command-palette seed from the request graph. Non-fatal callers should
|
|
46
|
+
* catch and fall back to an empty seed (the palette then renders nothing). */
|
|
47
|
+
export declare function loadCommandPaletteData(input: {
|
|
48
|
+
graph: CompositionGraphClient;
|
|
49
|
+
}): Promise<CommandPaletteData>;
|
|
35
50
|
export declare function renderMeridianEntityView(input: RenderMeridianEntityViewInput): Promise<RenderMeridianEntityViewResult>;
|
|
36
51
|
/** The host-supplied transport that fetches a Layout's ViewDescriptor as proto-JSON.
|
|
37
52
|
* Abstracted so the gRPC client (Connect / grpc-js) is a host adapter, not a dep of
|
package/dist/render-server.js
CHANGED
|
@@ -12,9 +12,10 @@ import { createEntityOpsInvoker, projectEntityView, } from "@savvifi/meridian-ai
|
|
|
12
12
|
import { buildPageRequest, readPage, ViewRenderer, } from "@savvifi/meridian-web-react";
|
|
13
13
|
import { MeridianMuiProvider } from "@savvifi/meridian-mui-kit";
|
|
14
14
|
import { parseAndValidateEntityRoute } from "@aion/entity-route-runtime/entity-route/index.js";
|
|
15
|
-
import { loadInstalledEntityDetailCompositions, loadInstalledEntityListCompositions, } from "@aion/entity-route-runtime/composition/loader.js";
|
|
15
|
+
import { loadInstalledDialogCompositions, loadInstalledEntityDetailCompositions, loadInstalledEntityListCompositions, } from "@aion/entity-route-runtime/composition/loader.js";
|
|
16
16
|
import { savviSkin } from "./savvi-skin.js";
|
|
17
17
|
import { NotFoundSignal } from "./not-found.js";
|
|
18
|
+
import { neutralAdhocRegistry } from "./adhoc-bridge.js";
|
|
18
19
|
/** `/entities/products/id/123` → `["products","id","123"]` (strip the prefix). */
|
|
19
20
|
function routeSegments(pathname) {
|
|
20
21
|
const parts = pathname.replace(/^\/+|\/+$/g, "").split("/");
|
|
@@ -30,6 +31,30 @@ function firstTablePanel(view) {
|
|
|
30
31
|
}
|
|
31
32
|
return undefined;
|
|
32
33
|
}
|
|
34
|
+
/** The first detail panel's record fetch (header / record-card share the read op),
|
|
35
|
+
* so the SSR seed carries the record — the client hydrates without a refetch. */
|
|
36
|
+
function firstDetailPopulate(view) {
|
|
37
|
+
for (const slot of view.slots) {
|
|
38
|
+
const body = slot.panel?.body;
|
|
39
|
+
if ((body?.case === "detailHeader" || body?.case === "recordCard") && body.value.populate) {
|
|
40
|
+
return { populate: body.value.populate, idField: body.value.idField || "id" };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
/** Load the command-palette seed from the request graph. Non-fatal callers should
|
|
46
|
+
* catch and fall back to an empty seed (the palette then renders nothing). */
|
|
47
|
+
export async function loadCommandPaletteData(input) {
|
|
48
|
+
const systemGraphFactory = async () => input.graph;
|
|
49
|
+
const [listCompositions, dialogCompositions] = await Promise.all([
|
|
50
|
+
loadInstalledEntityListCompositions({ systemGraphFactory }),
|
|
51
|
+
loadInstalledDialogCompositions({ systemGraphFactory }),
|
|
52
|
+
]);
|
|
53
|
+
return {
|
|
54
|
+
compositions: Object.values(listCompositions),
|
|
55
|
+
dialogCompositions,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
33
58
|
export async function renderMeridianEntityView(input) {
|
|
34
59
|
const { caller, graph, mode = "light", notFound = "throw" } = input;
|
|
35
60
|
const parsed = parseAndValidateEntityRoute(routeSegments(input.pathname));
|
|
@@ -49,12 +74,19 @@ export async function renderMeridianEntityView(input) {
|
|
|
49
74
|
const composition = compositions[entityType];
|
|
50
75
|
if (!composition)
|
|
51
76
|
return miss();
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
const view = projectEntityView(composition, {
|
|
77
|
+
// CURSOR pagination: the aion entity list ops now honor a runtime page size +
|
|
78
|
+
// pageToken and return a real `{ items, nextToken, hasMore, totalCount }` page
|
|
79
|
+
// (DEV-18242), so page true server-side — the renderer sends `pageToken`/`pageSize`
|
|
80
|
+
// and reads `nextToken`/`totalCount` back. Scales past one page, unlike the prior
|
|
81
|
+
// CLIENT workaround that fetched a bounded set and sliced it locally.
|
|
82
|
+
const view = projectEntityView(composition, {
|
|
83
|
+
kind,
|
|
84
|
+
pagination: "cursor",
|
|
85
|
+
pageSize: 20,
|
|
86
|
+
// DETAIL: the record id from the route → ViewDescriptor.subject_id, which the
|
|
87
|
+
// detail panels bind into their populate request.
|
|
88
|
+
entityId: entityId ?? undefined,
|
|
89
|
+
});
|
|
58
90
|
const invoker = createEntityOpsInvoker(caller.entities?.ops);
|
|
59
91
|
// Pre-fetch page-0 for the table so the SSR HTML carries real rows (and the
|
|
60
92
|
// client hydrates without an immediate refetch). Non-fatal: on failure we
|
|
@@ -71,7 +103,23 @@ export async function renderMeridianEntityView(input) {
|
|
|
71
103
|
// leave unseeded
|
|
72
104
|
}
|
|
73
105
|
}
|
|
74
|
-
|
|
106
|
+
// DETAIL: pre-fetch the record so the header / record-card panels paint real
|
|
107
|
+
// values server-side. Stored as a one-row page — useRecord reads `seed.rows[0]`.
|
|
108
|
+
// Non-fatal, same as the table seed.
|
|
109
|
+
const detailFetch = firstDetailPopulate(view);
|
|
110
|
+
if (detailFetch && entityId) {
|
|
111
|
+
try {
|
|
112
|
+
const request = { [detailFetch.idField]: entityId };
|
|
113
|
+
const response = await invoker.invoke(detailFetch.populate.service, detailFetch.populate.method, request);
|
|
114
|
+
initialData[`${detailFetch.populate.service}.${detailFetch.populate.method}`] = {
|
|
115
|
+
rows: [response],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// leave unseeded
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const node = createElement(MeridianMuiProvider, { invoker, theme: savviSkin, mode, adhoc: neutralAdhocRegistry }, createElement(ViewRenderer, { view, initialData }));
|
|
75
123
|
return {
|
|
76
124
|
node,
|
|
77
125
|
dehydrated: { viewJson: toJson(ViewDescriptorSchema, view), initialData },
|
|
@@ -98,7 +146,7 @@ export async function renderCrdLayout(input) {
|
|
|
98
146
|
// leave unseeded
|
|
99
147
|
}
|
|
100
148
|
}
|
|
101
|
-
const node = createElement(MeridianMuiProvider, { invoker, theme: savviSkin, mode }, createElement(ViewRenderer, { view: descriptor, initialData }));
|
|
149
|
+
const node = createElement(MeridianMuiProvider, { invoker, theme: savviSkin, mode, adhoc: neutralAdhocRegistry }, createElement(ViewRenderer, { view: descriptor, initialData }));
|
|
102
150
|
return {
|
|
103
151
|
node,
|
|
104
152
|
dehydrated: { viewJson: toJson(ViewDescriptorSchema, descriptor), initialData },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@savvifi/meridian-aion-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The aion/web <-> meridian-ux render seam: renderMeridianEntityView (server — parse route, load composition, project to a ViewDescriptor, pre-fetch page-0, render + dehydrate) + MeridianEntityView (client hydration) + savviSkin. Lets an SSR host (e.g. aion/web on Hono+Vite) render entity routes through meridian instead of the legacy React path.",
|
|
6
6
|
"publishConfig": {
|
|
@@ -36,11 +36,11 @@
|
|
|
36
36
|
"typecheck": "tsc -p tsconfig.json"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@savvifi/meridian-aion-projection": "^0.
|
|
40
|
-
"@savvifi/meridian-mui-kit": "^0.
|
|
41
|
-
"@savvifi/meridian-proto-ts": "^0.
|
|
39
|
+
"@savvifi/meridian-aion-projection": "^0.3.0",
|
|
40
|
+
"@savvifi/meridian-mui-kit": "^0.4.0",
|
|
41
|
+
"@savvifi/meridian-proto-ts": "^0.8.0",
|
|
42
42
|
"@savvifi/meridian-schemas": "^0.7.0",
|
|
43
|
-
"@savvifi/meridian-web-react": "^0.
|
|
43
|
+
"@savvifi/meridian-web-react": "^0.6.0",
|
|
44
44
|
"@bufbuild/protobuf": "2.12.1"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|