Skip to content

Commit

Permalink
FB-05 additional nodes: network, parser, sequence, storage nodes (#19)
Browse files Browse the repository at this point in the history
* Test out http request auth in Node-RED

* Add jquery-migrate to red environment

* Don't create node instance until form is opened in node-editor

* Populate and parse node credentials in node-editor

* Update red/image urls in definition script rather than in finalizeEditor

* Mock additional RED values

* Completly disable console logging in jquery-migrate

* Fix tests

* Correctly handle checkbox and multi select elements in node-editor

* Add TODO notes to README.md
  • Loading branch information
JoshuaCWebDeveloper authored Apr 26, 2024
1 parent 13ce1d5 commit 6f2fce5
Show file tree
Hide file tree
Showing 12 changed files with 1,645 additions and 34 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"oneditresize",
"oneditsave",
"projectstorm",
"ptype"
"ptype",
"PWRD"
]
}
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,13 @@ The backlog is organized by epic, with each task having a unique ID, description

Use the Scrum Board to visually track the progress of tasks through the To Do, In Progress, In Review, and Done columns. This method provides a clear view of the project's progress and helps identify any bottlenecks or areas that require additional focus.

### TODO Notes

- Inject functionality
- Debug functionality
- Display junctions
- Display comments

## Flow Builder Development - Technical Details

The Flow Builder is the cornerstone of our custom frontend client for Node-RED, enabling users to visually create and edit flows with ease. To achieve a robust, intuitive, and efficient development of this epic, we have selected the following key libraries:
Expand Down
95 changes: 83 additions & 12 deletions packages/flow-client/src/app/components/node-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,8 @@ export const NodeEditor = () => {
'red-typed-input.css': false,
});
const loaded = useRef(false);
const [nodeInstance, setNodeInstance] = useState(
createNodeInstance({} as FlowNodeEntity)
const [nodeInstance, setNodeInstance] = useState<FlowNodeEntity | null>(
null
);
const editing = useAppSelector(selectEditing);
const editingNode = useAppSelector(state =>
Expand Down Expand Up @@ -225,10 +225,13 @@ export const NodeEditor = () => {
const handleCancel = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
if (!nodeInstance) {
return;
}
executeNodeFn(
['oneditcancel'],
editingNodeEntity,
nodeInstance,
nodeInstance as FlowNodeEntity,
(propertiesForm?.getRootNode() as ShadowRoot) ?? undefined
);
closeEditor();
Expand All @@ -237,11 +240,14 @@ export const NodeEditor = () => {
);

const handleDelete = useCallback(() => {
if (!nodeInstance) {
return;
}
// exec oneditsave
executeNodeFn(
['oneditdelete'],
editingNodeEntity,
nodeInstance,
nodeInstance as FlowNodeEntity,
(propertiesForm?.getRootNode() as ShadowRoot) ?? undefined
);
// TODO: Implement logic method for removing any old input links (if necessary)
Expand All @@ -258,22 +264,32 @@ export const NodeEditor = () => {

const handleSave = useCallback(() => {
const form = propertiesForm;
if (!form) {
if (!form || !nodeInstance) {
return;
}
// exec oneditsave
executeNodeFn(
['oneditsave'],
editingNodeEntity,
nodeInstance,
nodeInstance as FlowNodeEntity,
(propertiesForm?.getRootNode() as ShadowRoot) ?? undefined
);
// get our form data
const formData = Object.fromEntries(
Object.entries(selectNodeFormFields(form)).map(([key, field]) => [
key,
field.value,
])
Object.entries(selectNodeFormFields(form)).map(([key, field]) => {
if (field.type === 'checkbox') {
return [key, (field as HTMLInputElement).checked];
} else if (field.type === 'select-multiple') {
return [
key,
Array.from(
(field as HTMLSelectElement).selectedOptions
).map(option => option.value),
];
} else {
return [key, field.value];
}
})
);
// collect node updates
const nodeUpdates: Partial<FlowNodeEntity> = {};
Expand All @@ -286,6 +302,24 @@ export const NodeEditor = () => {
nodeUpdates[key] = formData[key];
}
});
// collect credentials
if (editingNodeEntity.credentials) {
nodeUpdates.credentials = {};
Object.keys(editingNodeEntity.credentials).forEach(key => {
if (!formData[key]) {
return;
}
if (editingNodeEntity.credentials?.[key].type === 'password') {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
nodeUpdates.credentials![`has_${key}`] = !!formData[key];
if (formData[key] === '__PWRD__') {
return;
}
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
nodeUpdates.credentials![key] = formData[key];
});
}
// update node
dispatch(flowLogic.updateFlowNode(editingNode.id, nodeUpdates));
// close editor
Expand Down Expand Up @@ -313,10 +347,47 @@ export const NodeEditor = () => {
// apply node values to form fields
const formFields = selectNodeFormFields(propertiesForm);
Object.entries(editingNode).forEach(([key, value]) => {
if (Object.prototype.hasOwnProperty.call(formFields, key)) {
formFields[key].value = value as string;
if (!Object.prototype.hasOwnProperty.call(formFields, key)) {
return;
}
const field = formFields[key];
if (field.type === 'checkbox') {
(field as HTMLInputElement).checked = Boolean(value);
} else if (field.type === 'select-multiple') {
const arrayValue = Array.isArray(value) ? value : [value];
Array.from((field as HTMLSelectElement).options).forEach(
option => {
option.selected = arrayValue.includes(option.value);
}
);
} else {
field.value = value as string;
}
});
// apply credentials
if (editingNodeEntity.credentials) {
const credentials = editingNode.credentials ?? {};
Object.keys(editingNodeEntity.credentials).forEach(key => {
if (!Object.prototype.hasOwnProperty.call(formFields, key)) {
return;
}
const value = credentials[key];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (editingNodeEntity.credentials![key].type !== 'password') {
formFields[key].value = value as string;
return;
}
if (value) {
formFields[key].value = value as string;
return;
}
if (credentials[`has_${key}`]) {
formFields[key].value = '__PWRD__';
return;
}
formFields[key].value = '';
});
}
// exec oneditprepare
const nodeInstance = createNodeInstance(editingNode);
const context =
Expand Down
24 changes: 6 additions & 18 deletions packages/flow-client/src/app/red/execute-script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ const executeDefinitionScript = (
definitionScript: string,
RED: ReturnType<typeof createMockRed>
) => {
// update any red/images urls in our script with our node red root
const updatedScript = definitionScript.replace(
/red\/images\/(.*)/g,
`${environment.NODE_RED_API_ROOT}/red/images/$1`
);
// eslint-disable-next-line no-new-func
const scriptFunction = new Function('RED', '$', definitionScript);
const scriptFunction = new Function('RED', '$', updatedScript);

try {
// Call the script function with the RED object
Expand Down Expand Up @@ -232,21 +237,4 @@ export const finalizeNodeEditor = (
// call i18n plugin on newly created content
const RED = createMockRed(rootContext);
(RED.$(dialogForm) as unknown as { i18n: () => void }).i18n();

// update typed input urls
Array.from(
dialogForm.querySelectorAll<HTMLImageElement>(
'img[src^="red/images/typedInput"]'
)
).forEach(img => {
const baseUrl = environment.NODE_RED_API_ROOT;
const originalSrc = img.getAttribute('src');
if (originalSrc) {
const newPath = originalSrc.replace(
/.*red\/images\/typedInput/,
`${baseUrl}/red/images/typedInput`
);
img.setAttribute('src', newPath);
}
});
};
Loading

0 comments on commit 6f2fce5

Please sign in to comment.