Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions examples/blog/templates/Template5050/Template5050.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ const assets = Object.values(assetModules).map(

export default schema.define({
name: 'Template5050',
metadata: {
title: 'Two Column Layout',
description: 'A balanced two-column layout with customizable content and assets',
image: 'https://images.unsplash.com/photo-1586953208448-b95a79798f07?w=300&h=200&fit=crop&auto=format&q=80'
},
description: 'Basic 50x50.',
fields: [
schema.string({
Expand Down
5 changes: 5 additions & 0 deletions examples/blog/templates/TemplateHero/TemplateHero.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import {schema} from '@blinkk/root-cms';

export default schema.define({
name: 'TemplateHero',
metadata: {
title: 'Hero Section',
description: 'A prominent hero section with customizable content and image',
image: 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=300&h=200&fit=crop&auto=format&q=80'
},
description: 'Basic hero.',
fields: [
schema.string({
Expand Down
41 changes: 41 additions & 0 deletions packages/root-cms/core/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,3 +416,44 @@ test('define schema', () => {
}
`);
});

test('define schema with metadata', () => {
const schemaWithMetadata = schema.define({
name: 'TemplateHomepageHero',
metadata: {
title: 'Home Hero',
description: 'A prominent hero section for the homepage',
image: '/images/hero-thumbnail.jpg'
},
description: 'Homepage hero section',
fields: []
});

expect(schemaWithMetadata).toEqual({
name: 'TemplateHomepageHero',
metadata: {
title: 'Home Hero',
description: 'A prominent hero section for the homepage',
image: '/images/hero-thumbnail.jpg'
},
description: 'Homepage hero section',
fields: []
});
});

test('define schema without metadata', () => {
const schemaWithoutMetadata = schema.define({
name: 'TemplateBasic',
description: 'Basic template',
fields: []
});

expect(schemaWithoutMetadata).toEqual({
name: 'TemplateBasic',
description: 'Basic template',
fields: []
});

// Verify that metadata is undefined (backward compatibility)
expect(schemaWithoutMetadata.metadata).toBeUndefined();
});
5 changes: 5 additions & 0 deletions packages/root-cms/core/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,11 @@ export type ObjectLikeField =

export interface Schema {
name: string;
metadata?: {
title?: string;
description?: string;
image?: string;
};
description?: string;
fields: FieldWithId[];
}
Expand Down
112 changes: 100 additions & 12 deletions packages/root-cms/ui/components/DocEditor/DocEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Button,
LoadingOverlay,
Menu,
Modal,
Select,
Tooltip,
} from '@mantine/core';
Expand All @@ -17,6 +18,7 @@ import {
IconClipboardCopy,
IconCopy,
IconDotsVertical,
IconGridDots,
IconLanguage,
IconLock,
IconPlanet,
Expand Down Expand Up @@ -1124,13 +1126,17 @@ DocEditor.ArrayField = (props: FieldProps) => {
DocEditor.OneOfField = (props: FieldProps) => {
const field = props.field as schema.OneOfField;
const [type, setType] = useState('');
const [modalOpen, setModalOpen] = useState(false);
const typesMap: Record<string, schema.Schema> = {};
const dropdownValues: Array<{value: string; label: string}> = [
{value: '', label: field.placeholder || 'Select type'},
];
field.types.forEach((type) => {
typesMap[type.name] = type;
dropdownValues.push({value: type.name, label: type.name});
dropdownValues.push({
value: type.name,
label: type.metadata?.title || type.name
});
});
const selectedType = typesMap[type || ''];

Expand Down Expand Up @@ -1174,18 +1180,100 @@ DocEditor.OneOfField = (props: FieldProps) => {
<div className="DocEditor__OneOfField">
<div className="DocEditor__OneOfField__select">
<div className="DocEditor__OneOfField__select__label">Type:</div>
<Select
data={dropdownValues}
value={type}
placeholder={field.placeholder}
onChange={(e: string) => onTypeChange(e || '')}
size="xs"
radius={0}
searchable
// Due to issues with preact/compat, use a div for the dropdown el.
dropdownComponent="div"
/>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Select
data={dropdownValues}
value={type}
placeholder={field.placeholder}
onChange={(e: string) => onTypeChange(e || '')}
size="xs"
radius={0}
searchable
// Due to issues with preact/compat, use a div for the dropdown el.
dropdownComponent="div"
/>
<Tooltip label="Browse templates">
<ActionIcon
size="xs"
variant="default"
onClick={() => setModalOpen(true)}
>
<IconGridDots size={16} />
</ActionIcon>
</Tooltip>
</div>
</div>

<Modal
opened={modalOpen}
onClose={() => setModalOpen(false)}
title="Select Template"
size="lg"
>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{field.types.map((schemaType) => (
<div
key={schemaType.name}
style={{
border: type === schemaType.name ? '2px solid #228be6' : '1px solid #e9ecef',
borderRadius: '8px',
padding: '16px',
cursor: 'pointer',
backgroundColor: type === schemaType.name ? '#f0f8ff' : 'white',
}}
onClick={() => {
onTypeChange(schemaType.name);
setModalOpen(false);
}}
>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '12px' }}>
{schemaType.metadata?.image && (
<img
src={schemaType.metadata.image}
alt={schemaType.metadata.title || schemaType.name}
style={{
width: '60px',
height: '60px',
objectFit: 'cover',
borderRadius: '4px',
flexShrink: 0,
}}
/>
)}
<div style={{ flex: 1 }}>
<div style={{
fontWeight: 'bold',
fontSize: '14px',
marginBottom: '4px',
color: '#212529'
}}>
{schemaType.metadata?.title || schemaType.name}
</div>
{schemaType.metadata?.description && (
<div style={{
fontSize: '12px',
color: '#6c757d',
lineHeight: '1.4'
}}>
{schemaType.metadata.description}
</div>
)}
{!schemaType.metadata?.description && schemaType.description && (
<div style={{
fontSize: '12px',
color: '#6c757d',
lineHeight: '1.4'
}}>
{schemaType.description}
</div>
)}
</div>
</div>
</div>
))}
</div>
</Modal>

{selectedType && (
<div className="DocEditor__OneOfField__fields">
{selectedType.fields.map((field) => (
Expand Down