meno-core 1.0.20 → 1.0.21
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/lib/client/core/ComponentBuilder.test.ts +68 -56
- package/lib/client/core/ComponentBuilder.ts +6 -4
- package/lib/client/core/builders/embedBuilder.ts +10 -1
- package/lib/client/core/builders/index.ts +6 -2
- package/lib/client/core/builders/{cmsListBuilder.ts → listBuilder.ts} +227 -95
- package/lib/client/routing/Router.tsx +35 -7
- package/lib/client/templateEngine.test.ts +126 -0
- package/lib/client/templateEngine.ts +32 -11
- package/lib/server/ssr/attributeBuilder.ts +8 -0
- package/lib/server/ssr/index.ts +1 -1
- package/lib/server/ssr/ssrRenderer.ts +223 -110
- package/lib/server/ssrRenderer.test.ts +197 -3
- package/lib/shared/constants.test.ts +1 -1
- package/lib/shared/constants.ts +5 -1
- package/lib/shared/cssGeneration.test.ts +17 -0
- package/lib/shared/cssGeneration.ts +3 -2
- package/lib/shared/index.ts +3 -0
- package/lib/shared/itemTemplateUtils.test.ts +44 -2
- package/lib/shared/itemTemplateUtils.ts +15 -2
- package/lib/shared/nodeUtils.ts +23 -4
- package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +2 -2
- package/lib/shared/registry/nodeTypes/ListNodeType.ts +186 -0
- package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +6 -0
- package/lib/shared/registry/nodeTypes/index.ts +6 -5
- package/lib/shared/styleNodeUtils.ts +5 -5
- package/lib/shared/tree/PathBuilder.ts +3 -3
- package/lib/shared/treePathUtils.ts +7 -5
- package/lib/shared/types/cms.ts +4 -57
- package/lib/shared/types/components.ts +45 -4
- package/lib/shared/types/index.ts +13 -0
- package/lib/shared/validation/propValidator.ts +9 -1
- package/lib/shared/validation/schemas.ts +60 -14
- package/package.json +1 -1
- package/lib/shared/registry/nodeTypes/CMSListNodeType.ts +0 -109
|
@@ -16,7 +16,8 @@ import { SlotMarkerType, SlotMarkerSchema, type SlotMarker } from './SlotMarkerT
|
|
|
16
16
|
import { EmbedNodeType, EmbedNodeSchema, type EmbedNode } from './EmbedNodeType';
|
|
17
17
|
import { LinkNodeType, LinkNodeSchema, type LinkNode } from './LinkNodeType';
|
|
18
18
|
import { LocaleListNodeType, LocaleListNodeSchema, type LocaleListNode } from './LocaleListNodeType';
|
|
19
|
-
import {
|
|
19
|
+
import { ListNodeType, ListNodeSchema, type ListNode } from './ListNodeType';
|
|
20
|
+
|
|
20
21
|
/**
|
|
21
22
|
* All built-in node type definitions
|
|
22
23
|
* Type assertion needed because TypeScript sees each as a specific generic type
|
|
@@ -28,7 +29,7 @@ export const builtInNodeTypes: NodeTypeDefinition[] = [
|
|
|
28
29
|
EmbedNodeType as NodeTypeDefinition,
|
|
29
30
|
LinkNodeType as NodeTypeDefinition,
|
|
30
31
|
LocaleListNodeType as NodeTypeDefinition,
|
|
31
|
-
|
|
32
|
+
ListNodeType as NodeTypeDefinition,
|
|
32
33
|
];
|
|
33
34
|
|
|
34
35
|
/**
|
|
@@ -41,7 +42,7 @@ export const nodeSchemas = {
|
|
|
41
42
|
embed: EmbedNodeSchema,
|
|
42
43
|
link: LinkNodeSchema,
|
|
43
44
|
localeList: LocaleListNodeSchema,
|
|
44
|
-
|
|
45
|
+
list: ListNodeSchema,
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
/**
|
|
@@ -59,7 +60,7 @@ export { SlotMarkerType, SlotMarkerSchema, type SlotMarker } from './SlotMarkerT
|
|
|
59
60
|
export { EmbedNodeType, EmbedNodeSchema, type EmbedNode } from './EmbedNodeType';
|
|
60
61
|
export { LinkNodeType, LinkNodeSchema, type LinkNode } from './LinkNodeType';
|
|
61
62
|
export { LocaleListNodeType, LocaleListNodeSchema, type LocaleListNode } from './LocaleListNodeType';
|
|
62
|
-
export {
|
|
63
|
+
export { ListNodeType, ListNodeSchema, type ListNode } from './ListNodeType';
|
|
63
64
|
|
|
64
65
|
/**
|
|
65
66
|
* Discriminated union of all component node types
|
|
@@ -72,4 +73,4 @@ export type ComponentNode =
|
|
|
72
73
|
| EmbedNode
|
|
73
74
|
| LinkNode
|
|
74
75
|
| LocaleListNode
|
|
75
|
-
|
|
|
76
|
+
| ListNode;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { ComponentNode, HtmlNode, ComponentInstanceNode, StyleValue, ResponsiveStyleObject } from './types';
|
|
7
|
-
import { isComponentNode, isHtmlNode, isEmbedNode } from './nodeUtils';
|
|
7
|
+
import { isComponentNode, isHtmlNode, isEmbedNode, isListNode } from './nodeUtils';
|
|
8
8
|
import { isResponsiveStyle } from './styleUtils';
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -26,8 +26,8 @@ export function applyStylesToNode(
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
node.props.style = { ...(node.props.style || {}), ...styles };
|
|
29
|
-
} else if (isHtmlNode(node) || isEmbedNode(node)) {
|
|
30
|
-
// HTML node and
|
|
29
|
+
} else if (isHtmlNode(node) || isEmbedNode(node) || isListNode(node)) {
|
|
30
|
+
// HTML node, Embed node, and List node: put styles at top level
|
|
31
31
|
node.style = styles as StyleValue;
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -45,8 +45,8 @@ export function mergeNodeStyles(
|
|
|
45
45
|
): ComponentNode {
|
|
46
46
|
if (!instanceStyles) return node;
|
|
47
47
|
|
|
48
|
-
if (isHtmlNode(node) || isEmbedNode(node)) {
|
|
49
|
-
// For HTML nodes and
|
|
48
|
+
if (isHtmlNode(node) || isEmbedNode(node) || isListNode(node)) {
|
|
49
|
+
// For HTML nodes, Embed nodes, and List nodes: merge instance styles with existing top-level styles
|
|
50
50
|
const existingStyle = node.style;
|
|
51
51
|
if (existingStyle && typeof existingStyle === 'object') {
|
|
52
52
|
node.style = {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import type { Path } from '../pathArrayUtils';
|
|
11
11
|
import type { PageData, ComponentNode } from '../types';
|
|
12
12
|
import { getRootData } from '../treePathUtils';
|
|
13
|
-
import { isComponentNode, isSlotMarker, isLocaleListNode, isCMSListNode } from '../nodeUtils';
|
|
13
|
+
import { isComponentNode, isSlotMarker, isLocaleListNode, isCMSListNode, isListNode } from '../nodeUtils';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Node path data containing both logical and rendered paths
|
|
@@ -289,7 +289,7 @@ export function buildTreePathsWithRendered(
|
|
|
289
289
|
if (
|
|
290
290
|
typeof child === 'object' &&
|
|
291
291
|
child !== null &&
|
|
292
|
-
('tag' in child || 'component' in child || 'html' in child || 'src' in child || 'href' in child || isLocaleListNode(child) || isCMSListNode(child) || (cumulativeInstancePath && isSlotMarker(child)))
|
|
292
|
+
('tag' in child || 'component' in child || 'html' in child || 'src' in child || 'href' in child || isLocaleListNode(child) || isCMSListNode(child) || isListNode(child) || (cumulativeInstancePath && isSlotMarker(child)))
|
|
293
293
|
) {
|
|
294
294
|
const childPath = [...currentPath, index];
|
|
295
295
|
traverse(child as ComponentNode, childPath);
|
|
@@ -361,7 +361,7 @@ export function buildComponentTreePaths(
|
|
|
361
361
|
if (
|
|
362
362
|
typeof child === 'object' &&
|
|
363
363
|
child !== null &&
|
|
364
|
-
('tag' in child || 'component' in child || 'html' in child || 'src' in child || 'href' in child || isLocaleListNode(child) || isCMSListNode(child) || isSlotMarker(child))
|
|
364
|
+
('tag' in child || 'component' in child || 'html' in child || 'src' in child || 'href' in child || isLocaleListNode(child) || isCMSListNode(child) || isListNode(child) || isSlotMarker(child))
|
|
365
365
|
) {
|
|
366
366
|
const childPath = [...currentPath, index];
|
|
367
367
|
traverse(child as ComponentNode, childPath);
|
|
@@ -293,14 +293,16 @@ export function isComponentNodeParent(
|
|
|
293
293
|
// - SlotMarker: type === 'slot'
|
|
294
294
|
// - LinkNode: type === 'link'
|
|
295
295
|
// - EmbedNode: type === 'embed'
|
|
296
|
-
// -
|
|
296
|
+
// - ListNode: type === 'list' (also handles legacy 'cms-list' for migration)
|
|
297
|
+
const nodeType = (parent as any).type;
|
|
297
298
|
return 'type' in parent && (
|
|
298
299
|
'tag' in parent ||
|
|
299
300
|
'component' in parent ||
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
301
|
+
nodeType === 'slot' ||
|
|
302
|
+
nodeType === 'link' ||
|
|
303
|
+
nodeType === 'embed' ||
|
|
304
|
+
nodeType === 'cms-list' || // Legacy support for migration
|
|
305
|
+
nodeType === 'list'
|
|
304
306
|
);
|
|
305
307
|
}
|
|
306
308
|
|
package/lib/shared/types/cms.ts
CHANGED
|
@@ -147,64 +147,11 @@ export interface CMSListQuery {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* Context variables available in children (with named context):
|
|
154
|
-
* - {{<itemAs>.field}} - Access CMS item fields (e.g., {{post.title}})
|
|
155
|
-
* - {{<itemAs>Index}} - Zero-based index of current item (e.g., {{postIndex}})
|
|
156
|
-
* - {{<itemAs>First}} - Boolean, true if first item (e.g., {{postFirst}})
|
|
157
|
-
* - {{<itemAs>Last}} - Boolean, true if last item (e.g., {{postLast}})
|
|
158
|
-
*
|
|
159
|
-
* Legacy context (still supported):
|
|
160
|
-
* - {{item.field}}, {{itemIndex}}, {{itemFirst}}, {{itemLast}}
|
|
150
|
+
* @deprecated CMSListNode is now replaced by ListNode with sourceType: 'collection'.
|
|
151
|
+
* This type alias is kept for backward compatibility during migration.
|
|
152
|
+
* Import ListNode from '../registry/nodeTypes/ListNodeType' instead.
|
|
161
153
|
*/
|
|
162
|
-
export
|
|
163
|
-
type: 'cms-list';
|
|
164
|
-
/** Custom label displayed in structure tree */
|
|
165
|
-
label?: string;
|
|
166
|
-
/** Conditional rendering - skip node when false */
|
|
167
|
-
if?: { field: string; operator?: 'eq' | 'neq' | 'truthy' | 'falsy'; value?: unknown };
|
|
168
|
-
/** Inline styles */
|
|
169
|
-
style?: Record<string, unknown>;
|
|
170
|
-
/** Interactive CSS rules (hover, active, etc.) */
|
|
171
|
-
interactiveStyles?: Array<{
|
|
172
|
-
style: Record<string, unknown>;
|
|
173
|
-
name?: string;
|
|
174
|
-
prefix?: string;
|
|
175
|
-
postfix?: string;
|
|
176
|
-
previewProp?: string;
|
|
177
|
-
}>;
|
|
178
|
-
/** Generate element class without styles (for custom CSS) */
|
|
179
|
-
generateElementClass?: boolean;
|
|
180
|
-
/** HTML attributes */
|
|
181
|
-
attributes?: Record<string, string | number | boolean>;
|
|
182
|
-
/** Collection to query */
|
|
183
|
-
collection: string;
|
|
184
|
-
/** Variable name for item in templates (default: singular of collection) */
|
|
185
|
-
itemAs?: string;
|
|
186
|
-
/** Direct item IDs or template expression for referenced items (e.g., "{{post.authorId}}") */
|
|
187
|
-
items?: string | string[];
|
|
188
|
-
/** Filter conditions */
|
|
189
|
-
filter?: CMSFilterCondition | CMSFilterCondition[] | Record<string, unknown>;
|
|
190
|
-
/** Sort configuration */
|
|
191
|
-
sort?: CMSSortConfig | CMSSortConfig[];
|
|
192
|
-
/** Maximum number of items to return */
|
|
193
|
-
limit?: number;
|
|
194
|
-
/** Number of items to skip */
|
|
195
|
-
offset?: number;
|
|
196
|
-
/** Exclude the current CMS item from the list (useful for "related items" sections) */
|
|
197
|
-
excludeCurrentItem?: boolean;
|
|
198
|
-
/**
|
|
199
|
-
* Emit item template for dynamic client-side rendering.
|
|
200
|
-
* When true, SSR emits a `<template data-meno-item>` element with unprocessed
|
|
201
|
-
* {{item.field}} placeholders. MenoFilter uses this to render items beyond
|
|
202
|
-
* the SSR'd limit dynamically.
|
|
203
|
-
*/
|
|
204
|
-
emitTemplate?: boolean;
|
|
205
|
-
/** Children are repeated for each item */
|
|
206
|
-
children?: unknown[];
|
|
207
|
-
}
|
|
154
|
+
export type CMSListNode = import('../registry/nodeTypes/ListNodeType').ListNode;
|
|
208
155
|
|
|
209
156
|
/**
|
|
210
157
|
* Configuration for nested CMS list placeholders.
|
|
@@ -8,7 +8,7 @@ import type { ComponentNode } from './nodes';
|
|
|
8
8
|
/**
|
|
9
9
|
* Prop type definitions
|
|
10
10
|
*/
|
|
11
|
-
export type PropType = 'string' | 'select' | 'boolean' | 'number' | 'link' | 'file' | 'rich-text';
|
|
11
|
+
export type PropType = 'string' | 'select' | 'boolean' | 'number' | 'link' | 'file' | 'rich-text' | 'list';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Internationalization (i18n) value object
|
|
@@ -53,15 +53,56 @@ export interface LinkPropValue {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
*
|
|
56
|
+
* Base prop definition without list-specific fields
|
|
57
57
|
*/
|
|
58
|
-
export interface
|
|
59
|
-
type: PropType
|
|
58
|
+
export interface BasePropDefinition {
|
|
59
|
+
type: Exclude<PropType, 'list'>;
|
|
60
60
|
default?: string | number | boolean | I18nValue | LinkPropValue;
|
|
61
61
|
options?: readonly string[]; // Required for "select" type
|
|
62
62
|
accept?: string; // For "file" type: MIME pattern like "image/*", "video/*"
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* List item schema - defines the structure of each item in a list prop
|
|
67
|
+
* Uses the same prop types as component interfaces (except nested lists)
|
|
68
|
+
*/
|
|
69
|
+
export type ListItemSchema = Record<string, BasePropDefinition>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* List item value type
|
|
73
|
+
*/
|
|
74
|
+
export type ListItemValue = Record<string, string | number | boolean | LinkPropValue | null>;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* List prop definition - for array/list data
|
|
78
|
+
*/
|
|
79
|
+
export interface ListPropDefinition {
|
|
80
|
+
type: 'list';
|
|
81
|
+
/** Schema defining the structure of each list item */
|
|
82
|
+
itemSchema: ListItemSchema;
|
|
83
|
+
/** Default value is an array of items */
|
|
84
|
+
default?: ListItemValue[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Prop definition with improved type safety
|
|
89
|
+
*/
|
|
90
|
+
export type PropDefinition = BasePropDefinition | ListPropDefinition;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Type guard to check if a prop definition is a list type
|
|
94
|
+
*/
|
|
95
|
+
export function isListPropDefinition(def: PropDefinition): def is ListPropDefinition {
|
|
96
|
+
return def.type === 'list';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Type guard to check if a prop definition is a base (non-list) type
|
|
101
|
+
*/
|
|
102
|
+
export function isBasePropDefinition(def: PropDefinition): def is BasePropDefinition {
|
|
103
|
+
return def.type !== 'list';
|
|
104
|
+
}
|
|
105
|
+
|
|
65
106
|
/**
|
|
66
107
|
* Structured component definition
|
|
67
108
|
*/
|
|
@@ -22,6 +22,10 @@ export type {
|
|
|
22
22
|
ComponentDefinition,
|
|
23
23
|
StructuredComponentDefinition,
|
|
24
24
|
PropDefinition,
|
|
25
|
+
BasePropDefinition,
|
|
26
|
+
ListPropDefinition,
|
|
27
|
+
ListItemSchema,
|
|
28
|
+
ListItemValue,
|
|
25
29
|
PropType,
|
|
26
30
|
JSONPage,
|
|
27
31
|
PageData,
|
|
@@ -29,6 +33,12 @@ export type {
|
|
|
29
33
|
I18nValue,
|
|
30
34
|
I18nConfig,
|
|
31
35
|
LocaleConfig,
|
|
36
|
+
LinkPropValue,
|
|
37
|
+
} from './components';
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
isListPropDefinition,
|
|
41
|
+
isBasePropDefinition,
|
|
32
42
|
} from './components';
|
|
33
43
|
|
|
34
44
|
// Export all style types
|
|
@@ -80,6 +90,9 @@ export type {
|
|
|
80
90
|
ReferenceLocation,
|
|
81
91
|
} from './cms';
|
|
82
92
|
|
|
93
|
+
// Export ListNode from registry (unified list node type)
|
|
94
|
+
export type { ListNode } from '../registry/nodeTypes/ListNodeType';
|
|
95
|
+
|
|
83
96
|
// Export CMS utilities
|
|
84
97
|
export { singularize } from './cms';
|
|
85
98
|
|
|
@@ -94,7 +94,9 @@ function validateSingleProp(
|
|
|
94
94
|
return { valid: true };
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
const { type
|
|
97
|
+
const { type } = propDef;
|
|
98
|
+
// Options is only available on select type (from BasePropDefinition)
|
|
99
|
+
const options = 'options' in propDef ? propDef.options : undefined;
|
|
98
100
|
|
|
99
101
|
// Type checking and coercion
|
|
100
102
|
let coercedValue: unknown;
|
|
@@ -164,6 +166,12 @@ function validateSingleProp(
|
|
|
164
166
|
typeValid = typeof value === 'string';
|
|
165
167
|
break;
|
|
166
168
|
|
|
169
|
+
case 'list':
|
|
170
|
+
// List type accepts arrays
|
|
171
|
+
coercedValue = value;
|
|
172
|
+
typeValid = Array.isArray(value);
|
|
173
|
+
break;
|
|
174
|
+
|
|
167
175
|
default:
|
|
168
176
|
return {
|
|
169
177
|
valid: false,
|
|
@@ -13,21 +13,48 @@ import type { StyleObject, StyleMapping, LinkMapping } from '../types/styles';
|
|
|
13
13
|
import { NODE_TYPE } from '../constants';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Prop type schema - validates PropType values
|
|
16
|
+
* Prop type schema - validates PropType values (excluding list for base definition)
|
|
17
17
|
*/
|
|
18
|
-
export const
|
|
18
|
+
export const BasePropTypeSchema = z.enum(['string', 'select', 'boolean', 'number', 'link', 'file', 'rich-text']);
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
|
|
21
|
+
* Full prop type schema including list
|
|
22
|
+
*/
|
|
23
|
+
export const PropTypeSchema = z.enum(['string', 'select', 'boolean', 'number', 'link', 'file', 'rich-text', 'list']);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Base prop definition schema (for non-list props)
|
|
23
27
|
*/
|
|
24
|
-
export const
|
|
25
|
-
type:
|
|
28
|
+
export const BasePropDefinitionSchema = z.object({
|
|
29
|
+
type: BasePropTypeSchema,
|
|
26
30
|
default: z.union([z.string(), z.number(), z.boolean(), z.object({ href: z.string(), target: z.string().optional() })]).optional(),
|
|
27
31
|
options: z.array(z.string()).readonly().optional(),
|
|
28
32
|
accept: z.string().optional(), // For 'file' type: MIME pattern like "image/*", "video/*"
|
|
29
33
|
}).passthrough();
|
|
30
34
|
|
|
35
|
+
/**
|
|
36
|
+
* List item schema - defines structure of each item in a list prop
|
|
37
|
+
*/
|
|
38
|
+
export const ListItemSchemaSchema = z.record(z.string(), BasePropDefinitionSchema);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* List prop definition schema
|
|
42
|
+
*/
|
|
43
|
+
export const ListPropDefinitionSchema = z.object({
|
|
44
|
+
type: z.literal('list'),
|
|
45
|
+
itemSchema: ListItemSchemaSchema,
|
|
46
|
+
default: z.array(z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.object({ href: z.string(), target: z.string().optional() }), z.null()]))).optional(),
|
|
47
|
+
}).passthrough();
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Prop definition schema (union of base and list)
|
|
51
|
+
* Validates prop definitions from component interfaces
|
|
52
|
+
*/
|
|
53
|
+
export const PropDefinitionSchema = z.union([
|
|
54
|
+
ListPropDefinitionSchema,
|
|
55
|
+
BasePropDefinitionSchema,
|
|
56
|
+
]);
|
|
57
|
+
|
|
31
58
|
/**
|
|
32
59
|
* Style mapping schema
|
|
33
60
|
*/
|
|
@@ -124,10 +151,16 @@ export const InteractiveStylesSchema = z.array(InteractiveStyleRuleSchema);
|
|
|
124
151
|
|
|
125
152
|
/**
|
|
126
153
|
* Slot marker schema
|
|
154
|
+
* default: Optional default content when instance has no children
|
|
155
|
+
* Can be an array of ComponentNodes or a string
|
|
127
156
|
*/
|
|
128
|
-
export const SlotMarkerSchema = z.object({
|
|
157
|
+
export const SlotMarkerSchema: z.ZodType<any> = z.lazy(() => z.object({
|
|
129
158
|
type: z.literal(NODE_TYPE.SLOT),
|
|
130
|
-
|
|
159
|
+
default: z.union([
|
|
160
|
+
z.array(z.union([ComponentNodeSchema, z.string()])),
|
|
161
|
+
z.string(),
|
|
162
|
+
]).optional(),
|
|
163
|
+
}).passthrough());
|
|
131
164
|
|
|
132
165
|
/**
|
|
133
166
|
* Component node schema - discriminated union (forward declaration)
|
|
@@ -226,28 +259,41 @@ export const LocaleListNodeSchema: z.ZodType<any> = z.lazy(() => z.object({
|
|
|
226
259
|
}).passthrough());
|
|
227
260
|
|
|
228
261
|
/**
|
|
229
|
-
*
|
|
230
|
-
*
|
|
262
|
+
* Unified List node schema (basic version for ComponentNodeSchema union)
|
|
263
|
+
* Handles both prop-based lists and CMS collection-based lists.
|
|
264
|
+
* Uses sourceType to distinguish: 'prop' (default) or 'collection'.
|
|
265
|
+
* Also supports legacy 'cms-list' type for backward compatibility.
|
|
231
266
|
*/
|
|
232
|
-
export const
|
|
233
|
-
type: z.literal(NODE_TYPE.
|
|
234
|
-
|
|
267
|
+
export const ListNodeSchemaBasic: z.ZodType<any> = z.lazy(() => z.object({
|
|
268
|
+
type: z.union([z.literal(NODE_TYPE.LIST), z.literal('cms-list')]), // Support both for migration
|
|
269
|
+
sourceType: z.enum(['prop', 'collection']).optional(), // defaults to 'prop'
|
|
270
|
+
source: z.string().optional(), // Source prop name or collection name
|
|
271
|
+
collection: z.string().optional(), // Legacy field for cms-list migration
|
|
272
|
+
tag: z.string().optional(), // Wrapper element tag, defaults to 'div'
|
|
235
273
|
label: z.string().optional(),
|
|
236
274
|
if: IfConditionSchema.optional(),
|
|
237
275
|
style: StyleValueSchema.optional(),
|
|
238
276
|
interactiveStyles: InteractiveStylesSchema.optional(),
|
|
239
277
|
generateElementClass: z.boolean().optional(),
|
|
240
278
|
itemAs: z.string().optional(),
|
|
279
|
+
// Collection-specific options
|
|
241
280
|
items: z.union([z.string(), z.array(z.string())]).optional(),
|
|
242
281
|
filter: z.unknown().optional(),
|
|
243
282
|
sort: z.unknown().optional(),
|
|
244
283
|
limit: z.number().optional(),
|
|
245
284
|
offset: z.number().optional(),
|
|
246
285
|
excludeCurrentItem: z.boolean().optional(),
|
|
286
|
+
emitTemplate: z.boolean().optional(),
|
|
247
287
|
attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
248
288
|
children: z.array(z.union([ComponentNodeSchema, z.string()])).optional(),
|
|
249
289
|
}).passthrough());
|
|
250
290
|
|
|
291
|
+
/**
|
|
292
|
+
* @deprecated Use ListNodeSchemaBasic instead.
|
|
293
|
+
* Kept for backward compatibility during migration.
|
|
294
|
+
*/
|
|
295
|
+
export const CMSListNodeSchemaBasic = ListNodeSchemaBasic;
|
|
296
|
+
|
|
251
297
|
/**
|
|
252
298
|
* Component node schema - discriminated union (now defined after dependencies)
|
|
253
299
|
*/
|
|
@@ -258,7 +304,7 @@ ComponentNodeSchema = z.union([
|
|
|
258
304
|
EmbedNodeSchema,
|
|
259
305
|
LinkNodeSchema,
|
|
260
306
|
LocaleListNodeSchema,
|
|
261
|
-
|
|
307
|
+
ListNodeSchemaBasic,
|
|
262
308
|
]);
|
|
263
309
|
|
|
264
310
|
// Export ComponentNodeSchema
|
package/package.json
CHANGED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CMS List Node Type Definition
|
|
3
|
-
* Renders children for each CMS item in a collection
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { z } from 'zod';
|
|
7
|
-
import { createElement as h } from 'react';
|
|
8
|
-
import { StyleValueSchema, InteractiveStylesSchema, IfConditionSchema } from '../../validation/schemas';
|
|
9
|
-
import { createNodeType } from '../createNodeType';
|
|
10
|
-
import { NODE_TYPE } from '../../constants';
|
|
11
|
-
|
|
12
|
-
// Schema is the SINGLE source of truth
|
|
13
|
-
const CMSListNodeSchemaInternal = z.object({
|
|
14
|
-
type: z.literal('cms-list'),
|
|
15
|
-
label: z.string().optional(), // Custom label displayed in structure tree
|
|
16
|
-
if: IfConditionSchema.optional(), // Conditional rendering - skip node when false
|
|
17
|
-
style: StyleValueSchema.optional(),
|
|
18
|
-
interactiveStyles: InteractiveStylesSchema.optional(), // Interactive CSS rules (hover, active, etc.)
|
|
19
|
-
generateElementClass: z.boolean().optional(), // Generate element class without styles (for custom CSS)
|
|
20
|
-
attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
21
|
-
/** Collection to query */
|
|
22
|
-
collection: z.string(),
|
|
23
|
-
/** Filter conditions */
|
|
24
|
-
filter: z.union([
|
|
25
|
-
z.object({
|
|
26
|
-
field: z.string(),
|
|
27
|
-
operator: z.enum(['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'contains', 'in']).optional(),
|
|
28
|
-
value: z.unknown(),
|
|
29
|
-
}),
|
|
30
|
-
z.array(z.object({
|
|
31
|
-
field: z.string(),
|
|
32
|
-
operator: z.enum(['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'contains', 'in']).optional(),
|
|
33
|
-
value: z.unknown(),
|
|
34
|
-
})),
|
|
35
|
-
z.record(z.unknown()),
|
|
36
|
-
]).optional(),
|
|
37
|
-
/** Sort configuration */
|
|
38
|
-
sort: z.union([
|
|
39
|
-
z.object({
|
|
40
|
-
field: z.string(),
|
|
41
|
-
order: z.enum(['asc', 'desc']).optional(),
|
|
42
|
-
}),
|
|
43
|
-
z.array(z.object({
|
|
44
|
-
field: z.string(),
|
|
45
|
-
order: z.enum(['asc', 'desc']).optional(),
|
|
46
|
-
})),
|
|
47
|
-
]).optional(),
|
|
48
|
-
/** Maximum number of items to return */
|
|
49
|
-
limit: z.number().optional(),
|
|
50
|
-
/** Number of items to skip */
|
|
51
|
-
offset: z.number().optional(),
|
|
52
|
-
/** Children are repeated for each item */
|
|
53
|
-
children: z.array(z.unknown()).optional(),
|
|
54
|
-
}).passthrough();
|
|
55
|
-
|
|
56
|
-
// TypeScript type inferred from schema
|
|
57
|
-
export type CMSListNode = z.infer<typeof CMSListNodeSchemaInternal>;
|
|
58
|
-
|
|
59
|
-
// Export schema for validation/schemas.ts
|
|
60
|
-
export const CMSListNodeSchema = CMSListNodeSchemaInternal;
|
|
61
|
-
|
|
62
|
-
export const CMSListNodeType = createNodeType({
|
|
63
|
-
type: NODE_TYPE.CMS_LIST,
|
|
64
|
-
displayName: 'CMS List',
|
|
65
|
-
category: 'special',
|
|
66
|
-
schema: CMSListNodeSchemaInternal,
|
|
67
|
-
|
|
68
|
-
defaultValues: {
|
|
69
|
-
collection: '',
|
|
70
|
-
children: [],
|
|
71
|
-
style: { base: {} },
|
|
72
|
-
},
|
|
73
|
-
|
|
74
|
-
treeDisplay: {
|
|
75
|
-
icon: 'HTML_ELEMENT',
|
|
76
|
-
getLabel: (node) => {
|
|
77
|
-
const cmsNode = node as CMSListNode;
|
|
78
|
-
return cmsNode.collection ? `CMS List: ${cmsNode.collection}` : 'CMS List';
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
|
|
82
|
-
clientRenderer: (node, context) => {
|
|
83
|
-
const cmsNode = node as CMSListNode;
|
|
84
|
-
// In editor, shows placeholder with collection name
|
|
85
|
-
return h('div', {
|
|
86
|
-
key: context.key,
|
|
87
|
-
'data-cms-list': 'true',
|
|
88
|
-
'data-collection': cmsNode.collection,
|
|
89
|
-
style: {
|
|
90
|
-
padding: '8px 12px',
|
|
91
|
-
background: 'rgba(139, 92, 246, 0.1)',
|
|
92
|
-
border: '1px dashed rgba(139, 92, 246, 0.5)',
|
|
93
|
-
borderRadius: '4px',
|
|
94
|
-
color: '#8b5cf6',
|
|
95
|
-
fontSize: '12px',
|
|
96
|
-
},
|
|
97
|
-
}, `[CMS List: ${cmsNode.collection || 'No collection'}]`);
|
|
98
|
-
},
|
|
99
|
-
|
|
100
|
-
ssrRenderer: (_node, _context) => {
|
|
101
|
-
// Placeholder - actual SSR is handled by processCMSList in ssrRenderer.ts
|
|
102
|
-
return '<!-- cms-list rendered by processCMSList -->';
|
|
103
|
-
},
|
|
104
|
-
|
|
105
|
-
capabilities: {
|
|
106
|
-
canHaveChildren: true,
|
|
107
|
-
requiresProps: ['collection'],
|
|
108
|
-
},
|
|
109
|
-
});
|