Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FB-05 additional nodes: network, parser, sequence, storage nodes #19

Merged
merged 10 commits into from
Apr 26, 2024
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