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 @@ + + + + + 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([]);