feat(editor): Implement new banners framework (#6603)
* ⚡ 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
This commit is contained in:
committed by
GitHub
parent
ff0759530d
commit
4240e76253
@@ -26,7 +26,7 @@
|
||||
<template #footer="{ close }">
|
||||
<div :class="$style.footer">
|
||||
<el-checkbox :value="checked" @change="handleCheckboxChange">{{
|
||||
$locale.baseText('activationModal.dontShowAgain')
|
||||
$locale.baseText('generic.dontShowAgain')
|
||||
}}</el-checkbox>
|
||||
<n8n-button @click="close" :label="$locale.baseText('activationModal.gotIt')" />
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,8 @@ import { BREAKPOINT_SM, BREAKPOINT_MD, BREAKPOINT_LG, BREAKPOINT_XL } from '@/co
|
||||
|
||||
import { genericHelpers } from '@/mixins/genericHelpers';
|
||||
import { debounceHelper } from '@/mixins/debounce';
|
||||
import { useUIStore } from '@/stores';
|
||||
import { getBannerRowHeight } from '@/utils';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BreakpointsObserver',
|
||||
@@ -41,6 +43,10 @@ export default defineComponent({
|
||||
},
|
||||
onResizeEnd() {
|
||||
this.$data.width = window.innerWidth;
|
||||
this.$nextTick(async () => {
|
||||
const bannerHeight = await getBannerRowHeight();
|
||||
useUIStore().updateBannersHeight(bannerHeight);
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -76,7 +76,7 @@ onBeforeUnmount(() => {
|
||||
.zoomMenu {
|
||||
position: absolute;
|
||||
width: 210px;
|
||||
bottom: 108px;
|
||||
bottom: var(--spacing-2xl);
|
||||
left: 35px;
|
||||
line-height: 25px;
|
||||
color: #444;
|
||||
|
||||
@@ -172,6 +172,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.top-menu {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
|
||||
@@ -54,7 +54,7 @@ export default defineComponent({
|
||||
.container {
|
||||
position: absolute;
|
||||
top: 47px;
|
||||
left: calc(50% + 100px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
min-height: 30px;
|
||||
display: flex;
|
||||
@@ -62,10 +62,6 @@ export default defineComponent({
|
||||
background-color: var(--color-foreground-base);
|
||||
border-radius: var(--border-radius-base);
|
||||
transition: all 150ms ease-in-out;
|
||||
|
||||
&.menuCollapsed {
|
||||
left: 52%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 430px) {
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<template #beforeLowerMenu>
|
||||
<ExecutionsUsage
|
||||
:cloud-plan-data="currentPlanAndUsageData"
|
||||
v-if="!isCollapsed && userIsTrialing"
|
||||
v-if="fullyExpanded && userIsTrialing"
|
||||
/></template>
|
||||
<template #menuSuffix>
|
||||
<div>
|
||||
|
||||
@@ -139,7 +139,7 @@ export default defineComponent({
|
||||
|
||||
<style lang="scss" module>
|
||||
.nodeButtonsWrapper {
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
width: 150px;
|
||||
height: 200px;
|
||||
top: 0;
|
||||
@@ -164,9 +164,9 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.nodeCreatorButton {
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: calc(#{$header-height} + var(--spacing-s));
|
||||
top: var(--spacing-s);
|
||||
right: var(--spacing-s);
|
||||
pointer-events: all !important;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<div
|
||||
v-if="active"
|
||||
:class="$style.nodeCreator"
|
||||
:style="nodeCreatorInlineStyle"
|
||||
ref="nodeCreator"
|
||||
v-click-outside="onClickOutside"
|
||||
@dragover="onDragOver"
|
||||
@@ -30,6 +31,7 @@ import { useViewStacks } from './composables/useViewStacks';
|
||||
import { useKeyboardNavigation } from './composables/useKeyboardNavigation';
|
||||
import { useActionsGenerator } from './composables/useActionsGeneration';
|
||||
import NodesListPanel from './Panel/NodesListPanel.vue';
|
||||
import { useUIStore } from '@/stores';
|
||||
|
||||
export interface Props {
|
||||
active?: boolean;
|
||||
@@ -42,6 +44,7 @@ const emit = defineEmits<{
|
||||
(event: 'closeNodeCreator'): void;
|
||||
(event: 'nodeTypeSelected', value: string[]): void;
|
||||
}>();
|
||||
const uiStore = useUIStore();
|
||||
|
||||
const { setShowScrim, setActions, setMergeNodes } = useNodeCreatorStore();
|
||||
const { generateMergedNodesAndActions } = useActionsGenerator();
|
||||
@@ -55,6 +58,10 @@ const showScrim = computed(() => useNodeCreatorStore().showScrim);
|
||||
|
||||
const viewStacksLength = computed(() => useViewStacks().viewStacks.length);
|
||||
|
||||
const nodeCreatorInlineStyle = computed(() => {
|
||||
return { top: `${uiStore.bannersHeight + uiStore.headerHeight}px` };
|
||||
});
|
||||
|
||||
function onClickOutside(event: Event) {
|
||||
// We need to prevent cases where user would click inside the node creator
|
||||
// and try to drag non-draggable element. In that case the click event would
|
||||
|
||||
@@ -247,7 +247,7 @@ export default defineComponent({
|
||||
<style lang="scss" module>
|
||||
.container {
|
||||
min-width: $sidebar-expanded-width;
|
||||
height: 100vh;
|
||||
height: 100%;
|
||||
background-color: var(--color-background-xlight);
|
||||
border-right: var(--border-base);
|
||||
position: relative;
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
<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>
|
||||
151
packages/editor-ui/src/components/__tests__/BannersStack.test.ts
Normal file
151
packages/editor-ui/src/components/__tests__/BannersStack.test.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { PiniaVuePlugin } from 'pinia';
|
||||
import { render, within } from '@testing-library/vue';
|
||||
import { merge } from 'lodash-es';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
|
||||
import { STORES } from '@/constants';
|
||||
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import BannerStack from '@/components/banners/BannerStack.vue';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
|
||||
let uiStore: ReturnType<typeof useUIStore>;
|
||||
let usersStore: ReturnType<typeof useUsersStore>;
|
||||
|
||||
const DEFAULT_SETUP = {
|
||||
pinia: createTestingPinia({
|
||||
initialState: {
|
||||
[STORES.SETTINGS]: {
|
||||
settings: merge({}, SETTINGS_STORE_DEFAULT_STATE.settings),
|
||||
},
|
||||
[STORES.UI]: {
|
||||
banners: {
|
||||
V1: { dismissed: false },
|
||||
TRIAL: { dismissed: false },
|
||||
TRIAL_OVER: { dismissed: false },
|
||||
},
|
||||
},
|
||||
[STORES.USERS]: {
|
||||
currentUserId: 'aaa-bbb',
|
||||
users: {
|
||||
'aaa-bbb': {
|
||||
id: 'aaa-bbb',
|
||||
globalRole: {
|
||||
id: '1',
|
||||
name: 'owner',
|
||||
scope: 'global',
|
||||
},
|
||||
},
|
||||
'bbb-bbb': {
|
||||
id: 'bbb-bbb',
|
||||
globalRoleId: 2,
|
||||
globalRole: {
|
||||
id: '2',
|
||||
name: 'member',
|
||||
scope: 'global',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const renderComponent = (renderOptions: Parameters<typeof render>[1] = {}) =>
|
||||
render(BannerStack, merge(DEFAULT_SETUP, renderOptions), (vue) => {
|
||||
vue.use(PiniaVuePlugin);
|
||||
});
|
||||
|
||||
describe('BannerStack', () => {
|
||||
beforeEach(() => {
|
||||
uiStore = useUIStore();
|
||||
usersStore = useUsersStore();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render default configuration', async () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
|
||||
const bannerStack = getByTestId('banner-stack');
|
||||
expect(bannerStack).toBeInTheDocument();
|
||||
|
||||
expect(within(bannerStack).getByTestId('banners-TRIAL')).toBeInTheDocument();
|
||||
expect(within(bannerStack).getByTestId('banners-TRIAL_OVER')).toBeInTheDocument();
|
||||
expect(within(bannerStack).getByTestId('banners-V1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render dismissed banners', async () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: merge(
|
||||
{
|
||||
[STORES.UI]: {
|
||||
banners: {
|
||||
V1: { dismissed: true },
|
||||
TRIAL: { dismissed: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
DEFAULT_SETUP.pinia,
|
||||
),
|
||||
}),
|
||||
});
|
||||
const bannerStack = getByTestId('banner-stack');
|
||||
expect(bannerStack).toBeInTheDocument();
|
||||
|
||||
expect(within(bannerStack).queryByTestId('banners-V1')).not.toBeInTheDocument();
|
||||
expect(within(bannerStack).queryByTestId('banners-TRIAL')).not.toBeInTheDocument();
|
||||
expect(within(bannerStack).getByTestId('banners-TRIAL_OVER')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should dismiss banner on click', async () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
const dismissBannerSpy = vi
|
||||
.spyOn(useUIStore(), 'dismissBanner')
|
||||
.mockImplementation(async (banner, mode) => {});
|
||||
const closeTrialBannerButton = getByTestId('banner-TRIAL_OVER-close');
|
||||
expect(closeTrialBannerButton).toBeInTheDocument();
|
||||
await userEvent.click(closeTrialBannerButton);
|
||||
expect(dismissBannerSpy).toHaveBeenCalledWith('TRIAL_OVER');
|
||||
});
|
||||
|
||||
it('should permanently dismiss banner on click', async () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: merge(DEFAULT_SETUP.pinia, {
|
||||
[STORES.UI]: {
|
||||
banners: {
|
||||
V1: { dismissed: false },
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
const dismissBannerSpy = vi
|
||||
.spyOn(useUIStore(), 'dismissBanner')
|
||||
.mockImplementation(async (banner, mode) => {});
|
||||
|
||||
const permanentlyDismissBannerLink = getByTestId('banner-confirm-v1');
|
||||
expect(permanentlyDismissBannerLink).toBeInTheDocument();
|
||||
await userEvent.click(permanentlyDismissBannerLink);
|
||||
expect(dismissBannerSpy).toHaveBeenCalledWith('V1', 'permanent');
|
||||
});
|
||||
|
||||
it('should not render permanent dismiss link if user is not owner', async () => {
|
||||
const { queryByTestId } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: merge(DEFAULT_SETUP.pinia, {
|
||||
[STORES.USERS]: {
|
||||
currentUserId: 'bbb-bbb',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
expect(queryByTestId('banner-confirm-v1')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -8,11 +8,10 @@ import { SOURCE_CONTROL_PULL_MODAL_KEY, STORES } from '@/constants';
|
||||
import { i18nInstance } from '@/plugins/i18n';
|
||||
import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
|
||||
import MainSidebarSourceControl from '@/components/MainSidebarSourceControl.vue';
|
||||
import { useUsersStore, useSourceControlStore, useUIStore } from '@/stores';
|
||||
import { useSourceControlStore, useUIStore } from '@/stores';
|
||||
|
||||
let pinia: ReturnType<typeof createTestingPinia>;
|
||||
let sourceControlStore: ReturnType<typeof useSourceControlStore>;
|
||||
let usersStore: ReturnType<typeof useUsersStore>;
|
||||
let uiStore: ReturnType<typeof useUIStore>;
|
||||
|
||||
const renderComponent = (renderOptions: Parameters<typeof render>[1] = {}) => {
|
||||
@@ -44,7 +43,6 @@ describe('MainSidebarSourceControl', () => {
|
||||
|
||||
sourceControlStore = useSourceControlStore();
|
||||
uiStore = useUIStore();
|
||||
usersStore = useUsersStore();
|
||||
});
|
||||
|
||||
it('should render nothing', async () => {
|
||||
|
||||
36
packages/editor-ui/src/components/banners/BannerStack.vue
Normal file
36
packages/editor-ui/src/components/banners/BannerStack.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import TrialOverBanner from '@/components/banners/TrialOverBanner.vue';
|
||||
import TrialBanner from '@/components/banners/TrialBanner.vue';
|
||||
import V1Banner from '@/components/banners/V1Banner.vue';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { onMounted, watch } from 'vue';
|
||||
import { getBannerRowHeight } from '@/utils';
|
||||
import type { Banners } from 'n8n-workflow';
|
||||
|
||||
const uiStore = useUIStore();
|
||||
|
||||
function shouldShowBanner(bannerName: Banners) {
|
||||
return uiStore.banners[bannerName].dismissed === false;
|
||||
}
|
||||
|
||||
async function updateCurrentBannerHeight() {
|
||||
const bannerHeight = await getBannerRowHeight();
|
||||
uiStore.updateBannersHeight(bannerHeight);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await updateCurrentBannerHeight();
|
||||
});
|
||||
|
||||
watch(uiStore.banners, async () => {
|
||||
await updateCurrentBannerHeight();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-test-id="banner-stack">
|
||||
<trial-over-banner v-if="shouldShowBanner('TRIAL_OVER')" />
|
||||
<trial-banner v-if="shouldShowBanner('TRIAL')" />
|
||||
<v1-banner v-if="shouldShowBanner('V1')" />
|
||||
</div>
|
||||
</template>
|
||||
64
packages/editor-ui/src/components/banners/BaseBanner.vue
Normal file
64
packages/editor-ui/src/components/banners/BaseBanner.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<script lang="ts" setup>
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import type { Banners } from 'n8n-workflow';
|
||||
|
||||
interface Props {
|
||||
name: Banners;
|
||||
theme?: string;
|
||||
customIcon?: string;
|
||||
dismissible?: boolean;
|
||||
}
|
||||
|
||||
const uiStore = useUIStore();
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
theme: 'info',
|
||||
dismissible: true,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
async function onCloseClick() {
|
||||
await uiStore.dismissBanner(props.name);
|
||||
emit('close');
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<n8n-callout
|
||||
:theme="props.theme"
|
||||
:icon="props.customIcon"
|
||||
iconSize="medium"
|
||||
:roundCorners="false"
|
||||
:data-test-id="`banners-${props.name}`"
|
||||
>
|
||||
<div :class="$style.mainContent">
|
||||
<slot name="mainContent" />
|
||||
</div>
|
||||
<template #trailingContent>
|
||||
<div :class="$style.trailingContent">
|
||||
<slot name="trailingContent" />
|
||||
<n8n-icon
|
||||
v-if="dismissible"
|
||||
size="small"
|
||||
icon="times"
|
||||
title="Dismiss"
|
||||
class="clickable"
|
||||
:data-test-id="`banner-${props.name}-close`"
|
||||
@click="onCloseClick"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</n8n-callout>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.mainContent {
|
||||
display: flex;
|
||||
gap: var(--spacing-4xs);
|
||||
}
|
||||
.trailingContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
</style>
|
||||
36
packages/editor-ui/src/components/banners/TrialBanner.vue
Normal file
36
packages/editor-ui/src/components/banners/TrialBanner.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script lang="ts" setup>
|
||||
import BaseBanner from '@/components/banners/BaseBanner.vue';
|
||||
import { i18n as locale } from '@/plugins/i18n';
|
||||
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
|
||||
import { computed } from 'vue';
|
||||
import { useUIStore } from '@/stores';
|
||||
|
||||
const trialDaysLeft = computed(() => {
|
||||
const { trialDaysLeft } = useCloudPlanStore();
|
||||
return -1 * trialDaysLeft;
|
||||
});
|
||||
|
||||
const messageText = computed(() => {
|
||||
return locale.baseText('banners.trial.message', {
|
||||
adjustToNumber: trialDaysLeft.value,
|
||||
interpolate: { count: String(trialDaysLeft.value) },
|
||||
});
|
||||
});
|
||||
|
||||
function onUpdatePlanClick() {
|
||||
useUIStore().goToUpgrade('canvas-nav', 'upgrade-canvas-nav', 'redirect');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<base-banner name="TRIAL" theme="custom">
|
||||
<template #mainContent>
|
||||
<span>{{ messageText }}</span>
|
||||
</template>
|
||||
<template #trailingContent>
|
||||
<n8n-button type="success" @click="onUpdatePlanClick" icon="gem" size="small">{{
|
||||
locale.baseText('generic.upgradeNow')
|
||||
}}</n8n-button>
|
||||
</template>
|
||||
</base-banner>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts" setup>
|
||||
import BaseBanner from '@/components/banners/BaseBanner.vue';
|
||||
import { i18n as locale } from '@/plugins/i18n';
|
||||
import { useUIStore } from '@/stores';
|
||||
|
||||
function onUpdatePlanClick() {
|
||||
useUIStore().goToUpgrade('canvas-nav', 'upgrade-canvas-nav', 'redirect');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<base-banner customIcon="info-circle" theme="warning" name="TRIAL_OVER">
|
||||
<template #mainContent>
|
||||
<span>{{ locale.baseText('banners.trialOver.message') }}</span>
|
||||
</template>
|
||||
<template #trailingContent>
|
||||
<n8n-button type="success" @click="onUpdatePlanClick" icon="gem" size="small">{{
|
||||
locale.baseText('generic.upgradeNow')
|
||||
}}</n8n-button>
|
||||
</template>
|
||||
</base-banner>
|
||||
</template>
|
||||
38
packages/editor-ui/src/components/banners/V1Banner.vue
Normal file
38
packages/editor-ui/src/components/banners/V1Banner.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script lang="ts" setup>
|
||||
import BaseBanner from '@/components/banners/BaseBanner.vue';
|
||||
import { i18n as locale } from '@/plugins/i18n';
|
||||
import { useUsersStore } from '@/stores';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
|
||||
const uiStore = useUIStore();
|
||||
|
||||
const { isInstanceOwner } = useUsersStore();
|
||||
|
||||
async function dismissPermanently() {
|
||||
await uiStore.dismissBanner('V1', 'permanent');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<base-banner customIcon="info-circle" theme="warning" name="V1">
|
||||
<template #mainContent>
|
||||
<span v-html="locale.baseText('banners.v1.message')"></span>
|
||||
<a
|
||||
v-if="isInstanceOwner"
|
||||
:class="$style.link"
|
||||
@click="dismissPermanently"
|
||||
data-test-id="banner-confirm-v1"
|
||||
>
|
||||
<span v-html="locale.baseText('generic.dontShowAgain')"></span>
|
||||
</a>
|
||||
</template>
|
||||
</base-banner>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
a,
|
||||
.link {
|
||||
font-weight: var(--font-weight-bold);
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user