mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-10 06:34:05 -08:00
feat(editor): Add v1 banner (#6443)
This commit is contained in:
parent
85372aabdf
commit
0fe415add2
|
@ -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(),
|
||||
|
|
1
packages/cli/src/config/types.d.ts
vendored
1
packages/cli/src/config/types.d.ts
vendored
|
@ -80,6 +80,7 @@ type ExceptionPaths = {
|
|||
'nodes.exclude': string[] | undefined;
|
||||
'nodes.include': string[] | undefined;
|
||||
'userManagement.isInstanceOwnerSetUp': boolean;
|
||||
'ui.banners.v1.dismissed': boolean;
|
||||
};
|
||||
|
||||
// -----------------------------------
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Settings> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
[$style.sidebarCollapsed]: uiStore.sidebarMenuCollapsed,
|
||||
}"
|
||||
>
|
||||
<V1Banner />
|
||||
<div id="header" :class="$style.header">
|
||||
<router-view name="header"></router-view>
|
||||
</div>
|
||||
|
@ -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;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
6
packages/editor-ui/src/api/ui.ts
Normal file
6
packages/editor-ui/src/api/ui.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import type { IRestApiContext } from '@/Interface';
|
||||
import { makeRestApiRequest } from '@/utils/apiUtils';
|
||||
|
||||
export async function dismissV1BannerPermanently(context: IRestApiContext): Promise<void> {
|
||||
return makeRestApiRequest(context, 'POST', '/owner/dismiss-v1');
|
||||
}
|
76
packages/editor-ui/src/components/V1Banner.vue
Normal file
76
packages/editor-ui/src/components/V1Banner.vue
Normal file
|
@ -0,0 +1,76 @@
|
|||
<template>
|
||||
<n8n-callout
|
||||
v-if="shouldDisplay"
|
||||
theme="warning"
|
||||
icon="info-circle"
|
||||
override-icon
|
||||
:class="$style['v1-banner']"
|
||||
>
|
||||
<span v-html="locale.baseText('banners.v1.message')"></span>
|
||||
{{ '' }}
|
||||
<a v-if="isInstanceOwner" @click="dismissBanner('v1', 'permanent')">
|
||||
<span v-html="locale.baseText('banners.v1.action')"></span>
|
||||
</a>
|
||||
<template #trailingContent>
|
||||
<n8n-icon
|
||||
size="small"
|
||||
icon="xmark"
|
||||
:title="locale.baseText('banners.v1.iconTitle')"
|
||||
:class="$style.xmark"
|
||||
@click="dismissBanner('v1', 'temporary')"
|
||||
/>
|
||||
</template>
|
||||
</n8n-callout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { VIEWS } from '@/constants';
|
||||
import { computed } from 'vue';
|
||||
import { useUIStore, useUsersStore, useRootStore } from '@/stores';
|
||||
import { useRoute } from 'vue-router/composables';
|
||||
import { i18n as locale } from '@/plugins/i18n';
|
||||
|
||||
const { isInstanceOwner } = useUsersStore();
|
||||
const { dismissBanner } = useUIStore();
|
||||
|
||||
const shouldDisplay = computed(() => {
|
||||
if (!useRootStore().versionCli.startsWith('1.')) return false;
|
||||
|
||||
if (useUIStore().banners.v1.dismissed) return false;
|
||||
|
||||
const VIEWABLE_AT: string[] = [
|
||||
VIEWS.HOMEPAGE,
|
||||
VIEWS.COLLECTION,
|
||||
VIEWS.TEMPLATE,
|
||||
VIEWS.TEMPLATES,
|
||||
VIEWS.CREDENTIALS,
|
||||
VIEWS.VARIABLES,
|
||||
VIEWS.WORKFLOWS,
|
||||
VIEWS.EXECUTIONS,
|
||||
];
|
||||
|
||||
const { name } = useRoute();
|
||||
|
||||
if (name && VIEWABLE_AT.includes(name)) return true;
|
||||
|
||||
return false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.v1-banner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 999;
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.xmark {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -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 });
|
||||
});
|
||||
|
||||
|
|
|
@ -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 <a target=\"_blank\" href=\"https://docs.n8n.io/1-0-migration-checklist\">migration guide</a> 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",
|
||||
|
|
|
@ -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',
|
||||
],
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<void> {
|
||||
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<CurlToJSONResponse> {
|
||||
const rootStore = useRootStore();
|
||||
return getCurlToJson(rootStore.getRestApiContext, curlCommand);
|
||||
|
|
|
@ -2146,4 +2146,9 @@ export interface IN8nUISettings {
|
|||
variables: {
|
||||
limit: number;
|
||||
};
|
||||
banners: {
|
||||
v1: {
|
||||
dismissed: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue