mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
fix(editor): Add tooltips to workflow history button (#10570)
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
parent
7522dde3d1
commit
4a125f511c
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -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>
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in a new issue