mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat(core): Change workflow deletions to soft deletes (#14894)
Adds soft‑deletion support for workflows through a new boolean column `isArchived`. When a workflow is archived we now set `isArchived` flag to true and the workflows stays in the database and is omitted from the default workflow listing query. Archived workflows can be viewed in read-only mode, but they cannot be activated. Archived workflows are still available by ID and can be invoked as sub-executions, so existing Execute Workflow nodes continue to work. Execution engine doesn't care about isArchived flag. Users can restore workflows via Unarchive action at the UI.
This commit is contained in:
@@ -62,13 +62,13 @@ describe('Workflows', () => {
|
||||
cy.contains('No workflows found').should('be.visible');
|
||||
});
|
||||
|
||||
it('should delete all the workflows', () => {
|
||||
it('should archive all the workflows', () => {
|
||||
WorkflowsPage.getters.workflowCards().should('have.length', multipleWorkflowsCount + 1);
|
||||
|
||||
for (let i = 0; i < multipleWorkflowsCount + 1; i++) {
|
||||
cy.getByTestId('workflow-card-actions').first().click();
|
||||
WorkflowsPage.getters.workflowDeleteButton().click();
|
||||
cy.get('button').contains('delete').click();
|
||||
WorkflowsPage.getters.workflowArchiveButton().click();
|
||||
cy.get('button').contains('archive').click();
|
||||
successToast().should('be.visible');
|
||||
}
|
||||
|
||||
@@ -141,4 +141,40 @@ describe('Workflows', () => {
|
||||
WorkflowsPage.getters.workflowActionItem('share').click();
|
||||
workflowSharingModal.getters.modal().should('be.visible');
|
||||
});
|
||||
|
||||
it('should delete archived workflows', () => {
|
||||
cy.visit(WorkflowsPage.url);
|
||||
|
||||
// Toggle "Show archived workflows" filter
|
||||
WorkflowsPage.getters.workflowFilterButton().click();
|
||||
WorkflowsPage.getters.workflowArchivedCheckbox().click();
|
||||
|
||||
WorkflowsPage.getters.workflowCards().should('have.length', multipleWorkflowsCount + 3);
|
||||
|
||||
cy.reload();
|
||||
|
||||
WorkflowsPage.getters.workflowCards().should('have.length', multipleWorkflowsCount + 3);
|
||||
|
||||
// Archive -> Unarchive -> Archive -> Delete on the first workflow
|
||||
cy.getByTestId('workflow-card-actions').first().click();
|
||||
WorkflowsPage.getters.workflowArchiveButton().click();
|
||||
cy.get('button').contains('archive').click();
|
||||
successToast().should('be.visible');
|
||||
|
||||
cy.getByTestId('workflow-card-actions').first().click();
|
||||
WorkflowsPage.getters.workflowUnarchiveButton().click();
|
||||
successToast().should('be.visible');
|
||||
|
||||
cy.getByTestId('workflow-card-actions').first().click();
|
||||
WorkflowsPage.getters.workflowArchiveButton().click();
|
||||
cy.get('button').contains('archive').click();
|
||||
successToast().should('be.visible');
|
||||
|
||||
cy.getByTestId('workflow-card-actions').first().click();
|
||||
WorkflowsPage.getters.workflowDeleteButton().click();
|
||||
cy.get('button').contains('delete').click();
|
||||
successToast().should('be.visible');
|
||||
|
||||
WorkflowsPage.getters.workflowCards().should('have.length', multipleWorkflowsCount + 2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -257,23 +257,103 @@ describe('Workflow Actions', () => {
|
||||
}).as('loadWorkflows');
|
||||
});
|
||||
|
||||
it('should not be able to delete unsaved workflow', () => {
|
||||
it('should not be able to archive or delete unsaved workflow', () => {
|
||||
WorkflowPage.getters.workflowMenu().should('be.visible');
|
||||
WorkflowPage.getters.workflowMenu().click();
|
||||
WorkflowPage.getters.workflowMenuItemDelete().closest('li').should('have.class', 'is-disabled');
|
||||
WorkflowPage.getters.workflowMenuItemDelete().should('not.exist');
|
||||
WorkflowPage.getters
|
||||
.workflowMenuItemArchive()
|
||||
.closest('li')
|
||||
.should('have.class', 'is-disabled');
|
||||
});
|
||||
|
||||
it('should delete workflow', () => {
|
||||
it('should archive workflow and then delete it', () => {
|
||||
WorkflowPage.actions.saveWorkflowOnButtonClick();
|
||||
WorkflowPage.getters.archivedTag().should('not.exist');
|
||||
|
||||
// Archive the workflow
|
||||
WorkflowPage.getters.workflowMenu().should('be.visible');
|
||||
WorkflowPage.getters.workflowMenu().click();
|
||||
WorkflowPage.getters.workflowMenuItemArchive().click();
|
||||
WorkflowPage.actions.acceptConfirmModal();
|
||||
|
||||
successToast().should('exist');
|
||||
cy.url().should('include', WorkflowPages.url);
|
||||
|
||||
// Return back to the workflow
|
||||
cy.go('back');
|
||||
|
||||
WorkflowPage.getters.archivedTag().should('be.visible');
|
||||
WorkflowPage.getters.nodeCreatorPlusButton().should('not.exist');
|
||||
|
||||
// Delete the workflow
|
||||
WorkflowPage.getters.workflowMenu().should('be.visible');
|
||||
WorkflowPage.getters.workflowMenu().click();
|
||||
WorkflowPage.getters.workflowMenuItemDelete().click();
|
||||
cy.get('div[role=dialog][aria-modal=true]').should('be.visible');
|
||||
cy.get('button.btn--confirm').should('be.visible').click();
|
||||
WorkflowPage.actions.acceptConfirmModal();
|
||||
successToast().should('exist');
|
||||
cy.url().should('include', WorkflowPages.url);
|
||||
});
|
||||
|
||||
it('should archive workflow and then unarchive it', () => {
|
||||
WorkflowPage.actions.saveWorkflowOnButtonClick();
|
||||
WorkflowPage.getters.archivedTag().should('not.exist');
|
||||
|
||||
// Archive the workflow
|
||||
WorkflowPage.getters.workflowMenu().should('be.visible');
|
||||
WorkflowPage.getters.workflowMenu().click();
|
||||
WorkflowPage.getters.workflowMenuItemArchive().click();
|
||||
WorkflowPage.actions.acceptConfirmModal();
|
||||
successToast().should('exist');
|
||||
cy.url().should('include', WorkflowPages.url);
|
||||
|
||||
// Return back to the workflow
|
||||
cy.go('back');
|
||||
|
||||
WorkflowPage.getters.archivedTag().should('be.visible');
|
||||
WorkflowPage.getters.nodeCreatorPlusButton().should('not.exist');
|
||||
|
||||
// Unarchive the workflow
|
||||
WorkflowPage.getters.workflowMenu().should('be.visible');
|
||||
WorkflowPage.getters.workflowMenu().click();
|
||||
WorkflowPage.getters.workflowMenuItemUnarchive().click();
|
||||
successToast().should('exist');
|
||||
WorkflowPage.getters.archivedTag().should('not.exist');
|
||||
WorkflowPage.getters.nodeCreatorPlusButton().should('be.visible');
|
||||
});
|
||||
|
||||
it('should deactivate active workflow on archive', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.saveWorkflowOnButtonClick();
|
||||
WorkflowPage.actions.activateWorkflow();
|
||||
WorkflowPage.getters.isWorkflowActivated();
|
||||
|
||||
// Archive the workflow
|
||||
WorkflowPage.getters.workflowMenu().click();
|
||||
WorkflowPage.getters.workflowMenuItemArchive().click();
|
||||
WorkflowPage.actions.acceptConfirmModal();
|
||||
successToast().should('exist');
|
||||
cy.url().should('include', WorkflowPages.url);
|
||||
|
||||
// Return back to the workflow
|
||||
cy.go('back');
|
||||
|
||||
WorkflowPage.getters.archivedTag().should('be.visible');
|
||||
WorkflowPage.getters.isWorkflowDeactivated();
|
||||
WorkflowPage.getters.activatorSwitch().find('input').first().should('be.disabled');
|
||||
|
||||
// Unarchive the workflow
|
||||
WorkflowPage.getters.workflowMenu().should('be.visible');
|
||||
WorkflowPage.getters.workflowMenu().click();
|
||||
WorkflowPage.getters.workflowMenuItemUnarchive().click();
|
||||
successToast().should('exist');
|
||||
WorkflowPage.getters.archivedTag().should('not.exist');
|
||||
|
||||
// Activate the workflow again
|
||||
WorkflowPage.actions.activateWorkflow();
|
||||
WorkflowPage.getters.isWorkflowActivated();
|
||||
});
|
||||
|
||||
describe('duplicate workflow', () => {
|
||||
function duplicateWorkflow() {
|
||||
WorkflowPage.getters.workflowMenu().should('be.visible');
|
||||
|
||||
@@ -84,6 +84,8 @@ export class WorkflowPage extends BasePage {
|
||||
firstStepButton: () => cy.getByTestId('canvas-add-button'),
|
||||
isWorkflowSaved: () => this.getters.saveButton().should('match', 'span'), // In Element UI, disabled button turn into spans 🤷♂️
|
||||
isWorkflowActivated: () => this.getters.activatorSwitch().should('have.class', 'is-checked'),
|
||||
isWorkflowDeactivated: () =>
|
||||
this.getters.activatorSwitch().should('not.have.class', 'is-checked'),
|
||||
expressionModalInput: () => cy.getByTestId('expression-modal-input').find('[role=textbox]'),
|
||||
expressionModalOutput: () => cy.getByTestId('expression-modal-output'),
|
||||
|
||||
@@ -117,6 +119,8 @@ export class WorkflowPage extends BasePage {
|
||||
workflowMenuItemImportFromFile: () => cy.getByTestId('workflow-menu-item-import-from-file'),
|
||||
workflowMenuItemSettings: () => cy.getByTestId('workflow-menu-item-settings'),
|
||||
workflowMenuItemDelete: () => cy.getByTestId('workflow-menu-item-delete'),
|
||||
workflowMenuItemArchive: () => cy.getByTestId('workflow-menu-item-archive'),
|
||||
workflowMenuItemUnarchive: () => cy.getByTestId('workflow-menu-item-unarchive'),
|
||||
workflowMenuItemGitPush: () => cy.getByTestId('workflow-menu-item-push'),
|
||||
// Workflow settings dialog elements
|
||||
workflowSettingsModal: () => cy.getByTestId('workflow-settings-dialog'),
|
||||
@@ -136,6 +140,7 @@ export class WorkflowPage extends BasePage {
|
||||
workflowSettingsSaveButton: () =>
|
||||
cy.getByTestId('workflow-settings-save-button').find('button'),
|
||||
|
||||
archivedTag: () => cy.getByTestId('workflow-archived-tag'),
|
||||
shareButton: () => cy.getByTestId('workflow-share-button'),
|
||||
|
||||
duplicateWorkflowModal: () => cy.getByTestId('duplicate-modal'),
|
||||
@@ -214,6 +219,7 @@ export class WorkflowPage extends BasePage {
|
||||
}
|
||||
return parseFloat(element.css('top'));
|
||||
},
|
||||
confirmModal: () => cy.get('div[role=dialog][aria-modal=true]'),
|
||||
};
|
||||
|
||||
actions = {
|
||||
@@ -551,5 +557,9 @@ export class WorkflowPage extends BasePage {
|
||||
top: +$el[0].style.top.replace('px', ''),
|
||||
}));
|
||||
},
|
||||
acceptConfirmModal: () => {
|
||||
this.getters.confirmModal().should('be.visible');
|
||||
cy.get('button.btn--confirm').should('be.visible').click();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@ export class WorkflowsPage extends BasePage {
|
||||
workflowCardActions: (workflowName: string) =>
|
||||
this.getters.workflowCard(workflowName).findChildByTestId('workflow-card-actions'),
|
||||
workflowActionItem: (action: string) => cy.getByTestId(`action-${action}`).filter(':visible'),
|
||||
workflowArchiveButton: () =>
|
||||
cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Archive'),
|
||||
workflowUnarchiveButton: () =>
|
||||
cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Unarchive'),
|
||||
workflowDeleteButton: () =>
|
||||
cy.getByTestId('action-toggle-dropdown').filter(':visible').contains('Delete'),
|
||||
workflowMoveButton: () =>
|
||||
@@ -47,6 +51,7 @@ export class WorkflowsPage extends BasePage {
|
||||
workflowStatusItem: (status: string) => cy.getByTestId('status').contains(status),
|
||||
workflowOwnershipDropdown: () => cy.getByTestId('user-select-trigger'),
|
||||
workflowOwner: (email: string) => cy.getByTestId('user-email').contains(email),
|
||||
workflowArchivedCheckbox: () => cy.getByTestId('show-archived-checkbox'),
|
||||
workflowResetFilters: () => cy.getByTestId('workflows-filter-reset'),
|
||||
workflowSortDropdown: () => cy.getByTestId('resources-list-sort'),
|
||||
workflowSortItem: (sort: string) =>
|
||||
|
||||
Reference in New Issue
Block a user