From 89ed2d602f8c29965b7969d572ce6a49e02ef4e4 Mon Sep 17 00:00:00 2001 From: Eric Tuvesson Date: Thu, 5 Sep 2024 09:59:11 +0200 Subject: [PATCH 01/25] feat: Add source maps (#63) --- .../webpack-configs/constants.js | 18 +++++++++--------- .../webpack-configs/webpack.viewer.dev.js | 10 +++++----- .../webpack-configs/webpack.viewer.prod.js | 3 ++- .../static/deploy/index.json | 3 ++- .../webpack-configs/constants.js | 18 ++++++++---------- .../webpack-configs/webpack.common.js | 2 +- .../webpack-configs/webpack.deploy.dev.js | 10 +++++----- .../webpack-configs/webpack.deploy.prod.js | 7 ++++--- .../webpack-configs/webpack.ssr.common.js | 2 +- .../webpack-configs/webpack.ssr.prod.js | 3 ++- .../webpack-configs/webpack.viewer.dev.js | 10 +++++----- .../webpack-configs/webpack.viewer.prod.js | 7 ++++--- 12 files changed, 48 insertions(+), 45 deletions(-) diff --git a/packages/noodl-viewer-cloud/webpack-configs/constants.js b/packages/noodl-viewer-cloud/webpack-configs/constants.js index bbbf34c7..9123752d 100644 --- a/packages/noodl-viewer-cloud/webpack-configs/constants.js +++ b/packages/noodl-viewer-cloud/webpack-configs/constants.js @@ -1,9 +1,9 @@ -const path = require('path'); - -module.exports = { - // Allows to define the output path of the files built by the viewer. - // - // For example in the CLI, we will also build this, just with a different output path. - outPath: process.env.OUT_PATH || path.resolve(__dirname, '../../noodl-editor/src/external'), - runtimeVersion: 'cloud-runtime-' + require('../package.json').version.replaceAll('.', '-') -}; +const path = require('path'); + +module.exports = { + // Allows to define the output path of the files built by the viewer. + // + // For example in the CLI, we will also build this, just with a different output path. + outPath: process.env.OUT_PATH || path.resolve(__dirname, '../../noodl-editor/src/external'), + runtimeVersion: 'cloud-runtime-' + require('../package.json').version.replaceAll('.', '-') +}; diff --git a/packages/noodl-viewer-cloud/webpack-configs/webpack.viewer.dev.js b/packages/noodl-viewer-cloud/webpack-configs/webpack.viewer.dev.js index ffc4c9ec..6481f21e 100644 --- a/packages/noodl-viewer-cloud/webpack-configs/webpack.viewer.dev.js +++ b/packages/noodl-viewer-cloud/webpack-configs/webpack.viewer.dev.js @@ -1,8 +1,8 @@ -const { merge } = require("webpack-merge"); -const common = require("./webpack.viewer.common.js"); +const { merge } = require('webpack-merge'); +const common = require('./webpack.viewer.common.js'); module.exports = merge(common, { - mode: "development", - devtool: "inline-source-map", - watch: true, + mode: 'development', + devtool: 'inline-source-map', + watch: true }); diff --git a/packages/noodl-viewer-cloud/webpack-configs/webpack.viewer.prod.js b/packages/noodl-viewer-cloud/webpack-configs/webpack.viewer.prod.js index 0ecaa9cf..188b34c1 100644 --- a/packages/noodl-viewer-cloud/webpack-configs/webpack.viewer.prod.js +++ b/packages/noodl-viewer-cloud/webpack-configs/webpack.viewer.prod.js @@ -2,5 +2,6 @@ const { merge } = require('webpack-merge'); const common = require('./webpack.viewer.common.js'); module.exports = merge(common, { - mode: 'production' + mode: 'production', + devtool: 'source-map' }); diff --git a/packages/noodl-viewer-react/static/deploy/index.json b/packages/noodl-viewer-react/static/deploy/index.json index f169aa97..66947b67 100644 --- a/packages/noodl-viewer-react/static/deploy/index.json +++ b/packages/noodl-viewer-react/static/deploy/index.json @@ -4,6 +4,7 @@ {"url":"noodl-app.png"}, {"url":"load_terminator.js"}, {"url":"noodl.deploy.js"}, + {"url":"noodl.deploy.js.map"}, {"url":"react.production.min.js"}, {"url":"react-dom.production.min.js"} -] \ No newline at end of file +] diff --git a/packages/noodl-viewer-react/webpack-configs/constants.js b/packages/noodl-viewer-react/webpack-configs/constants.js index ff65e650..542c4d5e 100644 --- a/packages/noodl-viewer-react/webpack-configs/constants.js +++ b/packages/noodl-viewer-react/webpack-configs/constants.js @@ -1,10 +1,8 @@ -const path = require("path"); - -module.exports = { - // Allows to define the output path of the files built by the viewer. - // - // For example in the CLI, we will also build this, just with a different output path. - outPath: - process.env.OUT_PATH || - path.resolve(__dirname, "../../noodl-editor/src/external"), -}; +const path = require('path'); + +module.exports = { + // Allows to define the output path of the files built by the viewer. + // + // For example in the CLI, we will also build this, just with a different output path. + outPath: process.env.OUT_PATH || path.resolve(__dirname, '../../noodl-editor/src/external') +}; diff --git a/packages/noodl-viewer-react/webpack-configs/webpack.common.js b/packages/noodl-viewer-react/webpack-configs/webpack.common.js index f1bbf69d..8c0d3430 100644 --- a/packages/noodl-viewer-react/webpack-configs/webpack.common.js +++ b/packages/noodl-viewer-react/webpack-configs/webpack.common.js @@ -8,7 +8,7 @@ module.exports = { resolve: { extensions: ['.tsx', '.ts', '.jsx', '.js'], fallback: { - events: require.resolve('events/'), + events: require.resolve('events/') } }, module: { diff --git a/packages/noodl-viewer-react/webpack-configs/webpack.deploy.dev.js b/packages/noodl-viewer-react/webpack-configs/webpack.deploy.dev.js index 1fb86ab4..b9246168 100644 --- a/packages/noodl-viewer-react/webpack-configs/webpack.deploy.dev.js +++ b/packages/noodl-viewer-react/webpack-configs/webpack.deploy.dev.js @@ -1,8 +1,8 @@ -const { merge } = require("webpack-merge"); -const common = require("./webpack.deploy.common.js"); +const { merge } = require('webpack-merge'); +const common = require('./webpack.deploy.common.js'); module.exports = merge(common, { - mode: "development", - devtool: "inline-source-map", - watch: true, + mode: 'development', + devtool: 'inline-source-map', + watch: true }); diff --git a/packages/noodl-viewer-react/webpack-configs/webpack.deploy.prod.js b/packages/noodl-viewer-react/webpack-configs/webpack.deploy.prod.js index d45e340f..c2886800 100644 --- a/packages/noodl-viewer-react/webpack-configs/webpack.deploy.prod.js +++ b/packages/noodl-viewer-react/webpack-configs/webpack.deploy.prod.js @@ -1,6 +1,7 @@ -const { merge } = require("webpack-merge"); -const common = require("./webpack.deploy.common.js"); +const { merge } = require('webpack-merge'); +const common = require('./webpack.deploy.common.js'); module.exports = merge(common, { - mode: "production", + mode: 'production', + devtool: 'source-map' }); diff --git a/packages/noodl-viewer-react/webpack-configs/webpack.ssr.common.js b/packages/noodl-viewer-react/webpack-configs/webpack.ssr.common.js index 9634bf12..2c12b808 100644 --- a/packages/noodl-viewer-react/webpack-configs/webpack.ssr.common.js +++ b/packages/noodl-viewer-react/webpack-configs/webpack.ssr.common.js @@ -42,7 +42,7 @@ module.exports = { resolve: { extensions: ['.tsx', '.ts', '.jsx', '.js'], fallback: { - events: require.resolve('events/'), + events: require.resolve('events/') } }, module: { diff --git a/packages/noodl-viewer-react/webpack-configs/webpack.ssr.prod.js b/packages/noodl-viewer-react/webpack-configs/webpack.ssr.prod.js index f0667278..212d8b98 100644 --- a/packages/noodl-viewer-react/webpack-configs/webpack.ssr.prod.js +++ b/packages/noodl-viewer-react/webpack-configs/webpack.ssr.prod.js @@ -2,5 +2,6 @@ const { merge } = require('webpack-merge'); const common = require('./webpack.ssr.common.js'); module.exports = merge(common, { - mode: 'production' + mode: 'production', + devtool: 'source-map' }); diff --git a/packages/noodl-viewer-react/webpack-configs/webpack.viewer.dev.js b/packages/noodl-viewer-react/webpack-configs/webpack.viewer.dev.js index ffc4c9ec..6481f21e 100644 --- a/packages/noodl-viewer-react/webpack-configs/webpack.viewer.dev.js +++ b/packages/noodl-viewer-react/webpack-configs/webpack.viewer.dev.js @@ -1,8 +1,8 @@ -const { merge } = require("webpack-merge"); -const common = require("./webpack.viewer.common.js"); +const { merge } = require('webpack-merge'); +const common = require('./webpack.viewer.common.js'); module.exports = merge(common, { - mode: "development", - devtool: "inline-source-map", - watch: true, + mode: 'development', + devtool: 'inline-source-map', + watch: true }); diff --git a/packages/noodl-viewer-react/webpack-configs/webpack.viewer.prod.js b/packages/noodl-viewer-react/webpack-configs/webpack.viewer.prod.js index 542a4d0c..188b34c1 100644 --- a/packages/noodl-viewer-react/webpack-configs/webpack.viewer.prod.js +++ b/packages/noodl-viewer-react/webpack-configs/webpack.viewer.prod.js @@ -1,6 +1,7 @@ -const { merge } = require("webpack-merge"); -const common = require("./webpack.viewer.common.js"); +const { merge } = require('webpack-merge'); +const common = require('./webpack.viewer.common.js'); module.exports = merge(common, { - mode: "production", + mode: 'production', + devtool: 'source-map' }); From d85dce8d024d95735a9007207835c5b66a943fd2 Mon Sep 17 00:00:00 2001 From: Eric Tuvesson Date: Thu, 5 Sep 2024 13:19:17 +0200 Subject: [PATCH 02/25] refactor(editor): useNodeReferences to React context (#64) --- .../NodeReferencesContext.tsx | 133 ++++++++++++++++++ .../contexts/NodeReferencesContext/index.ts | 1 + .../src/pages/EditorPage/EditorPage.tsx | 49 ++++--- .../NodeReferencesPanel.tsx | 116 ++------------- 4 files changed, 169 insertions(+), 130 deletions(-) create mode 100644 packages/noodl-editor/src/editor/src/contexts/NodeReferencesContext/NodeReferencesContext.tsx create mode 100644 packages/noodl-editor/src/editor/src/contexts/NodeReferencesContext/index.ts diff --git a/packages/noodl-editor/src/editor/src/contexts/NodeReferencesContext/NodeReferencesContext.tsx b/packages/noodl-editor/src/editor/src/contexts/NodeReferencesContext/NodeReferencesContext.tsx new file mode 100644 index 00000000..fff24a13 --- /dev/null +++ b/packages/noodl-editor/src/editor/src/contexts/NodeReferencesContext/NodeReferencesContext.tsx @@ -0,0 +1,133 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; + +import { type ComponentModel } from '@noodl-models/componentmodel'; +import { type NodeGraphNode } from '@noodl-models/nodegraphmodel'; +import { type NodeLibraryNodeType } from '@noodl-models/nodelibrary'; +import { type Slot } from '@noodl-core-ui/types/global'; +import { ProjectModel } from '@noodl-models/projectmodel'; +import { EventDispatcher } from '../../../../shared/utils/EventDispatcher'; + +export type NodeReference = { + type: NodeLibraryNodeType; + displayName: string; + referenaces: { + displayName: string; + node?: NodeGraphNode; + component: ComponentModel; + }[]; +}; + +export interface NodeReferencesContext { + nodeReferences: NodeReference[]; +} + +const NodeReferencesContext = createContext({ + nodeReferences: [], +}); + +export interface NodeReferencesContextProps { + children: Slot; +} + +export function NodeReferencesContextProvider({ children }: NodeReferencesContextProps) { + const [group] = useState({}); + const [nodeReferences, setNodeReferences] = useState([]); + + useEffect(() => { + function updateIndex() { + const types: { [key: string]: NodeReference['type'] } = {}; + const references = new Map(); + + function handleComponent(component: ComponentModel) { + component.forEachNode((node: NodeGraphNode) => { + const name = node.type.name; + + // Add the reference + references.set(name, [ + ...(references.get(name) || []), + { + displayName: component.displayName || component.name, + node, + component + } + ]); + + // Repeater + if (name === 'For Each' && node.parameters.template) { + const templateComponent = ProjectModel.instance.getComponentWithName(node.parameters.template); + + if (templateComponent) { + references.set(templateComponent.fullName, [ + ...(references.get(templateComponent.fullName) || []), + { + displayName: component.displayName || component.name, + node, + component + } + ]); + + handleComponent(templateComponent); + } + } + + // Add some metadata for this node if we dont have it yet. + if (!types[name]) { + types[name] = node.type; + } + }); + } + + // Loop all the nodes in the project + ProjectModel.instance.forEachComponent(handleComponent); + + // Combine the result to look a little better. + const results: NodeReference[] = Array.from(references.keys()) + .map((key) => ({ + type: types[key], + displayName: types[key]?.displayName || key, + referenaces: references.get(key) + })) + .sort((a, b) => b.referenaces.length - a.referenaces.length); + + setNodeReferences(results); + } + + updateIndex(); + + EventDispatcher.instance.on( + [ + 'Model.nodeAdded', + 'Model.nodeRemoved', + 'Model.componentAdded', + 'Model.componentRemoved', + 'Model.componentRenamed' + ], + updateIndex, + group + ); + + return function () { + EventDispatcher.instance.off(group); + }; + }, []); + + return ( + + {children} + + ); +} + +export function useNodeReferencesContext() { + const context = useContext(NodeReferencesContext); + + if (context === undefined) { + throw new Error('useNodeReferencesContext must be a child of NodeReferencesContextProvider'); + } + + return context; +} diff --git a/packages/noodl-editor/src/editor/src/contexts/NodeReferencesContext/index.ts b/packages/noodl-editor/src/editor/src/contexts/NodeReferencesContext/index.ts new file mode 100644 index 00000000..3fc21d41 --- /dev/null +++ b/packages/noodl-editor/src/editor/src/contexts/NodeReferencesContext/index.ts @@ -0,0 +1 @@ +export * from './NodeReferencesContext'; diff --git a/packages/noodl-editor/src/editor/src/pages/EditorPage/EditorPage.tsx b/packages/noodl-editor/src/editor/src/pages/EditorPage/EditorPage.tsx index 0c04092b..68e6807c 100644 --- a/packages/noodl-editor/src/editor/src/pages/EditorPage/EditorPage.tsx +++ b/packages/noodl-editor/src/editor/src/pages/EditorPage/EditorPage.tsx @@ -1,4 +1,6 @@ import { NodeGraphContextProvider } from '@noodl-contexts/NodeGraphContext/NodeGraphContext'; +import { NodeReferencesContextProvider } from '@noodl-contexts/NodeReferencesContext'; +import { PluginContextProvider } from '@noodl-contexts/PluginContext'; import { ProjectDesignTokenContextProvider } from '@noodl-contexts/ProjectDesignTokenContext'; import { useKeyboardCommands } from '@noodl-hooks/useKeyboardCommands'; import { useModel } from '@noodl-hooks/useModel'; @@ -43,7 +45,6 @@ import { BaseWindow } from '../../views/windows/BaseWindow'; import { whatsnewRender } from '../../whats-new'; import { IRouteProps } from '../AppRoute'; import { useSetupSettings } from './useSetupSettings'; -import { PluginContextProvider } from '@noodl-contexts/PluginContext'; // eslint-disable-next-line @typescript-eslint/no-var-requires const ImportOverwritePopupTemplate = require('../../templates/importoverwritepopup.html'); @@ -223,28 +224,30 @@ export function EditorPage({ route }: EditorPageProps) { return ( - - - - {isLoading ? ( - - ) : ( - <> - } - second={{Boolean(Document) && }} - sizeMin={200} - size={frameDividerSize} - horizontal - onSizeChanged={setFrameDividerSize} - /> - - {Boolean(lesson) && } - - )} - - - + + + + + {isLoading ? ( + + ) : ( + <> + } + second={{Boolean(Document) && }} + sizeMin={200} + size={frameDividerSize} + horizontal + onSizeChanged={setFrameDividerSize} + /> + + {Boolean(lesson) && } + + )} + + + + ); } diff --git a/packages/noodl-editor/src/editor/src/views/panels/NodeReferencesPanel/NodeReferencesPanel.tsx b/packages/noodl-editor/src/editor/src/views/panels/NodeReferencesPanel/NodeReferencesPanel.tsx index 5d24fb69..3669c0e9 100644 --- a/packages/noodl-editor/src/editor/src/views/panels/NodeReferencesPanel/NodeReferencesPanel.tsx +++ b/packages/noodl-editor/src/editor/src/views/panels/NodeReferencesPanel/NodeReferencesPanel.tsx @@ -1,14 +1,12 @@ import { NodeGraphContextTmp } from '@noodl-contexts/NodeGraphContext/NodeGraphContext'; +import { type NodeReference, useNodeReferencesContext } from '@noodl-contexts/NodeReferencesContext'; import { useFocusRefOnPanelActive } from '@noodl-hooks/useFocusRefOnPanelActive'; import { useNodeLibraryLoaded } from '@noodl-hooks/useNodeLibraryLoaded'; -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useMemo, useRef, useState } from 'react'; import { INodeColorScheme } from '@noodl-types/nodeTypes'; -import { ComponentModel } from '@noodl-models/componentmodel'; -import { NodeGraphNode } from '@noodl-models/nodegraphmodel'; -import { NodeLibrary, NodeLibraryNodeType } from '@noodl-models/nodelibrary'; +import { NodeLibrary } from '@noodl-models/nodelibrary'; import { BasicNodeType } from '@noodl-models/nodelibrary/BasicNodeType'; -import { ProjectModel } from '@noodl-models/projectmodel'; import { EditorNode } from '@noodl-core-ui/components/common/EditorNode'; import { IconName, IconSize } from '@noodl-core-ui/components/common/Icon'; @@ -26,113 +24,17 @@ import { Section, SectionVariant } from '@noodl-core-ui/components/sidebar/Secti import { Label } from '@noodl-core-ui/components/typography/Label'; import { NodeReferencesPanel_ID } from '.'; -import { EventDispatcher } from '../../../../../shared/utils/EventDispatcher'; - -type ResultItem = { - type: NodeLibraryNodeType; - displayName: string; - referenaces: { - displayName: string; - node?: NodeGraphNode; - component: ComponentModel; - }[]; -}; - -function useNodeReferences() { - const [group] = useState({}); - const [result, setResult] = useState([]); - - useEffect(() => { - function updateIndex() { - const types: { [key: string]: ResultItem['type'] } = {}; - const references = new Map(); - - function handleComponent(component: ComponentModel) { - component.forEachNode((node: NodeGraphNode) => { - const name = node.type.name; - - // Add the reference - references.set(name, [ - ...(references.get(name) || []), - { - displayName: component.displayName || component.name, - node, - component - } - ]); - - // Repeater - if (name === 'For Each' && node.parameters.template) { - const templateComponent = ProjectModel.instance.getComponentWithName(node.parameters.template); - - if (templateComponent) { - references.set(templateComponent.fullName, [ - ...(references.get(templateComponent.fullName) || []), - { - displayName: component.displayName || component.name, - node, - component - } - ]); - - handleComponent(templateComponent); - } - } - - // Add some metadata for this node if we dont have it yet. - if (!types[name]) { - types[name] = node.type; - } - }); - } - - // Loop all the nodes in the project - ProjectModel.instance.forEachComponent(handleComponent); - - // Combine the result to look a little better. - const results: ResultItem[] = Array.from(references.keys()) - .map((key) => ({ - type: types[key], - displayName: types[key]?.displayName || key, - referenaces: references.get(key) - })) - .sort((a, b) => b.referenaces.length - a.referenaces.length); - - setResult(results); - } - - updateIndex(); - - EventDispatcher.instance.on( - [ - 'Model.nodeAdded', - 'Model.nodeRemoved', - 'Model.componentAdded', - 'Model.componentRemoved', - 'Model.componentRenamed' - ], - updateIndex, - group - ); - - return function () { - EventDispatcher.instance.off(group); - }; - }, []); - - return [result]; -} export function NodeReferencesPanel() { const [searchTerm, setSearchTerm] = useState(''); const [includeCoreNodes, setIncludeCoreNodes] = useState(false); const inputRef = useRef(null); - const [result] = useNodeReferences(); + const { nodeReferences } = useNodeReferencesContext(); const nodeLibraryLoaded = useNodeLibraryLoaded(); useFocusRefOnPanelActive(inputRef, NodeReferencesPanel_ID); - function searchFilter(x: ResultItem) { + function searchFilter(x: NodeReference) { if (x.displayName.toLowerCase().includes(searchTerm)) { return true; } @@ -144,7 +46,7 @@ export function NodeReferencesPanel() { return false; } - let filteredResult = result.filter(searchFilter); + let filteredResult = nodeReferences.filter(searchFilter); if (!includeCoreNodes) { filteredResult = filteredResult.filter((x) => x.displayName.startsWith('/')); } @@ -185,7 +87,7 @@ export function NodeReferencesPanel() { } interface ItemProps { - entry: ResultItem; + entry: NodeReference; } function Item({ entry }: ItemProps) { @@ -245,8 +147,8 @@ function Item({ entry }: ItemProps) { } interface ItemReferenceProps { - entry: ResultItem; - referenace: ResultItem['referenaces'][0]; + entry: NodeReference; + referenace: NodeReference['referenaces'][0]; colors: INodeColorScheme; } From 34c3d071121309f05572b47b447272464f868776 Mon Sep 17 00:00:00 2001 From: Eric Tuvesson Date: Thu, 5 Sep 2024 13:35:15 +0200 Subject: [PATCH 03/25] feat(editor): Add "Used in x places" in Component List menu (#65) --- .../NodeReferencesContext.tsx | 11 +++++++++- .../panels/componentspanel/ComponentsPanel.ts | 21 ++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/noodl-editor/src/editor/src/contexts/NodeReferencesContext/NodeReferencesContext.tsx b/packages/noodl-editor/src/editor/src/contexts/NodeReferencesContext/NodeReferencesContext.tsx index fff24a13..b14e8f9e 100644 --- a/packages/noodl-editor/src/editor/src/contexts/NodeReferencesContext/NodeReferencesContext.tsx +++ b/packages/noodl-editor/src/editor/src/contexts/NodeReferencesContext/NodeReferencesContext.tsx @@ -8,7 +8,7 @@ import { ProjectModel } from '@noodl-models/projectmodel'; import { EventDispatcher } from '../../../../shared/utils/EventDispatcher'; export type NodeReference = { - type: NodeLibraryNodeType; + type: NodeLibraryNodeType | undefined; displayName: string; referenaces: { displayName: string; @@ -25,6 +25,14 @@ const NodeReferencesContext = createContext({ nodeReferences: [], }); +// Since all the editor code is not written in React we need a way to be able to +// access this information outside of React too. +let HACK_nodeReferences: NodeReference[] = []; +export function HACK_findNodeReference(componentName: string): NodeReference | undefined { + return HACK_nodeReferences.find(x => x.type?.fullName === componentName); +} + + export interface NodeReferencesContextProps { children: Slot; } @@ -89,6 +97,7 @@ export function NodeReferencesContextProvider({ children }: NodeReferencesContex })) .sort((a, b) => b.referenaces.length - a.referenaces.length); + HACK_nodeReferences = results; setNodeReferences(results); } diff --git a/packages/noodl-editor/src/editor/src/views/panels/componentspanel/ComponentsPanel.ts b/packages/noodl-editor/src/editor/src/views/panels/componentspanel/ComponentsPanel.ts index 9c842ca8..a358c6bd 100644 --- a/packages/noodl-editor/src/editor/src/views/panels/componentspanel/ComponentsPanel.ts +++ b/packages/noodl-editor/src/editor/src/views/panels/componentspanel/ComponentsPanel.ts @@ -17,9 +17,11 @@ import { EventDispatcher } from '../../../../../shared/utils/EventDispatcher'; import View from '../../../../../shared/view'; import { NodeGraphEditor } from '../../nodegrapheditor'; import * as NewPopupLayer from '../../PopupLayer/index'; +import { type PopupMenuItem } from '../../PopupLayer/index'; import { ToastLayer } from '../../ToastLayer/ToastLayer'; import { ComponentsPanelFolder } from './ComponentsPanelFolder'; import { ComponentTemplates } from './ComponentTemplates'; +import { HACK_findNodeReference } from '@noodl-contexts/NodeReferencesContext'; const PopupLayer = require('@noodl-views/popuplayer'); const ComponentsPanelTemplate = require('../../../templates/componentspanel.html'); @@ -961,7 +963,7 @@ export class ComponentsPanelView extends View { forRuntimeType: this.getRuntimeType() }); - let items: TSFixme[] = templates.map((t) => ({ + let items: PopupMenuItem[] = templates.map((t) => ({ icon: IconName.Plus, label: t.label, onClick: () => { @@ -987,6 +989,10 @@ export class ComponentsPanelView extends View { }); } + // Find references + const nodeReference = HACK_findNodeReference(scope.comp.name); + const nodeReferencesText = `Used in ${nodeReference?.referenaces?.length || 0} places`; + items = items.concat([ { icon: IconName.Pencil, @@ -1011,6 +1017,9 @@ export class ComponentsPanelView extends View { _this.onDeleteClicked(scope, el); evt.stopPropagation(); } + }, + { + label: nodeReferencesText } ]); @@ -1110,6 +1119,16 @@ 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`; + + items = items.concat([{ + label: nodeReferencesText + }]); + } + const menu = new NewPopupLayer.PopupMenu({ items: items }); From 46f6cb2da9da8a4a0aef021ff85b872db1e322ee Mon Sep 17 00:00:00 2001 From: Eric Tuvesson Date: Sat, 7 Sep 2024 14:14:39 +0200 Subject: [PATCH 04/25] feat(viewer-react): Add Target Page input to "Push Component To Stack" (#66) --- .../src/nodes/navigation/navigate.js | 3 +- .../src/nodes/navigation/navigation-stack.jsx | 113 +++++++++++++----- 2 files changed, 82 insertions(+), 34 deletions(-) diff --git a/packages/noodl-viewer-react/src/nodes/navigation/navigate.js b/packages/noodl-viewer-react/src/nodes/navigation/navigate.js index 8090044a..8c5d8893 100644 --- a/packages/noodl-viewer-react/src/nodes/navigation/navigate.js +++ b/packages/noodl-viewer-react/src/nodes/navigation/navigate.js @@ -192,8 +192,7 @@ function setup(context, graphModel) { enums: pages.map((p) => ({ label: p.label, value: p.id - })), - allowEditOnly: true + })) }, group: 'General', displayName: 'Target Page', diff --git a/packages/noodl-viewer-react/src/nodes/navigation/navigation-stack.jsx b/packages/noodl-viewer-react/src/nodes/navigation/navigation-stack.jsx index 0de7d327..b9db1eb1 100644 --- a/packages/noodl-viewer-react/src/nodes/navigation/navigation-stack.jsx +++ b/packages/noodl-viewer-react/src/nodes/navigation/navigation-stack.jsx @@ -1,3 +1,5 @@ +import React from 'react'; + import ASyncQueue from '../../async-queue'; import { createNodeFromReactComponent } from '../../react-component-node'; @@ -75,10 +77,13 @@ const PageStack = { const info = [{ type: 'text', value: 'Active Components:' }]; return info.concat( - this._internal.stack.map((p, i) => ({ - type: 'text', - value: '- ' + this._internal.pages.find((pi) => pi.id === p.pageId).label - })) + this._internal.stack.map((p) => { + const pageInfo = this._findPage(p.pageId); + return { + type: 'text', + value: '- ' + pageInfo.label + }; + }) ); }, defaultCss: { @@ -189,12 +194,31 @@ const PageStack = { _deregisterPageStack() { NavigationHandler.instance.deregisterPageStack(this._internal.name, this); }, - _pageNameForId(id) { - if (this._internal.pages === undefined) return; - const page = this._internal.pages.find((p) => p.id === id); - if (page === undefined) return; + /** + * @param {String} pageIdOrLabel + */ + _findPage(pageIdOrLabel) { + if (this._internal.pageInfo[pageIdOrLabel]) { + const pageInfo = this._internal.pageInfo[pageIdOrLabel]; + const pageRef = this._internal.pages.find((x) => x.id === pageIdOrLabel); + return { + component: String(pageInfo.component), + label: String(pageRef.label), + id: String(pageIdOrLabel) + }; + } - return page.label; + const pageRef = this._internal.pages.find((x) => x.label === pageIdOrLabel); + if (pageRef) { + const pageInfo = this._internal.pageInfo[pageRef.id]; + return { + component: String(pageInfo.component), + label: String(pageRef.label), + id: String(pageRef.id) + }; + } + + return undefined; }, setPageOutputs(outputs) { for (const prop in outputs) { @@ -230,8 +254,9 @@ const PageStack = { if (this._internal.pages === undefined || this._internal.pages.length === 0) return; - var startPageId, - params = {}; + let startPageId; + let params = {}; + var pageFromUrl = this.matchPageFromUrl(); if (pageFromUrl !== undefined) { // We have an url matching a page, use that page as start page @@ -239,13 +264,16 @@ const PageStack = { params = Object.assign({}, pageFromUrl.query, pageFromUrl.params); } else { - var startPageId = this._internal.startPageId; + startPageId = this._internal.startPageId; if (startPageId === undefined) startPageId = this._internal.pages[0].id; } - var pageInfo = this._internal.pageInfo[startPageId]; - - if (pageInfo === undefined || pageInfo.component === undefined) return; // No component specified for page + // Find the page by either ID or by Label + const pageInfo = this._findPage(startPageId); + if (pageInfo === undefined || pageInfo.component === undefined) { + // No page was found + return; + } var content = await this.nodeScope.createNode(pageInfo.component, guid()); @@ -269,7 +297,7 @@ const PageStack = { ]; this.setPageOutputs({ - topPageName: this._pageNameForId(startPageId), + topPageName: pageInfo.label, stackDepth: this._internal.stack.length }); }, @@ -458,13 +486,22 @@ const PageStack = { this._internal.asyncQueue.enqueue(this.replaceAsync.bind(this, args)); }, async replaceAsync(args) { - if (this._internal.pages === undefined || this._internal.pages.length === 0) return; + if (this._internal.pages === undefined || this._internal.pages.length === 0) { + return; + } - if (this._internal.isTransitioning) return; + if (this._internal.isTransitioning) { + return; + } - var pageId = args.target || this._internal.pages[0].id; - var pageInfo = this._internal.pageInfo[pageId]; - if (pageInfo === undefined || pageInfo.component === undefined) return; // No component specified for page + const pageId = args.target || this._internal.pages[0].id; + + // Find the page by either ID or by Label + const pageInfo = this._findPage(pageId); + if (pageInfo === undefined || pageInfo.component === undefined) { + // No page was found + return; + } // Remove all current pages in the stack var children = this.getChildren(); @@ -498,7 +535,7 @@ const PageStack = { ]; this.setPageOutputs({ - topPageName: this._pageNameForId(pageId), + topPageName: pageInfo.label, stackDepth: this._internal.stack.length }); @@ -510,13 +547,22 @@ const PageStack = { this._internal.asyncQueue.enqueue(this.navigateAsync.bind(this, args)); }, async navigateAsync(args) { - if (this._internal.pages === undefined || this._internal.pages.length === 0) return; + if (this._internal.pages === undefined || this._internal.pages.length === 0) { + return; + } - if (this._internal.isTransitioning) return; + if (this._internal.isTransitioning) { + return; + } + + const pageId = args.target || this._internal.pages[0].id; - var pageId = args.target || this._internal.pages[0].id; - var pageInfo = this._internal.pageInfo[pageId]; - if (pageInfo === undefined || pageInfo.component === undefined) return; // No component specified for page + // Find the page by either ID or by Label + const pageInfo = this._findPage(pageId); + if (pageInfo === undefined || pageInfo.component === undefined) { + // No page was found + return; + } // Create the container group const group = this.createPageContainer(); @@ -530,7 +576,7 @@ const PageStack = { group.addChild(content); // Connect navigate back nodes - var navigateBackNodes = content.nodeScope.getNodesWithType('PageStackNavigateBack'); + const navigateBackNodes = content.nodeScope.getNodesWithType('PageStackNavigateBack'); if (navigateBackNodes && navigateBackNodes.length > 0) { for (var j = 0; j < navigateBackNodes.length; j++) { navigateBackNodes[j]._setBackCallback(this.back.bind(this)); @@ -538,8 +584,8 @@ const PageStack = { } // Push the new top - var top = this._internal.stack[this._internal.stack.length - 1]; - var newTop = { + const top = this._internal.stack[this._internal.stack.length - 1]; + const newTop = { from: top.page, page: group, pageInfo: pageInfo, @@ -551,7 +597,7 @@ const PageStack = { }; this._internal.stack.push(newTop); this.setPageOutputs({ - topPageName: this._pageNameForId(args.target), + topPageName: pageInfo.label, stackDepth: this._internal.stack.length }); this._updateUrlWithTopPage(); @@ -584,8 +630,11 @@ const PageStack = { this.addChild(top.from, 0); top.backCallback && top.backCallback(args.backAction, args.results); + // Find the page by either ID or by Label + const pageInfo = this._findPage(this._internal.stack[this._internal.stack.length - 2].pageId); + this.setPageOutputs({ - topPageName: this._pageNameForId(this._internal.stack[this._internal.stack.length - 2].pageId), + topPageName: pageInfo.label, stackDepth: this._internal.stack.length - 1 }); From cc79ea5f7e4b73c4bff80882ed5f9796c32e79cc Mon Sep 17 00:00:00 2001 From: Eric Tuvesson Date: Sat, 7 Sep 2024 14:31:06 +0200 Subject: [PATCH 05/25] feat(viewer-react): Add groups to Component Stack outputs (#67) --- .../src/nodes/navigation/navigation-stack.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/noodl-viewer-react/src/nodes/navigation/navigation-stack.jsx b/packages/noodl-viewer-react/src/nodes/navigation/navigation-stack.jsx index b9db1eb1..5a45844b 100644 --- a/packages/noodl-viewer-react/src/nodes/navigation/navigation-stack.jsx +++ b/packages/noodl-viewer-react/src/nodes/navigation/navigation-stack.jsx @@ -175,6 +175,7 @@ const PageStack = { topPageName: { type: 'string', displayName: 'Top Component Name', + group: 'General', get() { return this._internal.topPageName; } @@ -182,6 +183,7 @@ const PageStack = { stackDepth: { type: 'number', displayName: 'Stack Depth', + group: 'General', get() { return this._internal.stackDepth; } From 48541347f0a63ba2b9182eceb581b8b5359071d9 Mon Sep 17 00:00:00 2001 From: Eric Tuvesson Date: Mon, 9 Sep 2024 17:35:08 +0200 Subject: [PATCH 06/25] fix(editor): Remove all the Cloud Triggers from the Cloud Function node Functions dropdown (#69) --- .../models/NodeTypeAdapters/CloudFunctionAdapter.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/noodl-editor/src/editor/src/models/NodeTypeAdapters/CloudFunctionAdapter.ts b/packages/noodl-editor/src/editor/src/models/NodeTypeAdapters/CloudFunctionAdapter.ts index 86f01e86..5e7fafb0 100644 --- a/packages/noodl-editor/src/editor/src/models/NodeTypeAdapters/CloudFunctionAdapter.ts +++ b/packages/noodl-editor/src/editor/src/models/NodeTypeAdapters/CloudFunctionAdapter.ts @@ -57,10 +57,13 @@ export class CloudFunctionAdapter extends NodeTypeAdapter { // Collect all cloud function components const functionRequestNodes = ProjectModel.instance.getNodesWithType('noodl.cloud.request'); - const functions = functionRequestNodes.map((r) => { - const component = r.owner.owner; - return component.fullName; - }); + const functions = functionRequestNodes + .map((r) => { + const component = r.owner.owner; + return component.fullName; + }) + // Remove all the Cloud Trigger functions + .filter((x) => !x.startsWith('/#__cloud__/__noodl_cloud_triggers__')); ports.push({ plug: 'input', From 5febc490b4471a011d6d858d898d5986056e811a Mon Sep 17 00:00:00 2001 From: Eric Tuvesson Date: Tue, 10 Sep 2024 11:31:51 +0200 Subject: [PATCH 07/25] feat(runtime): Query Records, add "Is Empty" output (#70) --- .../std-library/data/dbcollectionnode2.js | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/noodl-runtime/src/nodes/std-library/data/dbcollectionnode2.js b/packages/noodl-runtime/src/nodes/std-library/data/dbcollectionnode2.js index c5a1c2c6..d6d3d7d8 100644 --- a/packages/noodl-runtime/src/nodes/std-library/data/dbcollectionnode2.js +++ b/packages/noodl-runtime/src/nodes/std-library/data/dbcollectionnode2.js @@ -28,6 +28,7 @@ var DbCollectionNode = { _this.scheduleAfterInputsHaveUpdated(function () { _this.flagOutputDirty('count'); _this.flagOutputDirty('firstItemId'); + _this.flagOutputDirty('isEmpty'); collectionChangedScheduled = false; }); }; @@ -66,6 +67,7 @@ var DbCollectionNode = { _this.flagOutputDirty('count'); _this.flagOutputDirty('firstItemId'); + _this.flagOutputDirty('isEmpty'); } if (args.type === 'create') { @@ -91,6 +93,7 @@ var DbCollectionNode = { _this.flagOutputDirty('count'); _this.flagOutputDirty('firstItemId'); + _this.flagOutputDirty('isEmpty'); } else if (matchesQuery && !_this._internal.collection.contains(m)) { // It's not part of the result collection but now matches they query, add it and resort _addModelAtCorrectIndex(m); @@ -106,6 +109,7 @@ var DbCollectionNode = { _this.flagOutputDirty('count'); _this.flagOutputDirty('firstItemId'); + _this.flagOutputDirty('isEmpty'); } } }; @@ -153,6 +157,17 @@ var DbCollectionNode = { } } }, + isEmpty: { + type: 'boolean', + displayName: 'Is Empty', + group: 'General', + getter: function () { + if (this._internal.collection) { + return this._internal.collection.size() === 0; + } + return true; + } + }, count: { type: 'number', displayName: 'Count', @@ -189,6 +204,7 @@ var DbCollectionNode = { setCollection: function (collection) { this.bindCollection(collection); this.flagOutputDirty('firstItemId'); + this.flagOutputDirty('isEmpty'); this.flagOutputDirty('items'); this.flagOutputDirty('count'); }, @@ -257,7 +273,7 @@ var DbCollectionNode = { limit: limit, skip: skip, count: count, - success: (results,count) => { + success: (results, count) => { if (results !== undefined) { _c.set( results.map((i) => { @@ -267,10 +283,9 @@ var DbCollectionNode = { }) ); } - if(count !== undefined) { + if (count !== undefined) { this._internal.storageSettings.storageTotalCount = count; - if(this.hasOutput('storageTotalCount')) - this.flagOutputDirty('storageTotalCount'); + if (this.hasOutput('storageTotalCount')) this.flagOutputDirty('storageTotalCount'); } this.setCollection(_c); this.sendSignalOnOutput('fetched'); @@ -383,7 +398,7 @@ var DbCollectionNode = { if (!storageSettings['storageEnableLimit']) return; else return storageSettings['storageSkip'] || 0; }, - getStorageFetchTotalCount: function() { + getStorageFetchTotalCount: function () { const storageSettings = this._internal.storageSettings; return !!storageSettings['storageEnableCount']; From e1a1b312136984785aeb0b5208a731ea27d2d64b Mon Sep 17 00:00:00 2001 From: Eric Tuvesson Date: Wed, 11 Sep 2024 16:14:49 +0200 Subject: [PATCH 08/25] feat(viewer-react): Array, add "First Item Id" output (#71) Same behaviour as Query Record's "First Record Id" --- .../nodes/std-library/data/collectionnode2.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/noodl-viewer-react/src/nodes/std-library/data/collectionnode2.js b/packages/noodl-viewer-react/src/nodes/std-library/data/collectionnode2.js index f32ac1e9..d1502c1e 100644 --- a/packages/noodl-viewer-react/src/nodes/std-library/data/collectionnode2.js +++ b/packages/noodl-viewer-react/src/nodes/std-library/data/collectionnode2.js @@ -2,8 +2,7 @@ const { Node } = require('@noodl/runtime'); -var Model = require('@noodl/runtime/src/model'), - Collection = require('@noodl/runtime/src/collection'); +const Collection = require('@noodl/runtime/src/collection'); var CollectionNode = { name: 'Collection2', @@ -27,6 +26,7 @@ var CollectionNode = { _this.scheduleAfterInputsHaveUpdated(function () { _this.sendSignalOnOutput('changed'); + _this.flagOutputDirty('firstItemId'); _this.flagOutputDirty('count'); collectionChangedScheduled = false; }); @@ -117,6 +117,17 @@ var CollectionNode = { return this._internal.collection; } }, + firstItemId: { + type: 'string', + displayName: 'First Item Id', + group: 'General', + getter: function () { + if (this._internal.collection) { + var firstItem = this._internal.collection.get(0); + if (firstItem !== undefined) return firstItem.getId(); + } + } + }, count: { type: 'number', displayName: 'Count', @@ -150,6 +161,7 @@ var CollectionNode = { collection.on('change', this._internal.collectionChangedCallback); this.flagOutputDirty('items'); + this.flagOutputDirty('firstItemId'); this.flagOutputDirty('count'); }, setSourceCollection: function (collection) { From 2eb18acfefcf0f8385fd4f26b82199516a9b9f51 Mon Sep 17 00:00:00 2001 From: Eric Tuvesson Date: Wed, 11 Sep 2024 16:15:02 +0200 Subject: [PATCH 09/25] fix(viewer-react): Update CurrentUserObject TS typings (#72) --- packages/noodl-viewer-react/static/viewer/global.d.ts.keep | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/noodl-viewer-react/static/viewer/global.d.ts.keep b/packages/noodl-viewer-react/static/viewer/global.d.ts.keep index e21e938f..5461b824 100644 --- a/packages/noodl-viewer-react/static/viewer/global.d.ts.keep +++ b/packages/noodl-viewer-react/static/viewer/global.d.ts.keep @@ -482,7 +482,12 @@ declare namespace Noodl { const Records: RecordsApi; interface CurrentUserObject { - UserId: string; + id: string; + email: string; + emailVerified: boolean; + username: string; + + Properties: unknown; /** * Log out the current user and terminate the session. From 72aec29e27100fc3a8fb253663e0e6624e9452a6 Mon Sep 17 00:00:00 2001 From: Eric Tuvesson Date: Mon, 16 Sep 2024 22:12:07 +0200 Subject: [PATCH 10/25] feat(runtime): Add "className" option support to "relatedTo" (#73) * feat(runtime): Add "className" option support to "relatedTo" Also includes error handling when "className" is not found. --- packages/noodl-runtime/src/api/queryutils.js | 100 +++++++++++-------- 1 file changed, 60 insertions(+), 40 deletions(-) diff --git a/packages/noodl-runtime/src/api/queryutils.js b/packages/noodl-runtime/src/api/queryutils.js index 9bd10bf4..6ed811db 100644 --- a/packages/noodl-runtime/src/api/queryutils.js +++ b/packages/noodl-runtime/src/api/queryutils.js @@ -36,16 +36,15 @@ function convertVisualFilter(query, options) { const _res = {}; var cond; var value = query.input !== undefined ? inputs[query.input] : query.value; - + if (query.operator === 'exist') { - _res[query.property] = { $exists: true }; - return _res; - } - else if (query.operator === 'not exist') { - _res[query.property] = { $exists: false }; - return _res; + _res[query.property] = { $exists: true }; + return _res; + } else if (query.operator === 'not exist') { + _res[query.property] = { $exists: false }; + return _res; } - + if (value === undefined) return; if (CloudStore._collections[options.collectionName]) @@ -80,7 +79,6 @@ function convertVisualFilter(query, options) { cond = { $regex: value, $options: 'i' }; } - _res[query.property] = cond; return _res; @@ -163,10 +161,22 @@ function _value(v) { return v; } +/** + * + * @param {Record} filter + * @param {{ + * collectionName?: string; + * modelScope?: unknown; + * error: (error: string) => void; + * }} options + * @returns + */ function convertFilterOp(filter, options) { const keys = Object.keys(filter); if (keys.length === 0) return {}; - if (keys.length !== 1) return options.error('Filter must only have one key found ' + keys.join(',')); + if (keys.length !== 1) { + return options.error('Filter must only have one key found ' + keys.join(',')); + } const res = {}; const key = keys[0]; @@ -179,18 +189,27 @@ function convertFilterOp(filter, options) { } else if (filter['idContainedIn'] !== undefined) { res['objectId'] = { $in: filter['idContainedIn'] }; } else if (filter['relatedTo'] !== undefined) { - var modelId = filter['relatedTo']['id']; - if (modelId === undefined) return options.error('Must provide id in relatedTo filter'); + const modelId = filter['relatedTo']['id']; + if (modelId === undefined) { + return options.error('Must provide id in relatedTo filter'); + } - var relationKey = filter['relatedTo']['key']; - if (relationKey === undefined) return options.error('Must provide key in relatedTo filter'); + const relationKey = filter['relatedTo']['key']; + if (relationKey === undefined) { + return options.error('Must provide key in relatedTo filter'); + } + + const className = filter['relatedTo']['className'] || (options.modelScope || Model).get(modelId)?._class; + if (typeof className === 'undefined') { + // Either the pointer is loaded as an object or we allow passing in the className. + return options.error('Must preload the Pointer or include className'); + } - var m = (options.modelScope || Model).get(modelId); res['$relatedTo'] = { object: { __type: 'Pointer', objectId: modelId, - className: m._class + className }, key: relationKey }; @@ -208,13 +227,14 @@ function convertFilterOp(filter, options) { else if (opAndValue['containedIn'] !== undefined) res[key] = { $in: opAndValue['containedIn'] }; else if (opAndValue['notContainedIn'] !== undefined) res[key] = { $nin: opAndValue['notContainedIn'] }; else if (opAndValue['pointsTo'] !== undefined) { - var m = (options.modelScope || Model).get(opAndValue['pointsTo']); - if (CloudStore._collections[options.collectionName]) - var schema = CloudStore._collections[options.collectionName].schema; + let schema = null; + if (CloudStore._collections[options.collectionName]) { + schema = CloudStore._collections[options.collectionName].schema; + } - var targetClass = + const targetClass = schema && schema.properties && schema.properties[key] ? schema.properties[key].targetClass : undefined; - var type = schema && schema.properties && schema.properties[key] ? schema.properties[key].type : undefined; + const type = schema && schema.properties && schema.properties[key] ? schema.properties[key].type : undefined; if (type === 'Relation') { res[key] = { @@ -223,13 +243,13 @@ function convertFilterOp(filter, options) { className: targetClass }; } else { - if (Array.isArray(opAndValue['pointsTo'])) + if (Array.isArray(opAndValue['pointsTo'])) { res[key] = { $in: opAndValue['pointsTo'].map((v) => { return { __type: 'Pointer', objectId: v, className: targetClass }; }) }; - else + } else { res[key] = { $eq: { __type: 'Pointer', @@ -237,6 +257,7 @@ function convertFilterOp(filter, options) { className: targetClass } }; + } } } else if (opAndValue['matchesRegex'] !== undefined) { res[key] = { @@ -257,43 +278,42 @@ function convertFilterOp(filter, options) { } } }; - // Geo points + // Geo points } else if (opAndValue['nearSphere'] !== undefined) { var _v = opAndValue['nearSphere']; res[key] = { $nearSphere: { - __type: "GeoPoint", + __type: 'GeoPoint', latitude: _v.latitude, - longitude: _v.longitude, + longitude: _v.longitude }, - $maxDistanceInMiles:_v.$maxDistanceInMiles, - $maxDistanceInKilometers:_v.maxDistanceInKilometers, - $maxDistanceInRadians:_v.maxDistanceInRadians + $maxDistanceInMiles: _v.$maxDistanceInMiles, + $maxDistanceInKilometers: _v.maxDistanceInKilometers, + $maxDistanceInRadians: _v.maxDistanceInRadians }; } else if (opAndValue['withinBox'] !== undefined) { var _v = opAndValue['withinBox']; res[key] = { - $within:{ - $box: _v.map(gp => ({ - __type:"GeoPoint", - latitude:gp.latitude, - longitude:gp.longitude + $within: { + $box: _v.map((gp) => ({ + __type: 'GeoPoint', + latitude: gp.latitude, + longitude: gp.longitude })) } }; } else if (opAndValue['withinPolygon'] !== undefined) { var _v = opAndValue['withinPolygon']; res[key] = { - $geoWithin:{ - $polygon: _v.map(gp => ({ - __type:"GeoPoint", - latitude:gp.latitude, - longitude:gp.longitude + $geoWithin: { + $polygon: _v.map((gp) => ({ + __type: 'GeoPoint', + latitude: gp.latitude, + longitude: gp.longitude })) } }; } - } else { options.error('Unrecognized filter keys ' + keys.join(',')); } From a98e381f8cb8cf5c84cfaa5824ba87562c74c97a Mon Sep 17 00:00:00 2001 From: Eric Tuvesson Date: Mon, 23 Sep 2024 08:44:54 +0200 Subject: [PATCH 11/25] fix: deploy in devmode (#74) This occurred because building in dev mode does not create the source map files which it expects to copy over. --- .../editor/src/utils/compilation/build/deploy-index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/noodl-editor/src/editor/src/utils/compilation/build/deploy-index.ts b/packages/noodl-editor/src/editor/src/utils/compilation/build/deploy-index.ts index 2967581d..a573f6f6 100644 --- a/packages/noodl-editor/src/editor/src/utils/compilation/build/deploy-index.ts +++ b/packages/noodl-editor/src/editor/src/utils/compilation/build/deploy-index.ts @@ -67,6 +67,14 @@ async function _writeFileToFolder({ runtimeType }: WriteFileToFolderArgs) { const fullPath = filesystem.join(getExternalFolderPath(), runtimeType, url); + + if (!filesystem.exists(fullPath)) { + // TODO: Save this warning somewhere, usually, this is not an issue though. + // This occurred because building in dev mode does not create the source map + // files which it expects to copy over. + return; + } + let content = await filesystem.readFile(fullPath); let filename = url; From d80870e83550fc4109d33f272ae6dfb9dc03b9aa Mon Sep 17 00:00:00 2001 From: Eric Tuvesson Date: Mon, 23 Sep 2024 21:11:32 +0200 Subject: [PATCH 12/25] fix(runtime): Passing in invalid date to "Date To String" node causes node scope to fail (#75) --- .../src/nodes/std-library/datetostring.js | 55 ++++++++++++------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/packages/noodl-runtime/src/nodes/std-library/datetostring.js b/packages/noodl-runtime/src/nodes/std-library/datetostring.js index 93e0b3e6..edf50ce3 100644 --- a/packages/noodl-runtime/src/nodes/std-library/datetostring.js +++ b/packages/noodl-runtime/src/nodes/std-library/datetostring.js @@ -31,8 +31,6 @@ const DateToStringNode = { this._internal.currentInput = _value; this._format(); - this.flagOutputDirty('currentValue'); - this.sendSignalOnOutput('inputChanged'); } } }, @@ -49,30 +47,45 @@ const DateToStringNode = { type: 'signal', displayName: 'Date Changed', group: 'Signals' + }, + onError: { + type: 'signal', + displayName: 'Invalid Date', + group: 'Signals' } }, methods: { _format() { - const t = this._internal.currentInput; - const format = this._internal.formatString; - const date = ('0' + t.getDate()).slice(-2); - const month = ('0' + (t.getMonth() + 1)).slice(-2); - const monthShort = new Intl.DateTimeFormat('en-US', { month: 'short' }).format(t); - const year = t.getFullYear(); - const yearShort = year.toString().substring(2); - const hours = ('0' + t.getHours()).slice(-2); - const minutes = ('0' + t.getMinutes()).slice(-2); - const seconds = ('0' + t.getSeconds()).slice(-2); + try { + const t = this._internal.currentInput; + const format = this._internal.formatString; + const date = ('0' + t.getDate()).slice(-2); + const month = ('0' + (t.getMonth() + 1)).slice(-2); + const monthShort = new Intl.DateTimeFormat('en-US', { month: 'short' }).format(t); + const year = t.getFullYear(); + const yearShort = year.toString().substring(2); + const hours = ('0' + t.getHours()).slice(-2); + const minutes = ('0' + t.getMinutes()).slice(-2); + const seconds = ('0' + t.getSeconds()).slice(-2); + + this._internal.dateString = format + .replace(/\{date\}/g, date) + .replace(/\{month\}/g, month) + .replace(/\{monthShort\}/g, monthShort) + .replace(/\{year\}/g, year) + .replace(/\{yearShort\}/g, yearShort) + .replace(/\{hours\}/g, hours) + .replace(/\{minutes\}/g, minutes) + .replace(/\{seconds\}/g, seconds); + } catch (error) { + // Set the output to be blank, makes it easier to handle. + this._internal.dateString = ''; + this.flagOutputDirty('onError'); + } - this._internal.dateString = format - .replace(/\{date\}/g, date) - .replace(/\{month\}/g, month) - .replace(/\{monthShort\}/g, monthShort) - .replace(/\{year\}/g, year) - .replace(/\{yearShort\}/g, yearShort) - .replace(/\{hours\}/g, hours) - .replace(/\{minutes\}/g, minutes) - .replace(/\{seconds\}/g, seconds); + // Flag that the value have changed + this.flagOutputDirty('currentValue'); + this.sendSignalOnOutput('inputChanged'); } } }; From 5dbb11bac8521280827734446ab1a85adc48528d Mon Sep 17 00:00:00 2001 From: Eric Tuvesson Date: Tue, 1 Oct 2024 16:09:03 +0200 Subject: [PATCH 13/25] chore: code clean up (#76) --- packages/noodl-runtime/src/api/cloudstore.js | 45 ++++++++++----- packages/noodl-runtime/src/api/records.js | 29 +++++++--- .../nodes/std-library/data/dbmodelnode2.js | 55 +++++++++++-------- .../std-library/data/variablenode.js | 5 +- 4 files changed, 85 insertions(+), 49 deletions(-) diff --git a/packages/noodl-runtime/src/api/cloudstore.js b/packages/noodl-runtime/src/api/cloudstore.js index 346b665c..d3344964 100644 --- a/packages/noodl-runtime/src/api/cloudstore.js +++ b/packages/noodl-runtime/src/api/cloudstore.js @@ -73,8 +73,9 @@ class CloudStore { xhr.open(options.method || 'GET', this.endpoint + path, true); xhr.setRequestHeader('X-Parse-Application-Id', this.appId); - if (typeof _noodl_cloudservices !== 'undefined') + if (typeof _noodl_cloudservices !== 'undefined') { xhr.setRequestHeader('X-Parse-Master-Key', _noodl_cloudservices.masterKey); + } // Check for current users var _cu = localStorage['Parse/' + this.appId + '/currentUser']; @@ -191,13 +192,13 @@ class CloudStore { // I don't know which version the API was changed, lets just say above 4 for now. if (this.dbVersionMajor && this.dbVersionMajor > 4) { grouping._id = null; - + if (options.where) args.push('$match=' + encodeURIComponent(JSON.stringify(options.where))); args.push('$group=' + JSON.stringify(grouping)); } else { grouping.objectId = null; - + if (options.where) args.push('match=' + encodeURIComponent(JSON.stringify(options.where))); args.push('group=' + JSON.stringify(grouping)); @@ -257,11 +258,22 @@ class CloudStore { }); } + /** + * + * @param {{ + * objectId: string; + * collection: string; + * include?: string[] | string; + * success: (data: unknown) => void; + * error: (error: unknown) => void; + * }} options + */ fetch(options) { const args = []; - if (options.include) + if (options.include) { args.push('include=' + (Array.isArray(options.include) ? options.include.join(',') : options.include)); + } this._makeRequest( '/classes/' + options.collection + '/' + options.objectId + (args.length > 0 ? '?' + args.join('&') : ''), @@ -433,6 +445,8 @@ class CloudStore { * file: { * name: string; * } + * success: (data: unknown) => void; + * error: (error: unknown) => void; * }} options */ deleteFile(options) { @@ -563,21 +577,26 @@ function _deserializeJSON(data, type, modelScope) { } function _fromJSON(item, collectionName, modelScope) { - const m = (modelScope || Model).get(item.objectId); - m._class = collectionName; + const modelStore = modelScope || Model; - if (collectionName !== undefined && CloudStore._collections[collectionName] !== undefined) - var schema = CloudStore._collections[collectionName].schema; + const model = modelStore.get(item.objectId); + model._class = collectionName; - for (var key in item) { - if (key === 'objectId' || key === 'ACL') continue; + let schema = undefined; + if (collectionName !== undefined && CloudStore._collections[collectionName] !== undefined) { + schema = CloudStore._collections[collectionName].schema; + } - var _type = schema && schema.properties && schema.properties[key] ? schema.properties[key].type : undefined; + for (const key in item) { + if (key === 'objectId' || key === 'ACL') { + continue; + } - m.set(key, _deserializeJSON(item[key], _type, modelScope)); + const _type = schema && schema.properties && schema.properties[key] ? schema.properties[key].type : undefined; + model.set(key, _deserializeJSON(item[key], _type, modelScope)); } - return m; + return model; } CloudStore._fromJSON = _fromJSON; diff --git a/packages/noodl-runtime/src/api/records.js b/packages/noodl-runtime/src/api/records.js index bd4d69c0..323f8b80 100644 --- a/packages/noodl-runtime/src/api/records.js +++ b/packages/noodl-runtime/src/api/records.js @@ -12,7 +12,7 @@ function createRecordsAPI(modelScope) { return { async query(className, query, options) { - if (typeof className === "undefined") throw new Error("'className' is undefined"); + if (typeof className === 'undefined') throw new Error("'className' is undefined"); return new Promise((resolve, reject) => { cloudstore().query({ collection: className, @@ -27,9 +27,9 @@ function createRecordsAPI(modelScope) { include: options ? options.include : undefined, select: options ? options.select : undefined, count: options ? options.count : undefined, - success: (results,count) => { + success: (results, count) => { const _results = results.map((r) => cloudstore()._fromJSON(r, className)); - if(count !== undefined) resolve({results:_results,count}); + if (count !== undefined) resolve({ results: _results, count }); else resolve(_results); }, error: (err) => { @@ -40,7 +40,7 @@ function createRecordsAPI(modelScope) { }, async count(className, query) { - if (typeof className === "undefined") throw new Error("'className' is undefined"); + if (typeof className === 'undefined') throw new Error("'className' is undefined"); return new Promise((resolve, reject) => { cloudstore().count({ collection: className, @@ -62,7 +62,7 @@ function createRecordsAPI(modelScope) { }, async distinct(className, property, query) { - if (typeof className === "undefined") throw new Error("'className' is undefined"); + if (typeof className === 'undefined') throw new Error("'className' is undefined"); return new Promise((resolve, reject) => { cloudstore().distinct({ collection: className, @@ -85,7 +85,7 @@ function createRecordsAPI(modelScope) { }, async aggregate(className, group, query) { - if (typeof className === "undefined") throw new Error("'className' is undefined"); + if (typeof className === 'undefined') throw new Error("'className' is undefined"); return new Promise((resolve, reject) => { cloudstore().aggregate({ collection: className, @@ -107,20 +107,31 @@ function createRecordsAPI(modelScope) { }); }, + /** + * + * @param {string | { getId(): string; }} objectOrId + * @param {{ + * className: string; + * include?: string[] | string; + * }} options + * @returns {Promise} + */ async fetch(objectOrId, options) { if (typeof objectOrId === 'undefined') return Promise.reject(new Error("'objectOrId' is undefined.")); if (typeof objectOrId !== 'string') objectOrId = objectOrId.getId(); const className = (options ? options.className : undefined) || (modelScope || Model).get(objectOrId)._class; return new Promise((resolve, reject) => { - if (!className) return reject('No class name specified'); + if (!className) { + return reject('No class name specified'); + } cloudstore().fetch({ collection: className, objectId: objectOrId, include: options ? options.include : undefined, success: function (response) { - var record = cloudstore()._fromJSON(response, className); + const record = cloudstore()._fromJSON(response, className); resolve(record); }, error: function (err) { @@ -186,7 +197,7 @@ function createRecordsAPI(modelScope) { }, async create(className, properties, options) { - if (typeof className === "undefined") throw new Error("'className' is undefined"); + if (typeof className === 'undefined') throw new Error("'className' is undefined"); return new Promise((resolve, reject) => { cloudstore().create({ collection: className, diff --git a/packages/noodl-runtime/src/nodes/std-library/data/dbmodelnode2.js b/packages/noodl-runtime/src/nodes/std-library/data/dbmodelnode2.js index f4ecbbbd..8552b3b0 100644 --- a/packages/noodl-runtime/src/nodes/std-library/data/dbmodelnode2.js +++ b/packages/noodl-runtime/src/nodes/std-library/data/dbmodelnode2.js @@ -2,10 +2,10 @@ const { Node, EdgeTriggeredInput } = require('../../../../noodl-runtime'); -var Model = require('../../../model'); +const Model = require('../../../model'); const CloudStore = require('../../../api/cloudstore'); -var ModelNodeDefinition = { +const ModelNodeDefinition = { name: 'DbModel2', docs: 'https://docs.noodl.net/nodes/data/cloud-data/record', displayNodeName: 'Record', @@ -21,11 +21,11 @@ var ModelNodeDefinition = { } ], initialize: function () { - var internal = this._internal; + const internal = this._internal; internal.inputValues = {}; internal.relationModelIds = {}; - var _this = this; + const _this = this; this._internal.onModelChangedCallback = function (args) { if (_this.isInputConnected('fetch')) return; @@ -109,13 +109,18 @@ var ModelNodeDefinition = { displayName: 'Id', group: 'General', set: function (value) { - if (value instanceof Model) value = value.getId(); - // Can be passed as model as well - else if (typeof value === 'object') value = Model.create(value).getId(); // If this is an js object, dereference it + if (value instanceof Model) { + // Can be passed as model as well + value = value.getId(); + } else if (typeof value === 'object') { + // If this is an js object, dereference it + value = Model.create(value).getId(); + } this._internal.modelId = value; // Wait to fetch data - if (this.isInputConnected('fetch') === false) this.setModelID(value); - else { + if (this.isInputConnected('fetch') === false) { + this.setModelID(value); + } else { this.flagOutputDirty('id'); } } @@ -138,9 +143,10 @@ var ModelNodeDefinition = { this.setModel(model); }, setModel: function (model) { - if (this._internal.model) + if (this._internal.model) { // Remove old listener if existing this._internal.model.off('change', this._internal.onModelChangedCallback); + } this._internal.model = model; this.flagOutputDirty('id'); @@ -148,7 +154,9 @@ var ModelNodeDefinition = { // We have a new model, mark all outputs as dirty for (var key in model.data) { - if (this.hasOutput('prop-' + key)) this.flagOutputDirty('prop-' + key); + if (this.hasOutput('prop-' + key)) { + this.flagOutputDirty('prop-' + key); + } } this.sendSignalOnOutput('fetched'); }, @@ -184,7 +192,7 @@ var ModelNodeDefinition = { } }, scheduleFetch: function () { - var _this = this; + const _this = this; const internal = this._internal; this.scheduleOnce('Fetch', function () { @@ -199,12 +207,13 @@ var ModelNodeDefinition = { collection: internal.collectionId, objectId: internal.modelId, // Get the objectId part of the model id success: function (response) { - var model = cloudstore._fromJSON(response, internal.collectionId); + const model = cloudstore._fromJSON(response, internal.collectionId); if (internal.model !== model) { // Check if we need to change model - if (internal.model) + if (internal.model) { // Remove old listener if existing internal.model.off('change', internal.onModelChangedCallback); + } internal.model = model; model.on('change', internal.onModelChangedCallback); @@ -213,8 +222,10 @@ var ModelNodeDefinition = { delete response.objectId; - for (var key in response) { - if (_this.hasOutput('prop-' + key)) _this.flagOutputDirty('prop-' + key); + for (const key in response) { + if (_this.hasOutput('prop-' + key)) { + _this.flagOutputDirty('prop-' + key); + } } _this.sendSignalOnOutput('fetched'); @@ -226,7 +237,6 @@ var ModelNodeDefinition = { }); }, scheduleStore: function () { - var _this = this; var internal = this._internal; if (!internal.model) return; @@ -247,8 +257,6 @@ var ModelNodeDefinition = { }); }, registerInputIfNeeded: function (name) { - var _this = this; - if (this.hasInput(name)) { return; } @@ -328,8 +336,7 @@ function updatePorts(nodeId, parameters, editorConnection, graphModel) { var p = props[key]; if (ports.find((_p) => _p.name === key)) continue; - if (p.type === 'Relation') { - } else { + if (p.type !== 'Relation') { // Other schema type ports const _typeMap = { String: 'string', @@ -373,16 +380,16 @@ module.exports = { function _managePortsForNode(node) { updatePorts(node.id, node.parameters, context.editorConnection, graphModel); - node.on('parameterUpdated', function (event) { + node.on('parameterUpdated', function () { updatePorts(node.id, node.parameters, context.editorConnection, graphModel); }); - graphModel.on('metadataChanged.dbCollections', function (data) { + graphModel.on('metadataChanged.dbCollections', function () { CloudStore.invalidateCollections(); updatePorts(node.id, node.parameters, context.editorConnection, graphModel); }); - graphModel.on('metadataChanged.systemCollections', function (data) { + graphModel.on('metadataChanged.systemCollections', function () { CloudStore.invalidateCollections(); updatePorts(node.id, node.parameters, context.editorConnection, graphModel); }); diff --git a/packages/noodl-viewer-react/src/nodes-deprecated/std-library/data/variablenode.js b/packages/noodl-viewer-react/src/nodes-deprecated/std-library/data/variablenode.js index 38e58dab..2446a5b8 100644 --- a/packages/noodl-viewer-react/src/nodes-deprecated/std-library/data/variablenode.js +++ b/packages/noodl-viewer-react/src/nodes-deprecated/std-library/data/variablenode.js @@ -1,10 +1,9 @@ 'use strict'; const { Node } = require('@noodl/runtime'); +const Model = require('@noodl/runtime/src/model'); -var Model = require('@noodl/runtime/src/model'); - -var VariableNodeDefinition = { +const VariableNodeDefinition = { name: 'Variable', docs: 'https://docs.noodl.net/nodes/data/variable', category: 'Data', From fff03c05bf5220cec12f4682ce35facffb7b56d0 Mon Sep 17 00:00:00 2001 From: Eric Tuvesson Date: Thu, 3 Oct 2024 11:24:57 +0200 Subject: [PATCH 14/25] fix(runtime): Close Popup node with no actions causing error (#78) TypeError: Cannot read properties of undefined (reading 'replace') at Object.onClosePopup (navigation.js:10:1) at EventEmitter. (nodecontext.js:491:1) at Object.onceWrapper (events.js:242:1) at EventEmitter.emit (events.js:153:1) at NoodlRuntime._doUpdate (noodl-runtime.js:338:1) --- .../src/nodes/navigation/closepopup.js | 3 ++- .../src/nodes/navigation/showpopup.js | 17 +++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/noodl-viewer-react/src/nodes/navigation/closepopup.js b/packages/noodl-viewer-react/src/nodes/navigation/closepopup.js index db038400..961e9a57 100644 --- a/packages/noodl-viewer-react/src/nodes/navigation/closepopup.js +++ b/packages/noodl-viewer-react/src/nodes/navigation/closepopup.js @@ -51,8 +51,9 @@ const ClosePopupNode = { } }, close: function () { - if (this._internal.closeCallback) + if (this._internal.closeCallback) { this._internal.closeCallback(this._internal.closeAction, this._internal.resultValues); + } }, closeActionTriggered: function (name) { this._internal.closeAction = name; diff --git a/packages/noodl-viewer-react/src/nodes/navigation/showpopup.js b/packages/noodl-viewer-react/src/nodes/navigation/showpopup.js index e589bac5..d3076a81 100644 --- a/packages/noodl-viewer-react/src/nodes/navigation/showpopup.js +++ b/packages/noodl-viewer-react/src/nodes/navigation/showpopup.js @@ -53,15 +53,24 @@ const ShowPopupNode = { this.context.showPopup(this._internal.target, this._internal.popupParams, { senderNode: this.nodeScope.componentOwner, + /** + * @param {string | undefined} action + * @param {*} results + */ onClosePopup: (action, results) => { this._internal.closeResults = results; - for (var key in results) { - if (this.hasOutput('closeResult-' + key)) this.flagOutputDirty('closeResult-' + key); + for (const key in results) { + if (this.hasOutput('closeResult-' + key)) { + this.flagOutputDirty('closeResult-' + key); + } } - if (!action) this.sendSignalOnOutput('Closed'); - else this.sendSignalOnOutput(action); + if (!action) { + this.sendSignalOnOutput('Closed'); + } else { + this.sendSignalOnOutput(action); + } } }); }, From 016837f466aeb46cd6e2e64c6b8e401896fc838f Mon Sep 17 00:00:00 2001 From: Eric Tuvesson Date: Tue, 12 Nov 2024 15:50:49 +0100 Subject: [PATCH 15/25] feat(runtime): Add "keys" and "excludeKeys" to fetch record api (#79) Adding support for some more options when fetching a record. https://github.com/parse-community/parse-server/blob/91f9aca25bc6212ae27aac7af328ee1f19f058c8/src/Routers/ClassesRouter.js#L60-L68 --- packages/noodl-runtime/src/api/cloudstore.js | 12 ++++++++++++ packages/noodl-runtime/src/api/records.js | 6 +++++- .../static/viewer/global.d.ts.keep | 3 +++ .../static/viewer/global.d.ts.keep | 3 +++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/noodl-runtime/src/api/cloudstore.js b/packages/noodl-runtime/src/api/cloudstore.js index d3344964..4c586d54 100644 --- a/packages/noodl-runtime/src/api/cloudstore.js +++ b/packages/noodl-runtime/src/api/cloudstore.js @@ -263,7 +263,9 @@ class CloudStore { * @param {{ * objectId: string; * collection: string; + * keys?: string[] | string; * include?: string[] | string; + * excludeKeys?: string[] | string; * success: (data: unknown) => void; * error: (error: unknown) => void; * }} options @@ -275,6 +277,16 @@ class CloudStore { args.push('include=' + (Array.isArray(options.include) ? options.include.join(',') : options.include)); } + if (options.keys) { + args.push('keys=' + (Array.isArray(options.keys) ? options.keys.join(',') : options.keys)); + } + + if (options.excludeKeys) { + args.push( + 'excludeKeys=' + (Array.isArray(options.excludeKeys) ? options.excludeKeys.join(',') : options.excludeKeys) + ); + } + this._makeRequest( '/classes/' + options.collection + '/' + options.objectId + (args.length > 0 ? '?' + args.join('&') : ''), { diff --git a/packages/noodl-runtime/src/api/records.js b/packages/noodl-runtime/src/api/records.js index 323f8b80..a9730a30 100644 --- a/packages/noodl-runtime/src/api/records.js +++ b/packages/noodl-runtime/src/api/records.js @@ -112,7 +112,9 @@ function createRecordsAPI(modelScope) { * @param {string | { getId(): string; }} objectOrId * @param {{ * className: string; + * keys?: string[] | string; * include?: string[] | string; + * excludeKeys?: string[] | string; * }} options * @returns {Promise} */ @@ -129,7 +131,9 @@ function createRecordsAPI(modelScope) { cloudstore().fetch({ collection: className, objectId: objectOrId, - include: options ? options.include : undefined, + keys: options?.keys, + include: options?.include, + excludeKeys: options?.excludeKeys, success: function (response) { const record = cloudstore()._fromJSON(response, className); resolve(record); diff --git a/packages/noodl-viewer-cloud/static/viewer/global.d.ts.keep b/packages/noodl-viewer-cloud/static/viewer/global.d.ts.keep index 1a8c715b..39a6fa60 100644 --- a/packages/noodl-viewer-cloud/static/viewer/global.d.ts.keep +++ b/packages/noodl-viewer-cloud/static/viewer/global.d.ts.keep @@ -227,6 +227,9 @@ declare namespace Noodl { objectOrId: string | { getId(): string; }, options?: { className?: RecordClassName; + keys?: string[] | string; + include?: string[] | string; + excludeKeys?: string[] | string; } ): Promise; diff --git a/packages/noodl-viewer-react/static/viewer/global.d.ts.keep b/packages/noodl-viewer-react/static/viewer/global.d.ts.keep index 5461b824..613e6069 100644 --- a/packages/noodl-viewer-react/static/viewer/global.d.ts.keep +++ b/packages/noodl-viewer-react/static/viewer/global.d.ts.keep @@ -274,6 +274,9 @@ declare namespace Noodl { objectOrId: string | { getId(): string; }, options?: { className?: RecordClassName; + keys?: string[] | string; + include?: string[] | string; + excludeKeys?: string[] | string; } ): Promise; From e25155556f4fbca85fc0b2025452a91914bf9528 Mon Sep 17 00:00:00 2001 From: Eric Tuvesson Date: Wed, 13 Nov 2024 13:35:03 +0100 Subject: [PATCH 16/25] feat(runtime): 'Set Variable' node, add editor getInspectInfo (#80) It is very nice to be able to inspect the value inside the variable even via the "Set Variable" node. Most of the time I see an additional variable node placed above the "Set Variable" node to just inspect the current value. --- .../src/nodes/std-library/data/setvariablenode.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/noodl-viewer-react/src/nodes/std-library/data/setvariablenode.js b/packages/noodl-viewer-react/src/nodes/std-library/data/setvariablenode.js index b348ca0d..553bd17d 100644 --- a/packages/noodl-viewer-react/src/nodes/std-library/data/setvariablenode.js +++ b/packages/noodl-viewer-react/src/nodes/std-library/data/setvariablenode.js @@ -1,11 +1,9 @@ 'use strict'; -const { Node } = require('@noodl/runtime'); - const Model = require('@noodl/runtime/src/model'); const Collection = require('@noodl/runtime/src/collection'); -var SetVariableNodeDefinition = { +const SetVariableNodeDefinition = { name: 'Set Variable', docs: 'https://docs.noodl.net/nodes/data/variable/set-variable', category: 'Data', @@ -16,6 +14,13 @@ var SetVariableNodeDefinition = { internal.variablesModel = Model.get('--ndl--global-variables'); }, + getInspectInfo() { + if (this._internal.name) { + return this._internal.variablesModel.get(this._internal.name); + } + + return '[No value set]'; + }, outputs: { done: { type: 'signal', From 14786b21446d03f83f5ca60c784d742f8efb8096 Mon Sep 17 00:00:00 2001 From: Eric Tuvesson Date: Mon, 18 Nov 2024 17:01:49 +0100 Subject: [PATCH 17/25] chore(runtime): Add some JSDocs (#81) --- packages/noodl-runtime/src/collection.js | 100 +++++++++--------- packages/noodl-runtime/src/model.js | 47 ++++++-- .../src/nodes/navigation/closepopup.js | 9 +- .../nodes/std-library/data/setvariablenode.js | 21 ++-- 4 files changed, 105 insertions(+), 72 deletions(-) diff --git a/packages/noodl-runtime/src/collection.js b/packages/noodl-runtime/src/collection.js index 5aa09ff4..1c99c18c 100644 --- a/packages/noodl-runtime/src/collection.js +++ b/packages/noodl-runtime/src/collection.js @@ -1,6 +1,6 @@ -"use strict"; +'use strict'; -var Model = require("./model"); +var Model = require('./model'); // Get and set proxy /*const proxies = {} @@ -221,48 +221,48 @@ Collection.prototype.toJSON = function() { }*/ // ---- -Object.defineProperty(Array.prototype, "items", { +Object.defineProperty(Array.prototype, 'items', { enumerable: false, get() { return this; }, set(data) { this.set(data); - }, + } }); -Object.defineProperty(Array.prototype, "each", { +Object.defineProperty(Array.prototype, 'each', { enumerable: false, writable: false, - value: Array.prototype.forEach, + value: Array.prototype.forEach }); -Object.defineProperty(Array.prototype, "size", { +Object.defineProperty(Array.prototype, 'size', { enumerable: false, writable: false, value: function () { return this.length; - }, + } }); -Object.defineProperty(Array.prototype, "get", { +Object.defineProperty(Array.prototype, 'get', { enumerable: false, writable: false, value: function (index) { return this[index]; - }, + } }); -Object.defineProperty(Array.prototype, "getId", { +Object.defineProperty(Array.prototype, 'getId', { enumerable: false, writable: false, value: function () { return this._id; - }, + } }); -Object.defineProperty(Array.prototype, "id", { +Object.defineProperty(Array.prototype, 'id', { enumerable: false, get() { return this.getId(); - }, + } }); -Object.defineProperty(Array.prototype, "set", { +Object.defineProperty(Array.prototype, 'set', { enumerable: false, writable: false, value: function (src) { @@ -323,10 +323,10 @@ Object.defineProperty(Array.prototype, "set", { for (i = aItems.length; i < bItems.length; i++) { this.add(bItems[i]); } - }, + } }); -Object.defineProperty(Array.prototype, "notify", { +Object.defineProperty(Array.prototype, 'notify', { enumerable: false, writable: false, value: async function (event, args) { @@ -337,80 +337,80 @@ Object.defineProperty(Array.prototype, "notify", { for (var i = 0; i < l.length; i++) { await l[i](args); } - }, + } }); -Object.defineProperty(Array.prototype, "contains", { +Object.defineProperty(Array.prototype, 'contains', { enumerable: false, writable: false, value: function (item) { return this.indexOf(item) !== -1; - }, + } }); -Object.defineProperty(Array.prototype, "add", { +Object.defineProperty(Array.prototype, 'add', { enumerable: false, writable: false, value: async function (item) { if (this.contains(item)) return; // Already contains item this.items.push(item); - await this.notify("add", { item: item, index: this.items.length - 1 }); - await this.notify("change"); - await item.notify("add", { collection: this }); - }, + await this.notify('add', { item: item, index: this.items.length - 1 }); + await this.notify('change'); + await item.notify('add', { collection: this }); + } }); -Object.defineProperty(Array.prototype, "remove", { +Object.defineProperty(Array.prototype, 'remove', { enumerable: false, writable: false, value: function (item) { var idx = this.items.indexOf(item); if (idx !== -1) this.removeAtIndex(idx); - }, + } }); -Object.defineProperty(Array.prototype, "addAtIndex", { +Object.defineProperty(Array.prototype, 'addAtIndex', { enumerable: false, writable: false, value: async function (item, index) { if (this.contains(item)) return; // Already contains item this.items.splice(index, 0, item); - await this.notify("add", { item: item, index: index }); - await this.notify("change"); - await item.notify("add", { collection: this, index: index }); - }, + await this.notify('add', { item: item, index: index }); + await this.notify('change'); + await item.notify('add', { collection: this, index: index }); + } }); -Object.defineProperty(Array.prototype, "removeAtIndex", { +Object.defineProperty(Array.prototype, 'removeAtIndex', { enumerable: false, writable: false, value: async function (idx) { var item = this.items[idx]; this.items.splice(idx, 1); - await this.notify("remove", { item: item, index: idx }); - await this.notify("change"); - await item.notify("remove", { collection: this }); - }, + await this.notify('remove', { item: item, index: idx }); + await this.notify('change'); + await item.notify('remove', { collection: this }); + } }); -Object.defineProperty(Array.prototype, "on", { +Object.defineProperty(Array.prototype, 'on', { enumerable: false, writable: false, value: function (event, listener) { if (!this._listeners) - Object.defineProperty(this, "_listeners", { + Object.defineProperty(this, '_listeners', { enumerable: false, writable: false, - value: {}, + value: {} }); if (!this._listeners[event]) this._listeners[event] = []; this._listeners[event].push(listener); - }, + } }); -Object.defineProperty(Array.prototype, "off", { +Object.defineProperty(Array.prototype, 'off', { enumerable: false, writable: false, value: function (event, listener) { @@ -418,20 +418,20 @@ Object.defineProperty(Array.prototype, "off", { if (!this._listeners[event]) return; var idx = this._listeners[event].indexOf(listener); if (idx !== -1) this._listeners[event].splice(idx, 1); - }, + } }); class Collection extends Array {} -var collections = (Collection._collections = {}); +const collections = (Collection._collections = {}); Collection.create = function (items) { const name = Model.guid(); collections[name] = new Collection(); - Object.defineProperty(collections[name], "_id", { + Object.defineProperty(collections[name], '_id', { enumerable: false, writable: false, - value: name, + value: name }); if (items) { collections[name].set(items); @@ -439,14 +439,18 @@ Collection.create = function (items) { return collections[name]; }; +/** + * @param {string} name + * @returns {Collection} + */ Collection.get = function (name) { if (name === undefined) name = Model.guid(); if (!collections[name]) { collections[name] = new Collection(); - Object.defineProperty(collections[name], "_id", { + Object.defineProperty(collections[name], '_id', { enumerable: false, writable: false, - value: name, + value: name }); } diff --git a/packages/noodl-runtime/src/model.js b/packages/noodl-runtime/src/model.js index 850e6d92..877aa4c7 100644 --- a/packages/noodl-runtime/src/model.js +++ b/packages/noodl-runtime/src/model.js @@ -35,6 +35,11 @@ const _modelProxyHandler = { } }; +/** + * + * @param {*} id + * @returns {Model} + */ Model.get = function (id) { if (id === undefined) id = Model.guid(); if (!models[id]) { @@ -122,13 +127,22 @@ Model.prototype.fill = function (value = null) { } }; +/** + * @param {string} name + * @param {unknown} value + * @param {{ + * resolve?: boolean; + * forceChange?: boolean; + * silent?: boolean; + * }} args + */ Model.prototype.set = function (name, value, args) { if (args && args.resolve && name.indexOf('.') !== -1) { // We should resolve path references - var path = name.split('.'); - var model = this; - for (var i = 0; i < path.length - 1; i++) { - var v = model.get(path[i]); + const path = name.split('.'); + let model = this; + for (let i = 0; i < path.length - 1; i++) { + const v = model.get(path[i]); if (Model.instanceOf(v)) model = v; else return; // Path resolve failed } @@ -138,24 +152,35 @@ Model.prototype.set = function (name, value, args) { const forceChange = args && args.forceChange; - var oldValue = this.data[name]; + const oldValue = this.data[name]; this.data[name] = value; - (forceChange || oldValue !== value) && - (!args || !args.silent) && + + if ((forceChange || oldValue !== value) && (!args || !args.silent)) { this.notify('change', { name: name, value: value, old: oldValue }); + } }; +/** + * @returns {string} + */ Model.prototype.getId = function () { return this.id; }; +/** + * @param {string} name + * @param {{ + * resolve?: boolean; + * }} args + * @returns {unknown} + */ Model.prototype.get = function (name, args) { if (args && args.resolve && name.indexOf('.') !== -1) { // We should resolve path references - var path = name.split('.'); - var model = this; - for (var i = 0; i < path.length - 1; i++) { - var v = model.get(path[i]); + const path = name.split('.'); + let model = this; + for (let i = 0; i < path.length - 1; i++) { + const v = model.get(path[i]); if (Model.instanceOf(v)) model = v; else return; // Path resolve failed } diff --git a/packages/noodl-viewer-react/src/nodes/navigation/closepopup.js b/packages/noodl-viewer-react/src/nodes/navigation/closepopup.js index 961e9a57..15caa396 100644 --- a/packages/noodl-viewer-react/src/nodes/navigation/closepopup.js +++ b/packages/noodl-viewer-react/src/nodes/navigation/closepopup.js @@ -40,8 +40,8 @@ const ClosePopupNode = { this._internal.closeCallback = cb; }, scheduleClose: function () { - var _this = this; - var internal = this._internal; + const _this = this; + const internal = this._internal; if (!internal.hasScheduledClose) { internal.hasScheduledClose = true; this.scheduleAfterInputsHaveUpdated(function () { @@ -113,9 +113,8 @@ module.exports = { var closeActions = node.parameters['closeActions']; if (closeActions) { closeActions = closeActions ? closeActions.split(',') : undefined; - for (var i in closeActions) { - var p = closeActions[i]; - + for (const i in closeActions) { + const p = closeActions[i]; ports.push({ type: 'signal', plug: 'input', diff --git a/packages/noodl-viewer-react/src/nodes/std-library/data/setvariablenode.js b/packages/noodl-viewer-react/src/nodes/std-library/data/setvariablenode.js index 553bd17d..f1f299b7 100644 --- a/packages/noodl-viewer-react/src/nodes/std-library/data/setvariablenode.js +++ b/packages/noodl-viewer-react/src/nodes/std-library/data/setvariablenode.js @@ -10,8 +10,7 @@ const SetVariableNodeDefinition = { usePortAsLabel: 'name', color: 'data', initialize: function () { - var internal = this._internal; - + const internal = this._internal; internal.variablesModel = Model.get('--ndl--global-variables'); }, getInspectInfo() { @@ -79,17 +78,22 @@ const SetVariableNodeDefinition = { if (this.hasScheduledStore) return; this.hasScheduledStore = true; - var internal = this._internal; + const internal = this._internal; this.scheduleAfterInputsHaveUpdated(function () { this.hasScheduledStore = false; - var value = internal.setWith === 'emptyString' ? '' : internal.value; + let value = internal.setWith === 'emptyString' ? '' : internal.value; + + // Can set arrays with "id" or array + if (internal.setWith === 'object' && typeof value === 'string') value = Model.get(value); + + // Can set arrays with "id" or array + if (internal.setWith === 'array' && typeof value === 'string') value = Collection.get(value); - if (internal.setWith === 'object' && typeof value === 'string') value = Model.get(value); // Can set arrays with "id" or array - if (internal.setWith === 'array' && typeof value === 'string') value = Collection.get(value); // Can set arrays with "id" or array if (internal.setWith === 'boolean') value = !!value; - //use forceChange to always trigger Variable nodes to send the value on their output, even if it's the same value twice + // use forceChange to always trigger Variable nodes to send the value on + // their output, even if it's the same value twice internal.variablesModel.set(internal.name, value, { forceChange: true }); @@ -101,10 +105,11 @@ const SetVariableNodeDefinition = { return; } - if (name === 'value') + if (name === 'value') { this.registerInput(name, { set: this.setValue.bind(this) }); + } } } }; From 6205d084518ac2db6bc33dfb2d2c128d6f298075 Mon Sep 17 00:00:00 2001 From: Eric Tuvesson Date: Mon, 18 Nov 2024 17:06:10 +0100 Subject: [PATCH 18/25] fix: showPopup replace error (#82) Error: "TypeError: Cannot read properties of undefined (reading 'replace')" --- packages/noodl-runtime/src/nodecontext.js | 11 ++++++++++- packages/noodl-viewer-react/src/api/navigation.js | 8 +++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/noodl-runtime/src/nodecontext.js b/packages/noodl-runtime/src/nodecontext.js index 32e26dc0..9060e9a6 100644 --- a/packages/noodl-runtime/src/nodecontext.js +++ b/packages/noodl-runtime/src/nodecontext.js @@ -230,7 +230,7 @@ NodeContext.prototype.deregisterComponentModel = function (componentModel) { NodeContext.prototype.fetchComponentBundle = async function (name) { const fetchBundle = async (name) => { - let baseUrl = Noodl.Env["BaseUrl"] || '/'; + let baseUrl = Noodl.Env['BaseUrl'] || '/'; let bundleUrl = `${baseUrl}noodl_bundles/${name}.json`; const response = await fetch(bundleUrl); @@ -455,6 +455,15 @@ NodeContext.prototype.setPopupCallbacks = function ({ onShow, onClose }) { this.onClosePopup = onClose; }; +/** + * @param {string} popupComponent + * @param {Record} params + * @param {{ + * senderNode?: unknown; + * onClosePopup?: (action?: string, results: object) => void; + * }} args + * @returns + */ NodeContext.prototype.showPopup = async function (popupComponent, params, args) { if (!this.onShowPopup) return; diff --git a/packages/noodl-viewer-react/src/api/navigation.js b/packages/noodl-viewer-react/src/api/navigation.js index 7612aee5..06463046 100644 --- a/packages/noodl-viewer-react/src/api/navigation.js +++ b/packages/noodl-viewer-react/src/api/navigation.js @@ -2,12 +2,18 @@ const { RouterHandler } = require('../nodes/navigation/router-handler'); const NoodlRuntime = require('@noodl/runtime'); const navigation = { + /** + * This is set by "packages/noodl-viewer-react/src/noodl-js-api.js" + * @type {NoodlRuntime} + */ + _noodlRuntime: undefined, + async showPopup(componentPath, params) { return new Promise((resolve) => { navigation._noodlRuntime.context.showPopup(componentPath, params, { onClosePopup: (action, results) => { resolve({ - action: action.replace('closeAction-', ''), + action: action?.replace('closeAction-', ''), parameters: results }); } From 95db9f6528c4f12719df5606c2a5cbe5eaf49531 Mon Sep 17 00:00:00 2001 From: Eric Tuvesson Date: Wed, 8 Jan 2025 21:58:37 +0100 Subject: [PATCH 19/25] chore: Add some TS types to WarningsModel (#86) --- .../src/editor/src/ViewerConnection.ts | 6 +- .../NodeTypeAdapters/RouterNavigateAdapter.ts | 4 +- .../src/editor/src/models/VariantModel.ts | 4 +- .../src/editor/src/models/warningsmodel.ts | 112 ++++++++++++------ 4 files changed, 82 insertions(+), 44 deletions(-) diff --git a/packages/noodl-editor/src/editor/src/ViewerConnection.ts b/packages/noodl-editor/src/editor/src/ViewerConnection.ts index aec9a0c4..e4f88e3d 100644 --- a/packages/noodl-editor/src/editor/src/ViewerConnection.ts +++ b/packages/noodl-editor/src/editor/src/ViewerConnection.ts @@ -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) }; diff --git a/packages/noodl-editor/src/editor/src/models/NodeTypeAdapters/RouterNavigateAdapter.ts b/packages/noodl-editor/src/editor/src/models/NodeTypeAdapters/RouterNavigateAdapter.ts index a0544ceb..7c9509d8 100644 --- a/packages/noodl-editor/src/editor/src/models/NodeTypeAdapters/RouterNavigateAdapter.ts +++ b/packages/noodl-editor/src/editor/src/models/NodeTypeAdapters/RouterNavigateAdapter.ts @@ -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", diff --git a/packages/noodl-editor/src/editor/src/models/VariantModel.ts b/packages/noodl-editor/src/editor/src/models/VariantModel.ts index a8c770a0..50eb190e 100644 --- a/packages/noodl-editor/src/editor/src/models/VariantModel.ts +++ b/packages/noodl-editor/src/editor/src/models/VariantModel.ts @@ -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 }, diff --git a/packages/noodl-editor/src/editor/src/models/warningsmodel.ts b/packages/noodl-editor/src/editor/src/models/warningsmodel.ts index 12225c79..7f77afe3 100644 --- a/packages/noodl-editor/src/editor/src/models/warningsmodel.ts +++ b/packages/noodl-editor/src/editor/src/models/warningsmodel.ts @@ -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; From 680bd58442728e6c59acf714ba04a96727712fa7 Mon Sep 17 00:00:00 2001 From: Eric Tuvesson Date: Wed, 8 Jan 2025 22:02:10 +0100 Subject: [PATCH 20/25] 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. --- .../src/views/panels/componentspanel/ComponentsPanel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/noodl-editor/src/editor/src/views/panels/componentspanel/ComponentsPanel.ts b/packages/noodl-editor/src/editor/src/views/panels/componentspanel/ComponentsPanel.ts index a358c6bd..47c5a560 100644 --- a/packages/noodl-editor/src/editor/src/views/panels/componentspanel/ComponentsPanel.ts +++ b/packages/noodl-editor/src/editor/src/views/panels/componentspanel/ComponentsPanel.ts @@ -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 From 0e13a8b03333f1e41565313fa38ca615ed9799b0 Mon Sep 17 00:00:00 2001 From: Eric Tuvesson Date: Thu, 16 Jan 2025 14:40:44 +0100 Subject: [PATCH 21/25] fix: Keep object id when fetching a Object column from the Cloud Service (#87) --- packages/noodl-runtime/src/api/cloudstore.js | 86 ++++++++++++++------ 1 file changed, 63 insertions(+), 23 deletions(-) diff --git a/packages/noodl-runtime/src/api/cloudstore.js b/packages/noodl-runtime/src/api/cloudstore.js index 4c586d54..1802a668 100644 --- a/packages/noodl-runtime/src/api/cloudstore.js +++ b/packages/noodl-runtime/src/api/cloudstore.js @@ -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; From 2cfd36147a8d3b807ae9f9bcab18508e02848f85 Mon Sep 17 00:00:00 2001 From: Eric Tuvesson Date: Tue, 25 Feb 2025 13:55:53 +0100 Subject: [PATCH 22/25] chore(editor): Convert Universal Search to TypeScript (#89) --- .../models/nodegraphmodel/NodeGraphNode.ts | 2 +- ...niversal-search.js => universal-search.ts} | 57 ++++++++++--------- 2 files changed, 31 insertions(+), 28 deletions(-) rename packages/noodl-editor/src/editor/src/utils/{universal-search.js => universal-search.ts} (70%) diff --git a/packages/noodl-editor/src/editor/src/models/nodegraphmodel/NodeGraphNode.ts b/packages/noodl-editor/src/editor/src/models/nodegraphmodel/NodeGraphNode.ts index 316254b2..f13a466c 100644 --- a/packages/noodl-editor/src/editor/src/models/nodegraphmodel/NodeGraphNode.ts +++ b/packages/noodl-editor/src/editor/src/models/nodegraphmodel/NodeGraphNode.ts @@ -72,7 +72,7 @@ export class NodeGraphNode extends Model { metadata?: Record; private _variant: TSFixme; - private _label: TSFixme; + _label: TSFixme; private _type: TSFixme; private _ports: TSFixme; diff --git a/packages/noodl-editor/src/editor/src/utils/universal-search.js b/packages/noodl-editor/src/editor/src/utils/universal-search.ts similarity index 70% rename from packages/noodl-editor/src/editor/src/utils/universal-search.js rename to packages/noodl-editor/src/editor/src/utils/universal-search.ts index 1f97c1fc..7d16883e 100644 --- a/packages/noodl-editor/src/editor/src/utils/universal-search.js +++ b/packages/noodl-editor/src/editor/src/utils/universal-search.ts @@ -1,13 +1,16 @@ -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'; + +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 = []; + let matchLabel = null; + let i = 0; if (node._label !== undefined && matchStrings(node._label, searchTerms)) { matchLabel = node.label; @@ -16,7 +19,7 @@ function searchInNodeRecursive(node, searchTerms, component) { } 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 +28,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 +54,9 @@ function searchInNodeRecursive(node, searchTerms, component) { } if (matchLabel === null) { - var ports = node.dynamicports; + const ports = node.dynamicports; for (i = 0; i < ports.length; ++i) { - var port = ports[i]; + const port = ports[i]; if (matchStrings(port.name, searchTerms)) { matchLabel = node.label + ' : ' + port.name; break; @@ -62,9 +65,9 @@ function searchInNodeRecursive(node, searchTerms, component) { } if (matchLabel === null) { - var ports = node.ports; + const ports = node.ports; for (i = 0; i < ports.length; ++i) { - var port = ports[i]; + const port = ports[i]; if (matchStrings(port.name, searchTerms)) { matchLabel = node.label + ' : ' + port.name; break; @@ -84,16 +87,16 @@ function searchInNodeRecursive(node, searchTerms, component) { } for (i = 0; i < node.children.length; ++i) { - var child = node.children[i]; - var childResults = searchInNodeRecursive(child, searchTerms, component); + 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 = []; if (matchStrings(component.displayName, searchTerms)) { results.push({ componentTarget: component, @@ -102,14 +105,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 +135,17 @@ function searchInComponent(component, searchTerms) { } } -export function performSearch(searchTerms) { - var results = []; - var root = ProjectModel.instance.getRootNode(); +export function performSearch(searchTerms: string) { + const results = []; + 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) { From fc89bd9ce1ea11c64808a5bb5c340d63da6318a6 Mon Sep 17 00:00:00 2001 From: Eric Tuvesson Date: Tue, 25 Feb 2025 15:39:15 +0100 Subject: [PATCH 23/25] feat(editor): Search only show ID searches when 1:1 match (#90) This removes a lot of the clutter from the search results. --- .../src/editor/src/utils/universal-search.ts | 25 +++++++++++------- .../panels/search-panel/search-panel.tsx | 26 +++++-------------- 2 files changed, 22 insertions(+), 29 deletions(-) diff --git a/packages/noodl-editor/src/editor/src/utils/universal-search.ts b/packages/noodl-editor/src/editor/src/utils/universal-search.ts index 7d16883e..6d3cf8c4 100644 --- a/packages/noodl-editor/src/editor/src/utils/universal-search.ts +++ b/packages/noodl-editor/src/editor/src/utils/universal-search.ts @@ -3,18 +3,25 @@ import type { NodeGraphNode } from '@noodl-models/nodegraphmodel'; 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: NodeGraphNode, searchTerms: string, component: ComponentModel) { - let results = []; - let matchLabel = null; - let i = 0; + 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; @@ -55,7 +62,7 @@ function searchInNodeRecursive(node: NodeGraphNode, searchTerms: string, compone if (matchLabel === null) { const ports = node.dynamicports; - for (i = 0; i < ports.length; ++i) { + for (let i = 0; i < ports.length; ++i) { const port = ports[i]; if (matchStrings(port.name, searchTerms)) { matchLabel = node.label + ' : ' + port.name; @@ -66,7 +73,7 @@ function searchInNodeRecursive(node: NodeGraphNode, searchTerms: string, compone if (matchLabel === null) { const ports = node.ports; - for (i = 0; i < ports.length; ++i) { + for (let i = 0; i < ports.length; ++i) { const port = ports[i]; if (matchStrings(port.name, searchTerms)) { matchLabel = node.label + ' : ' + port.name; @@ -86,7 +93,7 @@ function searchInNodeRecursive(node: NodeGraphNode, searchTerms: string, compone }); } - for (i = 0; i < node.children.length; ++i) { + for (let i = 0; i < node.children.length; ++i) { const child = node.children[i]; const childResults = searchInNodeRecursive(child, searchTerms, component); results = results.concat(childResults); @@ -96,7 +103,7 @@ function searchInNodeRecursive(node: NodeGraphNode, searchTerms: string, compone } function searchInComponent(component: ComponentModel, searchTerms: string) { - let results = []; + let results: SearchResult[] = []; if (matchStrings(component.displayName, searchTerms)) { results.push({ componentTarget: component, @@ -136,7 +143,7 @@ function searchInComponent(component: ComponentModel, searchTerms: string) { } export function performSearch(searchTerms: string) { - const results = []; + const results: ReturnType[] = []; const root = ProjectModel.instance.getRootNode(); if (root === undefined) return; diff --git a/packages/noodl-editor/src/editor/src/views/panels/search-panel/search-panel.tsx b/packages/noodl-editor/src/editor/src/views/panels/search-panel/search-panel.tsx index e9d3298d..1628aacc 100644 --- a/packages/noodl-editor/src/editor/src/views/panels/search-panel/search-panel.tsx +++ b/packages/noodl-editor/src/editor/src/views/panels/search-panel/search-panel.tsx @@ -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>([]); const inputRef = useRef(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[0]; + onSearchItemClicked: (item: SearchResult) => void; }; function SearchItem({ component, onSearchItemClicked }: SearchItemProps) { @@ -130,11 +116,11 @@ function SearchItem({ component, onSearchItemClicked }: SearchItemProps) {
{component.results.map((result, index) => (
onSearchItemClicked(result)} >