feat(editor): Workflow history [WIP]- Add restore and clone into new workflow actions (no-changelog) (#7359)

This commit is contained in:
Csaba Tuncsik 2023-10-09 13:50:08 +02:00 committed by GitHub
parent 77643e5ccb
commit b3247e5935
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 321 additions and 46 deletions

View file

@ -139,6 +139,16 @@
<DebugPaywallModal data-test-id="debug-paywall-modal" :modalName="modalName" :data="data" /> <DebugPaywallModal data-test-id="debug-paywall-modal" :modalName="modalName" :data="data" />
</template> </template>
</ModalRoot> </ModalRoot>
<ModalRoot :name="WORKFLOW_HISTORY_VERSION_RESTORE">
<template #default="{ modalName, data }">
<WorkflowHistoryVersionRestoreModal
data-test-id="workflow-history-version-restore-modal"
:modalName="modalName"
:data="data"
/>
</template>
</ModalRoot>
</div> </div>
</template> </template>
@ -172,6 +182,7 @@ import {
EXTERNAL_SECRETS_PROVIDER_MODAL_KEY, EXTERNAL_SECRETS_PROVIDER_MODAL_KEY,
DEBUG_PAYWALL_MODAL_KEY, DEBUG_PAYWALL_MODAL_KEY,
MFA_SETUP_MODAL_KEY, MFA_SETUP_MODAL_KEY,
WORKFLOW_HISTORY_VERSION_RESTORE,
} from '@/constants'; } from '@/constants';
import AboutModal from './AboutModal.vue'; import AboutModal from './AboutModal.vue';
@ -202,6 +213,7 @@ import SourceControlPushModal from '@/components/SourceControlPushModal.ee.vue';
import SourceControlPullModal from '@/components/SourceControlPullModal.ee.vue'; import SourceControlPullModal from '@/components/SourceControlPullModal.ee.vue';
import ExternalSecretsProviderModal from '@/components/ExternalSecretsProviderModal.ee.vue'; import ExternalSecretsProviderModal from '@/components/ExternalSecretsProviderModal.ee.vue';
import DebugPaywallModal from '@/components/DebugPaywallModal.vue'; import DebugPaywallModal from '@/components/DebugPaywallModal.vue';
import WorkflowHistoryVersionRestoreModal from '@/components/WorkflowHistory/WorkflowHistoryVersionRestoreModal.vue';
export default defineComponent({ export default defineComponent({
name: 'Modals', name: 'Modals',
@ -234,6 +246,7 @@ export default defineComponent({
ExternalSecretsProviderModal, ExternalSecretsProviderModal,
DebugPaywallModal, DebugPaywallModal,
MfaSetupModal, MfaSetupModal,
WorkflowHistoryVersionRestoreModal,
}, },
data: () => ({ data: () => ({
CHAT_EMBED_MODAL_KEY, CHAT_EMBED_MODAL_KEY,
@ -263,6 +276,7 @@ export default defineComponent({
EXTERNAL_SECRETS_PROVIDER_MODAL_KEY, EXTERNAL_SECRETS_PROVIDER_MODAL_KEY,
DEBUG_PAYWALL_MODAL_KEY, DEBUG_PAYWALL_MODAL_KEY,
MFA_SETUP_MODAL_KEY, MFA_SETUP_MODAL_KEY,
WORKFLOW_HISTORY_VERSION_RESTORE,
}), }),
}); });
</script> </script>

View file

@ -24,7 +24,11 @@ const props = defineProps<{
const emit = defineEmits<{ const emit = defineEmits<{
( (
event: 'action', event: 'action',
value: { action: WorkflowHistoryActionTypes[number]; id: WorkflowVersionId }, value: {
action: WorkflowHistoryActionTypes[number];
id: WorkflowVersionId;
data: { formattedCreatedAt: string };
},
): void; ): void;
(event: 'preview', value: { event: MouseEvent; id: WorkflowVersionId }): void; (event: 'preview', value: { event: MouseEvent; id: WorkflowVersionId }): void;
(event: 'loadMore', value: WorkflowHistoryRequestParams): void; (event: 'loadMore', value: WorkflowHistoryRequestParams): void;
@ -67,12 +71,14 @@ const observeElement = (element: Element) => {
const onAction = ({ const onAction = ({
action, action,
id, id,
data,
}: { }: {
action: WorkflowHistoryActionTypes[number]; action: WorkflowHistoryActionTypes[number];
id: WorkflowVersionId; id: WorkflowVersionId;
data: { formattedCreatedAt: string };
}) => { }) => {
shouldAutoScroll.value = false; shouldAutoScroll.value = false;
emit('action', { action, id }); emit('action', { action, id, data });
}; };
const onPreview = ({ event, id }: { event: MouseEvent; id: WorkflowVersionId }) => { const onPreview = ({ event, id }: { event: MouseEvent; id: WorkflowVersionId }) => {

View file

@ -18,7 +18,11 @@ const props = defineProps<{
const emit = defineEmits<{ const emit = defineEmits<{
( (
event: 'action', event: 'action',
value: { action: WorkflowHistoryActionTypes[number]; id: WorkflowVersionId }, value: {
action: WorkflowHistoryActionTypes[number];
id: WorkflowVersionId;
data: { formattedCreatedAt: string };
},
): void; ): void;
(event: 'preview', value: { event: MouseEvent; id: WorkflowVersionId }): void; (event: 'preview', value: { event: MouseEvent; id: WorkflowVersionId }): void;
(event: 'mounted', value: { index: number; offsetTop: number; isActive: boolean }): void; (event: 'mounted', value: { index: number; offsetTop: number; isActive: boolean }): void;
@ -31,11 +35,11 @@ const itemElement = ref<HTMLElement | null>(null);
const authorElement = ref<HTMLElement | null>(null); const authorElement = ref<HTMLElement | null>(null);
const isAuthorElementTruncated = ref(false); const isAuthorElementTruncated = ref(false);
const formattedCreatedAtDate = computed<string>(() => { const formattedCreatedAt = computed<string>(() => {
const currentYear = new Date().getFullYear().toString(); const currentYear = new Date().getFullYear().toString();
const [date, time] = dateformat( const [date, time] = dateformat(
props.item.createdAt, props.item.createdAt,
`${props.item.createdAt.startsWith(currentYear) ? '' : 'yyyy '} mmm d"#"HH:MM`, `${props.item.createdAt.startsWith(currentYear) ? '' : 'yyyy '}mmm d"#"HH:MM`,
).split('#'); ).split('#');
return i18n.baseText('workflowHistory.item.createdAt', { interpolate: { date, time } }); return i18n.baseText('workflowHistory.item.createdAt', { interpolate: { date, time } });
@ -60,7 +64,11 @@ const idLabel = computed<string>(() =>
); );
const onAction = (action: WorkflowHistoryActionTypes[number]) => { const onAction = (action: WorkflowHistoryActionTypes[number]) => {
emit('action', { action, id: props.item.versionId }); emit('action', {
action,
id: props.item.versionId,
data: { formattedCreatedAt: formattedCreatedAt.value },
});
}; };
const onVisibleChange = (visible: boolean) => { const onVisibleChange = (visible: boolean) => {
@ -92,7 +100,7 @@ onMounted(() => {
}" }"
> >
<p @click="onItemClick"> <p @click="onItemClick">
<time :datetime="item.createdAt">{{ formattedCreatedAtDate }}</time> <time :datetime="item.createdAt">{{ formattedCreatedAt }}</time>
<n8n-tooltip placement="right-end" :disabled="authors.size < 2 && !isAuthorElementTruncated"> <n8n-tooltip placement="right-end" :disabled="authors.size < 2 && !isAuthorElementTruncated">
<template #content>{{ props.item.authors }}</template> <template #content>{{ props.item.authors }}</template>
<span ref="authorElement">{{ authors.label }}</span> <span ref="authorElement">{{ authors.label }}</span>

View file

@ -0,0 +1,90 @@
<script lang="ts" setup>
import { useI18n } from '@/composables';
import Modal from '@/components/Modal.vue';
import { useUIStore } from '@/stores';
const props = defineProps<{
modalName: string;
data: {
isWorkflowActivated: boolean;
formattedCreatedAt: string;
beforeClose: () => void;
buttons: Array<{
text: string;
type: string;
action: () => void;
}>;
};
}>();
const i18n = useI18n();
const uiStore = useUIStore();
const closeModal = () => {
uiStore.closeModal(props.modalName);
};
</script>
<template>
<Modal width="500px" :name="props.modalName" :before-close="props.data.beforeClose">
<template #header>
<n8n-heading tag="h2" size="xlarge">
{{ i18n.baseText('workflowHistory.action.restore.modal.title') }}
</n8n-heading>
</template>
<template #content>
<div>
<n8n-text>
<i18n-t keypath="workflowHistory.action.restore.modal.subtitle" tag="span">
<template #date>
<strong>{{ props.data.formattedCreatedAt }}</strong>
</template>
</i18n-t>
<br />
<br />
<i18n-t
v-if="props.data.isWorkflowActivated"
keypath="workflowHistory.action.restore.modal.text"
tag="span"
>
<template #buttonText>
&ldquo;{{
i18n.baseText('workflowHistory.action.restore.modal.button.deactivateAndRestore')
}}&rdquo;
</template>
</i18n-t>
</n8n-text>
</div>
</template>
<template #footer>
<div :class="$style.footer">
<n8n-button
v-for="(button, index) in props.data.buttons"
size="medium"
:key="index"
:type="button.type"
@click="
() => {
button.action();
closeModal();
}
"
>
{{ button.text }}
</n8n-button>
</div>
</template>
</Modal>
</template>
<style module lang="scss">
.footer {
display: flex;
flex-direction: row;
justify-content: flex-end;
button {
margin-left: var(--spacing-2xs);
}
}
</style>

View file

@ -91,10 +91,10 @@ describe('WorkflowHistoryList', () => {
await userEvent.click(within(listItem).getByText(/ID: /)); await userEvent.click(within(listItem).getByText(/ID: /));
expect(emitted().preview).toEqual([ expect(emitted().preview).toEqual([
[ [
expect.objectContaining({ {
id: items[items.length - 1].versionId, id: items[items.length - 1].versionId,
event: expect.any(MouseEvent), event: expect.any(MouseEvent),
}), },
], ],
]); ]);
@ -141,7 +141,15 @@ describe('WorkflowHistoryList', () => {
expect(actionsDropdown).toBeInTheDocument(); expect(actionsDropdown).toBeInTheDocument();
await userEvent.click(within(actionsDropdown).getByTestId(`action-${action}`)); await userEvent.click(within(actionsDropdown).getByTestId(`action-${action}`));
expect(emitted().action).toEqual([[{ action, id: items[index].versionId }]]); expect(emitted().action).toEqual([
[
{
action,
id: items[index].versionId,
data: { formattedCreatedAt: expect.any(String) },
},
],
]);
}); });
it('should show upgrade message', async () => { it('should show upgrade message', async () => {

View file

@ -70,7 +70,9 @@ describe('WorkflowHistoryListItem', () => {
expect(getByTestId('action-toggle-dropdown')).toBeInTheDocument(); expect(getByTestId('action-toggle-dropdown')).toBeInTheDocument();
await userEvent.click(getByTestId(`action-${action}`)); await userEvent.click(getByTestId(`action-${action}`));
expect(emitted().action).toEqual([[{ action, id: item.versionId }]]); expect(emitted().action).toEqual([
[{ action, id: item.versionId, data: { formattedCreatedAt: expect.any(String) } }],
]);
expect(queryByText(/Latest saved/)).not.toBeInTheDocument(); expect(queryByText(/Latest saved/)).not.toBeInTheDocument();
expect(emitted().mounted).toEqual([[{ index: 2, isActive: true, offsetTop: 0 }]]); expect(emitted().mounted).toEqual([[{ index: 2, isActive: true, offsetTop: 0 }]]);

View file

@ -52,6 +52,7 @@ export const SOURCE_CONTROL_PUSH_MODAL_KEY = 'sourceControlPush';
export const SOURCE_CONTROL_PULL_MODAL_KEY = 'sourceControlPull'; export const SOURCE_CONTROL_PULL_MODAL_KEY = 'sourceControlPull';
export const DEBUG_PAYWALL_MODAL_KEY = 'debugPaywall'; export const DEBUG_PAYWALL_MODAL_KEY = 'debugPaywall';
export const MFA_SETUP_MODAL_KEY = 'mfaSetup'; export const MFA_SETUP_MODAL_KEY = 'mfaSetup';
export const WORKFLOW_HISTORY_VERSION_RESTORE = 'workflowHistoryVersionRestore';
export const EXTERNAL_SECRETS_PROVIDER_MODAL_KEY = 'externalSecretsProvider'; export const EXTERNAL_SECRETS_PROVIDER_MODAL_KEY = 'externalSecretsProvider';

View file

@ -1860,6 +1860,15 @@
"workflowHistory.limit": "Version history is limited to {evaluatedPruneTime} days", "workflowHistory.limit": "Version history is limited to {evaluatedPruneTime} days",
"workflowHistory.upgrade": "{link} to activate full history", "workflowHistory.upgrade": "{link} to activate full history",
"workflowHistory.upgrade.link": "Upgrade plan", "workflowHistory.upgrade.link": "Upgrade plan",
"workflowHistory.action.error.title": "Failed to {action}",
"workflowHistory.action.restore.modal.title": "Restore previous workflow version?",
"workflowHistory.action.restore.modal.subtitle": "Your workflow will revert to the version from {date}",
"workflowHistory.action.restore.modal.text": "Your workflow is currently active, so production executions will immediately start using the restored version. If you'd like to deactivate it before restoring, click {buttonText}.",
"workflowHistory.action.restore.modal.button.deactivateAndRestore": "Deactivate and restore",
"workflowHistory.action.restore.modal.button.restore": "Restore",
"workflowHistory.action.restore.modal.button.cancel": "Cancel",
"workflowHistory.action.restore.success.title": "Successfully restored workflow version",
"workflowHistory.action.clone.success.title": "Successfully cloned workflow version",
"workflows.heading": "Workflows", "workflows.heading": "Workflows",
"workflows.add": "Add Workflow", "workflows.add": "Add Workflow",
"workflows.menu.my": "My workflows", "workflows.menu.my": "My workflows",

View file

@ -36,6 +36,7 @@ import {
SOURCE_CONTROL_PULL_MODAL_KEY, SOURCE_CONTROL_PULL_MODAL_KEY,
DEBUG_PAYWALL_MODAL_KEY, DEBUG_PAYWALL_MODAL_KEY,
N8N_PRICING_PAGE_URL, N8N_PRICING_PAGE_URL,
WORKFLOW_HISTORY_VERSION_RESTORE,
} from '@/constants'; } from '@/constants';
import type { import type {
CloudUpdateLinkSourceType, CloudUpdateLinkSourceType,
@ -157,6 +158,9 @@ export const useUIStore = defineStore(STORES.UI, {
[DEBUG_PAYWALL_MODAL_KEY]: { [DEBUG_PAYWALL_MODAL_KEY]: {
open: false, open: false,
}, },
[WORKFLOW_HISTORY_VERSION_RESTORE]: {
open: false,
},
}, },
modalStack: [], modalStack: [],
sidebarMenuCollapsed: true, sidebarMenuCollapsed: true,

View file

@ -1,17 +1,22 @@
import { computed } from 'vue'; import { computed } from 'vue';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import * as whApi from '@/api/workflowHistory'; import { saveAs } from 'file-saver';
import { useRootStore } from '@/stores/n8nRoot.store'; import type { IWorkflowDataUpdate } from '@/Interface';
import { useSettingsStore } from '@/stores/settings.store';
import type { import type {
WorkflowHistory, WorkflowHistory,
WorkflowVersion, WorkflowVersion,
WorkflowHistoryRequestParams, WorkflowHistoryRequestParams,
WorkflowVersionId,
} from '@/types/workflowHistory'; } from '@/types/workflowHistory';
import * as whApi from '@/api/workflowHistory';
import { useRootStore } from '@/stores/n8nRoot.store';
import { useSettingsStore } from '@/stores/settings.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
export const useWorkflowHistoryStore = defineStore('workflowHistory', () => { export const useWorkflowHistoryStore = defineStore('workflowHistory', () => {
const rootStore = useRootStore(); const rootStore = useRootStore();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const workflowsStore = useWorkflowsStore();
const licensePruneTime = computed(() => settingsStore.settings.workflowHistory.licensePruneTime); const licensePruneTime = computed(() => settingsStore.settings.workflowHistory.licensePruneTime);
const pruneTime = computed(() => settingsStore.settings.workflowHistory.pruneTime); const pruneTime = computed(() => settingsStore.settings.workflowHistory.pruneTime);
@ -40,9 +45,65 @@ export const useWorkflowHistoryStore = defineStore('workflowHistory', () => {
return null; return null;
}); });
const downloadVersion = async (workflowId: string, workflowVersionId: WorkflowVersionId) => {
const [workflow, workflowVersion] = await Promise.all([
workflowsStore.fetchWorkflow(workflowId),
getWorkflowVersion(workflowId, workflowVersionId),
]);
if (workflow && workflowVersion) {
const { connections, nodes } = workflowVersion;
const blob = new Blob([JSON.stringify({ ...workflow, nodes, connections }, null, 2)], {
type: 'application/json;charset=utf-8',
});
saveAs(blob, `${workflow.name}-${workflowVersionId}.json`);
}
};
const cloneIntoNewWorkflow = async (
workflowId: string,
workflowVersionId: string,
data: { formattedCreatedAt: string },
) => {
const [workflow, workflowVersion] = await Promise.all([
workflowsStore.fetchWorkflow(workflowId),
getWorkflowVersion(workflowId, workflowVersionId),
]);
if (workflow && workflowVersion) {
const { connections, nodes } = workflowVersion;
const { name } = workflow;
const newWorkflowData: IWorkflowDataUpdate = {
nodes,
connections,
name: `${name} (${data.formattedCreatedAt})`,
};
await workflowsStore.createNewWorkflow(newWorkflowData);
}
};
const restoreWorkflow = async (
workflowId: string,
workflowVersionId: string,
shouldDeactivate: boolean,
) => {
const workflowVersion = await getWorkflowVersion(workflowId, workflowVersionId);
if (workflowVersion?.nodes && workflowVersion?.connections) {
const { connections, nodes } = workflowVersion;
const updateData: IWorkflowDataUpdate = { connections, nodes };
if (shouldDeactivate) {
updateData.active = false;
}
await workflowsStore.updateWorkflow(workflowId, updateData, true);
}
};
return { return {
getWorkflowHistory, getWorkflowHistory,
getWorkflowVersion, getWorkflowVersion,
downloadVersion,
cloneIntoNewWorkflow,
restoreWorkflow,
evaluatedPruneTime, evaluatedPruneTime,
shouldUpgrade, shouldUpgrade,
}; };

View file

@ -1,10 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { saveAs } from 'file-saver';
import { onBeforeMount, ref, watchEffect, computed } from 'vue'; import { onBeforeMount, ref, watchEffect, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import type { IWorkflowDb } from '@/Interface'; import type { IWorkflowDb } from '@/Interface';
import { VIEWS } from '@/constants'; import { VIEWS, WORKFLOW_HISTORY_VERSION_RESTORE } from '@/constants';
import { useI18n } from '@/composables'; import { useI18n, useToast } from '@/composables';
import type { import type {
WorkflowHistoryActionTypes, WorkflowHistoryActionTypes,
WorkflowVersionId, WorkflowVersionId,
@ -22,6 +21,12 @@ type WorkflowHistoryActionRecord = {
[K in Uppercase<WorkflowHistoryActionTypes[number]>]: Lowercase<K>; [K in Uppercase<WorkflowHistoryActionTypes[number]>]: Lowercase<K>;
}; };
const enum WorkflowHistoryVersionRestoreModalActions {
restore = 'restore',
deactivateAndRestore = 'deactivateAndRestore',
cancel = 'cancel',
}
const workflowHistoryActionTypes: WorkflowHistoryActionTypes = [ const workflowHistoryActionTypes: WorkflowHistoryActionTypes = [
'restore', 'restore',
'clone', 'clone',
@ -36,6 +41,7 @@ const WORKFLOW_HISTORY_ACTIONS = workflowHistoryActionTypes.reduce(
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const i18n = useI18n(); const i18n = useI18n();
const toast = useToast();
const workflowHistoryStore = useWorkflowHistoryStore(); const workflowHistoryStore = useWorkflowHistoryStore();
const uiStore = useUIStore(); const uiStore = useUIStore();
const workflowsStore = useWorkflowsStore(); const workflowsStore = useWorkflowsStore();
@ -102,42 +108,107 @@ const openInNewTab = (id: WorkflowVersionId) => {
window.open(href, '_blank'); window.open(href, '_blank');
}; };
const downloadVersion = async (id: WorkflowVersionId) => { const openRestorationModal = async (
const workflowVersion = await workflowHistoryStore.getWorkflowVersion( isWorkflowActivated: boolean,
route.params.workflowId, formattedCreatedAt: string,
id, ): Promise<WorkflowHistoryVersionRestoreModalActions> => {
); return new Promise((resolve, reject) => {
if (workflowVersion?.nodes && workflowVersion?.connections && activeWorkflow.value) { const buttons = [
const { connections, nodes } = workflowVersion;
const blob = new Blob(
[JSON.stringify({ ...activeWorkflow.value, nodes, connections }, null, 2)],
{ {
type: 'application/json;charset=utf-8', text: i18n.baseText('workflowHistory.action.restore.modal.button.cancel'),
type: 'tertiary',
action: () => {
resolve(WorkflowHistoryVersionRestoreModalActions.cancel);
},
}, },
); ];
saveAs(
blob, if (isWorkflowActivated) {
`${activeWorkflow.value.name.replace(/[^a-zA-Z0-9]/gi, '_')}-${ buttons.push({
workflowVersion.versionId text: i18n.baseText('workflowHistory.action.restore.modal.button.deactivateAndRestore'),
}.json`, type: 'tertiary',
); action: () => {
} resolve(WorkflowHistoryVersionRestoreModalActions.deactivateAndRestore);
},
});
}
buttons.push({
text: i18n.baseText('workflowHistory.action.restore.modal.button.restore'),
type: 'primary',
action: () => {
resolve(WorkflowHistoryVersionRestoreModalActions.restore);
},
});
try {
uiStore.openModalWithData({
name: WORKFLOW_HISTORY_VERSION_RESTORE,
data: {
beforeClose: () => {
resolve(WorkflowHistoryVersionRestoreModalActions.cancel);
},
isWorkflowActivated,
formattedCreatedAt,
buttons,
},
});
} catch (error) {
reject(error);
}
});
}; };
const onAction = async ({ const onAction = async ({
action, action,
id, id,
data,
}: { }: {
action: WorkflowHistoryActionTypes[number]; action: WorkflowHistoryActionTypes[number];
id: WorkflowVersionId; id: WorkflowVersionId;
data: { formattedCreatedAt: string };
}) => { }) => {
switch (action) { try {
case WORKFLOW_HISTORY_ACTIONS.OPEN: switch (action) {
openInNewTab(id); case WORKFLOW_HISTORY_ACTIONS.OPEN:
break; openInNewTab(id);
case WORKFLOW_HISTORY_ACTIONS.DOWNLOAD: break;
await downloadVersion(id); case WORKFLOW_HISTORY_ACTIONS.DOWNLOAD:
break; await workflowHistoryStore.downloadVersion(route.params.workflowId, id);
break;
case WORKFLOW_HISTORY_ACTIONS.CLONE:
await workflowHistoryStore.cloneIntoNewWorkflow(route.params.workflowId, id, data);
toast.showMessage({
title: i18n.baseText('workflowHistory.action.clone.success.title'),
type: 'success',
});
break;
case WORKFLOW_HISTORY_ACTIONS.RESTORE:
const workflow = await workflowsStore.fetchWorkflow(route.params.workflowId);
const modalAction = await openRestorationModal(workflow.active, data.formattedCreatedAt);
if (modalAction === WorkflowHistoryVersionRestoreModalActions.cancel) {
break;
}
await workflowHistoryStore.restoreWorkflow(
route.params.workflowId,
id,
modalAction === WorkflowHistoryVersionRestoreModalActions.deactivateAndRestore,
);
toast.showMessage({
title: i18n.baseText('workflowHistory.action.restore.success.title'),
type: 'success',
});
break;
}
} catch (error) {
toast.showError(
error,
i18n.baseText('workflowHistory.action.error.title', {
interpolate: {
action: i18n.baseText(`workflowHistory.item.actions.${action}`).toLowerCase(),
},
}),
);
} }
}; };
@ -161,10 +232,11 @@ const onUpgrade = () => {
watchEffect(async () => { watchEffect(async () => {
if (route.params.versionId) { if (route.params.versionId) {
const workflowVersion = await workflowHistoryStore.getWorkflowVersion( const [workflow, workflowVersion] = await Promise.all([
route.params.workflowId, workflowsStore.fetchWorkflow(route.params.workflowId),
route.params.versionId, workflowHistoryStore.getWorkflowVersion(route.params.workflowId, route.params.versionId),
); ]);
activeWorkflow.value = workflow;
activeWorkflowVersion.value = workflowVersion; activeWorkflowVersion.value = workflowVersion;
} }
}); });