Commit c3ea7524 authored by Illya Klymov's avatar Illya Klymov Committed by Ezekiel Kigbo

Add modal warning and count when importing multiple projects

* Display confirmation modal to prevent mis-clicks
* Display proper count
parent 005b5b1c
<script> <script>
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import { GlButton, GlLoadingIcon } from '@gitlab/ui'; import { GlButton, GlLoadingIcon, GlModal } from '@gitlab/ui';
import { __, sprintf } from '~/locale'; import { n__, __, sprintf } from '~/locale';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import ProviderRepoTableRow from './provider_repo_table_row.vue'; import ProviderRepoTableRow from './provider_repo_table_row.vue';
import PageQueryParamSync from './page_query_param_sync.vue'; import PageQueryParamSync from './page_query_param_sync.vue';
...@@ -16,6 +16,7 @@ export default { ...@@ -16,6 +16,7 @@ export default {
PageQueryParamSync, PageQueryParamSync,
GlLoadingIcon, GlLoadingIcon,
GlButton, GlButton,
GlModal,
PaginationLinks, PaginationLinks,
}, },
props: { props: {
...@@ -42,6 +43,7 @@ export default { ...@@ -42,6 +43,7 @@ export default {
'isImportingAnyRepo', 'isImportingAnyRepo',
'hasImportableRepos', 'hasImportableRepos',
'hasIncompatibleRepos', 'hasIncompatibleRepos',
'importAllCount',
]), ]),
availableNamespaces() { availableNamespaces() {
...@@ -61,8 +63,12 @@ export default { ...@@ -61,8 +63,12 @@ export default {
importAllButtonText() { importAllButtonText() {
return this.hasIncompatibleRepos return this.hasIncompatibleRepos
? __('Import all compatible repositories') ? n__(
: __('Import all repositories'); 'Import %d compatible repository',
'Import %d compatible repositories',
this.importAllCount,
)
: n__('Import %d repository', 'Import %d repositories', this.importAllCount);
}, },
emptyStateText() { emptyStateText() {
...@@ -111,9 +117,8 @@ export default { ...@@ -111,9 +117,8 @@ export default {
<template> <template>
<div> <div>
<page-query-param-sync :page="pageInfo.page" @popstate="setPage" /> <page-query-param-sync :page="pageInfo.page" @popstate="setPage" />
<p class="light text-nowrap mt-2"> <p class="light text-nowrap mt-2">
{{ s__('ImportProjects|Select the projects you want to import') }} {{ s__('ImportProjects|Select the repositories you want to import') }}
</p> </p>
<template v-if="hasIncompatibleRepos"> <template v-if="hasIncompatibleRepos">
<slot name="incompatible-repos-warning"></slot> <slot name="incompatible-repos-warning"></slot>
...@@ -130,9 +135,25 @@ export default { ...@@ -130,9 +135,25 @@ export default {
:loading="isImportingAnyRepo" :loading="isImportingAnyRepo"
:disabled="!hasImportableRepos" :disabled="!hasImportableRepos"
type="button" type="button"
@click="importAll" @click="$refs.importAllModal.show()"
>{{ importAllButtonText }}</gl-button >{{ importAllButtonText }}</gl-button
> >
<gl-modal
ref="importAllModal"
modal-id="import-all-modal"
:title="s__('ImportProjects|Import repositories')"
:ok-title="__('Import')"
@ok="importAll"
>
{{
n__(
'Are you sure you want to import %d repository?',
'Are you sure you want to import %d repositories?',
importAllCount,
)
}}
</gl-modal>
<slot name="actions"></slot> <slot name="actions"></slot>
<form v-if="filterable" class="gl-ml-auto" novalidate @submit.prevent> <form v-if="filterable" class="gl-ml-auto" novalidate @submit.prevent>
<input <input
...@@ -140,7 +161,7 @@ export default { ...@@ -140,7 +161,7 @@ export default {
data-qa-selector="githubish_import_filter_field" data-qa-selector="githubish_import_filter_field"
class="form-control" class="form-control"
name="filter" name="filter"
:placeholder="__('Filter your projects by name')" :placeholder="__('Filter your repositories by name')"
autofocus autofocus
size="40" size="40"
@input="handleFilterInput($event)" @input="handleFilterInput($event)"
......
...@@ -14,6 +14,8 @@ export const hasIncompatibleRepos = state => state.repositories.some(isIncompati ...@@ -14,6 +14,8 @@ export const hasIncompatibleRepos = state => state.repositories.some(isIncompati
export const hasImportableRepos = state => state.repositories.some(isProjectImportable); export const hasImportableRepos = state => state.repositories.some(isProjectImportable);
export const importAllCount = state => state.repositories.filter(isProjectImportable).length;
export const getImportTarget = state => repoId => { export const getImportTarget = state => repoId => {
if (state.customImportTargets[repoId]) { if (state.customImportTargets[repoId]) {
return state.customImportTargets[repoId]; return state.customImportTargets[repoId];
......
---
title: Add confirmation dialog when importing multiple projects
merge_request: 41306
author:
type: changed
...@@ -3268,6 +3268,11 @@ msgstr "" ...@@ -3268,6 +3268,11 @@ msgstr ""
msgid "Are you sure you want to erase this build?" msgid "Are you sure you want to erase this build?"
msgstr "" msgstr ""
msgid "Are you sure you want to import %d repository?"
msgid_plural "Are you sure you want to import %d repositories?"
msgstr[0] ""
msgstr[1] ""
msgid "Are you sure you want to lose unsaved changes?" msgid "Are you sure you want to lose unsaved changes?"
msgstr "" msgstr ""
...@@ -10965,7 +10970,7 @@ msgstr "" ...@@ -10965,7 +10970,7 @@ msgstr ""
msgid "Filter results..." msgid "Filter results..."
msgstr "" msgstr ""
msgid "Filter your projects by name" msgid "Filter your repositories by name"
msgstr "" msgstr ""
msgid "Filter..." msgid "Filter..."
...@@ -13012,6 +13017,16 @@ msgstr "" ...@@ -13012,6 +13017,16 @@ msgstr ""
msgid "Import" msgid "Import"
msgstr "" msgstr ""
msgid "Import %d compatible repository"
msgid_plural "Import %d compatible repositories"
msgstr[0] ""
msgstr[1] ""
msgid "Import %d repository"
msgid_plural "Import %d repositories"
msgstr[0] ""
msgstr[1] ""
msgid "Import CSV" msgid "Import CSV"
msgstr "" msgstr ""
...@@ -13021,15 +13036,9 @@ msgstr "" ...@@ -13021,15 +13036,9 @@ msgstr ""
msgid "Import all compatible projects" msgid "Import all compatible projects"
msgstr "" msgstr ""
msgid "Import all compatible repositories"
msgstr ""
msgid "Import all projects" msgid "Import all projects"
msgstr "" msgstr ""
msgid "Import all repositories"
msgstr ""
msgid "Import an exported GitLab project" msgid "Import an exported GitLab project"
msgstr "" msgstr ""
...@@ -13117,6 +13126,9 @@ msgstr "" ...@@ -13117,6 +13126,9 @@ msgstr ""
msgid "ImportProjects|Error importing repository %{project_safe_import_url} into %{project_full_path} - %{message}" msgid "ImportProjects|Error importing repository %{project_safe_import_url} into %{project_full_path} - %{message}"
msgstr "" msgstr ""
msgid "ImportProjects|Import repositories"
msgstr ""
msgid "ImportProjects|Importing the project failed" msgid "ImportProjects|Importing the project failed"
msgstr "" msgstr ""
...@@ -13129,7 +13141,7 @@ msgstr "" ...@@ -13129,7 +13141,7 @@ msgstr ""
msgid "ImportProjects|Requesting your %{provider} repositories failed" msgid "ImportProjects|Requesting your %{provider} repositories failed"
msgstr "" msgstr ""
msgid "ImportProjects|Select the projects you want to import" msgid "ImportProjects|Select the repositories you want to import"
msgstr "" msgstr ""
msgid "ImportProjects|The remote data could not be imported." msgid "ImportProjects|The remote data could not be imported."
......
...@@ -20,7 +20,7 @@ RSpec.describe 'Import multiple repositories by uploading a manifest file', :js ...@@ -20,7 +20,7 @@ RSpec.describe 'Import multiple repositories by uploading a manifest file', :js
attach_file('manifest', Rails.root.join('spec/fixtures/aosp_manifest.xml')) attach_file('manifest', Rails.root.join('spec/fixtures/aosp_manifest.xml'))
click_on 'List available repositories' click_on 'List available repositories'
expect(page).to have_button('Import all repositories') expect(page).to have_button('Import 660 repositories')
expect(page).to have_content('https://android-review.googlesource.com/platform/build/blueprint') expect(page).to have_content('https://android-review.googlesource.com/platform/build/blueprint')
end end
......
...@@ -16,15 +16,24 @@ describe('ImportProjectsTable', () => { ...@@ -16,15 +16,24 @@ describe('ImportProjectsTable', () => {
wrapper.find('input[data-qa-selector="githubish_import_filter_field"]'); wrapper.find('input[data-qa-selector="githubish_import_filter_field"]');
const providerTitle = 'THE PROVIDER'; const providerTitle = 'THE PROVIDER';
const providerRepo = { id: 10, sanitizedName: 'sanitizedName', fullName: 'fullName' }; const providerRepo = {
importSource: {
id: 10,
sanitizedName: 'sanitizedName',
fullName: 'fullName',
},
importedProject: null,
};
const findImportAllButton = () => const findImportAllButton = () =>
wrapper wrapper
.findAll(GlButton) .findAll(GlButton)
.filter(w => w.props().variant === 'success') .filter(w => w.props().variant === 'success')
.at(0); .at(0);
const findImportAllModal = () => wrapper.find({ ref: 'importAllModal' });
const importAllFn = jest.fn(); const importAllFn = jest.fn();
const importAllModalShowFn = jest.fn();
const setPageFn = jest.fn(); const setPageFn = jest.fn();
function createComponent({ function createComponent({
...@@ -64,6 +73,9 @@ describe('ImportProjectsTable', () => { ...@@ -64,6 +73,9 @@ describe('ImportProjectsTable', () => {
paginatable, paginatable,
}, },
slots, slots,
stubs: {
GlModal: { template: '<div>Modal!</div>', methods: { show: importAllModalShowFn } },
},
}); });
} }
...@@ -110,18 +122,21 @@ describe('ImportProjectsTable', () => { ...@@ -110,18 +122,21 @@ describe('ImportProjectsTable', () => {
}); });
it.each` it.each`
hasIncompatibleRepos | buttonText hasIncompatibleRepos | count | buttonText
${false} | ${'Import all repositories'} ${false} | ${1} | ${'Import 1 repository'}
${true} | ${'Import all compatible repositories'} ${true} | ${1} | ${'Import 1 compatible repository'}
${false} | ${5} | ${'Import 5 repositories'}
${true} | ${5} | ${'Import 5 compatible repositories'}
`( `(
'import all button has "$buttonText" text when hasIncompatibleRepos is $hasIncompatibleRepos', 'import all button has "$buttonText" text when hasIncompatibleRepos is $hasIncompatibleRepos and repos count is $count',
({ hasIncompatibleRepos, buttonText }) => { ({ hasIncompatibleRepos, buttonText, count }) => {
createComponent({ createComponent({
state: { state: {
providerRepos: [providerRepo], providerRepos: [providerRepo],
}, },
getters: { getters: {
hasIncompatibleRepos: () => hasIncompatibleRepos, hasIncompatibleRepos: () => hasIncompatibleRepos,
importAllCount: () => count,
}, },
}); });
...@@ -129,19 +144,28 @@ describe('ImportProjectsTable', () => { ...@@ -129,19 +144,28 @@ describe('ImportProjectsTable', () => {
}, },
); );
it('renders an empty state if there are no projects available', () => { it('renders an empty state if there are no repositories available', () => {
createComponent({ state: { repositories: [] } }); createComponent({ state: { repositories: [] } });
expect(wrapper.find(ProviderRepoTableRow).exists()).toBe(false); expect(wrapper.find(ProviderRepoTableRow).exists()).toBe(false);
expect(wrapper.text()).toContain(`No ${providerTitle} repositories found`); expect(wrapper.text()).toContain(`No ${providerTitle} repositories found`);
}); });
it('sends importAll event when import button is clicked', async () => { it('opens confirmation modal when import all button is clicked', async () => {
createComponent({ state: { providerRepos: [providerRepo] } }); createComponent({ state: { repositories: [providerRepo] } });
findImportAllButton().vm.$emit('click'); findImportAllButton().vm.$emit('click');
await nextTick(); await nextTick();
expect(importAllModalShowFn).toHaveBeenCalled();
});
it('triggers importAll action when modal is confirmed', async () => {
createComponent({ state: { providerRepos: [providerRepo] } });
findImportAllModal().vm.$emit('ok');
await nextTick();
expect(importAllFn).toHaveBeenCalled(); expect(importAllFn).toHaveBeenCalled();
}); });
......
...@@ -3,6 +3,7 @@ import { ...@@ -3,6 +3,7 @@ import {
isImportingAnyRepo, isImportingAnyRepo,
hasIncompatibleRepos, hasIncompatibleRepos,
hasImportableRepos, hasImportableRepos,
importAllCount,
getImportTarget, getImportTarget,
} from '~/import_projects/store/getters'; } from '~/import_projects/store/getters';
import { STATUSES } from '~/import_projects/constants'; import { STATUSES } from '~/import_projects/constants';
...@@ -97,6 +98,19 @@ describe('import_projects store getters', () => { ...@@ -97,6 +98,19 @@ describe('import_projects store getters', () => {
}); });
}); });
describe('importAllCount', () => {
it('returns count of available importable projects ', () => {
localState.repositories = [
IMPORTABLE_REPO,
IMPORTABLE_REPO,
IMPORTED_REPO,
INCOMPATIBLE_REPO,
];
expect(importAllCount(localState)).toBe(2);
});
});
describe('getImportTarget', () => { describe('getImportTarget', () => {
it('returns default value if no custom target available', () => { it('returns default value if no custom target available', () => {
localState.defaultTargetNamespace = 'default'; localState.defaultTargetNamespace = 'default';
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment