diff --git a/cypress/e2e/10-undo-redo.cy.ts b/cypress/e2e/10-undo-redo.cy.ts
index bad159b1cb..f87fee5b78 100644
--- a/cypress/e2e/10-undo-redo.cy.ts
+++ b/cypress/e2e/10-undo-redo.cy.ts
@@ -6,12 +6,10 @@ import {
MANUAL_TRIGGER_NODE_NAME,
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
} from '../constants';
-import { MessageBox as MessageBoxClass } from '../pages/modals/message-box';
import { NDV } from '../pages/ndv';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
const WorkflowPage = new WorkflowPageClass();
-const messageBox = new MessageBoxClass();
const ndv = new NDV();
describe('Undo/Redo', () => {
@@ -256,11 +254,11 @@ describe('Undo/Redo', () => {
WorkflowPage.getters.workflowMenuItemImportFromURLItem().should('be.visible');
WorkflowPage.getters.workflowMenuItemImportFromURLItem().click();
// Try while prompt is open
- messageBox.getters.header().click();
+ WorkflowPage.getters.inputURLImportWorkflowFromURL().click();
WorkflowPage.actions.hitUndo();
WorkflowPage.getters.canvasNodes().should('have.have.length', 1);
// Close prompt and try again
- messageBox.actions.cancel();
+ WorkflowPage.getters.cancelActionImportWorkflowFromURL().click();
WorkflowPage.actions.hitUndo();
WorkflowPage.getters.canvasNodes().should('have.have.length', 0);
});
diff --git a/cypress/e2e/39-import-workflow.cy.ts b/cypress/e2e/39-import-workflow.cy.ts
index f92790eb3b..1d31c0c611 100644
--- a/cypress/e2e/39-import-workflow.cy.ts
+++ b/cypress/e2e/39-import-workflow.cy.ts
@@ -1,9 +1,7 @@
import { WorkflowPage } from '../pages';
-import { MessageBox as MessageBoxClass } from '../pages/modals/message-box';
import { errorToast, successToast } from '../pages/notifications';
const workflowPage = new WorkflowPage();
-const messageBox = new MessageBoxClass();
before(() => {
cy.fixture('Onboarding_workflow.json').then((data) => {
@@ -20,11 +18,13 @@ describe('Import workflow', () => {
workflowPage.getters.workflowMenu().click();
workflowPage.getters.workflowMenuItemImportFromURLItem().click();
- messageBox.getters.modal().should('be.visible');
+ workflowPage.getters.inputURLImportWorkflowFromURL().should('be.visible');
- messageBox.getters.content().type('https://fakepage.com/workflow.json');
+ workflowPage.getters
+ .inputURLImportWorkflowFromURL()
+ .type('https://fakepage.com/workflow.json');
- messageBox.getters.confirm().click();
+ workflowPage.getters.confirmActionImportWorkflowFromURL().click();
workflowPage.actions.zoomToFit();
@@ -37,7 +37,6 @@ describe('Import workflow', () => {
it('clicking outside modal should not show error toast', () => {
workflowPage.actions.visit(true);
-
workflowPage.getters.workflowMenu().click();
workflowPage.getters.workflowMenuItemImportFromURLItem().click();
@@ -51,7 +50,7 @@ describe('Import workflow', () => {
workflowPage.getters.workflowMenu().click();
workflowPage.getters.workflowMenuItemImportFromURLItem().click();
- messageBox.getters.cancel().click();
+ workflowPage.getters.cancelActionImportWorkflowFromURL().click();
errorToast().should('not.exist');
});
diff --git a/cypress/pages/workflow.ts b/cypress/pages/workflow.ts
index 2fdd7844ea..4acdeb79e8 100644
--- a/cypress/pages/workflow.ts
+++ b/cypress/pages/workflow.ts
@@ -219,6 +219,9 @@ export class WorkflowPage extends BasePage {
}
return parseFloat(element.css('top'));
},
+ inputURLImportWorkflowFromURL: () => cy.getByTestId('workflow-url-import-input'),
+ cancelActionImportWorkflowFromURL: () => cy.getByTestId('cancel-workflow-import-url-button'),
+ confirmActionImportWorkflowFromURL: () => cy.getByTestId('confirm-workflow-import-url-button'),
confirmModal: () => cy.get('div[role=dialog][aria-modal=true]'),
};
diff --git a/packages/frontend/editor-ui/src/components/ImportWorkflowUrlModal.test.ts b/packages/frontend/editor-ui/src/components/ImportWorkflowUrlModal.test.ts
new file mode 100644
index 0000000000..f424f5d04d
--- /dev/null
+++ b/packages/frontend/editor-ui/src/components/ImportWorkflowUrlModal.test.ts
@@ -0,0 +1,88 @@
+import { createComponentRenderer } from '@/__tests__/render';
+import ImportWorkflowUrlModal from './ImportWorkflowUrlModal.vue';
+import { createTestingPinia } from '@pinia/testing';
+import { useUIStore } from '@/stores/ui.store';
+import { nodeViewEventBus } from '@/event-bus';
+import { IMPORT_WORKFLOW_URL_MODAL_KEY } from '@/constants';
+import userEvent from '@testing-library/user-event';
+
+const ModalStub = {
+ template: `
+
+
+
+
+
+
+ `,
+};
+
+const initialState = {
+ modalsById: {
+ [IMPORT_WORKFLOW_URL_MODAL_KEY]: {
+ open: true,
+ },
+ },
+ modalStack: [IMPORT_WORKFLOW_URL_MODAL_KEY],
+};
+
+const global = {
+ stubs: {
+ Modal: ModalStub,
+ },
+};
+
+const renderModal = createComponentRenderer(ImportWorkflowUrlModal);
+let pinia: ReturnType;
+
+describe('ImportWorkflowUrlModal', () => {
+ beforeEach(() => {
+ pinia = createTestingPinia({ initialState });
+ });
+
+ it('should close the modal on cancel', async () => {
+ const { getByTestId } = renderModal({
+ global,
+ pinia,
+ });
+
+ const uiStore = useUIStore();
+
+ await userEvent.click(getByTestId('cancel-workflow-import-url-button'));
+
+ expect(uiStore.closeModal).toHaveBeenCalledWith(IMPORT_WORKFLOW_URL_MODAL_KEY);
+ });
+
+ it('should emit importWorkflowUrl event on confirm', async () => {
+ const { getByTestId } = renderModal({
+ global,
+ pinia,
+ });
+
+ const urlInput = getByTestId('workflow-url-import-input');
+ const confirmButton = getByTestId('confirm-workflow-import-url-button');
+
+ await userEvent.type(urlInput, 'https://valid-url.com/workflow.json');
+ expect(confirmButton).toBeEnabled();
+
+ const emitSpy = vi.spyOn(nodeViewEventBus, 'emit');
+ await userEvent.click(confirmButton);
+
+ expect(emitSpy).toHaveBeenCalledWith('importWorkflowUrl', {
+ url: 'https://valid-url.com/workflow.json',
+ });
+ });
+
+ it('should disable confirm button for invalid URL', async () => {
+ const { getByTestId } = renderModal({
+ global,
+ pinia,
+ });
+
+ const urlInput = getByTestId('workflow-url-import-input');
+ const confirmButton = getByTestId('confirm-workflow-import-url-button');
+
+ await userEvent.type(urlInput, 'invalid-url');
+ expect(confirmButton).toBeDisabled();
+ });
+});
diff --git a/packages/frontend/editor-ui/src/components/ImportWorkflowUrlModal.vue b/packages/frontend/editor-ui/src/components/ImportWorkflowUrlModal.vue
new file mode 100644
index 0000000000..f04f000b11
--- /dev/null
+++ b/packages/frontend/editor-ui/src/components/ImportWorkflowUrlModal.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+ {{ i18n.baseText('mainSidebar.prompt.invalidUrl') }}
+
+
+
+
+
+
+ {{ i18n.baseText('mainSidebar.prompt.import') }}
+
+
+ {{ i18n.baseText('mainSidebar.prompt.cancel') }}
+
+
+
+
+
+
+
diff --git a/packages/frontend/editor-ui/src/components/MainHeader/WorkflowDetails.vue b/packages/frontend/editor-ui/src/components/MainHeader/WorkflowDetails.vue
index 05c37df349..3373766ba1 100644
--- a/packages/frontend/editor-ui/src/components/MainHeader/WorkflowDetails.vue
+++ b/packages/frontend/editor-ui/src/components/MainHeader/WorkflowDetails.vue
@@ -6,11 +6,11 @@ import {
MODAL_CONFIRM,
PLACEHOLDER_EMPTY_WORKFLOW_ID,
SOURCE_CONTROL_PUSH_MODAL_KEY,
- VALID_WORKFLOW_IMPORT_URL_REGEX,
VIEWS,
WORKFLOW_MENU_ACTIONS,
WORKFLOW_SETTINGS_MODAL_KEY,
WORKFLOW_SHARE_MODAL_KEY,
+ IMPORT_WORKFLOW_URL_MODAL_KEY,
} from '@/constants';
import ShortenName from '@/components/ShortenName.vue';
import WorkflowTagsContainer from '@/components/WorkflowTagsContainer.vue';
@@ -476,24 +476,7 @@ async function onWorkflowMenuSelect(action: WORKFLOW_MENU_ACTIONS): Promise
@@ -124,6 +126,10 @@ import type { EventBus } from '@n8n/utils/event-bus';
+
+
+
+
diff --git a/packages/frontend/editor-ui/src/constants.ts b/packages/frontend/editor-ui/src/constants.ts
index 53984ea8cd..72d9001c27 100644
--- a/packages/frontend/editor-ui/src/constants.ts
+++ b/packages/frontend/editor-ui/src/constants.ts
@@ -51,6 +51,7 @@ export const CREDENTIAL_SELECT_MODAL_KEY = 'selectCredential';
export const DELETE_USER_MODAL_KEY = 'deleteUser';
export const INVITE_USER_MODAL_KEY = 'inviteUser';
export const DUPLICATE_MODAL_KEY = 'duplicate';
+export const IMPORT_WORKFLOW_URL_MODAL_KEY = 'importWorkflowUrl';
export const TAGS_MANAGER_MODAL_KEY = 'tagsManager';
export const ANNOTATION_TAGS_MANAGER_MODAL_KEY = 'annotationTagsManager';
export const VERSIONS_MODAL_KEY = 'versions';
diff --git a/packages/frontend/editor-ui/src/stores/ui.store.ts b/packages/frontend/editor-ui/src/stores/ui.store.ts
index 8e30bcb832..ddb50ad843 100644
--- a/packages/frontend/editor-ui/src/stores/ui.store.ts
+++ b/packages/frontend/editor-ui/src/stores/ui.store.ts
@@ -41,6 +41,7 @@ import {
MOVE_FOLDER_MODAL_KEY,
WORKFLOW_ACTIVATION_CONFLICTING_WEBHOOK_MODAL_KEY,
FROM_AI_PARAMETERS_MODAL_KEY,
+ IMPORT_WORKFLOW_URL_MODAL_KEY,
} from '@/constants';
import type {
INodeUi,
@@ -126,6 +127,7 @@ export const useUIStore = defineStore(STORES.UI, () => {
SETUP_CREDENTIALS_MODAL_KEY,
PROJECT_MOVE_RESOURCE_MODAL,
NEW_ASSISTANT_SESSION_MODAL,
+ IMPORT_WORKFLOW_URL_MODAL_KEY,
].map((modalKey) => [modalKey, { open: false }]),
),
[DELETE_USER_MODAL_KEY]: {
@@ -199,6 +201,12 @@ export const useUIStore = defineStore(STORES.UI, () => {
nodeName: undefined,
},
},
+ [IMPORT_WORKFLOW_URL_MODAL_KEY]: {
+ open: false,
+ data: {
+ url: '',
+ },
+ },
});
const modalStack = ref([]);