Commit 67969908 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera

Merge branch 'xanf-bulk-import-all-on-the-page' into 'master'

GitLab Migration - Bulk import all groups on the page

See merge request gitlab-org/gitlab!61097
parents d02a1930 6a31a37b
<script>
import {
GlButton,
GlEmptyState,
GlDropdown,
GlDropdownItem,
......@@ -8,10 +9,13 @@ import {
GlLoadingIcon,
GlSearchBoxByClick,
GlSprintf,
GlSafeHtmlDirective as SafeHtml,
GlTooltip,
} from '@gitlab/ui';
import { s__, __ } from '~/locale';
import { s__, __, n__ } from '~/locale';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import importGroupMutation from '../graphql/mutations/import_group.mutation.graphql';
import { STATUSES } from '../../constants';
import importGroupsMutation from '../graphql/mutations/import_groups.mutation.graphql';
import setNewNameMutation from '../graphql/mutations/set_new_name.mutation.graphql';
import setTargetNamespaceMutation from '../graphql/mutations/set_target_namespace.mutation.graphql';
import availableNamespacesQuery from '../graphql/queries/available_namespaces.query.graphql';
......@@ -23,6 +27,7 @@ const DEFAULT_PAGE_SIZE = PAGE_SIZES[0];
export default {
components: {
GlButton,
GlEmptyState,
GlDropdown,
GlDropdownItem,
......@@ -31,9 +36,13 @@ export default {
GlLoadingIcon,
GlSearchBoxByClick,
GlSprintf,
GlTooltip,
ImportTableRow,
PaginationLinks,
},
directives: {
SafeHtml,
},
props: {
sourceUrl: {
......@@ -65,12 +74,28 @@ export default {
},
computed: {
groups() {
return this.bulkImportSourceGroups?.nodes ?? [];
},
hasGroupsWithValidationError() {
return this.groups.some((g) => g.validation_errors.length);
},
availableGroupsForImport() {
return this.groups.filter((g) => g.progress.status === STATUSES.NONE);
},
isImportAllButtonDisabled() {
return this.hasGroupsWithValidationError || this.availableGroupsForImport.length === 0;
},
humanizedTotal() {
return this.paginationInfo.total >= 1000 ? __('1000+') : this.paginationInfo.total;
},
hasGroups() {
return this.bulkImportSourceGroups?.nodes?.length > 0;
return this.groups.length > 0;
},
hasEmptyFilter() {
......@@ -105,6 +130,10 @@ export default {
},
methods: {
groupsCount(count) {
return n__('%d group', '%d groups', count);
},
setPage(page) {
this.page = page;
},
......@@ -123,24 +152,57 @@ export default {
});
},
importGroup(sourceGroupId) {
importGroups(sourceGroupIds) {
this.$apollo.mutate({
mutation: importGroupMutation,
variables: { sourceGroupId },
mutation: importGroupsMutation,
variables: { sourceGroupIds },
});
},
importAllGroups() {
this.importGroups(this.availableGroupsForImport.map((g) => g.id));
},
setPageSize(size) {
this.perPage = size;
},
},
gitlabLogo: window.gon.gitlab_logo,
PAGE_SIZES,
};
</script>
<template>
<div>
<h1
class="gl-my-0 gl-py-4 gl-font-size-h1 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-display-flex"
>
<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"
>
......@@ -153,7 +215,7 @@ export default {
<strong>{{ paginationInfo.end }}</strong>
</template>
<template #total>
<strong>{{ n__('%d group', '%d groups', paginationInfo.total) }}</strong>
<strong>{{ groupsCount(paginationInfo.total) }}</strong>
</template>
<template #filter>
<strong>{{ filter }}</strong>
......@@ -196,7 +258,7 @@ export default {
:group-path-regex="groupPathRegex"
@update-target-namespace="updateTargetNamespace(group.id, $event)"
@update-new-name="updateNewName(group.id, $event)"
@import-group="importGroup(group.id)"
@import-group="importGroups([group.id])"
/>
</template>
</tbody>
......
......@@ -10,8 +10,11 @@ import {
GlFormInput,
} from '@gitlab/ui';
import { joinPaths } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import ImportStatus from '../../components/import_status.vue';
import { STATUSES } from '../../constants';
import addValidationErrorMutation from '../graphql/mutations/add_validation_error.mutation.graphql';
import removeValidationErrorMutation from '../graphql/mutations/remove_validation_error.mutation.graphql';
import groupQuery from '../graphql/queries/group.query.graphql';
const DEBOUNCE_INTERVAL = 300;
......@@ -52,6 +55,27 @@ export default {
fullPath: this.fullPath,
};
},
update({ existingGroup }) {
const variables = {
field: 'new_name',
sourceGroupId: this.group.id,
};
if (!existingGroup) {
this.$apollo.mutate({
mutation: removeValidationErrorMutation,
variables,
});
} else {
this.$apollo.mutate({
mutation: addValidationErrorMutation,
variables: {
...variables,
message: s__('BulkImport|Name already exists.'),
},
});
}
},
skip() {
return !this.isNameValid || this.isAlreadyImported;
},
......@@ -63,8 +87,12 @@ export default {
return this.group.import_target;
},
invalidNameValidationMessage() {
return this.group.validation_errors.find(({ field }) => field === 'new_name')?.message;
},
isInvalid() {
return Boolean(!this.isNameValid || this.existingGroup);
return Boolean(!this.isNameValid || this.invalidNameValidationMessage);
},
isNameValid() {
......@@ -157,21 +185,21 @@ export default {
<template v-if="!isNameValid">
{{ __('Please choose a group URL with no special characters.') }}
</template>
<template v-else-if="existingGroup">
{{ s__('BulkImport|Name already exists.') }}
<template v-else-if="invalidNameValidationMessage">
{{ invalidNameValidationMessage }}
</template>
</p>
</div>
</div>
</td>
<td class="gl-p-4 gl-white-space-nowrap">
<import-status :status="group.progress.status" />
<import-status :status="group.progress.status" class="gl-mt-2" />
</td>
<td class="gl-p-4">
<gl-button
v-if="!isAlreadyImported"
:disabled="isInvalid"
variant="success"
variant="confirm"
category="secondary"
@click="$emit('import-group')"
>{{ __('Import') }}</gl-button
......
......@@ -20,6 +20,7 @@ export const clientTypenames = {
BulkImportPageInfo: 'ClientBulkImportPageInfo',
BulkImportTarget: 'ClientBulkImportTarget',
BulkImportProgress: 'ClientBulkImportProgress',
BulkImportValidationError: 'ClientBulkImportValidationError',
};
function makeGroup(data) {
......@@ -106,6 +107,7 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
return makeGroup({
...group,
validation_errors: [],
progress: {
id: jobId ?? localProgressId(group.id),
status: cachedImportState?.status ?? STATUSES.NONE,
......@@ -152,7 +154,7 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
async setImportProgress(_, { sourceGroupId, status, jobId }) {
if (jobId) {
groupsManager.saveImportState(jobId, { status });
groupsManager.updateImportProgress(jobId, status);
}
return makeGroup({
......@@ -165,7 +167,7 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
},
async updateImportStatus(_, { id, status }) {
groupsManager.saveImportState(id, { status });
groupsManager.updateImportProgress(id, status);
return {
__typename: clientTypenames.BulkImportProgress,
......@@ -174,39 +176,81 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
};
},
async importGroup(_, { sourceGroupId }, { client }) {
async addValidationError(_, { sourceGroupId, field, message }, { client }) {
const {
data: { bulkImportSourceGroup: group },
data: {
bulkImportSourceGroup: { validation_errors: validationErrors, ...group },
},
} = await client.query({
query: bulkImportSourceGroupQuery,
variables: { id: sourceGroupId },
});
return {
...group,
validation_errors: [
...validationErrors.filter(({ field: f }) => f !== field),
{
__typename: clientTypenames.BulkImportValidationError,
field,
message,
},
],
};
},
async removeValidationError(_, { sourceGroupId, field }, { client }) {
const {
data: {
bulkImportSourceGroup: { validation_errors: validationErrors, ...group },
},
} = await client.query({
query: bulkImportSourceGroupQuery,
variables: { id: sourceGroupId },
});
const GROUP_BEING_SCHEDULED = makeGroup({
return {
...group,
validation_errors: validationErrors.filter(({ field: f }) => f !== field),
};
},
async importGroups(_, { sourceGroupIds }, { client }) {
const groups = await Promise.all(
sourceGroupIds.map((id) =>
client
.query({
query: bulkImportSourceGroupQuery,
variables: { id },
})
.then(({ data }) => data.bulkImportSourceGroup),
),
);
const GROUPS_BEING_SCHEDULED = sourceGroupIds.map((sourceGroupId) =>
makeGroup({
id: sourceGroupId,
progress: {
id: localProgressId(sourceGroupId),
status: STATUSES.SCHEDULING,
},
});
}),
);
const defaultErrorMessage = s__('BulkImport|Importing the group failed');
axios
.post(endpoints.createBulkImport, {
bulk_import: [
{
bulk_import: groups.map((group) => ({
source_type: 'group_entity',
source_full_path: group.full_path,
destination_namespace: group.import_target.target_namespace,
destination_name: group.import_target.new_name,
},
],
})),
})
.then(({ data: { id: jobId } }) => {
groupsManager.saveImportState(jobId, {
id: group.id,
importTarget: group.import_target,
groupsManager.createImportState(jobId, {
status: STATUSES.CREATED,
groups,
});
return { status: STATUSES.CREATED, jobId };
......@@ -217,14 +261,16 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
return { status: STATUSES.NONE };
})
.then((newStatus) =>
sourceGroupIds.forEach((sourceGroupId) =>
client.mutate({
mutation: setImportProgressMutation,
variables: { sourceGroupId, ...newStatus },
}),
),
)
.catch(() => createFlash({ message: defaultErrorMessage }));
return GROUP_BEING_SCHEDULED;
return GROUPS_BEING_SCHEDULED;
},
},
};
......
......@@ -12,4 +12,8 @@ fragment BulkImportSourceGroupItem on ClientBulkImportSourceGroup {
target_namespace
new_name
}
validation_errors {
field
message
}
}
mutation addValidationError($sourceGroupId: String!, $field: String!, $message: String!) {
addValidationError(sourceGroupId: $sourceGroupId, field: $field, message: $message) @client {
id
validation_errors {
field
message
}
}
}
mutation importGroup($sourceGroupId: String!) {
importGroup(sourceGroupId: $sourceGroupId) @client {
mutation importGroups($sourceGroupIds: [String!]!) {
importGroups(sourceGroupIds: $sourceGroupIds) @client {
id
progress {
id
......
mutation removeValidationError($sourceGroupId: String!, $field: String!) {
removeValidationError(sourceGroupId: $sourceGroupId, field: $field) @client {
id
validation_errors {
field
message
}
}
}
......@@ -13,25 +13,42 @@ export class SourceGroupsManager {
loadImportStatesFromStorage() {
try {
return JSON.parse(this.storage.getItem(KEY)) ?? {};
return Object.fromEntries(
Object.entries(JSON.parse(this.storage.getItem(KEY)) ?? {}).map(([jobId, config]) => {
// new format of storage
if (config.groups) {
return [jobId, config];
}
return [
jobId,
{
status: config.status,
groups: [{ id: config.id, importTarget: config.importTarget }],
},
];
}),
);
} catch {
return {};
}
}
saveImportState(importId, group) {
const key = this.getStorageKey(importId);
const oldState = this.importStates[key] ?? {};
createImportState(importId, jobConfig) {
this.importStates[this.getStorageKey(importId)] = {
status: jobConfig.status,
groups: jobConfig.groups.map((g) => ({ importTarget: g.import_target, id: g.id })),
};
this.saveImportStatesToStorage();
}
if (!oldState.id && !group.id) {
updateImportProgress(importId, status) {
const currentState = this.importStates[this.getStorageKey(importId)];
if (!currentState) {
return;
}
this.importStates[key] = {
...oldState,
...group,
status: group.status,
};
currentState.status = status;
this.saveImportStatesToStorage();
}
......@@ -39,10 +56,15 @@ export class SourceGroupsManager {
const PREFIX = this.getStorageKey('');
const [jobId, importState] =
Object.entries(this.importStates).find(
([key, group]) => key.startsWith(PREFIX) && group.id === groupId,
([key, state]) => key.startsWith(PREFIX) && state.groups.some((g) => g.id === groupId),
) ?? [];
return { jobId, importState };
if (!jobId) {
return null;
}
const group = importState.groups.find((g) => g.id === groupId);
return { jobId, importState: { ...group, status: importState.status } };
}
getStorageKey(importId) {
......
......@@ -18,6 +18,11 @@ type ClientBulkImportProgress {
status: String!
}
type ClientBulkImportValidationError {
field: String!
message: String!
}
type ClientBulkImportSourceGroup {
id: ID!
web_url: String!
......@@ -25,6 +30,7 @@ type ClientBulkImportSourceGroup {
full_name: String!
progress: ClientBulkImportProgress!
import_target: ClientBulkImportTarget!
validation_errors: [ClientBulkImportValidationError!]!
}
type ClientBulkImportPageInfo {
......@@ -45,9 +51,15 @@ extend type Query {
}
extend type Mutation {
setNewName(newName: String, sourceGroupId: ID!): ClientTargetNamespace!
setTargetNamespace(targetNamespace: String, sourceGroupId: ID!): ClientTargetNamespace!
importGroup(id: ID!): ClientBulkImportSourceGroup!
setNewName(newName: String, sourceGroupId: ID!): ClientBulkImportSourceGroup!
setTargetNamespace(targetNamespace: String, sourceGroupId: ID!): ClientBulkImportSourceGroup!
importGroups(sourceGroupIds: [ID!]!): [ClientBulkImportSourceGroup!]!
setImportProgress(id: ID, status: String!): ClientBulkImportSourceGroup!
updateImportProgress(id: ID, status: String!): ClientBulkImportProgress
addValidationError(
sourceGroupId: ID!
field: String!
message: String!
): ClientBulkImportSourceGroup!
removeValidationError(sourceGroupId: ID!, field: String!): ClientBulkImportSourceGroup!
}
......@@ -2,9 +2,6 @@
- add_page_specific_style 'page_bundles/import'
- breadcrumb_title _('Import groups')
%h1.gl-my-0.gl-py-4.gl-font-size-h1.gl-border-solid.gl-border-gray-200.gl-border-0.gl-border-b-1
= s_('BulkImport|Import groups from GitLab')
#import-groups-mount-element{ data: { status_path: status_import_bulk_imports_path(format: :json),
available_namespaces_path: import_available_namespaces_path(format: :json),
create_bulk_import_path: import_bulk_imports_path(format: :json),
......
---
title: Implement bulk import for all groups on the page
merge_request: 61097
author:
type: added
......@@ -5460,6 +5460,9 @@ 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 ""
......@@ -5472,9 +5475,15 @@ msgstr ""
msgid "BulkImport|Name already exists."
msgstr ""
msgid "BulkImport|No groups on this page are available for import"
msgstr ""
msgid "BulkImport|No parent"
msgstr ""
msgid "BulkImport|One or more groups has validation errors"
msgstr ""
msgid "BulkImport|Showing %{start}-%{end} of %{total}"
msgstr ""
......
......@@ -19,6 +19,7 @@ const getFakeGroup = (status) => ({
new_name: 'group1',
},
id: 1,
validation_errors: [],
progress: { status },
});
......@@ -187,21 +188,25 @@ describe('import table row', () => {
expect(wrapper.text()).toContain('Please choose a group URL with no special characters.');
});
it('Reports invalid group name if group already exists', async () => {
it('Reports invalid group name if relevant validation error exists', async () => {
const FAKE_ERROR_MESSAGE = 'fake error';
createComponent({
group: {
...getFakeGroup(STATUSES.NONE),
import_target: {
target_namespace: EXISTING_GROUP_TARGET_NAMESPACE,
new_name: EXISTING_GROUP_PATH,
validation_errors: [
{
field: 'new_name',
message: FAKE_ERROR_MESSAGE,
},
],
},
});
jest.runOnlyPendingTimers();
await nextTick();
expect(wrapper.text()).toContain('Name already exists.');
expect(wrapper.text()).toContain(FAKE_ERROR_MESSAGE);
});
});
});
import {
GlButton,
GlEmptyState,
GlLoadingIcon,
GlSearchBoxByClick,
......@@ -14,7 +15,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { STATUSES } from '~/import_entities/constants';
import ImportTable from '~/import_entities/import_groups/components/import_table.vue';
import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue';
import importGroupMutation from '~/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql';
import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql';
import setNewNameMutation from '~/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql';
import setTargetNamespaceMutation from '~/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
......@@ -40,6 +41,7 @@ describe('import table', () => {
];
const FAKE_PAGE_INFO = { page: 1, perPage: 20, total: 40, totalPages: 2 };
const findImportAllButton = () => wrapper.find('h1').find(GlButton);
const findPaginationDropdown = () => wrapper.findComponent(GlDropdown);
const findPaginationDropdownText = () => findPaginationDropdown().find({ ref: 'text' }).text();
......@@ -72,7 +74,6 @@ describe('import table', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders loading icon while performing request', async () => {
......@@ -141,7 +142,7 @@ describe('import table', () => {
event | payload | mutation | variables
${'update-target-namespace'} | ${'new-namespace'} | ${setTargetNamespaceMutation} | ${{ sourceGroupId: FAKE_GROUP.id, targetNamespace: 'new-namespace' }}
${'update-new-name'} | ${'new-name'} | ${setNewNameMutation} | ${{ sourceGroupId: FAKE_GROUP.id, newName: 'new-name' }}
${'import-group'} | ${undefined} | ${importGroupMutation} | ${{ sourceGroupId: FAKE_GROUP.id }}
${'import-group'} | ${undefined} | ${importGroupsMutation} | ${{ sourceGroupIds: [FAKE_GROUP.id] }}
`('correctly maps $event to mutation', async ({ event, payload, mutation, variables }) => {
jest.spyOn(apolloProvider.defaultClient, 'mutate');
wrapper.find(ImportTableRow).vm.$emit(event, payload);
......@@ -277,4 +278,66 @@ describe('import table', () => {
);
});
});
describe('import all button', () => {
it('does not exists when no groups available', () => {
createComponent({
bulkImportSourceGroups: () => new Promise(() => {}),
});
expect(findImportAllButton().exists()).toBe(false);
});
it('exists when groups are available for import', async () => {
createComponent({
bulkImportSourceGroups: () => ({
nodes: FAKE_GROUPS,
pageInfo: FAKE_PAGE_INFO,
}),
});
await waitForPromises();
expect(findImportAllButton().exists()).toBe(true);
});
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 }),
];
createComponent({
bulkImportSourceGroups: () => ({
nodes: NEW_GROUPS,
pageInfo: FAKE_PAGE_INFO,
}),
});
await waitForPromises();
expect(findImportAllButton().text()).toMatchInterpolatedText('Import 2 groups');
});
it('disables button when any group has 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' }],
}),
generateFakeEntry({ id: 3, status: STATUSES.FINISHED }),
];
createComponent({
bulkImportSourceGroups: () => ({
nodes: NEW_GROUPS,
pageInfo: FAKE_PAGE_INFO,
}),
});
await waitForPromises();
expect(findImportAllButton().props().disabled).toBe(true);
});
});
});
......@@ -8,7 +8,9 @@ import {
clientTypenames,
createResolvers,
} from '~/import_entities/import_groups/graphql/client_factory';
import importGroupMutation from '~/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql';
import addValidationErrorMutation from '~/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql';
import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql';
import removeValidationErrorMutation from '~/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql';
import setImportProgressMutation from '~/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql';
import setNewNameMutation from '~/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql';
import setTargetNamespaceMutation from '~/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql';
......@@ -240,6 +242,7 @@ describe('Bulk import resolvers', () => {
target_namespace: 'root',
new_name: 'group1',
},
validation_errors: [],
},
],
pageInfo: {
......@@ -294,8 +297,8 @@ describe('Bulk import resolvers', () => {
axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(() => new Promise(() => {}));
client.mutate({
mutation: importGroupMutation,
variables: { sourceGroupId: GROUP_ID },
mutation: importGroupsMutation,
variables: { sourceGroupIds: [GROUP_ID] },
});
await waitForPromises();
......@@ -325,8 +328,8 @@ describe('Bulk import resolvers', () => {
it('sets import status to CREATED when request completes', async () => {
axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(httpStatus.OK, { id: 1 });
await client.mutate({
mutation: importGroupMutation,
variables: { sourceGroupId: GROUP_ID },
mutation: importGroupsMutation,
variables: { sourceGroupIds: [GROUP_ID] },
});
await waitForPromises();
......@@ -340,8 +343,8 @@ describe('Bulk import resolvers', () => {
client
.mutate({
mutation: importGroupMutation,
variables: { sourceGroupId: GROUP_ID },
mutation: [importGroupsMutation],
variables: { sourceGroupIds: [GROUP_ID] },
})
.catch(() => {});
await waitForPromises();
......@@ -357,8 +360,8 @@ describe('Bulk import resolvers', () => {
client
.mutate({
mutation: importGroupMutation,
variables: { sourceGroupId: GROUP_ID },
mutation: importGroupsMutation,
variables: { sourceGroupIds: [GROUP_ID] },
})
.catch(() => {});
await waitForPromises();
......@@ -375,8 +378,8 @@ describe('Bulk import resolvers', () => {
client
.mutate({
mutation: importGroupMutation,
variables: { sourceGroupId: GROUP_ID },
mutation: importGroupsMutation,
variables: { sourceGroupIds: [GROUP_ID] },
})
.catch(() => {});
await waitForPromises();
......@@ -418,5 +421,41 @@ describe('Bulk import resolvers', () => {
status: NEW_STATUS,
});
});
it('addValidationError adds error to group', async () => {
const FAKE_FIELD = 'some-field';
const FAKE_MESSAGE = 'some-message';
const {
data: {
addValidationError: { validation_errors: validationErrors },
},
} = await client.mutate({
mutation: addValidationErrorMutation,
variables: { sourceGroupId: GROUP_ID, field: FAKE_FIELD, message: FAKE_MESSAGE },
});
expect(validationErrors).toMatchObject([{ field: FAKE_FIELD, message: FAKE_MESSAGE }]);
});
it('removeValidationError removes error from group', async () => {
const FAKE_FIELD = 'some-field';
const FAKE_MESSAGE = 'some-message';
await client.mutate({
mutation: addValidationErrorMutation,
variables: { sourceGroupId: GROUP_ID, field: FAKE_FIELD, message: FAKE_MESSAGE },
});
const {
data: {
removeValidationError: { validation_errors: validationErrors },
},
} = await client.mutate({
mutation: removeValidationErrorMutation,
variables: { sourceGroupId: GROUP_ID, field: FAKE_FIELD },
});
expect(validationErrors).toMatchObject([]);
});
});
});
......@@ -14,6 +14,7 @@ export const generateFakeEntry = ({ id, status, ...rest }) => ({
id: `test-${id}`,
status,
},
validation_errors: [],
...rest,
});
......
......@@ -22,33 +22,42 @@ describe('SourceGroupsManager', () => {
const IMPORT_ID = 1;
const IMPORT_TARGET = { destination_name: 'demo', destination_namespace: 'foo' };
const STATUS = 'FAKE_STATUS';
const FAKE_GROUP = { id: 1, importTarget: IMPORT_TARGET, status: STATUS };
const FAKE_GROUP = { id: 1, import_target: IMPORT_TARGET, status: STATUS };
it('loads state from storage on creation', () => {
expect(storage.getItem).toHaveBeenCalledWith(KEY);
});
it('saves to storage when saveImportState is called', () => {
manager.saveImportState(IMPORT_ID, FAKE_GROUP);
it('saves to storage when createImportState is called', () => {
const FAKE_STATUS = 'fake;';
manager.createImportState(IMPORT_ID, { status: FAKE_STATUS, groups: [FAKE_GROUP] });
const storedObject = JSON.parse(storage.setItem.mock.calls[0][1]);
expect(Object.values(storedObject)[0]).toStrictEqual({
status: FAKE_STATUS,
groups: [
{
id: FAKE_GROUP.id,
importTarget: IMPORT_TARGET,
status: STATUS,
},
],
});
});
it('updates storage when previous state is available', () => {
const CHANGED_STATUS = 'changed';
manager.saveImportState(IMPORT_ID, FAKE_GROUP);
manager.createImportState(IMPORT_ID, { status: STATUS, groups: [FAKE_GROUP] });
manager.saveImportState(IMPORT_ID, { status: CHANGED_STATUS });
manager.updateImportProgress(IMPORT_ID, CHANGED_STATUS);
const storedObject = JSON.parse(storage.setItem.mock.calls[1][1]);
expect(Object.values(storedObject)[0]).toStrictEqual({
status: CHANGED_STATUS,
groups: [
{
id: FAKE_GROUP.id,
importTarget: IMPORT_TARGET,
status: CHANGED_STATUS,
},
],
});
});
});
......
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