From 770150d53bf81336b4bf8b17b5e73a149493917d Mon Sep 17 00:00:00 2001 From: Julia Miocene Date: Mon, 10 Feb 2025 23:32:57 +0100 Subject: [PATCH 1/3] Migrate an Import from Bitbucket Server form to Vue --- .../import_from_bitbucket_server_app.vue | 159 ++++++++++++++++++ .../import/bitbucket_server/index.js | 22 +++ .../import/bitbucket_server/new/index.js | 3 + .../import/bitbucket_server/new.html.haml | 52 +++--- locale/gitlab.pot | 12 ++ .../bitbucket_server_controller_spec.rb | 1 + .../import_from_bitbucket_server_app_spec.js | 120 +++++++++++++ 7 files changed, 346 insertions(+), 23 deletions(-) create mode 100644 app/assets/javascripts/import/bitbucket_server/import_from_bitbucket_server_app.vue create mode 100644 app/assets/javascripts/import/bitbucket_server/index.js create mode 100644 app/assets/javascripts/pages/import/bitbucket_server/new/index.js create mode 100644 spec/frontend/import/bitbucket_server/import_from_bitbucket_server_app_spec.js diff --git a/app/assets/javascripts/import/bitbucket_server/import_from_bitbucket_server_app.vue b/app/assets/javascripts/import/bitbucket_server/import_from_bitbucket_server_app.vue new file mode 100644 index 00000000000000..2af7510162a442 --- /dev/null +++ b/app/assets/javascripts/import/bitbucket_server/import_from_bitbucket_server_app.vue @@ -0,0 +1,159 @@ + + + diff --git a/app/assets/javascripts/import/bitbucket_server/index.js b/app/assets/javascripts/import/bitbucket_server/index.js new file mode 100644 index 00000000000000..e13a2dfa4201c1 --- /dev/null +++ b/app/assets/javascripts/import/bitbucket_server/index.js @@ -0,0 +1,22 @@ +import Vue from 'vue'; +import importFromBitbucketServerApp from './import_from_bitbucket_server_app.vue'; + +export function initBitbucketServerImportProjectForm() { + const el = document.getElementById('js-vue-import-bitbucket-server-project-root'); + + if (!el) { + return null; + } + + const { backButtonPath, formPath } = el.dataset; + + const props = { backButtonPath, formPath }; + + return new Vue({ + el, + name: 'ImportFromBitbucketServerRoot', + render(h) { + return h(importFromBitbucketServerApp, { props }); + }, + }); +} diff --git a/app/assets/javascripts/pages/import/bitbucket_server/new/index.js b/app/assets/javascripts/pages/import/bitbucket_server/new/index.js new file mode 100644 index 00000000000000..d9e73477919bb9 --- /dev/null +++ b/app/assets/javascripts/pages/import/bitbucket_server/new/index.js @@ -0,0 +1,3 @@ +import { initBitbucketServerImportProjectForm } from '~/import/bitbucket_server'; + +initBitbucketServerImportProjectForm(); diff --git a/app/views/import/bitbucket_server/new.html.haml b/app/views/import/bitbucket_server/new.html.haml index c981c859602e25..19205733fa8ec4 100644 --- a/app/views/import/bitbucket_server/new.html.haml +++ b/app/views/import/bitbucket_server/new.html.haml @@ -2,27 +2,33 @@ - header_title _("New project"), new_project_path - add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project') -= render ::Layouts::PageHeadingComponent.new('') do |c| - - c.with_heading do - %span.gl-inline-flex.gl-items-center.gl-gap-3 - = sprite_icon('bitbucket', size: 32) - = _('Import repositories from Bitbucket Server') - - c.with_description do - = _('Enter in your Bitbucket Server URL and personal access token below') +- if Feature.enabled?(:new_project_creation_form, @user) + #js-vue-import-bitbucket-server-project-root{ data: { + back_button_path: new_project_path(anchor: 'import_project'), + form_path: configure_import_bitbucket_server_path(namespace_id: params[:namespace_id]) + } } +- else + = render ::Layouts::PageHeadingComponent.new('') do |c| + - c.with_heading do + %span.gl-inline-flex.gl-items-center.gl-gap-3 + = sprite_icon('bitbucket', size: 32) + = _('Import repositories from Bitbucket Server') + - c.with_description do + = _('Enter in your Bitbucket Server URL and personal access token below') -= form_tag configure_import_bitbucket_server_path(namespace_id: params[:namespace_id]), method: :post do - .form-group.row - = label_tag :bitbucket_server_url, 'Bitbucket Server URL', class: 'col-form-label col-md-2' - .col-md-4 - = text_field_tag :bitbucket_server_url, '', class: 'form-control gl-form-input gl-mr-3', placeholder: _('https://your-bitbucket-server'), size: 40 - .form-group.row - = label_tag :bitbucket_server_url, 'Username', class: 'col-form-label col-md-2' - .col-md-4 - = text_field_tag :bitbucket_server_username, '', class: 'form-control gl-form-input gl-mr-3', placeholder: _('username'), size: 40 - .form-group.row - = label_tag :personal_access_token, 'Password/Personal access token', class: 'col-form-label col-md-2' - .col-md-4 - = password_field_tag :personal_access_token, '', class: 'form-control gl-form-input gl-mr-3', placeholder: _('Personal access token'), size: 40 - .col-sm-12.gl-mt-5 - = render Pajamas::ButtonComponent.new(type: 'submit', variant: :confirm) do - = _('List your Bitbucket Server repositories') + = form_tag configure_import_bitbucket_server_path(namespace_id: params[:namespace_id]), method: :post do + .form-group.row + = label_tag :bitbucket_server_url, 'Bitbucket Server URL', class: 'col-form-label col-md-2' + .col-md-4 + = text_field_tag :bitbucket_server_url, '', class: 'form-control gl-form-input gl-mr-3', placeholder: _('https://your-bitbucket-server'), size: 40 + .form-group.row + = label_tag :bitbucket_server_url, 'Username', class: 'col-form-label col-md-2' + .col-md-4 + = text_field_tag :bitbucket_server_username, '', class: 'form-control gl-form-input gl-mr-3', placeholder: _('username'), size: 40 + .form-group.row + = label_tag :personal_access_token, 'Password/Personal access token', class: 'col-form-label col-md-2' + .col-md-4 + = password_field_tag :personal_access_token, '', class: 'form-control gl-form-input gl-mr-3', placeholder: _('Personal access token'), size: 40 + .col-sm-12.gl-mt-5 + = render Pajamas::ButtonComponent.new(type: 'submit', variant: :confirm) do + = _('List your Bitbucket Server repositories') diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 7cde08c11df185..45c9cb0a274d72 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -46021,6 +46021,9 @@ msgstr "" msgid "ProjectsNew|Available only for projects within groups" msgstr "" +msgid "ProjectsNew|Bitbucket Server URL" +msgstr "" + msgid "ProjectsNew|Choose a group" msgstr "" @@ -46090,6 +46093,9 @@ msgstr "" msgid "ProjectsNew|Import projects from Gitea" msgstr "" +msgid "ProjectsNew|Import repositories from Bitbucket Server" +msgstr "" + msgid "ProjectsNew|Include a Getting Started README" msgstr "" @@ -46120,12 +46126,18 @@ msgstr "" msgid "ProjectsNew|No import options available" msgstr "" +msgid "ProjectsNew|Password/Personal access token" +msgstr "" + msgid "ProjectsNew|Pick a group or namespace" msgstr "" msgid "ProjectsNew|Pick a group or namespace where you want to create this project." msgstr "" +msgid "ProjectsNew|Please enter a valid Bitbucket Server URL." +msgstr "" + msgid "ProjectsNew|Please enter a valid Gitea host URL." msgstr "" diff --git a/spec/controllers/import/bitbucket_server_controller_spec.rb b/spec/controllers/import/bitbucket_server_controller_spec.rb index 777c1ccdbf57ce..010fcd770d5b97 100644 --- a/spec/controllers/import/bitbucket_server_controller_spec.rb +++ b/spec/controllers/import/bitbucket_server_controller_spec.rb @@ -17,6 +17,7 @@ def assign_session_tokens end before do + stub_feature_flags(new_project_creation_form: false) sign_in(user) stub_application_setting(import_sources: ['bitbucket_server']) end diff --git a/spec/frontend/import/bitbucket_server/import_from_bitbucket_server_app_spec.js b/spec/frontend/import/bitbucket_server/import_from_bitbucket_server_app_spec.js new file mode 100644 index 00000000000000..75e82575066ba8 --- /dev/null +++ b/spec/frontend/import/bitbucket_server/import_from_bitbucket_server_app_spec.js @@ -0,0 +1,120 @@ +import { nextTick } from 'vue'; +import { GlFormInput } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import importFromBitbucketServerApp from '~/import/bitbucket_server/import_from_bitbucket_server_app.vue'; +import MultiStepFormTemplate from '~/vue_shared/components/multi_step_form_template.vue'; + +jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' })); + +describe('Import from Bitbucket Server app', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMountExtended(importFromBitbucketServerApp, { + propsData: { + backButtonPath: '/projects/new#import_project', + formPath: '/import/bitbucket_server/configure', + }, + stubs: { + GlFormInput, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + const findMultiStepForm = () => wrapper.findComponent(MultiStepFormTemplate); + const findForm = () => wrapper.find('form'); + const findUrlInput = () => wrapper.findByTestId('url-input'); + const findUsernameInput = () => wrapper.findByTestId('username-input'); + const findTokenInput = () => wrapper.findByTestId('token-input'); + const findBackButton = () => wrapper.findByTestId('back-button'); + const findNextButton = () => wrapper.findByTestId('next-button'); + + describe('form', () => { + it('renders the multi step form correctly', () => { + expect(findMultiStepForm().props()).toMatchObject({ + currentStep: 3, + stepsTotal: 4, + }); + }); + + it('renders the form element correctly', () => { + const form = findForm(); + + expect(form.attributes('action')).toBe('/import/bitbucket_server/configure'); + expect(form.find('input[type=hidden][name=authenticity_token]').attributes('value')).toBe( + 'mock-csrf-token', + ); + }); + + it('does not submit the form without required 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 findUrlInput().setValue('https://your-bitbucket-server'); + await findUrlInput().trigger('blur'); + await findUsernameInput().setValue('username'); + await findUsernameInput().trigger('blur'); + await findTokenInput().setValue('863638293ddkdl29'); + await findTokenInput().trigger('blur'); + await nextTick(); + + findForm().trigger('submit'); + expect(submitSpy).toHaveBeenCalledWith(); + }); + }); + + describe('validation', () => { + it('shows an error message when url is cleared', async () => { + findUrlInput().setValue(''); + findUrlInput().trigger('blur'); + await nextTick(); + + const formGroup = wrapper.findByTestId('url-group'); + expect(formGroup.vm.$attrs['invalid-feedback']).toBe( + 'Please enter a valid Bitbucket Server URL.', + ); + }); + + it('shows an error message when username is cleared', async () => { + findUsernameInput().setValue(''); + findUsernameInput().trigger('blur'); + await nextTick(); + + const formGroup = wrapper.findByTestId('username-group'); + expect(formGroup.vm.$attrs['invalid-feedback']).toBe('Please enter a valid username.'); + }); + + it('shows an error message when token is cleared', async () => { + findTokenInput().setValue(''); + findTokenInput().trigger('blur'); + await nextTick(); + + const formGroup = wrapper.findByTestId('token-group'); + expect(formGroup.vm.$attrs['invalid-feedback']).toBe('Please enter a valid token.'); + }); + }); + + 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 button', () => { + expect(findNextButton().exists()).toBe(true); + expect(findNextButton().attributes('type')).toBe('submit'); + }); + }); +}); -- GitLab From b0c55ceaad6fb14b58818d07309357104fcb3a55 Mon Sep 17 00:00:00 2001 From: Julia Miocene Date: Tue, 18 Feb 2025 23:54:06 +0100 Subject: [PATCH 2/3] Apply suggestions --- .../bitbucket_server/import_from_bitbucket_server_app.vue | 1 + locale/gitlab.pot | 6 ++++++ .../import_from_bitbucket_server_app_spec.js | 3 --- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/import/bitbucket_server/import_from_bitbucket_server_app.vue b/app/assets/javascripts/import/bitbucket_server/import_from_bitbucket_server_app.vue index 2af7510162a442..eebffd0da4fdfa 100644 --- a/app/assets/javascripts/import/bitbucket_server/import_from_bitbucket_server_app.vue +++ b/app/assets/javascripts/import/bitbucket_server/import_from_bitbucket_server_app.vue @@ -128,6 +128,7 @@ export default { :state="form.fields.personal_access_token.state" name="personal_access_token" required + type="password" :placeholder="$options.placeholders.token" data-testid="token-input" /> diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 45c9cb0a274d72..472fe406737f7a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -46150,6 +46150,12 @@ msgstr "" msgid "ProjectsNew|Please enter a valid project slug." msgstr "" +msgid "ProjectsNew|Please enter a valid token." +msgstr "" + +msgid "ProjectsNew|Please enter a valid username." +msgstr "" + msgid "ProjectsNew|Please upload a valid GitLab project export file." msgstr "" diff --git a/spec/frontend/import/bitbucket_server/import_from_bitbucket_server_app_spec.js b/spec/frontend/import/bitbucket_server/import_from_bitbucket_server_app_spec.js index 75e82575066ba8..95a1e14688dff6 100644 --- a/spec/frontend/import/bitbucket_server/import_from_bitbucket_server_app_spec.js +++ b/spec/frontend/import/bitbucket_server/import_from_bitbucket_server_app_spec.js @@ -61,11 +61,8 @@ describe('Import from Bitbucket Server app', () => { const submitSpy = jest.spyOn(findForm().element, 'submit'); await findUrlInput().setValue('https://your-bitbucket-server'); - await findUrlInput().trigger('blur'); await findUsernameInput().setValue('username'); - await findUsernameInput().trigger('blur'); await findTokenInput().setValue('863638293ddkdl29'); - await findTokenInput().trigger('blur'); await nextTick(); findForm().trigger('submit'); -- GitLab From e30b9a5ab204f4245736e9a93445304beeabac52 Mon Sep 17 00:00:00 2001 From: Julia Miocene Date: Thu, 20 Feb 2025 16:59:04 +0100 Subject: [PATCH 3/3] Apply suggestions --- .../import_from_bitbucket_server_app.vue | 35 ++++++------------- .../import_from_gitlab_export_app.vue | 3 +- .../vue_shared/directives/validation.js | 2 +- .../import_from_bitbucket_server_app_spec.js | 6 ++-- .../vue_shared/directives/validation_spec.js | 1 + 5 files changed, 18 insertions(+), 29 deletions(-) diff --git a/app/assets/javascripts/import/bitbucket_server/import_from_bitbucket_server_app.vue b/app/assets/javascripts/import/bitbucket_server/import_from_bitbucket_server_app.vue index eebffd0da4fdfa..ae37256e619162 100644 --- a/app/assets/javascripts/import/bitbucket_server/import_from_bitbucket_server_app.vue +++ b/app/assets/javascripts/import/bitbucket_server/import_from_bitbucket_server_app.vue @@ -1,22 +1,9 @@ @@ -91,6 +77,7 @@ export default { :validation-message="s__('ProjectsNew|Please enter a valid Bitbucket Server URL.')" :state="form.fields.bitbucket_server_url.state" name="bitbucket_server_url" + type="url" required :placeholder="$options.placeholders.url" data-testid="url-input" @@ -110,7 +97,7 @@ export default { :state="form.fields.bitbucket_server_username.state" name="bitbucket_server_username" required - :placeholder="__('username')" + :placeholder="$options.placeholders.username" data-testid="username-input" /> 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 index 4c261a59377a5a..4eddd30880bc6f 100644 --- 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 @@ -1,6 +1,7 @@