Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/@n8n/api-types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ export type {
} from './schemas/project.schema';

export {
isSourceControlledFileStatus,
type SourceControlledFileStatus,
type SourceControlledFile,
SOURCE_CONTROL_FILE_LOCATION,
SOURCE_CONTROL_FILE_STATUS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ const FileStatusSchema = z.enum([
]);
export const SOURCE_CONTROL_FILE_STATUS = FileStatusSchema.Values;

export type SourceControlledFileStatus = z.infer<typeof FileStatusSchema>;

export function isSourceControlledFileStatus(value: unknown): value is SourceControlledFileStatus {
return FileStatusSchema.safeParse(value).success;
}

const FileLocationSchema = z.enum(['local', 'remote']);
export const SOURCE_CONTROL_FILE_LOCATION = FileLocationSchema.Values;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,68 @@ describe('useWorkflowDiffRouting', () => {
});
});

it('should parse valid workflowStatus query parameter when diff is present', async () => {
mockRoute.value.query = {
diff: 'workflow-123',
direction: 'push',
workflowStatus: 'modified',
};

useWorkflowDiffRouting();
await nextTick();

expect(mockUiStore.openModalWithData).toHaveBeenCalledWith({
name: WORKFLOW_DIFF_MODAL_KEY,
data: {
eventBus: mockEventBus,
workflowId: 'workflow-123',
workflowStatus: 'modified',
direction: 'push',
},
});
});

it('should ignore workflowStatus when diff is not present', async () => {
mockRoute.value.query = {
direction: 'push',
workflowStatus: 'modified',
};

useWorkflowDiffRouting();
await nextTick();

expect(mockUiStore.openModalWithData).toHaveBeenCalledWith({
name: SOURCE_CONTROL_PUSH_MODAL_KEY,
data: { eventBus: mockEventBus },
});

expect(mockUiStore.openModalWithData).not.toHaveBeenCalledWith(
expect.objectContaining({
name: WORKFLOW_DIFF_MODAL_KEY,
}),
);
});

it('should ignore invalid workflowStatus values', async () => {
mockRoute.value.query = {
diff: 'workflow-123',
direction: 'push',
workflowStatus: 'invalid-status',
};

useWorkflowDiffRouting();
await nextTick();

expect(mockUiStore.openModalWithData).toHaveBeenCalledWith({
name: WORKFLOW_DIFF_MODAL_KEY,
data: {
eventBus: mockEventBus,
workflowId: 'workflow-123',
direction: 'push',
},
});
});

it('should ignore invalid diff query parameter types', async () => {
mockRoute.value.query = {
diff: ['array-value'],
Expand Down Expand Up @@ -173,6 +235,27 @@ describe('useWorkflowDiffRouting', () => {
});
});

it('should include workflowStatus in diff modal data when provided', async () => {
mockRoute.value.query = {
diff: 'workflow-456',
direction: 'pull',
workflowStatus: 'created',
};

useWorkflowDiffRouting();
await nextTick();

expect(mockUiStore.openModalWithData).toHaveBeenCalledWith({
name: WORKFLOW_DIFF_MODAL_KEY,
data: {
eventBus: mockEventBus,
workflowId: 'workflow-456',
workflowStatus: 'created',
direction: 'pull',
},
});
});

it('should not open diff modal when diff is present but direction is missing', async () => {
mockRoute.value.query = {
diff: 'workflow-456',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { watch } from 'vue';
import { useRoute } from 'vue-router';
import { createEventBus } from '@n8n/utils/event-bus';
import { isSourceControlledFileStatus, type SourceControlledFileStatus } from '@n8n/api-types';
import { useUIStore } from '@/app/stores/ui.store';
import { WORKFLOW_DIFF_MODAL_KEY } from '@/app/constants';
import {
Expand Down Expand Up @@ -48,6 +49,7 @@ export function useWorkflowDiffRouting() {

const handleDiffModal = (
diffWorkflowId: string | undefined,
diffWorkflowStatus: SourceControlledFileStatus | undefined,
direction: Direction | undefined,
) => {
const shouldOpen = diffWorkflowId && direction;
Expand All @@ -59,6 +61,7 @@ export function useWorkflowDiffRouting() {
data: {
eventBus: workflowDiffEventBus,
workflowId: diffWorkflowId,
workflowStatus: diffWorkflowStatus,
direction,
},
});
Expand Down Expand Up @@ -107,6 +110,10 @@ export function useWorkflowDiffRouting() {

const handleRouteChange = () => {
const diffWorkflowId = typeof route.query.diff === 'string' ? route.query.diff : undefined;
const diffWorkflowStatus =
diffWorkflowId && isSourceControlledFileStatus(route.query.workflowStatus)
? route.query.workflowStatus
: undefined;
const direction =
typeof route.query.direction === 'string' &&
(route.query.direction === 'push' || route.query.direction === 'pull')
Expand All @@ -118,7 +125,7 @@ export function useWorkflowDiffRouting() {
? route.query.sourceControl
: undefined;

handleDiffModal(diffWorkflowId, direction);
handleDiffModal(diffWorkflowId, diffWorkflowStatus, direction);
handleSourceControlModals(sourceControl, diffWorkflowId, direction);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,20 @@ const mockRoute = reactive({
name: '',
params: {},
fullPath: '',
query: {},
});

const mockRouterInstance = {
back: vi.fn(),
push: vi.fn(),
replace: vi.fn(),
go: vi.fn(),
currentRoute: { value: mockRoute },
};

vi.mock('vue-router', () => ({
useRoute: () => mockRoute,
useRouter: () => ({
back: vi.fn(),
push: vi.fn(),
replace: vi.fn(),
go: vi.fn(),
}),
useRouter: () => mockRouterInstance,
RouterLink: {
template: '<a><slot></slot></a>',
props: ['to', 'target'],
Expand Down Expand Up @@ -859,6 +863,59 @@ describe('SourceControlPushModal', () => {
});
});

describe('workflow diff button', () => {
beforeEach(() => {
settingsStore.settings.enterprise.workflowDiffs = true;
vi.clearAllMocks();
});

it('should set workflowStatus url param when diff button is clicked for created workflow', async () => {
const status: SourceControlledFile[] = [
{
id: 'workflow-2',
name: 'New workflow',
type: 'workflow',
status: 'created',
location: 'local',
conflict: false,
file: '/home/user/.n8n/git/workflows/workflow-2.json',
updatedAt: '2024-09-20T10:31:40.000Z',
},
];

sourceControlStore.getAggregatedStatus.mockResolvedValue(status);

const { getByTestId, getByText, getAllByTestId } = renderModal({
pinia,
props: {
data: {
eventBus,
status,
},
},
});

await waitFor(() => {
expect(getByText('Commit and push changes')).toBeInTheDocument();
});

await waitFor(() => {
expect(getAllByTestId('source-control-push-modal-file-checkbox')).toHaveLength(1);
});

const compareButton = getByTestId('source-control-workflow-diff-button');
await userEvent.click(compareButton);

expect(mockRouterInstance.push).toHaveBeenCalledWith({
query: expect.objectContaining({
diff: 'workflow-2',
workflowStatus: 'created',
direction: 'push',
}),
});
});
});

describe('Enter key behavior', () => {
it('should trigger commit and push when Enter is pressed and submit is enabled', async () => {
const status: SourceControlledFile[] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import type {
} from '@/features/collaboration/projects/projects.types';
import { ResourceType } from '@/features/collaboration/projects/projects.utils';
import { getPushPriorityByStatus, getStatusText, getStatusTheme } from '../sourceControl.utils';
import type { SourceControlledFile } from '@n8n/api-types';
import type { SourceControlledFile, SourceControlledFileStatus } from '@n8n/api-types';
import {
ROLE,
SOURCE_CONTROL_FILE_LOCATION,
Expand Down Expand Up @@ -134,8 +134,6 @@ const projectsForFilters = computed(() => {
const concatenateWithAnd = (messages: string[]) =>
new Intl.ListFormat(i18n.locale, { style: 'long', type: 'conjunction' }).format(messages);

type SourceControlledFileStatus = SourceControlledFile['status'];

type SourceControlledFileWithProject = SourceControlledFile & { project?: ProjectListItem };

type Changes = {
Expand Down Expand Up @@ -665,7 +663,7 @@ function castProject(project: ProjectListItem): WorkflowResource {
return resource;
}

function openDiffModal(id: string) {
function openDiffModal(id: string, workflowStatus: SourceControlledFileStatus) {
telemetry.track('User clicks compare workflows', {
workflow_id: id,
context: 'source_control_push',
Expand All @@ -676,6 +674,7 @@ function openDiffModal(id: string) {
query: {
...route.query,
diff: id,
workflowStatus,
direction: 'push',
},
});
Expand Down Expand Up @@ -929,9 +928,10 @@ onMounted(async () => {
placement="top"
>
<N8nIconButton
data-test-id="source-control-workflow-diff-button"
icon="file-diff"
type="secondary"
@click="openDiffModal(file.id)"
@click="openDiffModal(file.id, file.status)"
/>
</N8nTooltip>
</template>
Expand Down
Loading
Loading