mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-11 14:52:55 +01:00
feat(editor): implement embedded template system (TASK-009)
- Add ProjectTemplate TypeScript interfaces for type-safe templates - Implement EmbeddedTemplateProvider for bundled templates - Create Hello World template (Router + Home page + Text) - Update LocalProjectsModel to use embedded templates by default - Remove programmatic project creation workaround - Fix: Add required fields (id, comments, metadata) per TASK-010 - Fix: Correct node type 'PageRouter' → 'Router' - Add comprehensive developer documentation Benefits: - No more path resolution issues (__dirname/process.cwd()) - Works identically in dev and production - Type-safe template definitions - Easy to add new templates Closes TASK-009 (Phase 3 - Editor UX Overhaul)
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
# TASK-009: Template System Refactoring
|
# TASK-009: Template System Refactoring
|
||||||
|
|
||||||
**Status**: 📋 Planned
|
**Status**: 🟢 Complete (Backend)
|
||||||
**Priority**: Medium
|
**Priority**: Medium
|
||||||
**Complexity**: Medium
|
**Complexity**: Medium
|
||||||
**Estimated Effort**: 2-3 days
|
**Actual Effort**: 1 day (Backend implementation)
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
@@ -244,12 +244,100 @@ class TemplateManager {
|
|||||||
- Failed attempt: `packages/noodl-editor/src/editor/src/utils/forge/template/providers/local-template-provider.ts`
|
- Failed attempt: `packages/noodl-editor/src/editor/src/utils/forge/template/providers/local-template-provider.ts`
|
||||||
- Template registry: `packages/noodl-editor/src/editor/src/utils/forge/index.ts`
|
- Template registry: `packages/noodl-editor/src/editor/src/utils/forge/index.ts`
|
||||||
|
|
||||||
|
## Implementation Summary (January 9, 2026)
|
||||||
|
|
||||||
|
### ✅ What Was Completed
|
||||||
|
|
||||||
|
**Phase 1-3: Backend Implementation (Complete)**
|
||||||
|
|
||||||
|
1. **Type System Created**
|
||||||
|
|
||||||
|
- `ProjectTemplate.ts` - Complete TypeScript interfaces for templates
|
||||||
|
- Comprehensive type definitions for components, nodes, connections, and settings
|
||||||
|
|
||||||
|
2. **EmbeddedTemplateProvider Implemented**
|
||||||
|
|
||||||
|
- Provider that handles `embedded://` protocol
|
||||||
|
- Templates stored as TypeScript objects, bundled by webpack
|
||||||
|
- No file I/O dependencies, works identically in dev and production
|
||||||
|
|
||||||
|
3. **Hello World Template Created**
|
||||||
|
|
||||||
|
- Structure: App → PageRouter → Page "/Home" → Text "Hello World!"
|
||||||
|
- Clean and minimal, demonstrates Page Router usage
|
||||||
|
- Located in `models/template/templates/hello-world.template.ts`
|
||||||
|
|
||||||
|
4. **Template Registry Integration**
|
||||||
|
|
||||||
|
- `EmbeddedTemplateProvider` registered with highest priority
|
||||||
|
- Backward compatible with existing HTTP/Noodl Docs providers
|
||||||
|
|
||||||
|
5. **LocalProjectsModel Updated**
|
||||||
|
|
||||||
|
- Removed programmatic project creation workaround
|
||||||
|
- Default template now uses `embedded://hello-world`
|
||||||
|
- Maintains backward compatibility with external templates
|
||||||
|
|
||||||
|
6. **Documentation**
|
||||||
|
- Complete developer guide in `models/template/README.md`
|
||||||
|
- Instructions for creating custom templates
|
||||||
|
- Architecture overview and best practices
|
||||||
|
|
||||||
|
### 📁 Files Created
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/noodl-editor/src/editor/src/models/template/
|
||||||
|
├── ProjectTemplate.ts # Type definitions
|
||||||
|
├── EmbeddedTemplateProvider.ts # Provider implementation
|
||||||
|
├── README.md # Developer documentation
|
||||||
|
└── templates/
|
||||||
|
└── hello-world.template.ts # Default template
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📝 Files Modified
|
||||||
|
|
||||||
|
- `utils/forge/index.ts` - Registered EmbeddedTemplateProvider
|
||||||
|
- `utils/LocalProjectsModel.ts` - Updated newProject() to use embedded templates
|
||||||
|
|
||||||
|
### 🎯 Benefits Achieved
|
||||||
|
|
||||||
|
✅ No more `__dirname` or `process.cwd()` path resolution issues
|
||||||
|
✅ Templates work identically in development and production builds
|
||||||
|
✅ Type-safe template definitions with full IDE support
|
||||||
|
✅ Easy to add new templates - just create a TypeScript file
|
||||||
|
✅ Maintains backward compatibility with external template URLs
|
||||||
|
|
||||||
|
### ⏳ Remaining Work (Future Tasks)
|
||||||
|
|
||||||
|
- **UI for Template Selection**: Gallery/dialog to choose templates when creating projects
|
||||||
|
- **Additional Templates**: Blank, Dashboard, E-commerce templates
|
||||||
|
- **Template Export**: Allow users to save their projects as templates
|
||||||
|
- **Unit Tests**: Test suite for EmbeddedTemplateProvider
|
||||||
|
- **Template Validation**: Verify template structure before project creation
|
||||||
|
|
||||||
|
### 🚀 Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Create project with embedded template (automatic default)
|
||||||
|
LocalProjectsModel.instance.newProject(callback, {
|
||||||
|
name: 'My Project'
|
||||||
|
// Uses 'embedded://hello-world' by default
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create project with specific template
|
||||||
|
LocalProjectsModel.instance.newProject(callback, {
|
||||||
|
name: 'My Project',
|
||||||
|
projectTemplate: 'embedded://hello-world'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
## Related Tasks
|
## Related Tasks
|
||||||
|
|
||||||
- None yet (this is the first comprehensive template system task)
|
- **TASK-009-UI**: Template selection gallery (future)
|
||||||
|
- **TASK-009-EXPORT**: Template export functionality (future)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Created**: January 8, 2026
|
**Created**: January 8, 2026
|
||||||
**Last Updated**: January 8, 2026
|
**Last Updated**: January 9, 2026
|
||||||
**Assignee**: TBD
|
**Implementation**: January 9, 2026 (Backend complete)
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* EmbeddedTemplateProvider
|
||||||
|
*
|
||||||
|
* Provides access to templates that are embedded directly in the application code.
|
||||||
|
* These templates are bundled with the editor and work reliably in both
|
||||||
|
* development and production (no file I/O or path resolution issues).
|
||||||
|
*
|
||||||
|
* @module noodl-editor/models/template
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ITemplateProvider, TemplateItem } from '../../utils/forge/template/template';
|
||||||
|
import { ProjectTemplate } from './ProjectTemplate';
|
||||||
|
import { helloWorldTemplate } from './templates/hello-world.template';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider for templates that are embedded in the application code
|
||||||
|
*/
|
||||||
|
export class EmbeddedTemplateProvider implements ITemplateProvider {
|
||||||
|
/**
|
||||||
|
* Registry of all embedded templates
|
||||||
|
* New templates should be added here
|
||||||
|
*/
|
||||||
|
private templates: Map<string, ProjectTemplate> = new Map([
|
||||||
|
['hello-world', helloWorldTemplate]
|
||||||
|
// Add more templates here as they are created
|
||||||
|
]);
|
||||||
|
|
||||||
|
get name(): string {
|
||||||
|
return 'embedded-templates';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all available embedded templates
|
||||||
|
* @returns Array of template items
|
||||||
|
*/
|
||||||
|
async list(): Promise<ReadonlyArray<TemplateItem>> {
|
||||||
|
const items: TemplateItem[] = [];
|
||||||
|
|
||||||
|
for (const [id, template] of this.templates) {
|
||||||
|
items.push({
|
||||||
|
title: template.name,
|
||||||
|
desc: template.description,
|
||||||
|
category: template.category,
|
||||||
|
iconURL: template.thumbnail || '',
|
||||||
|
projectURL: `embedded://${id}`,
|
||||||
|
useCloudServices: false,
|
||||||
|
cloudServicesTemplateURL: undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this provider can handle the given URL
|
||||||
|
* @param url - The template URL to check
|
||||||
|
* @returns True if URL starts with "embedded://"
|
||||||
|
*/
|
||||||
|
async canDownload(url: string): Promise<boolean> {
|
||||||
|
return url.startsWith('embedded://');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Download" (copy) the template to the destination directory
|
||||||
|
*
|
||||||
|
* Note: For embedded templates, we write the project.json directly
|
||||||
|
* rather than copying files from disk.
|
||||||
|
*
|
||||||
|
* @param url - Template URL (e.g., "embedded://hello-world")
|
||||||
|
* @param destination - Destination directory path
|
||||||
|
* @returns Promise that resolves when template is written
|
||||||
|
*/
|
||||||
|
async download(url: string, destination: string): Promise<void> {
|
||||||
|
// Extract template ID from URL
|
||||||
|
const templateId = url.replace('embedded://', '');
|
||||||
|
|
||||||
|
const template = this.templates.get(templateId);
|
||||||
|
if (!template) {
|
||||||
|
throw new Error(`Unknown embedded template: ${templateId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the template content (which will have its name overridden by the caller)
|
||||||
|
const projectContent = template.content;
|
||||||
|
|
||||||
|
// Ensure destination directory exists
|
||||||
|
const { filesystem } = await import('@noodl/platform');
|
||||||
|
|
||||||
|
// Create destination directory if it doesn't exist
|
||||||
|
if (!filesystem.exists(destination)) {
|
||||||
|
await filesystem.makeDirectory(destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write project.json to destination
|
||||||
|
const projectJsonPath = filesystem.join(destination, 'project.json');
|
||||||
|
await filesystem.writeFile(projectJsonPath, JSON.stringify(projectContent, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific template by ID (utility method)
|
||||||
|
* @param id - Template ID
|
||||||
|
* @returns The template, or undefined if not found
|
||||||
|
*/
|
||||||
|
getTemplate(id: string): ProjectTemplate | undefined {
|
||||||
|
return this.templates.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all template IDs (utility method)
|
||||||
|
* @returns Array of template IDs
|
||||||
|
*/
|
||||||
|
getTemplateIds(): string[] {
|
||||||
|
return Array.from(this.templates.keys());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
/**
|
||||||
|
* ProjectTemplate
|
||||||
|
*
|
||||||
|
* Defines the structure for project templates that can be used
|
||||||
|
* to create new projects with pre-configured components and settings.
|
||||||
|
*
|
||||||
|
* @module noodl-editor/models/template
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a complete project template structure
|
||||||
|
*/
|
||||||
|
export interface ProjectTemplate {
|
||||||
|
/** Unique identifier for the template */
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/** Display name of the template */
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/** Description of what the template provides */
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
/** Category for grouping templates (e.g., "Getting Started", "Dashboard") */
|
||||||
|
category: string;
|
||||||
|
|
||||||
|
/** Template version (semver) */
|
||||||
|
version: string;
|
||||||
|
|
||||||
|
/** Optional thumbnail/icon URL for UI display */
|
||||||
|
thumbnail?: string;
|
||||||
|
|
||||||
|
/** The actual project content */
|
||||||
|
content: ProjectContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The core content structure of a Noodl project
|
||||||
|
*/
|
||||||
|
export interface ProjectContent {
|
||||||
|
/** Project name (will be overridden by user input) */
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/** Array of component definitions */
|
||||||
|
components: ComponentDefinition[];
|
||||||
|
|
||||||
|
/** Project-level settings */
|
||||||
|
settings?: ProjectSettings;
|
||||||
|
|
||||||
|
/** Project metadata */
|
||||||
|
metadata?: ProjectMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definition of a single component in the project
|
||||||
|
*/
|
||||||
|
export interface ComponentDefinition {
|
||||||
|
/** Component name (e.g., "App", "/#__page__/Home") */
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/** Component graph structure */
|
||||||
|
graph?: ComponentGraph;
|
||||||
|
|
||||||
|
/** Whether this is a visual component */
|
||||||
|
visual?: boolean;
|
||||||
|
|
||||||
|
/** Component ID (optional, will be generated if not provided) */
|
||||||
|
id?: string;
|
||||||
|
|
||||||
|
/** Port definitions for the component */
|
||||||
|
ports?: PortDefinition[];
|
||||||
|
|
||||||
|
/** Visual state transitions (for visual components) */
|
||||||
|
visualStateTransitions?: unknown[];
|
||||||
|
|
||||||
|
/** Component metadata */
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component graph containing nodes and connections
|
||||||
|
*/
|
||||||
|
export interface ComponentGraph {
|
||||||
|
/** Root nodes in the component */
|
||||||
|
roots: NodeDefinition[];
|
||||||
|
|
||||||
|
/** Connections between nodes */
|
||||||
|
connections: ConnectionDefinition[];
|
||||||
|
|
||||||
|
/** Comments in the graph (required by NodeGraphModel) */
|
||||||
|
comments?: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definition of a single node in the component graph
|
||||||
|
*/
|
||||||
|
export interface NodeDefinition {
|
||||||
|
/** Unique node ID */
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/** Node type (e.g., "Group", "Text", "PageRouter") */
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
/** X position on canvas */
|
||||||
|
x: number;
|
||||||
|
|
||||||
|
/** Y position on canvas */
|
||||||
|
y: number;
|
||||||
|
|
||||||
|
/** Node parameters/properties */
|
||||||
|
parameters: Record<string, unknown>;
|
||||||
|
|
||||||
|
/** Port definitions */
|
||||||
|
ports?: PortDefinition[];
|
||||||
|
|
||||||
|
/** Child nodes (for visual hierarchy) */
|
||||||
|
children?: NodeDefinition[];
|
||||||
|
|
||||||
|
/** Variant (for some node types) */
|
||||||
|
variant?: string;
|
||||||
|
|
||||||
|
/** State parameters (for state nodes) */
|
||||||
|
stateParameters?: Record<string, unknown>;
|
||||||
|
|
||||||
|
/** State transitions (for state nodes) */
|
||||||
|
stateTransitions?: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connection between two nodes
|
||||||
|
*/
|
||||||
|
export interface ConnectionDefinition {
|
||||||
|
/** Source node ID */
|
||||||
|
fromId: string;
|
||||||
|
|
||||||
|
/** Source port/property name */
|
||||||
|
fromProperty: string;
|
||||||
|
|
||||||
|
/** Target node ID */
|
||||||
|
toId: string;
|
||||||
|
|
||||||
|
/** Target port/property name */
|
||||||
|
toProperty: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Port definition for components/nodes
|
||||||
|
*/
|
||||||
|
export interface PortDefinition {
|
||||||
|
/** Port name */
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/** Port type (e.g., "string", "number", "signal") */
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
/** Port direction ("input" or "output") */
|
||||||
|
plug: 'input' | 'output';
|
||||||
|
|
||||||
|
/** Port index (for ordering) */
|
||||||
|
index?: number;
|
||||||
|
|
||||||
|
/** Default value */
|
||||||
|
default?: unknown;
|
||||||
|
|
||||||
|
/** Display name */
|
||||||
|
displayName?: string;
|
||||||
|
|
||||||
|
/** Port group */
|
||||||
|
group?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project-level settings
|
||||||
|
*/
|
||||||
|
export interface ProjectSettings {
|
||||||
|
/** Project settings go here */
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project metadata
|
||||||
|
*/
|
||||||
|
export interface ProjectMetadata {
|
||||||
|
/** Project title */
|
||||||
|
title?: string;
|
||||||
|
|
||||||
|
/** Project description */
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
/** Other metadata fields */
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
173
packages/noodl-editor/src/editor/src/models/template/README.md
Normal file
173
packages/noodl-editor/src/editor/src/models/template/README.md
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# Template System Documentation
|
||||||
|
|
||||||
|
This directory contains the embedded project template system implemented in TASK-009.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The template system allows creating new Noodl projects from pre-defined templates that are embedded directly in the application code. This ensures templates work reliably in both development and production without file path resolution issues.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
models/template/
|
||||||
|
├── ProjectTemplate.ts # TypeScript interfaces for templates
|
||||||
|
├── EmbeddedTemplateProvider.ts # Provider for embedded templates
|
||||||
|
├── templates/ # Template definitions
|
||||||
|
│ └── hello-world.template.ts # Default Hello World template
|
||||||
|
└── README.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. **Template Definition**: Templates are defined as TypeScript objects using the `ProjectTemplate` interface
|
||||||
|
2. **Provider Registration**: The `EmbeddedTemplateProvider` is registered in `utils/forge/index.ts` with the highest priority
|
||||||
|
3. **Template Usage**: When creating a new project, templates are referenced via `embedded://template-id` URLs
|
||||||
|
4. **Project Creation**: The provider writes the template's `project.json` directly to the destination directory
|
||||||
|
|
||||||
|
## Creating a New Template
|
||||||
|
|
||||||
|
### Step 1: Define Your Template
|
||||||
|
|
||||||
|
Create a new file in `templates/` (e.g., `dashboard.template.ts`):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ProjectTemplate } from '../ProjectTemplate';
|
||||||
|
|
||||||
|
export const dashboardTemplate: ProjectTemplate = {
|
||||||
|
id: 'dashboard',
|
||||||
|
name: 'Dashboard Template',
|
||||||
|
description: 'A dashboard with navigation and multiple pages',
|
||||||
|
category: 'Business Apps',
|
||||||
|
version: '1.0.0',
|
||||||
|
thumbnail: undefined,
|
||||||
|
|
||||||
|
content: {
|
||||||
|
name: 'Dashboard Project',
|
||||||
|
components: [
|
||||||
|
// Define your components here
|
||||||
|
{
|
||||||
|
name: 'App',
|
||||||
|
visual: true,
|
||||||
|
ports: [],
|
||||||
|
visualStateTransitions: [],
|
||||||
|
graph: {
|
||||||
|
roots: [
|
||||||
|
// Add your nodes here
|
||||||
|
],
|
||||||
|
connections: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
settings: {},
|
||||||
|
metadata: {
|
||||||
|
title: 'Dashboard Project',
|
||||||
|
description: 'A complete dashboard template'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Register the Template
|
||||||
|
|
||||||
|
Add your template to `EmbeddedTemplateProvider.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { dashboardTemplate } from './templates/dashboard.template';
|
||||||
|
|
||||||
|
export class EmbeddedTemplateProvider implements ITemplateProvider {
|
||||||
|
private templates: Map<string, ProjectTemplate> = new Map([
|
||||||
|
['hello-world', helloWorldTemplate],
|
||||||
|
['dashboard', dashboardTemplate] // Add your template here
|
||||||
|
]);
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Use Your Template
|
||||||
|
|
||||||
|
Create a project with your template:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
LocalProjectsModel.instance.newProject(
|
||||||
|
(project) => {
|
||||||
|
console.log('Project created:', project);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'My Dashboard',
|
||||||
|
projectTemplate: 'embedded://dashboard'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Template Structure Reference
|
||||||
|
|
||||||
|
### Component Definition
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
name: 'ComponentName', // Component name (use '/#__page__/Name' for pages)
|
||||||
|
visual: true, // Whether this is a visual component
|
||||||
|
ports: [], // Component ports
|
||||||
|
visualStateTransitions: [], // State transitions
|
||||||
|
graph: {
|
||||||
|
roots: [/* nodes */], // Root-level nodes
|
||||||
|
connections: [] // Connections between nodes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Node Definition
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
id: generateId(), // Unique node ID
|
||||||
|
type: 'NodeType', // Node type (e.g., 'Text', 'Group', 'PageRouter')
|
||||||
|
x: 100, // X position on canvas
|
||||||
|
y: 100, // Y position on canvas
|
||||||
|
parameters: { // Node parameters
|
||||||
|
text: 'Hello',
|
||||||
|
fontSize: { value: 16, unit: 'px' }
|
||||||
|
},
|
||||||
|
ports: [], // Node-specific ports
|
||||||
|
children: [] // Child nodes (for visual hierarchy)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Use the Helper Function**: Use the `generateId()` function for generating unique IDs
|
||||||
|
2. **Structure Over Data**: Define component structure, not specific user data
|
||||||
|
3. **Minimal & Clear**: Keep templates simple and focused on structure
|
||||||
|
4. **Test Both Modes**: Test templates in both development and production builds
|
||||||
|
5. **Document Purpose**: Add JSDoc comments explaining what the template provides
|
||||||
|
|
||||||
|
## Default Template
|
||||||
|
|
||||||
|
When no template is specified in `newProject()`, the system automatically uses `embedded://hello-world` as the default template.
|
||||||
|
|
||||||
|
## Advantages Over Previous System
|
||||||
|
|
||||||
|
✅ **No Path Resolution Issues**: Templates are embedded in code, bundled by webpack
|
||||||
|
✅ **Dev/Prod Parity**: Works identically in development and production
|
||||||
|
✅ **Type Safety**: Full TypeScript support with interfaces
|
||||||
|
✅ **Easy to Extend**: Add new templates by creating a file and registering it
|
||||||
|
✅ **No External Dependencies**: No need for external template files or URLs
|
||||||
|
|
||||||
|
## Migration from Old System
|
||||||
|
|
||||||
|
The old system used:
|
||||||
|
|
||||||
|
- Programmatic project creation (JSON literal in code)
|
||||||
|
- File-based templates (with path resolution issues)
|
||||||
|
- External template URLs
|
||||||
|
|
||||||
|
The new system:
|
||||||
|
|
||||||
|
- Uses embedded template objects
|
||||||
|
- Provides a consistent API via `templateRegistry`
|
||||||
|
- Maintains backward compatibility with external template URLs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: January 9, 2026
|
||||||
|
**Related**: TASK-009-template-system-refactoring
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* Hello World Template
|
||||||
|
*
|
||||||
|
* A simple starter project with:
|
||||||
|
* - App component (root)
|
||||||
|
* - Page Router configured
|
||||||
|
* - Home page with "Hello World" text
|
||||||
|
*
|
||||||
|
* @module noodl-editor/models/template/templates
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ProjectTemplate } from '../ProjectTemplate';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a unique ID for nodes
|
||||||
|
*/
|
||||||
|
function generateId(): string {
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||||
|
const r = (Math.random() * 16) | 0;
|
||||||
|
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hello World template
|
||||||
|
* Creates a basic project with Page Router and a home page
|
||||||
|
*/
|
||||||
|
export const helloWorldTemplate: ProjectTemplate = {
|
||||||
|
id: 'hello-world',
|
||||||
|
name: 'Hello World',
|
||||||
|
description: 'A simple starter project with a home page displaying "Hello World"',
|
||||||
|
category: 'Getting Started',
|
||||||
|
version: '1.0.0',
|
||||||
|
thumbnail: undefined,
|
||||||
|
|
||||||
|
content: {
|
||||||
|
name: 'Hello World Project',
|
||||||
|
components: [
|
||||||
|
// App component (root)
|
||||||
|
{
|
||||||
|
name: 'App',
|
||||||
|
id: generateId(),
|
||||||
|
visual: true,
|
||||||
|
ports: [],
|
||||||
|
visualStateTransitions: [],
|
||||||
|
graph: {
|
||||||
|
roots: [
|
||||||
|
{
|
||||||
|
id: generateId(),
|
||||||
|
type: 'Router',
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
parameters: {
|
||||||
|
startPage: '/#__page__/Home'
|
||||||
|
},
|
||||||
|
ports: [],
|
||||||
|
children: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
connections: [],
|
||||||
|
comments: []
|
||||||
|
},
|
||||||
|
metadata: {}
|
||||||
|
},
|
||||||
|
// Home Page component
|
||||||
|
{
|
||||||
|
name: '/#__page__/Home',
|
||||||
|
id: generateId(),
|
||||||
|
visual: true,
|
||||||
|
ports: [],
|
||||||
|
visualStateTransitions: [],
|
||||||
|
graph: {
|
||||||
|
roots: [
|
||||||
|
{
|
||||||
|
id: generateId(),
|
||||||
|
type: 'Text',
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
parameters: {
|
||||||
|
text: 'Hello World!',
|
||||||
|
fontSize: { value: 32, unit: 'px' },
|
||||||
|
textAlign: 'center'
|
||||||
|
},
|
||||||
|
ports: [],
|
||||||
|
children: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
connections: [],
|
||||||
|
comments: []
|
||||||
|
},
|
||||||
|
metadata: {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
settings: {},
|
||||||
|
metadata: {
|
||||||
|
title: 'Hello World Project',
|
||||||
|
description: 'A simple starter project'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -260,57 +260,21 @@ export class LocalProjectsModel extends Model {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Create a minimal Hello World project programmatically
|
// No template specified - use default embedded Hello World template
|
||||||
// This is a temporary solution until TASK-009-template-system-refactoring is implemented
|
// This uses the template system implemented in TASK-009
|
||||||
const minimalProject = {
|
const defaultTemplate = 'embedded://hello-world';
|
||||||
name: name,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
name: 'App',
|
|
||||||
id: guid(),
|
|
||||||
graph: {
|
|
||||||
roots: [
|
|
||||||
{
|
|
||||||
id: guid(),
|
|
||||||
type: 'Group',
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
parameters: {},
|
|
||||||
ports: [],
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: guid(),
|
|
||||||
type: 'Text',
|
|
||||||
x: 50,
|
|
||||||
y: 50,
|
|
||||||
parameters: {
|
|
||||||
text: 'Hello World!'
|
|
||||||
},
|
|
||||||
ports: [],
|
|
||||||
children: []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
connections: [],
|
|
||||||
comments: []
|
|
||||||
},
|
|
||||||
metadata: {}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
settings: {},
|
|
||||||
metadata: {
|
|
||||||
title: name,
|
|
||||||
description: 'A new Noodl project'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await filesystem.writeFile(filesystem.join(dirEntry, 'project.json'), JSON.stringify(minimalProject, null, 2));
|
// For embedded templates, write directly to the project directory
|
||||||
|
// (no need for temporary folder + copy)
|
||||||
|
const { EmbeddedTemplateProvider } = await import('../models/template/EmbeddedTemplateProvider');
|
||||||
|
const embeddedProvider = new EmbeddedTemplateProvider();
|
||||||
|
|
||||||
|
await embeddedProvider.download(defaultTemplate, dirEntry);
|
||||||
|
|
||||||
// Load the newly created project
|
// Load the newly created project
|
||||||
projectFromDirectory(dirEntry, (project) => {
|
projectFromDirectory(dirEntry, (project) => {
|
||||||
if (!project) {
|
if (!project) {
|
||||||
console.error('Failed to create project from generated structure');
|
console.error('Failed to create project from template');
|
||||||
fn();
|
fn();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import getDocsEndpoint from '@noodl-utils/getDocsEndpoint';
|
import getDocsEndpoint from '@noodl-utils/getDocsEndpoint';
|
||||||
|
|
||||||
|
import { EmbeddedTemplateProvider } from '../../models/template/EmbeddedTemplateProvider';
|
||||||
import { HttpTemplateProvider } from './template/providers/http-template-provider';
|
import { HttpTemplateProvider } from './template/providers/http-template-provider';
|
||||||
import { NoodlDocsTemplateProvider } from './template/providers/noodl-docs-template-provider';
|
import { NoodlDocsTemplateProvider } from './template/providers/noodl-docs-template-provider';
|
||||||
import { TemplateRegistry } from './template/template-registry';
|
import { TemplateRegistry } from './template/template-registry';
|
||||||
|
|
||||||
// The order of the providers matters,
|
// The order of the providers matters,
|
||||||
// when looking for a template it will take the first one that allows it.
|
// when looking for a template it will take the first one that allows it.
|
||||||
|
// EmbeddedTemplateProvider is first as it provides built-in templates that work reliably.
|
||||||
const templateRegistry = new TemplateRegistry([
|
const templateRegistry = new TemplateRegistry([
|
||||||
|
new EmbeddedTemplateProvider(),
|
||||||
new NoodlDocsTemplateProvider(getDocsEndpoint),
|
new NoodlDocsTemplateProvider(getDocsEndpoint),
|
||||||
new HttpTemplateProvider()
|
new HttpTemplateProvider()
|
||||||
]);
|
]);
|
||||||
|
|||||||
Reference in New Issue
Block a user