mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-14 08:34:07 -08:00
feat: add diff status and fix bugs
This commit is contained in:
parent
0a15e85d63
commit
bbb9e2fab4
|
@ -10,6 +10,7 @@ import WorkflowPreview from '@/components/WorkflowPreview.vue';
|
|||
import WorkflowHistoryListItem from '@/components/WorkflowHistory/WorkflowHistoryListItem.vue';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { compareWorkflows } from '@/utils/workflowDiff';
|
||||
import { WORKFLOW_HISTORY_ACTIONS } from '@/constants';
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
|
@ -27,7 +28,7 @@ const emit = defineEmits<{
|
|||
value: {
|
||||
action: WorkflowHistoryActionType;
|
||||
id: WorkflowVersionId;
|
||||
data: { formattedCreatedAt: string };
|
||||
data?: { formattedCreatedAt: string };
|
||||
},
|
||||
];
|
||||
}>();
|
||||
|
@ -83,67 +84,120 @@ const onAction = ({
|
|||
}) => {
|
||||
emit('action', { action, id, data });
|
||||
};
|
||||
|
||||
function onClickCloseDiff() {
|
||||
emit('action', {
|
||||
action: WORKFLOW_HISTORY_ACTIONS.CLOSEDIFF,
|
||||
id: props.workflowVersion?.versionId ?? '',
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.content">
|
||||
<div :class="$style.splitView">
|
||||
<WorkflowPreview
|
||||
v-if="props.workflowVersion"
|
||||
:workflow="workflowVersionPreview"
|
||||
:diff="workflowComparison"
|
||||
:loading="props.isListLoading"
|
||||
loader-type="spinner"
|
||||
/>
|
||||
<WorkflowPreview
|
||||
v-if="props.workflowDiff"
|
||||
:workflow="workflowDiffPreview"
|
||||
:loading="props.isListLoading"
|
||||
loader-type="spinner"
|
||||
/>
|
||||
<div v-if="props.workflowVersion" :class="$style.splitViewPanel">
|
||||
<WorkflowPreview
|
||||
:workflow="workflowVersionPreview"
|
||||
:diff="workflowComparison"
|
||||
:loading="props.isListLoading"
|
||||
loader-type="spinner"
|
||||
/>
|
||||
<div :class="$style.info">
|
||||
<WorkflowHistoryListItem
|
||||
:class="$style.card"
|
||||
:index="-1"
|
||||
:item="props.workflowVersion"
|
||||
:is-active="false"
|
||||
:actions="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="large" data-test-id="action-toggle-button">
|
||||
{{ i18n.baseText('workflowHistory.content.actions') }}
|
||||
<n8n-icon class="ml-3xs" icon="chevron-down" size="small" />
|
||||
</n8n-button>
|
||||
</template>
|
||||
</WorkflowHistoryListItem>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="props.workflowDiff" :class="$style.splitViewPanel">
|
||||
<WorkflowPreview
|
||||
:workflow="workflowDiffPreview"
|
||||
:diff="workflowComparison"
|
||||
:loading="props.isListLoading"
|
||||
loader-type="spinner"
|
||||
/>
|
||||
<div :class="$style.info">
|
||||
<WorkflowHistoryListItem
|
||||
:class="$style.card"
|
||||
:index="-1"
|
||||
:item="props.workflowDiff"
|
||||
:is-active="false"
|
||||
>
|
||||
<template #default="{ formattedCreatedAt }">
|
||||
<section :class="$style.text">
|
||||
<p>
|
||||
<span :class="$style.label">
|
||||
{{ i18n.baseText('workflowHistory.content.title') }}:
|
||||
</span>
|
||||
<time :datetime="props.workflowDiff.createdAt">{{ formattedCreatedAt }}</time>
|
||||
</p>
|
||||
<p>
|
||||
<span :class="$style.label">
|
||||
{{ i18n.baseText('workflowHistory.content.editedBy') }}:
|
||||
</span>
|
||||
<span>{{ props.workflowDiff.authors }}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span :class="$style.label">
|
||||
{{ i18n.baseText('workflowHistory.content.versionId') }}:
|
||||
</span>
|
||||
<data :value="props.workflowDiff.versionId">{{
|
||||
props.workflowDiff.versionId
|
||||
}}</data>
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
<template #button>
|
||||
<n8n-button
|
||||
type="tertiary"
|
||||
size="large"
|
||||
data-test-id="close-diff-button"
|
||||
@click="onClickCloseDiff"
|
||||
>
|
||||
{{ i18n.baseText('workflowHistory.item.actions.closediff') }}
|
||||
<n8n-icon class="ml-3xs" icon="times" size="small" />
|
||||
</n8n-button>
|
||||
</template>
|
||||
</WorkflowHistoryListItem>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul :class="$style.info">
|
||||
<WorkflowHistoryListItem
|
||||
v-if="props.workflowVersion"
|
||||
:class="$style.card"
|
||||
:index="-1"
|
||||
:item="props.workflowVersion"
|
||||
:is-active="false"
|
||||
:actions="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="large" data-test-id="action-toggle-button">
|
||||
{{ i18n.baseText('workflowHistory.content.actions') }}
|
||||
<n8n-icon class="ml-3xs" icon="chevron-down" size="small" />
|
||||
</n8n-button>
|
||||
</template>
|
||||
</WorkflowHistoryListItem>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -164,8 +218,15 @@ const onAction = ({
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
> *:first-child {
|
||||
border-right: 1px double var(--color-foreground-base);
|
||||
.splitViewPanel {
|
||||
flex: 1 0 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
&:first-child {
|
||||
border-right: 1px double var(--color-foreground-base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import { useI18n } from '@/composables/useI18n';
|
|||
const props = defineProps<{
|
||||
item: WorkflowHistory;
|
||||
index: number;
|
||||
actions: UserAction[];
|
||||
actions?: UserAction[];
|
||||
isActive: boolean;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
|
@ -116,6 +116,7 @@ onMounted(() => {
|
|||
{{ i18n.baseText('workflowHistory.item.latest') }}
|
||||
</n8n-badge>
|
||||
<n8n-action-toggle
|
||||
v-if="props.actions"
|
||||
theme="dark"
|
||||
:class="$style.actions"
|
||||
:actions="props.actions"
|
||||
|
@ -126,6 +127,7 @@ onMounted(() => {
|
|||
>
|
||||
<slot name="action-toggle-button" />
|
||||
</n8n-action-toggle>
|
||||
<slot name="button" />
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
|
|
@ -78,15 +78,7 @@ const loadWorkflow = () => {
|
|||
'*',
|
||||
);
|
||||
|
||||
if (props.diff) {
|
||||
iframeRef.value?.contentWindow?.postMessage?.(
|
||||
JSON.stringify({
|
||||
command: 'setDiff',
|
||||
diff: props.diff,
|
||||
}),
|
||||
'*',
|
||||
);
|
||||
}
|
||||
loadDiff();
|
||||
} catch (error) {
|
||||
toast.showError(
|
||||
error,
|
||||
|
@ -96,6 +88,45 @@ const loadWorkflow = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const loadDiff = () => {
|
||||
if (props.diff) {
|
||||
iframeRef.value?.contentWindow?.postMessage?.(
|
||||
JSON.stringify({
|
||||
command: 'setDiff',
|
||||
diff: props.diff,
|
||||
}),
|
||||
'*',
|
||||
);
|
||||
|
||||
fitView();
|
||||
}
|
||||
};
|
||||
|
||||
const unloadDiff = () => {
|
||||
iframeRef.value?.contentWindow?.postMessage?.(
|
||||
JSON.stringify({
|
||||
command: 'setDiff',
|
||||
diff: {},
|
||||
}),
|
||||
'*',
|
||||
);
|
||||
|
||||
fitView();
|
||||
};
|
||||
|
||||
const fitView = () => {
|
||||
if (!ready.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
iframeRef.value?.contentWindow?.postMessage?.(
|
||||
JSON.stringify({
|
||||
command: 'fitView',
|
||||
}),
|
||||
'*',
|
||||
);
|
||||
};
|
||||
|
||||
const loadExecution = () => {
|
||||
try {
|
||||
if (!props.executionId) {
|
||||
|
@ -203,6 +234,17 @@ watch(
|
|||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.diff,
|
||||
(value) => {
|
||||
if (value) {
|
||||
loadDiff();
|
||||
} else {
|
||||
unloadDiff();
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -8,6 +8,7 @@ import { useCanvasNode } from '@/composables/useCanvasNode';
|
|||
import { NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS } from '@/constants';
|
||||
import { N8nTooltip } from 'n8n-design-system';
|
||||
import type { CanvasNodeDefaultRender } from '@/types';
|
||||
import { capitalize } from 'lodash-es';
|
||||
|
||||
const $style = useCssModule();
|
||||
const i18n = useI18n();
|
||||
|
@ -30,6 +31,7 @@ const {
|
|||
executionRunning,
|
||||
hasRunData,
|
||||
hasIssues,
|
||||
diff,
|
||||
render,
|
||||
} = useCanvasNode();
|
||||
const {
|
||||
|
@ -60,6 +62,7 @@ const classes = computed(() => {
|
|||
[$style.configurable]: renderOptions.value.configurable,
|
||||
[$style.configuration]: renderOptions.value.configuration,
|
||||
[$style.trigger]: renderOptions.value.trigger,
|
||||
...(diff.value ? { [$style[`diff${capitalize(diff.value.status)}`]]: true } : {}),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -121,6 +124,7 @@ function openContextMenu(event: MouseEvent) {
|
|||
<FontAwesomeIcon icon="bolt" size="lg" />
|
||||
</div>
|
||||
</N8nTooltip>
|
||||
<CanvasNodeDiffStatus v-if="diff" :class="$style.diffStatus" />
|
||||
<CanvasNodeStatusIcons v-if="!isDisabled" :class="$style.statusIcons" />
|
||||
<CanvasNodeDisabledStrikeThrough v-if="isStrikethroughVisible" />
|
||||
<div :class="$style.description">
|
||||
|
@ -220,6 +224,28 @@ function openContextMenu(event: MouseEvent) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Diff classes
|
||||
*/
|
||||
|
||||
&.diffAdded {
|
||||
border-color: green;
|
||||
border-style: dashed;
|
||||
box-shadow: 0 0 var(--spacing-xs) 0 rgba(0, 255, 0, 0.25);
|
||||
}
|
||||
|
||||
&.diffDeleted {
|
||||
border-color: red;
|
||||
border-style: dashed;
|
||||
box-shadow: 0 0 var(--spacing-xs) 0 rgba(255, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
&.diffModified {
|
||||
border-color: orange;
|
||||
border-style: dashed;
|
||||
box-shadow: 0 0 var(--spacing-xs) 0 rgba(255, 153, 0, 0.25);
|
||||
}
|
||||
|
||||
/**
|
||||
* State classes
|
||||
* The reverse order defines the priority in case multiple states are active
|
||||
|
@ -293,6 +319,13 @@ function openContextMenu(event: MouseEvent) {
|
|||
font-weight: 400;
|
||||
}
|
||||
|
||||
.diffStatus {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.statusIcons {
|
||||
position: absolute;
|
||||
bottom: var(--canvas-node--status-icons-offset);
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, useCssModule } from 'vue';
|
||||
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { NodeDiffStatus } from '@/types';
|
||||
|
||||
const { diff } = useCanvasNode();
|
||||
|
||||
const $style = useCssModule();
|
||||
const i18n = useI18n();
|
||||
|
||||
const classes = computed(() => ({
|
||||
[$style.diffStatus]: true,
|
||||
[$style[diff.value?.status]]: diff.value,
|
||||
}));
|
||||
|
||||
const label = computed(() => (diff.value ? i18n.baseText(`diffStatus.${diff.value.status}`) : ''));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<N8nBadge v-if="diff?.status !== NodeDiffStatus.Eq" :class="classes">{{ label }}</N8nBadge>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.diffStatus {
|
||||
color: var(--prim-color-white);
|
||||
|
||||
:global(.n8n-text) {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.added {
|
||||
background: var(--color-success);
|
||||
border-color: var(--color-success);
|
||||
}
|
||||
|
||||
.modified {
|
||||
background: var(--color-warning);
|
||||
border-color: var(--color-warning);
|
||||
}
|
||||
|
||||
.deleted {
|
||||
background: var(--color-danger);
|
||||
border-color: var(--color-danger);
|
||||
}
|
||||
</style>
|
|
@ -380,6 +380,8 @@ export function useCanvasMapping({
|
|||
}, {}),
|
||||
);
|
||||
|
||||
const nodeDiffById = computed(() => workflowsStore.workflowDiff || {});
|
||||
|
||||
const additionalNodePropertiesById = computed(() => {
|
||||
type StickyNoteBoundingBox = BoundingBox & {
|
||||
id: string;
|
||||
|
@ -459,6 +461,7 @@ export function useCanvasMapping({
|
|||
disabled: node.disabled,
|
||||
inputs: nodeInputsById.value[node.id] ?? [],
|
||||
outputs: nodeOutputsById.value[node.id] ?? [],
|
||||
diff: nodeDiffById.value[node.id],
|
||||
connections: {
|
||||
[CanvasConnectionMode.Input]: inputConnections,
|
||||
[CanvasConnectionMode.Output]: outputConnections,
|
||||
|
|
|
@ -22,6 +22,7 @@ export function useCanvasNode() {
|
|||
disabled: false,
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
diff: undefined,
|
||||
connections: { [CanvasConnectionMode.Input]: {}, [CanvasConnectionMode.Output]: {} },
|
||||
issues: { items: [], visible: false },
|
||||
pinnedData: { count: 0, visible: false },
|
||||
|
@ -63,6 +64,8 @@ export function useCanvasNode() {
|
|||
const runDataIterations = computed(() => data.value.runData.iterations);
|
||||
const hasRunData = computed(() => data.value.runData.visible);
|
||||
|
||||
const diff = computed(() => data.value.diff);
|
||||
|
||||
const render = computed(() => data.value.render);
|
||||
|
||||
const eventBus = computed(() => node?.eventBus.value);
|
||||
|
@ -89,6 +92,7 @@ export function useCanvasNode() {
|
|||
executionStatus,
|
||||
executionWaiting,
|
||||
executionRunning,
|
||||
diff,
|
||||
render,
|
||||
eventBus,
|
||||
};
|
||||
|
|
|
@ -10,6 +10,7 @@ import type {
|
|||
CanvasNodeInjectionData,
|
||||
} from '@/types';
|
||||
import type { InjectionKey } from 'vue';
|
||||
import type { WorkflowHistoryActionType } from '@/types/workflowHistory';
|
||||
|
||||
export const MAX_WORKFLOW_SIZE = 1024 * 1024 * 16; // Workflow size limit in bytes
|
||||
export const MAX_EXPECTED_REQUEST_SIZE = 2048; // Expected maximum workflow request metadata (i.e. headers) size in bytes
|
||||
|
@ -878,3 +879,23 @@ export const CanvasNodeHandleKey =
|
|||
export const BROWSER_ID_STORAGE_KEY = 'n8n-browserId';
|
||||
|
||||
export const APP_MODALS_ELEMENT_ID = 'app-modals';
|
||||
|
||||
/**
|
||||
* Workflow History
|
||||
*/
|
||||
|
||||
export const workflowHistoryActionTypes: WorkflowHistoryActionType[] = [
|
||||
'showdiff',
|
||||
'closediff',
|
||||
'restore',
|
||||
'clone',
|
||||
'open',
|
||||
'download',
|
||||
];
|
||||
|
||||
export const WORKFLOW_HISTORY_ACTIONS = workflowHistoryActionTypes.reduce(
|
||||
(record, key) => ({ ...record, [key.toUpperCase()]: key }),
|
||||
{} as {
|
||||
[K in Uppercase<WorkflowHistoryActionType>]: Lowercase<K>;
|
||||
},
|
||||
);
|
||||
|
|
|
@ -2202,7 +2202,8 @@
|
|||
"workflowHistory.item.actions.clone": "Clone to new workflow",
|
||||
"workflowHistory.item.actions.open": "Open version in new tab",
|
||||
"workflowHistory.item.actions.download": "Download",
|
||||
"workflowHistory.item.actions.diff": "Show workflow diff",
|
||||
"workflowHistory.item.actions.showdiff": "Show workflow diff",
|
||||
"workflowHistory.item.actions.closediff": "Close diff",
|
||||
"workflowHistory.item.unsaved.title": "Unsaved version",
|
||||
"workflowHistory.item.latest": "Latest saved",
|
||||
"workflowHistory.empty": "No versions yet.",
|
||||
|
@ -2224,6 +2225,10 @@
|
|||
"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",
|
||||
"diffStatus.added": "Added",
|
||||
"diffStatus.modified": "Modified",
|
||||
"diffStatus.deleted": "Deleted",
|
||||
"diffStatus.equal": "Equal",
|
||||
"workflows.heading": "Workflows",
|
||||
"workflows.add": "Add workflow",
|
||||
"workflows.project.add": "Add workflow to project",
|
||||
|
|
|
@ -554,6 +554,8 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
|||
activeExecutionId.value = null;
|
||||
executingNode.value.length = 0;
|
||||
executionWaitingForWebhook.value = false;
|
||||
|
||||
workflowDiff.value = undefined;
|
||||
}
|
||||
|
||||
function addExecutingNode(nodeName: string) {
|
||||
|
|
|
@ -10,6 +10,7 @@ import type { IExecutionResponse, INodeUi } from '@/Interface';
|
|||
import type { ComputedRef, Ref } from 'vue';
|
||||
import type { PartialBy } from '@/utils/typeHelpers';
|
||||
import type { EventBus } from 'n8n-design-system';
|
||||
import { NodeDiff } from '@/types/workflowDiff.types';
|
||||
|
||||
export type CanvasConnectionPortType = NodeConnectionType;
|
||||
|
||||
|
@ -86,6 +87,7 @@ export interface CanvasNodeData {
|
|||
disabled: INodeUi['disabled'];
|
||||
inputs: CanvasConnectionPort[];
|
||||
outputs: CanvasConnectionPort[];
|
||||
diff?: NodeDiff;
|
||||
connections: {
|
||||
[CanvasConnectionMode.Input]: INodeConnections;
|
||||
[CanvasConnectionMode.Output]: INodeConnections;
|
||||
|
|
|
@ -5,4 +5,8 @@ export const enum NodeDiffStatus {
|
|||
Deleted = 'deleted',
|
||||
}
|
||||
|
||||
export type WorkflowDiff = Record<string, NodeDiffStatus>;
|
||||
export type NodeDiff = {
|
||||
status: NodeDiffStatus;
|
||||
};
|
||||
|
||||
export type WorkflowDiff = Record<string, NodeDiff>;
|
||||
|
|
|
@ -16,6 +16,12 @@ export type WorkflowVersion = WorkflowHistory & {
|
|||
connections: IConnections;
|
||||
};
|
||||
|
||||
export type WorkflowHistoryActionType = 'restore' | 'clone' | 'open' | 'download' | 'diff';
|
||||
export type WorkflowHistoryActionType =
|
||||
| 'restore'
|
||||
| 'clone'
|
||||
| 'open'
|
||||
| 'download'
|
||||
| 'showdiff'
|
||||
| 'closediff';
|
||||
|
||||
export type WorkflowHistoryRequestParams = { take: number; skip?: number };
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import type { WorkflowDiff } from '@/types/workflowDiff.types';
|
||||
import type { INodeUi, IWorkflowDb } from '@/Interface';
|
||||
import { NodeDiffStatus } from '@/types/workflowDiff.types';
|
||||
import _pick from 'lodash/pick';
|
||||
import _isEqual from 'lodash/isEqual';
|
||||
import type { INodeUi, IWorkflowDb } from '@/Interface';
|
||||
import { pick as _pick, isEqual as _isEqual } from 'lodash-es';
|
||||
|
||||
export function compareNodes(base: INodeUi, target: INodeUi): boolean {
|
||||
const propsToCompare = ['name', 'type', 'typeVersion', 'webhookId', 'credentials', 'parameters'];
|
||||
|
@ -38,17 +37,17 @@ export function compareWorkflows(
|
|||
|
||||
Object.keys(baseNodes).forEach((id) => {
|
||||
if (!targetNodes[id]) {
|
||||
diff[id] = NodeDiffStatus.Deleted;
|
||||
diff[id] = { status: NodeDiffStatus.Deleted };
|
||||
} else if (!nodesEqual(baseNodes[id], targetNodes[id])) {
|
||||
diff[id] = NodeDiffStatus.Modified;
|
||||
diff[id] = { status: NodeDiffStatus.Modified };
|
||||
} else {
|
||||
diff[id] = NodeDiffStatus.Eq;
|
||||
diff[id] = { status: NodeDiffStatus.Eq };
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(targetNodes).forEach((id) => {
|
||||
if (!baseNodes[id]) {
|
||||
diff[id] = NodeDiffStatus.Added;
|
||||
diff[id] = { status: NodeDiffStatus.Added };
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1351,6 +1351,8 @@ async function onPostMessageReceived(messageEvent: MessageEvent) {
|
|||
)) as ExecutionSummary;
|
||||
} else if (json?.command === 'setDiff') {
|
||||
workflowsStore.workflowDiff = json.diff;
|
||||
} else if (json?.command === 'fitView') {
|
||||
fitView();
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import { onBeforeMount, ref, watchEffect, computed, h } from 'vue';
|
||||
import { onBeforeMount, ref, watchEffect, computed, h, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import type { IWorkflowDb, UserAction } from '@/Interface';
|
||||
import { VIEWS, WORKFLOW_HISTORY_VERSION_RESTORE } from '@/constants';
|
||||
import {
|
||||
VIEWS,
|
||||
WORKFLOW_HISTORY_ACTIONS,
|
||||
WORKFLOW_HISTORY_VERSION_RESTORE,
|
||||
workflowHistoryActionTypes,
|
||||
} from '@/constants';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import type {
|
||||
|
@ -21,28 +26,12 @@ import { telemetry } from '@/plugins/telemetry';
|
|||
import { useRootStore } from '@/stores/root.store';
|
||||
import { getResourcePermissions } from '@/permissions';
|
||||
|
||||
type WorkflowHistoryActionRecord = {
|
||||
[K in Uppercase<WorkflowHistoryActionType>]: Lowercase<K>;
|
||||
};
|
||||
|
||||
const enum WorkflowHistoryVersionRestoreModalActions {
|
||||
restore = 'restore',
|
||||
deactivateAndRestore = 'deactivateAndRestore',
|
||||
cancel = 'cancel',
|
||||
}
|
||||
|
||||
const workflowHistoryActionTypes: WorkflowHistoryActionType[] = [
|
||||
'diff',
|
||||
'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();
|
||||
|
@ -64,9 +53,7 @@ const selectedWorkflowVersion = ref<WorkflowVersion | null>(null);
|
|||
const selectedVersionId = computed(() => normalizeSingleRouteParam('versionId'));
|
||||
|
||||
const selectedDiffWorkflowVersion = ref<WorkflowVersion | null>(null);
|
||||
const selectedDiffVersionId = computed(() =>
|
||||
route.name === VIEWS.WORKFLOW_HISTORY_DIFF ? normalizeSingleRouteParam('diffId') : null,
|
||||
);
|
||||
const selectedDiffVersionId = computed(() => normalizeSingleRouteParam('diffId'));
|
||||
|
||||
const activeVersionId = computed(() => workflowHistory.value[0]?.versionId);
|
||||
|
||||
|
@ -82,13 +69,21 @@ const workflowPermissions = computed(
|
|||
);
|
||||
|
||||
const actions = computed<UserAction[]>(() =>
|
||||
workflowHistoryActionTypes.map((value) => ({
|
||||
label: i18n.baseText(`workflowHistory.item.actions.${value}`),
|
||||
disabled:
|
||||
(value === 'clone' && !workflowPermissions.value.create) ||
|
||||
(value === 'restore' && !workflowPermissions.value.update),
|
||||
value,
|
||||
})),
|
||||
workflowHistoryActionTypes
|
||||
.filter((value) => {
|
||||
if (value === WORKFLOW_HISTORY_ACTIONS.SHOWDIFF && selectedDiffVersionId.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return value !== WORKFLOW_HISTORY_ACTIONS.CLOSEDIFF;
|
||||
})
|
||||
.map((value) => ({
|
||||
label: i18n.baseText(`workflowHistory.item.actions.${value}`),
|
||||
disabled:
|
||||
(value === 'clone' && !workflowPermissions.value.create) ||
|
||||
(value === 'restore' && !workflowPermissions.value.update),
|
||||
value,
|
||||
})),
|
||||
);
|
||||
|
||||
const isFirstItemShown = computed(
|
||||
|
@ -281,7 +276,7 @@ const onAction = async ({
|
|||
await restoreWorkflowVersion(id, data);
|
||||
sendTelemetry('User restored version');
|
||||
break;
|
||||
case WORKFLOW_HISTORY_ACTIONS.DIFF:
|
||||
case WORKFLOW_HISTORY_ACTIONS.SHOWDIFF:
|
||||
sendTelemetry('User viewed workflow diff');
|
||||
await router.push({
|
||||
name: VIEWS.WORKFLOW_HISTORY_DIFF,
|
||||
|
@ -292,6 +287,16 @@ const onAction = async ({
|
|||
},
|
||||
});
|
||||
break;
|
||||
case WORKFLOW_HISTORY_ACTIONS.CLOSEDIFF:
|
||||
sendTelemetry('User closed workflow diff');
|
||||
await router.push({
|
||||
name: VIEWS.WORKFLOW_HISTORY,
|
||||
params: {
|
||||
workflowId: workflowId.value,
|
||||
versionId: id,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
toast.showError(
|
||||
|
@ -309,6 +314,15 @@ const onPreview = async ({ event, id }: { event: MouseEvent; id: WorkflowVersion
|
|||
if (event.metaKey || event.ctrlKey) {
|
||||
openInNewTab(id);
|
||||
sendTelemetry('User opened version in new tab');
|
||||
} else if (selectedDiffVersionId.value) {
|
||||
await router.push({
|
||||
name: VIEWS.WORKFLOW_HISTORY_DIFF,
|
||||
params: {
|
||||
workflowId: workflowId.value,
|
||||
versionId: id,
|
||||
diffId: selectedDiffVersionId.value,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await router.push({
|
||||
name: VIEWS.WORKFLOW_HISTORY,
|
||||
|
@ -342,22 +356,6 @@ watchEffect(async () => {
|
|||
);
|
||||
}
|
||||
|
||||
if (selectedDiffVersionId.value) {
|
||||
try {
|
||||
selectedDiffWorkflowVersion.value = await workflowHistoryStore.getWorkflowVersion(
|
||||
workflowId.value,
|
||||
selectedDiffVersionId.value,
|
||||
);
|
||||
} catch (error) {
|
||||
toast.showError(
|
||||
new Error(`${error.message} "${selectedVersionId.value}" `),
|
||||
i18n.baseText('workflowHistory.title'),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
selectedDiffWorkflowVersion.value = null;
|
||||
}
|
||||
|
||||
try {
|
||||
activeWorkflow.value = await workflowsStore.fetchWorkflow(workflowId.value);
|
||||
} catch (error) {
|
||||
|
@ -365,6 +363,28 @@ watchEffect(async () => {
|
|||
toast.showError(error, i18n.baseText('workflowHistory.title'));
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
selectedDiffVersionId,
|
||||
async (diffVersionId) => {
|
||||
if (diffVersionId) {
|
||||
try {
|
||||
selectedDiffWorkflowVersion.value = await workflowHistoryStore.getWorkflowVersion(
|
||||
workflowId.value,
|
||||
diffVersionId,
|
||||
);
|
||||
} catch (error) {
|
||||
toast.showError(
|
||||
new Error(`${error.message} "${selectedVersionId.value}" `),
|
||||
i18n.baseText('workflowHistory.title'),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
selectedDiffWorkflowVersion.value = null;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div :class="$style.view">
|
||||
|
|
Loading…
Reference in a new issue