fix(editor): Add tooltips to workflow history button (#10570)

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
Csaba Tuncsik 2024-08-28 13:06:01 +02:00 committed by GitHub
parent 7522dde3d1
commit 4a125f511c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 172 additions and 76 deletions

View file

@ -1,30 +1,22 @@
import WorkflowDetails from '@/components/MainHeader/WorkflowDetails.vue'; import WorkflowDetails from '@/components/MainHeader/WorkflowDetails.vue';
import { createComponentRenderer } from '@/__tests__/render'; import { createComponentRenderer } from '@/__tests__/render';
import { STORES, WORKFLOW_SHARE_MODAL_KEY } from '@/constants'; import { EnterpriseEditionFeature, STORES, WORKFLOW_SHARE_MODAL_KEY } from '@/constants';
import { createTestingPinia } from '@pinia/testing'; import { createTestingPinia } from '@pinia/testing';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
vi.mock('vue-router', async () => { vi.mock('vue-router', () => ({
const actual = await import('vue-router'); useRoute: () => vi.fn(),
useRouter: () => vi.fn(),
return { RouterLink: vi.fn(),
...actual, }));
useRoute: () => ({
value: {
params: {
id: '1',
},
},
}),
};
});
const initialState = { const initialState = {
[STORES.SETTINGS]: { [STORES.SETTINGS]: {
settings: { settings: {
enterprise: { enterprise: {
sharing: true, [EnterpriseEditionFeature.Sharing]: true,
[EnterpriseEditionFeature.WorkflowHistory]: true,
}, },
}, },
areTagsEnabled: true, areTagsEnabled: true,
@ -45,21 +37,26 @@ const initialState = {
const renderComponent = createComponentRenderer(WorkflowDetails, { const renderComponent = createComponentRenderer(WorkflowDetails, {
pinia: createTestingPinia({ initialState }), pinia: createTestingPinia({ initialState }),
global: {
stubs: {
RouterLink: true,
},
},
}); });
let uiStore: ReturnType<typeof useUIStore>; let uiStore: ReturnType<typeof useUIStore>;
const workflow = {
id: '1',
name: 'Test Workflow',
tags: ['1', '2'],
active: false,
};
describe('WorkflowDetails', () => { describe('WorkflowDetails', () => {
beforeEach(() => { beforeEach(() => {
uiStore = useUIStore(); uiStore = useUIStore();
}); });
it('renders workflow name and tags', async () => { it('renders workflow name and tags', async () => {
const workflow = {
id: '1',
name: 'Test Workflow',
tags: ['1', '2'],
};
const { getByTestId, getByText } = renderComponent({ const { getByTestId, getByText } = renderComponent({
props: { props: {
workflow, workflow,
@ -79,11 +76,7 @@ describe('WorkflowDetails', () => {
const onSaveButtonClick = vi.fn(); const onSaveButtonClick = vi.fn();
const { getByTestId } = renderComponent({ const { getByTestId } = renderComponent({
props: { props: {
workflow: { workflow,
id: '1',
name: 'Test Workflow',
tags: [],
},
readOnly: false, readOnly: false,
}, },
global: { global: {
@ -102,11 +95,7 @@ describe('WorkflowDetails', () => {
const { getByTestId } = renderComponent({ const { getByTestId } = renderComponent({
props: { props: {
workflow: { workflow,
id: '1',
name: 'Test Workflow',
tags: [],
},
readOnly: false, readOnly: false,
}, },
}); });

View file

@ -21,6 +21,7 @@ import SaveButton from '@/components/SaveButton.vue';
import TagsDropdown from '@/components/TagsDropdown.vue'; import TagsDropdown from '@/components/TagsDropdown.vue';
import InlineTextEdit from '@/components/InlineTextEdit.vue'; import InlineTextEdit from '@/components/InlineTextEdit.vue';
import BreakpointsObserver from '@/components/BreakpointsObserver.vue'; import BreakpointsObserver from '@/components/BreakpointsObserver.vue';
import WorkflowHistoryButton from '@/components/MainHeader/WorkflowHistoryButton.vue';
import { useRootStore } from '@/stores/root.store'; import { useRootStore } from '@/stores/root.store';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
@ -216,19 +217,6 @@ const isWorkflowHistoryFeatureEnabled = computed(() => {
return settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.WorkflowHistory]; return settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.WorkflowHistory];
}); });
const workflowHistoryRoute = computed<{ name: string; params: { workflowId: string } }>(() => {
return {
name: VIEWS.WORKFLOW_HISTORY,
params: {
workflowId: props.workflow.id,
},
};
});
const isWorkflowHistoryButtonDisabled = computed(() => {
return isNewWorkflow.value;
});
const workflowTagIds = computed(() => { const workflowTagIds = computed(() => {
return (props.workflow.tags ?? []).map((tag) => (typeof tag === 'string' ? tag : tag.id)); return (props.workflow.tags ?? []).map((tag) => (typeof tag === 'string' ? tag : tag.id));
}); });
@ -588,6 +576,10 @@ function goToUpgrade() {
void uiStore.goToUpgrade('workflow_sharing', 'upgrade-workflow-sharing'); void uiStore.goToUpgrade('workflow_sharing', 'upgrade-workflow-sharing');
} }
function goToWorkflowHistoryUpgrade() {
void uiStore.goToUpgrade('workflow-history', 'upgrade-workflow-history');
}
function showCreateWorkflowSuccessToast(id?: string) { function showCreateWorkflowSuccessToast(id?: string) {
if (!id || ['new', PLACEHOLDER_EMPTY_WORKFLOW_ID].includes(id)) { if (!id || ['new', PLACEHOLDER_EMPTY_WORKFLOW_ID].includes(id)) {
let toastTitle = locale.baseText('workflows.create.personal.toast.title'); let toastTitle = locale.baseText('workflows.create.personal.toast.title');
@ -732,20 +724,12 @@ function showCreateWorkflowSuccessToast(id?: string) {
data-test-id="workflow-save-button" data-test-id="workflow-save-button"
@click="onSaveButtonClick" @click="onSaveButtonClick"
/> />
<RouterLink <WorkflowHistoryButton
v-if="isWorkflowHistoryFeatureEnabled" :workflow-id="props.workflow.id"
:to="workflowHistoryRoute" :is-feature-enabled="isWorkflowHistoryFeatureEnabled"
:class="$style.workflowHistoryButton" :is-new-workflow="isNewWorkflow"
> @upgrade="goToWorkflowHistoryUpgrade"
<N8nIconButton />
:disabled="isWorkflowHistoryButtonDisabled"
data-test-id="workflow-history-button"
type="tertiary"
icon="history"
size="medium"
text
/>
</RouterLink>
</div> </div>
<div :class="[$style.workflowMenuContainer, $style.group]"> <div :class="[$style.workflowMenuContainer, $style.group]">
<input <input
@ -848,21 +832,4 @@ $--header-spacing: 20px;
.disabledShareButton { .disabledShareButton {
cursor: not-allowed; cursor: not-allowed;
} }
.workflowHistoryButton {
width: 30px;
height: 30px;
color: var(--color-text-dark);
border-radius: var(--border-radius-base);
&:hover {
background-color: var(--color-background-base);
}
:disabled {
background: transparent;
border: none;
opacity: 0.5;
}
}
</style> </style>

View file

@ -0,0 +1,63 @@
import { createComponentRenderer } from '@/__tests__/render';
import { within } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import WorkflowHistoryButton from '@/components/MainHeader/WorkflowHistoryButton.vue';
vi.mock('vue-router', () => ({
useRoute: () => vi.fn(),
useRouter: () => vi.fn(),
RouterLink: vi.fn(),
}));
const renderComponent = createComponentRenderer(WorkflowHistoryButton, {
global: {
stubs: {
RouterLink: true,
'router-link': {
template: '<div><slot /></div>',
},
},
},
});
describe('WorkflowHistoryButton', () => {
it('should be disabled if the feature is disabled', async () => {
const { getByRole, emitted } = renderComponent({
props: {
workflowId: '1',
isNewWorkflow: false,
isFeatureEnabled: false,
},
});
expect(getByRole('button')).toBeDisabled();
await userEvent.hover(getByRole('button'));
expect(getByRole('tooltip')).toBeVisible();
within(getByRole('tooltip')).getByText('View plans').click();
expect(emitted()).toHaveProperty('upgrade');
});
it('should be disabled if the feature is enabled but the workflow is new', async () => {
const { getByRole } = renderComponent({
props: {
workflowId: '1',
isNewWorkflow: true,
isFeatureEnabled: true,
},
});
expect(getByRole('button')).toBeDisabled();
});
it('should be enabled if the feature is enabled and the workflow is not new', async () => {
const { getByRole } = renderComponent({
props: {
workflowId: '1',
isNewWorkflow: false,
isFeatureEnabled: true,
},
});
expect(getByRole('button')).toBeEnabled();
});
});

View file

@ -0,0 +1,73 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { VIEWS } from '@/constants';
import { useI18n } from '@/composables/useI18n';
const locale = useI18n();
const props = defineProps<{
workflowId: string;
isNewWorkflow: boolean;
isFeatureEnabled: boolean;
}>();
const emit = defineEmits<{
upgrade: [];
}>();
const workflowHistoryRoute = computed<{ name: string; params: { workflowId: string } }>(() => ({
name: VIEWS.WORKFLOW_HISTORY,
params: {
workflowId: props.workflowId,
},
}));
</script>
<template>
<N8nTooltip placement="bottom">
<RouterLink :to="workflowHistoryRoute" :class="$style.workflowHistoryButton">
<N8nIconButton
:disabled="isNewWorkflow || !isFeatureEnabled"
data-test-id="workflow-history-button"
type="tertiary"
icon="history"
size="medium"
text
/>
</RouterLink>
<template #content>
<span v-if="isFeatureEnabled && isNewWorkflow">
{{ locale.baseText('workflowHistory.button.tooltip.empty') }}
</span>
<span v-else-if="isFeatureEnabled">{{
locale.baseText('workflowHistory.button.tooltip.enabled')
}}</span>
<i18n-t v-else keypath="workflowHistory.button.tooltip.disabled">
<template #link>
<N8nLink size="small" @click="emit('upgrade')">
{{ locale.baseText('workflowHistory.button.tooltip.disabled.link') }}
</N8nLink>
</template>
</i18n-t>
</template>
</N8nTooltip>
</template>
<style lang="scss" module>
.workflowHistoryButton {
width: 30px;
height: 30px;
color: var(--color-text-dark);
border-radius: var(--border-radius-base);
&:hover {
background-color: var(--color-background-base);
}
:disabled {
background: transparent;
border: none;
opacity: 0.5;
}
}
</style>

View file

@ -2166,6 +2166,10 @@
"workflowHistory.action.restore.success.title": "Successfully restored workflow version", "workflowHistory.action.restore.success.title": "Successfully restored workflow version",
"workflowHistory.action.clone.success.title": "Successfully cloned workflow version", "workflowHistory.action.clone.success.title": "Successfully cloned workflow version",
"workflowHistory.action.clone.success.message": "Open cloned workflow in a new tab", "workflowHistory.action.clone.success.message": "Open cloned workflow in a new tab",
"workflowHistory.button.tooltip.empty": "This workflow currently has no history to view. Once you've made your first save, you'll be able to view previous versions",
"workflowHistory.button.tooltip.enabled": "Workflow history to view and restore previous versions of your workflows",
"workflowHistory.button.tooltip.disabled": "Upgrade to unlock workflow history to view and restore previous versions of your workflows. {link}",
"workflowHistory.button.tooltip.disabled.link": "View plans",
"workflows.heading": "Workflows", "workflows.heading": "Workflows",
"workflows.add": "Add workflow", "workflows.add": "Add workflow",
"workflows.project.add": "Add workflow to project", "workflows.project.add": "Add workflow to project",