feat(editor): Create call to action tooltip for trying the new canvas (no-changelog) (#11230)

This commit is contained in:
Alex Grozav 2024-10-21 14:06:24 +03:00 committed by GitHub
parent 3c93ec88cd
commit ba2827e7bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 113 additions and 38 deletions

View file

@ -70,6 +70,10 @@ Cypress.Commands.add('signin', ({ email, password }) => {
}) })
.then((response) => { .then((response) => {
Cypress.env('currentUserId', response.body.data.id); Cypress.env('currentUserId', response.body.data.id);
cy.window().then((win) => {
win.localStorage.setItem('NodeView.switcher.discovered', 'true'); // @TODO Remove this once the switcher is removed
});
}); });
}); });
}); });

View file

@ -117,6 +117,9 @@ defineExpose({ open, close });
<span :class="$style.label"> <span :class="$style.label">
{{ item.label }} {{ item.label }}
</span> </span>
<span v-if="item.badge">
<N8nBadge theme="primary" size="xsmall">{{ item.badge }}</N8nBadge>
</span>
<N8nKeyboardShortcut <N8nKeyboardShortcut
v-if="item.shortcut" v-if="item.shortcut"
v-bind="item.shortcut" v-bind="item.shortcut"

View file

@ -3,6 +3,7 @@ import type { KeyboardShortcut } from 'n8n-design-system/types/keyboardshortcut'
export interface ActionDropdownItem { export interface ActionDropdownItem {
id: string; id: string;
label: string; label: string;
badge?: string;
icon?: string; icon?: string;
divided?: boolean; divided?: boolean;
disabled?: boolean; disabled?: boolean;

View file

@ -55,7 +55,7 @@ import { useI18n } from '@/composables/useI18n';
import { useTelemetry } from '@/composables/useTelemetry'; import { useTelemetry } from '@/composables/useTelemetry';
import type { BaseTextKey } from '@/plugins/i18n'; import type { BaseTextKey } from '@/plugins/i18n';
import { useNpsSurveyStore } from '@/stores/npsSurvey.store'; import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
import { useLocalStorage } from '@vueuse/core'; import { useNodeViewVersionSwitcher } from '@/composables/useNodeViewVersionSwitcher';
const props = defineProps<{ const props = defineProps<{
readOnly?: boolean; readOnly?: boolean;
@ -99,16 +99,14 @@ const importFileRef = ref<HTMLInputElement | undefined>();
const tagsEventBus = createEventBus(); const tagsEventBus = createEventBus();
const sourceControlModalEventBus = createEventBus(); const sourceControlModalEventBus = createEventBus();
const nodeViewSwitcher = useLocalStorage('NodeView.switcher', ''); const {
const nodeViewVersion = useLocalStorage('NodeView.version', '1'); nodeViewVersion,
nodeViewSwitcherDiscovered,
const isNodeViewSwitcherEnabled = computed(() => { isNodeViewDiscoveryTooltipVisible,
return ( switchNodeViewVersion,
import.meta.env.DEV || setNodeViewSwitcherDropdownOpened,
nodeViewSwitcher.value === 'true' || setNodeViewSwitcherDiscovered,
settingsStore.deploymentType === 'n8n-internal' } = useNodeViewVersionSwitcher();
);
});
const hasChanged = (prev: string[], curr: string[]) => { const hasChanged = (prev: string[], curr: string[]) => {
if (prev.length !== curr.length) { if (prev.length !== curr.length) {
@ -189,16 +187,17 @@ const workflowMenuItems = computed<ActionDropdownItem[]>(() => {
disabled: !onWorkflowPage.value || isNewWorkflow.value, disabled: !onWorkflowPage.value || isNewWorkflow.value,
}); });
if (isNodeViewSwitcherEnabled.value) { actions.push({
actions.push({ id: WORKFLOW_MENU_ACTIONS.SWITCH_NODE_VIEW_VERSION,
id: WORKFLOW_MENU_ACTIONS.SWITCH_NODE_VIEW_VERSION, ...(nodeViewSwitcherDiscovered.value
label: ? {}
nodeViewVersion.value === '2' : { badge: locale.baseText('menuActions.badge.new') }),
? locale.baseText('menuActions.switchToOldNodeViewVersion') label:
: locale.baseText('menuActions.switchToNewNodeViewVersion'), nodeViewVersion.value === '2'
disabled: !onWorkflowPage.value, ? locale.baseText('menuActions.switchToOldNodeViewVersion')
}); : locale.baseText('menuActions.switchToNewNodeViewVersion'),
} disabled: !onWorkflowPage.value,
});
if ((workflowPermissions.value.delete && !props.readOnly) || isNewWorkflow.value) { if ((workflowPermissions.value.delete && !props.readOnly) || isNewWorkflow.value) {
actions.push({ actions.push({
@ -399,6 +398,10 @@ async function handleFileImport(): Promise<void> {
} }
} }
function onWorkflowMenuOpen() {
setNodeViewSwitcherDropdownOpened();
}
async function onWorkflowMenuSelect(action: WORKFLOW_MENU_ACTIONS): Promise<void> { async function onWorkflowMenuSelect(action: WORKFLOW_MENU_ACTIONS): Promise<void> {
switch (action) { switch (action) {
case WORKFLOW_MENU_ACTIONS.DUPLICATE: { case WORKFLOW_MENU_ACTIONS.DUPLICATE: {
@ -499,6 +502,8 @@ async function onWorkflowMenuSelect(action: WORKFLOW_MENU_ACTIONS): Promise<void
break; break;
} }
case WORKFLOW_MENU_ACTIONS.SWITCH_NODE_VIEW_VERSION: { case WORKFLOW_MENU_ACTIONS.SWITCH_NODE_VIEW_VERSION: {
setNodeViewSwitcherDiscovered();
if (uiStore.stateIsDirty) { if (uiStore.stateIsDirty) {
const confirmModal = await message.confirm( const confirmModal = await message.confirm(
locale.baseText('generic.unsavedWork.confirmMessage.message'), locale.baseText('generic.unsavedWork.confirmMessage.message'),
@ -522,11 +527,7 @@ async function onWorkflowMenuSelect(action: WORKFLOW_MENU_ACTIONS): Promise<void
} }
} }
if (nodeViewVersion.value === '1') { switchNodeViewVersion();
nodeViewVersion.value = '2';
} else {
nodeViewVersion.value = '1';
}
break; break;
} }
@ -734,11 +735,17 @@ function showCreateWorkflowSuccessToast(id?: string) {
data-test-id="workflow-import-input" data-test-id="workflow-import-input"
@change="handleFileImport()" @change="handleFileImport()"
/> />
<N8nActionDropdown <N8nTooltip :visible="isNodeViewDiscoveryTooltipVisible">
:items="workflowMenuItems" <N8nActionDropdown
data-test-id="workflow-menu" :items="workflowMenuItems"
@select="onWorkflowMenuSelect" data-test-id="workflow-menu"
/> @select="onWorkflowMenuSelect"
@visible-change="onWorkflowMenuOpen"
/>
<template #content>
{{ $locale.baseText('menuActions.nodeViewDiscovery.tooltip') }}
</template>
</N8nTooltip>
</div> </div>
</PushConnectionTracker> </PushConnectionTracker>
</div> </div>

View file

@ -0,0 +1,63 @@
import { computed, ref } from 'vue';
import { useLocalStorage } from '@vueuse/core';
import { useSettingsStore } from '@/stores/settings.store';
import { useTelemetry } from '@/composables/useTelemetry';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { debouncedRef } from '@vueuse/core';
export function useNodeViewVersionSwitcher() {
const workflowsStore = useWorkflowsStore();
const settingsStore = useSettingsStore();
const telemetry = useTelemetry();
const isNewUser = computed(() => workflowsStore.activeWorkflows.length === 0);
const nodeViewVersion = useLocalStorage(
'NodeView.version',
settingsStore.deploymentType === 'n8n-internal' ? '2' : '1',
);
const nodeViewSwitcherDropdownOpened = ref(false);
function setNodeViewSwitcherDropdownOpened() {
nodeViewSwitcherDropdownOpened.value = true;
}
const nodeViewSwitcherDiscovered = useLocalStorage('NodeView.switcher.discovered', false);
function setNodeViewSwitcherDiscovered() {
nodeViewSwitcherDiscovered.value = true;
}
const isNodeViewDiscoveryTooltipVisibleRaw = computed(
() =>
nodeViewVersion.value !== '2' &&
!(
isNewUser.value ||
nodeViewSwitcherDropdownOpened.value ||
nodeViewSwitcherDiscovered.value
),
);
const isNodeViewDiscoveryTooltipVisible = debouncedRef(
isNodeViewDiscoveryTooltipVisibleRaw,
3000,
);
function switchNodeViewVersion() {
const toVersion = nodeViewVersion.value === '1' ? '2' : '1';
telemetry.track('User switched canvas version', {
to_version: toVersion,
});
nodeViewVersion.value = toVersion;
}
return {
nodeViewVersion,
nodeViewSwitcherDiscovered,
isNodeViewDiscoveryTooltipVisible,
setNodeViewSwitcherDropdownOpened,
setNodeViewSwitcherDiscovered,
switchNodeViewVersion,
};
}

View file

@ -906,6 +906,8 @@
"menuActions.delete": "Delete", "menuActions.delete": "Delete",
"menuActions.switchToNewNodeViewVersion": "Switch to new canvas", "menuActions.switchToNewNodeViewVersion": "Switch to new canvas",
"menuActions.switchToOldNodeViewVersion": "Switch to old canvas", "menuActions.switchToOldNodeViewVersion": "Switch to old canvas",
"menuActions.badge.new": "NEW",
"menuActions.nodeViewDiscovery.tooltip": "Try our new, more performant canvas",
"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,4 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useLocalStorage } from '@vueuse/core';
import { computed, watch } from 'vue'; import { computed, 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';
@ -9,20 +8,16 @@ import { MAIN_HEADER_TABS, PLACEHOLDER_EMPTY_WORKFLOW_ID, VIEWS } from '@/consta
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { useSourceControlStore } from '@/stores/sourceControl.store'; import { useSourceControlStore } from '@/stores/sourceControl.store';
import { useSettingsStore } from '@/stores/settings.store'; import { useNodeViewVersionSwitcher } from '@/composables/useNodeViewVersionSwitcher';
const workflowsStore = useWorkflowsStore(); const workflowsStore = useWorkflowsStore();
const sourceControlStore = useSourceControlStore(); const sourceControlStore = useSourceControlStore();
const settingsStore = useSettingsStore();
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const workflowHelpers = useWorkflowHelpers({ router }); const workflowHelpers = useWorkflowHelpers({ router });
const nodeViewVersion = useLocalStorage( const { nodeViewVersion } = useNodeViewVersionSwitcher();
'NodeView.version',
settingsStore.deploymentType === 'n8n-internal' ? '2' : '1',
);
const workflowId = computed<string>(() => route.params.name as string); const workflowId = computed<string>(() => route.params.name as string);