@persona/layout-editor-remote-module 0.5.1 → 0.5.2
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/vite-plugin.mjs +16 -15
- package/package.json +2 -2
- package/skills/getting-started/SKILL.md +124 -10
package/dist/vite-plugin.mjs
CHANGED
|
@@ -135,15 +135,17 @@ function personaLayoutModules(options) {
|
|
|
135
135
|
let definitions = [];
|
|
136
136
|
return {
|
|
137
137
|
async closeBundle() {
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
const envConfig = this.environment?.config;
|
|
139
|
+
if (definitions.length === 0 || envConfig?.command !== "build") return;
|
|
140
|
+
if (envConfig.consumer !== "client") return;
|
|
141
|
+
const outDir = path.isAbsolute(envConfig.build.outDir) ? envConfig.build.outDir : path.resolve(root, envConfig.build.outDir);
|
|
140
142
|
const input = {};
|
|
141
143
|
for (const definition of definitions) if (definition.render.kind === "code") input[entryNameFor(definition)] = definition.render.absolutePath;
|
|
142
144
|
let emitted = [];
|
|
143
145
|
if (Object.keys(input).length > 0) {
|
|
144
146
|
const shimAlias = await ensureRemoteShimFiles();
|
|
145
147
|
const result = await build$1({
|
|
146
|
-
base:
|
|
148
|
+
base: envConfig.base,
|
|
147
149
|
build: {
|
|
148
150
|
emptyOutDir: false,
|
|
149
151
|
outDir,
|
|
@@ -159,14 +161,14 @@ function personaLayoutModules(options) {
|
|
|
159
161
|
mode: "production",
|
|
160
162
|
plugins: options.modulePlugins?.() ?? [],
|
|
161
163
|
resolve: { alias: {
|
|
162
|
-
...resolveAliasForEsbuild(
|
|
164
|
+
...resolveAliasForEsbuild(envConfig),
|
|
163
165
|
...shimAlias
|
|
164
166
|
} },
|
|
165
167
|
root
|
|
166
168
|
});
|
|
167
169
|
emitted = (Array.isArray(result) ? result : [result]).flatMap((output) => "output" in output ? output.output : []);
|
|
168
170
|
}
|
|
169
|
-
const base =
|
|
171
|
+
const base = envConfig.base ?? "/";
|
|
170
172
|
const manifests = [];
|
|
171
173
|
for (const definition of definitions) {
|
|
172
174
|
const render = resolveManifestDelivery(definition, definition.render, emitted, root, base);
|
|
@@ -188,15 +190,18 @@ function personaLayoutModules(options) {
|
|
|
188
190
|
await mkdir(path.dirname(manifestFilePath), { recursive: true });
|
|
189
191
|
await writeFile(manifestFilePath, JSON.stringify({ modules: manifests }, null, 2), "utf8");
|
|
190
192
|
},
|
|
191
|
-
async config(config
|
|
193
|
+
async config(config) {
|
|
192
194
|
root = config.root ? path.resolve(config.root) : process.cwd();
|
|
193
195
|
definitions = options.modules.map((input) => ({
|
|
194
196
|
definitionFilePath: input.key,
|
|
195
197
|
input,
|
|
196
198
|
render: resolveEntry(root, input.render)
|
|
197
199
|
}));
|
|
200
|
+
},
|
|
201
|
+
configEnvironment(name, envConfig, env) {
|
|
198
202
|
if (env.command !== "build") return;
|
|
199
|
-
|
|
203
|
+
if ((envConfig.consumer ?? (name === "client" ? "client" : "server")) !== "client") return;
|
|
204
|
+
const existingInputs = absoluteInputPaths(envConfig, root);
|
|
200
205
|
const toInject = {};
|
|
201
206
|
for (const definition of definitions) {
|
|
202
207
|
const delivery = definition.render;
|
|
@@ -205,14 +210,10 @@ function personaLayoutModules(options) {
|
|
|
205
210
|
existingInputs.add(delivery.absolutePath);
|
|
206
211
|
}
|
|
207
212
|
}
|
|
208
|
-
if (Object.keys(toInject).length > 0) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
...toInputRecord(config.build.rollupOptions.input, root),
|
|
213
|
-
...toInject
|
|
214
|
-
};
|
|
215
|
-
}
|
|
213
|
+
if (Object.keys(toInject).length > 0) return { build: { rollupOptions: { input: {
|
|
214
|
+
...toInputRecord(envConfig.build?.rollupOptions?.input, root),
|
|
215
|
+
...toInject
|
|
216
|
+
} } } };
|
|
216
217
|
},
|
|
217
218
|
configResolved(config) {
|
|
218
219
|
resolvedConfig = config;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@persona/layout-editor-remote-module",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Shared manifest format, Vite plugin, and host/iframe bridge for remotely-defined dashboard layout modules in Persona dashboard apps.",
|
|
5
5
|
"license": "SEE LICENSE",
|
|
6
6
|
"type": "module",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"esbuild": ">=0.21.0 <1",
|
|
42
42
|
"react": "^17 || ^18 || ^19",
|
|
43
43
|
"react-dom": "^17 || ^18 || ^19",
|
|
44
|
-
"vite": "^
|
|
44
|
+
"vite": "^6 || ^7 || ^8",
|
|
45
45
|
"zod": "^4.3"
|
|
46
46
|
},
|
|
47
47
|
"peerDependenciesMeta": {
|
|
@@ -3,13 +3,14 @@ name: getting-started
|
|
|
3
3
|
description: >
|
|
4
4
|
Wire up @persona/layout-editor-remote-module in a Persona dashboard app: register
|
|
5
5
|
personaLayoutModules in a typical app's vite.config.ts, author modules with defineLayoutModule
|
|
6
|
-
using a document (.html) render delivery, add a zod configSchema and an RJSF uiConfigSchema
|
|
7
|
-
|
|
8
|
-
(layout data, host context such as
|
|
9
|
-
height). Activate when adding a new
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
that never receives data from the
|
|
6
|
+
using a document (.html) render delivery, add a zod configSchema and an RJSF uiConfigSchema
|
|
7
|
+
(labels, field order, DataSourceField data-source pickers), and talk to the host from the iframed
|
|
8
|
+
document with LayoutModuleBridge from the bridge-frame entry (layout data, host context such as
|
|
9
|
+
layoutType / layoutObjectId / isReadOnly, resolveDataPath, and height). Activate when adding a new
|
|
10
|
+
layout module to an app, integrating this package for the first time, wiring a module document to
|
|
11
|
+
the dashboard host, or debugging a manifest that fails to build or validate (duplicate key,
|
|
12
|
+
missing defaultSize entry, render path not found) or a module that never receives data from the
|
|
13
|
+
host.
|
|
13
14
|
metadata:
|
|
14
15
|
type: core
|
|
15
16
|
library: '@persona/layout-editor-remote-module'
|
|
@@ -123,9 +124,9 @@ as static JSON Schema, not re-executed as zod on the consuming side.
|
|
|
123
124
|
|
|
124
125
|
### `uiConfigSchema` — config panel layout
|
|
125
126
|
|
|
126
|
-
The host renders the config panel with RJSF. Pass an RJSF `uiSchema`
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
The host renders the config panel with RJSF. Pass an RJSF `uiSchema` as `uiConfigSchema`; it is
|
|
128
|
+
already plain data, so the plugin copies it into the manifest unchanged. Its top-level keys are the
|
|
129
|
+
`configSchema` property names, and each one holds that property's `ui:*` entries:
|
|
129
130
|
|
|
130
131
|
```ts
|
|
131
132
|
defineLayoutModule({
|
|
@@ -141,6 +142,104 @@ defineLayoutModule({
|
|
|
141
142
|
});
|
|
142
143
|
```
|
|
143
144
|
|
|
145
|
+
- **Labels and help text** come from the property's JSON Schema `title` / `description` — set them
|
|
146
|
+
with zod's `.meta({ title, description })` — and fall back to the property name. Override them per
|
|
147
|
+
field with `'ui:options': { title, description }`; the description shows as the label's tooltip.
|
|
148
|
+
- **Field order** is `'ui:order'` at the top level (use `'*'` for "everything else").
|
|
149
|
+
- **Controls are picked from the schema** without a `uiConfigSchema` entry: an enum renders as a
|
|
150
|
+
dropdown, an array of enum values with `uniqueItems: true` in its JSON Schema as a multi-select, a
|
|
151
|
+
boolean as a checkbox, and an array of objects as a list whose items open in an editing panel.
|
|
152
|
+
- **Everything is JSON.** The manifest is static JSON, so function-valued RJSF options
|
|
153
|
+
(`createItem`, `itemLabel`, …) and custom React fields/widgets cannot be supplied by a module —
|
|
154
|
+
only the `ui:field` names and options the host already registers, listed below.
|
|
155
|
+
|
|
156
|
+
#### `DataSourceField` — pick a value from the record
|
|
157
|
+
|
|
158
|
+
`'ui:field': 'DataSourceField'` turns a property into the host's data-source input: an
|
|
159
|
+
autocompleting method-chain input (e.g. `current_case.id`, or `query.<variable>` for a layout
|
|
160
|
+
variable) checked against the platform schema. Declare the property as a **string** — the field then
|
|
161
|
+
stores the chain itself as a string:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
defineLayoutModule({
|
|
165
|
+
// ...
|
|
166
|
+
layouts: ['account'],
|
|
167
|
+
defaultSize: { account: { width: 6, height: 24 } },
|
|
168
|
+
configSchema: z.object({
|
|
169
|
+
accountId: z.string().optional(),
|
|
170
|
+
}),
|
|
171
|
+
uiConfigSchema: {
|
|
172
|
+
accountId: {
|
|
173
|
+
'ui:field': 'DataSourceField',
|
|
174
|
+
'ui:options': {
|
|
175
|
+
title: 'Account ID',
|
|
176
|
+
description: 'Account whose transactions are monitored',
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`ui:options` it reads:
|
|
184
|
+
|
|
185
|
+
- `title`, `description` — label and tooltip; default to the schema's `title` / `description`.
|
|
186
|
+
- `expectedSchemaType` — the platform object type the chain must resolve to (e.g. `'case'`,
|
|
187
|
+
`'user'`); a chain resolving to another type is flagged. Unset means any chain is accepted. An
|
|
188
|
+
empty value is never validated.
|
|
189
|
+
- `rootPath` — limits validation to chains under one top-level property (e.g. `'current_case'`).
|
|
190
|
+
Only set it when `layouts` names a single layout that has that root; a chain outside `rootPath`
|
|
191
|
+
still saves but is not validated.
|
|
192
|
+
|
|
193
|
+
The config holds the **chain, not its value**. Resolve it at render time with `resolveDataPath` from
|
|
194
|
+
`LayoutModuleBridge` (below), skipping an unset field:
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
import { useEffect, useState } from 'react';
|
|
198
|
+
import {
|
|
199
|
+
type Json,
|
|
200
|
+
type LayoutModuleBridgeRenderProps,
|
|
201
|
+
} from '@persona/layout-editor-remote-module/bridge-frame';
|
|
202
|
+
|
|
203
|
+
function TransactionMonitor({
|
|
204
|
+
data,
|
|
205
|
+
resolveDataPath,
|
|
206
|
+
}: Pick<LayoutModuleBridgeRenderProps, 'data' | 'resolveDataPath'>) {
|
|
207
|
+
const chain = (data as { accountId?: string } | null)?.accountId;
|
|
208
|
+
const [accountId, setAccountId] = useState<Json>(null);
|
|
209
|
+
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
setAccountId(null);
|
|
212
|
+
if (!chain) return;
|
|
213
|
+
let current = true;
|
|
214
|
+
resolveDataPath(chain).then(
|
|
215
|
+
(value) => current && setAccountId(value),
|
|
216
|
+
() => current && setAccountId(null),
|
|
217
|
+
);
|
|
218
|
+
return () => {
|
|
219
|
+
current = false;
|
|
220
|
+
};
|
|
221
|
+
}, [chain, resolveDataPath]);
|
|
222
|
+
|
|
223
|
+
// ...
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
The host pushes config updates into the mounted module, so the chain can change or be cleared at any
|
|
228
|
+
time: reset the resolved value whenever it does, and ignore responses to an outdated chain.
|
|
229
|
+
`resolveDataPath` resolves to `null` when the chain can't be resolved on the current record and
|
|
230
|
+
rejects on a host error or timeout, so handle both.
|
|
231
|
+
|
|
232
|
+
#### Other `ui:options`
|
|
233
|
+
|
|
234
|
+
- `errorMessages` — per-keyword validation messages, e.g.
|
|
235
|
+
`{ 'ui:options': { errorMessages: { pattern: 'Use letters only.' } } }` (keys are JSON Schema
|
|
236
|
+
keywords: `pattern`, `minLength`, `maximum`, …).
|
|
237
|
+
- On an enum multi-select: `unsetSelectsAll: true` shows an unset value as every option selected,
|
|
238
|
+
for a key whose absence means "all" and should stay absent.
|
|
239
|
+
- On an array of objects: `identifierField` (item property that labels each row), `emptyText`,
|
|
240
|
+
`editLabel` (the editing panel's title), `listAriaLabel`, and `inlineItems: true` to render the
|
|
241
|
+
items inline instead of as a list with an editing panel.
|
|
242
|
+
|
|
144
243
|
### `bridge-frame` — talking to the host from the iframed document
|
|
145
244
|
|
|
146
245
|
The host renders a document-delivered module in an iframe and speaks to it over `postMessage`. Wrap
|
|
@@ -279,6 +378,21 @@ resolve to `null` and `children` renders, so a stuck fallback is always a host/o
|
|
|
279
378
|
|
|
280
379
|
Source: `src/bridge/iframe/createModuleBridge.ts`, `src/bridge/origin.ts`.
|
|
281
380
|
|
|
381
|
+
### [HIGH] `DataSourceField` on a non-string property, or read as the resolved value
|
|
382
|
+
|
|
383
|
+
Declare a `DataSourceField` property as `z.string()`. On a non-string property the host stores a
|
|
384
|
+
structured data-source object instead of the chain string, and `resolveDataPath` only accepts a
|
|
385
|
+
string — it resolves to `null`. On a string property the stored value is the method chain the user
|
|
386
|
+
typed (e.g. `"current_account.id"`), not the account ID: pass `data.accountId` to `resolveDataPath`
|
|
387
|
+
rather than using it directly.
|
|
388
|
+
|
|
389
|
+
### [MEDIUM] A `ui:field` name the host doesn't register
|
|
390
|
+
|
|
391
|
+
A `ui:field` name the host has no component for falls back to RJSF's default field for the
|
|
392
|
+
property's type — no build or validation error, just a plain input instead of the intended control.
|
|
393
|
+
Spell `'DataSourceField'` exactly; module-supplied field components are not possible (see
|
|
394
|
+
"Everything is JSON" above).
|
|
395
|
+
|
|
282
396
|
### [HIGH] `key` uses the wrong charset or collides with another module
|
|
283
397
|
|
|
284
398
|
Wrong:
|