From 6046d24c741570d1d092230808a392bce0103d33 Mon Sep 17 00:00:00 2001 From: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> Date: Wed, 6 Aug 2025 11:15:10 +0200 Subject: [PATCH] feat(editor): Add Production checklist for active workflows (#17756) Co-authored-by: Claude Co-authored-by: Giulio Andreini --- cypress/support/commands.ts | 51 ++ cypress/support/index.ts | 1 + .../src/components/N8nButton/Button.scss | 27 + .../components/N8nButton/Button.stories.ts | 11 +- .../src/components/N8nButton/Button.test.ts | 17 + .../src/components/N8nButton/Button.vue | 4 + .../__snapshots__/Button.test.ts.snap | 6 + .../src/components/N8nLink/Link.test.ts | 118 +++ .../src/components/N8nLink/Link.vue | 3 +- .../N8nLink/__snapshots__/Link.test.ts.snap | 11 + .../N8nPopoverReka/N8nPopoverReka.vue | 17 +- .../__snapshots__/N8nPopoverReka.test.ts.snap | 2 +- .../src/components/N8nRoute/Route.test.ts | 29 + .../src/components/N8nRoute/Route.vue | 2 + .../SuggestedActions.stories.ts | 377 ++++++++ .../SuggestedActions.test.ts | 336 +++++++ .../N8nSuggestedActions/SuggestedActions.vue | 218 +++++ .../components/N8nSuggestedActions/index.ts | 1 + .../TableHeaderControlsButton.vue | 144 +-- .../TableHeaderControlsButton.test.ts.snap | 216 ++--- .../design-system/src/components/index.ts | 2 + .../design-system/src/css/_tokens.dark.scss | 12 + .../@n8n/design-system/src/css/_tokens.scss | 13 + .../@n8n/design-system/src/locale/lang/en.ts | 3 + .../@n8n/design-system/src/types/button.ts | 10 +- .../frontend/@n8n/i18n/src/locales/en.json | 14 +- .../src/components/MainHeader/MainHeader.vue | 19 + .../components/MainHeader/WorkflowDetails.vue | 32 +- .../MainHeader/WorkflowHistoryButton.test.ts | 30 +- .../MainHeader/WorkflowHistoryButton.vue | 24 +- .../WorkflowHistoryButton.test.ts.snap | 28 + .../src/components/WorkflowActivator.vue | 1 - .../WorkflowProductionChecklist.test.ts | 846 ++++++++++++++++++ .../WorkflowProductionChecklist.vue | 237 +++++ .../src/components/WorkflowSettings.vue | 24 +- .../src/composables/useWorkflowsCache.test.ts | 365 ++++++++ .../src/composables/useWorkflowsCache.ts | 139 +++ packages/frontend/editor-ui/src/constants.ts | 4 + .../Evaluations.ee/EvaluationsRootView.vue | 4 +- .../Evaluations.ee/TestRunDetailView.vue | 27 +- packages/testing/playwright/README.md | 9 +- .../composables/ProductionChecklist.ts | 23 + .../composables/WorkflowActivationModal.ts | 14 + .../composables/WorkflowSettingsModal.ts | 9 + .../testing/playwright/pages/CanvasPage.ts | 22 + .../53-workflow-production-checklist.spec.ts | 187 ++++ 46 files changed, 3443 insertions(+), 246 deletions(-) create mode 100644 packages/frontend/@n8n/design-system/src/components/N8nLink/Link.test.ts create mode 100644 packages/frontend/@n8n/design-system/src/components/N8nLink/__snapshots__/Link.test.ts.snap create mode 100644 packages/frontend/@n8n/design-system/src/components/N8nSuggestedActions/SuggestedActions.stories.ts create mode 100644 packages/frontend/@n8n/design-system/src/components/N8nSuggestedActions/SuggestedActions.test.ts create mode 100644 packages/frontend/@n8n/design-system/src/components/N8nSuggestedActions/SuggestedActions.vue create mode 100644 packages/frontend/@n8n/design-system/src/components/N8nSuggestedActions/index.ts create mode 100644 packages/frontend/editor-ui/src/components/MainHeader/__snapshots__/WorkflowHistoryButton.test.ts.snap create mode 100644 packages/frontend/editor-ui/src/components/WorkflowProductionChecklist.test.ts create mode 100644 packages/frontend/editor-ui/src/components/WorkflowProductionChecklist.vue create mode 100644 packages/frontend/editor-ui/src/composables/useWorkflowsCache.test.ts create mode 100644 packages/frontend/editor-ui/src/composables/useWorkflowsCache.ts create mode 100644 packages/testing/playwright/composables/ProductionChecklist.ts create mode 100644 packages/testing/playwright/composables/WorkflowActivationModal.ts create mode 100644 packages/testing/playwright/composables/WorkflowSettingsModal.ts create mode 100644 packages/testing/playwright/tests/ui/53-workflow-production-checklist.spec.ts diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index f4237c8adc..c5d939f6b6 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -260,6 +260,57 @@ Cypress.Commands.add('resetDatabase', () => { }); }); +Cypress.Commands.add('clearIndexedDB', (dbName: string, storeName?: string) => { + cy.window().then((win) => { + return new Promise((resolve, reject) => { + if (!win.indexedDB) { + resolve(); + return; + } + + // If storeName is provided, clear specific store; otherwise delete entire database + if (storeName) { + const openRequest = win.indexedDB.open(dbName); + + openRequest.onsuccess = () => { + const db = openRequest.result; + + if (!db.objectStoreNames.contains(storeName)) { + db.close(); + resolve(); + return; + } + + const transaction = db.transaction([storeName], 'readwrite'); + const store = transaction.objectStore(storeName); + const clearRequest = store.clear(); + + clearRequest.onsuccess = () => { + db.close(); + resolve(); + }; + + clearRequest.onerror = () => { + db.close(); + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + reject(clearRequest.error); + }; + }; + + openRequest.onerror = () => { + resolve(); // Database doesn't exist, nothing to clear + }; + } else { + const deleteRequest = win.indexedDB.deleteDatabase(dbName); + + deleteRequest.onsuccess = () => resolve(); + deleteRequest.onerror = () => resolve(); // Ignore errors if DB doesn't exist + deleteRequest.onblocked = () => resolve(); // Ignore if blocked + } + }); + }); +}); + Cypress.Commands.add('interceptNewTab', () => { cy.window().then((win) => { cy.stub(win, 'open').as('windowOpen'); diff --git a/cypress/support/index.ts b/cypress/support/index.ts index 784e8b6001..0c428851d0 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -90,6 +90,7 @@ declare global { } >; resetDatabase(): void; + clearIndexedDB(dbName: string, storeName?: string): Chainable; setAppDate(targetDate: number | Date): void; interceptNewTab(): Chainable; visitInterceptedTab(): Chainable; diff --git a/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.scss b/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.scss index 42d0de7341..e83cac774e 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.scss +++ b/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.scss @@ -145,6 +145,33 @@ --button-loading-background-color: var(--color-button-secondary-loading-background); } +@mixin n8n-button-highlight { + --button-font-color: var(--color-button-highlight-font); + --button-border-color: var(--color-button-highlight-border); + --button-background-color: var(--color-button-highlight-background); + + --button-hover-font-color: var(--color-button-highlight-hover-active-focus-font); + --button-hover-border-color: var(--color-button-highlight-hover-active-focus-border); + --button-hover-background-color: var(--color-button-highlight-hover-background); + + --button-active-font-color: var(--color-button-highlight-hover-active-focus-font); + --button-active-border-color: var(--color-button-highlight-hover-active-focus-border); + --button-active-background-color: var(--color-button-highlight-active-focus-background); + + --button-focus-font-color: var(--color-button-highlight-hover-active-focus-font); + --button-focus-border-color: var(--color-button-highlight-hover-active-focus-border); + --button-focus-background-color: var(--color-button-highlight-active-focus-background); + --button-focus-outline-color: var(--color-button-highlight-focus-outline); + + --button-disabled-font-color: var(--color-button-highlight-disabled-font); + --button-disabled-border-color: var(--color-button-highlight-disabled-border); + --button-disabled-background-color: var(--color-button-highlight-disabled-background); + + --button-loading-font-color: var(--color-button-highlight-loading-font); + --button-loading-border-color: var(--color-button-highlight-loading-border); + --button-loading-background-color: var(--color-button-highlight-loading-background); +} + @mixin n8n-button-success { --button-font-color: var(--color-button-success-font); --button-border-color: var(--color-success); diff --git a/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.stories.ts b/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.stories.ts index 922116b225..bea19f4592 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.stories.ts +++ b/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.stories.ts @@ -9,7 +9,7 @@ export default { argTypes: { type: { control: 'select', - options: ['primary', 'secondary', 'tertiary', 'success', 'warning', 'danger'], + options: ['primary', 'secondary', 'tertiary', 'success', 'warning', 'danger', 'highlight'], }, size: { control: { @@ -78,6 +78,7 @@ const AllColorsAndSizesTemplate: StoryFn = (args, { argTypes }) => ({ +

@@ -86,6 +87,7 @@ const AllColorsAndSizesTemplate: StoryFn = (args, { argTypes }) => ({ +

@@ -94,6 +96,7 @@ const AllColorsAndSizesTemplate: StoryFn = (args, { argTypes }) => ({ + `, methods, }); @@ -152,6 +155,12 @@ WithIcon.args = { icon: 'circle-plus', }; +export const Highlight = AllSizesTemplate.bind({}); +Highlight.args = { + type: 'highlight', + label: 'Button', +}; + export const Square = AllColorsAndSizesTemplate.bind({}); Square.args = { label: '48', diff --git a/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.test.ts b/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.test.ts index 80df349f79..6007f8cb2b 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.test.ts +++ b/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.test.ts @@ -64,6 +64,23 @@ describe('components', () => { expect(wrapper.html()).toMatchSnapshot(); }); }); + + describe('type', () => { + it('should render highlight button', () => { + const wrapper = render(N8nButton, { + props: { + type: 'highlight', + }, + slots, + global: { + stubs, + }, + }); + const button = wrapper.container.querySelector('button'); + expect(button?.className).toContain('highlight'); + expect(wrapper.html()).toMatchSnapshot(); + }); + }); }); }); }); diff --git a/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.vue b/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.vue index 73f094aa88..4f6176d16b 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.vue +++ b/packages/frontend/@n8n/design-system/src/components/N8nButton/Button.vue @@ -120,6 +120,10 @@ $loading-overlay-background-color: rgba(255, 255, 255, 0); @include Button.n8n-button-secondary; } +.highlight { + @include Button.n8n-button-highlight; +} + .tertiary { @include Button.n8n-button-secondary; } diff --git a/packages/frontend/@n8n/design-system/src/components/N8nButton/__snapshots__/Button.test.ts.snap b/packages/frontend/@n8n/design-system/src/components/N8nButton/__snapshots__/Button.test.ts.snap index 5c2f13a3d9..1c0b76ffd8 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nButton/__snapshots__/Button.test.ts.snap +++ b/packages/frontend/@n8n/design-system/src/components/N8nButton/__snapshots__/Button.test.ts.snap @@ -10,6 +10,12 @@ exports[`components > N8nButton > props > square > should render square button 1 " `; +exports[`components > N8nButton > props > type > should render highlight button 1`] = ` +"" +`; + exports[`components > N8nButton > should render correctly 1`] = ` "