mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 12:44:07 -08:00
feat(editor): Workflow history [WIP]- Create workflow history item preview component (no-changelog) (#7378)
Co-authored-by: Giulio Andreini <g.andreini@gmail.com>
This commit is contained in:
parent
965db8f7f2
commit
53c3379282
|
@ -122,7 +122,11 @@ export default defineComponent({
|
|||
|
||||
.activator {
|
||||
cursor: pointer;
|
||||
padding: var(--spacing-2xs);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin: 0;
|
||||
border-radius: var(--border-radius-base);
|
||||
line-height: normal !important;
|
||||
|
@ -133,7 +137,7 @@ export default defineComponent({
|
|||
|
||||
&:hover {
|
||||
background-color: var(--color-background-base);
|
||||
color: initial !important;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,12 +7,14 @@
|
|||
@command="onCommand"
|
||||
@visible-change="onVisibleChange"
|
||||
>
|
||||
<span :class="{ [$style.button]: true, [$style[theme]]: !!theme }">
|
||||
<n8n-icon
|
||||
:icon="iconOrientation === 'horizontal' ? 'ellipsis-h' : 'ellipsis-v'"
|
||||
:size="iconSize"
|
||||
/>
|
||||
</span>
|
||||
<slot>
|
||||
<span :class="{ [$style.button]: true, [$style[theme]]: !!theme }">
|
||||
<n8n-icon
|
||||
:icon="iconOrientation === 'horizontal' ? 'ellipsis-h' : 'ellipsis-v'"
|
||||
:size="iconSize"
|
||||
/>
|
||||
</span>
|
||||
</slot>
|
||||
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu data-test-id="action-toggle-dropdown">
|
||||
|
|
|
@ -722,8 +722,16 @@ $--header-spacing: 20px;
|
|||
}
|
||||
|
||||
.workflowHistoryButton {
|
||||
margin-left: var(--spacing-l);
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-left: var(--spacing-m);
|
||||
margin-right: var(--spacing-4xs);
|
||||
color: var(--color-text-dark);
|
||||
border-radius: var(--border-radius-base);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-base);
|
||||
}
|
||||
|
||||
:disabled {
|
||||
background: transparent;
|
||||
|
|
|
@ -1,14 +1,110 @@
|
|||
<script setup lang="ts">
|
||||
import type { WorkflowVersion } from '@/types/workflowHistory';
|
||||
import { computed } from 'vue';
|
||||
import type { IWorkflowDb, UserAction } from '@/Interface';
|
||||
import type {
|
||||
WorkflowVersion,
|
||||
WorkflowHistoryActionTypes,
|
||||
WorkflowVersionId,
|
||||
} from '@/types/workflowHistory';
|
||||
import WorkflowPreview from '@/components/WorkflowPreview.vue';
|
||||
import WorkflowHistoryListItem from '@/components/WorkflowHistory/WorkflowHistoryListItem.vue';
|
||||
import { useI18n } from '@/composables';
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
workflow: IWorkflowDb | null;
|
||||
workflowVersion: WorkflowVersion | null;
|
||||
actions: UserAction[];
|
||||
isListLoading?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
event: 'action',
|
||||
value: {
|
||||
action: WorkflowHistoryActionTypes[number];
|
||||
id: WorkflowVersionId;
|
||||
data: { formattedCreatedAt: string };
|
||||
},
|
||||
): void;
|
||||
}>();
|
||||
|
||||
const workflowVersionPreview = computed<IWorkflowDb | undefined>(() => {
|
||||
if (!props.workflowVersion || !props.workflow) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
...props.workflow,
|
||||
nodes: props.workflowVersion.nodes,
|
||||
connections: props.workflowVersion.connections,
|
||||
};
|
||||
});
|
||||
|
||||
const onAction = ({
|
||||
action,
|
||||
id,
|
||||
data,
|
||||
}: {
|
||||
action: WorkflowHistoryActionTypes[number];
|
||||
id: WorkflowVersionId;
|
||||
data: { formattedCreatedAt: string };
|
||||
}) => {
|
||||
emit('action', { action, id, data });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.content">
|
||||
{{ props.workflowVersion }}
|
||||
<WorkflowPreview
|
||||
v-if="props.workflowVersion"
|
||||
:workflow="workflowVersionPreview"
|
||||
:loading="props.isListLoading"
|
||||
loaderType="spinner"
|
||||
/>
|
||||
<ul :class="$style.info">
|
||||
<workflow-history-list-item
|
||||
:class="$style.card"
|
||||
v-if="props.workflowVersion"
|
||||
:full="true"
|
||||
:index="-1"
|
||||
:item="props.workflowVersion"
|
||||
:isActive="false"
|
||||
:actions="props.actions"
|
||||
@action="onAction"
|
||||
>
|
||||
<template #default="{ formattedCreatedAt }">
|
||||
<section :class="$style.text">
|
||||
<p>
|
||||
<span :class="$style.label">
|
||||
{{ i18n.baseText('workflowHistory.content.title') }}:
|
||||
</span>
|
||||
<time :datetime="props.workflowVersion.createdAt">{{ formattedCreatedAt }}</time>
|
||||
</p>
|
||||
<p>
|
||||
<span :class="$style.label">
|
||||
{{ i18n.baseText('workflowHistory.content.editedBy') }}:
|
||||
</span>
|
||||
<span>{{ props.workflowVersion.authors }}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span :class="$style.label">
|
||||
{{ i18n.baseText('workflowHistory.content.versionId') }}:
|
||||
</span>
|
||||
<data :value="props.workflowVersion.versionId">{{
|
||||
props.workflowVersion.versionId
|
||||
}}</data>
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
<template #action-toggle-button>
|
||||
<n8n-button type="tertiary" size="small" data-test-id="action-toggle-button">
|
||||
{{ i18n.baseText('workflowHistory.content.actions') }}
|
||||
<n8n-icon class="ml-3xs" icon="chevron-down" size="small" />
|
||||
</n8n-button>
|
||||
</template>
|
||||
</workflow-history-list-item>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -20,5 +116,63 @@ const props = defineProps<{
|
|||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.info {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: var(--spacing-s) var(--spacing-l) 0 var(--spacing-xl);
|
||||
border: 0;
|
||||
align-items: start;
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
|
||||
p {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
cursor: default;
|
||||
|
||||
&:first-child {
|
||||
padding-top: var(--spacing-3xs);
|
||||
padding-bottom: var(--spacing-3xs);
|
||||
* {
|
||||
font-size: var(--font-size-m);
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-top: var(--spacing-3xs);
|
||||
|
||||
* {
|
||||
font-size: var(--font-size-2xs);
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
padding-right: var(--spacing-4xs);
|
||||
}
|
||||
|
||||
* {
|
||||
max-width: unset;
|
||||
justify-self: unset;
|
||||
white-space: unset;
|
||||
overflow: hidden;
|
||||
text-overflow: unset;
|
||||
padding: 0;
|
||||
font-size: var(--font-size-s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import type { UserAction } from 'n8n-design-system';
|
||||
import { useI18n } from '@/composables';
|
||||
import type {
|
||||
|
@ -13,7 +13,7 @@ import WorkflowHistoryListItem from '@/components/WorkflowHistory/WorkflowHistor
|
|||
const props = defineProps<{
|
||||
items: WorkflowHistory[];
|
||||
activeItem: WorkflowHistory | null;
|
||||
actionTypes: WorkflowHistoryActionTypes;
|
||||
actions: UserAction[];
|
||||
requestNumberOfItems: number;
|
||||
lastReceivedItemsLength: number;
|
||||
evaluatedPruneTime: number;
|
||||
|
@ -41,14 +41,6 @@ const listElement = ref<Element | null>(null);
|
|||
const shouldAutoScroll = ref(true);
|
||||
const observer = ref<IntersectionObserver | null>(null);
|
||||
|
||||
const actions = computed<UserAction[]>(() =>
|
||||
props.actionTypes.map((value) => ({
|
||||
label: i18n.baseText(`workflowHistory.item.actions.${value}`),
|
||||
disabled: false,
|
||||
value,
|
||||
})),
|
||||
);
|
||||
|
||||
const observeElement = (element: Element) => {
|
||||
observer.value = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
|
@ -116,8 +108,8 @@ const onItemMounted = ({
|
|||
:key="item.versionId"
|
||||
:index="index"
|
||||
:item="item"
|
||||
:is-active="item.versionId === props.activeItem?.versionId"
|
||||
:actions="actions"
|
||||
:isActive="item.versionId === props.activeItem?.versionId"
|
||||
:actions="props.actions"
|
||||
@action="onAction"
|
||||
@preview="onPreview"
|
||||
@mounted="onItemMounted"
|
||||
|
|
|
@ -39,7 +39,7 @@ const formattedCreatedAt = computed<string>(() => {
|
|||
const currentYear = new Date().getFullYear().toString();
|
||||
const [date, time] = dateformat(
|
||||
props.item.createdAt,
|
||||
`${props.item.createdAt.startsWith(currentYear) ? '' : 'yyyy '}mmm d"#"HH:MM`,
|
||||
`${props.item.createdAt.startsWith(currentYear) ? '' : 'yyyy '}mmm d"#"HH:MM:ss`,
|
||||
).split('#');
|
||||
|
||||
return i18n.baseText('workflowHistory.item.createdAt', { interpolate: { date, time } });
|
||||
|
@ -99,14 +99,19 @@ onMounted(() => {
|
|||
[$style.actionsVisible]: actionsVisible,
|
||||
}"
|
||||
>
|
||||
<p @click="onItemClick">
|
||||
<time :datetime="item.createdAt">{{ formattedCreatedAt }}</time>
|
||||
<n8n-tooltip placement="right-end" :disabled="authors.size < 2 && !isAuthorElementTruncated">
|
||||
<template #content>{{ props.item.authors }}</template>
|
||||
<span ref="authorElement">{{ authors.label }}</span>
|
||||
</n8n-tooltip>
|
||||
<data :value="item.versionId">{{ idLabel }}</data>
|
||||
</p>
|
||||
<slot :formattedCreatedAt="formattedCreatedAt">
|
||||
<p @click="onItemClick">
|
||||
<time :datetime="item.createdAt">{{ formattedCreatedAt }}</time>
|
||||
<n8n-tooltip
|
||||
placement="right-end"
|
||||
:disabled="authors.size < 2 && !isAuthorElementTruncated"
|
||||
>
|
||||
<template #content>{{ props.item.authors }}</template>
|
||||
<span ref="authorElement">{{ authors.label }}</span>
|
||||
</n8n-tooltip>
|
||||
<data :value="item.versionId">{{ idLabel }}</data>
|
||||
</p>
|
||||
</slot>
|
||||
<div :class="$style.tail">
|
||||
<n8n-badge v-if="props.index === 0">
|
||||
{{ i18n.baseText('workflowHistory.item.latest') }}
|
||||
|
@ -115,10 +120,13 @@ onMounted(() => {
|
|||
theme="dark"
|
||||
:class="$style.actions"
|
||||
:actions="props.actions"
|
||||
placement="bottom-end"
|
||||
@action="onAction"
|
||||
@click.stop
|
||||
@visible-change="onVisibleChange"
|
||||
/>
|
||||
>
|
||||
<slot name="action-toggle-button" />
|
||||
</n8n-action-toggle>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
@ -136,11 +144,11 @@ onMounted(() => {
|
|||
p {
|
||||
display: grid;
|
||||
padding: var(--spacing-s);
|
||||
line-height: unset;
|
||||
cursor: pointer;
|
||||
flex: 1 1 auto;
|
||||
|
||||
time {
|
||||
padding: 0 0 var(--spacing-3xs);
|
||||
padding: 0 0 var(--spacing-5xs);
|
||||
color: var(--color-text-dark);
|
||||
font-size: var(--font-size-s);
|
||||
font-weight: var(--font-weight-bold);
|
||||
|
@ -153,7 +161,7 @@ onMounted(() => {
|
|||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-top: var(--spacing-4xs);
|
||||
margin-top: calc(var(--spacing-4xs) * -1);
|
||||
font-size: var(--font-size-2xs);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type { UserAction } from 'n8n-design-system';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import WorkflowHistoryContent from '@/components/WorkflowHistory/WorkflowHistoryContent.vue';
|
||||
import type { WorkflowHistoryActionTypes } from '@/types/workflowHistory';
|
||||
import { workflowHistoryDataFactory } from '@/stores/__tests__/utils/workflowHistoryTestUtils';
|
||||
|
||||
const actionTypes: WorkflowHistoryActionTypes = ['restore', 'clone', 'open', 'download'];
|
||||
const actions: UserAction[] = actionTypes.map((value) => ({
|
||||
label: value,
|
||||
disabled: false,
|
||||
value,
|
||||
}));
|
||||
|
||||
const renderComponent = createComponentRenderer(WorkflowHistoryContent);
|
||||
|
||||
let pinia: ReturnType<typeof createPinia>;
|
||||
|
||||
describe('WorkflowHistoryContent', () => {
|
||||
beforeEach(() => {
|
||||
pinia = createPinia();
|
||||
setActivePinia(pinia);
|
||||
});
|
||||
|
||||
it('should use the list item component to render version data', () => {
|
||||
const workflowVersion = workflowHistoryDataFactory();
|
||||
const { getByTestId } = renderComponent({
|
||||
pinia,
|
||||
props: {
|
||||
workflow: null,
|
||||
workflowVersion,
|
||||
actions,
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByTestId('workflow-history-list-item')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test.each(actionTypes)('should emit %s event', async (action) => {
|
||||
const workflowVersion = workflowHistoryDataFactory();
|
||||
const { getByTestId, emitted } = renderComponent({
|
||||
pinia,
|
||||
props: {
|
||||
workflow: null,
|
||||
workflowVersion,
|
||||
actions,
|
||||
},
|
||||
});
|
||||
|
||||
await userEvent.click(getByTestId('action-toggle-button'));
|
||||
expect(getByTestId('action-toggle-dropdown')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(getByTestId(`action-${action}`));
|
||||
expect(emitted().action).toEqual([
|
||||
[{ action, id: workflowVersion.versionId, data: { formattedCreatedAt: expect.any(String) } }],
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -2,6 +2,7 @@ import { within } from '@testing-library/dom';
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { UserAction } from 'n8n-design-system';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import WorkflowHistoryList from '@/components/WorkflowHistory/WorkflowHistoryList.vue';
|
||||
import type { WorkflowHistoryActionTypes } from '@/types/workflowHistory';
|
||||
|
@ -18,6 +19,11 @@ vi.stubGlobal(
|
|||
);
|
||||
|
||||
const actionTypes: WorkflowHistoryActionTypes = ['restore', 'clone', 'open', 'download'];
|
||||
const actions: UserAction[] = actionTypes.map((value) => ({
|
||||
label: value,
|
||||
disabled: false,
|
||||
value,
|
||||
}));
|
||||
|
||||
const renderComponent = createComponentRenderer(WorkflowHistoryList);
|
||||
|
||||
|
@ -38,7 +44,7 @@ describe('WorkflowHistoryList', () => {
|
|||
pinia,
|
||||
props: {
|
||||
items: [],
|
||||
actionTypes,
|
||||
actions,
|
||||
activeItem: null,
|
||||
requestNumberOfItems: 20,
|
||||
lastReceivedItemsLength: 0,
|
||||
|
@ -55,7 +61,7 @@ describe('WorkflowHistoryList', () => {
|
|||
pinia,
|
||||
props: {
|
||||
items: [],
|
||||
actionTypes,
|
||||
actions,
|
||||
activeItem: null,
|
||||
requestNumberOfItems: 20,
|
||||
lastReceivedItemsLength: 0,
|
||||
|
@ -76,7 +82,7 @@ describe('WorkflowHistoryList', () => {
|
|||
pinia,
|
||||
props: {
|
||||
items,
|
||||
actionTypes,
|
||||
actions,
|
||||
activeItem: null,
|
||||
requestNumberOfItems: 20,
|
||||
lastReceivedItemsLength: 20,
|
||||
|
@ -108,7 +114,7 @@ describe('WorkflowHistoryList', () => {
|
|||
pinia,
|
||||
props: {
|
||||
items,
|
||||
actionTypes,
|
||||
actions,
|
||||
activeItem: items[0],
|
||||
requestNumberOfItems: 20,
|
||||
lastReceivedItemsLength: 20,
|
||||
|
@ -126,7 +132,7 @@ describe('WorkflowHistoryList', () => {
|
|||
pinia,
|
||||
props: {
|
||||
items,
|
||||
actionTypes,
|
||||
actions,
|
||||
activeItem: null,
|
||||
requestNumberOfItems: 20,
|
||||
lastReceivedItemsLength: 20,
|
||||
|
@ -159,7 +165,7 @@ describe('WorkflowHistoryList', () => {
|
|||
pinia,
|
||||
props: {
|
||||
items,
|
||||
actionTypes,
|
||||
actions,
|
||||
activeItem: items[0],
|
||||
requestNumberOfItems: 20,
|
||||
lastReceivedItemsLength: 20,
|
||||
|
|
|
@ -36,10 +36,10 @@ describe('WorkflowHistoryListItem', () => {
|
|||
},
|
||||
});
|
||||
|
||||
await userEvent.hover(container.querySelector('.el-tooltip__trigger'));
|
||||
await userEvent.hover(container.querySelector('.el-tooltip__trigger')!);
|
||||
expect(queryByRole('tooltip')).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.click(container.querySelector('p'));
|
||||
await userEvent.click(container.querySelector('p')!);
|
||||
expect(emitted().preview).toEqual([
|
||||
[expect.objectContaining({ id: item.versionId, event: expect.any(MouseEvent) })],
|
||||
]);
|
||||
|
@ -61,7 +61,7 @@ describe('WorkflowHistoryListItem', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const authorsTag = container.querySelector('.el-tooltip__trigger');
|
||||
const authorsTag = container.querySelector('.el-tooltip__trigger')!;
|
||||
expect(authorsTag).toHaveTextContent(`${authors[0]} + ${authors.length - 1}`);
|
||||
await userEvent.hover(authorsTag);
|
||||
expect(getByRole('tooltip')).toBeInTheDocument();
|
||||
|
|
|
@ -189,6 +189,11 @@ export default defineComponent({
|
|||
this.loadExecution();
|
||||
}
|
||||
},
|
||||
workflow() {
|
||||
if (this.mode === 'workflow' && this.workflow) {
|
||||
this.loadWorkflow();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('message', this.receiveMessage);
|
||||
|
|
|
@ -1847,6 +1847,10 @@
|
|||
"workflowSettings.timeoutWorkflow": "Timeout Workflow",
|
||||
"workflowSettings.timezone": "Timezone",
|
||||
"workflowHistory.title": "Version History",
|
||||
"workflowHistory.content.title": "Version",
|
||||
"workflowHistory.content.editedBy": "Edited by",
|
||||
"workflowHistory.content.versionId": "Version ID",
|
||||
"workflowHistory.content.actions": "Actions",
|
||||
"workflowHistory.item.id": "ID: {id}",
|
||||
"workflowHistory.item.createdAt": "{date} at {time}",
|
||||
"workflowHistory.item.actions.restore": "Restore this version",
|
||||
|
|
|
@ -29,23 +29,19 @@ export const useWorkflowHistoryStore = defineStore('workflowHistory', () => {
|
|||
workflowId: string,
|
||||
queryParams: WorkflowHistoryRequestParams,
|
||||
): Promise<WorkflowHistory[]> =>
|
||||
whApi
|
||||
.getWorkflowHistory(rootStore.getRestApiContext, workflowId, queryParams)
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
return [] as WorkflowHistory[];
|
||||
});
|
||||
whApi.getWorkflowHistory(rootStore.getRestApiContext, workflowId, queryParams);
|
||||
|
||||
const getWorkflowVersion = async (
|
||||
workflowId: string,
|
||||
versionId: string,
|
||||
): Promise<WorkflowVersion | null> =>
|
||||
whApi.getWorkflowVersion(rootStore.getRestApiContext, workflowId, versionId).catch((error) => {
|
||||
console.error(error);
|
||||
return null;
|
||||
});
|
||||
whApi.getWorkflowVersion(rootStore.getRestApiContext, workflowId, versionId);
|
||||
|
||||
const downloadVersion = async (workflowId: string, workflowVersionId: WorkflowVersionId) => {
|
||||
const downloadVersion = async (
|
||||
workflowId: string,
|
||||
workflowVersionId: WorkflowVersionId,
|
||||
data: { formattedCreatedAt: string },
|
||||
) => {
|
||||
const [workflow, workflowVersion] = await Promise.all([
|
||||
workflowsStore.fetchWorkflow(workflowId),
|
||||
getWorkflowVersion(workflowId, workflowVersionId),
|
||||
|
@ -55,7 +51,7 @@ export const useWorkflowHistoryStore = defineStore('workflowHistory', () => {
|
|||
const blob = new Blob([JSON.stringify({ ...workflow, nodes, connections }, null, 2)], {
|
||||
type: 'application/json;charset=utf-8',
|
||||
});
|
||||
saveAs(blob, `${workflow.name}-${workflowVersionId}.json`);
|
||||
saveAs(blob, `${workflow.name}(${data.formattedCreatedAt}).json`);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { onBeforeMount, ref, watchEffect, computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import type { IWorkflowDb } from '@/Interface';
|
||||
import type { IWorkflowDb, UserAction } from '@/Interface';
|
||||
import { VIEWS, WORKFLOW_HISTORY_VERSION_RESTORE } from '@/constants';
|
||||
import { useI18n, useToast } from '@/composables';
|
||||
import type {
|
||||
|
@ -46,6 +46,7 @@ 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);
|
||||
|
@ -58,16 +59,13 @@ const editorRoute = computed(() => ({
|
|||
const activeWorkflow = ref<IWorkflowDb | null>(null);
|
||||
const workflowHistory = ref<WorkflowHistory[]>([]);
|
||||
const activeWorkflowVersion = ref<WorkflowVersion | null>(null);
|
||||
const activeWorkflowVersionPreview = computed<IWorkflowDb | null>(() => {
|
||||
if (activeWorkflowVersion.value && activeWorkflow.value) {
|
||||
return {
|
||||
...activeWorkflow.value,
|
||||
nodes: activeWorkflowVersion.value.nodes,
|
||||
connections: activeWorkflowVersion.value.connections,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
const actions = computed<UserAction[]>(() =>
|
||||
workflowHistoryActionTypes.map((value) => ({
|
||||
label: i18n.baseText(`workflowHistory.item.actions.${value}`),
|
||||
disabled: false,
|
||||
value,
|
||||
})),
|
||||
);
|
||||
|
||||
const loadMore = async (queryParams: WorkflowHistoryRequestParams) => {
|
||||
const history = await workflowHistoryStore.getWorkflowHistory(
|
||||
|
@ -75,25 +73,30 @@ const loadMore = async (queryParams: WorkflowHistoryRequestParams) => {
|
|||
queryParams,
|
||||
);
|
||||
lastReceivedItemsLength.value = history.length;
|
||||
workflowHistory.value.push(...history);
|
||||
workflowHistory.value = workflowHistory.value.concat(history);
|
||||
};
|
||||
|
||||
onBeforeMount(async () => {
|
||||
const [workflow] = await Promise.all([
|
||||
workflowsStore.fetchWorkflow(route.params.workflowId),
|
||||
loadMore({ take: requestNumberOfItems.value }),
|
||||
]);
|
||||
activeWorkflow.value = workflow;
|
||||
isListLoading.value = false;
|
||||
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,
|
||||
},
|
||||
});
|
||||
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'));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -174,7 +177,7 @@ const onAction = async ({
|
|||
openInNewTab(id);
|
||||
break;
|
||||
case WORKFLOW_HISTORY_ACTIONS.DOWNLOAD:
|
||||
await workflowHistoryStore.downloadVersion(route.params.workflowId, id);
|
||||
await workflowHistoryStore.downloadVersion(route.params.workflowId, id, data);
|
||||
break;
|
||||
case WORKFLOW_HISTORY_ACTIONS.CLONE:
|
||||
await workflowHistoryStore.cloneIntoNewWorkflow(route.params.workflowId, id, data);
|
||||
|
@ -194,6 +197,10 @@ const onAction = async ({
|
|||
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',
|
||||
|
@ -231,13 +238,26 @@ const onUpgrade = () => {
|
|||
};
|
||||
|
||||
watchEffect(async () => {
|
||||
if (route.params.versionId) {
|
||||
const [workflow, workflowVersion] = await Promise.all([
|
||||
workflowsStore.fetchWorkflow(route.params.workflowId),
|
||||
workflowHistoryStore.getWorkflowVersion(route.params.workflowId, route.params.versionId),
|
||||
]);
|
||||
activeWorkflow.value = workflow;
|
||||
activeWorkflowVersion.value = workflowVersion;
|
||||
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>
|
||||
|
@ -254,15 +274,13 @@ watchEffect(async () => {
|
|||
<n8n-button type="tertiary" icon="times" size="small" text square />
|
||||
</router-link>
|
||||
</div>
|
||||
<div :class="$style.contentComponentWrapper">
|
||||
<workflow-history-content :workflow-version="activeWorkflowVersionPreview" />
|
||||
</div>
|
||||
<div :class="$style.listComponentWrapper">
|
||||
<workflow-history-list
|
||||
v-if="canRender"
|
||||
:items="workflowHistory"
|
||||
:lastReceivedItemsLength="lastReceivedItemsLength"
|
||||
:activeItem="activeWorkflowVersion"
|
||||
:actionTypes="workflowHistoryActionTypes"
|
||||
:actions="actions"
|
||||
:requestNumberOfItems="requestNumberOfItems"
|
||||
:shouldUpgrade="workflowHistoryStore.shouldUpgrade"
|
||||
:evaluatedPruneTime="workflowHistoryStore.evaluatedPruneTime"
|
||||
|
@ -273,6 +291,16 @@ watchEffect(async () => {
|
|||
@upgrade="onUpgrade"
|
||||
/>
|
||||
</div>
|
||||
<div :class="$style.contentComponentWrapper">
|
||||
<workflow-history-content
|
||||
v-if="canRender"
|
||||
:workflow="activeWorkflow"
|
||||
:workflow-version="activeWorkflowVersion"
|
||||
:actions="actions"
|
||||
:isListLoading="isListLoading"
|
||||
@action="onAction"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style module lang="scss">
|
||||
|
@ -308,13 +336,11 @@ watchEffect(async () => {
|
|||
.contentComponentWrapper {
|
||||
grid-area: content;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.listComponentWrapper {
|
||||
grid-area: list;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
|
|
|
@ -9,12 +9,14 @@ import { createComponentRenderer } from '@/__tests__/render';
|
|||
import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
|
||||
import WorkflowHistoryPage from '@/views/WorkflowHistory.vue';
|
||||
import { useWorkflowHistoryStore } from '@/stores/workflowHistory.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { STORES, VIEWS } from '@/constants';
|
||||
import {
|
||||
workflowHistoryDataFactory,
|
||||
workflowVersionDataFactory,
|
||||
} from '@/stores/__tests__/utils/workflowHistoryTestUtils';
|
||||
import type { WorkflowVersion } from '@/types/workflowHistory';
|
||||
import type { IWorkflowDb } from '@/Interface';
|
||||
|
||||
vi.mock('vue-router', () => {
|
||||
const params = {};
|
||||
|
@ -63,6 +65,7 @@ let pinia: ReturnType<typeof createTestingPinia>;
|
|||
let router: ReturnType<typeof useRouter>;
|
||||
let route: ReturnType<typeof useRoute>;
|
||||
let workflowHistoryStore: ReturnType<typeof useWorkflowHistoryStore>;
|
||||
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
|
||||
let windowOpenSpy: SpyInstance;
|
||||
|
||||
describe('WorkflowHistory', () => {
|
||||
|
@ -73,9 +76,11 @@ describe('WorkflowHistory', () => {
|
|||
},
|
||||
});
|
||||
workflowHistoryStore = useWorkflowHistoryStore();
|
||||
workflowsStore = useWorkflowsStore();
|
||||
route = useRoute();
|
||||
router = useRouter();
|
||||
|
||||
vi.spyOn(workflowsStore, 'fetchWorkflow').mockResolvedValue({} as IWorkflowDb);
|
||||
vi.spyOn(workflowHistoryStore, 'getWorkflowHistory').mockResolvedValue(historyData);
|
||||
vi.spyOn(workflowHistoryStore, 'getWorkflowVersion').mockResolvedValue(versionData);
|
||||
windowOpenSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
|
||||
|
|
Loading…
Reference in a new issue