feat(editor): Migrate existing users to new canvas and set new canvas as default (#11896)

This commit is contained in:
Alex Grozav
2024-11-27 10:11:33 +02:00
committed by GitHub
parent 132aa0b9f1
commit caa744785a
7 changed files with 203 additions and 25 deletions

View File

@@ -75,8 +75,13 @@ Cypress.Commands.add('signin', ({ email, password }) => {
.then((response) => { .then((response) => {
Cypress.env('currentUserId', response.body.data.id); Cypress.env('currentUserId', response.body.data.id);
// @TODO Remove this once the switcher is removed
cy.window().then((win) => { cy.window().then((win) => {
win.localStorage.setItem('NodeView.switcher.discovered', 'true'); // @TODO Remove this once the switcher is removed win.localStorage.setItem('NodeView.migrated', 'true');
win.localStorage.setItem('NodeView.switcher.discovered.beta', 'true');
const nodeViewVersion = Cypress.env('NODE_VIEW_VERSION');
win.localStorage.setItem('NodeView.version', nodeViewVersion ?? '1');
}); });
}); });
}); });

View File

@@ -20,11 +20,6 @@ beforeEach(() => {
win.localStorage.setItem('N8N_THEME', 'light'); win.localStorage.setItem('N8N_THEME', 'light');
win.localStorage.setItem('N8N_AUTOCOMPLETE_ONBOARDED', 'true'); win.localStorage.setItem('N8N_AUTOCOMPLETE_ONBOARDED', 'true');
win.localStorage.setItem('N8N_MAPPING_ONBOARDED', 'true'); win.localStorage.setItem('N8N_MAPPING_ONBOARDED', 'true');
const nodeViewVersion = Cypress.env('NODE_VIEW_VERSION');
if (nodeViewVersion) {
win.localStorage.setItem('NodeView.version', nodeViewVersion);
}
}); });
cy.intercept('GET', '/rest/settings', (req) => { cy.intercept('GET', '/rest/settings', (req) => {

View File

@@ -103,6 +103,7 @@ const tagsEventBus = createEventBus();
const sourceControlModalEventBus = createEventBus(); const sourceControlModalEventBus = createEventBus();
const { const {
isNewUser,
nodeViewVersion, nodeViewVersion,
nodeViewSwitcherDiscovered, nodeViewSwitcherDiscovered,
isNodeViewDiscoveryTooltipVisible, isNodeViewDiscoveryTooltipVisible,
@@ -193,10 +194,14 @@ const workflowMenuItems = computed<ActionDropdownItem[]>(() => {
actions.push({ actions.push({
id: WORKFLOW_MENU_ACTIONS.SWITCH_NODE_VIEW_VERSION, id: WORKFLOW_MENU_ACTIONS.SWITCH_NODE_VIEW_VERSION,
...(nodeViewVersion.value === '2' ...(nodeViewVersion.value === '2'
? {} ? nodeViewSwitcherDiscovered.value || isNewUser.value
? {}
: {
badge: locale.baseText('menuActions.badge.new'),
}
: nodeViewSwitcherDiscovered.value : nodeViewSwitcherDiscovered.value
? { ? {
badge: locale.baseText('menuActions.badge.alpha'), badge: locale.baseText('menuActions.badge.beta'),
badgeProps: { badgeProps: {
theme: 'tertiary', theme: 'tertiary',
}, },
@@ -756,9 +761,12 @@ function showCreateWorkflowSuccessToast(id?: string) {
/> />
<template #content> <template #content>
<div class="mb-4xs"> <div class="mb-4xs">
<N8nBadge>{{ i18n.baseText('menuActions.badge.alpha') }}</N8nBadge> <N8nBadge>{{ i18n.baseText('menuActions.badge.beta') }}</N8nBadge>
</div> </div>
{{ i18n.baseText('menuActions.nodeViewDiscovery.tooltip') }} <p>{{ i18n.baseText('menuActions.nodeViewDiscovery.tooltip') }}</p>
<N8nText color="text-light" size="small">
{{ i18n.baseText('menuActions.nodeViewDiscovery.tooltip.switchBack') }}
</N8nText>
<N8nIcon <N8nIcon
:class="$style.closeNodeViewDiscovery" :class="$style.closeNodeViewDiscovery"
icon="times-circle" icon="times-circle"

View File

@@ -0,0 +1,156 @@
import { useNodeViewVersionSwitcher } from './useNodeViewVersionSwitcher';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { createTestingPinia } from '@pinia/testing';
import { STORES } from '@/constants';
import { setActivePinia } from 'pinia';
import { mockedStore } from '@/__tests__/utils';
import { useNDVStore } from '@/stores/ndv.store';
vi.mock('@/composables/useTelemetry', () => ({
useTelemetry: () => ({
track: vi.fn(),
}),
}));
describe('useNodeViewVersionSwitcher', () => {
const initialState = {
[STORES.WORKFLOWS]: {},
[STORES.NDV]: {},
};
beforeEach(() => {
vi.clearAllMocks();
const pinia = createTestingPinia({ initialState });
setActivePinia(pinia);
});
describe('isNewUser', () => {
test('should return true when there are no active workflows', () => {
const { isNewUser } = useNodeViewVersionSwitcher();
expect(isNewUser.value).toBe(true);
});
test('should return false when there are active workflows', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
workflowsStore.activeWorkflows = ['1'];
const { isNewUser } = useNodeViewVersionSwitcher();
expect(isNewUser.value).toBe(false);
});
});
describe('nodeViewVersion', () => {
test('should initialize with default version "2"', () => {
const { nodeViewVersion } = useNodeViewVersionSwitcher();
expect(nodeViewVersion.value).toBe('2');
});
});
describe('isNodeViewDiscoveryTooltipVisible', () => {
test('should be visible under correct conditions', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
workflowsStore.activeWorkflows = ['1'];
const ndvStore = mockedStore(useNDVStore);
ndvStore.activeNodeName = null;
const { isNodeViewDiscoveryTooltipVisible } = useNodeViewVersionSwitcher();
expect(isNodeViewDiscoveryTooltipVisible.value).toBe(true);
});
test('should not be visible for new users', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
workflowsStore.activeWorkflows = [];
const { isNodeViewDiscoveryTooltipVisible } = useNodeViewVersionSwitcher();
expect(isNodeViewDiscoveryTooltipVisible.value).toBe(false);
});
test('should not be visible when node is selected', () => {
const ndvStore = mockedStore(useNDVStore);
ndvStore.activeNodeName = 'test-node';
const { isNodeViewDiscoveryTooltipVisible } = useNodeViewVersionSwitcher();
expect(isNodeViewDiscoveryTooltipVisible.value).toBe(false);
});
});
describe('switchNodeViewVersion', () => {
test('should switch from version 2 to 1 and back', () => {
const { nodeViewVersion, switchNodeViewVersion } = useNodeViewVersionSwitcher();
switchNodeViewVersion();
expect(nodeViewVersion.value).toBe('1');
switchNodeViewVersion();
expect(nodeViewVersion.value).toBe('2');
});
});
describe('migrateToNewNodeViewVersion', () => {
test('should not migrate if already migrated', () => {
const { nodeViewVersion, nodeViewVersionMigrated, migrateToNewNodeViewVersion } =
useNodeViewVersionSwitcher();
nodeViewVersionMigrated.value = true;
migrateToNewNodeViewVersion();
expect(nodeViewVersion.value).toBe('2');
});
test('should not migrate if already on version 2', () => {
const { nodeViewVersion, migrateToNewNodeViewVersion } = useNodeViewVersionSwitcher();
nodeViewVersion.value = '2';
migrateToNewNodeViewVersion();
expect(nodeViewVersion.value).not.toBe('1');
});
test('should migrate to version 2 if not migrated and on version 1', () => {
const { nodeViewVersion, nodeViewVersionMigrated, migrateToNewNodeViewVersion } =
useNodeViewVersionSwitcher();
nodeViewVersion.value = '1';
nodeViewVersionMigrated.value = false;
migrateToNewNodeViewVersion();
expect(nodeViewVersion.value).toBe('2');
expect(nodeViewVersionMigrated.value).toBe(true);
});
});
describe('setNodeViewSwitcherDropdownOpened', () => {
test('should set discovered when dropdown is closed', () => {
const { setNodeViewSwitcherDropdownOpened, nodeViewSwitcherDiscovered } =
useNodeViewVersionSwitcher();
setNodeViewSwitcherDropdownOpened(false);
expect(nodeViewSwitcherDiscovered.value).toBe(true);
nodeViewSwitcherDiscovered.value = false;
});
test('should not set discovered when dropdown is opened', () => {
const { setNodeViewSwitcherDropdownOpened, nodeViewSwitcherDiscovered } =
useNodeViewVersionSwitcher();
setNodeViewSwitcherDropdownOpened(true);
expect(nodeViewSwitcherDiscovered.value).toBe(false);
});
});
describe('setNodeViewSwitcherDiscovered', () => {
test('should set nodeViewSwitcherDiscovered to true', () => {
const { setNodeViewSwitcherDiscovered, nodeViewSwitcherDiscovered } =
useNodeViewVersionSwitcher();
setNodeViewSwitcherDiscovered();
expect(nodeViewSwitcherDiscovered.value).toBe(true);
});
});
});

View File

@@ -1,6 +1,5 @@
import { computed } from 'vue'; import { computed } from 'vue';
import { useLocalStorage, debouncedRef } from '@vueuse/core'; import { useLocalStorage } from '@vueuse/core';
import { useSettingsStore } from '@/stores/settings.store';
import { useTelemetry } from '@/composables/useTelemetry'; import { useTelemetry } from '@/composables/useTelemetry';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
@@ -8,16 +7,12 @@ import { useNDVStore } from '@/stores/ndv.store';
export function useNodeViewVersionSwitcher() { export function useNodeViewVersionSwitcher() {
const ndvStore = useNDVStore(); const ndvStore = useNDVStore();
const workflowsStore = useWorkflowsStore(); const workflowsStore = useWorkflowsStore();
const settingsStore = useSettingsStore();
const telemetry = useTelemetry(); const telemetry = useTelemetry();
const isNewUser = computed(() => workflowsStore.activeWorkflows.length === 0); const isNewUser = computed(() => workflowsStore.activeWorkflows.length === 0);
const isNewUserDebounced = debouncedRef(isNewUser, 3000);
const nodeViewVersion = useLocalStorage( const nodeViewVersion = useLocalStorage('NodeView.version', '2');
'NodeView.version', const nodeViewVersionMigrated = useLocalStorage('NodeView.migrated', false);
settingsStore.isCanvasV2Enabled ? '2' : '1',
);
function setNodeViewSwitcherDropdownOpened(visible: boolean) { function setNodeViewSwitcherDropdownOpened(visible: boolean) {
if (!visible) { if (!visible) {
@@ -25,20 +20,21 @@ export function useNodeViewVersionSwitcher() {
} }
} }
const nodeViewSwitcherDiscovered = useLocalStorage('NodeView.switcher.discovered', false); const nodeViewSwitcherDiscovered = useLocalStorage('NodeView.switcher.discovered.beta', false);
function setNodeViewSwitcherDiscovered() { function setNodeViewSwitcherDiscovered() {
nodeViewSwitcherDiscovered.value = true; nodeViewSwitcherDiscovered.value = true;
} }
const isNodeViewDiscoveryTooltipVisible = computed( const isNodeViewDiscoveryTooltipVisible = computed(
() => () =>
!isNewUser.value &&
!ndvStore.activeNodeName && !ndvStore.activeNodeName &&
nodeViewVersion.value !== '2' && nodeViewVersion.value === '2' &&
!(isNewUserDebounced.value || nodeViewSwitcherDiscovered.value), !nodeViewSwitcherDiscovered.value,
); );
function switchNodeViewVersion() { function switchNodeViewVersion() {
const toVersion = nodeViewVersion.value === '1' ? '2' : '1'; const toVersion = nodeViewVersion.value === '2' ? '1' : '2';
telemetry.track('User switched canvas version', { telemetry.track('User switched canvas version', {
to_version: toVersion, to_version: toVersion,
@@ -47,12 +43,24 @@ export function useNodeViewVersionSwitcher() {
nodeViewVersion.value = toVersion; nodeViewVersion.value = toVersion;
} }
function migrateToNewNodeViewVersion() {
if (nodeViewVersionMigrated.value || nodeViewVersion.value === '2') {
return;
}
switchNodeViewVersion();
nodeViewVersionMigrated.value = true;
}
return { return {
isNewUser,
nodeViewVersion, nodeViewVersion,
nodeViewVersionMigrated,
nodeViewSwitcherDiscovered, nodeViewSwitcherDiscovered,
isNodeViewDiscoveryTooltipVisible, isNodeViewDiscoveryTooltipVisible,
setNodeViewSwitcherDropdownOpened, setNodeViewSwitcherDropdownOpened,
setNodeViewSwitcherDiscovered, setNodeViewSwitcherDiscovered,
switchNodeViewVersion, switchNodeViewVersion,
migrateToNewNodeViewVersion,
}; };
} }

View File

@@ -912,7 +912,9 @@
"menuActions.switchToOldNodeViewVersion": "Switch to old canvas", "menuActions.switchToOldNodeViewVersion": "Switch to old canvas",
"menuActions.badge.new": "NEW", "menuActions.badge.new": "NEW",
"menuActions.badge.alpha": "ALPHA", "menuActions.badge.alpha": "ALPHA",
"menuActions.nodeViewDiscovery.tooltip": "Try our new, more performant canvas", "menuActions.badge.beta": "BETA",
"menuActions.nodeViewDiscovery.tooltip": "You're currently using our new, more performant canvas.",
"menuActions.nodeViewDiscovery.tooltip.switchBack": "You can switch back to the old version using this menu.",
"multipleParameter.addItem": "Add item", "multipleParameter.addItem": "Add item",
"multipleParameter.currentlyNoItemsExist": "Currently no items exist", "multipleParameter.currentlyNoItemsExist": "Currently no items exist",
"multipleParameter.deleteItem": "Delete item", "multipleParameter.deleteItem": "Delete item",

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, watch } from 'vue'; import { computed, onMounted, watch } from 'vue';
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'; import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router';
import NodeViewV1 from '@/views/NodeView.vue'; import NodeViewV1 from '@/views/NodeView.vue';
import NodeViewV2 from '@/views/NodeView.v2.vue'; import NodeViewV2 from '@/views/NodeView.v2.vue';
@@ -17,7 +17,7 @@ const router = useRouter();
const route = useRoute(); const route = useRoute();
const workflowHelpers = useWorkflowHelpers({ router }); const workflowHelpers = useWorkflowHelpers({ router });
const { nodeViewVersion } = useNodeViewVersionSwitcher(); const { nodeViewVersion, migrateToNewNodeViewVersion } = useNodeViewVersionSwitcher();
const workflowId = computed<string>(() => route.params.name as string); const workflowId = computed<string>(() => route.params.name as string);
@@ -25,6 +25,10 @@ const isReadOnlyEnvironment = computed(() => {
return sourceControlStore.preferences.branchReadOnly; return sourceControlStore.preferences.branchReadOnly;
}); });
onMounted(() => {
migrateToNewNodeViewVersion();
});
watch(nodeViewVersion, () => { watch(nodeViewVersion, () => {
router.go(0); router.go(0);
}); });