fix(editor): Fix route component caching, incorrect use of array reduce method and enable WF history feature (#7434)

Co-authored-by: Giulio Andreini <andreini@netseven.it>
This commit is contained in:
Csaba Tuncsik 2023-10-26 20:47:42 +02:00 committed by GitHub
parent ae616f146b
commit 12a89e6d14
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 211 additions and 23 deletions

View file

@ -0,0 +1,151 @@
import {
CODE_NODE_NAME,
EDIT_FIELDS_SET_NODE_NAME,
IF_NODE_NAME,
INSTANCE_OWNER,
SCHEDULE_TRIGGER_NODE_NAME,
} from '../constants';
import {
WorkflowExecutionsTab,
WorkflowPage as WorkflowPageClass,
WorkflowHistoryPage,
} from '../pages';
const workflowPage = new WorkflowPageClass();
const executionsTab = new WorkflowExecutionsTab();
const workflowHistoryPage = new WorkflowHistoryPage();
const createNewWorkflowAndActivate = () => {
workflowPage.actions.visit();
workflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.actions.activateWorkflow();
cy.get('.el-notification .el-notification--error').should('not.exist');
};
const editWorkflowAndDeactivate = () => {
workflowPage.getters.canvasNodePlusEndpointByName(SCHEDULE_TRIGGER_NODE_NAME).click();
workflowPage.getters.nodeCreatorSearchBar().should('be.visible');
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, false);
cy.get('.jtk-connector').should('have.length', 1);
workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.getters.activatorSwitch().click();
workflowPage.actions.zoomToFit();
cy.get('.el-notification .el-notification--error').should('not.exist');
};
const editWorkflowMoreAndActivate = () => {
cy.drag(workflowPage.getters.getEndpointSelector('plus', EDIT_FIELDS_SET_NODE_NAME), [200, 200], {
realMouse: true,
});
workflowPage.getters.nodeCreatorSearchBar().should('be.visible');
workflowPage.actions.addNodeToCanvas(CODE_NODE_NAME, false);
workflowPage.getters.nodeViewBackground().click(600, 200, { force: true });
cy.get('.jtk-connector').should('have.length', 2);
workflowPage.actions.zoomToFit();
workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.actions.addNodeToCanvas(IF_NODE_NAME);
workflowPage.getters.nodeViewBackground().click(600, 200, { force: true });
cy.get('.jtk-connector').should('have.length', 2);
const position = {
top: 0,
left: 0,
};
workflowPage.getters
.canvasNodeByName(IF_NODE_NAME)
.click()
.then(($element) => {
position.top = $element.position().top;
position.left = $element.position().left;
});
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 200], { clickToFinish: true });
workflowPage.getters
.canvasNodes()
.last()
.then(($element) => {
const finalPosition = {
top: $element.position().top,
left: $element.position().left,
};
expect(finalPosition.top).to.be.greaterThan(position.top);
expect(finalPosition.left).to.be.greaterThan(position.left);
});
cy.draganddrop(
workflowPage.getters.getEndpointSelector('output', CODE_NODE_NAME),
workflowPage.getters.getEndpointSelector('input', IF_NODE_NAME),
);
cy.get('.jtk-connector').should('have.length', 3);
workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.getters.activatorSwitch().click();
cy.get('.el-notification .el-notification--error').should('not.exist');
};
describe('Editor actions should work', () => {
beforeEach(() => {
cy.enableFeature('debugInEditor');
cy.enableFeature('workflowHistory');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
createNewWorkflowAndActivate();
});
it('after saving a new workflow', () => {
editWorkflowAndDeactivate();
editWorkflowMoreAndActivate();
});
it('after switching between Editor and Executions', () => {
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions');
executionsTab.actions.switchToExecutionsTab();
cy.wait(['@getExecutions', '@getCurrentExecutions']);
cy.wait(500);
executionsTab.actions.switchToEditorTab();
editWorkflowAndDeactivate();
editWorkflowMoreAndActivate();
});
it('after switching between Editor and Debug', () => {
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
cy.intercept('GET', '/rest/executions/*').as('getExecution');
cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions');
cy.intercept('POST', '/rest/workflows/run').as('postWorkflowRun');
editWorkflowAndDeactivate();
workflowPage.actions.executeWorkflow();
cy.wait(['@postWorkflowRun']);
executionsTab.actions.switchToExecutionsTab();
cy.wait(['@getExecutions', '@getCurrentExecutions']);
executionsTab.getters.executionListItems().should('have.length', 1).first().click();
cy.wait(['@getExecution']);
executionsTab.getters.executionDebugButton().should('have.text', 'Copy to editor').click();
editWorkflowMoreAndActivate();
});
it('after switching between Editor and Workflow history', () => {
cy.intercept('GET', '/rest/workflow-history/workflow/*/version/*').as('getVersion');
cy.intercept('GET', '/rest/workflow-history/workflow/*').as('getHistory');
editWorkflowAndDeactivate();
workflowPage.getters.workflowHistoryButton().click();
cy.wait(['@getHistory']);
cy.wait(['@getVersion']);
cy.intercept('GET', '/rest/workflows/*').as('workflowGet');
workflowHistoryPage.getters.workflowHistoryCloseButton().click();
cy.wait(['@workflowGet']);
cy.wait(1000);
editWorkflowMoreAndActivate();
});
});

View file

@ -10,3 +10,4 @@ export * from './ndv';
export * from './bannerStack'; export * from './bannerStack';
export * from './workflow-executions-tab'; export * from './workflow-executions-tab';
export * from './signin'; export * from './signin';
export * from './workflow-history';

View file

@ -0,0 +1,7 @@
import { BasePage } from "./base";
export class WorkflowHistoryPage extends BasePage {
getters = {
workflowHistoryCloseButton: () => cy.getByTestId('workflow-history-close-button'),
}
}

View file

@ -124,6 +124,7 @@ export class WorkflowPage extends BasePage {
addStickyButton: () => cy.getByTestId('add-sticky-button'), addStickyButton: () => cy.getByTestId('add-sticky-button'),
stickies: () => cy.getByTestId('sticky'), stickies: () => cy.getByTestId('sticky'),
editorTabButton: () => cy.getByTestId('radio-button-workflow'), editorTabButton: () => cy.getByTestId('radio-button-workflow'),
workflowHistoryButton: () => cy.getByTestId('workflow-history-button'),
}; };
actions = { actions = {
visit: (preventNodeViewUnload = true) => { visit: (preventNodeViewUnload = true) => {

View file

@ -20,9 +20,10 @@
</div> </div>
<div id="content" :class="$style.content"> <div id="content" :class="$style.content">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<keep-alive include="NodeView" :max="1"> <keep-alive v-if="$route.meta.keepWorkflowAlive" include="NodeView" :max="1">
<component :is="Component" /> <component :is="Component" />
</keep-alive> </keep-alive>
<component v-else :is="Component" />
</router-view> </router-view>
</div> </div>
<Modals /> <Modals />
@ -257,7 +258,7 @@ export default defineComponent({
void this.postAuthenticate(); void this.postAuthenticate();
} }
}, },
async $route(route) { async $route() {
await this.initSettings(); await this.initSettings();
await this.redirectIfNecessary(); await this.redirectIfNecessary();

View file

@ -108,6 +108,7 @@
> >
<n8n-icon-button <n8n-icon-button
:disabled="isWorkflowHistoryButtonDisabled" :disabled="isWorkflowHistoryButtonDisabled"
data-test-id="workflow-history-button"
type="tertiary" type="tertiary"
icon="history" icon="history"
size="medium" size="medium"
@ -349,9 +350,8 @@ export default defineComponent({
return actions; return actions;
}, },
isWorkflowHistoryFeatureEnabled(): boolean { isWorkflowHistoryFeatureEnabled(): boolean {
return ( return this.settingsStore.isEnterpriseFeatureEnabled(
this.settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.WorkflowHistory) && EnterpriseEditionFeature.WorkflowHistory,
this.settingsStore.isDevRelease
); );
}, },
workflowHistoryRoute(): { name: string; params: { workflowId: string } } { workflowHistoryRoute(): { name: string; params: { workflowId: string } } {

View file

@ -17,6 +17,7 @@ const props = defineProps<{
workflowVersion: WorkflowVersion | null; workflowVersion: WorkflowVersion | null;
actions: UserAction[]; actions: UserAction[];
isListLoading?: boolean; isListLoading?: boolean;
isFirstItemShown?: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -42,6 +43,12 @@ const workflowVersionPreview = computed<IWorkflowDb | undefined>(() => {
}; };
}); });
const actions = computed(() =>
props.isFirstItemShown
? props.actions.filter((action) => action.value !== 'restore')
: props.actions,
);
const onAction = ({ const onAction = ({
action, action,
id, id,
@ -67,11 +74,10 @@ const onAction = ({
<workflow-history-list-item <workflow-history-list-item
:class="$style.card" :class="$style.card"
v-if="props.workflowVersion" v-if="props.workflowVersion"
:full="true"
:index="-1" :index="-1"
:item="props.workflowVersion" :item="props.workflowVersion"
:isActive="false" :isActive="false"
:actions="props.actions" :actions="actions"
@action="onAction" @action="onAction"
> >
<template #default="{ formattedCreatedAt }"> <template #default="{ formattedCreatedAt }">
@ -99,7 +105,7 @@ const onAction = ({
</section> </section>
</template> </template>
<template #action-toggle-button> <template #action-toggle-button>
<n8n-button type="tertiary" size="small" data-test-id="action-toggle-button"> <n8n-button type="tertiary" size="large" data-test-id="action-toggle-button">
{{ i18n.baseText('workflowHistory.content.actions') }} {{ i18n.baseText('workflowHistory.content.actions') }}
<n8n-icon class="ml-3xs" icon="chevron-down" size="small" /> <n8n-icon class="ml-3xs" icon="chevron-down" size="small" />
</n8n-button> </n8n-button>
@ -146,8 +152,9 @@ const onAction = ({
&:first-child { &:first-child {
padding-top: var(--spacing-3xs); padding-top: var(--spacing-3xs);
padding-bottom: var(--spacing-3xs); padding-bottom: var(--spacing-4xs);
* { * {
margin-top: auto;
font-size: var(--font-size-m); font-size: var(--font-size-m);
} }
} }
@ -161,6 +168,7 @@ const onAction = ({
} }
.label { .label {
color: var(--color-text-light);
padding-right: var(--spacing-4xs); padding-right: var(--spacing-4xs);
} }

View file

@ -41,6 +41,9 @@ const listElement = ref<Element | null>(null);
const shouldAutoScroll = ref(true); const shouldAutoScroll = ref(true);
const observer = ref<IntersectionObserver | null>(null); const observer = ref<IntersectionObserver | null>(null);
const getActions = (index: number) =>
index === 0 ? props.actions.filter((action) => action.value !== 'restore') : props.actions;
const observeElement = (element: Element) => { const observeElement = (element: Element) => {
observer.value = new IntersectionObserver( observer.value = new IntersectionObserver(
([entry]) => { ([entry]) => {
@ -109,7 +112,7 @@ const onItemMounted = ({
:index="index" :index="index"
:item="item" :item="item"
:isActive="item.versionId === props.activeItem?.versionId" :isActive="item.versionId === props.activeItem?.versionId"
:actions="props.actions" :actions="getActions(index)"
@action="onAction" @action="onAction"
@preview="onPreview" @preview="onPreview"
@mounted="onItemMounted" @mounted="onItemMounted"

View file

@ -34,7 +34,7 @@ export function longestCommonPrefix(...strings: string[]) {
} }
return acc.slice(0, i); return acc.slice(0, i);
}); }, '');
} }
// Process user input if expressions are used as part of complex expression // Process user input if expressions are used as part of complex expression

View file

@ -235,6 +235,7 @@ export const routes = [
}, },
meta: { meta: {
nodeView: true, nodeView: true,
keepWorkflowAlive: true,
permissions: { permissions: {
allow: { allow: {
loginStatus: [LOGIN_STATUS.LoggedIn], loginStatus: [LOGIN_STATUS.LoggedIn],
@ -362,6 +363,7 @@ export const routes = [
}, },
meta: { meta: {
nodeView: true, nodeView: true,
keepWorkflowAlive: true,
permissions: { permissions: {
allow: { allow: {
loginStatus: [LOGIN_STATUS.LoggedIn], loginStatus: [LOGIN_STATUS.LoggedIn],
@ -393,6 +395,7 @@ export const routes = [
}, },
meta: { meta: {
nodeView: true, nodeView: true,
keepWorkflowAlive: true,
permissions: { permissions: {
allow: { allow: {
loginStatus: [LOGIN_STATUS.LoggedIn], loginStatus: [LOGIN_STATUS.LoggedIn],

View file

@ -90,7 +90,13 @@ export const useWorkflowHistoryStore = defineStore('workflowHistory', () => {
updateData.active = false; updateData.active = false;
} }
return workflowsStore.updateWorkflow(workflowId, updateData, true); return workflowsStore.updateWorkflow(workflowId, updateData, true).catch(async (error) => {
if (error.httpStatusCode === 400 && error.message.includes('can not be activated')) {
return workflowsStore.fetchWorkflow(workflowId);
} else {
throw new Error(error);
}
});
}; };
return { return {

View file

@ -325,7 +325,7 @@ export const getLeftmostTopNode = (nodes: INodeUi[]): INodeUi => {
} }
return node; return node;
}); }, nodes[0]);
}; };
export const getWorkflowCorners = (nodes: INodeUi[]): IBounds => { export const getWorkflowCorners = (nodes: INodeUi[]): IBounds => {
@ -949,15 +949,17 @@ export const getInputEndpointUUID = (
export const getFixedNodesList = (workflowNodes: INode[]) => { export const getFixedNodesList = (workflowNodes: INode[]) => {
const nodes = [...workflowNodes]; const nodes = [...workflowNodes];
const leftmostTop = getLeftmostTopNode(nodes); if (nodes.length) {
const leftmostTop = getLeftmostTopNode(nodes);
const diffX = DEFAULT_START_POSITION_X - leftmostTop.position[0]; const diffX = DEFAULT_START_POSITION_X - leftmostTop.position[0];
const diffY = DEFAULT_START_POSITION_Y - leftmostTop.position[1]; const diffY = DEFAULT_START_POSITION_Y - leftmostTop.position[1];
nodes.map((node) => { nodes.forEach((node) => {
node.position[0] += diffX + NODE_SIZE * 2; node.position[0] += diffX + NODE_SIZE * 2;
node.position[1] += diffY; node.position[1] += diffY;
}); });
}
return nodes; return nodes;
}; };

View file

@ -67,6 +67,10 @@ const actions = computed<UserAction[]>(() =>
})), })),
); );
const isFirstItemShown = computed(
() => workflowHistory.value[0]?.versionId === route.params.versionId,
);
const loadMore = async (queryParams: WorkflowHistoryRequestParams) => { const loadMore = async (queryParams: WorkflowHistoryRequestParams) => {
const history = await workflowHistoryStore.getWorkflowHistory( const history = await workflowHistoryStore.getWorkflowHistory(
route.params.workflowId, route.params.workflowId,
@ -294,14 +298,14 @@ watchEffect(async () => {
</script> </script>
<template> <template>
<div :class="$style.view"> <div :class="$style.view">
<n8n-heading :class="$style.header" tag="h2" size="medium" bold> <n8n-heading :class="$style.header" tag="h2" size="medium">
{{ activeWorkflow?.name }} {{ activeWorkflow?.name }}
</n8n-heading> </n8n-heading>
<div :class="$style.corner"> <div :class="$style.corner">
<n8n-heading tag="h2" size="medium" bold> <n8n-heading tag="h2" size="medium" bold>
{{ i18n.baseText('workflowHistory.title') }} {{ i18n.baseText('workflowHistory.title') }}
</n8n-heading> </n8n-heading>
<router-link :to="editorRoute"> <router-link :to="editorRoute" data-test-id="workflow-history-close-button">
<n8n-button type="tertiary" icon="times" size="small" text square /> <n8n-button type="tertiary" icon="times" size="small" text square />
</router-link> </router-link>
</div> </div>
@ -326,9 +330,10 @@ watchEffect(async () => {
<workflow-history-content <workflow-history-content
v-if="canRender" v-if="canRender"
:workflow="activeWorkflow" :workflow="activeWorkflow"
:workflow-version="activeWorkflowVersion" :workflowVersion="activeWorkflowVersion"
:actions="actions" :actions="actions"
:isListLoading="isListLoading" :isListLoading="isListLoading"
:isFirstItemShown="isFirstItemShown"
@action="onAction" @action="onAction"
/> />
</div> </div>