Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 5 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,13 @@ Cypress.Commands.add('signin', ({ email, password }) => {
.then((response) => {
Cypress.env('currentUserId', response.body.data.id);

// @TODO Remove this once the switcher is removed
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');
});
});
});
Expand Down
5 changes: 0 additions & 5 deletions cypress/support/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,6 @@ beforeEach(() => {
win.localStorage.setItem('N8N_THEME', 'light');
win.localStorage.setItem('N8N_AUTOCOMPLETE_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) => {
Expand Down
16 changes: 12 additions & 4 deletions packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ const tagsEventBus = createEventBus();
const sourceControlModalEventBus = createEventBus();

const {
isNewUser,
nodeViewVersion,
nodeViewSwitcherDiscovered,
isNodeViewDiscoveryTooltipVisible,
Expand Down Expand Up @@ -193,10 +194,14 @@ const workflowMenuItems = computed<ActionDropdownItem[]>(() => {
actions.push({
id: WORKFLOW_MENU_ACTIONS.SWITCH_NODE_VIEW_VERSION,
...(nodeViewVersion.value === '2'
? {}
? nodeViewSwitcherDiscovered.value || isNewUser.value
? {}
: {
badge: locale.baseText('menuActions.badge.new'),
}
: nodeViewSwitcherDiscovered.value
? {
badge: locale.baseText('menuActions.badge.alpha'),
badge: locale.baseText('menuActions.badge.beta'),
badgeProps: {
theme: 'tertiary',
},
Expand Down Expand Up @@ -756,9 +761,12 @@ function showCreateWorkflowSuccessToast(id?: string) {
/>
<template #content>
<div class="mb-4xs">
<N8nBadge>{{ i18n.baseText('menuActions.badge.alpha') }}</N8nBadge>
<N8nBadge>{{ i18n.baseText('menuActions.badge.beta') }}</N8nBadge>
</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
:class="$style.closeNodeViewDiscovery"
icon="times-circle"
Expand Down
156 changes: 156 additions & 0 deletions packages/editor-ui/src/composables/useNodeViewVersionSwitcher.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
32 changes: 20 additions & 12 deletions packages/editor-ui/src/composables/useNodeViewVersionSwitcher.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,40 @@
import { computed } from 'vue';
import { useLocalStorage, debouncedRef } from '@vueuse/core';
import { useSettingsStore } from '@/stores/settings.store';
import { useLocalStorage } from '@vueuse/core';
import { useTelemetry } from '@/composables/useTelemetry';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNDVStore } from '@/stores/ndv.store';

export function useNodeViewVersionSwitcher() {
const ndvStore = useNDVStore();
const workflowsStore = useWorkflowsStore();
const settingsStore = useSettingsStore();
const telemetry = useTelemetry();

const isNewUser = computed(() => workflowsStore.activeWorkflows.length === 0);
const isNewUserDebounced = debouncedRef(isNewUser, 3000);

const nodeViewVersion = useLocalStorage(
'NodeView.version',
settingsStore.isCanvasV2Enabled ? '2' : '1',
);
const nodeViewVersion = useLocalStorage('NodeView.version', '2');
const nodeViewVersionMigrated = useLocalStorage('NodeView.migrated', false);

function setNodeViewSwitcherDropdownOpened(visible: boolean) {
if (!visible) {
setNodeViewSwitcherDiscovered();
}
}

const nodeViewSwitcherDiscovered = useLocalStorage('NodeView.switcher.discovered', false);
const nodeViewSwitcherDiscovered = useLocalStorage('NodeView.switcher.discovered.beta', false);
function setNodeViewSwitcherDiscovered() {
nodeViewSwitcherDiscovered.value = true;
}

const isNodeViewDiscoveryTooltipVisible = computed(
() =>
!isNewUser.value &&
!ndvStore.activeNodeName &&
nodeViewVersion.value !== '2' &&
!(isNewUserDebounced.value || nodeViewSwitcherDiscovered.value),
nodeViewVersion.value === '2' &&
!nodeViewSwitcherDiscovered.value,
);

function switchNodeViewVersion() {
const toVersion = nodeViewVersion.value === '1' ? '2' : '1';
const toVersion = nodeViewVersion.value === '2' ? '1' : '2';

telemetry.track('User switched canvas version', {
to_version: toVersion,
Expand All @@ -47,12 +43,24 @@ export function useNodeViewVersionSwitcher() {
nodeViewVersion.value = toVersion;
}

function migrateToNewNodeViewVersion() {
if (nodeViewVersionMigrated.value || nodeViewVersion.value === '2') {
return;
}

switchNodeViewVersion();
nodeViewVersionMigrated.value = true;
}

return {
isNewUser,
nodeViewVersion,
nodeViewVersionMigrated,
nodeViewSwitcherDiscovered,
isNodeViewDiscoveryTooltipVisible,
setNodeViewSwitcherDropdownOpened,
setNodeViewSwitcherDiscovered,
switchNodeViewVersion,
migrateToNewNodeViewVersion,
};
}
4 changes: 3 additions & 1 deletion packages/editor-ui/src/plugins/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -911,7 +911,9 @@
"menuActions.switchToOldNodeViewVersion": "Switch to old canvas",
"menuActions.badge.new": "NEW",
"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.currentlyNoItemsExist": "Currently no items exist",
"multipleParameter.deleteItem": "Delete item",
Expand Down
8 changes: 6 additions & 2 deletions packages/editor-ui/src/views/NodeViewSwitcher.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { computed, watch } from 'vue';
import { computed, onMounted, watch } from 'vue';
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router';
import NodeViewV1 from '@/views/NodeView.vue';
import NodeViewV2 from '@/views/NodeView.v2.vue';
Expand All @@ -17,14 +17,18 @@ const router = useRouter();
const route = useRoute();
const workflowHelpers = useWorkflowHelpers({ router });

const { nodeViewVersion } = useNodeViewVersionSwitcher();
const { nodeViewVersion, migrateToNewNodeViewVersion } = useNodeViewVersionSwitcher();

const workflowId = computed<string>(() => route.params.name as string);

const isReadOnlyEnvironment = computed(() => {
return sourceControlStore.preferences.branchReadOnly;
});

onMounted(() => {
migrateToNewNodeViewVersion();
});

watch(nodeViewVersion, () => {
router.go(0);
});
Expand Down
Loading