diff --git a/app/assets/javascripts/import/gitea/import_from_gitea_root.vue b/app/assets/javascripts/import/gitea/import_from_gitea_root.vue
new file mode 100644
index 0000000000000000000000000000000000000000..c7d8a83fe798959c84b4a61ae4cbeec83b8237ba
--- /dev/null
+++ b/app/assets/javascripts/import/gitea/import_from_gitea_root.vue
@@ -0,0 +1,166 @@
+
+
+
+
+
diff --git a/app/assets/javascripts/import/gitea/index.js b/app/assets/javascripts/import/gitea/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..d305564679993a5d6a54de622b1664faad9fae95
--- /dev/null
+++ b/app/assets/javascripts/import/gitea/index.js
@@ -0,0 +1,22 @@
+import Vue from 'vue';
+import ImportFromGiteaRoot from './import_from_gitea_root.vue';
+
+export function initGiteaImportProjectForm() {
+ const el = document.getElementById('js-vue-import-gitea-project-root');
+
+ if (!el) {
+ return null;
+ }
+
+ const { backButtonPath, namespaceId, formPath } = el.dataset;
+
+ const props = { backButtonPath, namespaceId, formPath };
+
+ return new Vue({
+ el,
+ name: 'ImportFromGiteaRoot',
+ render(h) {
+ return h(ImportFromGiteaRoot, { props });
+ },
+ });
+}
diff --git a/app/assets/javascripts/pages/import/gitea/new/index.js b/app/assets/javascripts/pages/import/gitea/new/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..d917983432bc08a243324dd5e47356f97422bef7
--- /dev/null
+++ b/app/assets/javascripts/pages/import/gitea/new/index.js
@@ -0,0 +1,3 @@
+import { initGiteaImportProjectForm } from '~/import/gitea';
+
+initGiteaImportProjectForm();
diff --git a/app/views/import/gitea/new.html.haml b/app/views/import/gitea/new.html.haml
index caed978b396141ff5a0aed87172863cde967b35e..aeca521203abb1accc6c3f7a0a1a9630f6ebc7c1 100644
--- a/app/views/import/gitea/new.html.haml
+++ b/app/views/import/gitea/new.html.haml
@@ -2,26 +2,33 @@
- header_title _("New project"), new_project_path
- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
-%h1.page-title.gl-text-size-h-display.gl-flex.gl-items-center
- .gl-flex.gl-items-center.gl-justify-center
- = sprite_icon('gitea', css_class: 'gl-mr-3', size: 48)
- = _('Import projects from Gitea')
-%hr
+- if Feature.enabled?(:new_project_creation_form, @user)
+ #js-vue-import-gitea-project-root{ data: {
+ back_button_path: new_project_path(anchor: 'import_project'),
+ namespace_id: namespace_id_from(params) || @current_user_group&.id,
+ form_path: personal_access_token_import_gitea_path
+ } }
+- else
+ %h1.page-title.gl-text-size-h-display.gl-flex.gl-items-center
+ .gl-flex.gl-items-center.gl-justify-center
+ = sprite_icon('gitea', css_class: 'gl-mr-3', size: 48)
+ = _('Import projects from Gitea')
+ %hr
-%p
- - link_to_personal_token = link_to(_('personal access token'), 'https://docs.gitea.io/en-us/api-usage/#authentication-via-the-api')
- = _('To get started, please enter your Gitea host URL and a %{link_to_personal_token}.').html_safe % { link_to_personal_token: link_to_personal_token }
+ %p
+ - link_to_personal_token = link_to(_('personal access token'), 'https://docs.gitea.io/en-us/api-usage/#authentication-via-the-api')
+ = _('To get started, please enter your Gitea host URL and a %{link_to_personal_token}.').html_safe % { link_to_personal_token: link_to_personal_token }
-= form_tag personal_access_token_import_gitea_path do
- = hidden_field_tag(:namespace_id, params[:namespace_id])
- .form-group.row
- = label_tag :gitea_host_url, _('Gitea host URL'), class: 'col-form-label col-sm-2'
- .col-sm-4
- = text_field_tag :gitea_host_url, nil, placeholder: 'https://gitea.com', class: 'form-control gl-form-input'
- .form-group.row
- = label_tag :personal_access_token, _('Personal access token'), for: :personal_access_token, class: 'col-form-label col-sm-2'
- .col-sm-4
- = password_field_tag :personal_access_token, nil, class: 'form-control gl-form-input'
- .form-actions
- = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm) do
- = _('List your Gitea repositories')
+ = form_tag personal_access_token_import_gitea_path do
+ = hidden_field_tag(:namespace_id, params[:namespace_id])
+ .form-group.row
+ = label_tag :gitea_host_url, _('Gitea host URL'), class: 'col-form-label col-sm-2'
+ .col-sm-4
+ = text_field_tag :gitea_host_url, nil, placeholder: 'https://gitea.com', class: 'form-control gl-form-input'
+ .form-group.row
+ = label_tag :personal_access_token, _('Personal access token'), for: :personal_access_token, class: 'col-form-label col-sm-2'
+ .col-sm-4
+ = password_field_tag :personal_access_token, nil, class: 'form-control gl-form-input'
+ .form-actions
+ = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm) do
+ = _('List your Gitea repositories')
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index cc2fe88051bb9db4232ac98ed25f0621e1920424..f791caabfaed314d89609679c2ffb3c1432e5803 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -26460,6 +26460,9 @@ msgstr ""
msgid "GithubImport|Import failed because of a GitHub error: %{original} (HTTP %{code})"
msgstr ""
+msgid "GithubImport|Learn more about %{linkStart}Gitea personal access tokens%{linkEnd}."
+msgstr ""
+
msgid "GithubImport|Namespace or group to import repository into does not exist."
msgstr ""
@@ -45954,6 +45957,9 @@ msgstr ""
msgid "ProjectsNew|Get started with one of our popular project templates."
msgstr ""
+msgid "ProjectsNew|Gitea host URL"
+msgstr ""
+
msgid "ProjectsNew|Group name"
msgstr ""
@@ -45963,6 +45969,9 @@ msgstr ""
msgid "ProjectsNew|Import project"
msgstr ""
+msgid "ProjectsNew|Import projects from Gitea"
+msgstr ""
+
msgid "ProjectsNew|Include a Getting Started README"
msgstr ""
@@ -45996,6 +46005,12 @@ msgstr ""
msgid "ProjectsNew|Pick a group or namespace where you want to create this project."
msgstr ""
+msgid "ProjectsNew|Please enter a valid Gitea host URL."
+msgstr ""
+
+msgid "ProjectsNew|Please enter a valid personal access token."
+msgstr ""
+
msgid "ProjectsNew|Project Configuration"
msgstr ""
diff --git a/spec/frontend/import/gitea/import_from_gitea_root_spec.js b/spec/frontend/import/gitea/import_from_gitea_root_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..d27bcbb760b113dcd322fdfef67ee89340340f65
--- /dev/null
+++ b/spec/frontend/import/gitea/import_from_gitea_root_spec.js
@@ -0,0 +1,111 @@
+import { nextTick } from 'vue';
+import { GlFormInput } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ImportFromGiteaRoot from '~/import/gitea/import_from_gitea_root.vue';
+import MultiStepFormTemplate from '~/vue_shared/components/multi_step_form_template.vue';
+
+jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
+
+describe('Import from Gitea app', () => {
+ let wrapper;
+
+ const defaultProps = {
+ backButtonPath: '/projects/new#import_project',
+ namespaceId: '1',
+ formPath: '/import/gitea/personal_access_token',
+ };
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ImportFromGiteaRoot, {
+ propsData: {
+ ...defaultProps,
+ },
+ stubs: {
+ GlFormInput,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ const findMultiStepForm = () => wrapper.findComponent(MultiStepFormTemplate);
+ const findForm = () => wrapper.find('form');
+ const findGiteaHostUrlInput = () => wrapper.findByTestId('gitea-host-url-input');
+ const findPersonalAccessTokenInput = () => wrapper.findByTestId('personal-access-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(defaultProps.formPath);
+ 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 findGiteaHostUrlInput().setValue('https://test.gitea.cloud/');
+ await findGiteaHostUrlInput().trigger('blur');
+ await findPersonalAccessTokenInput().setValue('863638293ddkdl29');
+ await findPersonalAccessTokenInput().trigger('blur');
+ await nextTick();
+
+ findForm().trigger('submit');
+ expect(submitSpy).toHaveBeenCalledWith();
+ });
+ });
+
+ describe('validation', () => {
+ it('shows an error message when url is cleared', async () => {
+ findGiteaHostUrlInput().setValue('');
+ findGiteaHostUrlInput().trigger('blur');
+ await nextTick();
+
+ const formGroup = wrapper.findByTestId('gitea-host-url-group');
+ expect(formGroup.vm.$attrs['invalid-feedback']).toBe('Please enter a valid Gitea host URL.');
+ });
+
+ it('shows an error message when token is cleared', async () => {
+ findPersonalAccessTokenInput().setValue('');
+ findPersonalAccessTokenInput().trigger('blur');
+ await nextTick();
+
+ const formGroup = wrapper.findByTestId('personal-access-token-group');
+ expect(formGroup.vm.$attrs['invalid-feedback']).toBe(
+ 'Please enter a valid personal access token.',
+ );
+ });
+ });
+
+ describe('back button', () => {
+ it('renders a back button', () => {
+ expect(findBackButton().attributes('href')).toBe(defaultProps.backButtonPath);
+ });
+ });
+
+ describe('next button', () => {
+ it('renders a next button', () => {
+ expect(findNextButton().attributes('type')).toBe('submit');
+ });
+ });
+});