mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-09 03:47:29 -08:00
394 lines
10 KiB
Vue
394 lines
10 KiB
Vue
<script setup lang="ts">
|
|
import { onBeforeMount, ref, watchEffect, computed, h } from 'vue';
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
import type { IWorkflowDb, UserAction } from '@/Interface';
|
|
import { VIEWS, WORKFLOW_HISTORY_VERSION_RESTORE } from '@/constants';
|
|
import { useI18n, useToast } from '@/composables';
|
|
import type {
|
|
WorkflowHistoryActionTypes,
|
|
WorkflowVersionId,
|
|
WorkflowHistoryRequestParams,
|
|
WorkflowHistory,
|
|
WorkflowVersion,
|
|
} from '@/types/workflowHistory';
|
|
import WorkflowHistoryList from '@/components/WorkflowHistory/WorkflowHistoryList.vue';
|
|
import WorkflowHistoryContent from '@/components/WorkflowHistory/WorkflowHistoryContent.vue';
|
|
import { useWorkflowHistoryStore } from '@/stores/workflowHistory.store';
|
|
import { useUIStore } from '@/stores/ui.store';
|
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
|
|
|
type WorkflowHistoryActionRecord = {
|
|
[K in Uppercase<WorkflowHistoryActionTypes[number]>]: Lowercase<K>;
|
|
};
|
|
|
|
const enum WorkflowHistoryVersionRestoreModalActions {
|
|
restore = 'restore',
|
|
deactivateAndRestore = 'deactivateAndRestore',
|
|
cancel = 'cancel',
|
|
}
|
|
|
|
const workflowHistoryActionTypes: WorkflowHistoryActionTypes = [
|
|
'restore',
|
|
'clone',
|
|
'open',
|
|
'download',
|
|
];
|
|
const WORKFLOW_HISTORY_ACTIONS = workflowHistoryActionTypes.reduce(
|
|
(record, key) => ({ ...record, [key.toUpperCase()]: key }),
|
|
{} as WorkflowHistoryActionRecord,
|
|
);
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const i18n = useI18n();
|
|
const toast = useToast();
|
|
const workflowHistoryStore = useWorkflowHistoryStore();
|
|
const uiStore = useUIStore();
|
|
const workflowsStore = useWorkflowsStore();
|
|
|
|
const canRender = ref(true);
|
|
const isListLoading = ref(true);
|
|
const requestNumberOfItems = ref(20);
|
|
const lastReceivedItemsLength = ref(0);
|
|
const editorRoute = computed(() => ({
|
|
name: VIEWS.WORKFLOW,
|
|
params: {
|
|
name: route.params.workflowId,
|
|
},
|
|
}));
|
|
const activeWorkflow = ref<IWorkflowDb | null>(null);
|
|
const workflowHistory = ref<WorkflowHistory[]>([]);
|
|
const activeWorkflowVersion = ref<WorkflowVersion | null>(null);
|
|
const actions = computed<UserAction[]>(() =>
|
|
workflowHistoryActionTypes.map((value) => ({
|
|
label: i18n.baseText(`workflowHistory.item.actions.${value}`),
|
|
disabled: false,
|
|
value,
|
|
})),
|
|
);
|
|
|
|
const isFirstItemShown = computed(
|
|
() => workflowHistory.value[0]?.versionId === route.params.versionId,
|
|
);
|
|
const evaluatedPruneTime = computed(() => Math.floor(workflowHistoryStore.evaluatedPruneTime / 24));
|
|
|
|
const loadMore = async (queryParams: WorkflowHistoryRequestParams) => {
|
|
const history = await workflowHistoryStore.getWorkflowHistory(
|
|
route.params.workflowId,
|
|
queryParams,
|
|
);
|
|
lastReceivedItemsLength.value = history.length;
|
|
workflowHistory.value = workflowHistory.value.concat(history);
|
|
};
|
|
|
|
onBeforeMount(async () => {
|
|
try {
|
|
const [workflow] = await Promise.all([
|
|
workflowsStore.fetchWorkflow(route.params.workflowId),
|
|
loadMore({ take: requestNumberOfItems.value }),
|
|
]);
|
|
activeWorkflow.value = workflow;
|
|
isListLoading.value = false;
|
|
|
|
if (!route.params.versionId && workflowHistory.value.length) {
|
|
await router.replace({
|
|
name: VIEWS.WORKFLOW_HISTORY,
|
|
params: {
|
|
workflowId: route.params.workflowId,
|
|
versionId: workflowHistory.value[0].versionId,
|
|
},
|
|
});
|
|
}
|
|
} catch (error) {
|
|
canRender.value = false;
|
|
toast.showError(error, i18n.baseText('workflowHistory.title'));
|
|
}
|
|
});
|
|
|
|
const openInNewTab = (id: WorkflowVersionId) => {
|
|
const { href } = router.resolve({
|
|
name: VIEWS.WORKFLOW_HISTORY,
|
|
params: {
|
|
workflowId: route.params.workflowId,
|
|
versionId: id,
|
|
},
|
|
});
|
|
window.open(href, '_blank');
|
|
};
|
|
|
|
const openRestorationModal = async (
|
|
isWorkflowActivated: boolean,
|
|
formattedCreatedAt: string,
|
|
): Promise<WorkflowHistoryVersionRestoreModalActions> => {
|
|
return new Promise((resolve, reject) => {
|
|
const buttons = [
|
|
{
|
|
text: i18n.baseText('workflowHistory.action.restore.modal.button.cancel'),
|
|
type: 'tertiary',
|
|
action: () => {
|
|
resolve(WorkflowHistoryVersionRestoreModalActions.cancel);
|
|
},
|
|
},
|
|
];
|
|
|
|
if (isWorkflowActivated) {
|
|
buttons.push({
|
|
text: i18n.baseText('workflowHistory.action.restore.modal.button.deactivateAndRestore'),
|
|
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 cloneWorkflowVersion = async (
|
|
id: WorkflowVersionId,
|
|
data: { formattedCreatedAt: string },
|
|
) => {
|
|
const clonedWorkflow = await workflowHistoryStore.cloneIntoNewWorkflow(
|
|
route.params.workflowId,
|
|
id,
|
|
data,
|
|
);
|
|
const { href } = router.resolve({
|
|
name: VIEWS.WORKFLOW,
|
|
params: {
|
|
name: clonedWorkflow.id,
|
|
},
|
|
});
|
|
|
|
toast.showMessage({
|
|
title: i18n.baseText('workflowHistory.action.clone.success.title'),
|
|
message: h(
|
|
'a',
|
|
{ href, target: '_blank' },
|
|
i18n.baseText('workflowHistory.action.clone.success.message'),
|
|
),
|
|
type: 'success',
|
|
duration: 10000,
|
|
});
|
|
};
|
|
|
|
const restoreWorkflowVersion = async (
|
|
id: WorkflowVersionId,
|
|
data: { formattedCreatedAt: string },
|
|
) => {
|
|
const workflow = await workflowsStore.fetchWorkflow(route.params.workflowId);
|
|
const modalAction = await openRestorationModal(workflow.active, data.formattedCreatedAt);
|
|
if (modalAction === WorkflowHistoryVersionRestoreModalActions.cancel) {
|
|
return;
|
|
}
|
|
activeWorkflow.value = await workflowHistoryStore.restoreWorkflow(
|
|
route.params.workflowId,
|
|
id,
|
|
modalAction === WorkflowHistoryVersionRestoreModalActions.deactivateAndRestore,
|
|
);
|
|
const history = await workflowHistoryStore.getWorkflowHistory(route.params.workflowId, {
|
|
take: 1,
|
|
});
|
|
workflowHistory.value = history.concat(workflowHistory.value);
|
|
toast.showMessage({
|
|
title: i18n.baseText('workflowHistory.action.restore.success.title'),
|
|
type: 'success',
|
|
});
|
|
};
|
|
|
|
const onAction = async ({
|
|
action,
|
|
id,
|
|
data,
|
|
}: {
|
|
action: WorkflowHistoryActionTypes[number];
|
|
id: WorkflowVersionId;
|
|
data: { formattedCreatedAt: string };
|
|
}) => {
|
|
try {
|
|
switch (action) {
|
|
case WORKFLOW_HISTORY_ACTIONS.OPEN:
|
|
openInNewTab(id);
|
|
break;
|
|
case WORKFLOW_HISTORY_ACTIONS.DOWNLOAD:
|
|
await workflowHistoryStore.downloadVersion(route.params.workflowId, id, data);
|
|
break;
|
|
case WORKFLOW_HISTORY_ACTIONS.CLONE:
|
|
await cloneWorkflowVersion(id, data);
|
|
break;
|
|
case WORKFLOW_HISTORY_ACTIONS.RESTORE:
|
|
await restoreWorkflowVersion(id, data);
|
|
break;
|
|
}
|
|
} catch (error) {
|
|
toast.showError(
|
|
error,
|
|
i18n.baseText('workflowHistory.action.error.title', {
|
|
interpolate: {
|
|
action: i18n.baseText(`workflowHistory.item.actions.${action}`).toLowerCase(),
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
};
|
|
|
|
const onPreview = async ({ event, id }: { event: MouseEvent; id: WorkflowVersionId }) => {
|
|
if (event.metaKey || event.ctrlKey) {
|
|
openInNewTab(id);
|
|
} else {
|
|
await router.push({
|
|
name: VIEWS.WORKFLOW_HISTORY,
|
|
params: {
|
|
workflowId: route.params.workflowId,
|
|
versionId: id,
|
|
},
|
|
});
|
|
}
|
|
};
|
|
|
|
const onUpgrade = () => {
|
|
void uiStore.goToUpgrade('workflow-history', 'upgrade-workflow-history');
|
|
};
|
|
|
|
watchEffect(async () => {
|
|
if (!route.params.versionId) {
|
|
return;
|
|
}
|
|
try {
|
|
activeWorkflowVersion.value = await workflowHistoryStore.getWorkflowVersion(
|
|
route.params.workflowId,
|
|
route.params.versionId,
|
|
);
|
|
} catch (error) {
|
|
toast.showError(
|
|
new Error(`${error.message} "${route.params.versionId}" `),
|
|
i18n.baseText('workflowHistory.title'),
|
|
);
|
|
}
|
|
|
|
try {
|
|
activeWorkflow.value = await workflowsStore.fetchWorkflow(route.params.workflowId);
|
|
} catch (error) {
|
|
canRender.value = false;
|
|
toast.showError(error, i18n.baseText('workflowHistory.title'));
|
|
}
|
|
});
|
|
</script>
|
|
<template>
|
|
<div :class="$style.view">
|
|
<n8n-heading :class="$style.header" tag="h2" size="medium">
|
|
{{ activeWorkflow?.name }}
|
|
</n8n-heading>
|
|
<div :class="$style.corner">
|
|
<n8n-heading tag="h2" size="medium" bold>
|
|
{{ i18n.baseText('workflowHistory.title') }}
|
|
</n8n-heading>
|
|
<router-link :to="editorRoute" data-test-id="workflow-history-close-button">
|
|
<n8n-button type="tertiary" icon="times" size="small" text square />
|
|
</router-link>
|
|
</div>
|
|
<div :class="$style.listComponentWrapper">
|
|
<workflow-history-list
|
|
v-if="canRender"
|
|
:items="workflowHistory"
|
|
:lastReceivedItemsLength="lastReceivedItemsLength"
|
|
:activeItem="activeWorkflowVersion"
|
|
:actions="actions"
|
|
:requestNumberOfItems="requestNumberOfItems"
|
|
:shouldUpgrade="workflowHistoryStore.shouldUpgrade"
|
|
:evaluatedPruneTime="evaluatedPruneTime"
|
|
:isListLoading="isListLoading"
|
|
@action="onAction"
|
|
@preview="onPreview"
|
|
@load-more="loadMore"
|
|
@upgrade="onUpgrade"
|
|
/>
|
|
</div>
|
|
<div :class="$style.contentComponentWrapper">
|
|
<workflow-history-content
|
|
v-if="canRender"
|
|
:workflow="activeWorkflow"
|
|
:workflowVersion="activeWorkflowVersion"
|
|
:actions="actions"
|
|
:isListLoading="isListLoading"
|
|
:isFirstItemShown="isFirstItemShown"
|
|
@action="onAction"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<style module lang="scss">
|
|
.view {
|
|
position: relative;
|
|
display: grid;
|
|
width: 100%;
|
|
grid-template-areas: 'header corner' 'content list';
|
|
grid-template-columns: auto 330px;
|
|
grid-template-rows: 65px auto;
|
|
background-color: var(--color-background-xlight);
|
|
}
|
|
|
|
.header {
|
|
grid-area: header;
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0 var(--spacing-l);
|
|
border-bottom: var(--border-width-base) var(--border-style-base) var(--color-foreground-base);
|
|
}
|
|
|
|
.corner {
|
|
grid-area: corner;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0 var(--spacing-3xs) 0 var(--spacing-s);
|
|
background-color: var(--color-background-lighter);
|
|
border-bottom: var(--border-width-base) var(--border-style-base) var(--color-foreground-base);
|
|
border-left: var(--border-width-base) var(--border-style-base) var(--color-foreground-base);
|
|
}
|
|
|
|
.contentComponentWrapper {
|
|
grid-area: content;
|
|
position: relative;
|
|
}
|
|
|
|
.listComponentWrapper {
|
|
grid-area: list;
|
|
position: relative;
|
|
|
|
&::before {
|
|
content: '';
|
|
display: block;
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: var(--border-width-base);
|
|
background-color: var(--color-foreground-base);
|
|
}
|
|
}
|
|
</style>
|