From d85dce8d024d95735a9007207835c5b66a943fd2 Mon Sep 17 00:00:00 2001 From: Eric Tuvesson Date: Thu, 5 Sep 2024 13:19:17 +0200 Subject: [PATCH] refactor(editor): useNodeReferences to React context (#64) --- .../NodeReferencesContext.tsx | 133 ++++++++++++++++++ .../contexts/NodeReferencesContext/index.ts | 1 + .../src/pages/EditorPage/EditorPage.tsx | 47 ++++--- .../NodeReferencesPanel.tsx | 116 ++------------- 4 files changed, 168 insertions(+), 129 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 0000000..fff24a1 --- /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 0000000..3fc21d4 --- /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 0c04092..68e6807 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} - /> + + + + + {isLoading ? ( + + ) : ( + <> + } + second={{Boolean(Document) && }} + sizeMin={200} + size={frameDividerSize} + horizontal + onSizeChanged={setFrameDividerSize} + /> - {Boolean(lesson) && } - - )} - - - + {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 5d24fb6..3669c0e 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; }