@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.
@@ -135,15 +135,17 @@ function personaLayoutModules(options) {
135
135
  let definitions = [];
136
136
  return {
137
137
  async closeBundle() {
138
- if (definitions.length === 0 || resolvedConfig.command !== "build") return;
139
- const outDir = path.isAbsolute(resolvedConfig.build.outDir) ? resolvedConfig.build.outDir : path.resolve(root, resolvedConfig.build.outDir);
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: resolvedConfig.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(resolvedConfig),
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 = resolvedConfig.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, env) {
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
- const existingInputs = absoluteInputPaths(config, root);
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
- config.build ??= {};
210
- config.build.rollupOptions ??= {};
211
- config.build.rollupOptions.input = {
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.1",
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": "^5 || ^6 || ^7 || ^8",
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, and
7
- talk to the host from the iframed document with LayoutModuleBridge from the bridge-frame entry
8
- (layout data, host context such as layoutType / layoutObjectId / isReadOnly, resolveDataPath, and
9
- height). Activate when adding a new layout module to an app, integrating this package for the
10
- first time, wiring a module document to the dashboard host, or debugging a manifest that fails to
11
- build or validate (duplicate key, missing defaultSize entry, render path not found) or a module
12
- that never receives data from the host.
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` (widget choice, field order,
127
- labels) as `uiConfigSchema`; it is already plain data, so the plugin copies it into the manifest
128
- unchanged:
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: