diff --git a/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-009-template-system-refactoring/README.md b/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-009-template-system-refactoring/README.md index 70706c7..61760a0 100644 --- a/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-009-template-system-refactoring/README.md +++ b/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-009-template-system-refactoring/README.md @@ -1,9 +1,9 @@ # TASK-009: Template System Refactoring -**Status**: 📋 Planned +**Status**: 🟢 Complete (Backend) **Priority**: Medium **Complexity**: Medium -**Estimated Effort**: 2-3 days +**Actual Effort**: 1 day (Backend implementation) ## Context @@ -244,12 +244,100 @@ class TemplateManager { - 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` +## 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 -- 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 -**Last Updated**: January 8, 2026 -**Assignee**: TBD +**Last Updated**: January 9, 2026 +**Implementation**: January 9, 2026 (Backend complete) diff --git a/packages/noodl-editor/src/editor/src/models/template/EmbeddedTemplateProvider.ts b/packages/noodl-editor/src/editor/src/models/template/EmbeddedTemplateProvider.ts new file mode 100644 index 0000000..4775014 --- /dev/null +++ b/packages/noodl-editor/src/editor/src/models/template/EmbeddedTemplateProvider.ts @@ -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 = 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> { + 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 { + 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 { + // 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()); + } +} diff --git a/packages/noodl-editor/src/editor/src/models/template/ProjectTemplate.ts b/packages/noodl-editor/src/editor/src/models/template/ProjectTemplate.ts new file mode 100644 index 0000000..f926c90 --- /dev/null +++ b/packages/noodl-editor/src/editor/src/models/template/ProjectTemplate.ts @@ -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; +} + +/** + * 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; + + /** 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; + + /** 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; +} diff --git a/packages/noodl-editor/src/editor/src/models/template/README.md b/packages/noodl-editor/src/editor/src/models/template/README.md new file mode 100644 index 0000000..16b924d --- /dev/null +++ b/packages/noodl-editor/src/editor/src/models/template/README.md @@ -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 = 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 diff --git a/packages/noodl-editor/src/editor/src/models/template/templates/hello-world.template.ts b/packages/noodl-editor/src/editor/src/models/template/templates/hello-world.template.ts new file mode 100644 index 0000000..90b10d3 --- /dev/null +++ b/packages/noodl-editor/src/editor/src/models/template/templates/hello-world.template.ts @@ -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' + } + } +}; diff --git a/packages/noodl-editor/src/editor/src/utils/LocalProjectsModel.ts b/packages/noodl-editor/src/editor/src/utils/LocalProjectsModel.ts index 017d8fb..8cc0574 100644 --- a/packages/noodl-editor/src/editor/src/utils/LocalProjectsModel.ts +++ b/packages/noodl-editor/src/editor/src/utils/LocalProjectsModel.ts @@ -260,57 +260,21 @@ export class LocalProjectsModel extends Model { }); }); } else { - // Create a minimal Hello World project programmatically - // This is a temporary solution until TASK-009-template-system-refactoring is implemented - const minimalProject = { - 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' - } - }; + // No template specified - use default embedded Hello World template + // This uses the template system implemented in TASK-009 + const defaultTemplate = 'embedded://hello-world'; - 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 projectFromDirectory(dirEntry, (project) => { if (!project) { - console.error('Failed to create project from generated structure'); + console.error('Failed to create project from template'); fn(); return; } diff --git a/packages/noodl-editor/src/editor/src/utils/forge/index.ts b/packages/noodl-editor/src/editor/src/utils/forge/index.ts index eb45ef0..4e882c9 100644 --- a/packages/noodl-editor/src/editor/src/utils/forge/index.ts +++ b/packages/noodl-editor/src/editor/src/utils/forge/index.ts @@ -1,12 +1,15 @@ import getDocsEndpoint from '@noodl-utils/getDocsEndpoint'; +import { EmbeddedTemplateProvider } from '../../models/template/EmbeddedTemplateProvider'; import { HttpTemplateProvider } from './template/providers/http-template-provider'; import { NoodlDocsTemplateProvider } from './template/providers/noodl-docs-template-provider'; import { TemplateRegistry } from './template/template-registry'; // The order of the providers matters, // 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([ + new EmbeddedTemplateProvider(), new NoodlDocsTemplateProvider(getDocsEndpoint), new HttpTemplateProvider() ]);