Commit 3b7490d0 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'xanf-refactor-validation' into 'master'

Move validation logic from Vue component to resolvers

See merge request gitlab-org/gitlab!67836
parents 4cdce8e1 5571c177
......@@ -16,8 +16,7 @@ import { s__, __, n__ } from '~/locale';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
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 setImportTargetMutation from '../graphql/mutations/set_import_target.mutation.graphql';
import availableNamespacesQuery from '../graphql/queries/available_namespaces.query.graphql';
import bulkImportSourceGroupsQuery from '../graphql/queries/bulk_import_source_groups.query.graphql';
import ImportTableRow from './import_table_row.vue';
......@@ -142,17 +141,10 @@ export default {
this.page = page;
},
updateTargetNamespace(sourceGroupId, targetNamespace) {
updateImportTarget(sourceGroupId, targetNamespace, newName) {
this.$apollo.mutate({
mutation: setTargetNamespaceMutation,
variables: { sourceGroupId, targetNamespace },
});
},
updateNewName(sourceGroupId, newName) {
this.$apollo.mutate({
mutation: setNewNameMutation,
variables: { sourceGroupId, newName },
mutation: setImportTargetMutation,
variables: { sourceGroupId, targetNamespace, newName },
});
},
......@@ -266,8 +258,12 @@ export default {
:available-namespaces="availableNamespaces"
:group-path-regex="groupPathRegex"
:group-url-error-message="groupUrlErrorMessage"
@update-target-namespace="updateTargetNamespace(group.id, $event)"
@update-new-name="updateNewName(group.id, $event)"
@update-target-namespace="
updateImportTarget(group.id, $event, group.import_target.new_name)
"
@update-new-name="
updateImportTarget(group.id, group.import_target.target_namespace, $event)
"
@import-group="importGroups([group.id])"
/>
</template>
......
......@@ -9,15 +9,9 @@ import {
GlFormInput,
} from '@gitlab/ui';
import { joinPaths } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import ImportGroupDropdown from '../../components/group_dropdown.vue';
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 groupAndProjectQuery from '../graphql/queries/groupAndProject.query.graphql';
const DEBOUNCE_INTERVAL = 300;
export default {
components: {
......@@ -50,42 +44,6 @@ export default {
},
},
apollo: {
existingGroupAndProject: {
query: groupAndProjectQuery,
debounce: DEBOUNCE_INTERVAL,
variables() {
return {
fullPath: this.fullPath,
};
},
update({ existingGroup, existingProject }) {
const variables = {
field: 'new_name',
sourceGroupId: this.group.id,
};
if (!existingGroup && !existingProject) {
this.$apollo.mutate({
mutation: removeValidationErrorMutation,
variables,
});
} else {
this.$apollo.mutate({
mutation: addValidationErrorMutation,
variables: {
...variables,
message: this.$options.i18n.NAME_ALREADY_EXISTS,
},
});
}
},
skip() {
return !this.isNameValid || this.isAlreadyImported;
},
},
},
computed: {
availableNamespaceNames() {
return this.availableNamespaces.map((ns) => ns.full_path);
......@@ -123,10 +81,6 @@ export default {
return joinPaths(gon.relative_url_root || '/', this.fullPath);
},
},
i18n: {
NAME_ALREADY_EXISTS: s__('BulkImport|Name already exists.'),
},
};
</script>
......
import { s__ } from '~/locale';
export const i18n = {
NAME_ALREADY_EXISTS: s__('BulkImport|Name already exists.'),
};
......@@ -4,11 +4,15 @@ import axios from '~/lib/utils/axios_utils';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import { STATUSES } from '../../constants';
import { i18n } from '../constants';
import bulkImportSourceGroupItemFragment from './fragments/bulk_import_source_group_item.fragment.graphql';
import addValidationErrorMutation from './mutations/add_validation_error.mutation.graphql';
import removeValidationErrorMutation from './mutations/remove_validation_error.mutation.graphql';
import setImportProgressMutation from './mutations/set_import_progress.mutation.graphql';
import updateImportStatusMutation from './mutations/update_import_status.mutation.graphql';
import availableNamespacesQuery from './queries/available_namespaces.query.graphql';
import bulkImportSourceGroupQuery from './queries/bulk_import_source_group.query.graphql';
import groupAndProjectQuery from './queries/group_and_project.query.graphql';
import { SourceGroupsManager } from './services/source_groups_manager';
import { StatusPoller } from './services/status_poller';
import typeDefs from './typedefs.graphql';
......@@ -46,6 +50,37 @@ function makeGroup(data) {
return result;
}
async function checkImportTargetIsValid({ client, newName, targetNamespace, sourceGroupId }) {
const {
data: { existingGroup, existingProject },
} = await client.query({
query: groupAndProjectQuery,
variables: {
fullPath: `${targetNamespace}/${newName}`,
},
});
const variables = {
field: 'new_name',
sourceGroupId,
};
if (!existingGroup && !existingProject) {
client.mutate({
mutation: removeValidationErrorMutation,
variables,
});
} else {
client.mutate({
mutation: addValidationErrorMutation,
variables: {
...variables,
message: i18n.NAME_ALREADY_EXISTS,
},
});
}
}
const localProgressId = (id) => `not-started-${id}`;
export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGroupsManager }) {
......@@ -99,7 +134,7 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
]) => {
const pagination = parseIntPagination(normalizeHeaders(headers));
return {
const response = {
__typename: clientTypenames.BulkImportSourceGroupConnection,
nodes: data.importable_data.map((group) => {
const { jobId, importState: cachedImportState } =
......@@ -123,6 +158,21 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
...pagination,
},
};
setTimeout(() => {
response.nodes.forEach((group) => {
if (group.progress.status === STATUSES.NONE) {
checkImportTargetIsValid({
client,
newName: group.import_target.new_name,
targetNamespace: group.import_target.target_namespace,
sourceGroupId: group.id,
});
}
});
});
return response;
},
);
},
......@@ -136,6 +186,22 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
),
},
Mutation: {
setImportTarget(_, { targetNamespace, newName, sourceGroupId }, { client }) {
checkImportTargetIsValid({
client,
sourceGroupId,
targetNamespace,
newName,
});
return makeGroup({
id: sourceGroupId,
import_target: {
target_namespace: targetNamespace,
new_name: newName,
},
});
},
setTargetNamespace: (_, { targetNamespace, sourceGroupId }) =>
makeGroup({
id: sourceGroupId,
......
mutation setImportTarget($newName: String!, $targetNamespace: String!, $sourceGroupId: String!) {
setImportTarget(
newName: $newName
targetNamespace: $targetNamespace
sourceGroupId: $sourceGroupId
) @client {
id
import_target {
new_name
target_namespace
}
}
}
mutation setNewName($newName: String!, $sourceGroupId: String!) {
setNewName(newName: $newName, sourceGroupId: $sourceGroupId) @client {
id
import_target {
new_name
}
}
}
mutation setTargetNamespace($targetNamespace: String!, $sourceGroupId: String!) {
setTargetNamespace(targetNamespace: $targetNamespace, sourceGroupId: $sourceGroupId) @client {
id
import_target {
target_namespace
}
}
}
......@@ -2,19 +2,13 @@ import { GlButton, GlDropdownItem, GlLink, GlFormInput } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import ImportGroupDropdown from '~/import_entities/components/group_dropdown.vue';
import { STATUSES } from '~/import_entities/constants';
import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue';
import addValidationErrorMutation from '~/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql';
import removeValidationErrorMutation from '~/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql';
import groupAndProjectQuery from '~/import_entities/import_groups/graphql/queries/groupAndProject.query.graphql';
import { availableNamespacesFixture } from '../graphql/fixtures';
Vue.use(VueApollo);
const { i18n: I18N } = ImportTableRow;
const getFakeGroup = (status) => ({
web_url: 'https://fake.host/',
full_path: 'fake_group_1',
......@@ -28,13 +22,8 @@ const getFakeGroup = (status) => ({
progress: { status },
});
const EXISTING_GROUP_TARGET_NAMESPACE = 'existing-group';
const EXISTING_GROUP_PATH = 'existing-path';
const EXISTING_PROJECT_PATH = 'existing-project-path';
describe('import table row', () => {
let wrapper;
let apolloProvider;
let group;
const findByText = (cmp, text) => {
......@@ -45,27 +34,7 @@ describe('import table row', () => {
const findNamespaceDropdown = () => wrapper.find(ImportGroupDropdown);
const createComponent = (props) => {
apolloProvider = createMockApollo([
[
groupAndProjectQuery,
({ fullPath }) => {
const existingGroup =
fullPath === `${EXISTING_GROUP_TARGET_NAMESPACE}/${EXISTING_GROUP_PATH}`
? { id: 1 }
: null;
const existingProject =
fullPath === `${EXISTING_GROUP_TARGET_NAMESPACE}/${EXISTING_PROJECT_PATH}`
? { id: 1 }
: null;
return Promise.resolve({ data: { existingGroup, existingProject } });
},
],
]);
wrapper = shallowMount(ImportTableRow, {
apolloProvider,
stubs: { ImportGroupDropdown },
propsData: {
availableNamespaces: availableNamespacesFixture,
......@@ -224,101 +193,5 @@ describe('import table row', () => {
expect(wrapper.text()).toContain(FAKE_ERROR_MESSAGE);
});
it('sets validation error when targetting existing group', async () => {
const testGroup = getFakeGroup(STATUSES.NONE);
createComponent({
group: {
...testGroup,
import_target: {
target_namespace: EXISTING_GROUP_TARGET_NAMESPACE,
new_name: EXISTING_GROUP_PATH,
},
},
});
jest.spyOn(wrapper.vm.$apollo, 'mutate');
jest.runOnlyPendingTimers();
await nextTick();
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: addValidationErrorMutation,
variables: {
field: 'new_name',
message: I18N.NAME_ALREADY_EXISTS,
sourceGroupId: testGroup.id,
},
});
});
it('sets validation error when targetting existing project', async () => {
const testGroup = getFakeGroup(STATUSES.NONE);
createComponent({
group: {
...testGroup,
import_target: {
target_namespace: EXISTING_GROUP_TARGET_NAMESPACE,
new_name: EXISTING_PROJECT_PATH,
},
},
});
jest.spyOn(wrapper.vm.$apollo, 'mutate');
jest.runOnlyPendingTimers();
await nextTick();
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: addValidationErrorMutation,
variables: {
field: 'new_name',
message: I18N.NAME_ALREADY_EXISTS,
sourceGroupId: testGroup.id,
},
});
});
it('clears validation error when target is updated', async () => {
const testGroup = getFakeGroup(STATUSES.NONE);
createComponent({
group: {
...testGroup,
import_target: {
target_namespace: EXISTING_GROUP_TARGET_NAMESPACE,
new_name: EXISTING_PROJECT_PATH,
},
},
});
jest.runOnlyPendingTimers();
await nextTick();
jest.spyOn(wrapper.vm.$apollo, 'mutate');
await wrapper.setProps({
group: {
...testGroup,
import_target: {
target_namespace: 'valid_namespace',
new_name: 'valid_path',
},
},
});
jest.runOnlyPendingTimers();
await nextTick();
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: removeValidationErrorMutation,
variables: {
field: 'new_name',
sourceGroupId: testGroup.id,
},
});
});
});
});
......@@ -16,8 +16,7 @@ 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 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 setImportTargetMutation from '~/import_entities/import_groups/graphql/mutations/set_import_target.mutation.graphql';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import { availableNamespacesFixture, generateFakeEntry } from '../graphql/fixtures';
......@@ -140,10 +139,10 @@ describe('import table', () => {
});
it.each`
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} | ${importGroupsMutation} | ${{ sourceGroupIds: [FAKE_GROUP.id] }}
event | payload | mutation | variables
${'update-target-namespace'} | ${'new-namespace'} | ${setImportTargetMutation} | ${{ sourceGroupId: FAKE_GROUP.id, targetNamespace: 'new-namespace', newName: 'group1' }}
${'update-new-name'} | ${'new-name'} | ${setImportTargetMutation} | ${{ sourceGroupId: FAKE_GROUP.id, targetNamespace: 'root', newName: 'new-name' }}
${'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);
......
......@@ -12,12 +12,12 @@ import addValidationErrorMutation from '~/import_entities/import_groups/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';
import setImportTargetMutation from '~/import_entities/import_groups/graphql/mutations/set_import_target.mutation.graphql';
import updateImportStatusMutation from '~/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql';
import availableNamespacesQuery from '~/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql';
import bulkImportSourceGroupQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql';
import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql';
import groupAndProjectQuery from '~/import_entities/import_groups/graphql/queries/group_and_project.query.graphql';
import { StatusPoller } from '~/import_entities/import_groups/graphql/services/status_poller';
import axios from '~/lib/utils/axios_utils';
......@@ -38,18 +38,29 @@ const FAKE_ENDPOINTS = {
jobs: '/fake_jobs',
};
const FAKE_GROUP_AND_PROJECTS_QUERY_HANDLER = jest.fn().mockResolvedValue({
data: {
existingGroup: null,
existingProject: null,
},
});
describe('Bulk import resolvers', () => {
let axiosMockAdapter;
let client;
const createClient = (extraResolverArgs) => {
return createMockClient({
const mockedClient = createMockClient({
cache: new InMemoryCache({
fragmentMatcher: { match: () => true },
addTypename: false,
}),
resolvers: createResolvers({ endpoints: FAKE_ENDPOINTS, ...extraResolverArgs }),
});
mockedClient.setRequestHandler(groupAndProjectQuery, FAKE_GROUP_AND_PROJECTS_QUERY_HANDLER);
return mockedClient;
};
beforeEach(() => {
......@@ -196,6 +207,12 @@ describe('Bulk import resolvers', () => {
const [statusPoller] = StatusPoller.mock.instances;
expect(statusPoller.startPolling).toHaveBeenCalled();
});
it('requests validation status when request completes', async () => {
expect(FAKE_GROUP_AND_PROJECTS_QUERY_HANDLER).not.toHaveBeenCalled();
jest.runOnlyPendingTimers();
expect(FAKE_GROUP_AND_PROJECTS_QUERY_HANDLER).toHaveBeenCalled();
});
});
it.each`
......@@ -256,40 +273,49 @@ describe('Bulk import resolvers', () => {
});
});
it('setTargetNamespaces updates group target namespace', async () => {
const NEW_TARGET_NAMESPACE = 'target';
const {
data: {
setTargetNamespace: {
id: idInResponse,
import_target: { target_namespace: namespaceInResponse },
describe('setImportTarget', () => {
it('updates group target namespace and name', async () => {
const NEW_TARGET_NAMESPACE = 'target';
const NEW_NAME = 'new';
const {
data: {
setImportTarget: {
id: idInResponse,
import_target: { target_namespace: namespaceInResponse, new_name: newNameInResponse },
},
},
},
} = await client.mutate({
mutation: setTargetNamespaceMutation,
variables: { sourceGroupId: GROUP_ID, targetNamespace: NEW_TARGET_NAMESPACE },
} = await client.mutate({
mutation: setImportTargetMutation,
variables: {
sourceGroupId: GROUP_ID,
targetNamespace: NEW_TARGET_NAMESPACE,
newName: NEW_NAME,
},
});
expect(idInResponse).toBe(GROUP_ID);
expect(namespaceInResponse).toBe(NEW_TARGET_NAMESPACE);
expect(newNameInResponse).toBe(NEW_NAME);
});
expect(idInResponse).toBe(GROUP_ID);
expect(namespaceInResponse).toBe(NEW_TARGET_NAMESPACE);
});
it('invokes validation', async () => {
const NEW_TARGET_NAMESPACE = 'target';
const NEW_NAME = 'new';
it('setNewName updates group target name', async () => {
const NEW_NAME = 'new';
const {
data: {
setNewName: {
id: idInResponse,
import_target: { new_name: nameInResponse },
await client.mutate({
mutation: setImportTargetMutation,
variables: {
sourceGroupId: GROUP_ID,
targetNamespace: NEW_TARGET_NAMESPACE,
newName: NEW_NAME,
},
},
} = await client.mutate({
mutation: setNewNameMutation,
variables: { sourceGroupId: GROUP_ID, newName: NEW_NAME },
});
});
expect(idInResponse).toBe(GROUP_ID);
expect(nameInResponse).toBe(NEW_NAME);
expect(FAKE_GROUP_AND_PROJECTS_QUERY_HANDLER).toHaveBeenCalledWith({
fullPath: `${NEW_TARGET_NAMESPACE}/${NEW_NAME}`,
});
});
});
describe('importGroup', () => {
......
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