Commit 40a2b67e authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '267953-gitlab-migration-select-multiple-top-groups-for-bulk-import' into 'master'

GitLab Migration - Select multiple top groups for bulk import

See merge request gitlab-org/gitlab!68030
parents e7762cd5 22e62e6d
......@@ -11,7 +11,7 @@ import {
GlSprintf,
GlSafeHtmlDirective as SafeHtml,
GlTable,
GlTooltip,
GlFormCheckbox,
} from '@gitlab/ui';
import { s__, __, n__ } from '~/locale';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
......@@ -40,8 +40,8 @@ export default {
GlLink,
GlLoadingIcon,
GlSearchBoxByClick,
GlFormCheckbox,
GlSprintf,
GlTooltip,
GlTable,
ImportStatus,
ImportTargetCell,
......@@ -71,6 +71,7 @@ export default {
filter: '',
page: 1,
perPage: DEFAULT_PAGE_SIZE,
selectedGroups: [],
};
},
......@@ -85,11 +86,20 @@ export default {
},
fields: [
{
key: 'selected',
label: '',
// eslint-disable-next-line @gitlab/require-i18n-strings
thClass: `${DEFAULT_TH_CLASSES} gl-w-3 gl-pr-3!`,
// eslint-disable-next-line @gitlab/require-i18n-strings
tdClass: `${DEFAULT_TD_CLASSES} gl-pr-3!`,
},
{
key: 'web_url',
label: s__('BulkImport|From source group'),
thClass: `${DEFAULT_TH_CLASSES} import-jobs-from-col`,
tdClass: DEFAULT_TD_CLASSES,
thClass: `${DEFAULT_TH_CLASSES} gl-pl-0! import-jobs-from-col`,
// eslint-disable-next-line @gitlab/require-i18n-strings
tdClass: `${DEFAULT_TD_CLASSES} gl-pl-0!`,
},
{
key: 'import_target',
......@@ -117,16 +127,16 @@ export default {
return this.bulkImportSourceGroups?.nodes ?? [];
},
hasGroupsWithValidationError() {
return this.groups.some((g) => g.validation_errors.length);
hasSelectedGroups() {
return this.selectedGroups.length > 0;
},
availableGroupsForImport() {
return this.groups.filter((g) => g.progress.status === STATUSES.NONE);
hasAllAvailableGroupsSelected() {
return this.selectedGroups.length === this.availableGroupsForImport.length;
},
isImportAllButtonDisabled() {
return this.hasGroupsWithValidationError || this.availableGroupsForImport.length === 0;
availableGroupsForImport() {
return this.groups.filter((g) => g.progress.status === STATUSES.NONE && !this.isInvalid(g));
},
humanizedTotal() {
......@@ -156,7 +166,7 @@ export default {
total: 0,
};
const start = (page - 1) * perPage + 1;
const end = start + (this.bulkImportSourceGroups.nodes?.length ?? 0) - 1;
const end = start + this.groups.length - 1;
return { start, end, total };
},
......@@ -166,6 +176,17 @@ export default {
filter() {
this.page = 1;
},
groups() {
const table = this.getTableRef();
this.groups.forEach((g, idx) => {
if (this.selectedGroups.includes(g)) {
this.$nextTick(() => {
table.selectRow(idx);
});
}
});
this.selectedGroups = [];
},
},
methods: {
......@@ -210,13 +231,33 @@ export default {
});
},
importAllGroups() {
this.importGroups(this.availableGroupsForImport.map((g) => g.id));
importSelectedGroups() {
this.importGroups(this.selectedGroups.map((g) => g.id));
},
setPageSize(size) {
this.perPage = size;
},
getTableRef() {
// Acquire reference to BTable to manipulate selection
// issue: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1531
// refs are not reactive, so do not use computed here
return this.$refs.table?.$children[0];
},
preventSelectingAlreadyImportedGroups(updatedSelection) {
if (updatedSelection) {
this.selectedGroups = updatedSelection;
}
const table = this.getTableRef();
this.groups.forEach((group, idx) => {
if (table.isRowSelected(idx) && (this.isAlreadyImported(group) || this.isInvalid(group))) {
table.unselectRow(idx);
}
});
},
},
gitlabLogo: window.gon.gitlab_logo,
......@@ -231,28 +272,6 @@ export default {
>
<img :src="$options.gitlabLogo" class="gl-w-6 gl-h-6 gl-mb-2 gl-display-inline gl-mr-2" />
{{ s__('BulkImport|Import groups from GitLab') }}
<div ref="importAllButtonWrapper" class="gl-ml-auto">
<gl-button
v-if="!$apollo.loading && hasGroups"
:disabled="isImportAllButtonDisabled"
variant="confirm"
@click="importAllGroups"
>
<gl-sprintf :message="s__('BulkImport|Import %{groups}')">
<template #groups>
{{ groupsCount(availableGroupsForImport.length) }}
</template>
</gl-sprintf>
</gl-button>
</div>
<gl-tooltip v-if="isImportAllButtonDisabled" :target="() => $refs.importAllButtonWrapper">
<template v-if="hasGroupsWithValidationError">
{{ s__('BulkImport|One or more groups has validation errors') }}
</template>
<template v-else>
{{ s__('BulkImport|No groups on this page are available for import') }}
</template>
</gl-tooltip>
</h1>
<div
class="gl-py-5 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-display-flex"
......@@ -298,19 +317,58 @@ export default {
:description="s__('Check your source instance permissions.')"
/>
<template v-else>
<div
class="gl-bg-gray-10 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-p-4 gl-display-flex gl-align-items-center"
>
<gl-sprintf :message="__('%{count} selected')">
<template #count>
{{ selectedGroups.length }}
</template>
</gl-sprintf>
<gl-button
category="primary"
variant="confirm"
class="gl-ml-4"
:disabled="!hasSelectedGroups"
@click="importSelectedGroups"
>{{ s__('BulkImport|Import selected') }}</gl-button
>
</div>
<gl-table
ref="table"
class="gl-w-full"
data-qa-selector="import_table"
tbody-tr-class="gl-border-gray-200 gl-border-0 gl-border-b-1 gl-border-solid"
:tbody-tr-attr="qaRowAttributes"
:items="bulkImportSourceGroups.nodes"
:items="groups"
:fields="$options.fields"
selectable
select-mode="multi"
selected-variant="primary"
@row-selected="preventSelectingAlreadyImportedGroups"
>
<template #head(selected)="{ selectAllRows, clearSelected }">
<gl-form-checkbox
:key="`checkbox-${selectedGroups.length}`"
class="gl-h-7 gl-pt-3"
:checked="hasSelectedGroups"
:indeterminate="hasSelectedGroups && !hasAllAvailableGroupsSelected"
@change="hasAllAvailableGroupsSelected ? clearSelected() : selectAllRows()"
/>
</template>
<template #cell(selected)="{ rowSelected, selectRow, unselectRow, item: group }">
<gl-form-checkbox
class="gl-h-7 gl-pt-3"
:checked="rowSelected"
:disabled="isAlreadyImported(group) || isInvalid(group)"
@change="rowSelected ? unselectRow() : selectRow()"
/>
</template>
<template #cell(web_url)="{ value: web_url, item: { full_path } }">
<gl-link
:href="web_url"
target="_blank"
class="gl-display-flex gl-align-items-center gl-h-7"
class="gl-display-inline-flex gl-align-items-center gl-h-7"
>
{{ full_path }} <gl-icon name="external-link" />
</gl-link>
......@@ -330,7 +388,7 @@ export default {
/>
</template>
<template #cell(progress)="{ value: { status } }">
<import-status :status="status" class="gl-mt-2" />
<import-status :status="status" class="gl-line-height-32" />
</template>
<template #cell(actions)="{ item: group }">
<gl-button
......
......@@ -87,7 +87,7 @@ export default {
<template>
<gl-link
v-if="isFinished"
class="gl-display-flex gl-align-items-center gl-h-7"
class="gl-display-inline-flex gl-align-items-center gl-h-7"
:href="absolutePath"
>
{{ fullPath }}
......
......@@ -522,6 +522,9 @@ msgstr[1] ""
msgid "%{count} related %{pluralized_subject}: %{links}"
msgstr ""
msgid "%{count} selected"
msgstr ""
msgid "%{count} total weight"
msgstr ""
......@@ -5813,30 +5816,24 @@ msgstr ""
msgid "BulkImport|From source group"
msgstr ""
msgid "BulkImport|Import %{groups}"
msgstr ""
msgid "BulkImport|Import failed: Destination cannot be a subgroup of the source group. Change the destination and try again."
msgstr ""
msgid "BulkImport|Import groups from GitLab"
msgstr ""
msgid "BulkImport|Importing the group failed"
msgid "BulkImport|Import selected"
msgstr ""
msgid "BulkImport|Name already exists."
msgid "BulkImport|Importing the group failed"
msgstr ""
msgid "BulkImport|No groups on this page are available for import"
msgid "BulkImport|Name already exists."
msgstr ""
msgid "BulkImport|No parent"
msgstr ""
msgid "BulkImport|One or more groups has validation errors"
msgstr ""
msgid "BulkImport|Showing %{start}-%{end} of %{total}"
msgstr ""
......
......@@ -5,8 +5,10 @@ import {
GlSearchBoxByClick,
GlDropdown,
GlDropdownItem,
GlTable,
} from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import stubChildren from 'helpers/stub_children';
......@@ -40,10 +42,15 @@ describe('import table', () => {
];
const FAKE_PAGE_INFO = { page: 1, perPage: 20, total: 40, totalPages: 2 };
const findImportAllButton = () => wrapper.find('h1').find(GlButton);
const findImportSelectedButton = () =>
wrapper.findAllComponents(GlButton).wrappers.find((w) => w.text() === 'Import selected');
const findPaginationDropdown = () => wrapper.findComponent(GlDropdown);
const findPaginationDropdownText = () => findPaginationDropdown().find({ ref: 'text' }).text();
// TODO: remove this ugly approach when
// issue: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1531
const findTable = () => wrapper.vm.getTableRef();
const createComponent = ({ bulkImportSourceGroups }) => {
apolloProvider = createMockApollo([], {
Query: {
......@@ -294,16 +301,20 @@ describe('import table', () => {
});
});
describe('import all button', () => {
it('does not exists when no groups available', () => {
describe('bulk operations', () => {
it('import selected button is disabled when no groups selected', async () => {
createComponent({
bulkImportSourceGroups: () => new Promise(() => {}),
bulkImportSourceGroups: () => ({
nodes: FAKE_GROUPS,
pageInfo: FAKE_PAGE_INFO,
}),
});
await waitForPromises();
expect(findImportAllButton().exists()).toBe(false);
expect(findImportSelectedButton().props().disabled).toBe(true);
});
it('exists when groups are available for import', async () => {
it('import selected button is enabled when groups were selected for import', async () => {
createComponent({
bulkImportSourceGroups: () => ({
nodes: FAKE_GROUPS,
......@@ -311,16 +322,14 @@ describe('import table', () => {
}),
});
await waitForPromises();
wrapper.find(GlTable).vm.$emit('row-selected', [FAKE_GROUPS[0]]);
await nextTick();
expect(findImportAllButton().exists()).toBe(true);
expect(findImportSelectedButton().props().disabled).toBe(false);
});
it('counts only not-imported groups', async () => {
const NEW_GROUPS = [
generateFakeEntry({ id: 1, status: STATUSES.NONE }),
generateFakeEntry({ id: 2, status: STATUSES.NONE }),
generateFakeEntry({ id: 3, status: STATUSES.FINISHED }),
];
it('does not allow selecting already started groups', async () => {
const NEW_GROUPS = [generateFakeEntry({ id: 1, status: STATUSES.FINISHED })];
createComponent({
bulkImportSourceGroups: () => ({
......@@ -330,17 +339,41 @@ describe('import table', () => {
});
await waitForPromises();
expect(findImportAllButton().text()).toMatchInterpolatedText('Import 2 groups');
findTable().selectRow(0);
await nextTick();
expect(findImportSelectedButton().props().disabled).toBe(true);
});
it('disables button when any group has validation errors', async () => {
it('does not allow selecting groups with validation errors', async () => {
const NEW_GROUPS = [
generateFakeEntry({ id: 1, status: STATUSES.NONE }),
generateFakeEntry({
id: 2,
status: STATUSES.NONE,
validation_errors: [{ field: 'new_name', message: 'test validation error' }],
validation_errors: [{ field: 'new_name', message: 'FAKE_VALIDATION_ERROR' }],
}),
];
createComponent({
bulkImportSourceGroups: () => ({
nodes: NEW_GROUPS,
pageInfo: FAKE_PAGE_INFO,
}),
});
await waitForPromises();
// TODO: remove this ugly approach when
// issue: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1531
findTable().selectRow(0);
await nextTick();
expect(findImportSelectedButton().props().disabled).toBe(true);
});
it('invokes importGroups mutation when import selected button is clicked', async () => {
const NEW_GROUPS = [
generateFakeEntry({ id: 1, status: STATUSES.NONE }),
generateFakeEntry({ id: 2, status: STATUSES.NONE }),
generateFakeEntry({ id: 3, status: STATUSES.FINISHED }),
];
......@@ -350,9 +383,19 @@ describe('import table', () => {
pageInfo: FAKE_PAGE_INFO,
}),
});
jest.spyOn(apolloProvider.defaultClient, 'mutate');
await waitForPromises();
expect(findImportAllButton().props().disabled).toBe(true);
findTable().selectRow(0);
findTable().selectRow(1);
await nextTick();
findImportSelectedButton().vm.$emit('click');
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
mutation: importGroupsMutation,
variables: { sourceGroupIds: [NEW_GROUPS[0].id, NEW_GROUPS[1].id] },
});
});
});
});
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