fix(editor): Code node overwrites code when switching nodes after edits (#13078)

This commit is contained in:
Elias Meire
2025-02-05 17:07:32 +01:00
committed by GitHub
parent 16d59e98ed
commit 00e3ebc9e2
5 changed files with 43 additions and 31 deletions

View File

@@ -7,6 +7,9 @@ import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
const WorkflowPage = new WorkflowPageClass(); const WorkflowPage = new WorkflowPageClass();
const ndv = new NDV(); const ndv = new NDV();
const getParameter = () => ndv.getters.parameterInput('jsCode').should('be.visible');
const getEditor = () => getParameter().find('.cm-content').should('exist');
describe('Code node', () => { describe('Code node', () => {
describe('Code editor', () => { describe('Code editor', () => {
beforeEach(() => { beforeEach(() => {
@@ -40,10 +43,26 @@ describe('Code node', () => {
successToast().contains('Node executed successfully'); successToast().contains('Node executed successfully');
}); });
it('should show lint errors in `runOnceForAllItems` mode', () => { it('should allow switching between sibling code nodes', () => {
const getParameter = () => ndv.getters.parameterInput('jsCode').should('be.visible'); // Setup
const getEditor = () => getParameter().find('.cm-content').should('exist'); getEditor().type('{selectall}').paste("console.log('code node 1')");
ndv.actions.close();
WorkflowPage.actions.addNodeToCanvas('Code', true, true);
getEditor().type('{selectall}').paste("console.log('code node 2')");
ndv.actions.close();
WorkflowPage.actions.openNode('Code');
ndv.actions.clickFloatingNode('Code1');
getEditor().should('have.text', "console.log('code node 2')");
getEditor().type('{selectall}').type("console.log('code node 2 edited')");
// wait for debounce
cy.wait(200);
ndv.actions.clickFloatingNode('Code');
getEditor().should('have.text', "console.log('code node 1')");
});
it('should show lint errors in `runOnceForAllItems` mode', () => {
getEditor() getEditor()
.type('{selectall}') .type('{selectall}')
.paste(`$input.itemMatching() .paste(`$input.itemMatching()
@@ -66,9 +85,6 @@ return
}); });
it('should show lint errors in `runOnceForEachItem` mode', () => { it('should show lint errors in `runOnceForEachItem` mode', () => {
const getParameter = () => ndv.getters.parameterInput('jsCode').should('be.visible');
const getEditor = () => getParameter().find('.cm-content').should('exist');
ndv.getters.parameterInput('mode').click(); ndv.getters.parameterInput('mode').click();
ndv.actions.selectOptionInParameterDropdown('mode', 'Run Once for Each Item'); ndv.actions.selectOptionInParameterDropdown('mode', 'Run Once for Each Item');
getEditor() getEditor()

View File

@@ -151,6 +151,9 @@ export class NDV extends BasePage {
schemaViewNodeName: () => cy.getByTestId('run-data-schema-node-name'), schemaViewNodeName: () => cy.getByTestId('run-data-schema-node-name'),
expressionExpanders: () => cy.getByTestId('expander'), expressionExpanders: () => cy.getByTestId('expander'),
expressionModalOutput: () => cy.getByTestId('expression-modal-output'), expressionModalOutput: () => cy.getByTestId('expression-modal-output'),
floatingNodes: () => cy.getByTestId('floating-node'),
floatingNodeByName: (name: string) =>
cy.getByTestId('floating-node').filter(`[data-node-name="${name}"]`),
}; };
actions = { actions = {
@@ -339,6 +342,9 @@ export class NDV extends BasePage {
dragMainPanelToRight: () => { dragMainPanelToRight: () => {
cy.drag('[data-test-id=panel-drag-button]', [1000, 0], { moveTwice: true }); cy.drag('[data-test-id=panel-drag-button]', [1000, 0], { moveTwice: true });
}, },
clickFloatingNode: (name: string) => {
this.getters.floatingNodeByName(name).realHover().click({ force: true });
},
}; };
} }

View File

@@ -118,7 +118,7 @@ const emit = defineEmits<{
const externalHooks = useExternalHooks(); const externalHooks = useExternalHooks();
const i18n = useI18n(); const i18n = useI18n();
const nodeHelpers = useNodeHelpers(); const nodeHelpers = useNodeHelpers();
const { callDebounced } = useDebounce(); const { debounce } = useDebounce();
const router = useRouter(); const router = useRouter();
const workflowHelpers = useWorkflowHelpers({ router }); const workflowHelpers = useWorkflowHelpers({ router });
const telemetry = useTelemetry(); const telemetry = useTelemetry();
@@ -801,9 +801,9 @@ function onTextInputChange(value: string) {
emit('textInput', parameterData); emit('textInput', parameterData);
} }
function valueChangedDebounced(value: NodeParameterValueType | {} | Date) {
void callDebounced(valueChanged, { debounceTime: 100 }, value); const valueChangedDebounced = debounce(valueChanged, { debounceTime: 100 });
}
function onUpdateTextInput(value: string) { function onUpdateTextInput(value: string) {
valueChanged(value); valueChanged(value);
onTextInputChange(value); onTextInputChange(value);
@@ -1032,6 +1032,7 @@ defineExpose({
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
valueChangedDebounced.cancel();
props.eventBus.off('optionSelected', optionSelected); props.eventBus.off('optionSelected', optionSelected);
}); });

View File

@@ -154,10 +154,7 @@ export const useCodeEditor = <L extends CodeEditorLanguage>({
} }
} }
const emitChanges = debounce((update: ViewUpdate) => { const emitChanges = debounce(onChange, 300);
onChange(update);
}, 300);
const lastChange = ref<ViewUpdate>();
function onEditorUpdate(update: ViewUpdate) { function onEditorUpdate(update: ViewUpdate) {
autocompleteStatus.value = completionStatus(update.view.state); autocompleteStatus.value = completionStatus(update.view.state);
@@ -168,7 +165,6 @@ export const useCodeEditor = <L extends CodeEditorLanguage>({
); );
if (update.docChanged && !shouldIgnoreUpdate) { if (update.docChanged && !shouldIgnoreUpdate) {
lastChange.value = update;
hasChanges.value = true; hasChanges.value = true;
emitChanges(update); emitChanges(update);
} }
@@ -375,9 +371,7 @@ export const useCodeEditor = <L extends CodeEditorLanguage>({
localStorage.removeItem(storedStateId.value); localStorage.removeItem(storedStateId.value);
} }
if (lastChange.value) { emitChanges.flush();
onChange(lastChange.value);
}
editor.value.destroy(); editor.value.destroy();
} }
}); });

View File

@@ -1,24 +1,19 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { debounce as _debounce } from 'lodash-es'; import { debounce as _debounce, type DebouncedFunc } from 'lodash-es';
export interface DebounceOptions { export interface DebounceOptions {
debounceTime: number; debounceTime: number;
trailing?: boolean; trailing?: boolean;
} }
export type DebouncedFunction<Args extends unknown[] = unknown[], R = void> = (...args: Args) => R; // eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyFunction = (...args: any) => any;
export function useDebounce() { export function useDebounce() {
// Create a ref for the WeakMap to store debounced functions. // Create a ref for the WeakMap to store debounced functions.
const debounceCache = ref( const debounceCache = ref(new WeakMap<AnyFunction, DebouncedFunc<AnyFunction>>());
// eslint-disable-next-line @typescript-eslint/no-explicit-any
new WeakMap<DebouncedFunction<any, any>, DebouncedFunction<any, any>>(),
);
const debounce = <T extends DebouncedFunction<Parameters<T>, ReturnType<T>>>( const debounce = <T extends AnyFunction>(fn: T, options: DebounceOptions): DebouncedFunc<T> => {
fn: T,
options: DebounceOptions,
): T => {
const { trailing, debounceTime } = options; const { trailing, debounceTime } = options;
// Check if a debounced version of the function is already stored in the WeakMap. // Check if a debounced version of the function is already stored in the WeakMap.
@@ -37,14 +32,14 @@ export function useDebounce() {
debounceCache.value.set(fn, debouncedFn); debounceCache.value.set(fn, debouncedFn);
} }
return debouncedFn as T; return debouncedFn;
}; };
const callDebounced = <T extends DebouncedFunction<Parameters<T>, ReturnType<T>>>( const callDebounced = <T extends AnyFunction>(
fn: T, fn: T,
options: DebounceOptions, options: DebounceOptions,
...inputParameters: Parameters<T> ...inputParameters: Parameters<T>
): ReturnType<T> => { ): ReturnType<T> | undefined => {
const debouncedFn = debounce(fn, options); const debouncedFn = debounce(fn, options);
return debouncedFn(...inputParameters); return debouncedFn(...inputParameters);