@decocms/blocks 7.9.0 → 7.11.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 +1 -1
- package/src/cms/client.ts +10 -1
- package/src/cms/index.ts +10 -1
- package/src/cms/resolve.ts +6 -13
- package/src/cms/schema.test.ts +220 -0
- package/src/cms/schema.ts +87 -2
package/package.json
CHANGED
package/src/cms/client.ts
CHANGED
|
@@ -51,13 +51,22 @@ export {
|
|
|
51
51
|
} from "./registry";
|
|
52
52
|
export type { SectionLoaderFn } from "./sectionLoaders";
|
|
53
53
|
export { compose, withDevice, withMobile, withSearchParam, withSectionLoader } from "./sectionMixins";
|
|
54
|
-
export type {
|
|
54
|
+
export type {
|
|
55
|
+
ActionConfig,
|
|
56
|
+
AppSchemas,
|
|
57
|
+
BlockPropsSchema,
|
|
58
|
+
LoaderConfig,
|
|
59
|
+
MatcherConfig,
|
|
60
|
+
MetaResponse,
|
|
61
|
+
} from "./schema";
|
|
55
62
|
export {
|
|
56
63
|
composeMeta,
|
|
57
64
|
getRegisteredLoaders,
|
|
58
65
|
getRegisteredMatchers,
|
|
66
|
+
inferLoaderTags,
|
|
59
67
|
registerActionSchema,
|
|
60
68
|
registerActionSchemas,
|
|
69
|
+
registerAppSchemas,
|
|
61
70
|
registerLoaderSchema,
|
|
62
71
|
registerLoaderSchemas,
|
|
63
72
|
registerMatcherSchema,
|
package/src/cms/index.ts
CHANGED
|
@@ -100,13 +100,22 @@ export {
|
|
|
100
100
|
} from "./sectionMixins";
|
|
101
101
|
export type { ApplySectionConventionsInput, SectionMetaEntry } from "./applySectionConventions";
|
|
102
102
|
export { applySectionConventions } from "./applySectionConventions";
|
|
103
|
-
export type {
|
|
103
|
+
export type {
|
|
104
|
+
ActionConfig,
|
|
105
|
+
AppSchemas,
|
|
106
|
+
BlockPropsSchema,
|
|
107
|
+
LoaderConfig,
|
|
108
|
+
MatcherConfig,
|
|
109
|
+
MetaResponse,
|
|
110
|
+
} from "./schema";
|
|
104
111
|
export {
|
|
105
112
|
composeMeta,
|
|
106
113
|
getRegisteredLoaders,
|
|
107
114
|
getRegisteredMatchers,
|
|
115
|
+
inferLoaderTags,
|
|
108
116
|
registerActionSchema,
|
|
109
117
|
registerActionSchemas,
|
|
118
|
+
registerAppSchemas,
|
|
110
119
|
registerLoaderSchema,
|
|
111
120
|
registerLoaderSchemas,
|
|
112
121
|
registerMatcherSchema,
|
package/src/cms/resolve.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type ActionConfig,
|
|
3
|
+
inferLoaderTags,
|
|
3
4
|
type LoaderConfig,
|
|
4
5
|
registerActionSchemas,
|
|
5
6
|
registerLoaderSchemas,
|
|
@@ -456,6 +457,9 @@ export function registerCommerceLoaders(loaders: Record<string, CommerceLoader>)
|
|
|
456
457
|
|
|
457
458
|
// Auto-register loader + action schemas for the admin manifest.
|
|
458
459
|
// Separate actions (keys containing "/actions/") from loaders.
|
|
460
|
+
// These are stub registrations (isStub) — a real schema for the same key
|
|
461
|
+
// (registered via registerAppSchemas from an app's schemas.gen.ts) always
|
|
462
|
+
// wins, no matter which side registers first.
|
|
459
463
|
const loaderConfigs: LoaderConfig[] = [];
|
|
460
464
|
const actionConfigs: ActionConfig[] = [];
|
|
461
465
|
|
|
@@ -464,7 +468,7 @@ export function registerCommerceLoaders(loaders: Record<string, CommerceLoader>)
|
|
|
464
468
|
const schema = { type: "object" as const, additionalProperties: true };
|
|
465
469
|
|
|
466
470
|
if (key.includes("/actions/")) {
|
|
467
|
-
actionConfigs.push({ key, title: key, namespace, propsSchema: schema });
|
|
471
|
+
actionConfigs.push({ key, title: key, namespace, propsSchema: schema, isStub: true });
|
|
468
472
|
} else {
|
|
469
473
|
loaderConfigs.push({
|
|
470
474
|
key,
|
|
@@ -472,6 +476,7 @@ export function registerCommerceLoaders(loaders: Record<string, CommerceLoader>)
|
|
|
472
476
|
namespace,
|
|
473
477
|
propsSchema: schema,
|
|
474
478
|
tags: inferLoaderTags(key),
|
|
479
|
+
isStub: true,
|
|
475
480
|
});
|
|
476
481
|
}
|
|
477
482
|
}
|
|
@@ -480,18 +485,6 @@ export function registerCommerceLoaders(loaders: Record<string, CommerceLoader>)
|
|
|
480
485
|
registerActionSchemas(actionConfigs);
|
|
481
486
|
}
|
|
482
487
|
|
|
483
|
-
function inferLoaderTags(key: string): string[] {
|
|
484
|
-
if (
|
|
485
|
-
key.includes("productList") ||
|
|
486
|
-
key.includes("ProductList") ||
|
|
487
|
-
key.includes("ProductShelf") ||
|
|
488
|
-
key.includes("SearchResult")
|
|
489
|
-
) {
|
|
490
|
-
return ["product-list"];
|
|
491
|
-
}
|
|
492
|
-
return [];
|
|
493
|
-
}
|
|
494
|
-
|
|
495
488
|
/** Delete a single commerce loader by key. No-op if key is absent. */
|
|
496
489
|
export function unregisterCommerceLoader(key: string): void {
|
|
497
490
|
delete commerceLoaders[key];
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the schema registry's real-over-stub precedence and the
|
|
3
|
+
* definition builders' additionalProperties preservation.
|
|
4
|
+
*
|
|
5
|
+
* Background: registerCommerceLoaders() auto-registers a
|
|
6
|
+
* `{ type: "object", additionalProperties: true }` stub for every commerce
|
|
7
|
+
* loader/action key. App packages register REAL props schemas (from their
|
|
8
|
+
* build-time schemas.gen.ts) via registerAppSchemas(). The published meta
|
|
9
|
+
* must contain the real schema no matter which side registered first, and
|
|
10
|
+
* the stub's additionalProperties flag must survive into the definition so
|
|
11
|
+
* the Studio can tell "props unknown — edit as JSON" apart from "takes no
|
|
12
|
+
* input".
|
|
13
|
+
*
|
|
14
|
+
* NOTE: the registries are module-global singletons — every test uses its own
|
|
15
|
+
* unique keys instead of resetting shared state.
|
|
16
|
+
*/
|
|
17
|
+
import { describe, expect, it } from "vitest";
|
|
18
|
+
import {
|
|
19
|
+
composeMeta,
|
|
20
|
+
getRegisteredLoaders,
|
|
21
|
+
type MetaResponse,
|
|
22
|
+
registerActionSchema,
|
|
23
|
+
registerAppSchemas,
|
|
24
|
+
registerLoaderSchema,
|
|
25
|
+
} from "./schema";
|
|
26
|
+
|
|
27
|
+
const b64 = (s: string) => Buffer.from(s).toString("base64");
|
|
28
|
+
|
|
29
|
+
const emptySiteMeta = (): MetaResponse => ({
|
|
30
|
+
major: 1,
|
|
31
|
+
version: "1.0.0",
|
|
32
|
+
namespace: "site",
|
|
33
|
+
site: "test",
|
|
34
|
+
manifest: { blocks: {} },
|
|
35
|
+
schema: { definitions: {}, root: {} },
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const STUB_SCHEMA = { type: "object" as const, additionalProperties: true };
|
|
39
|
+
|
|
40
|
+
function stubLoader(key: string) {
|
|
41
|
+
registerLoaderSchema({
|
|
42
|
+
key,
|
|
43
|
+
title: key,
|
|
44
|
+
namespace: "vtex",
|
|
45
|
+
propsSchema: STUB_SCHEMA,
|
|
46
|
+
isStub: true,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const REAL_PLP_PROPS = {
|
|
51
|
+
type: "object" as const,
|
|
52
|
+
properties: {
|
|
53
|
+
sort: {
|
|
54
|
+
type: "string",
|
|
55
|
+
enum: ["OrderByPriceDESC", "OrderByPriceASC", "OrderByTopSaleDESC"],
|
|
56
|
+
},
|
|
57
|
+
count: { type: "number" },
|
|
58
|
+
fq: { type: "string" },
|
|
59
|
+
},
|
|
60
|
+
required: ["count"],
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function realLoader(key: string) {
|
|
64
|
+
registerAppSchemas({ namespace: "vtex", loaders: { [key]: REAL_PLP_PROPS } });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function definitionFor(key: string) {
|
|
68
|
+
return composeMeta(emptySiteMeta()).schema.definitions[b64(key)];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe("real-over-stub precedence", () => {
|
|
72
|
+
it("keeps the real schema when the stub registers afterwards (loaders)", () => {
|
|
73
|
+
const key = "vtex/loaders/test/realThenStub.ts";
|
|
74
|
+
realLoader(key);
|
|
75
|
+
stubLoader(key);
|
|
76
|
+
|
|
77
|
+
const def = definitionFor(key);
|
|
78
|
+
expect(def.properties.sort.enum).toEqual([
|
|
79
|
+
"OrderByPriceDESC",
|
|
80
|
+
"OrderByPriceASC",
|
|
81
|
+
"OrderByTopSaleDESC",
|
|
82
|
+
]);
|
|
83
|
+
expect(def.required).toEqual(["__resolveType", "count"]);
|
|
84
|
+
expect(def.additionalProperties).toBeUndefined();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("replaces the stub when the real schema registers afterwards (loaders)", () => {
|
|
88
|
+
const key = "vtex/loaders/test/stubThenReal.ts";
|
|
89
|
+
stubLoader(key);
|
|
90
|
+
realLoader(key);
|
|
91
|
+
|
|
92
|
+
const def = definitionFor(key);
|
|
93
|
+
expect(def.properties.count).toEqual({ type: "number" });
|
|
94
|
+
expect(def.additionalProperties).toBeUndefined();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("applies the same precedence to actions", () => {
|
|
98
|
+
const key = "vtex/actions/test/addCoupon.ts";
|
|
99
|
+
registerAppSchemas({
|
|
100
|
+
namespace: "vtex",
|
|
101
|
+
actions: {
|
|
102
|
+
[key]: {
|
|
103
|
+
type: "object",
|
|
104
|
+
properties: { coupon: { type: "string" } },
|
|
105
|
+
required: ["coupon"],
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
registerActionSchema({
|
|
110
|
+
key,
|
|
111
|
+
title: key,
|
|
112
|
+
namespace: "vtex",
|
|
113
|
+
propsSchema: STUB_SCHEMA,
|
|
114
|
+
isStub: true,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const def = definitionFor(key);
|
|
118
|
+
expect(def.properties.coupon).toEqual({ type: "string" });
|
|
119
|
+
expect(def.required).toEqual(["__resolveType", "coupon"]);
|
|
120
|
+
expect(def.additionalProperties).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("lets a later stub replace an earlier stub (hot-reload re-registration)", () => {
|
|
124
|
+
const key = "vtex/loaders/test/stubTwice.ts";
|
|
125
|
+
stubLoader(key);
|
|
126
|
+
stubLoader(key);
|
|
127
|
+
expect(definitionFor(key).additionalProperties).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("lets a later real schema replace an earlier real schema", () => {
|
|
131
|
+
const key = "vtex/loaders/test/realTwice.ts";
|
|
132
|
+
realLoader(key);
|
|
133
|
+
registerAppSchemas({
|
|
134
|
+
namespace: "vtex",
|
|
135
|
+
loaders: {
|
|
136
|
+
[key]: { type: "object", properties: { slug: { type: "string" } } },
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
const def = definitionFor(key);
|
|
140
|
+
expect(def.properties.slug).toEqual({ type: "string" });
|
|
141
|
+
expect(def.properties.sort).toBeUndefined();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("additionalProperties preservation in definitions", () => {
|
|
146
|
+
it("emits additionalProperties: true for stub-only keys (props unknown → JSON editor)", () => {
|
|
147
|
+
const key = "vtex/loaders/test/stubOnly.ts";
|
|
148
|
+
stubLoader(key);
|
|
149
|
+
|
|
150
|
+
const def = definitionFor(key);
|
|
151
|
+
expect(def.additionalProperties).toBe(true);
|
|
152
|
+
expect(Object.keys(def.properties)).toEqual(["__resolveType"]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("emits NO additionalProperties for a real schema without props (takes no input)", () => {
|
|
156
|
+
const key = "vtex/loaders/test/noInput.ts";
|
|
157
|
+
registerAppSchemas({
|
|
158
|
+
namespace: "vtex",
|
|
159
|
+
loaders: { [key]: { type: "object", properties: {} } },
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const def = definitionFor(key);
|
|
163
|
+
expect(def.additionalProperties).toBeUndefined();
|
|
164
|
+
expect(Object.keys(def.properties)).toEqual(["__resolveType"]);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("registerAppSchemas", () => {
|
|
169
|
+
it("registers every key form present in the artifact (bare and .ts)", () => {
|
|
170
|
+
const bare = "vtex/loaders/test/aliased";
|
|
171
|
+
registerAppSchemas({
|
|
172
|
+
namespace: "vtex",
|
|
173
|
+
loaders: {
|
|
174
|
+
[bare]: REAL_PLP_PROPS,
|
|
175
|
+
[`${bare}.ts`]: REAL_PLP_PROPS,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const meta = composeMeta(emptySiteMeta());
|
|
180
|
+
for (const key of [bare, `${bare}.ts`]) {
|
|
181
|
+
const def = meta.schema.definitions[b64(key)];
|
|
182
|
+
expect(def.properties.sort.enum).toContain("OrderByPriceDESC");
|
|
183
|
+
expect(def.properties.__resolveType.enum).toEqual([key]);
|
|
184
|
+
expect(meta.manifest.blocks.loaders[key]).toEqual({
|
|
185
|
+
$ref: `#/definitions/${b64(key)}`,
|
|
186
|
+
namespace: "vtex",
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("propagates schema title/description into the definition", () => {
|
|
192
|
+
const key = "vtex/loaders/test/titled.ts";
|
|
193
|
+
registerAppSchemas({
|
|
194
|
+
namespace: "vtex",
|
|
195
|
+
loaders: {
|
|
196
|
+
[key]: {
|
|
197
|
+
type: "object",
|
|
198
|
+
title: "Product Listing Page",
|
|
199
|
+
description: "Fetches a PLP",
|
|
200
|
+
properties: { term: { type: "string" } },
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const def = definitionFor(key);
|
|
206
|
+
expect(def.title).toBe("Product Listing Page");
|
|
207
|
+
expect(def.description).toBe("Fetches a PLP");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("infers the product-list tag for loader keys, matching the stub inference", () => {
|
|
211
|
+
const key = "vtex/loaders/test/productListShelf.ts";
|
|
212
|
+
registerAppSchemas({
|
|
213
|
+
namespace: "vtex",
|
|
214
|
+
loaders: { [key]: { type: "object", properties: {} } },
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const registered = getRegisteredLoaders().find((l) => l.key === key);
|
|
218
|
+
expect(registered?.tags).toEqual(["product-list"]);
|
|
219
|
+
});
|
|
220
|
+
});
|
package/src/cms/schema.ts
CHANGED
|
@@ -68,6 +68,12 @@ export interface LoaderConfig {
|
|
|
68
68
|
propsSchema: Record<string, any>;
|
|
69
69
|
/** Tags for property matching (e.g., "product-list" enables injection into Product[] props). */
|
|
70
70
|
tags?: string[];
|
|
71
|
+
/**
|
|
72
|
+
* Auto-registered placeholder with no real props (see registerCommerceLoaders).
|
|
73
|
+
* A stub never overwrites a real schema for the same key, regardless of
|
|
74
|
+
* registration order.
|
|
75
|
+
*/
|
|
76
|
+
isStub?: boolean;
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
const loaderRegistry: LoaderConfig[] = [];
|
|
@@ -76,6 +82,7 @@ const loaderRegistry: LoaderConfig[] = [];
|
|
|
76
82
|
export function registerLoaderSchema(config: LoaderConfig) {
|
|
77
83
|
const idx = loaderRegistry.findIndex((l) => l.key === config.key);
|
|
78
84
|
if (idx >= 0) {
|
|
85
|
+
if (config.isStub && !loaderRegistry[idx].isStub) return;
|
|
79
86
|
loaderRegistry[idx] = config;
|
|
80
87
|
} else {
|
|
81
88
|
loaderRegistry.push(config);
|
|
@@ -107,6 +114,8 @@ export interface ActionConfig {
|
|
|
107
114
|
description?: string;
|
|
108
115
|
icon?: string;
|
|
109
116
|
propsSchema: Record<string, any>;
|
|
117
|
+
/** Auto-registered placeholder — never overwrites a real schema. See LoaderConfig.isStub. */
|
|
118
|
+
isStub?: boolean;
|
|
110
119
|
}
|
|
111
120
|
|
|
112
121
|
const actionRegistry: ActionConfig[] = [];
|
|
@@ -115,6 +124,7 @@ const actionRegistry: ActionConfig[] = [];
|
|
|
115
124
|
export function registerActionSchema(config: ActionConfig) {
|
|
116
125
|
const idx = actionRegistry.findIndex((a) => a.key === config.key);
|
|
117
126
|
if (idx >= 0) {
|
|
127
|
+
if (config.isStub && !actionRegistry[idx].isStub) return;
|
|
118
128
|
actionRegistry[idx] = config;
|
|
119
129
|
} else {
|
|
120
130
|
actionRegistry.push(config);
|
|
@@ -126,6 +136,74 @@ export function registerActionSchemas(configs: ActionConfig[]) {
|
|
|
126
136
|
for (const config of configs) registerActionSchema(config);
|
|
127
137
|
}
|
|
128
138
|
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// App block schemas — real props generated at app build time
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Loader keys matching these substrings return product lists — the admin uses
|
|
145
|
+
* the "product-list" tag to offer them as loader pickers on Product[] props.
|
|
146
|
+
*/
|
|
147
|
+
export function inferLoaderTags(key: string): string[] {
|
|
148
|
+
if (
|
|
149
|
+
key.includes("productList") ||
|
|
150
|
+
key.includes("ProductList") ||
|
|
151
|
+
key.includes("ProductShelf") ||
|
|
152
|
+
key.includes("SearchResult")
|
|
153
|
+
) {
|
|
154
|
+
return ["product-list"];
|
|
155
|
+
}
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Props schema of a single loader/action as emitted by blocks-cli's
|
|
161
|
+
* generate-app-schemas.ts into an app package's src/schemas.gen.ts.
|
|
162
|
+
* Same shape generate-schema.ts produces for site loaders.
|
|
163
|
+
*/
|
|
164
|
+
export interface BlockPropsSchema {
|
|
165
|
+
type?: "object";
|
|
166
|
+
title?: string;
|
|
167
|
+
description?: string;
|
|
168
|
+
properties?: Record<string, any>;
|
|
169
|
+
required?: string[];
|
|
170
|
+
additionalProperties?: boolean;
|
|
171
|
+
/** Any other JSON Schema keyword or JSDoc passthrough tag (nullable, …). */
|
|
172
|
+
[keyword: string]: any;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface AppSchemas {
|
|
176
|
+
namespace: string;
|
|
177
|
+
loaders?: Record<string, BlockPropsSchema>;
|
|
178
|
+
actions?: Record<string, BlockPropsSchema>;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Register the real loader/action props schemas an app package generated at
|
|
183
|
+
* build time (src/schemas.gen.ts). These take precedence over the
|
|
184
|
+
* `__resolveType`-only stubs auto-registered by registerCommerceLoaders(),
|
|
185
|
+
* regardless of which side registers first.
|
|
186
|
+
*/
|
|
187
|
+
export function registerAppSchemas(app: AppSchemas): void {
|
|
188
|
+
const loaderConfigs: LoaderConfig[] = Object.entries(app.loaders ?? {}).map(([key, schema]) => ({
|
|
189
|
+
key,
|
|
190
|
+
title: schema.title || key,
|
|
191
|
+
namespace: app.namespace,
|
|
192
|
+
...(schema.description ? { description: schema.description } : {}),
|
|
193
|
+
propsSchema: schema,
|
|
194
|
+
tags: inferLoaderTags(key),
|
|
195
|
+
}));
|
|
196
|
+
const actionConfigs: ActionConfig[] = Object.entries(app.actions ?? {}).map(([key, schema]) => ({
|
|
197
|
+
key,
|
|
198
|
+
title: schema.title || key,
|
|
199
|
+
namespace: app.namespace,
|
|
200
|
+
...(schema.description ? { description: schema.description } : {}),
|
|
201
|
+
propsSchema: schema,
|
|
202
|
+
}));
|
|
203
|
+
registerLoaderSchemas(loaderConfigs);
|
|
204
|
+
registerActionSchemas(actionConfigs);
|
|
205
|
+
}
|
|
206
|
+
|
|
129
207
|
function buildActionDefinitions() {
|
|
130
208
|
const definitions: Record<string, any> = {};
|
|
131
209
|
const manifestBlocks: Record<string, any> = {};
|
|
@@ -134,7 +212,7 @@ function buildActionDefinitions() {
|
|
|
134
212
|
const defKey = toBase64(action.key);
|
|
135
213
|
|
|
136
214
|
definitions[defKey] = {
|
|
137
|
-
title: action.key,
|
|
215
|
+
title: action.title || action.key,
|
|
138
216
|
...(action.description ? { description: action.description } : {}),
|
|
139
217
|
...(action.icon ? { icon: action.icon } : {}),
|
|
140
218
|
type: "object",
|
|
@@ -147,6 +225,9 @@ function buildActionDefinitions() {
|
|
|
147
225
|
},
|
|
148
226
|
...(action.propsSchema?.properties || {}),
|
|
149
227
|
},
|
|
228
|
+
// Preserved so the admin can tell "props unknown — edit as JSON" (stub)
|
|
229
|
+
// apart from "takes no input" (real schema without the flag).
|
|
230
|
+
...(action.propsSchema?.additionalProperties === true ? { additionalProperties: true } : {}),
|
|
150
231
|
};
|
|
151
232
|
|
|
152
233
|
manifestBlocks[action.key] = {
|
|
@@ -493,7 +574,7 @@ function buildLoaderDefinitions() {
|
|
|
493
574
|
const defKey = toBase64(loader.key);
|
|
494
575
|
|
|
495
576
|
definitions[defKey] = {
|
|
496
|
-
title: loader.key,
|
|
577
|
+
title: loader.title || loader.key,
|
|
497
578
|
...(loader.description ? { description: loader.description } : {}),
|
|
498
579
|
...(loader.icon ? { icon: loader.icon } : {}),
|
|
499
580
|
type: "object",
|
|
@@ -506,6 +587,9 @@ function buildLoaderDefinitions() {
|
|
|
506
587
|
},
|
|
507
588
|
...(loader.propsSchema?.properties || {}),
|
|
508
589
|
},
|
|
590
|
+
// Preserved so the admin can tell "props unknown — edit as JSON" (stub)
|
|
591
|
+
// apart from "takes no input" (real schema without the flag).
|
|
592
|
+
...(loader.propsSchema?.additionalProperties === true ? { additionalProperties: true } : {}),
|
|
509
593
|
};
|
|
510
594
|
|
|
511
595
|
manifestBlocks[loader.key] = {
|
|
@@ -544,6 +628,7 @@ function buildMatcherDefinitions() {
|
|
|
544
628
|
},
|
|
545
629
|
...(matcher.propsSchema?.properties || {}),
|
|
546
630
|
},
|
|
631
|
+
...(matcher.propsSchema?.additionalProperties === true ? { additionalProperties: true } : {}),
|
|
547
632
|
};
|
|
548
633
|
manifestBlocks[matcher.key] = {
|
|
549
634
|
$ref: `#/definitions/${defKey}`,
|