diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index ec52032bf7..422d8fdf27 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -2577,6 +2577,7 @@ "workflowSettings.helpTexts.workflowCallerPolicy": "Workflows that are allowed to call this workflow using the Execute Workflow node", "workflowSettings.hours": "hours", "workflowSettings.minutes": "minutes", + "workflowSettings.name": "Workflow name", "workflowSettings.noWorkflow": "- No Workflow -", "workflowSettings.save": "@:_reusableBaseText.save", "workflowSettings.saveDataErrorExecution": "Save failed production executions", diff --git a/packages/frontend/editor-ui/src/App.vue b/packages/frontend/editor-ui/src/App.vue index 6634aa1903..032ec07940 100644 --- a/packages/frontend/editor-ui/src/App.vue +++ b/packages/frontend/editor-ui/src/App.vue @@ -23,6 +23,7 @@ import { useUIStore } from '@/stores/ui.store'; import { useUsersStore } from '@/stores/users.store'; import { useSettingsStore } from '@/stores/settings.store'; import { useHistoryHelper } from '@/composables/useHistoryHelper'; +import { useWorkflowDiffRouting } from '@/composables/useWorkflowDiffRouting'; import { useStyles } from './composables/useStyles'; import { locale } from '@n8n/design-system'; import axios from 'axios'; @@ -40,6 +41,9 @@ const { setAppZIndexes } = useStyles(); // Initialize undo/redo useHistoryHelper(route); +// Initialize workflow diff routing management +useWorkflowDiffRouting(); + const loading = ref(true); const defaultLocale = computed(() => rootStore.defaultLocale); const isDemoMode = computed(() => route.name === VIEWS.DEMO); diff --git a/packages/frontend/editor-ui/src/components/MainHeader/WorkflowDetails.vue b/packages/frontend/editor-ui/src/components/MainHeader/WorkflowDetails.vue index 72c843303e..557ee86d16 100644 --- a/packages/frontend/editor-ui/src/components/MainHeader/WorkflowDetails.vue +++ b/packages/frontend/editor-ui/src/components/MainHeader/WorkflowDetails.vue @@ -7,7 +7,6 @@ import { MODAL_CONFIRM, PLACEHOLDER_EMPTY_WORKFLOW_ID, PROJECT_MOVE_RESOURCE_MODAL, - SOURCE_CONTROL_PUSH_MODAL_KEY, VIEWS, WORKFLOW_MENU_ACTIONS, WORKFLOW_SETTINGS_MODAL_KEY, @@ -41,7 +40,6 @@ import { getResourcePermissions } from '@n8n/permissions'; import { createEventBus } from '@n8n/utils/event-bus'; import { nodeViewEventBus } from '@/event-bus'; import { hasPermission } from '@/utils/rbac/permissions'; -import { useCanvasStore } from '@/stores/canvas.store'; import { useRoute, useRouter } from 'vue-router'; import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; import { computed, ref, useCssModule, useTemplateRef, watch } from 'vue'; @@ -82,7 +80,6 @@ const props = defineProps<{ const $style = useCssModule(); const rootStore = useRootStore(); -const canvasStore = useCanvasStore(); const settingsStore = useSettingsStore(); const sourceControlStore = useSourceControlStore(); const tagsStore = useTagsStore(); @@ -112,7 +109,6 @@ const tagsSaving = ref(false); const importFileRef = ref(); const tagsEventBus = createEventBus(); -const sourceControlModalEventBus = createEventBus(); const changeOwnerEventBus = createEventBus(); const hasChanged = (prev: string[], curr: string[]) => { @@ -488,15 +484,15 @@ async function onWorkflowMenuSelect(value: string): Promise { break; } case WORKFLOW_MENU_ACTIONS.PUSH: { - canvasStore.startLoading(); try { await onSaveButtonClick(); - const status = await sourceControlStore.getAggregatedStatus(); - - uiStore.openModalWithData({ - name: SOURCE_CONTROL_PUSH_MODAL_KEY, - data: { eventBus: sourceControlModalEventBus, status }, + // Navigate to route with sourceControl param - modal will handle data loading and loading states + void router.push({ + query: { + ...route.query, + sourceControl: 'push', + }, }); } catch (error) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access @@ -511,8 +507,6 @@ async function onWorkflowMenuSelect(value: string): Promise { default: toast.showError(error, locale.baseText('error')); } - } finally { - canvasStore.stopLoading(); } break; diff --git a/packages/frontend/editor-ui/src/components/MainSidebarSourceControl.test.ts b/packages/frontend/editor-ui/src/components/MainSidebarSourceControl.test.ts index af92f39f17..6831c19630 100644 --- a/packages/frontend/editor-ui/src/components/MainSidebarSourceControl.test.ts +++ b/packages/frontend/editor-ui/src/components/MainSidebarSourceControl.test.ts @@ -3,27 +3,32 @@ import { waitFor } from '@testing-library/vue'; import userEvent from '@testing-library/user-event'; import { createTestingPinia } from '@pinia/testing'; import merge from 'lodash/merge'; -import { SOURCE_CONTROL_PULL_MODAL_KEY, SOURCE_CONTROL_PUSH_MODAL_KEY } from '@/constants'; +import { reactive } from 'vue'; import { STORES } from '@n8n/stores'; import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils'; import MainSidebarSourceControl from '@/components/MainSidebarSourceControl.vue'; import { useSourceControlStore } from '@/stores/sourceControl.store'; -import { useUIStore } from '@/stores/ui.store'; import { useRBACStore } from '@/stores/rbac.store'; import { createComponentRenderer } from '@/__tests__/render'; import { useProjectsStore } from '@/stores/projects.store'; let pinia: ReturnType; let sourceControlStore: ReturnType; -let uiStore: ReturnType; let rbacStore: ReturnType; let projectStore: ReturnType; -const showMessage = vi.fn(); -const showError = vi.fn(); -const showToast = vi.fn(); -vi.mock('@/composables/useToast', () => ({ - useToast: () => ({ showMessage, showError, showToast }), +const mockRoute = reactive({ + query: {}, +}); + +const mockRouterPush = vi.fn(); + +vi.mock('vue-router', () => ({ + useRoute: () => mockRoute, + useRouter: () => ({ + push: mockRouterPush, + }), + RouterLink: vi.fn(), })); const renderComponent = createComponentRenderer(MainSidebarSourceControl); @@ -31,6 +36,13 @@ const renderComponent = createComponentRenderer(MainSidebarSourceControl); describe('MainSidebarSourceControl', () => { beforeEach(() => { vi.resetAllMocks(); + + // Reset route mock to default values + mockRoute.query = {}; + + // Reset router push mock + mockRouterPush.mockReset(); + pinia = createTestingPinia({ initialState: { [STORES.SETTINGS]: { @@ -45,8 +57,6 @@ describe('MainSidebarSourceControl', () => { sourceControlStore = useSourceControlStore(); vi.spyOn(sourceControlStore, 'isEnterpriseSourceControlEnabled', 'get').mockReturnValue(true); - - uiStore = useUIStore(); }); it('should render nothing when not instance owner', async () => { @@ -173,26 +183,7 @@ describe('MainSidebarSourceControl', () => { expect(pushButton).toBeDisabled(); }); - it('should show toast error if pull response http status code is not 409', async () => { - vi.spyOn(sourceControlStore, 'pullWorkfolder').mockRejectedValueOnce({ - response: { status: 400 }, - }); - const { getAllByRole } = renderComponent({ - pinia, - props: { isCollapsed: false }, - }); - - await userEvent.click(getAllByRole('button')[0]); - await waitFor(() => expect(showError).toHaveBeenCalled()); - }); - - it('should show confirm if pull response http status code is 409', async () => { - const status = {}; - vi.spyOn(sourceControlStore, 'pullWorkfolder').mockRejectedValueOnce({ - response: { status: 409, data: { data: status } }, - }); - const openModalSpy = vi.spyOn(uiStore, 'openModalWithData'); - + it('should navigate to pull route when pull button is clicked', async () => { const { getAllByRole } = renderComponent({ pinia, props: { isCollapsed: false }, @@ -200,20 +191,15 @@ describe('MainSidebarSourceControl', () => { await userEvent.click(getAllByRole('button')[0]); await waitFor(() => - expect(openModalSpy).toHaveBeenCalledWith( - expect.objectContaining({ - name: SOURCE_CONTROL_PULL_MODAL_KEY, - data: expect.objectContaining({ - status, - }), - }), - ), + expect(mockRouterPush).toHaveBeenCalledWith({ + query: { + sourceControl: 'pull', + }, + }), ); }); - it('should show toast when there are no changes', async () => { - vi.spyOn(sourceControlStore, 'getAggregatedStatus').mockResolvedValueOnce([]); - + it('should navigate to push route when push button is clicked', async () => { const { getAllByRole } = renderComponent({ pinia, props: { isCollapsed: false }, @@ -221,43 +207,11 @@ describe('MainSidebarSourceControl', () => { await userEvent.click(getAllByRole('button')[1]); await waitFor(() => - expect(showMessage).toHaveBeenCalledWith( - expect.objectContaining({ title: 'No changes to commit' }), - ), - ); - }); - - it('should open push modal when there are changes', async () => { - const status = [ - { - id: '014da93897f146d2b880-baa374b9d02d', - name: 'vuelfow2', - type: 'workflow' as const, - status: 'created' as const, - location: 'local' as const, - conflict: false, - file: '/014da93897f146d2b880-baa374b9d02d.json', - updatedAt: '2025-01-09T13:12:24.580Z', - }, - ]; - vi.spyOn(sourceControlStore, 'getAggregatedStatus').mockResolvedValueOnce(status); - const openModalSpy = vi.spyOn(uiStore, 'openModalWithData'); - - const { getAllByRole } = renderComponent({ - pinia, - props: { isCollapsed: false }, - }); - - await userEvent.click(getAllByRole('button')[1]); - await waitFor(() => - expect(openModalSpy).toHaveBeenCalledWith( - expect.objectContaining({ - name: SOURCE_CONTROL_PUSH_MODAL_KEY, - data: expect.objectContaining({ - status, - }), - }), - ), + expect(mockRouterPush).toHaveBeenCalledWith({ + query: { + sourceControl: 'push', + }, + }), ); }); }); diff --git a/packages/frontend/editor-ui/src/components/MainSidebarSourceControl.vue b/packages/frontend/editor-ui/src/components/MainSidebarSourceControl.vue index 5aa323623f..a9760c9c0e 100644 --- a/packages/frontend/editor-ui/src/components/MainSidebarSourceControl.vue +++ b/packages/frontend/editor-ui/src/components/MainSidebarSourceControl.vue @@ -1,34 +1,21 @@ diff --git a/packages/frontend/editor-ui/src/components/Projects/ProjectCardBadge.vue b/packages/frontend/editor-ui/src/components/Projects/ProjectCardBadge.vue index 109d7cc753..8c842f1583 100644 --- a/packages/frontend/editor-ui/src/components/Projects/ProjectCardBadge.vue +++ b/packages/frontend/editor-ui/src/components/Projects/ProjectCardBadge.vue @@ -166,9 +166,9 @@ const projectLocation = computed(() => { > - + - +