From 0fe415add2baa8e70e29087f7a90312bd1ab38af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 19 Jun 2023 16:23:57 +0200 Subject: [PATCH] feat(editor): Add v1 banner (#6443) --- packages/cli/src/Server.ts | 15 ++++ packages/cli/src/config/types.d.ts | 1 + .../cli/src/controllers/owner.controller.ts | 7 ++ .../repositories/settings.repository.ts | 13 ++++ .../src/components/N8nCallout/Callout.vue | 6 ++ packages/editor-ui/src/App.vue | 7 +- packages/editor-ui/src/Interface.ts | 6 ++ .../__tests__/server/endpoints/settings.ts | 6 +- packages/editor-ui/src/api/ui.ts | 6 ++ .../editor-ui/src/components/V1Banner.vue | 76 +++++++++++++++++++ packages/editor-ui/src/main.ts | 4 +- .../src/plugins/i18n/locales/en.json | 3 + .../editor-ui/src/plugins/icons/custom.ts | 12 +++ packages/editor-ui/src/plugins/icons/index.ts | 3 +- .../editor-ui/src/stores/settings.store.ts | 4 + packages/editor-ui/src/stores/ui.store.ts | 29 +++++++ packages/workflow/src/Interfaces.ts | 5 ++ 17 files changed, 198 insertions(+), 5 deletions(-) create mode 100644 packages/editor-ui/src/api/ui.ts create mode 100644 packages/editor-ui/src/components/V1Banner.vue diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index f264a3944c..333f4c1e95 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -316,6 +316,11 @@ export class Server extends AbstractServer { variables: { limit: 0, }, + banners: { + v1: { + dismissed: false, + }, + }, }; } @@ -411,6 +416,16 @@ export class Server extends AbstractServer { config.getEnv('deployment.type').startsWith('desktop_') === false, }); + let v1Dismissed = false; + + try { + v1Dismissed = config.getEnv('ui.banners.v1.dismissed'); + } catch { + // not yet in DB + } + + this.frontendSettings.banners.v1.dismissed = v1Dismissed; + // refresh enterprise status Object.assign(this.frontendSettings.enterprise, { sharing: isSharingEnabled(), diff --git a/packages/cli/src/config/types.d.ts b/packages/cli/src/config/types.d.ts index e733219634..7c110cd3d9 100644 --- a/packages/cli/src/config/types.d.ts +++ b/packages/cli/src/config/types.d.ts @@ -80,6 +80,7 @@ type ExceptionPaths = { 'nodes.exclude': string[] | undefined; 'nodes.include': string[] | undefined; 'userManagement.isInstanceOwnerSetUp': boolean; + 'ui.banners.v1.dismissed': boolean; }; // ----------------------------------- diff --git a/packages/cli/src/controllers/owner.controller.ts b/packages/cli/src/controllers/owner.controller.ts index 217cda5e7a..2f01aef32c 100644 --- a/packages/cli/src/controllers/owner.controller.ts +++ b/packages/cli/src/controllers/owner.controller.ts @@ -148,4 +148,11 @@ 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 }; + } } diff --git a/packages/cli/src/databases/repositories/settings.repository.ts b/packages/cli/src/databases/repositories/settings.repository.ts index d0ae091ce6..d4fe68e15c 100644 --- a/packages/cli/src/databases/repositories/settings.repository.ts +++ b/packages/cli/src/databases/repositories/settings.repository.ts @@ -1,10 +1,23 @@ import { Service } from 'typedi'; import { DataSource, Repository } from 'typeorm'; import { Settings } from '../entities/Settings'; +import config from '@/config'; @Service() export class SettingsRepository extends Repository { constructor(dataSource: DataSource) { super(Settings, dataSource.manager); } + + async saveSetting(key: string, value: string, loadOnStartup = true) { + const setting = await this.findOneBy({ key }); + + if (setting) { + await this.update({ key }, { value, loadOnStartup }); + } else { + await this.save({ key, value, loadOnStartup }); + } + + if (loadOnStartup) config.set('ui.banners.v1.dismissed', true); + } } diff --git a/packages/design-system/src/components/N8nCallout/Callout.vue b/packages/design-system/src/components/N8nCallout/Callout.vue index 604cec6a25..fae3c92e6c 100644 --- a/packages/design-system/src/components/N8nCallout/Callout.vue +++ b/packages/design-system/src/components/N8nCallout/Callout.vue @@ -50,6 +50,10 @@ export default defineComponent({ slim: { type: Boolean, }, + overrideIcon: { + type: Boolean, + default: false, + }, }, computed: { classes(): string[] { @@ -61,6 +65,8 @@ export default defineComponent({ ]; }, getIcon(): string { + if (this.overrideIcon) return this.icon; + if (Object.keys(CALLOUT_DEFAULT_ICONS).includes(this.theme)) { return CALLOUT_DEFAULT_ICONS[this.theme]; } diff --git a/packages/editor-ui/src/App.vue b/packages/editor-ui/src/App.vue index 9770fcd89c..565f51e0ce 100644 --- a/packages/editor-ui/src/App.vue +++ b/packages/editor-ui/src/App.vue @@ -9,6 +9,7 @@ [$style.sidebarCollapsed]: uiStore.sidebarMenuCollapsed, }" > + @@ -30,6 +31,7 @@ import { defineComponent } from 'vue'; import { mapStores } from 'pinia'; +import V1Banner from '@/components/V1Banner.vue'; import Modals from '@/components/Modals.vue'; import LoadingView from '@/views/LoadingView.vue'; import Telemetry from '@/components/Telemetry.vue'; @@ -60,6 +62,7 @@ export default defineComponent({ LoadingView, Telemetry, Modals, + V1Banner, }, mixins: [newVersions, userHelpers], setup(props) { @@ -278,12 +281,12 @@ export default defineComponent({ .header { grid-area: header; - z-index: 999; + z-index: 99; } .sidebar { grid-area: sidebar; height: 100vh; - z-index: 999; + z-index: 99; } diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 859a2ca104..aaa6ae8d4b 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -1044,6 +1044,12 @@ export interface UIState { activeActions: string[]; activeCredentialType: string | null; sidebarMenuCollapsed: boolean; + banners: { + v1: { + dismissed: boolean; + mode: 'temporary' | 'permanent'; + }; + }; modalStack: string[]; modals: Modals; isPageLoading: boolean; diff --git a/packages/editor-ui/src/__tests__/server/endpoints/settings.ts b/packages/editor-ui/src/__tests__/server/endpoints/settings.ts index 4462f6e3e5..ead6545e7b 100644 --- a/packages/editor-ui/src/__tests__/server/endpoints/settings.ts +++ b/packages/editor-ui/src/__tests__/server/endpoints/settings.ts @@ -56,7 +56,6 @@ const defaultSettings: IN8nUISettings = { urlBaseEditor: '', urlBaseWebhook: '', userManagement: { - enabled: true, showSetupOnFirstLoad: true, smtpSetup: true, authenticationMethod: 'email', @@ -75,6 +74,11 @@ const defaultSettings: IN8nUISettings = { deployment: { type: 'default', }, + banners: { + v1: { + dismissed: false, + }, + }, }; export function routesForSettings(server: Server) { diff --git a/packages/editor-ui/src/api/ui.ts b/packages/editor-ui/src/api/ui.ts new file mode 100644 index 0000000000..e23e96c0b5 --- /dev/null +++ b/packages/editor-ui/src/api/ui.ts @@ -0,0 +1,6 @@ +import type { IRestApiContext } from '@/Interface'; +import { makeRestApiRequest } from '@/utils/apiUtils'; + +export async function dismissV1BannerPermanently(context: IRestApiContext): Promise { + return makeRestApiRequest(context, 'POST', '/owner/dismiss-v1'); +} diff --git a/packages/editor-ui/src/components/V1Banner.vue b/packages/editor-ui/src/components/V1Banner.vue new file mode 100644 index 0000000000..7de0fb653a --- /dev/null +++ b/packages/editor-ui/src/components/V1Banner.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/packages/editor-ui/src/main.ts b/packages/editor-ui/src/main.ts index 1f196c3472..afb5b05211 100644 --- a/packages/editor-ui/src/main.ts +++ b/packages/editor-ui/src/main.ts @@ -24,7 +24,7 @@ import { FontAwesomePlugin } from './plugins/icons'; import { runExternalHook } from '@/utils'; import { createPinia, PiniaVuePlugin } from 'pinia'; -import { useWebhooksStore } from '@/stores'; +import { useWebhooksStore, useUIStore } from '@/stores'; Vue.config.productionTip = false; @@ -46,6 +46,8 @@ new Vue({ }).$mount('#app'); router.afterEach((to, from) => { + useUIStore().restoreBanner('v1'); + void runExternalHook('main.routeChange', useWebhooksStore(), { from, to }); }); diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 0340765193..c8b5ed989d 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -109,6 +109,9 @@ "auth.signup.setupYourAccount": "Set up your account", "auth.signup.setupYourAccountError": "Problem setting up your account", "auth.signup.tokenValidationError": "Issue validating invite token", + "banners.v1.message": "n8n has been updated to version 1, introducing some breaking changes. Please consult the migration guide for more information.", + "banners.v1.action": "Confirm", + "banners.v1.iconTitle": "Dismiss v1 banner", "binaryDataDisplay.backToList": "Back to list", "binaryDataDisplay.backToOverviewPage": "Back to overview page", "binaryDataDisplay.noDataFoundToDisplay": "No data found to display", diff --git a/packages/editor-ui/src/plugins/icons/custom.ts b/packages/editor-ui/src/plugins/icons/custom.ts index 9817abf254..8ce0ffa2dc 100644 --- a/packages/editor-ui/src/plugins/icons/custom.ts +++ b/packages/editor-ui/src/plugins/icons/custom.ts @@ -11,3 +11,15 @@ export const faVariable: IconDefinition = { 'M42.6,17.8c2.4,0,7.2-2,7.2-8.4c0-6.4-4.6-6.8-6.1-6.8c-2.8,0-5.6,2-8.1,6.3c-2.5,4.4-5.3,9.1-5.3,9.1 l-0.1,0c-0.6-3.1-1.1-5.6-1.3-6.7c-0.5-2.7-3.6-8.4-9.9-8.4c-6.4,0-12.2,3.7-12.2,3.7l0,0C5.8,7.3,5.1,8.5,5.1,9.9 c0,2.1,1.7,3.9,3.9,3.9c0.6,0,1.2-0.2,1.7-0.4l0,0c0,0,4.8-2.7,5.9,0c0.3,0.8,0.6,1.7,0.9,2.7c1.2,4.2,2.4,9.1,3.3,13.5l-4.2,6 c0,0-4.7-1.7-7.1-1.7s-7.2,2-7.2,8.4s4.6,6.8,6.1,6.8c2.8,0,5.6-2,8.1-6.3c2.5-4.4,5.3-9.1,5.3-9.1c0.8,4,1.5,7.1,1.9,8.5 c1.6,4.5,5.3,7.2,10.1,7.2c0,0,5,0,10.9-3.3c1.4-0.6,2.4-2,2.4-3.6c0-2.1-1.7-3.9-3.9-3.9c-0.6,0-1.2,0.2-1.7,0.4l0,0 c0,0-4.2,2.4-5.6,0.5c-1-2-1.9-4.6-2.6-7.8c-0.6-2.8-1.3-6.2-2-9.5l4.3-6.2C35.5,16.1,40.2,17.8,42.6,17.8z', ], }; + +export const faXmark: IconDefinition = { + prefix: 'fas' as IconPrefix, + iconName: 'xmark' as IconName, + icon: [ + 400, + 400, + [], + '', + 'M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z', + ], +}; diff --git a/packages/editor-ui/src/plugins/icons/index.ts b/packages/editor-ui/src/plugins/icons/index.ts index beeebae671..a5b6d2b792 100644 --- a/packages/editor-ui/src/plugins/icons/index.ts +++ b/packages/editor-ui/src/plugins/icons/index.ts @@ -134,7 +134,7 @@ import { faUserLock, faGem, } from '@fortawesome/free-solid-svg-icons'; -import { faVariable } from './custom'; +import { faVariable, faXmark } from './custom'; import { faStickyNote } from '@fortawesome/free-regular-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; @@ -277,6 +277,7 @@ export const FontAwesomePlugin: PluginObject<{}> = { addIcon(faTree); addIcon(faUserLock); addIcon(faGem); + addIcon(faXmark); app.component('font-awesome-icon', FontAwesomeIcon); }, diff --git a/packages/editor-ui/src/stores/settings.store.ts b/packages/editor-ui/src/stores/settings.store.ts index 7112af3cb5..c846b7753b 100644 --- a/packages/editor-ui/src/stores/settings.store.ts +++ b/packages/editor-ui/src/stores/settings.store.ts @@ -212,6 +212,10 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, { rootStore.setN8nMetadata(settings.n8nMetadata || {}); rootStore.setDefaultLocale(settings.defaultLocale); rootStore.setIsNpmAvailable(settings.isNpmAvailable); + if (settings.banners.v1.dismissed) { + useUIStore().setBanners({ v1: { dismissed: true, mode: 'permanent' } }); + } + useVersionsStore().setVersionNotificationSettings(settings.versionNotifications); }, stopShowingSetupPage(): void { diff --git a/packages/editor-ui/src/stores/ui.store.ts b/packages/editor-ui/src/stores/ui.store.ts index fa96e4ae22..8f81edd54e 100644 --- a/packages/editor-ui/src/stores/ui.store.ts +++ b/packages/editor-ui/src/stores/ui.store.ts @@ -52,6 +52,7 @@ import type { BaseTextKey } from '@/plugins/i18n'; import { i18n as locale } from '@/plugins/i18n'; import type { Modals, NewCredentialsModal } from '@/Interface'; import { useTelemetryStore } from '@/stores/telemetry.store'; +import { dismissV1BannerPermanently } from '@/api/ui'; export const useUIStore = defineStore(STORES.UI, { state: (): UIState => ({ @@ -139,6 +140,12 @@ export const useUIStore = defineStore(STORES.UI, { }, modalStack: [], sidebarMenuCollapsed: true, + banners: { + v1: { + dismissed: false, + mode: 'temporary', + }, + }, isPageLoading: true, currentView: '', mainPanelPosition: 0.5, @@ -332,6 +339,12 @@ export const useUIStore = defineStore(STORES.UI, { }, }, actions: { + setBanners(banners: UIState['banners']): void { + this.banners = { + ...this.banners, + ...banners, + }; + }, setMode(name: keyof Modals, mode: string): void { this.modals[name] = { ...this.modals[name], @@ -508,6 +521,22 @@ export const useUIStore = defineStore(STORES.UI, { toggleSidebarMenuCollapse(): void { this.sidebarMenuCollapsed = !this.sidebarMenuCollapsed; }, + async dismissBanner(bannerType: 'v1', mode: 'temporary' | 'permanent'): Promise { + if (mode === 'permanent') { + await dismissV1BannerPermanently(useRootStore().getRestApiContext); + this.banners[bannerType].dismissed = true; + this.banners[bannerType].mode = 'permanent'; + return; + } + + this.banners[bannerType].dismissed = true; + this.banners[bannerType].mode = 'temporary'; + }, + restoreBanner(bannerType: 'v1'): void { + if (this.banners[bannerType].dismissed && this.banners[bannerType].mode === 'temporary') { + this.banners[bannerType].dismissed = false; + } + }, async getCurlToJson(curlCommand: string): Promise { const rootStore = useRootStore(); return getCurlToJson(rootStore.getRestApiContext, curlCommand); diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 46238b1e7d..46bb64da61 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -2146,4 +2146,9 @@ export interface IN8nUISettings { variables: { limit: number; }; + banners: { + v1: { + dismissed: boolean; + }; + }; }