From 34530bac6a263805428a37f4a7986b595e245b19 Mon Sep 17 00:00:00 2001 From: Julia Miocene Date: Mon, 10 Feb 2025 15:50:51 +0100 Subject: [PATCH 1/3] Migrate UI part of the Import By Export File form to Vue --- .../javascripts/import/gitlab_project/app.vue | 236 ++++++++++++++++++ .../import/gitlab_project/index.js | 46 ++++ .../pages/import/gitlab_projects/new/index.js | 2 + .../components/project_destination_select.vue | 77 +++--- .../projects/project_import_gitlab_project.js | 34 +-- .../stylesheets/page_bundles/projects.scss | 5 + .../import/gitlab_projects/new.html.haml | 57 +++-- locale/gitlab.pot | 15 ++ .../import/gitlab_project/app_spec.js | 27 ++ .../gitlab_projects/new.html.haml_spec.rb | 1 + 10 files changed, 430 insertions(+), 70 deletions(-) create mode 100644 app/assets/javascripts/import/gitlab_project/app.vue create mode 100644 app/assets/javascripts/import/gitlab_project/index.js create mode 100644 spec/frontend/import/gitlab_project/app_spec.js diff --git a/app/assets/javascripts/import/gitlab_project/app.vue b/app/assets/javascripts/import/gitlab_project/app.vue new file mode 100644 index 00000000000000..66a320ed1753e9 --- /dev/null +++ b/app/assets/javascripts/import/gitlab_project/app.vue @@ -0,0 +1,236 @@ + + + diff --git a/app/assets/javascripts/import/gitlab_project/index.js b/app/assets/javascripts/import/gitlab_project/index.js new file mode 100644 index 00000000000000..072c5d6b156b38 --- /dev/null +++ b/app/assets/javascripts/import/gitlab_project/index.js @@ -0,0 +1,46 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import App from './app.vue'; + +export function initGitLabImportProjectForm() { + const el = document.getElementById('js-vue-import-gitlab-project-app'); + + if (!el) { + return null; + } + + const { + backButtonPath, + namespaceFullPath, + namespaceId, + rootPath, + userNamespaceId, + canCreateProject, + rootUrl, + } = el.dataset; + + const props = { + backButtonPath, + namespaceFullPath, + namespaceId, + rootPath, + }; + + const provide = { + userNamespaceId, + canCreateProject, + rootUrl, + }; + + return new Vue({ + el, + apolloProvider: new VueApollo({ + defaultClient: createDefaultClient(), + }), + provide, + render(h) { + return h(App, { props }); + }, + }); +} diff --git a/app/assets/javascripts/pages/import/gitlab_projects/new/index.js b/app/assets/javascripts/pages/import/gitlab_projects/new/index.js index d0560af5b3f5de..fe872bfcfc08fa 100644 --- a/app/assets/javascripts/pages/import/gitlab_projects/new/index.js +++ b/app/assets/javascripts/pages/import/gitlab_projects/new/index.js @@ -1,5 +1,7 @@ import initGitLabImportProject from '~/projects/project_import_gitlab_project'; import { initNewProjectUrlSelect } from '~/projects/new'; +import { initGitLabImportProjectForm } from '~/import/gitlab_project'; initNewProjectUrlSelect(); initGitLabImportProject(); +initGitLabImportProjectForm(); diff --git a/app/assets/javascripts/projects/new_v2/components/project_destination_select.vue b/app/assets/javascripts/projects/new_v2/components/project_destination_select.vue index 65f7f6f5164752..debdd3d01c8c6d 100644 --- a/app/assets/javascripts/projects/new_v2/components/project_destination_select.vue +++ b/app/assets/javascripts/projects/new_v2/components/project_destination_select.vue @@ -1,19 +1,17 @@ diff --git a/app/assets/javascripts/projects/project_import_gitlab_project.js b/app/assets/javascripts/projects/project_import_gitlab_project.js index 0cbd4dbf2cfd06..d7f710b5558887 100644 --- a/app/assets/javascripts/projects/project_import_gitlab_project.js +++ b/app/assets/javascripts/projects/project_import_gitlab_project.js @@ -25,20 +25,22 @@ export default () => { const $projectPath = document.querySelector('.js-path-name'); const { name, path } = prepareParameters(); - // get the project name from the URL and set it as input value - $projectName.value = name; - - // get the path url and append it in the input - $projectPath.value = path; - - // generate slug when project name changes - $projectName.addEventListener('keyup', () => { - projectNew.onProjectNameChange($projectName, $projectPath); - hasUserDefinedProjectName = $projectName.value.trim().length > 0; - }); - - // generate project name from the slug if one isn't set - $projectPath.addEventListener('keyup', () => - projectNew.onProjectPathChange($projectName, $projectPath, hasUserDefinedProjectName), - ); + if ($projectName || $projectPath) { + // get the project name from the URL and set it as input value + $projectName.value = name; + + // get the path url and append it in the input + $projectPath.value = path; + + // generate slug when project name changes + $projectName.addEventListener('keyup', () => { + projectNew.onProjectNameChange($projectName, $projectPath); + hasUserDefinedProjectName = $projectName.value.trim().length > 0; + }); + + // generate project name from the slug if one isn't set + $projectPath.addEventListener('keyup', () => + projectNew.onProjectPathChange($projectName, $projectPath, hasUserDefinedProjectName), + ); + } }; diff --git a/app/assets/stylesheets/page_bundles/projects.scss b/app/assets/stylesheets/page_bundles/projects.scss index 7cd0ae0e16cf35..ce095e6d5f98a0 100644 --- a/app/assets/stylesheets/page_bundles/projects.scss +++ b/app/assets/stylesheets/page_bundles/projects.scss @@ -557,3 +557,8 @@ @apply gl-line-clamp-2 gl-whitespace-normal; margin-bottom: 0; } + +// stylelint-disable-next-line gitlab/no-gl-class +.project-destination-select .gl-button-text { + flex-grow: 1; +} diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml index 52f008ae10b4aa..78665317e0bce9 100644 --- a/app/views/import/gitlab_projects/new.html.haml +++ b/app/views/import/gitlab_projects/new.html.haml @@ -1,28 +1,41 @@ - page_title _("GitLab Import") - header_title _("New project"), new_project_path - add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project') +- add_page_specific_style 'page_bundles/projects' -= render ::Layouts::PageHeadingComponent.new('') do |c| - - c.with_heading do - %span.gl-inline-flex.gl-items-center.gl-gap-3 - = sprite_icon('tanuki', size: 32) - = _('Import an exported GitLab project') +- if Feature.enabled?(:new_project_creation_form, @user) + = form_tag import_gitlab_project_path, class: 'new_project', multipart: true do + - namespace_id = namespace_id_from(params) + #js-vue-import-gitlab-project-app{ data: { + back_button_path: new_project_path(anchor: 'import_project'), + namespace_full_path: GroupFinder.new(current_user).execute(id: namespace_id)&.full_path || current_user.namespace.full_path, + namespace_id: namespace_id_from(params) || @current_user_group&.id, + root_path: root_path, + user_namespace_id: current_user.namespace_id, + can_create_project: current_user.can_create_project?.to_s, + root_url: root_url, + } } +- else + = render ::Layouts::PageHeadingComponent.new('') do |c| + - c.with_heading do + %span.gl-inline-flex.gl-items-center.gl-gap-3 + = sprite_icon('tanuki', size: 32) + = _('Import an exported GitLab project') -= form_tag import_gitlab_project_path, class: 'new_project', multipart: true do - = render 'import/shared/new_project_form' - - .row - .form-group.col-md-12 - = _("To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here.") - .row - .form-group.col-sm-12 - = label_tag :file, _('GitLab project export'), class: 'label-bold' - .form-group - = file_field_tag :file, class: '' - .row - .col-sm-12.gl-mt-5 - = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, button_options: { class: 'gl-mr-2', data: { testid: 'import-project-button' }}) do - = _('Import project') - = render Pajamas::ButtonComponent.new(href: new_project_path) do - = _('Cancel') + = form_tag import_gitlab_project_path, class: 'new_project', multipart: true do + = render 'import/shared/new_project_form' + .row + .form-group.col-md-12 + = _("To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here.") + .row + .form-group.col-sm-12 + = label_tag :file, _('GitLab project export'), class: 'label-bold' + .form-group + = file_field_tag :file, class: '' + .row + .col-sm-12.gl-mt-5 + = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, button_options: { class: 'gl-mr-2', data: { testid: 'import-project-button' }}) do + = _('Import project') + = render Pajamas::ButtonComponent.new(href: new_project_path) do + = _('Cancel') diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 2c695a6addca99..9df1dee0e03138 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -21384,6 +21384,9 @@ msgstr "" msgid "Drop or %{linkStart}upload%{linkEnd} files to attach" msgstr "" +msgid "Drop or upload file to attach" +msgstr "" + msgid "Drop your designs to start your upload." msgstr "" @@ -46048,6 +46051,9 @@ msgstr "" msgid "ProjectsNew|Get started with one of our popular project templates." msgstr "" +msgid "ProjectsNew|GitLab project export" +msgstr "" + msgid "ProjectsNew|Gitea host URL" msgstr "" @@ -46087,6 +46093,9 @@ msgstr "" msgid "ProjectsNew|Must start with a lowercase or uppercase letter, digit, emoji, or underscore. Can also contain dots, pluses, dashes, or spaces." msgstr "" +msgid "ProjectsNew|My awesome project" +msgstr "" + msgid "ProjectsNew|New project" msgstr "" @@ -46114,6 +46123,9 @@ msgstr "" msgid "ProjectsNew|Project name" msgstr "" +msgid "ProjectsNew|Project slug" +msgstr "" + msgid "ProjectsNew|Projects" msgstr "" @@ -46132,6 +46144,9 @@ msgstr "" msgid "ProjectsNew|Select a template" msgstr "" +msgid "ProjectsNew|To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here." +msgstr "" + msgid "ProjectsNew|Unable to suggest a path. Please refresh and try again." msgstr "" diff --git a/spec/frontend/import/gitlab_project/app_spec.js b/spec/frontend/import/gitlab_project/app_spec.js new file mode 100644 index 00000000000000..adfa8137e6a78b --- /dev/null +++ b/spec/frontend/import/gitlab_project/app_spec.js @@ -0,0 +1,27 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import App from '~/import/gitlab_project/app.vue'; +import MultiStepFormTemplate from '~/vue_shared/components/multi_step_form_template.vue'; + +describe('Import from GitLab export file app', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMountExtended(App, { + propsData: { + backButtonPath: '/projects/new#import_project', + namespaceFullPath: 'root', + namespaceId: '1', + rootPath: '/', + ...props, + }, + }); + }; + + const findMultiStepForm = () => wrapper.findComponent(MultiStepFormTemplate); + + it('renders a form', () => { + createComponent(); + + expect(findMultiStepForm().exists()).toBe(true); + }); +}); diff --git a/spec/views/import/gitlab_projects/new.html.haml_spec.rb b/spec/views/import/gitlab_projects/new.html.haml_spec.rb index 68e7019c892a51..af3be5009fd507 100644 --- a/spec/views/import/gitlab_projects/new.html.haml_spec.rb +++ b/spec/views/import/gitlab_projects/new.html.haml_spec.rb @@ -9,6 +9,7 @@ let(:user) { build_stubbed(:user, namespace: namespace) } before do + stub_feature_flags(new_project_creation_form: false) allow(view).to receive(:current_user).and_return(user) end -- GitLab From 9c4be81b2916589a39c1b38674fecc7f82c1daf8 Mon Sep 17 00:00:00 2001 From: Julia Miocene Date: Thu, 13 Feb 2025 15:23:49 +0100 Subject: [PATCH 2/3] Apply suggestions --- .../javascripts/import/gitlab_project/app.vue | 53 +++++++-- .../import/gitlab_project/index.js | 3 +- .../components/project_destination_select.vue | 6 + .../import/gitlab_projects/new.html.haml | 4 +- locale/gitlab.pot | 3 + .../import/gitlab_project/app_spec.js | 105 +++++++++++++++++- 6 files changed, 158 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/import/gitlab_project/app.vue b/app/assets/javascripts/import/gitlab_project/app.vue index 66a320ed1753e9..b9daedd04d3a59 100644 --- a/app/assets/javascripts/import/gitlab_project/app.vue +++ b/app/assets/javascripts/import/gitlab_project/app.vue @@ -80,6 +80,7 @@ export default { filePreviewURL: null, form, animateUploadIcon: false, + dropzoneState: true, }; }, computed: { @@ -97,13 +98,14 @@ export default { setFile() { this.file = this.$refs.fileUpload.files['0']; - const fileUurlReader = new FileReader(); + const fileUrlReader = new FileReader(); - fileUurlReader.readAsDataURL(this.file); + fileUrlReader.readAsDataURL(this.file); - fileUurlReader.onload = (e) => { + fileUrlReader.onload = (e) => { this.filePreviewURL = e.target?.result; }; + this.dropzoneState = true; }, onDropzoneMouseEnter() { this.animateUploadIcon = true; @@ -114,6 +116,9 @@ export default { openFileUpload() { this.$refs.fileUpload.click(); }, + onSubmitClick() { + if (this.file === null) this.dropzoneState = false; + }, }, csrf, projectNamePattern: `(${START_RULE.reg.source})|(${CONTAINS_RULE.reg.source})`, @@ -137,6 +142,7 @@ export default { ) " :invalid-feedback="form.fields.name.feedback" + data-testid="project-name-form-group" > -
- +
+ -
{{ __('/') }}
+
{{ __('/') }}
@@ -187,10 +200,18 @@ export default { }}

- + diff --git a/app/assets/javascripts/import/gitlab_project/index.js b/app/assets/javascripts/import/gitlab_project/index.js index 072c5d6b156b38..6cf7bdaf913e96 100644 --- a/app/assets/javascripts/import/gitlab_project/index.js +++ b/app/assets/javascripts/import/gitlab_project/index.js @@ -4,7 +4,7 @@ import createDefaultClient from '~/lib/graphql'; import App from './app.vue'; export function initGitLabImportProjectForm() { - const el = document.getElementById('js-vue-import-gitlab-project-app'); + const el = document.getElementById('js-import-gitlab-project-app'); if (!el) { return null; @@ -35,6 +35,7 @@ export function initGitLabImportProjectForm() { return new Vue({ el, + name: 'ImportGitLabProjectApp', apolloProvider: new VueApollo({ defaultClient: createDefaultClient(), }), diff --git a/app/assets/javascripts/projects/new_v2/components/project_destination_select.vue b/app/assets/javascripts/projects/new_v2/components/project_destination_select.vue index debdd3d01c8c6d..759705203ab0f1 100644 --- a/app/assets/javascripts/projects/new_v2/components/project_destination_select.vue +++ b/app/assets/javascripts/projects/new_v2/components/project_destination_select.vue @@ -47,6 +47,11 @@ export default { required: false, default: '', }, + toggleAriaLabelledBy: { + type: String, + required: false, + default: '', + }, groupsOnly: { type: Boolean, required: false, @@ -206,6 +211,7 @@ export default { :items="items" :toggle-text="dropdownText" toggle-class="gl-w-full" + :toggle-aria-labelled-by="toggleAriaLabelledBy" :no-results-text="$options.i18n.emptySearchResult" class="project-destination-select gl-w-full gl-max-w-full" @show="trackDropdownShow" diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml index 78665317e0bce9..b3175bb2ed29ff 100644 --- a/app/views/import/gitlab_projects/new.html.haml +++ b/app/views/import/gitlab_projects/new.html.haml @@ -1,12 +1,12 @@ - page_title _("GitLab Import") - header_title _("New project"), new_project_path - add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project') -- add_page_specific_style 'page_bundles/projects' - if Feature.enabled?(:new_project_creation_form, @user) + - add_page_specific_style 'page_bundles/projects' = form_tag import_gitlab_project_path, class: 'new_project', multipart: true do - namespace_id = namespace_id_from(params) - #js-vue-import-gitlab-project-app{ data: { + #js-import-gitlab-project-app{ data: { back_button_path: new_project_path(anchor: 'import_project'), namespace_full_path: GroupFinder.new(current_user).execute(id: namespace_id)&.full_path || current_user.namespace.full_path, namespace_id: namespace_id_from(params) || @current_user_group&.id, diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 9df1dee0e03138..811e17f2479203 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -46114,6 +46114,9 @@ msgstr "" msgid "ProjectsNew|Please enter a valid personal access token." msgstr "" +msgid "ProjectsNew|Please upload your file." +msgstr "" + msgid "ProjectsNew|Project Configuration" msgstr "" diff --git a/spec/frontend/import/gitlab_project/app_spec.js b/spec/frontend/import/gitlab_project/app_spec.js index adfa8137e6a78b..dd8add4d51689c 100644 --- a/spec/frontend/import/gitlab_project/app_spec.js +++ b/spec/frontend/import/gitlab_project/app_spec.js @@ -1,3 +1,5 @@ +import { nextTick } from 'vue'; +import { GlAnimatedUploadIcon, GlFormInput, GlButton } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import App from '~/import/gitlab_project/app.vue'; import MultiStepFormTemplate from '~/vue_shared/components/multi_step_form_template.vue'; @@ -5,23 +7,118 @@ import MultiStepFormTemplate from '~/vue_shared/components/multi_step_form_templ describe('Import from GitLab export file app', () => { let wrapper; - const createComponent = (props = {}) => { + const createComponent = () => { wrapper = shallowMountExtended(App, { propsData: { backButtonPath: '/projects/new#import_project', namespaceFullPath: 'root', namespaceId: '1', rootPath: '/', - ...props, + }, + stubs: { + GlFormInput, + GlButton, }, }); }; + beforeEach(() => { + createComponent(); + }); + const findMultiStepForm = () => wrapper.findComponent(MultiStepFormTemplate); + const findProjectNameInput = () => wrapper.findByTestId('project-name'); + const findProjectSlugInput = () => wrapper.findByTestId('project-slug'); + const findDropzoneButton = () => wrapper.findByTestId('dropzone-button'); + const findDropzoneInput = () => wrapper.findByTestId('dropzone-input'); + const findAnimatedUploadIcon = () => wrapper.findComponent(GlAnimatedUploadIcon); + const findBackButton = () => wrapper.findByTestId('back-button'); + const findNextButton = () => wrapper.findByTestId('next-button'); it('renders a form', () => { - createComponent(); - expect(findMultiStepForm().exists()).toBe(true); }); + + describe('validation', () => { + it('shows an error message when project name is cleared', async () => { + await findProjectNameInput().setValue(''); + await findProjectNameInput().trigger('blur'); + await nextTick(); + + const formGroup = wrapper.findByTestId('project-name-form-group'); + expect(formGroup.vm.$attrs['invalid-feedback']).toBe('Please fill out this field.'); + }); + + it('shows an error message when project name starts with invalid characters', async () => { + await findProjectNameInput().setValue('#test'); + await findProjectNameInput().trigger('blur'); + await nextTick(); + + const formGroup = wrapper.findByTestId('project-name-form-group'); + expect(formGroup.vm.$attrs['invalid-feedback']).toBe( + 'Name must start with a letter, digit, emoji, or underscore.', + ); + }); + + it('shows an error message when project name contains invalid characters', async () => { + await findProjectNameInput().setValue('test?'); + await findProjectNameInput().trigger('blur'); + await nextTick(); + + const formGroup = wrapper.findByTestId('project-name-form-group'); + expect(formGroup.vm.$attrs['invalid-feedback']).toBe( + 'Name can contain only lowercase or uppercase letters, digits, emoji, spaces, dots, underscores, dashes, or pluses.', + ); + }); + + it('shows an error message when there are no file uploaded', async () => { + await findNextButton().trigger('click'); + await nextTick(); + + const formGroup = wrapper.findByTestId('project-file-form-group'); + expect(formGroup.vm.$attrs['invalid-feedback']).toBe('Please upload your file.'); + }); + }); + + describe('project slug', () => { + it('updates the project slug appropriately when updating project name', async () => { + await findProjectNameInput().setValue('test project'); + await nextTick(); + + expect(findProjectSlugInput().props('value')).toBe('test-project'); + }); + }); + + describe('drop zone', () => { + it('renders a drop zone', () => { + expect(findDropzoneInput().exists()).toBe(true); + expect(findDropzoneButton().text()).toBe('Drop or upload file to attach'); + expect(findAnimatedUploadIcon().exists()).toBe(true); + }); + + it('uploads a file', async () => { + const file = new File(['foo'], 'foo.gz', { type: 'application/gzip', size: 1024 }); + Object.defineProperty(findDropzoneInput().element, 'files', { value: [file] }); + findDropzoneInput().trigger('change'); + + await nextTick(); + + expect(findDropzoneButton().text()).toContain('foo.gz'); + expect(findAnimatedUploadIcon().exists()).toBe(false); + }); + }); + + describe('back button', () => { + it('renders a back button', () => { + expect(findBackButton().exists()).toBe(true); + expect(findBackButton().attributes('href')).toBe('/projects/new#import_project'); + }); + }); + + describe('next button', () => { + it('renders a next button', () => { + expect(findNextButton().exists()).toBe(true); + expect(findNextButton().attributes('type')).toBe('submit'); + }); + }); }); -- GitLab From c1b6309d86d38ac6cfd6f55ea9bef99aa8f8a854 Mon Sep 17 00:00:00 2001 From: Julia Miocene Date: Mon, 17 Feb 2025 13:27:44 +0100 Subject: [PATCH 3/3] Apply suggestions --- .../javascripts/import/gitlab_project/app.vue | 271 ---------------- .../import_from_gitlab_export_app.vue | 292 ++++++++++++++++++ .../import/gitlab_project/index.js | 10 +- .../components/project_destination_select.vue | 2 +- .../import/gitlab_projects/new.html.haml | 22 +- locale/gitlab.pot | 11 +- ... => import_from_gitlab_export_app_spec.js} | 94 ++++-- 7 files changed, 387 insertions(+), 315 deletions(-) delete mode 100644 app/assets/javascripts/import/gitlab_project/app.vue create mode 100644 app/assets/javascripts/import/gitlab_project/import_from_gitlab_export_app.vue rename spec/frontend/import/gitlab_project/{app_spec.js => import_from_gitlab_export_app_spec.js} (59%) diff --git a/app/assets/javascripts/import/gitlab_project/app.vue b/app/assets/javascripts/import/gitlab_project/app.vue deleted file mode 100644 index b9daedd04d3a59..00000000000000 --- a/app/assets/javascripts/import/gitlab_project/app.vue +++ /dev/null @@ -1,271 +0,0 @@ - - - diff --git a/app/assets/javascripts/import/gitlab_project/import_from_gitlab_export_app.vue b/app/assets/javascripts/import/gitlab_project/import_from_gitlab_export_app.vue new file mode 100644 index 00000000000000..4c261a59377a5a --- /dev/null +++ b/app/assets/javascripts/import/gitlab_project/import_from_gitlab_export_app.vue @@ -0,0 +1,292 @@ + + + diff --git a/app/assets/javascripts/import/gitlab_project/index.js b/app/assets/javascripts/import/gitlab_project/index.js index 6cf7bdaf913e96..0954f3d929f5a1 100644 --- a/app/assets/javascripts/import/gitlab_project/index.js +++ b/app/assets/javascripts/import/gitlab_project/index.js @@ -1,10 +1,10 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; -import App from './app.vue'; +import importFromGitlabExportApp from './import_from_gitlab_export_app.vue'; export function initGitLabImportProjectForm() { - const el = document.getElementById('js-import-gitlab-project-app'); + const el = document.getElementById('js-import-gitlab-project-root'); if (!el) { return null; @@ -15,6 +15,7 @@ export function initGitLabImportProjectForm() { namespaceFullPath, namespaceId, rootPath, + importGitlabProjectPath, userNamespaceId, canCreateProject, rootUrl, @@ -25,6 +26,7 @@ export function initGitLabImportProjectForm() { namespaceFullPath, namespaceId, rootPath, + importGitlabProjectPath, }; const provide = { @@ -35,13 +37,13 @@ export function initGitLabImportProjectForm() { return new Vue({ el, - name: 'ImportGitLabProjectApp', + name: 'ImportGitLabProjectRoot', apolloProvider: new VueApollo({ defaultClient: createDefaultClient(), }), provide, render(h) { - return h(App, { props }); + return h(importFromGitlabExportApp, { props }); }, }); } diff --git a/app/assets/javascripts/projects/new_v2/components/project_destination_select.vue b/app/assets/javascripts/projects/new_v2/components/project_destination_select.vue index 759705203ab0f1..7675e1c9ed3fac 100644 --- a/app/assets/javascripts/projects/new_v2/components/project_destination_select.vue +++ b/app/assets/javascripts/projects/new_v2/components/project_destination_select.vue @@ -227,7 +227,7 @@ export default { ({ token: 'mock-csrf-token' })); + describe('Import from GitLab export file app', () => { let wrapper; const createComponent = () => { - wrapper = shallowMountExtended(App, { + wrapper = shallowMountExtended(importFromGitlabExportApp, { propsData: { backButtonPath: '/projects/new#import_project', namespaceFullPath: 'root', namespaceId: '1', rootPath: '/', + importGitlabProjectPath: 'import/path', }, stubs: { GlFormInput, - GlButton, }, }); }; @@ -27,6 +29,7 @@ describe('Import from GitLab export file app', () => { }); const findMultiStepForm = () => wrapper.findComponent(MultiStepFormTemplate); + const findForm = () => wrapper.find('form'); const findProjectNameInput = () => wrapper.findByTestId('project-name'); const findProjectSlugInput = () => wrapper.findByTestId('project-slug'); const findDropzoneButton = () => wrapper.findByTestId('dropzone-button'); @@ -35,24 +38,68 @@ describe('Import from GitLab export file app', () => { const findBackButton = () => wrapper.findByTestId('back-button'); const findNextButton = () => wrapper.findByTestId('next-button'); - it('renders a form', () => { - expect(findMultiStepForm().exists()).toBe(true); + const setProjectName = async (projectName) => { + await findProjectNameInput().setValue(projectName); + await findProjectNameInput().trigger('blur'); + + await nextTick(); + }; + + const uploadFile = async () => { + const file = new File(['foo'], 'foo.gz', { type: 'application/gzip', size: 1024 }); + Object.defineProperty(findDropzoneInput().element, 'files', { value: [file] }); + findDropzoneInput().trigger('change'); + + await nextTick(); + }; + + describe('form', () => { + it('renders the multi step form correctly', () => { + expect(findMultiStepForm().props()).toMatchObject({ + currentStep: 3, + stepsTotal: 3, + }); + }); + + it('renders the form element correctly', () => { + const form = findForm(); + + expect(form.attributes('action')).toBe('import/path'); + expect(form.find('input[type=hidden][name=authenticity_token]').attributes('value')).toBe( + 'mock-csrf-token', + ); + }); + + it('does not submit the form without requried fields', () => { + const submitSpy = jest.spyOn(findForm().element, 'submit'); + + findForm().trigger('submit'); + expect(submitSpy).not.toHaveBeenCalled(); + }); + + it('submits the form with valid form data', async () => { + const submitSpy = jest.spyOn(findForm().element, 'submit'); + + await setProjectName('test project'); + uploadFile(); + await nextTick(); + + findForm().trigger('submit'); + + expect(submitSpy).toHaveBeenCalledWith(); + }); }); describe('validation', () => { it('shows an error message when project name is cleared', async () => { - await findProjectNameInput().setValue(''); - await findProjectNameInput().trigger('blur'); - await nextTick(); + await setProjectName(''); const formGroup = wrapper.findByTestId('project-name-form-group'); - expect(formGroup.vm.$attrs['invalid-feedback']).toBe('Please fill out this field.'); + expect(formGroup.vm.$attrs['invalid-feedback']).toBe('Please enter a valid project name.'); }); it('shows an error message when project name starts with invalid characters', async () => { - await findProjectNameInput().setValue('#test'); - await findProjectNameInput().trigger('blur'); - await nextTick(); + await setProjectName('#test'); const formGroup = wrapper.findByTestId('project-name-form-group'); expect(formGroup.vm.$attrs['invalid-feedback']).toBe( @@ -61,9 +108,7 @@ describe('Import from GitLab export file app', () => { }); it('shows an error message when project name contains invalid characters', async () => { - await findProjectNameInput().setValue('test?'); - await findProjectNameInput().trigger('blur'); - await nextTick(); + await setProjectName('test?'); const formGroup = wrapper.findByTestId('project-name-form-group'); expect(formGroup.vm.$attrs['invalid-feedback']).toBe( @@ -72,18 +117,19 @@ describe('Import from GitLab export file app', () => { }); it('shows an error message when there are no file uploaded', async () => { - await findNextButton().trigger('click'); + findForm().trigger('submit'); await nextTick(); const formGroup = wrapper.findByTestId('project-file-form-group'); - expect(formGroup.vm.$attrs['invalid-feedback']).toBe('Please upload your file.'); + expect(formGroup.vm.$attrs['invalid-feedback']).toBe( + 'Please upload a valid GitLab project export file.', + ); }); }); describe('project slug', () => { it('updates the project slug appropriately when updating project name', async () => { - await findProjectNameInput().setValue('test project'); - await nextTick(); + await setProjectName('test project'); expect(findProjectSlugInput().props('value')).toBe('test-project'); }); @@ -97,11 +143,7 @@ describe('Import from GitLab export file app', () => { }); it('uploads a file', async () => { - const file = new File(['foo'], 'foo.gz', { type: 'application/gzip', size: 1024 }); - Object.defineProperty(findDropzoneInput().element, 'files', { value: [file] }); - findDropzoneInput().trigger('change'); - - await nextTick(); + await uploadFile(); expect(findDropzoneButton().text()).toContain('foo.gz'); expect(findAnimatedUploadIcon().exists()).toBe(false); @@ -110,14 +152,12 @@ describe('Import from GitLab export file app', () => { describe('back button', () => { it('renders a back button', () => { - expect(findBackButton().exists()).toBe(true); expect(findBackButton().attributes('href')).toBe('/projects/new#import_project'); }); }); describe('next button', () => { it('renders a next button', () => { - expect(findNextButton().exists()).toBe(true); expect(findNextButton().attributes('type')).toBe('submit'); }); }); -- GitLab