12 Commits

Author SHA1 Message Date
Eric Tuvesson
50677fd529 fix(runtime): Script node not handling types correctly (#97) 2025-06-24 18:26:58 +02:00
Eric Tuvesson
307a13f7be chore: bump version 2025-06-07 17:39:32 +02:00
Eric Tuvesson
4e96d23585 feat: Deploy popup forward in editor version (#96) 2025-06-03 00:17:55 +02:00
Eric Tuvesson
2fd4b67a08 feat: Command, add and use Cloud Service (#95) 2025-06-03 00:08:10 +02:00
Eric Tuvesson
50e266e3e4 feat: Save "nodelibrary.json" with the project (#94)
* feat: Save "nodelibrary.json" with the project

To manage the project without the editor/preview, we need to know more about the Node Library. To make this possible, let's save the Node Library next to the project. Hopefully it will work smoothly for a lot of projects.

I have tested it with a few big projects, and it works well for them.

* chore: clean up

* fix: Make compact and check if there is some data
2025-06-02 19:38:18 +02:00
Eric Tuvesson
7ca69b809a fix: Cloud Function failure response (#93)
Error: undefined is not an object (evaluating 'e.error')
2025-03-28 16:11:18 +01:00
Eric Tuvesson
80c7c01805 chore: Rename "Noodl AI" (#92) 2025-03-25 21:38:39 +13:00
Eric Tuvesson
fc89bd9ce1 feat(editor): Search only show ID searches when 1:1 match (#90)
This removes a lot of the clutter from the search results.
2025-02-25 15:39:15 +01:00
Eric Tuvesson
2cfd36147a chore(editor): Convert Universal Search to TypeScript (#89) 2025-02-25 13:55:53 +01:00
Eric Tuvesson
0e13a8b033 fix: Keep object id when fetching a Object column from the Cloud Service (#87) 2025-01-16 14:40:44 +01:00
Eric Tuvesson
680bd58442 chore: Update "Used in x places" text (#84)
We cannot guarantee that this is always correct since there is repeaters and dynamic repeaters etc, but this can give a hint to how many times a component is used.
2025-01-08 22:02:10 +01:00
Eric Tuvesson
95db9f6528 chore: Add some TS types to WarningsModel (#86) 2025-01-08 21:58:37 +01:00
22 changed files with 328 additions and 144 deletions

2
package-lock.json generated
View File

@@ -48981,7 +48981,7 @@
},
"packages/noodl-editor": {
"name": "fluxscape-editor",
"version": "1.1.0",
"version": "1.2.0",
"dependencies": {
"@electron/remote": "^2.1.2",
"@jaames/iro": "^5.5.2",

View File

@@ -4,7 +4,7 @@
"description": "Node-Based App Builder for Scalability & Rapid Development, a fork of Noodl",
"author": "Fluxscape <contact@fluxscape.io>",
"homepage": "https://fluxscape.io",
"version": "1.1.0",
"version": "1.2.0",
"main": "src/main/main.js",
"scripts": {
"build": "npx ts-node -P ./tsconfig.build.json ./scripts/build.ts",

View File

@@ -6,7 +6,7 @@ import { EventDispatcher } from '../../shared/utils/EventDispatcher';
import ProjectModules from '../../shared/utils/projectmodules';
import { NodeLibrary } from './models/nodelibrary';
import { ProjectModel } from './models/projectmodel';
import { WarningsModel } from './models/warningsmodel';
import { WarningRef, WarningsModel } from './models/warningsmodel';
import DebugInspector from './utils/debuginspector';
import * as Exporter from './utils/exporter';
@@ -112,7 +112,7 @@ export class ViewerConnection extends Model {
} else if (request.cmd === 'showwarning' && request.type === 'viewer') {
const content = JSON.parse(request.content);
if (ProjectModel.instance !== undefined) {
const ref = {
const ref: WarningRef = {
component: ProjectModel.instance.getComponentWithName(content.componentName),
node: ProjectModel.instance.findNodeWithId(content.nodeId),
key: content.key,
@@ -124,7 +124,7 @@ export class ViewerConnection extends Model {
}
} else if (request.cmd === 'clearwarnings' && request.type === 'viewer') {
const content = JSON.parse(request.content);
const ref = {
const ref: WarningRef = {
component: ProjectModel.instance.getComponentWithName(content.componentName),
node: ProjectModel.instance.findNodeWithId(content.nodeId)
};

View File

@@ -0,0 +1,72 @@
import { CloudService } from '@noodl-models/CloudServices';
import { ProjectModel } from '@noodl-models/projectmodel';
import { setCloudServices } from '@noodl-models/projectmodel.editor';
import { ToastLayer } from '../../../views/ToastLayer';
export type Command = {
kind: 'cloud-service';
use?: boolean;
name: string;
description?: string;
endpoint: string;
appId: string;
masterKey: string;
};
export async function execute(command: Command, _event: MessageEvent) {
const environment = await getOrCreate(command);
if (command.use) {
setCloudServices(ProjectModel.instance, environment);
}
}
async function getOrCreate(command: Command) {
const cloudServices = await CloudService.instance.backend.fetch();
const environment = cloudServices.find((c) => c.url === command.endpoint);
if (environment) {
if (
environment.name !== command.name ||
environment.description !== command.description ||
environment.appId !== command.appId ||
environment.masterKey !== command.masterKey
) {
await CloudService.instance.backend.update({
id: environment.id,
url: environment.url,
// Update the existing environment
name: command.name,
description: command.description,
appId: command.appId,
masterKey: command.masterKey
});
ToastLayer.showSuccess(`Cloud service "${command.name}" updated.`);
}
return {
id: environment.id,
endpoint: environment.url,
appId: command.appId
};
} else {
const create = await CloudService.instance.backend.create({
name: command.name,
description: command.description,
appId: command.appId,
url: command.endpoint,
masterKey: command.masterKey
});
ToastLayer.showSuccess(`Cloud service "${command.name}" added successfully.`);
return {
id: create.id,
endpoint: create.url,
appId: create.appId
};
}
}

View File

@@ -1,11 +1,13 @@
import * as CloudService from './commands/cloud-service';
import * as Notify from './commands/notify';
import * as UploadAwsS3 from './commands/upload-aws-s3';
type IFrameCommand = Notify.Command | UploadAwsS3.Command;
type IFrameCommand = Notify.Command | UploadAwsS3.Command | CloudService.Command;
const commands: Record<IFrameCommand['kind'], (command: IFrameCommand, event: MessageEvent) => Promise<void>> = {
notify: Notify.execute,
'upload-aws-s3': UploadAwsS3.execute
'upload-aws-s3': UploadAwsS3.execute,
'cloud-service': CloudService.execute
};
export function commandEventHandler(event: MessageEvent) {

View File

@@ -1,5 +1,5 @@
import { NodeGraphNode } from '@noodl-models/nodegraphmodel';
import { WarningsModel } from '@noodl-models/warningsmodel';
import { type Warning, WarningsModel } from '@noodl-models/warningsmodel';
import { ProjectModel } from '../projectmodel';
import NodeTypeAdapter from './NodeTypeAdapter';
@@ -103,7 +103,7 @@ export class RouterNavigateAdapter extends NodeTypeAdapter {
const hasValidTarget = target && pageComponents.includes(target);
const warning =
const warning: Warning =
hasValidTarget === false
? {
message: "The target page doesn't belong to the target router",

View File

@@ -458,11 +458,11 @@ export class VariantModel extends Model {
}
);
} else if (c.type === 'defaultStateTransition') {
var state =
const state =
this.getType().visualStates !== undefined
? this.getType().visualStates.find((s) => s.name === c.state)
: undefined;
var stateName = state !== undefined ? state.label : c.state;
const stateName = state !== undefined ? state.label : c.state;
WarningsModel.instance.setWarning(
{ key: 'variant-dst-conflict-' + this.name + '-' + this.getType().fullName + '-' + c.state },

View File

@@ -72,7 +72,7 @@ export class NodeGraphNode extends Model {
metadata?: Record<string, any>;
private _variant: TSFixme;
private _label: TSFixme;
_label: TSFixme;
private _type: TSFixme;
private _ports: TSFixme;

View File

@@ -1167,6 +1167,28 @@ EventDispatcher.instance.on(
null
);
NodeLibrary.instance.on('libraryUpdated', () => {
const library = NodeLibrary.instance.library;
// Check if we have any data from the browser
if (Object.keys(library?.nodeIndex || {}).length > 0) {
const filepath = filesystem.join(ProjectModel.instance._retainedProjectDirectory, 'nodelibrary.json');
const compactLibrary = {
typecasts: library.typecasts,
dynamicports: library.dynamicports,
nodetypes: library.nodetypes,
// NOTE: Let's save the node index for now, most likely this is something we will just ignore.
nodeIndex: library.nodeIndex,
projectsettings: library.projectsettings
};
filesystem.writeJson(filepath, compactLibrary).then(() => {
console.log('saved nodelibrary.json');
});
}
});
function saveProject() {
if (!ProjectModel.instance) return;

View File

@@ -1,9 +1,42 @@
import { ComponentModel } from '@noodl-models/componentmodel';
import { ProjectModel } from '@noodl-models/projectmodel';
import { toArray } from 'underscore';
import type { ComponentModel } from '@noodl-models/componentmodel';
import Model from '../../../shared/model';
import type { NodeGraphNode } from './nodegraphmodel';
import { NodeLibrary } from './nodelibrary';
export type WarningLabel = 'warning' | 'error';
export type Warning =
| {
type?: string;
level?: WarningLabel;
message: string;
showGlobally?: boolean;
}
| {
type: 'conflict' | 'conflict-source-code';
level?: WarningLabel;
message: string;
showGlobally?: boolean;
conflictMetadata: {
parameter: string;
ours: string;
theirs: string;
};
onDismiss: () => void;
onUseTheirs: () => void;
};
export type WarningRef = {
key?: string;
component?: ComponentModel;
connection?: TSFixme;
node?: NodeGraphNode;
isFromViewer?: boolean;
};
/**
* The first level of the warnings object is component name
* Second is the connection / node identifier
@@ -14,7 +47,7 @@ interface Warnings {
[node_connection_id: string]: {
[warningKey: string]: {
ref: TSFixme;
warning: TSFixme;
warning: Warning;
};
};
};
@@ -24,7 +57,7 @@ export class WarningsModel extends Model {
public static instance = new WarningsModel();
private warnings: Warnings = {};
private notifyChangedScheduled: boolean = false;
private notifyChangedScheduled = false;
constructor() {
super();
@@ -35,10 +68,12 @@ export class WarningsModel extends Model {
});
}
public setWarning(ref, warning) {
var w = this.getWarningsForRef(ref, warning !== undefined);
public setWarning(ref: WarningRef, warning: Warning) {
const w = this.getWarningsForRef(ref, warning !== undefined);
if (!warning) {
if (w) delete w[ref.key];
if (w) {
delete w[ref.key];
}
} else {
warning.level = warning.level || 'warning';
w[ref.key] = { ref: ref, warning: warning };
@@ -47,31 +82,34 @@ export class WarningsModel extends Model {
this.scheduleNotifyChanged();
}
public clearWarningsForRef(ref) {
var w = this.getWarningsForRef(ref);
public clearWarningsForRef(ref: WarningRef) {
const w = this.getWarningsForRef(ref);
if (!w) return;
for (var i in w) delete w[i];
for (const i in w) {
delete w[i];
}
this.scheduleNotifyChanged();
}
public clearAllWarningsForComponent(component) {
public clearAllWarningsForComponent(component: ComponentModel) {
const warnings = this.warnings[component.name];
if (!warnings) return;
for (let i in warnings) delete warnings[i];
for (const i in warnings) {
delete warnings[i];
}
this.scheduleNotifyChanged();
}
public clearWarningsForRefMatching(matchCb) {
public clearWarningsForRefMatching(matchFn: (ref: TSFixme) => boolean) {
for (const cw of Object.values(this.warnings)) {
for (const ws of Object.values(cw)) {
for (const key in ws) {
const w = ws[key];
if (matchCb(w.ref)) {
if (matchFn(w.ref)) {
delete ws[key];
}
}
@@ -87,15 +125,14 @@ export class WarningsModel extends Model {
this.scheduleNotifyChanged();
}
public getWarnings(ref) {
var w = this.getWarningsForRef(ref);
public getWarnings(ref: WarningRef) {
const w = this.getWarningsForRef(ref);
if (!w) return;
if (Object.keys(w).length === 0) return;
// Create short message for hover
var messages = [];
for (var k in w) {
const messages = [];
for (const k in w) {
if (w[k].warning) messages.push(w[k].warning.message);
}
@@ -106,13 +143,13 @@ export class WarningsModel extends Model {
}
public forEachWarningInComponent(c, callback, args) {
var cw = this.warnings[c ? c.name : '/'];
const cw = this.warnings[c ? c.name : '/'];
if (!cw) return;
for (var ref in cw) {
var ws = cw[ref];
for (const ref in cw) {
const ws = cw[ref];
for (var w in ws) {
for (const w in ws) {
if (!args || !args.levels || args.levels.indexOf(ws[w].warning.level) !== -1) {
callback(ws[w]);
}
@@ -121,7 +158,7 @@ export class WarningsModel extends Model {
}
public getAllWarningsForComponent(c, args) {
var warnings = [];
const warnings = [];
this.forEachWarningInComponent(
c,
function (warning) {
@@ -152,15 +189,15 @@ export class WarningsModel extends Model {
}
public getTotalNumberOfWarnings(args) {
var total = 0;
for (var key in this.warnings) {
let total = 0;
for (const key in this.warnings) {
total += this.getNumberOfWarningsForComponent({ name: key }, args);
}
return total;
}
public getTotalNumberOfWarningsMatching(matchCb) {
var total = 0;
let total = 0;
this.forEachWarning((c, ref, key, warning) => {
if (matchCb(key, ref, warning)) total++;
});
@@ -168,16 +205,16 @@ export class WarningsModel extends Model {
}
public forEachWarning(callback: (c: string, ref, key, warning) => void) {
var w = this.warnings;
const w = this.warnings;
for (const c in w) {
// Loop over all components
var _w = w[c];
const _w = w[c];
for (const ref in _w) {
// Loop over all refs
var __w = _w[ref];
const __w = _w[ref];
for (const key in __w) {
// Loop over all keys
var warning = __w[key];
const warning = __w[key];
callback(c, ref, key, warning);
}
@@ -195,12 +232,12 @@ export class WarningsModel extends Model {
// node: nodeRef,
// connection: connectionRef,
// key: key of warning as string}
private getWarningsForRef(ref, create?) {
var componentName = ref.component ? ref.component.name : '/';
private getWarningsForRef(ref: WarningRef, create?) {
const componentName = ref.component ? ref.component.name : '/';
if (!this.warnings[componentName]) this.warnings[componentName] = {};
var cw = this.warnings[componentName];
const cw = this.warnings[componentName];
var key;
let key;
if (ref.node) key = 'node/' + ref.node.id;
else if (ref.connection)
key =
@@ -220,7 +257,8 @@ export class WarningsModel extends Model {
/** Batch changed notifications so listeners don't get peppered */
private scheduleNotifyChanged() {
var _this = this;
// eslint-disable-next-line @typescript-eslint/no-this-alias
const _this = this;
if (this.notifyChangedScheduled) return;
this.notifyChangedScheduled = true;

View File

@@ -4,7 +4,7 @@ import { clearFolders } from './cleanup';
export async function copyProjectFilesToFolder(projectPath: string, direntry: string): Promise<void> {
// TODO: Load something like .noodlignore file list
const ignoreFiles = ['.DS_Store', '.gitignore', '.gitattributes', 'project.json', 'Dockerfile'];
const ignoreFiles = ['.DS_Store', '.gitignore', '.gitattributes', 'project.json', 'Dockerfile', 'nodelibrary.json'];
// Copy everything from the project folder
if (!projectPath) {

View File

@@ -1,22 +1,32 @@
const { ProjectModel } = require('../models/projectmodel');
import type { ComponentModel } from '@noodl-models/componentmodel';
import type { NodeGraphNode } from '@noodl-models/nodegraphmodel';
function matchStrings(string1, string2) {
import { ProjectModel } from '../models/projectmodel';
export type SearchResult = {
componentTarget: ComponentModel;
nodeTarget?: NodeGraphNode;
type?: string;
userLabel?: string;
label: string;
};
function matchStrings(string1: string, string2: string) {
return string1.toLowerCase().indexOf(string2.toLowerCase()) !== -1;
}
function searchInNodeRecursive(node, searchTerms, component) {
var results = [];
var matchLabel = null;
var i = 0;
function searchInNodeRecursive(node: NodeGraphNode, searchTerms: string, component: ComponentModel) {
let results: SearchResult[] = [];
let matchLabel: string | null = null;
if (node._label !== undefined && matchStrings(node._label, searchTerms)) {
matchLabel = node.label;
} else if (matchStrings(node.id, searchTerms)) {
} else if (node.id === searchTerms) {
matchLabel = node.id;
} else if (matchStrings(node.type.displayName || node.type.name, searchTerms)) {
matchLabel = node.label;
} else {
let parameterNames = Object.keys(node.parameters);
const parameterNames = Object.keys(node.parameters);
for (const parameterNameIndex in parameterNames) {
const parameterName = parameterNames[parameterNameIndex];
@@ -25,7 +35,7 @@ function searchInNodeRecursive(node, searchTerms, component) {
matchStrings(node.parameters[parameterName], searchTerms)
) {
let displayLabel = parameterName;
let connectionPort = node.type.ports?.find((port) => port.name === parameterName);
const connectionPort = node.type.ports?.find((port) => port.name === parameterName);
if (connectionPort) {
displayLabel = connectionPort.displayName;
}
@@ -51,9 +61,9 @@ function searchInNodeRecursive(node, searchTerms, component) {
}
if (matchLabel === null) {
var ports = node.dynamicports;
for (i = 0; i < ports.length; ++i) {
var port = ports[i];
const ports = node.dynamicports;
for (let i = 0; i < ports.length; ++i) {
const port = ports[i];
if (matchStrings(port.name, searchTerms)) {
matchLabel = node.label + ' : ' + port.name;
break;
@@ -62,9 +72,9 @@ function searchInNodeRecursive(node, searchTerms, component) {
}
if (matchLabel === null) {
var ports = node.ports;
for (i = 0; i < ports.length; ++i) {
var port = ports[i];
const ports = node.ports;
for (let i = 0; i < ports.length; ++i) {
const port = ports[i];
if (matchStrings(port.name, searchTerms)) {
matchLabel = node.label + ' : ' + port.name;
break;
@@ -83,17 +93,17 @@ function searchInNodeRecursive(node, searchTerms, component) {
});
}
for (i = 0; i < node.children.length; ++i) {
var child = node.children[i];
var childResults = searchInNodeRecursive(child, searchTerms, component);
for (let i = 0; i < node.children.length; ++i) {
const child = node.children[i];
const childResults = searchInNodeRecursive(child, searchTerms, component);
results = results.concat(childResults);
}
return results;
}
function searchInComponent(component, searchTerms) {
var results = [];
function searchInComponent(component: ComponentModel, searchTerms: string) {
let results: SearchResult[] = [];
if (matchStrings(component.displayName, searchTerms)) {
results.push({
componentTarget: component,
@@ -102,14 +112,14 @@ function searchInComponent(component, searchTerms) {
});
}
for (var i = 0; i < component.graph.roots.length; ++i) {
var node = component.graph.roots[i];
var nodeResults = searchInNodeRecursive(node, searchTerms, component);
for (let i = 0; i < component.graph.roots.length; ++i) {
const node = component.graph.roots[i];
const nodeResults = searchInNodeRecursive(node, searchTerms, component);
results = results.concat(nodeResults);
}
if (component.graph.commentsModel.comments) {
for (var i = 0; i < component.graph.commentsModel.comments.length; ++i) {
for (let i = 0; i < component.graph.commentsModel.comments.length; ++i) {
const comment = component.graph.commentsModel.comments[i];
if (matchStrings(comment.text, searchTerms)) {
results.push({
@@ -132,17 +142,17 @@ function searchInComponent(component, searchTerms) {
}
}
export function performSearch(searchTerms) {
var results = [];
var root = ProjectModel.instance.getRootNode();
export function performSearch(searchTerms: string) {
const results: ReturnType<typeof searchInComponent>[] = [];
const root = ProjectModel.instance.getRootNode();
if (root === undefined) return;
var components = ProjectModel.instance.components;
const components = ProjectModel.instance.components;
for (var i = 0; i < components.length; ++i) {
var component = components[i];
for (let i = 0; i < components.length; ++i) {
const component = components[i];
var componentResults = searchInComponent(component, searchTerms);
const componentResults = searchInComponent(component, searchTerms);
if (componentResults !== null) {
//limit the label length (it can search in markdown, css, etc)
for (const result of componentResults.results) {

View File

@@ -211,7 +211,7 @@ export default function Clippy() {
aiAssistantModel.removeActivity(id);
}
const initialPlaceholder = isInputOpen ? 'Select (or type) a command below' : 'Ask Noodl AI';
const initialPlaceholder = isInputOpen ? 'Select (or type) a command below' : 'Ask FluxScape AI';
const isPromptInWrongOrder = Boolean(!selectedOption) && Boolean(secondInputValue);
const isFullBeta = ['full-beta', 'enterprise'].includes(version);
const isLimitedBeta = false; // TODO: version === 'limited-beta';
@@ -412,8 +412,8 @@ export default function Clippy() {
<Text hasBottomSpacing>4. Click the "Verify API Key" button</Text>
<Text hasBottomSpacing>
If you dont have an API key with GPT-4 access, you can set the Noodl AI to use the Limited Beta in the
editor settings.
If you dont have an API key with GPT-4 access, you can set the FluxScape AI to use the Limited Beta in
the editor settings.
</Text>
<PrimaryButton
size={PrimaryButtonSize.Small}
@@ -597,8 +597,8 @@ export default function Clippy() {
</Label>
<Text hasBottomSpacing size={TextSize.Medium}>
You are running the limited beta of Noodl AI. If features fewer commands and a less capable AI. Get full
beta access by bringing your own GPT-4 API key.
You are running the limited beta of FluxScape AI. If features fewer commands and a less capable AI. Get
full beta access by bringing your own GPT-4 API key.
</Text>
<PrimaryButton

View File

@@ -1,4 +1,5 @@
import React, { RefObject } from 'react';
import { platform } from '@noodl/platform';
import { ProjectModel } from '@noodl-models/projectmodel';
@@ -64,7 +65,9 @@ export function DeployPopup(props: DeployPopupProps) {
}
function FluxscapeDeployTab() {
const params = {};
const params = {
version: platform.getVersion()
};
const projectId = ProjectModel.instance.id;
if (projectId) {

View File

@@ -6,8 +6,6 @@ import { CloudService } from '@noodl-models/CloudServices';
import { ActivityIndicator } from '@noodl-core-ui/components/common/ActivityIndicator';
import { IconName, IconSize } from '@noodl-core-ui/components/common/Icon';
import { IconButton } from '@noodl-core-ui/components/inputs/IconButton';
import { PrimaryButton } from '@noodl-core-ui/components/inputs/PrimaryButton';
import { Box } from '@noodl-core-ui/components/layout/Box';
import { ConditionalContainer } from '@noodl-core-ui/components/layout/ConditionalContainer';
import { Container } from '@noodl-core-ui/components/layout/Container';
import { VStack } from '@noodl-core-ui/components/layout/Stack';

View File

@@ -44,7 +44,7 @@ export function OpenAiSection() {
}
return (
<CollapsableSection title="Noodl AI (Beta)">
<CollapsableSection title="FluxScape AI (Beta)">
<Box hasXSpacing>
<VStack>
<PropertyPanelRow label="Version" isChanged={false}>
@@ -66,7 +66,7 @@ export function OpenAiSection() {
{enabledState === 'disabled' && (
<Box hasYSpacing>
<Text>Noodl AI is currently disabled.</Text>
<Text>FluxScape AI is currently disabled.</Text>
</Box>
)}
@@ -157,16 +157,16 @@ export function OpenAiSection() {
UNSAFE_style={{ borderRadius: '2px', background: 'var(--theme-color-bg-3)' }}
>
<Title size={TitleSize.Medium} hasBottomSpacing>
Noodl AI docs
FluxScape AI docs
</Title>
<Text hasBottomSpacing>See setup instructions and guides for how to use Noodl AI on our docs.</Text>
<Text hasBottomSpacing>See setup instructions and guides for how to use FluxScape AI on our docs.</Text>
<PrimaryButton
variant={PrimaryButtonVariant.Muted}
size={PrimaryButtonSize.Small}
isGrowing
label="Open docs"
onClick={() => {
platform.openExternal('https://docs.noodl.net/#/docs/getting-started/noodl-ai/');
platform.openExternal('https://docs.fluxscape.io/docs/getting-started/noodl-ai');
}}
/>
</Box>

View File

@@ -991,7 +991,7 @@ export class ComponentsPanelView extends View {
// Find references
const nodeReference = HACK_findNodeReference(scope.comp.name);
const nodeReferencesText = `Used in ${nodeReference?.referenaces?.length || 0} places`;
const nodeReferencesText = `Used in ~${nodeReference?.referenaces?.length || 0} places`;
items = items.concat([
{
@@ -1122,7 +1122,7 @@ export class ComponentsPanelView extends View {
if (scope.canBecomeRoot) {
// Find references
const nodeReference = HACK_findNodeReference(scope.folder.component.name);
const nodeReferencesText = `Used in ${nodeReference?.referenaces?.length || 0} places`;
const nodeReferencesText = `Used in ~${nodeReference?.referenaces?.length || 0} places`;
items = items.concat([{
label: nodeReferencesText

View File

@@ -182,7 +182,7 @@ function AiNodeChat({ context, onUpdated }: AiNodeChatProps) {
footer={
version === 'disabled' ? (
<Center>
<Text textType={TextType.Shy}>Noodl AI is currently disabled.</Text>
<Text textType={TextType.Shy}>FluxScape AI is currently disabled.</Text>
</Center>
) : (
<>

View File

@@ -5,10 +5,8 @@ import { useSidePanelKeyboardCommands } from '@noodl-hooks/useKeyboardCommands';
import classNames from 'classnames';
import React, { useEffect, useRef, useState } from 'react';
import { ComponentModel } from '@noodl-models/componentmodel';
import { NodeGraphNode } from '@noodl-models/nodegraphmodel';
import { KeyCode, KeyMod } from '@noodl-utils/keyboard/KeyCode';
import { performSearch } from '@noodl-utils/universal-search';
import { performSearch, SearchResult } from '@noodl-utils/universal-search';
import { SearchInput } from '@noodl-core-ui/components/inputs/SearchInput';
import { Container } from '@noodl-core-ui/components/layout/Container';
@@ -21,7 +19,7 @@ import css from './search-panel.module.scss';
export function SearchPanel() {
const [searchTerm, setSearchTerm] = useState('');
const [searchResults, setSearchResults] = useState([]);
const [searchResults, setSearchResults] = useState<ReturnType<typeof performSearch>>([]);
const inputRef = useRef<HTMLInputElement>(null);
// TODO: Not same context
@@ -56,7 +54,7 @@ export function SearchPanel() {
}
}, [debouncedSearchTerm]);
function onSearchItemClicked(searchResult: SearchResultItem) {
function onSearchItemClicked(searchResult: SearchResult) {
if (searchResult.type === 'Component') {
NodeGraphContextTmp.switchToComponent(searchResult.componentTarget, {
breadcrumbs: false,
@@ -99,21 +97,9 @@ export function SearchPanel() {
);
}
type SearchResultItem = {
componentTarget: ComponentModel;
label: string;
nodeTarget: NodeGraphNode;
type: string;
userLabel: string;
};
type SearchItemProps = {
component: {
componentName: string;
componentId: string;
results: SearchResultItem[];
};
onSearchItemClicked: (item: SearchResultItem) => void;
component: ReturnType<typeof performSearch>[0];
onSearchItemClicked: (item: SearchResult) => void;
};
function SearchItem({ component, onSearchItemClicked }: SearchItemProps) {
@@ -130,11 +116,11 @@ function SearchItem({ component, onSearchItemClicked }: SearchItemProps) {
<Section title={titleText} variant={SectionVariant.Panel}>
{component.results.map((result, index) => (
<div
key={index}
className={classNames(
css.SearchResultItem
// lastActiveComponentId === result.componentTarget.id && css['is-active']
)}
key={index}
onClick={() => onSearchItemClicked(result)}
>
<Label variant={TextType.Proud}>

View File

@@ -471,8 +471,15 @@ class CloudStore {
}
function _isArrayOfObjects(a) {
if (!Array.isArray(a)) return false;
for (var i = 0; i < a.length; i++) if (typeof a[i] !== 'object' || a[i] === null) return false;
if (!Array.isArray(a)) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (typeof a[i] !== 'object' || a[i] === null) {
return false;
}
}
return true;
}
@@ -544,54 +551,86 @@ function _serializeObject(data, collectionName, modelScope) {
return data;
}
/**
*
* @param {unknown} data
* @param {string} type
* @param {*} modelScope
* @returns
*/
function _deserializeJSON(data, type, modelScope) {
if (data === undefined) return;
if (data === undefined) return undefined;
if (data === null) return null;
if (type === 'Relation' && data.__type === 'Relation') {
return undefined; // Ignore relation fields
} else if (type === 'Pointer' && data.__type === 'Pointer') {
// This is a pointer type, resolve into id
}
// This is a pointer type, resolve into id
if (type === 'Pointer' && data.__type === 'Pointer') {
return data.objectId;
} else if (type === 'Date' && data.__type === 'Date') {
}
if (type === 'Date' && data.__type === 'Date') {
return new Date(data.iso);
} else if (type === 'Date' && typeof data === 'string') {
}
if (type === 'Date' && typeof data === 'string') {
return new Date(data);
} else if (type === 'File' && data.__type === 'File') {
}
if (type === 'File' && data.__type === 'File') {
return new CloudFile(data);
} else if (type === 'GeoPoint' && data.__type === 'GeoPoint') {
}
if (type === 'GeoPoint' && data.__type === 'GeoPoint') {
return {
latitude: data.latitude,
longitude: data.longitude
};
} else if (_isArrayOfObjects(data)) {
var a = [];
for (var i = 0; i < data.length; i++) {
}
if (_isArrayOfObjects(data)) {
const a = [];
for (let i = 0; i < data.length; i++) {
a.push(_deserializeJSON(data[i], undefined, modelScope));
}
var c = Collection.get();
const c = Collection.get();
c.set(a);
return c;
} else if (Array.isArray(data)) return data;
}
// An array with mixed types
if (Array.isArray(data)) {
return data;
}
// This is an array with mixed data, just return it
else if (data && data.__type === 'Object' && data.className !== undefined && data.objectId !== undefined) {
if (data && data.__type === 'Object' && data.className !== undefined && data.objectId !== undefined) {
const _data = Object.assign({}, data);
delete _data.className;
delete _data.__type;
return _fromJSON(_data, data.className, modelScope);
} else if (typeof data === 'object' && data !== null) {
var m = (modelScope || Model).get();
for (var key in data) {
m.set(key, _deserializeJSON(data[key], undefined, modelScope));
}
if (typeof data === 'object' && data !== null) {
// Try to get the model by id, if it is defined, otherwise we create a new unique id.
const model = (modelScope || Model).get(data.id);
for (const key in data) {
const nestedValue = _deserializeJSON(data[key], undefined, modelScope);
model.set(key, nestedValue);
}
return m;
} else return data;
return model;
}
return data;
}
function _fromJSON(item, collectionName, modelScope) {
const modelStore = modelScope || Model;
const model = modelStore.get(item.objectId);
// Try to get the model by the object id (record) or id, otherwise we create a new unique id.
const model = modelStore.get(item.objectId || item.id);
model._class = collectionName;
let schema = undefined;
@@ -605,7 +644,8 @@ function _fromJSON(item, collectionName, modelScope) {
}
const _type = schema && schema.properties && schema.properties[key] ? schema.properties[key].type : undefined;
model.set(key, _deserializeJSON(item[key], _type, modelScope));
const nestedValue = _deserializeJSON(item[key], _type, modelScope);
model.set(key, nestedValue);
}
return model;

View File

@@ -3,6 +3,17 @@ const CloudStore = require('@noodl/runtime/src/api/cloudstore');
('use strict');
/**
* @param {string} path
* @param {{
* endpoint: string;
* method: string;
* appId: string;
* content: any;
* success: (json: object) => void;
* error: (json: object | undefined) => void;
* }} options
*/
function _makeRequest(path, options) {
var xhr = new XMLHttpRequest();
@@ -15,7 +26,9 @@ function _makeRequest(path, options) {
if (xhr.status === 200 || xhr.status === 201) {
options.success(json);
} else options.error(json);
} else {
options.error(json);
}
}
};
@@ -191,7 +204,7 @@ var CloudFunctionNode = {
this.sendSignalOnOutput('success');
},
error: (e) => {
const error = typeof e === 'string' ? e : e.error || 'Failed running cloud function.';
const error = typeof e === 'string' ? e : e?.error || 'Failed running cloud function.';
this._internal.lastCallResult = {
status: 'failure',
error

View File

@@ -500,8 +500,8 @@ function onFrameStart() {
}
function _typename(type) {
if (typeof type === 'string') return type;
else return type.name;
if (typeof type === 'object') return type.name;
return type;
}
function userInputSetter(name, value) {