refactor(editor): useNodeReferences to React context (#64)

This commit is contained in:
Eric Tuvesson
2024-09-05 13:19:17 +02:00
committed by GitHub
parent 89ed2d602f
commit d85dce8d02
4 changed files with 168 additions and 129 deletions

View File

@@ -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<NodeReferencesContext>({
nodeReferences: [],
});
export interface NodeReferencesContextProps {
children: Slot;
}
export function NodeReferencesContextProvider({ children }: NodeReferencesContextProps) {
const [group] = useState({});
const [nodeReferences, setNodeReferences] = useState<NodeReference[]>([]);
useEffect(() => {
function updateIndex() {
const types: { [key: string]: NodeReference['type'] } = {};
const references = new Map<string, NodeReference['referenaces']>();
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 (
<NodeReferencesContext.Provider
value={{
nodeReferences,
}}
>
{children}
</NodeReferencesContext.Provider>
);
}
export function useNodeReferencesContext() {
const context = useContext(NodeReferencesContext);
if (context === undefined) {
throw new Error('useNodeReferencesContext must be a child of NodeReferencesContextProvider');
}
return context;
}

View File

@@ -0,0 +1 @@
export * from './NodeReferencesContext';

View File

@@ -1,4 +1,6 @@
import { NodeGraphContextProvider } from '@noodl-contexts/NodeGraphContext/NodeGraphContext'; 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 { ProjectDesignTokenContextProvider } from '@noodl-contexts/ProjectDesignTokenContext';
import { useKeyboardCommands } from '@noodl-hooks/useKeyboardCommands'; import { useKeyboardCommands } from '@noodl-hooks/useKeyboardCommands';
import { useModel } from '@noodl-hooks/useModel'; import { useModel } from '@noodl-hooks/useModel';
@@ -43,7 +45,6 @@ import { BaseWindow } from '../../views/windows/BaseWindow';
import { whatsnewRender } from '../../whats-new'; import { whatsnewRender } from '../../whats-new';
import { IRouteProps } from '../AppRoute'; import { IRouteProps } from '../AppRoute';
import { useSetupSettings } from './useSetupSettings'; import { useSetupSettings } from './useSetupSettings';
import { PluginContextProvider } from '@noodl-contexts/PluginContext';
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const ImportOverwritePopupTemplate = require('../../templates/importoverwritepopup.html'); const ImportOverwritePopupTemplate = require('../../templates/importoverwritepopup.html');
@@ -223,28 +224,30 @@ export function EditorPage({ route }: EditorPageProps) {
return ( return (
<NodeGraphContextProvider> <NodeGraphContextProvider>
<ProjectDesignTokenContextProvider> <NodeReferencesContextProvider>
<PluginContextProvider> <ProjectDesignTokenContextProvider>
<BaseWindow> <PluginContextProvider>
{isLoading ? ( <BaseWindow>
<ActivityIndicator /> {isLoading ? (
) : ( <ActivityIndicator />
<> ) : (
<FrameDivider <>
first={<SidePanel />} <FrameDivider
second={<ErrorBoundary>{Boolean(Document) && <Document />}</ErrorBoundary>} first={<SidePanel />}
sizeMin={200} second={<ErrorBoundary>{Boolean(Document) && <Document />}</ErrorBoundary>}
size={frameDividerSize} sizeMin={200}
horizontal size={frameDividerSize}
onSizeChanged={setFrameDividerSize} horizontal
/> onSizeChanged={setFrameDividerSize}
/>
{Boolean(lesson) && <Frame instance={lesson} isContentSize isFitWidth />} {Boolean(lesson) && <Frame instance={lesson} isContentSize isFitWidth />}
</> </>
)} )}
</BaseWindow> </BaseWindow>
</PluginContextProvider> </PluginContextProvider>
</ProjectDesignTokenContextProvider> </ProjectDesignTokenContextProvider>
</NodeReferencesContextProvider>
</NodeGraphContextProvider> </NodeGraphContextProvider>
); );
} }

View File

@@ -1,14 +1,12 @@
import { NodeGraphContextTmp } from '@noodl-contexts/NodeGraphContext/NodeGraphContext'; import { NodeGraphContextTmp } from '@noodl-contexts/NodeGraphContext/NodeGraphContext';
import { type NodeReference, useNodeReferencesContext } from '@noodl-contexts/NodeReferencesContext';
import { useFocusRefOnPanelActive } from '@noodl-hooks/useFocusRefOnPanelActive'; import { useFocusRefOnPanelActive } from '@noodl-hooks/useFocusRefOnPanelActive';
import { useNodeLibraryLoaded } from '@noodl-hooks/useNodeLibraryLoaded'; 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 { INodeColorScheme } from '@noodl-types/nodeTypes';
import { ComponentModel } from '@noodl-models/componentmodel'; import { NodeLibrary } from '@noodl-models/nodelibrary';
import { NodeGraphNode } from '@noodl-models/nodegraphmodel';
import { NodeLibrary, NodeLibraryNodeType } from '@noodl-models/nodelibrary';
import { BasicNodeType } from '@noodl-models/nodelibrary/BasicNodeType'; import { BasicNodeType } from '@noodl-models/nodelibrary/BasicNodeType';
import { ProjectModel } from '@noodl-models/projectmodel';
import { EditorNode } from '@noodl-core-ui/components/common/EditorNode'; import { EditorNode } from '@noodl-core-ui/components/common/EditorNode';
import { IconName, IconSize } from '@noodl-core-ui/components/common/Icon'; 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 { Label } from '@noodl-core-ui/components/typography/Label';
import { NodeReferencesPanel_ID } from '.'; 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<ResultItem[]>([]);
useEffect(() => {
function updateIndex() {
const types: { [key: string]: ResultItem['type'] } = {};
const references = new Map<string, ResultItem['referenaces']>();
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() { export function NodeReferencesPanel() {
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [includeCoreNodes, setIncludeCoreNodes] = useState(false); const [includeCoreNodes, setIncludeCoreNodes] = useState(false);
const inputRef = useRef(null); const inputRef = useRef(null);
const [result] = useNodeReferences(); const { nodeReferences } = useNodeReferencesContext();
const nodeLibraryLoaded = useNodeLibraryLoaded(); const nodeLibraryLoaded = useNodeLibraryLoaded();
useFocusRefOnPanelActive(inputRef, NodeReferencesPanel_ID); useFocusRefOnPanelActive(inputRef, NodeReferencesPanel_ID);
function searchFilter(x: ResultItem) { function searchFilter(x: NodeReference) {
if (x.displayName.toLowerCase().includes(searchTerm)) { if (x.displayName.toLowerCase().includes(searchTerm)) {
return true; return true;
} }
@@ -144,7 +46,7 @@ export function NodeReferencesPanel() {
return false; return false;
} }
let filteredResult = result.filter(searchFilter); let filteredResult = nodeReferences.filter(searchFilter);
if (!includeCoreNodes) { if (!includeCoreNodes) {
filteredResult = filteredResult.filter((x) => x.displayName.startsWith('/')); filteredResult = filteredResult.filter((x) => x.displayName.startsWith('/'));
} }
@@ -185,7 +87,7 @@ export function NodeReferencesPanel() {
} }
interface ItemProps { interface ItemProps {
entry: ResultItem; entry: NodeReference;
} }
function Item({ entry }: ItemProps) { function Item({ entry }: ItemProps) {
@@ -245,8 +147,8 @@ function Item({ entry }: ItemProps) {
} }
interface ItemReferenceProps { interface ItemReferenceProps {
entry: ResultItem; entry: NodeReference;
referenace: ResultItem['referenaces'][0]; referenace: NodeReference['referenaces'][0];
colors: INodeColorScheme; colors: INodeColorScheme;
} }