mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
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:
parent
ff0759530d
commit
4240e76253
|
@ -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);
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
// -----------------------------------
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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],
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -9,6 +9,24 @@ export class SettingsRepository extends Repository<Settings> {
|
|||
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<Settings> {
|
|||
await this.save({ key, value, loadOnStartup });
|
||||
}
|
||||
|
||||
if (loadOnStartup) config.set('ui.banners.v1.dismissed', true);
|
||||
if (loadOnStartup) config.set('ui.banners.dismissed', value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }>, {}>;
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div :class="classes" role="alert">
|
||||
<div :class="$style.messageSection">
|
||||
<div :class="$style.icon" v-if="!iconless">
|
||||
<n8n-icon :icon="getIcon" :size="theme === 'secondary' ? 'medium' : 'large'" />
|
||||
<n8n-icon :icon="getIcon" :size="getIconSize" />
|
||||
</div>
|
||||
<n8n-text size="small">
|
||||
<slot />
|
||||
|
@ -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 {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
// Vitest Snapshot v1
|
||||
|
||||
exports[`components > N8nCallout > should render additional slots correctly 1`] = `
|
||||
"<div role=\\"alert\\" class=\\"n8n-callout callout custom\\">
|
||||
"<div role=\\"alert\\" class=\\"n8n-callout callout custom round\\">
|
||||
<div class=\\"messageSection\\">
|
||||
<div class=\\"icon\\">
|
||||
<n8n-icon-stub icon=\\"code-branch\\" size=\\"large\\"></n8n-icon-stub>
|
||||
<n8n-icon-stub icon=\\"code-branch\\" size=\\"medium\\"></n8n-icon-stub>
|
||||
</div>
|
||||
<n8n-text-stub size=\\"small\\" tag=\\"span\\">
|
||||
<n8n-text-stub size=\\"small\\" tag=\\"span\\">This is a secondary callout.</n8n-text-stub>
|
||||
|
@ -15,10 +15,10 @@ exports[`components > N8nCallout > should render additional slots correctly 1`]
|
|||
`;
|
||||
|
||||
exports[`components > N8nCallout > should render custom theme correctly 1`] = `
|
||||
"<div role=\\"alert\\" class=\\"n8n-callout callout custom\\">
|
||||
"<div role=\\"alert\\" class=\\"n8n-callout callout custom round\\">
|
||||
<div class=\\"messageSection\\">
|
||||
<div class=\\"icon\\">
|
||||
<n8n-icon-stub icon=\\"code-branch\\" size=\\"large\\"></n8n-icon-stub>
|
||||
<n8n-icon-stub icon=\\"code-branch\\" size=\\"medium\\"></n8n-icon-stub>
|
||||
</div>
|
||||
<n8n-text-stub size=\\"small\\" tag=\\"span\\">
|
||||
<n8n-text-stub size=\\"small\\" tag=\\"span\\">This is a secondary callout.</n8n-text-stub>
|
||||
|
@ -28,10 +28,10 @@ exports[`components > N8nCallout > should render custom theme correctly 1`] = `
|
|||
`;
|
||||
|
||||
exports[`components > N8nCallout > should render danger theme correctly 1`] = `
|
||||
"<div role=\\"alert\\" class=\\"n8n-callout callout danger\\">
|
||||
"<div role=\\"alert\\" class=\\"n8n-callout callout danger round\\">
|
||||
<div class=\\"messageSection\\">
|
||||
<div class=\\"icon\\">
|
||||
<n8n-icon-stub icon=\\"times-circle\\" size=\\"large\\"></n8n-icon-stub>
|
||||
<n8n-icon-stub icon=\\"times-circle\\" size=\\"medium\\"></n8n-icon-stub>
|
||||
</div>
|
||||
<n8n-text-stub size=\\"small\\" tag=\\"span\\">
|
||||
<n8n-text-stub size=\\"small\\" tag=\\"span\\">This is a danger callout.</n8n-text-stub>
|
||||
|
@ -41,10 +41,10 @@ exports[`components > N8nCallout > should render danger theme correctly 1`] = `
|
|||
`;
|
||||
|
||||
exports[`components > N8nCallout > should render info theme correctly 1`] = `
|
||||
"<div role=\\"alert\\" class=\\"n8n-callout callout info\\">
|
||||
"<div role=\\"alert\\" class=\\"n8n-callout callout info round\\">
|
||||
<div class=\\"messageSection\\">
|
||||
<div class=\\"icon\\">
|
||||
<n8n-icon-stub icon=\\"info-circle\\" size=\\"large\\"></n8n-icon-stub>
|
||||
<n8n-icon-stub icon=\\"info-circle\\" size=\\"medium\\"></n8n-icon-stub>
|
||||
</div>
|
||||
<n8n-text-stub size=\\"small\\" tag=\\"span\\">
|
||||
<n8n-text-stub size=\\"small\\" tag=\\"span\\">This is an info callout.</n8n-text-stub>
|
||||
|
@ -54,7 +54,7 @@ exports[`components > N8nCallout > should render info theme correctly 1`] = `
|
|||
`;
|
||||
|
||||
exports[`components > N8nCallout > should render secondary theme correctly 1`] = `
|
||||
"<div role=\\"alert\\" class=\\"n8n-callout callout secondary\\">
|
||||
"<div role=\\"alert\\" class=\\"n8n-callout callout secondary round\\">
|
||||
<div class=\\"messageSection\\">
|
||||
<div class=\\"icon\\">
|
||||
<n8n-icon-stub icon=\\"info-circle\\" size=\\"medium\\"></n8n-icon-stub>
|
||||
|
@ -67,10 +67,10 @@ exports[`components > N8nCallout > should render secondary theme correctly 1`] =
|
|||
`;
|
||||
|
||||
exports[`components > N8nCallout > should render success theme correctly 1`] = `
|
||||
"<div role=\\"alert\\" class=\\"n8n-callout callout success\\">
|
||||
"<div role=\\"alert\\" class=\\"n8n-callout callout success round\\">
|
||||
<div class=\\"messageSection\\">
|
||||
<div class=\\"icon\\">
|
||||
<n8n-icon-stub icon=\\"check-circle\\" size=\\"large\\"></n8n-icon-stub>
|
||||
<n8n-icon-stub icon=\\"check-circle\\" size=\\"medium\\"></n8n-icon-stub>
|
||||
</div>
|
||||
<n8n-text-stub size=\\"small\\" tag=\\"span\\">
|
||||
<n8n-text-stub size=\\"small\\" tag=\\"span\\">This is a success callout.</n8n-text-stub>
|
||||
|
@ -80,10 +80,10 @@ exports[`components > N8nCallout > should render success theme correctly 1`] = `
|
|||
`;
|
||||
|
||||
exports[`components > N8nCallout > should render warning theme correctly 1`] = `
|
||||
"<div role=\\"alert\\" class=\\"n8n-callout callout warning\\">
|
||||
"<div role=\\"alert\\" class=\\"n8n-callout callout warning round\\">
|
||||
<div class=\\"messageSection\\">
|
||||
<div class=\\"icon\\">
|
||||
<n8n-icon-stub icon=\\"exclamation-triangle\\" size=\\"large\\"></n8n-icon-stub>
|
||||
<n8n-icon-stub icon=\\"exclamation-triangle\\" size=\\"medium\\"></n8n-icon-stub>
|
||||
</div>
|
||||
<n8n-text-stub size=\\"small\\" tag=\\"span\\">
|
||||
<n8n-text-stub size=\\"small\\" tag=\\"span\\">This is a warning callout.</n8n-text-stub>
|
||||
|
|
|
@ -9,7 +9,9 @@
|
|||
[$style.sidebarCollapsed]: uiStore.sidebarMenuCollapsed,
|
||||
}"
|
||||
>
|
||||
<V1Banner />
|
||||
<div id="banners" :class="$style.banners">
|
||||
<banner-stack v-if="!isDemoMode" />
|
||||
</div>
|
||||
<div id="header" :class="$style.header">
|
||||
<router-view name="header"></router-view>
|
||||
</div>
|
||||
|
@ -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<void> {
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -75,9 +75,7 @@ const defaultSettings: IN8nUISettings = {
|
|||
type: 'default',
|
||||
},
|
||||
banners: {
|
||||
v1: {
|
||||
dismissed: false,
|
||||
},
|
||||
dismissed: [],
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -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<void> {
|
||||
return makeRestApiRequest(context, 'POST', '/owner/dismiss-v1');
|
||||
export async function dismissBannerPermanently(
|
||||
context: IRestApiContext,
|
||||
data: { bannerName: Banners; dismissedBanners: string[] },
|
||||
): Promise<void> {
|
||||
return makeRestApiRequest(context, 'POST', '/owner/dismiss-banner', { banner: data.bannerName });
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -6,7 +6,6 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
|
|||
import {
|
||||
getMousePosition,
|
||||
getRelativePosition,
|
||||
HEADER_HEIGHT,
|
||||
SIDEBAR_WIDTH,
|
||||
SIDEBAR_WIDTH_EXPANDED,
|
||||
} from '@/utils/nodeViewUtils';
|
||||
|
@ -187,10 +186,11 @@ export default function useCanvasMouseSelect() {
|
|||
: uiStore.sidebarMenuCollapsed
|
||||
? SIDEBAR_WIDTH
|
||||
: SIDEBAR_WIDTH_EXPANDED;
|
||||
const headerHeight = canvasStore.isDemo ? 0 : HEADER_HEIGHT;
|
||||
|
||||
const relativeX = mouseX - sidebarWidth;
|
||||
const relativeY = mouseY - headerHeight;
|
||||
const relativeY = canvasStore.isDemo
|
||||
? mouseY
|
||||
: mouseY - uiStore.bannersHeight - uiStore.headerHeight;
|
||||
const nodeViewScale = canvasStore.nodeViewScale;
|
||||
const nodeViewOffsetPosition = uiStore.nodeViewOffsetPosition;
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ import { FontAwesomePlugin } from './plugins/icons';
|
|||
|
||||
import { runExternalHook } from '@/utils';
|
||||
import { createPinia, PiniaVuePlugin } from 'pinia';
|
||||
import { useWebhooksStore, useUIStore } from '@/stores';
|
||||
import { useWebhooksStore } from '@/stores';
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
|
@ -46,8 +46,6 @@ new Vue({
|
|||
}).$mount('#app');
|
||||
|
||||
router.afterEach((to, from) => {
|
||||
useUIStore().restoreBanner('v1');
|
||||
|
||||
void runExternalHook('main.routeChange', useWebhooksStore(), { from, to });
|
||||
});
|
||||
|
||||
|
|
|
@ -34,7 +34,12 @@ $badge-warning-color: var(--color-text-dark);
|
|||
// Warning tooltip
|
||||
$warning-tooltip-color: var(--color-danger);
|
||||
|
||||
$header-height: 65px;
|
||||
:root {
|
||||
// Using native css variable enables us to use this value in JS
|
||||
--header-height: 65;
|
||||
}
|
||||
// sass variable is used for scss files
|
||||
$header-height: calc(var(--header-height) * 1px);
|
||||
|
||||
// sidebar
|
||||
$sidebar-width: 65px;
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
"generic.communityNode.tooltip": "This is a node from our community. It's part of the {packageName} package. <a href=\"{docURL}\" target=\"_blank\" title=\"Read the n8n docs\">Learn more</a>",
|
||||
"generic.copy": "Copy",
|
||||
"generic.delete": "Delete",
|
||||
"generic.dontShowAgain": "Don't show again",
|
||||
"generic.executions": "Executions",
|
||||
"generic.or": "or",
|
||||
"generic.clickToCopy": "Click to copy",
|
||||
|
@ -44,6 +45,7 @@
|
|||
"generic.unsavedWork.confirmMessage.message": "If you don't save, you will lose your changes.",
|
||||
"generic.unsavedWork.confirmMessage.confirmButtonText": "Save",
|
||||
"generic.unsavedWork.confirmMessage.cancelButtonText": "Leave without saving",
|
||||
"generic.upgradeNow": "Upgrade now",
|
||||
"generic.workflow": "Workflow",
|
||||
"generic.workflowSaved": "Workflow changes saved",
|
||||
"generic.editor": "Editor",
|
||||
|
@ -58,7 +60,6 @@
|
|||
"askAi.dialog.body": "We’re still applying the finishing touches. Soon, you will be able to <strong>automatically generate code from simple text prompts</strong>. Join the waitlist to get early access to this feature.",
|
||||
"askAi.dialog.signup": "Join Waitlist",
|
||||
"activationModal.butYouCanSeeThem": "but you can see them in the",
|
||||
"activationModal.dontShowAgain": "Don't show again",
|
||||
"activationModal.executionList": "execution list",
|
||||
"activationModal.gotIt": "Got it",
|
||||
"activationModal.ifYouChooseTo": "if you choose to",
|
||||
|
@ -102,9 +103,9 @@
|
|||
"auth.signup.setupYourAccount": "Set up your account",
|
||||
"auth.signup.setupYourAccountError": "Problem setting up your account",
|
||||
"auth.signup.tokenValidationError": "Issue validating invite token",
|
||||
"banners.trial.message": "1 day left in your n8n trial | {count} days left in your n8n trial",
|
||||
"banners.trialOver.message": "Your trial is over. Upgrade now to keep automating.",
|
||||
"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": "Don't show again",
|
||||
"banners.v1.iconTitle": "Dismiss v1 banner",
|
||||
"binaryDataDisplay.backToList": "Back to list",
|
||||
"binaryDataDisplay.backToOverviewPage": "Back to overview page",
|
||||
"binaryDataDisplay.noDataFoundToDisplay": "No data found to display",
|
||||
|
|
|
@ -31,6 +31,7 @@ import { useUIStore } from './ui.store';
|
|||
import { useUsersStore } from './users.store';
|
||||
import { useVersionsStore } from './versions.store';
|
||||
import { makeRestApiRequest } from '@/utils';
|
||||
import { useCloudPlanStore } from './cloudPlan.store';
|
||||
|
||||
export const useSettingsStore = defineStore(STORES.SETTINGS, {
|
||||
state: (): ISettingsState => ({
|
||||
|
@ -170,6 +171,9 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
|
|||
isDefaultAuthenticationSaml(): boolean {
|
||||
return this.userManagement.authenticationMethod === UserManagementAuthenticationMethod.Saml;
|
||||
},
|
||||
permanentlyDismissedBanners(): string[] {
|
||||
return this.settings.banners?.dismissed ?? [];
|
||||
},
|
||||
isBelowUserQuota(): boolean {
|
||||
const userStore = useUsersStore();
|
||||
return (
|
||||
|
@ -219,8 +223,14 @@ 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' } });
|
||||
|
||||
const isV1BannerDismissedPermanently = settings.banners.dismissed.includes('V1');
|
||||
if (
|
||||
!isV1BannerDismissedPermanently &&
|
||||
useRootStore().versionCli.startsWith('1.') &&
|
||||
!useCloudPlanStore().userIsTrialing
|
||||
) {
|
||||
useUIStore().showBanner('V1');
|
||||
}
|
||||
|
||||
useVersionsStore().setVersionNotificationSettings(settings.versionNotifications);
|
||||
|
|
|
@ -34,6 +34,7 @@ import {
|
|||
SOURCE_CONTROL_PULL_MODAL_KEY,
|
||||
} from '@/constants';
|
||||
import type {
|
||||
CloudUpdateLinkSourceType,
|
||||
CurlToJSONResponse,
|
||||
IFakeDoorLocation,
|
||||
IMenuItem,
|
||||
|
@ -41,6 +42,7 @@ import type {
|
|||
IOnboardingCallPrompt,
|
||||
IUser,
|
||||
UIState,
|
||||
UTMCampaign,
|
||||
XYPosition,
|
||||
} from '@/Interface';
|
||||
import { defineStore } from 'pinia';
|
||||
|
@ -53,7 +55,9 @@ 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';
|
||||
import { getStyleTokenValue } from '@/utils';
|
||||
import { dismissBannerPermanently } from '@/api/ui';
|
||||
import type { Banners } from 'n8n-workflow';
|
||||
|
||||
export const useUIStore = defineStore(STORES.UI, {
|
||||
state: (): UIState => ({
|
||||
|
@ -144,12 +148,6 @@ export const useUIStore = defineStore(STORES.UI, {
|
|||
},
|
||||
modalStack: [],
|
||||
sidebarMenuCollapsed: true,
|
||||
banners: {
|
||||
v1: {
|
||||
dismissed: false,
|
||||
mode: 'temporary',
|
||||
},
|
||||
},
|
||||
isPageLoading: true,
|
||||
currentView: '',
|
||||
mainPanelPosition: 0.5,
|
||||
|
@ -191,6 +189,12 @@ export const useUIStore = defineStore(STORES.UI, {
|
|||
nodeViewInitialized: false,
|
||||
addFirstStepOnLoad: false,
|
||||
executionSidebarAutoRefresh: true,
|
||||
banners: {
|
||||
V1: { dismissed: true },
|
||||
TRIAL: { dismissed: true },
|
||||
TRIAL_OVER: { dismissed: true },
|
||||
},
|
||||
bannersHeight: 0,
|
||||
}),
|
||||
getters: {
|
||||
contextBasedTranslationKeys() {
|
||||
|
@ -341,6 +345,9 @@ export const useUIStore = defineStore(STORES.UI, {
|
|||
return linkUrl;
|
||||
};
|
||||
},
|
||||
headerHeight() {
|
||||
return Number(getStyleTokenValue('--header-height'));
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setBanners(banners: UIState['banners']): void {
|
||||
|
@ -525,27 +532,15 @@ 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);
|
||||
},
|
||||
goToUpgrade(source: string, utm_campaign: string, mode: 'open' | 'redirect' = 'open'): void {
|
||||
goToUpgrade(
|
||||
source: CloudUpdateLinkSourceType,
|
||||
utm_campaign: UTMCampaign,
|
||||
mode: 'open' | 'redirect' = 'open',
|
||||
): void {
|
||||
const { usageLeft, trialDaysLeft, userIsTrialing } = useCloudPlanStore();
|
||||
const { executionsLeft, workflowsLeft } = usageLeft;
|
||||
useTelemetryStore().track('User clicked upgrade CTA', {
|
||||
|
@ -562,5 +557,27 @@ export const useUIStore = defineStore(STORES.UI, {
|
|||
location.href = this.upgradeLinkUrl(source, utm_campaign);
|
||||
}
|
||||
},
|
||||
async dismissBanner(
|
||||
name: Banners,
|
||||
type: 'temporary' | 'permanent' = 'temporary',
|
||||
): Promise<void> {
|
||||
if (type === 'permanent') {
|
||||
await dismissBannerPermanently(useRootStore().getRestApiContext, {
|
||||
bannerName: name,
|
||||
dismissedBanners: useSettingsStore().permanentlyDismissedBanners,
|
||||
});
|
||||
this.banners[name].dismissed = true;
|
||||
this.banners[name].type = 'permanent';
|
||||
return;
|
||||
}
|
||||
this.banners[name].dismissed = true;
|
||||
this.banners[name].type = 'temporary';
|
||||
},
|
||||
showBanner(name: Banners): void {
|
||||
this.banners[name].dismissed = false;
|
||||
},
|
||||
updateBannersHeight(newHeight: number): void {
|
||||
this.bannersHeight = newHeight;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -60,3 +60,11 @@ export function isChildOf(parent: Element, child: Element): boolean {
|
|||
export const capitalizeFirstLetter = (text: string): string => {
|
||||
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||
};
|
||||
|
||||
export const getBannerRowHeight = async (): Promise<number> => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(document.getElementById('banners')?.clientHeight ?? 0);
|
||||
}, 0);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -15,6 +15,7 @@ import type {
|
|||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { EVENT_CONNECTION_MOUSEOUT, EVENT_CONNECTION_MOUSEOVER } from '@jsplumb/browser-ui';
|
||||
import { useUIStore } from '@/stores';
|
||||
|
||||
/*
|
||||
Canvas constants and functions.
|
||||
|
@ -689,6 +690,7 @@ export const getZoomToFit = (
|
|||
const { minX, minY, maxX, maxY } = getWorkflowCorners(nodes);
|
||||
const { editorWidth, editorHeight } = getContentDimensions();
|
||||
const footerHeight = addFooterPadding ? 200 : 100;
|
||||
const uiStore = useUIStore();
|
||||
|
||||
const PADDING = NODE_SIZE * 4;
|
||||
|
||||
|
@ -704,7 +706,10 @@ export const getZoomToFit = (
|
|||
xOffset += (editorWidth - (maxX - minX) * zoomLevel) / 2; // add padding to center workflow
|
||||
|
||||
let yOffset = minY * -1 * zoomLevel; // find top right corner
|
||||
yOffset += (editorHeight - (maxY - minY + footerHeight) * zoomLevel) / 2; // add padding to center workflow
|
||||
yOffset +=
|
||||
(editorHeight -
|
||||
(maxY - minY + footerHeight - uiStore.headerHeight + uiStore.bannersHeight) * zoomLevel) /
|
||||
2; // add padding to center workflow
|
||||
|
||||
return {
|
||||
zoomLevel,
|
||||
|
|
|
@ -4026,7 +4026,7 @@ export default defineComponent({
|
|||
align-items: center;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: 110px;
|
||||
bottom: var(--spacing-2xl);
|
||||
width: auto;
|
||||
|
||||
@media (max-width: $breakpoint-2xs) {
|
||||
|
|
|
@ -2163,8 +2163,8 @@ export interface IN8nUISettings {
|
|||
limit: number;
|
||||
};
|
||||
banners: {
|
||||
v1: {
|
||||
dismissed: boolean;
|
||||
};
|
||||
dismissed: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export type Banners = 'V1' | 'TRIAL_OVER' | 'TRIAL';
|
||||
|
|
Loading…
Reference in a new issue