mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-11 14:52:55 +01:00
Fix app startup issues and add TASK-009 template system refactoring
This commit is contained in:
@@ -86,6 +86,9 @@ export function NodeGraphContextProvider({ children }: NodeGraphContextProviderP
|
||||
if (!nodeGraph) return;
|
||||
|
||||
function _update(model: ComponentModel) {
|
||||
// Guard against undefined model (happens on empty projects)
|
||||
if (!model) return;
|
||||
|
||||
if (isComponentModel_CloudRuntime(model)) {
|
||||
setActive('backend');
|
||||
if (SidebarModel.instance.ActiveId === 'components') {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { filesystem } from '@noodl/platform';
|
||||
|
||||
import { bugtracker } from '@noodl-utils/bugtracker';
|
||||
|
||||
// TODO: Can we merge this with ProjectModules ?
|
||||
@@ -27,21 +28,32 @@ export async function listProjectModules(project: TSFixme /* ProjectModel */): P
|
||||
}[] = [];
|
||||
|
||||
const modulesPath = project._retainedProjectDirectory + '/noodl_modules';
|
||||
const files = await filesystem.listDirectory(modulesPath);
|
||||
|
||||
await Promise.all(
|
||||
files.map(async (file) => {
|
||||
if (file.isDirectory) {
|
||||
const manifestPath = filesystem.join(modulesPath, file.name, 'manifest.json');
|
||||
const manifest = await filesystem.readJson(manifestPath);
|
||||
try {
|
||||
const files = await filesystem.listDirectory(modulesPath);
|
||||
|
||||
modules.push({
|
||||
name: file.name,
|
||||
manifest
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
await Promise.all(
|
||||
files.map(async (file) => {
|
||||
if (file.isDirectory) {
|
||||
const manifestPath = filesystem.join(modulesPath, file.name, 'manifest.json');
|
||||
const manifest = await filesystem.readJson(manifestPath);
|
||||
|
||||
modules.push({
|
||||
name: file.name,
|
||||
manifest
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
// noodl_modules folder doesn't exist (fresh/empty project)
|
||||
if (error.code === 'ENOENT') {
|
||||
console.log('noodl_modules folder not found (fresh project), skipping module loading');
|
||||
return [];
|
||||
}
|
||||
// Re-throw other errors
|
||||
throw error;
|
||||
}
|
||||
|
||||
return modules;
|
||||
}
|
||||
@@ -50,40 +62,51 @@ export async function readProjectModules(project: TSFixme /* ProjectModel */): P
|
||||
bugtracker.debug('ProjectModel.readModules');
|
||||
|
||||
const modulesPath = project._retainedProjectDirectory + '/noodl_modules';
|
||||
const files = await filesystem.listDirectory(modulesPath);
|
||||
|
||||
project.modules = [];
|
||||
project.previews = [];
|
||||
project.componentAnnotations = {};
|
||||
|
||||
await Promise.all(
|
||||
files.map(async (file) => {
|
||||
if (file.isDirectory) {
|
||||
const manifestPath = filesystem.join(modulesPath, file.name, 'manifest.json');
|
||||
const manifest = await filesystem.readJson(manifestPath);
|
||||
try {
|
||||
const files = await filesystem.listDirectory(modulesPath);
|
||||
|
||||
if (manifest) {
|
||||
manifest.name = file.name;
|
||||
project.modules.push(manifest);
|
||||
await Promise.all(
|
||||
files.map(async (file) => {
|
||||
if (file.isDirectory) {
|
||||
const manifestPath = filesystem.join(modulesPath, file.name, 'manifest.json');
|
||||
const manifest = await filesystem.readJson(manifestPath);
|
||||
|
||||
if (manifest.componentAnnotations) {
|
||||
for (var comp in manifest.componentAnnotations) {
|
||||
var ca = manifest.componentAnnotations[comp];
|
||||
if (manifest) {
|
||||
manifest.name = file.name;
|
||||
project.modules.push(manifest);
|
||||
|
||||
if (!project.componentAnnotations[comp]) project.componentAnnotations[comp] = {};
|
||||
for (var key in ca) project.componentAnnotations[comp][key] = ca[key];
|
||||
if (manifest.componentAnnotations) {
|
||||
for (var comp in manifest.componentAnnotations) {
|
||||
var ca = manifest.componentAnnotations[comp];
|
||||
|
||||
if (!project.componentAnnotations[comp]) project.componentAnnotations[comp] = {};
|
||||
for (var key in ca) project.componentAnnotations[comp][key] = ca[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (manifest.previews) {
|
||||
project.previews = manifest.previews.concat(project.previews);
|
||||
}
|
||||
}
|
||||
|
||||
if (manifest.previews) {
|
||||
project.previews = manifest.previews.concat(project.previews);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
console.log(`Loaded ${project.modules.length} modules`);
|
||||
console.log(`Loaded ${project.modules.length} modules`);
|
||||
} catch (error) {
|
||||
// noodl_modules folder doesn't exist (fresh/empty project)
|
||||
if (error.code === 'ENOENT') {
|
||||
console.log('noodl_modules folder not found (fresh project), skipping module loading');
|
||||
return [];
|
||||
}
|
||||
// Re-throw other errors
|
||||
throw error;
|
||||
}
|
||||
|
||||
return project.modules;
|
||||
}
|
||||
|
||||
@@ -260,36 +260,67 @@ export class LocalProjectsModel extends Model {
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Default template path
|
||||
const defaultTemplatePath = './external/projecttemplates/helloworld.zip';
|
||||
|
||||
// Check if template exists, otherwise create an empty project
|
||||
if (filesystem.exists(defaultTemplatePath)) {
|
||||
this._unzipAndLaunchProject(defaultTemplatePath, dirEntry, fn, options);
|
||||
} else {
|
||||
console.warn('Default project template not found, creating empty project');
|
||||
|
||||
// Create minimal project.json for empty project
|
||||
const minimalProject = {
|
||||
name: name,
|
||||
components: [],
|
||||
settings: {}
|
||||
};
|
||||
|
||||
await filesystem.writeFile(filesystem.join(dirEntry, 'project.json'), JSON.stringify(minimalProject, null, 2));
|
||||
|
||||
// Load the newly created empty project
|
||||
projectFromDirectory(dirEntry, (project) => {
|
||||
if (!project) {
|
||||
fn();
|
||||
return;
|
||||
// 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',
|
||||
ports: [],
|
||||
visual: true,
|
||||
visualStateTransitions: [],
|
||||
nodes: [
|
||||
{
|
||||
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: []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
settings: {},
|
||||
metadata: {
|
||||
title: name,
|
||||
description: 'A new Noodl project'
|
||||
}
|
||||
};
|
||||
|
||||
project.name = name;
|
||||
this._addProject(project);
|
||||
fn(project);
|
||||
await filesystem.writeFile(filesystem.join(dirEntry, 'project.json'), JSON.stringify(minimalProject, null, 2));
|
||||
|
||||
// Load the newly created project
|
||||
projectFromDirectory(dirEntry, (project) => {
|
||||
if (!project) {
|
||||
fn();
|
||||
return;
|
||||
}
|
||||
|
||||
project.name = name;
|
||||
this._addProject(project);
|
||||
project.toDirectory(project._retainedProjectDirectory, (res) => {
|
||||
if (res.result === 'success') {
|
||||
fn(project);
|
||||
} else {
|
||||
fn();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ import { NodeGraphNode } from '@noodl-models/nodegraphmodel';
|
||||
import { RuntimeType } from '@noodl-models/nodelibrary/NodeLibraryData';
|
||||
|
||||
export function getComponentModelRuntimeType(node: ComponentModel) {
|
||||
// Guard against undefined node (happens on empty projects)
|
||||
if (!node) return RuntimeType.Browser;
|
||||
|
||||
const name = node.name;
|
||||
|
||||
if (name.startsWith('/#__cloud__/')) {
|
||||
|
||||
@@ -154,6 +154,11 @@ export async function getPageRoutes(project: ProjectModel, options: IndexedPages
|
||||
}
|
||||
});
|
||||
|
||||
// Check if traverser has valid root (empty project case)
|
||||
if (!traverser.root) {
|
||||
return { routes: [], pages: [], dynamicHash: {} };
|
||||
}
|
||||
|
||||
// Fetch all the Page nodes.
|
||||
const pages: TraverseNode[] = traverser.filter((node) => node.node.typename === 'Page');
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import path from 'node:path';
|
||||
import { filesystem } from '@noodl/platform';
|
||||
|
||||
import FileSystem from '../../../filesystem';
|
||||
import { ITemplateProvider, TemplateItem, TemplateListFilter } from '../template';
|
||||
|
||||
/**
|
||||
* Provides access to locally bundled project templates.
|
||||
* This provider is used for templates that ship with the editor.
|
||||
*/
|
||||
export class LocalTemplateProvider implements ITemplateProvider {
|
||||
get name(): string {
|
||||
return 'local-templates';
|
||||
}
|
||||
|
||||
async list(_options: TemplateListFilter): Promise<readonly TemplateItem[]> {
|
||||
// Return only the Hello World template
|
||||
return [
|
||||
{
|
||||
title: 'Hello World',
|
||||
category: 'Getting Started',
|
||||
desc: 'A simple starter project to begin your Noodl journey',
|
||||
iconURL: './assets/template-hello-world-icon.png',
|
||||
projectURL: 'local://hello-world',
|
||||
cloudServicesTemplateURL: undefined
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
canDownload(url: string): Promise<boolean> {
|
||||
// Handle local:// protocol
|
||||
return Promise.resolve(url.startsWith('local://'));
|
||||
}
|
||||
|
||||
async download(url: string, destination: string): Promise<void> {
|
||||
if (url === 'local://hello-world') {
|
||||
// The template is in project-examples folder at the repository root
|
||||
// Use process.cwd() which points to repository root during development
|
||||
const repoRoot = process.cwd();
|
||||
const sourcePath = path.join(repoRoot, 'project-examples', 'version 1.1.0', 'template-project');
|
||||
|
||||
if (!filesystem.exists(sourcePath)) {
|
||||
throw new Error('Hello World template not found at: ' + sourcePath);
|
||||
}
|
||||
|
||||
// Copy the template folder to destination
|
||||
// The destination is expected to be where unzipped content goes
|
||||
// So we copy the folder contents directly
|
||||
FileSystem.instance.copyRecursiveSync(sourcePath, destination);
|
||||
} else {
|
||||
throw new Error(`Unknown local template: ${url}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,14 +59,24 @@ export class NodeGraphTraverser {
|
||||
this.traverseComponent = typeof options.traverseComponent === 'boolean' ? options.traverseComponent : true;
|
||||
this.tagSelector = typeof options.tagSelector === 'function' ? options.tagSelector : null;
|
||||
|
||||
this.root = new TraverseNode(this, null, targetNode || project.getRootNode(), null);
|
||||
const rootNode = targetNode || project.getRootNode();
|
||||
|
||||
// Handle empty projects with no root node
|
||||
if (!rootNode) {
|
||||
this.root = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.root = new TraverseNode(this, null, rootNode, null);
|
||||
}
|
||||
|
||||
public forEach(callback: (node: TraverseNode) => void) {
|
||||
if (!this.root) return;
|
||||
this.root.forEach(callback);
|
||||
}
|
||||
|
||||
public map<T = any>(callback: (node: TraverseNode) => T) {
|
||||
if (!this.root) return [];
|
||||
const items: T[] = [];
|
||||
this.forEach((node) => {
|
||||
const result = callback(node);
|
||||
@@ -76,6 +86,7 @@ export class NodeGraphTraverser {
|
||||
}
|
||||
|
||||
public filter(callback: (node: TraverseNode) => boolean) {
|
||||
if (!this.root) return [];
|
||||
const items: TraverseNode[] = [];
|
||||
this.forEach((node) => {
|
||||
if (callback(node)) items.push(node);
|
||||
|
||||
Reference in New Issue
Block a user