From 4240e76253e02da13942d4b84a83ec22fd30aca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milorad=20FIlipovi=C4=87?= Date: Fri, 14 Jul 2023 15:36:17 +0200 Subject: [PATCH] feat(editor): Implement new banners framework (#6603) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ⚑ Implemented new grid row - banners * ✨ Fixing node creator and executions sidebar position after layout update * πŸ’„ Added configurable round corners to the Callout component * ⚑ Fixing mouse position detection and main tab bar position * ⚑ Implemented basic banner component structure * ⚑ Implemented banner state and dismiss logic * ⚑ Fixing grid layout. Updating banners height state dynamically * ⚑ Fix zoom to fit position, mouse position in demo mode and callout vertical alignment * ⚑ Implementing proper trial banners logic * πŸ’„ Only showing execution usage data once the sidebar is fully expanded * ✨ Implemented permanent/temporary dismiss logic for v1 flag * ⚑ Minor refactoring of banner logic * ⚑ Updating permanent dismiss logic to work with all banners * πŸ‘• Fixing linting errors * βœ”οΈ Updating Callout component test snapshots * πŸ’„ Tweaking zoom to fit position * βœ”οΈ Updating testing endpoints to use new store data * βœ… Added banners unit tests * βœ”οΈ Fixing failing banner tests * βœ… Added more banner tests * ⚑ Updating banners dimensions on resize, removing leftover code * βœ”οΈ Removing store import from API file * πŸ‘• Fixing lint errors * ⚑ Updating migration files * ⚑ Using query parameters in migrations * πŸ‘Œ Addressing design review feedback * ⚑ Updating upgrade plan button click * ⚑ Updating the migrations syntax * πŸ‘Œ Updating permanent banner dismiss endpoint and back-end logic * πŸ‘Œ Refactoring trial banner component and ui store * πŸ‘Œ Addressing more points from code review * πŸ‘Œ Moving DOM logic from the store * βœ”οΈ Updated callout component snapshots * πŸ‘Œ Updating mysql migration file * βœ”οΈ Updating e2e test canvas coordinates after setting it's position to absolute * πŸ‘Œ Addressing back-end review feedback * πŸ‘Œ Improving typing around banners * πŸ‘• Fixing lint errors --- cypress/e2e/10-undo-redo.cy.ts | 10 +- cypress/e2e/12-canvas-actions.cy.ts | 2 +- cypress/e2e/12-canvas.cy.ts | 2 +- cypress/e2e/25-stickies.cy.ts | 34 ++-- cypress/support/commands.ts | 2 +- packages/cli/src/Server.ts | 10 +- packages/cli/src/config/types.ts | 2 +- .../cli/src/controllers/e2e.controller.ts | 1 - .../cli/src/controllers/owner.controller.ts | 10 +- .../1646992772331-CreateUserManagement.ts | 2 +- .../1646992772331-CreateUserManagement.ts | 3 +- .../1646992772331-CreateUserManagement.ts | 9 +- .../repositories/settings.repository.ts | 20 ++- packages/cli/src/requests.ts | 3 + .../src/components/N8nCallout/Callout.vue | 37 +++-- .../__snapshots__/Callout.spec.ts.snap | 26 +-- packages/editor-ui/src/App.vue | 44 ++++- packages/editor-ui/src/Interface.ts | 42 ++++- .../__tests__/server/endpoints/settings.ts | 4 +- packages/editor-ui/src/api/ui.ts | 8 +- .../src/components/ActivationModal.vue | 2 +- .../src/components/BreakpointsObserver.vue | 6 + .../src/components/CanvasControls.vue | 2 +- .../src/components/MainHeader/MainHeader.vue | 1 + .../src/components/MainHeader/TabBar.vue | 6 +- .../editor-ui/src/components/MainSidebar.vue | 2 +- .../src/components/Node/NodeCreation.vue | 6 +- .../Node/NodeCreator/NodeCreator.vue | 7 + .../src/components/SettingsSidebar.vue | 2 +- .../editor-ui/src/components/V1Banner.vue | 76 --------- .../components/__tests__/BannersStack.test.ts | 151 ++++++++++++++++++ .../MainSidebarSourceControl.test.ts | 4 +- .../src/components/banners/BannerStack.vue | 36 +++++ .../src/components/banners/BaseBanner.vue | 64 ++++++++ .../src/components/banners/TrialBanner.vue | 36 +++++ .../components/banners/TrialOverBanner.vue | 22 +++ .../src/components/banners/V1Banner.vue | 38 +++++ .../src/composables/useCanvasMouseSelect.ts | 6 +- packages/editor-ui/src/main.ts | 4 +- .../editor-ui/src/n8n-theme-variables.scss | 7 +- .../src/plugins/i18n/locales/en.json | 7 +- .../editor-ui/src/stores/settings.store.ts | 14 +- packages/editor-ui/src/stores/ui.store.ts | 65 +++++--- packages/editor-ui/src/utils/htmlUtils.ts | 8 + packages/editor-ui/src/utils/nodeViewUtils.ts | 7 +- packages/editor-ui/src/views/NodeView.vue | 2 +- packages/workflow/src/Interfaces.ts | 6 +- 47 files changed, 637 insertions(+), 221 deletions(-) delete mode 100644 packages/editor-ui/src/components/V1Banner.vue create mode 100644 packages/editor-ui/src/components/__tests__/BannersStack.test.ts create mode 100644 packages/editor-ui/src/components/banners/BannerStack.vue create mode 100644 packages/editor-ui/src/components/banners/BaseBanner.vue create mode 100644 packages/editor-ui/src/components/banners/TrialBanner.vue create mode 100644 packages/editor-ui/src/components/banners/TrialOverBanner.vue create mode 100644 packages/editor-ui/src/components/banners/V1Banner.vue diff --git a/cypress/e2e/10-undo-redo.cy.ts b/cypress/e2e/10-undo-redo.cy.ts index b197196726..059777e3b9 100644 --- a/cypress/e2e/10-undo-redo.cy.ts +++ b/cypress/e2e/10-undo-redo.cy.ts @@ -121,17 +121,17 @@ describe('Undo/Redo', () => { WorkflowPage.getters .canvasNodes() .last() - .should('have.attr', 'style', 'left: 740px; top: 360px;'); + .should('have.attr', 'style', 'left: 740px; top: 320px;'); WorkflowPage.actions.hitUndo(); WorkflowPage.getters .canvasNodes() .last() - .should('have.attr', 'style', 'left: 640px; top: 260px;'); + .should('have.attr', 'style', 'left: 640px; top: 220px;'); WorkflowPage.actions.hitRedo(); WorkflowPage.getters .canvasNodes() .last() - .should('have.attr', 'style', 'left: 740px; top: 360px;'); + .should('have.attr', 'style', 'left: 740px; top: 320px;'); }); it('should undo/redo deleting a connection by pressing delete button', () => { @@ -281,7 +281,7 @@ describe('Undo/Redo', () => { WorkflowPage.getters .canvasNodes() .first() - .should('have.attr', 'style', 'left: 420px; top: 260px;'); + .should('have.attr', 'style', 'left: 420px; top: 220px;'); // Third undo: Should enable last node WorkflowPage.actions.hitUndo(); WorkflowPage.getters.disabledNodes().should('have.length', 0); @@ -294,7 +294,7 @@ describe('Undo/Redo', () => { WorkflowPage.getters .canvasNodes() .first() - .should('have.attr', 'style', 'left: 540px; top: 400px;'); + .should('have.attr', 'style', 'left: 540px; top: 360px;'); // Third redo: Should delete the Set node WorkflowPage.actions.hitRedo(); WorkflowPage.getters.canvasNodes().should('have.length', 3); diff --git a/cypress/e2e/12-canvas-actions.cy.ts b/cypress/e2e/12-canvas-actions.cy.ts index 0ea993f906..d336294f48 100644 --- a/cypress/e2e/12-canvas-actions.cy.ts +++ b/cypress/e2e/12-canvas-actions.cy.ts @@ -99,7 +99,7 @@ describe('Canvas Actions', () => { WorkflowPage.getters .canvasNodes() .last() - .should('have.attr', 'style', 'left: 860px; top: 260px;'); + .should('have.attr', 'style', 'left: 860px; top: 220px;'); }); it('should delete connections by pressing the delete button', () => { diff --git a/cypress/e2e/12-canvas.cy.ts b/cypress/e2e/12-canvas.cy.ts index ced589b33f..625b8b98f8 100644 --- a/cypress/e2e/12-canvas.cy.ts +++ b/cypress/e2e/12-canvas.cy.ts @@ -162,7 +162,7 @@ describe('Canvas Node Manipulation and Navigation', () => { WorkflowPage.getters .canvasNodes() .last() - .should('have.attr', 'style', 'left: 740px; top: 360px;'); + .should('have.attr', 'style', 'left: 740px; top: 320px;'); }); it('should zoom in', () => { diff --git a/cypress/e2e/25-stickies.cy.ts b/cypress/e2e/25-stickies.cy.ts index ad0fe60c35..afb2db3b5f 100644 --- a/cypress/e2e/25-stickies.cy.ts +++ b/cypress/e2e/25-stickies.cy.ts @@ -90,66 +90,66 @@ describe('Canvas Actions', () => { moveSticky({ left: 600, top: 200 }); cy.drag('[data-test-id="sticky"] [data-dir="left"]', [100, 100]); - checkStickiesStyle(140, 510, 160, 150); + checkStickiesStyle(100, 510, 160, 150); cy.drag('[data-test-id="sticky"] [data-dir="left"]', [-50, -50]); - checkStickiesStyle(140, 466, 160, 194); + checkStickiesStyle(100, 466, 160, 194); }); it('expands/shrinks sticky from the top edge', () => { workflowPage.actions.addSticky(); cy.drag('[data-test-id="sticky"]', [100, 100]); // move away from canvas button - checkStickiesStyle(360, 620, 160, 240); + checkStickiesStyle(300, 620, 160, 240); cy.drag('[data-test-id="sticky"] [data-dir="top"]', [100, 100]); - checkStickiesStyle(440, 620, 80, 240); + checkStickiesStyle(380, 620, 80, 240); cy.drag('[data-test-id="sticky"] [data-dir="top"]', [-50, -50]); - checkStickiesStyle(384, 620, 136, 240); + checkStickiesStyle(324, 620, 136, 240); }); it('expands/shrinks sticky from the bottom edge', () => { workflowPage.actions.addSticky(); cy.drag('[data-test-id="sticky"]', [100, 100]); // move away from canvas button - checkStickiesStyle(360, 620, 160, 240); + checkStickiesStyle(300, 620, 160, 240); cy.drag('[data-test-id="sticky"] [data-dir="bottom"]', [100, 100]); - checkStickiesStyle(360, 620, 254, 240); + checkStickiesStyle(300, 620, 254, 240); cy.drag('[data-test-id="sticky"] [data-dir="bottom"]', [-50, -50]); - checkStickiesStyle(360, 620, 198, 240); + checkStickiesStyle(300, 620, 198, 240); }); it('expands/shrinks sticky from the bottom right edge', () => { workflowPage.actions.addSticky(); cy.drag('[data-test-id="sticky"]', [-100, -100]); // move away from canvas button - checkStickiesStyle(160, 420, 160, 240); + checkStickiesStyle(100, 420, 160, 240); cy.drag('[data-test-id="sticky"] [data-dir="bottomRight"]', [100, 100]); - checkStickiesStyle(160, 420, 254, 346); + checkStickiesStyle(100, 420, 254, 346); cy.drag('[data-test-id="sticky"] [data-dir="bottomRight"]', [-50, -50]); - checkStickiesStyle(160, 420, 198, 302); + checkStickiesStyle(100, 420, 198, 302); }); it('expands/shrinks sticky from the top right edge', () => { addDefaultSticky(); cy.drag('[data-test-id="sticky"] [data-dir="topRight"]', [100, 100]); - checkStickiesStyle(420, 400, 80, 346); + checkStickiesStyle(360, 400, 80, 346); cy.drag('[data-test-id="sticky"] [data-dir="topRight"]', [-50, -50]); - checkStickiesStyle(364, 400, 136, 302); + checkStickiesStyle(304, 400, 136, 302); }); it('expands/shrinks sticky from the top left edge, and reach min height/width', () => { addDefaultSticky(); cy.drag('[data-test-id="sticky"] [data-dir="topLeft"]', [100, 100]); - checkStickiesStyle(420, 490, 80, 150); + checkStickiesStyle(360, 490, 80, 150); cy.drag('[data-test-id="sticky"] [data-dir="topLeft"]', [-150, -150]); - checkStickiesStyle(264, 346, 236, 294); + checkStickiesStyle(204, 346, 236, 294); }); it('sets sticky behind node', () => { @@ -157,7 +157,7 @@ describe('Canvas Actions', () => { addDefaultSticky(); cy.drag('[data-test-id="sticky"] [data-dir="topLeft"]', [-150, -150]); - checkStickiesStyle(184, 256, 316, 384, -121); + checkStickiesStyle(124, 256, 316, 384, -121); workflowPage.getters.canvasNodes().eq(0) .should(($el) => { @@ -235,7 +235,7 @@ function addDefaultSticky() { } function stickyShouldBePositionedCorrectly(position: Position) { - const yOffset = -60; + const yOffset = -100; const xOffset = -180; workflowPage.getters.stickies() .should(($el) => { diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index bfd00b0718..505022934a 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -101,7 +101,7 @@ Cypress.Commands.add('drag', (selector, pos, options) => { const originalLocation = Cypress.$(selector)[index].getBoundingClientRect(); - element.trigger('mousedown'); + element.trigger('mousedown', { force: true }); element.trigger('mousemove', { which: 1, pageX: options?.abs ? xDiff : originalLocation.right + xDiff, diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 796be00d85..32083baef2 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -319,9 +319,7 @@ export class Server extends AbstractServer { limit: 0, }, banners: { - v1: { - dismissed: false, - }, + dismissed: [], }, }; } @@ -415,15 +413,15 @@ export class Server extends AbstractServer { config.getEnv('deployment.type').startsWith('desktop_') === false, }); - let v1Dismissed = false; + let dismissedBanners: string[] = []; try { - v1Dismissed = config.getEnv('ui.banners.v1.dismissed'); + dismissedBanners = config.getEnv('ui.banners.dismissed') ?? []; } catch { // not yet in DB } - this.frontendSettings.banners.v1.dismissed = v1Dismissed; + this.frontendSettings.banners.dismissed = dismissedBanners; // refresh enterprise status Object.assign(this.frontendSettings.enterprise, { diff --git a/packages/cli/src/config/types.ts b/packages/cli/src/config/types.ts index 7c110cd3d9..28dee1e73f 100644 --- a/packages/cli/src/config/types.ts +++ b/packages/cli/src/config/types.ts @@ -80,7 +80,7 @@ type ExceptionPaths = { 'nodes.exclude': string[] | undefined; 'nodes.include': string[] | undefined; 'userManagement.isInstanceOwnerSetUp': boolean; - 'ui.banners.v1.dismissed': boolean; + 'ui.banners.dismissed': string[] | undefined; }; // ----------------------------------- diff --git a/packages/cli/src/controllers/e2e.controller.ts b/packages/cli/src/controllers/e2e.controller.ts index 573b722e1b..3e0a5fae3b 100644 --- a/packages/cli/src/controllers/e2e.controller.ts +++ b/packages/cli/src/controllers/e2e.controller.ts @@ -76,7 +76,6 @@ export class E2EController { @Post('/reset') async reset(req: ResetRequest) { - config.set('ui.banners.v1.dismissed', true); this.resetFeatures(); await this.resetLogStreaming(); await this.removeActiveWorkflows(); diff --git a/packages/cli/src/controllers/owner.controller.ts b/packages/cli/src/controllers/owner.controller.ts index 8b81ec8401..4875e10ec3 100644 --- a/packages/cli/src/controllers/owner.controller.ts +++ b/packages/cli/src/controllers/owner.controller.ts @@ -125,10 +125,10 @@ export class OwnerController { return sanitizeUser(owner); } - @Post('/dismiss-v1') - async dismissBanner() { - await this.settingsRepository.saveSetting('ui.banners.v1.dismissed', JSON.stringify(true)); - - return { success: true }; + @Post('/dismiss-banner') + async dismissBanner(req: OwnerRequest.DismissBanner) { + const bannerName = 'banner' in req.body ? (req.body.banner as string) : ''; + const response = await this.settingsRepository.dismissBanner({ bannerName }); + return response; } } diff --git a/packages/cli/src/databases/migrations/mysqldb/1646992772331-CreateUserManagement.ts b/packages/cli/src/databases/migrations/mysqldb/1646992772331-CreateUserManagement.ts index e21ded1aec..4a7fabd312 100644 --- a/packages/cli/src/databases/migrations/mysqldb/1646992772331-CreateUserManagement.ts +++ b/packages/cli/src/databases/migrations/mysqldb/1646992772331-CreateUserManagement.ts @@ -156,7 +156,7 @@ export class CreateUserManagement1646992772331 implements ReversibleMigration { ); await queryRunner.query( - `INSERT INTO ${tablePrefix}settings (\`key\`, value, loadOnStartup) VALUES ("ui.banners.v1.dismissed", "true", 1)`, + `INSERT INTO ${tablePrefix}settings (\`key\`, value, loadOnStartup) VALUES ("ui.banners.dismissed", "[\"V1\"]", 1)`, ); } diff --git a/packages/cli/src/databases/migrations/postgresdb/1646992772331-CreateUserManagement.ts b/packages/cli/src/databases/migrations/postgresdb/1646992772331-CreateUserManagement.ts index 1d597944c3..98547f3564 100644 --- a/packages/cli/src/databases/migrations/postgresdb/1646992772331-CreateUserManagement.ts +++ b/packages/cli/src/databases/migrations/postgresdb/1646992772331-CreateUserManagement.ts @@ -135,7 +135,8 @@ export class CreateUserManagement1646992772331 implements ReversibleMigration { ); await queryRunner.query( - `INSERT INTO ${tablePrefix}settings ("key", "value", "loadOnStartup") VALUES ('ui.banners.v1.dismissed', 'true', true)`, + `INSERT INTO ${tablePrefix}settings ("key", "value", "loadOnStartup") VALUES ($1, $2, $3)`, + ['ui.banners.dismissed', '["V1"]', true], ); } diff --git a/packages/cli/src/databases/migrations/sqlite/1646992772331-CreateUserManagement.ts b/packages/cli/src/databases/migrations/sqlite/1646992772331-CreateUserManagement.ts index 6a9964fa03..298db01c34 100644 --- a/packages/cli/src/databases/migrations/sqlite/1646992772331-CreateUserManagement.ts +++ b/packages/cli/src/databases/migrations/sqlite/1646992772331-CreateUserManagement.ts @@ -95,10 +95,13 @@ export class CreateUserManagement1646992772331 implements ReversibleMigration { ('userManagement.isInstanceOwnerSetUp', 'false', true), ('userManagement.skipInstanceOwnerSetup', 'false', true) `); - await queryRunner.query(` + await queryRunner.query( + ` INSERT INTO "${tablePrefix}settings" (key, value, loadOnStartup) - VALUES ('ui.banners.v1.dismissed', 'true', true) - `); + VALUES (?, ?, ?) + `, + ['ui.banners.dismissed', '["V1"]', true], + ); } async down({ queryRunner, tablePrefix }: MigrationContext) { diff --git a/packages/cli/src/databases/repositories/settings.repository.ts b/packages/cli/src/databases/repositories/settings.repository.ts index d4fe68e15c..b7a802c2f4 100644 --- a/packages/cli/src/databases/repositories/settings.repository.ts +++ b/packages/cli/src/databases/repositories/settings.repository.ts @@ -9,6 +9,24 @@ export class SettingsRepository extends Repository { super(Settings, dataSource.manager); } + async dismissBanner({ bannerName }: { bannerName: string }): Promise<{ success: boolean }> { + const dismissedBannersSetting = await this.findOneBy({ key: 'ui.banners.dismissed' }); + + if (dismissedBannersSetting) { + try { + const dismissedBanners = JSON.parse(dismissedBannersSetting.value) as string[]; + await this.saveSetting( + 'ui.banners.dismissed', + JSON.stringify([...dismissedBanners, bannerName]), + ); + return { success: true }; + } catch (error) { + return { success: false }; + } + } + return { success: false }; + } + async saveSetting(key: string, value: string, loadOnStartup = true) { const setting = await this.findOneBy({ key }); @@ -18,6 +36,6 @@ export class SettingsRepository extends Repository { await this.save({ key, value, loadOnStartup }); } - if (loadOnStartup) config.set('ui.banners.v1.dismissed', true); + if (loadOnStartup) config.set('ui.banners.dismissed', value); } } diff --git a/packages/cli/src/requests.ts b/packages/cli/src/requests.ts index 5b4bf5d51b..f1c0636557 100644 --- a/packages/cli/src/requests.ts +++ b/packages/cli/src/requests.ts @@ -1,5 +1,6 @@ import type express from 'express'; import type { + Banners, IConnections, ICredentialDataDecryptedObject, ICredentialNodeAccess, @@ -194,6 +195,8 @@ export interface UserSetupPayload { export declare namespace OwnerRequest { type Post = AuthenticatedRequest<{}, {}, UserSetupPayload, {}>; + + type DismissBanner = AuthenticatedRequest<{}, {}, Partial<{ bannerName: Banners }>, {}>; } // ---------------------------------- diff --git a/packages/design-system/src/components/N8nCallout/Callout.vue b/packages/design-system/src/components/N8nCallout/Callout.vue index fae3c92e6c..53545563ab 100644 --- a/packages/design-system/src/components/N8nCallout/Callout.vue +++ b/packages/design-system/src/components/N8nCallout/Callout.vue @@ -2,7 +2,7 @@
- +
@@ -42,7 +42,10 @@ export default defineComponent({ }, icon: { type: String, - default: 'info-circle', + }, + iconSize: { + type: String, + default: 'medium', }, iconless: { type: Boolean, @@ -50,9 +53,9 @@ export default defineComponent({ slim: { type: Boolean, }, - overrideIcon: { + roundCorners: { type: Boolean, - default: false, + default: true, }, }, computed: { @@ -62,16 +65,20 @@ export default defineComponent({ this.$style.callout, this.$style[this.theme], this.slim ? this.$style.slim : '', + this.roundCorners ? this.$style.round : '', ]; }, getIcon(): string { - if (this.overrideIcon) return this.icon; - - if (Object.keys(CALLOUT_DEFAULT_ICONS).includes(this.theme)) { - return CALLOUT_DEFAULT_ICONS[this.theme]; + return this.icon ?? CALLOUT_DEFAULT_ICONS?.[this.theme] ?? CALLOUT_DEFAULT_ICONS.info; + }, + getIconSize(): string { + if (this.iconSize) { + return this.iconSize; } - - return this.icon; + if (this.theme === 'secondary') { + return 'medium'; + } + return 'large'; }, }, }); @@ -84,7 +91,6 @@ export default defineComponent({ font-size: var(--font-size-2xs); padding: var(--spacing-xs); border: var(--border-width-base) var(--border-style-base); - border-radius: var(--border-radius-base); align-items: center; line-height: var(--font-line-height-loose); @@ -94,6 +100,10 @@ export default defineComponent({ } } +.round { + border-radius: var(--border-radius-base); +} + .messageSection { display: flex; align-items: center; @@ -102,7 +112,7 @@ export default defineComponent({ .info, .custom { border-color: var(--color-foreground-base); - background-color: var(--color-background-light); + background-color: var(--color-foreground-xlight); color: var(--color-info); } @@ -125,7 +135,8 @@ export default defineComponent({ } .icon { - margin-right: var(--spacing-xs); + line-height: 1; + margin-right: var(--spacing-2xs); } .secondary { diff --git a/packages/design-system/src/components/N8nCallout/__tests__/__snapshots__/Callout.spec.ts.snap b/packages/design-system/src/components/N8nCallout/__tests__/__snapshots__/Callout.spec.ts.snap index 6141319c81..0a58f816b2 100644 --- a/packages/design-system/src/components/N8nCallout/__tests__/__snapshots__/Callout.spec.ts.snap +++ b/packages/design-system/src/components/N8nCallout/__tests__/__snapshots__/Callout.spec.ts.snap @@ -1,10 +1,10 @@ // Vitest Snapshot v1 exports[`components > N8nCallout > should render additional slots correctly 1`] = ` -"
+"
- +
This is a secondary callout. @@ -15,10 +15,10 @@ exports[`components > N8nCallout > should render additional slots correctly 1`] `; exports[`components > N8nCallout > should render custom theme correctly 1`] = ` -"
+"
- +
This is a secondary callout. @@ -28,10 +28,10 @@ exports[`components > N8nCallout > should render custom theme correctly 1`] = ` `; exports[`components > N8nCallout > should render danger theme correctly 1`] = ` -"
+"
- +
This is a danger callout. @@ -41,10 +41,10 @@ exports[`components > N8nCallout > should render danger theme correctly 1`] = ` `; exports[`components > N8nCallout > should render info theme correctly 1`] = ` -"
+"
- +
This is an info callout. @@ -54,7 +54,7 @@ exports[`components > N8nCallout > should render info theme correctly 1`] = ` `; exports[`components > N8nCallout > should render secondary theme correctly 1`] = ` -"
+"
@@ -67,10 +67,10 @@ exports[`components > N8nCallout > should render secondary theme correctly 1`] = `; exports[`components > N8nCallout > should render success theme correctly 1`] = ` -"
+"
- +
This is a success callout. @@ -80,10 +80,10 @@ exports[`components > N8nCallout > should render success theme correctly 1`] = ` `; exports[`components > N8nCallout > should render warning theme correctly 1`] = ` -"
+"
- +
This is a warning callout. diff --git a/packages/editor-ui/src/App.vue b/packages/editor-ui/src/App.vue index 84d5e9f4d3..e5ed5fa0ed 100644 --- a/packages/editor-ui/src/App.vue +++ b/packages/editor-ui/src/App.vue @@ -9,7 +9,9 @@ [$style.sidebarCollapsed]: uiStore.sidebarMenuCollapsed, }" > - +
+ +
@@ -31,7 +33,7 @@ import { defineComponent } from 'vue'; import { mapStores } from 'pinia'; -import V1Banner from '@/components/V1Banner.vue'; +import BannerStack from '@/components/banners/BannerStack.vue'; import Modals from '@/components/Modals.vue'; import LoadingView from '@/views/LoadingView.vue'; import Telemetry from '@/components/Telemetry.vue'; @@ -59,10 +61,10 @@ import { useExternalHooks } from '@/composables'; export default defineComponent({ name: 'App', components: { + BannerStack, LoadingView, Telemetry, Modals, - V1Banner, }, mixins: [newVersions, userHelpers], setup(props) { @@ -89,6 +91,9 @@ export default defineComponent({ defaultLocale(): string { return this.rootStore.defaultLocale; }, + isDemoMode(): boolean { + return this.$route.name === VIEWS.DEMO; + }, }, data() { return { @@ -126,7 +131,7 @@ export default defineComponent({ } catch (e) {} }, logHiringBanner() { - if (this.settingsStore.isHiringBannerEnabled && this.$route.name !== VIEWS.DEMO) { + if (this.settingsStore.isHiringBannerEnabled && !this.isDemoMode) { console.log(HIRING_BANNER); // eslint-disable-line no-console } }, @@ -216,6 +221,16 @@ export default defineComponent({ } catch {} }, CLOUD_TRIAL_CHECK_INTERVAL); }, + async initBanners(): Promise { + if (this.cloudPlanStore.userIsTrialing) { + await this.uiStore.dismissBanner('V1', 'temporary'); + if (this.cloudPlanStore.trialExpired) { + this.uiStore.showBanner('TRIAL_OVER'); + } else { + this.uiStore.showBanner('TRIAL'); + } + } + }, async postAuthenticate() { if (this.postAuthenticateDone) { return; @@ -239,6 +254,12 @@ export default defineComponent({ this.authenticate(); this.redirectIfNecessary(); void this.checkForNewVersions(); + await this.checkForCloudPlanData(); + await this.initBanners(); + + if (this.sourceControlStore.isEnterpriseSourceControlEnabled) { + await this.sourceControlStore.getPreferences(); + } void this.checkForCloudPlanData(); void this.postAuthenticate(); @@ -279,17 +300,24 @@ export default defineComponent({ .container { display: grid; grid-template-areas: + 'banners banners' 'sidebar header' 'sidebar content'; grid-auto-columns: fit-content($sidebar-expanded-width) 1fr; - grid-template-rows: fit-content($sidebar-width) 1fr; + grid-template-rows: auto fit-content($header-height) 1fr; + height: 100vh; +} + +.banners { + grid-area: banners; + z-index: 999; } .content { display: flex; grid-area: content; overflow: auto; - height: 100vh; + height: 100%; width: 100%; justify-content: center; } @@ -301,7 +329,7 @@ export default defineComponent({ .sidebar { grid-area: sidebar; - height: 100vh; - z-index: 99; + height: 100%; + z-index: 999; } diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index f44d937e9d..7e0d39a1f3 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -34,6 +34,7 @@ import type { IUserManagementSettings, WorkflowSettings, IUserSettings, + Banners, } from 'n8n-workflow'; import type { SignInType } from './constants'; import type { @@ -1045,12 +1046,6 @@ export interface UIState { activeActions: string[]; activeCredentialType: string | null; sidebarMenuCollapsed: boolean; - banners: { - v1: { - dismissed: boolean; - mode: 'temporary' | 'permanent'; - }; - }; modalStack: string[]; modals: Modals; isPageLoading: boolean; @@ -1074,7 +1069,10 @@ export interface UIState { nodeViewInitialized: boolean; addFirstStepOnLoad: boolean; executionSidebarAutoRefresh: boolean; + bannersHeight: number; + banners: { [key in Banners]: { dismissed: boolean; type?: 'temporary' | 'permanent' } }; } + export type IFakeDoor = { id: FAKE_DOOR_FEATURES; featureName: string; @@ -1528,3 +1526,35 @@ export interface InstanceUsage { } export type CloudPlanAndUsageData = Cloud.PlanData & { usage: InstanceUsage }; + +export type CloudUpdateLinkSourceType = + | 'canvas-nav' + | 'custom-data-filter' + | 'workflow_sharing' + | 'credential_sharing' + | 'settings-n8n-api' + | 'audit-logs' + | 'ldap' + | 'log-streaming' + | 'source-control' + | 'sso' + | 'usage_page' + | 'settings-users' + | 'variables'; + +export type UTMCampaign = + | 'upgrade-custom-data-filter' + | 'upgrade-canvas-nav' + | 'upgrade-workflow-sharing' + | 'upgrade-canvas-nav' + | 'upgrade-credentials-sharing' + | 'upgrade-workflow-sharing' + | 'upgrade-api' + | 'upgrade-audit-logs' + | 'upgrade-ldap' + | 'upgrade-log-streaming' + | 'upgrade-source-control' + | 'upgrade-sso' + | 'open' + | 'upgrade-users' + | 'upgrade-variables'; diff --git a/packages/editor-ui/src/__tests__/server/endpoints/settings.ts b/packages/editor-ui/src/__tests__/server/endpoints/settings.ts index ead6545e7b..c8e17f65c0 100644 --- a/packages/editor-ui/src/__tests__/server/endpoints/settings.ts +++ b/packages/editor-ui/src/__tests__/server/endpoints/settings.ts @@ -75,9 +75,7 @@ const defaultSettings: IN8nUISettings = { type: 'default', }, banners: { - v1: { - dismissed: false, - }, + dismissed: [], }, }; diff --git a/packages/editor-ui/src/api/ui.ts b/packages/editor-ui/src/api/ui.ts index e23e96c0b5..25683ad46e 100644 --- a/packages/editor-ui/src/api/ui.ts +++ b/packages/editor-ui/src/api/ui.ts @@ -1,6 +1,10 @@ import type { IRestApiContext } from '@/Interface'; import { makeRestApiRequest } from '@/utils/apiUtils'; +import type { Banners } from 'n8n-workflow'; -export async function dismissV1BannerPermanently(context: IRestApiContext): Promise { - return makeRestApiRequest(context, 'POST', '/owner/dismiss-v1'); +export async function dismissBannerPermanently( + context: IRestApiContext, + data: { bannerName: Banners; dismissedBanners: string[] }, +): Promise { + return makeRestApiRequest(context, 'POST', '/owner/dismiss-banner', { banner: data.bannerName }); } diff --git a/packages/editor-ui/src/components/ActivationModal.vue b/packages/editor-ui/src/components/ActivationModal.vue index 7090a17dd3..9b97787ad8 100644 --- a/packages/editor-ui/src/components/ActivationModal.vue +++ b/packages/editor-ui/src/components/ActivationModal.vue @@ -26,7 +26,7 @@