Commit 66e3288b authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'xanf-refactor-import-projects-store' into 'master'

Refactor import_projects store

See merge request gitlab-org/gitlab!39383
parents 5df7a5d0 344cfc45
...@@ -6,7 +6,7 @@ import { __, sprintf } from '~/locale'; ...@@ -6,7 +6,7 @@ import { __, sprintf } from '~/locale';
import ImportedProjectTableRow from './imported_project_table_row.vue'; import ImportedProjectTableRow from './imported_project_table_row.vue';
import ProviderRepoTableRow from './provider_repo_table_row.vue'; import ProviderRepoTableRow from './provider_repo_table_row.vue';
import IncompatibleRepoTableRow from './incompatible_repo_table_row.vue'; import IncompatibleRepoTableRow from './incompatible_repo_table_row.vue';
import eventHub from '../event_hub'; import { isProjectImportable } from '../utils';
const reposFetchThrottleDelay = 1000; const reposFetchThrottleDelay = 1000;
...@@ -32,20 +32,29 @@ export default { ...@@ -32,20 +32,29 @@ export default {
}, },
computed: { computed: {
...mapState([ ...mapState(['filter', 'repositories', 'namespaces', 'defaultTargetNamespace']),
'importedProjects',
'providerRepos',
'incompatibleRepos',
'isLoadingRepos',
'filter',
]),
...mapGetters([ ...mapGetters([
'isLoading',
'isImportingAnyRepo', 'isImportingAnyRepo',
'hasProviderRepos', 'hasImportableRepos',
'hasImportedProjects',
'hasIncompatibleRepos', 'hasIncompatibleRepos',
]), ]),
availableNamespaces() {
const serializedNamespaces = this.namespaces.map(({ fullPath }) => ({
id: fullPath,
text: fullPath,
}));
return [
{ text: __('Groups'), children: serializedNamespaces },
{
text: __('Users'),
children: [{ id: this.defaultTargetNamespace, text: this.defaultTargetNamespace }],
},
];
},
importAllButtonText() { importAllButtonText() {
return this.hasIncompatibleRepos return this.hasIncompatibleRepos
? __('Import all compatible repositories') ? __('Import all compatible repositories')
...@@ -64,7 +73,8 @@ export default { ...@@ -64,7 +73,8 @@ export default {
}, },
mounted() { mounted() {
return this.fetchRepos(); this.fetchNamespaces();
this.fetchRepos();
}, },
beforeDestroy() { beforeDestroy() {
...@@ -75,17 +85,13 @@ export default { ...@@ -75,17 +85,13 @@ export default {
methods: { methods: {
...mapActions([ ...mapActions([
'fetchRepos', 'fetchRepos',
'fetchReposFiltered', 'fetchNamespaces',
'fetchJobs',
'stopJobsPolling', 'stopJobsPolling',
'clearJobsEtagPoll', 'clearJobsEtagPoll',
'setFilter', 'setFilter',
'importAll',
]), ]),
importAll() {
eventHub.$emit('importAll');
},
handleFilterInput({ target }) { handleFilterInput({ target }) {
this.setFilter(target.value); this.setFilter(target.value);
}, },
...@@ -93,6 +99,8 @@ export default { ...@@ -93,6 +99,8 @@ export default {
throttledFetchRepos: throttle(function fetch() { throttledFetchRepos: throttle(function fetch() {
this.fetchRepos(); this.fetchRepos();
}, reposFetchThrottleDelay), }, reposFetchThrottleDelay),
isProjectImportable,
}, },
}; };
</script> </script>
...@@ -103,21 +111,17 @@ export default { ...@@ -103,21 +111,17 @@ export default {
{{ s__('ImportProjects|Select the projects you want to import') }} {{ s__('ImportProjects|Select the projects 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>
</template> </template>
<div <div v-if="!isLoading" class="d-flex justify-content-between align-items-end flex-wrap mb-3">
v-if="!isLoadingRepos"
class="d-flex justify-content-between align-items-end flex-wrap mb-3"
>
<gl-button <gl-button
variant="success" variant="success"
:loading="isImportingAnyRepo" :loading="isImportingAnyRepo"
:disabled="!hasProviderRepos" :disabled="!hasImportableRepos"
type="button" type="button"
@click="importAll" @click="importAll"
>{{ importAllButtonText }}</gl-button
> >
{{ importAllButtonText }}
</gl-button>
<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
...@@ -134,14 +138,11 @@ export default { ...@@ -134,14 +138,11 @@ export default {
</form> </form>
</div> </div>
<gl-loading-icon <gl-loading-icon
v-if="isLoadingRepos" v-if="isLoading"
class="js-loading-button-icon import-projects-loading-icon" class="js-loading-button-icon import-projects-loading-icon"
size="md" size="md"
/> />
<div <div v-else-if="repositories.length" class="table-responsive">
v-else-if="hasProviderRepos || hasImportedProjects || hasIncompatibleRepos"
class="table-responsive"
>
<table class="table import-table"> <table class="table import-table">
<thead> <thead>
<th class="import-jobs-from-col">{{ fromHeaderText }}</th> <th class="import-jobs-from-col">{{ fromHeaderText }}</th>
...@@ -150,17 +151,20 @@ export default { ...@@ -150,17 +151,20 @@ export default {
<th class="import-jobs-cta-col"></th> <th class="import-jobs-cta-col"></th>
</thead> </thead>
<tbody> <tbody>
<imported-project-table-row <template v-for="repo in repositories">
v-for="project in importedProjects" <incompatible-repo-table-row
:key="project.id" v-if="repo.importSource.incompatible"
:project="project" :key="repo.importSource.id"
/> :repo="repo"
<provider-repo-table-row v-for="repo in providerRepos" :key="repo.id" :repo="repo" /> />
<incompatible-repo-table-row <provider-repo-table-row
v-for="repo in incompatibleRepos" v-else-if="isProjectImportable(repo)"
:key="repo.id" :key="repo.importSource.id"
:repo="repo" :repo="repo"
/> :available-namespaces="availableNamespaces"
/>
<imported-project-table-row v-else :key="repo.importSource.id" :project="repo" />
</template>
</tbody> </tbody>
</table> </table>
</div> </div>
......
...@@ -18,7 +18,7 @@ export default { ...@@ -18,7 +18,7 @@ export default {
computed: { computed: {
displayFullPath() { displayFullPath() {
return this.project.fullPath.replace(/^\//, ''); return this.project.importedProject.fullPath.replace(/^\//, '');
}, },
isFinished() { isFinished() {
...@@ -29,29 +29,30 @@ export default { ...@@ -29,29 +29,30 @@ export default {
</script> </script>
<template> <template>
<tr class="js-imported-project import-row"> <tr class="import-row">
<td> <td>
<a <a
:href="project.providerLink" :href="project.importSource.providerLink"
rel="noreferrer noopener" rel="noreferrer noopener"
target="_blank" target="_blank"
class="js-provider-link" data-testid="providerLink"
> >{{ project.importSource.fullName }}
{{ project.importSource }} <gl-icon v-if="project.importSource.providerLink" name="external-link" />
<gl-icon v-if="project.providerLink" name="external-link" />
</a> </a>
</td> </td>
<td class="js-full-path">{{ displayFullPath }}</td> <td data-testid="fullPath">{{ displayFullPath }}</td>
<td><import-status :status="project.importStatus" /></td> <td>
<import-status :status="project.importStatus" />
</td>
<td> <td>
<a <a
v-if="isFinished" v-if="isFinished"
class="btn btn-default js-go-to-project" class="btn btn-default"
:href="project.fullPath" data-testid="goToProject"
:href="project.importedProject.fullPath"
rel="noreferrer noopener" rel="noreferrer noopener"
target="_blank" target="_blank"
> >{{ __('Go to project') }}
{{ __('Go to project') }}
</a> </a>
</td> </td>
</tr> </tr>
......
...@@ -18,9 +18,9 @@ export default { ...@@ -18,9 +18,9 @@ export default {
<template> <template>
<tr class="import-row"> <tr class="import-row">
<td> <td>
<a :href="repo.providerLink" rel="noreferrer noopener" target="_blank"> <a :href="repo.importSource.providerLink" rel="noreferrer noopener" target="_blank"
{{ repo.fullName }} >{{ repo.importSource.fullName }}
<gl-icon v-if="repo.providerLink" name="external-link" /> <gl-icon v-if="repo.importSource.providerLink" name="external-link" />
</a> </a>
</td> </td>
<td></td> <td></td>
......
...@@ -3,8 +3,6 @@ import { mapState, mapGetters, mapActions } from 'vuex'; ...@@ -3,8 +3,6 @@ import { mapState, mapGetters, mapActions } from 'vuex';
import { GlIcon } from '@gitlab/ui'; import { GlIcon } from '@gitlab/ui';
import Select2Select from '~/vue_shared/components/select2_select.vue'; import Select2Select from '~/vue_shared/components/select2_select.vue';
import { __ } from '~/locale'; import { __ } from '~/locale';
import eventHub from '../event_hub';
import { STATUSES } from '../constants';
import ImportStatus from './import_status.vue'; import ImportStatus from './import_status.vue';
export default { export default {
...@@ -19,19 +17,19 @@ export default { ...@@ -19,19 +17,19 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
}, availableNamespaces: {
type: Array,
data() { required: true,
return { },
targetNamespace: this.$store.state.defaultTargetNamespace,
newName: this.repo.sanitizedName,
};
}, },
computed: { computed: {
...mapState(['namespaces', 'reposBeingImported', 'ciCdOnly']), ...mapState(['ciCdOnly']),
...mapGetters(['getImportTarget']),
...mapGetters(['namespaceSelectOptions']), importTarget() {
return this.getImportTarget(this.repo.importSource.id);
},
importButtonText() { importButtonText() {
return this.ciCdOnly ? __('Connect') : __('Import'); return this.ciCdOnly ? __('Connect') : __('Import');
...@@ -39,37 +37,36 @@ export default { ...@@ -39,37 +37,36 @@ export default {
select2Options() { select2Options() {
return { return {
data: this.namespaceSelectOptions, data: this.availableNamespaces,
containerCssClass: containerCssClass: 'import-namespace-select qa-project-namespace-select w-auto',
'import-namespace-select js-namespace-select qa-project-namespace-select w-auto',
}; };
}, },
isLoadingImport() { targetNamespaceSelect: {
return this.reposBeingImported.includes(this.repo.id); get() {
return this.importTarget.targetNamespace;
},
set(value) {
this.updateImportTarget({ targetNamespace: value });
},
}, },
status() { newNameInput: {
return this.isLoadingImport ? STATUSES.SCHEDULING : STATUSES.NONE; get() {
return this.importTarget.newName;
},
set(value) {
this.updateImportTarget({ newName: value });
},
}, },
}, },
created() {
eventHub.$on('importAll', this.importRepo);
},
beforeDestroy() {
eventHub.$off('importAll', this.importRepo);
},
methods: { methods: {
...mapActions(['fetchImport']), ...mapActions(['fetchImport', 'setImportTarget']),
updateImportTarget(changedValues) {
importRepo() { this.setImportTarget({
return this.fetchImport({ repoId: this.repo.importSource.id,
newName: this.newName, importTarget: { ...this.importTarget, ...changedValues },
targetNamespace: this.targetNamespace,
repo: this.repo,
}); });
}, },
}, },
...@@ -77,36 +74,36 @@ export default { ...@@ -77,36 +74,36 @@ export default {
</script> </script>
<template> <template>
<tr class="qa-project-import-row js-provider-repo import-row"> <tr class="qa-project-import-row import-row">
<td> <td>
<a <a
:href="repo.providerLink" :href="repo.importSource.providerLink"
rel="noreferrer noopener" rel="noreferrer noopener"
target="_blank" target="_blank"
class="js-provider-link" data-testid="providerLink"
> >{{ repo.importSource.fullName }}
{{ repo.fullName }} <gl-icon v-if="repo.importSource.providerLink" name="external-link" />
<gl-icon v-if="repo.providerLink" name="external-link" />
</a> </a>
</td> </td>
<td class="d-flex flex-wrap flex-lg-nowrap"> <td class="d-flex flex-wrap flex-lg-nowrap">
<select2-select v-model="targetNamespace" :options="select2Options" /> <select2-select v-model="targetNamespaceSelect" :options="select2Options" />
<span class="px-2 import-slash-divider d-flex justify-content-center align-items-center" <span class="px-2 import-slash-divider d-flex justify-content-center align-items-center"
>/</span >/</span
> >
<input <input
v-model="newName" v-model="newNameInput"
type="text" type="text"
class="form-control import-project-name-input js-new-name qa-project-path-field" class="form-control import-project-name-input qa-project-path-field"
/> />
</td> </td>
<td><import-status :status="status" /></td> <td>
<import-status :status="repo.importStatus" />
</td>
<td> <td>
<button <button
v-if="!isLoadingImport"
type="button" type="button"
class="qa-import-button js-import-button btn btn-default" class="qa-import-button btn btn-default"
@click="importRepo" @click="fetchImport(repo.importSource.id)"
> >
{{ importButtonText }} {{ importButtonText }}
</button> </button>
......
import createEventHub from '~/helpers/event_hub_factory';
export default createEventHub();
...@@ -8,22 +8,29 @@ Vue.use(Translate); ...@@ -8,22 +8,29 @@ Vue.use(Translate);
export function initStoreFromElement(element) { export function initStoreFromElement(element) {
const { const {
reposPath, ciCdOnly,
provider,
canSelectNamespace, canSelectNamespace,
provider,
reposPath,
jobsPath, jobsPath,
importPath, importPath,
ciCdOnly, namespacesPath,
} = element.dataset; } = element.dataset;
return createStore({ return createStore({
reposPath, initialState: {
provider, defaultTargetNamespace: gon.current_username,
jobsPath, ciCdOnly: parseBoolean(ciCdOnly),
importPath, canSelectNamespace: parseBoolean(canSelectNamespace),
defaultTargetNamespace: gon.current_username, provider,
ciCdOnly: parseBoolean(ciCdOnly), },
canSelectNamespace: parseBoolean(canSelectNamespace), endpoints: {
reposPath,
jobsPath,
importPath,
namespacesPath,
},
}); });
} }
......
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { isProjectImportable } from '../utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import Poll from '~/lib/utils/poll'; import Poll from '~/lib/utils/poll';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { jobsPathWithFilter, reposPathWithFilter } from './getters';
let eTagPoll; let eTagPoll;
const hasRedirectInError = e => e?.response?.data?.error?.redirect; const hasRedirectInError = e => e?.response?.data?.error?.redirect;
const redirectToUrlInError = e => visitUrl(e.response.data.error.redirect); const redirectToUrlInError = e => visitUrl(e.response.data.error.redirect);
const pathWithFilter = ({ path, filter = '' }) => (filter ? `${path}?filter=${filter}` : path);
export const clearJobsEtagPoll = () => { const isRequired = () => {
// eslint-disable-next-line @gitlab/require-i18n-strings
throw new Error('param is required');
};
const clearJobsEtagPoll = () => {
eTagPoll = null; eTagPoll = null;
}; };
export const stopJobsPolling = () => {
const stopJobsPolling = () => {
if (eTagPoll) eTagPoll.stop(); if (eTagPoll) eTagPoll.stop();
}; };
export const restartJobsPolling = () => {
const restartJobsPolling = () => {
if (eTagPoll) eTagPoll.restart(); if (eTagPoll) eTagPoll.restart();
}; };
export const setFilter = ({ commit }, filter) => commit(types.SET_FILTER, filter); const setFilter = ({ commit }, filter) => commit(types.SET_FILTER, filter);
const setImportTarget = ({ commit }, { repoId, importTarget }) =>
commit(types.SET_IMPORT_TARGET, { repoId, importTarget });
export const fetchRepos = ({ state, dispatch, commit }) => { const importAll = ({ state, dispatch }) => {
return Promise.all(
state.repositories
.filter(isProjectImportable)
.map(r => dispatch('fetchImport', r.importSource.id)),
);
};
const fetchReposFactory = (reposPath = isRequired()) => ({ state, dispatch, commit }) => {
dispatch('stopJobsPolling'); dispatch('stopJobsPolling');
commit(types.REQUEST_REPOS); commit(types.REQUEST_REPOS);
const { provider } = state; const { provider, filter } = state;
return axios return axios
.get(reposPathWithFilter(state)) .get(pathWithFilter({ path: reposPath, filter }))
.then(({ data }) => .then(({ data }) =>
commit(types.RECEIVE_REPOS_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true })), commit(types.RECEIVE_REPOS_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true })),
) )
...@@ -52,22 +71,24 @@ export const fetchRepos = ({ state, dispatch, commit }) => { ...@@ -52,22 +71,24 @@ export const fetchRepos = ({ state, dispatch, commit }) => {
}); });
}; };
export const fetchImport = ({ state, commit }, { newName, targetNamespace, repo }) => { const fetchImportFactory = (importPath = isRequired()) => ({ state, commit, getters }, repoId) => {
if (!state.reposBeingImported.includes(repo.id)) { const { ciCdOnly } = state;
commit(types.REQUEST_IMPORT, repo.id); const importTarget = getters.getImportTarget(repoId);
}
commit(types.REQUEST_IMPORT, { repoId, importTarget });
const { newName, targetNamespace } = importTarget;
return axios return axios
.post(state.importPath, { .post(importPath, {
ci_cd_only: state.ciCdOnly, repo_id: repoId,
ci_cd_only: ciCdOnly,
new_name: newName, new_name: newName,
repo_id: repo.id,
target_namespace: targetNamespace, target_namespace: targetNamespace,
}) })
.then(({ data }) => .then(({ data }) =>
commit(types.RECEIVE_IMPORT_SUCCESS, { commit(types.RECEIVE_IMPORT_SUCCESS, {
importedProject: convertObjectPropsToCamelCase(data, { deep: true }), importedProject: convertObjectPropsToCamelCase(data, { deep: true }),
repoId: repo.id, repoId,
}), }),
) )
.catch(e => { .catch(e => {
...@@ -84,14 +105,11 @@ export const fetchImport = ({ state, commit }, { newName, targetNamespace, repo ...@@ -84,14 +105,11 @@ export const fetchImport = ({ state, commit }, { newName, targetNamespace, repo
createFlash(flashMessage); createFlash(flashMessage);
commit(types.RECEIVE_IMPORT_ERROR, repo.id); commit(types.RECEIVE_IMPORT_ERROR, repoId);
}); });
}; };
export const receiveJobsSuccess = ({ commit }, updatedProjects) => export const fetchJobsFactory = (jobsPath = isRequired()) => ({ state, commit, dispatch }) => {
commit(types.RECEIVE_JOBS_SUCCESS, updatedProjects);
export const fetchJobs = ({ state, commit, dispatch }) => {
const { filter } = state; const { filter } = state;
if (eTagPoll) { if (eTagPoll) {
...@@ -101,7 +119,7 @@ export const fetchJobs = ({ state, commit, dispatch }) => { ...@@ -101,7 +119,7 @@ export const fetchJobs = ({ state, commit, dispatch }) => {
eTagPoll = new Poll({ eTagPoll = new Poll({
resource: { resource: {
fetchJobs: () => axios.get(jobsPathWithFilter(state)), fetchJobs: () => axios.get(pathWithFilter({ path: jobsPath, filter })),
}, },
method: 'fetchJobs', method: 'fetchJobs',
successCallback: ({ data }) => successCallback: ({ data }) =>
...@@ -128,3 +146,30 @@ export const fetchJobs = ({ state, commit, dispatch }) => { ...@@ -128,3 +146,30 @@ export const fetchJobs = ({ state, commit, dispatch }) => {
} }
}); });
}; };
const fetchNamespacesFactory = (namespacesPath = isRequired()) => ({ commit }) => {
commit(types.REQUEST_NAMESPACES);
axios
.get(namespacesPath)
.then(({ data }) =>
commit(types.RECEIVE_NAMESPACES_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true })),
)
.catch(() => {
createFlash(s__('ImportProjects|Requesting namespaces failed'));
commit(types.RECEIVE_NAMESPACES_ERROR);
});
};
export default ({ endpoints = isRequired() }) => ({
clearJobsEtagPoll,
stopJobsPolling,
restartJobsPolling,
setFilter,
setImportTarget,
importAll,
fetchRepos: fetchReposFactory(endpoints.reposPath),
fetchImport: fetchImportFactory(endpoints.importPath),
fetchJobs: fetchJobsFactory(endpoints.jobsPath),
fetchNamespaces: fetchNamespacesFactory(endpoints.namespacesPath),
});
import { __ } from '~/locale'; import { STATUSES } from '../constants';
export const namespaceSelectOptions = state => { export const isLoading = state => state.isLoadingRepos || state.isLoadingNamespaces;
const serializedNamespaces = state.namespaces.map(({ fullPath }) => ({
id: fullPath,
text: fullPath,
}));
return [ export const isImportingAnyRepo = state =>
{ text: __('Groups'), children: serializedNamespaces }, state.repositories.some(repo =>
{ [STATUSES.SCHEDULING, STATUSES.SCHEDULED, STATUSES.STARTED].includes(repo.importStatus),
text: __('Users'), );
children: [{ id: state.defaultTargetNamespace, text: state.defaultTargetNamespace }],
},
];
};
export const isImportingAnyRepo = state => state.reposBeingImported.length > 0; export const hasIncompatibleRepos = state =>
state.repositories.some(repo => repo.importSource.incompatible);
export const hasProviderRepos = state => state.providerRepos.length > 0; export const hasImportableRepos = state =>
state.repositories.some(repo => repo.importStatus === STATUSES.NONE);
export const hasImportedProjects = state => state.importedProjects.length > 0; export const getImportTarget = state => repoId => {
if (state.customImportTargets[repoId]) {
return state.customImportTargets[repoId];
}
export const hasIncompatibleRepos = state => state.incompatibleRepos.length > 0; const repo = state.repositories.find(r => r.importSource.id === repoId);
export const reposPathWithFilter = ({ reposPath, filter = '' }) => return {
filter ? `${reposPath}?filter=${filter}` : reposPath; newName: repo.importSource.sanitizedName,
export const jobsPathWithFilter = ({ jobsPath, filter = '' }) => targetNamespace: state.defaultTargetNamespace,
filter ? `${jobsPath}?filter=${filter}` : jobsPath; };
};
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import state from './state'; import state from './state';
import * as actions from './actions'; import actionsFactory from './actions';
import * as getters from './getters'; import * as getters from './getters';
import mutations from './mutations'; import mutations from './mutations';
Vue.use(Vuex); Vue.use(Vuex);
export { state, actions, getters, mutations }; export default ({ initialState, endpoints }) =>
export default initialState =>
new Vuex.Store({ new Vuex.Store({
state: { ...state(), ...initialState }, state: { ...state(), ...initialState },
actions, actions: actionsFactory({ endpoints }),
mutations, mutations,
getters, getters,
}); });
...@@ -2,6 +2,10 @@ export const REQUEST_REPOS = 'REQUEST_REPOS'; ...@@ -2,6 +2,10 @@ export const REQUEST_REPOS = 'REQUEST_REPOS';
export const RECEIVE_REPOS_SUCCESS = 'RECEIVE_REPOS_SUCCESS'; export const RECEIVE_REPOS_SUCCESS = 'RECEIVE_REPOS_SUCCESS';
export const RECEIVE_REPOS_ERROR = 'RECEIVE_REPOS_ERROR'; export const RECEIVE_REPOS_ERROR = 'RECEIVE_REPOS_ERROR';
export const REQUEST_NAMESPACES = 'REQUEST_NAMESPACES';
export const RECEIVE_NAMESPACES_SUCCESS = 'RECEIVE_NAMESPACES_SUCCESS';
export const RECEIVE_NAMESPACES_ERROR = 'RECEIVE_NAMESPACES_ERROR';
export const REQUEST_IMPORT = 'REQUEST_IMPORT'; export const REQUEST_IMPORT = 'REQUEST_IMPORT';
export const RECEIVE_IMPORT_SUCCESS = 'RECEIVE_IMPORT_SUCCESS'; export const RECEIVE_IMPORT_SUCCESS = 'RECEIVE_IMPORT_SUCCESS';
export const RECEIVE_IMPORT_ERROR = 'RECEIVE_IMPORT_ERROR'; export const RECEIVE_IMPORT_ERROR = 'RECEIVE_IMPORT_ERROR';
...@@ -9,3 +13,5 @@ export const RECEIVE_IMPORT_ERROR = 'RECEIVE_IMPORT_ERROR'; ...@@ -9,3 +13,5 @@ export const RECEIVE_IMPORT_ERROR = 'RECEIVE_IMPORT_ERROR';
export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS'; export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS';
export const SET_FILTER = 'SET_FILTER'; export const SET_FILTER = 'SET_FILTER';
export const SET_IMPORT_TARGET = 'SET_IMPORT_TARGET';
import Vue from 'vue'; import Vue from 'vue';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { STATUSES } from '../constants';
export default { export default {
[types.SET_FILTER](state, filter) { [types.SET_FILTER](state, filter) {
...@@ -12,48 +13,95 @@ export default { ...@@ -12,48 +13,95 @@ export default {
[types.RECEIVE_REPOS_SUCCESS]( [types.RECEIVE_REPOS_SUCCESS](
state, state,
{ importedProjects, providerRepos, incompatibleRepos, namespaces }, { importedProjects, providerRepos, incompatibleRepos = [] },
) { ) {
// Normalizing structure to support legacy backend format
// See https://gitlab.com/gitlab-org/gitlab/-/issues/27370#note_379034091 for details
state.isLoadingRepos = false; state.isLoadingRepos = false;
state.importedProjects = importedProjects; state.repositories = [
state.providerRepos = providerRepos; ...importedProjects.map(({ importSource, providerLink, importStatus, ...project }) => ({
state.incompatibleRepos = incompatibleRepos ?? []; importSource: {
state.namespaces = namespaces; id: `finished-${project.id}`,
fullName: importSource,
sanitizedName: project.name,
providerLink,
},
importStatus,
importedProject: project,
})),
...providerRepos.map(project => ({
importSource: project,
importStatus: STATUSES.NONE,
importedProject: null,
})),
...incompatibleRepos.map(project => ({
importSource: { ...project, incompatible: true },
importStatus: STATUSES.NONE,
importedProject: null,
})),
];
}, },
[types.RECEIVE_REPOS_ERROR](state) { [types.RECEIVE_REPOS_ERROR](state) {
state.isLoadingRepos = false; state.isLoadingRepos = false;
}, },
[types.REQUEST_IMPORT](state, repoId) { [types.REQUEST_IMPORT](state, { repoId, importTarget }) {
state.reposBeingImported.push(repoId); const existingRepo = state.repositories.find(r => r.importSource.id === repoId);
existingRepo.importStatus = STATUSES.SCHEDULING;
existingRepo.importedProject = {
fullPath: `/${importTarget.targetNamespace}/${importTarget.newName}`,
};
}, },
[types.RECEIVE_IMPORT_SUCCESS](state, { importedProject, repoId }) { [types.RECEIVE_IMPORT_SUCCESS](state, { importedProject, repoId }) {
const existingRepoIndex = state.reposBeingImported.indexOf(repoId); const { importStatus, ...project } = importedProject;
if (state.reposBeingImported.includes(repoId))
state.reposBeingImported.splice(existingRepoIndex, 1);
const providerRepoIndex = state.providerRepos.findIndex( const existingRepo = state.repositories.find(r => r.importSource.id === repoId);
providerRepo => providerRepo.id === repoId, existingRepo.importStatus = importStatus;
); existingRepo.importedProject = project;
state.providerRepos.splice(providerRepoIndex, 1);
state.importedProjects.unshift(importedProject);
}, },
[types.RECEIVE_IMPORT_ERROR](state, repoId) { [types.RECEIVE_IMPORT_ERROR](state, repoId) {
const repoIndex = state.reposBeingImported.indexOf(repoId); const existingRepo = state.repositories.find(r => r.importSource.id === repoId);
if (state.reposBeingImported.includes(repoId)) state.reposBeingImported.splice(repoIndex, 1); existingRepo.importStatus = STATUSES.NONE;
existingRepo.importedProject = null;
}, },
[types.RECEIVE_JOBS_SUCCESS](state, updatedProjects) { [types.RECEIVE_JOBS_SUCCESS](state, updatedProjects) {
updatedProjects.forEach(updatedProject => { updatedProjects.forEach(updatedProject => {
const existingProject = state.importedProjects.find( const repo = state.repositories.find(p => p.importedProject?.id === updatedProject.id);
importedProject => importedProject.id === updatedProject.id, if (repo) {
); repo.importStatus = updatedProject.importStatus;
}
Vue.set(existingProject, 'importStatus', updatedProject.importStatus);
}); });
}, },
[types.REQUEST_NAMESPACES](state) {
state.isLoadingNamespaces = true;
},
[types.RECEIVE_NAMESPACES_SUCCESS](state, namespaces) {
state.isLoadingNamespaces = false;
state.namespaces = namespaces;
},
[types.RECEIVE_NAMESPACES_ERROR](state) {
state.isLoadingNamespaces = false;
},
[types.SET_IMPORT_TARGET](state, { repoId, importTarget }) {
const existingRepo = state.repositories.find(r => r.importSource.id === repoId);
if (
importTarget.targetNamespace === state.defaultTargetNamespace &&
importTarget.newName === existingRepo.importSource.sanitizedName
) {
Vue.delete(state.customImportTargets, repoId);
} else {
Vue.set(state.customImportTargets, repoId, importTarget);
}
},
}; };
export default () => ({ export default () => ({
reposPath: '',
importPath: '',
jobsPath: '',
currentProjectId: '',
provider: '', provider: '',
currentUsername: '', repositories: [],
importedProjects: [],
providerRepos: [],
incompatibleRepos: [],
namespaces: [], namespaces: [],
reposBeingImported: [], customImportTargets: {},
isLoadingRepos: false, isLoadingRepos: false,
canSelectNamespace: false, isLoadingNamespaces: false,
ciCdOnly: false, ciCdOnly: false,
filter: '', filter: '',
}); });
import { STATUSES } from './constants';
// Will be expanded in future
// eslint-disable-next-line import/prefer-default-export
export function isProjectImportable(project) {
return project.importStatus === STATUSES.NONE && !project.importSource.incompatible;
}
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
#import-projects-mount-element{ data: { provider: provider, provider_title: provider_title, #import-projects-mount-element{ data: { provider: provider, provider_title: provider_title,
can_select_namespace: current_user.can_select_namespace?.to_s, can_select_namespace: current_user.can_select_namespace?.to_s,
ci_cd_only: has_ci_cd_only_params?.to_s, ci_cd_only: has_ci_cd_only_params?.to_s,
namespaces_path: import_available_namespaces_path,
repos_path: url_for([:status, :import, provider, format: :json]), repos_path: url_for([:status, :import, provider, format: :json]),
jobs_path: url_for([:realtime_changes, :import, provider, format: :json]), jobs_path: url_for([:realtime_changes, :import, provider, format: :json]),
import_path: url_for([:import, provider, format: :json]), import_path: url_for([:import, provider, format: :json]),
......
...@@ -12851,6 +12851,9 @@ msgstr "" ...@@ -12851,6 +12851,9 @@ msgstr ""
msgid "ImportProjects|Importing the project failed: %{reason}" msgid "ImportProjects|Importing the project failed: %{reason}"
msgstr "" msgstr ""
msgid "ImportProjects|Requesting namespaces failed"
msgstr ""
msgid "ImportProjects|Requesting your %{provider} repositories failed" msgid "ImportProjects|Requesting your %{provider} repositories failed"
msgstr "" msgstr ""
......
...@@ -2,17 +2,14 @@ import { nextTick } from 'vue'; ...@@ -2,17 +2,14 @@ import { nextTick } from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlLoadingIcon, GlButton } from '@gitlab/ui'; import { GlLoadingIcon, GlButton } from '@gitlab/ui';
import { state, getters } from '~/import_projects/store'; import state from '~/import_projects/store/state';
import eventHub from '~/import_projects/event_hub'; import * as getters from '~/import_projects/store/getters';
import { STATUSES } from '~/import_projects/constants';
import ImportProjectsTable from '~/import_projects/components/import_projects_table.vue'; import ImportProjectsTable from '~/import_projects/components/import_projects_table.vue';
import ImportedProjectTableRow from '~/import_projects/components/imported_project_table_row.vue'; import ImportedProjectTableRow from '~/import_projects/components/imported_project_table_row.vue';
import ProviderRepoTableRow from '~/import_projects/components/provider_repo_table_row.vue'; import ProviderRepoTableRow from '~/import_projects/components/provider_repo_table_row.vue';
import IncompatibleRepoTableRow from '~/import_projects/components/incompatible_repo_table_row.vue'; import IncompatibleRepoTableRow from '~/import_projects/components/incompatible_repo_table_row.vue';
jest.mock('~/import_projects/event_hub', () => ({
$emit: jest.fn(),
}));
describe('ImportProjectsTable', () => { describe('ImportProjectsTable', () => {
let wrapper; let wrapper;
...@@ -21,13 +18,6 @@ describe('ImportProjectsTable', () => { ...@@ -21,13 +18,6 @@ describe('ImportProjectsTable', () => {
const providerTitle = 'THE PROVIDER'; const providerTitle = 'THE PROVIDER';
const providerRepo = { id: 10, sanitizedName: 'sanitizedName', fullName: 'fullName' }; const providerRepo = { id: 10, sanitizedName: 'sanitizedName', fullName: 'fullName' };
const importedProject = {
id: 1,
fullPath: 'fullPath',
importStatus: 'started',
providerLink: 'providerLink',
importSource: 'importSource',
};
const findImportAllButton = () => const findImportAllButton = () =>
wrapper wrapper
...@@ -35,6 +25,7 @@ describe('ImportProjectsTable', () => { ...@@ -35,6 +25,7 @@ describe('ImportProjectsTable', () => {
.filter(w => w.props().variant === 'success') .filter(w => w.props().variant === 'success')
.at(0); .at(0);
const importAllFn = jest.fn();
function createComponent({ function createComponent({
state: initialState, state: initialState,
getters: customGetters, getters: customGetters,
...@@ -52,8 +43,9 @@ describe('ImportProjectsTable', () => { ...@@ -52,8 +43,9 @@ describe('ImportProjectsTable', () => {
}, },
actions: { actions: {
fetchRepos: jest.fn(), fetchRepos: jest.fn(),
fetchReposFiltered: jest.fn(),
fetchJobs: jest.fn(), fetchJobs: jest.fn(),
fetchNamespaces: jest.fn(),
importAll: importAllFn,
stopJobsPolling: jest.fn(), stopJobsPolling: jest.fn(),
clearJobsEtagPoll: jest.fn(), clearJobsEtagPoll: jest.fn(),
setFilter: jest.fn(), setFilter: jest.fn(),
...@@ -79,11 +71,13 @@ describe('ImportProjectsTable', () => { ...@@ -79,11 +71,13 @@ describe('ImportProjectsTable', () => {
}); });
it('renders a loading icon while repos are loading', () => { it('renders a loading icon while repos are loading', () => {
createComponent({ createComponent({ state: { isLoadingRepos: true } });
state: {
isLoadingRepos: true, expect(wrapper.contains(GlLoadingIcon)).toBe(true);
}, });
});
it('renders a loading icon while namespaces are loading', () => {
createComponent({ state: { isLoadingNamespaces: true } });
expect(wrapper.contains(GlLoadingIcon)).toBe(true); expect(wrapper.contains(GlLoadingIcon)).toBe(true);
}); });
...@@ -91,10 +85,16 @@ describe('ImportProjectsTable', () => { ...@@ -91,10 +85,16 @@ describe('ImportProjectsTable', () => {
it('renders a table with imported projects and provider repos', () => { it('renders a table with imported projects and provider repos', () => {
createComponent({ createComponent({
state: { state: {
importedProjects: [importedProject], namespaces: [{ fullPath: 'path' }],
providerRepos: [providerRepo], repositories: [
incompatibleRepos: [{ ...providerRepo, id: 11 }], { importSource: { id: 1 }, importedProject: null, importStatus: STATUSES.NONE },
namespaces: [{ path: 'path' }], { importSource: { id: 2 }, importedProject: {}, importStatus: STATUSES.FINISHED },
{
importSource: { id: 3, incompatible: true },
importedProject: {},
importStatus: STATUSES.NONE,
},
],
}, },
}); });
...@@ -133,13 +133,7 @@ describe('ImportProjectsTable', () => { ...@@ -133,13 +133,7 @@ describe('ImportProjectsTable', () => {
); );
it('renders an empty state if there are no projects available', () => { it('renders an empty state if there are no projects available', () => {
createComponent({ createComponent({ state: { repositories: [] } });
state: {
importedProjects: [],
providerRepos: [],
incompatibleProjects: [],
},
});
expect(wrapper.contains(ProviderRepoTableRow)).toBe(false); expect(wrapper.contains(ProviderRepoTableRow)).toBe(false);
expect(wrapper.contains(ImportedProjectTableRow)).toBe(false); expect(wrapper.contains(ImportedProjectTableRow)).toBe(false);
...@@ -147,34 +141,29 @@ describe('ImportProjectsTable', () => { ...@@ -147,34 +141,29 @@ describe('ImportProjectsTable', () => {
}); });
it('sends importAll event when import button is clicked', async () => { it('sends importAll event when import button is clicked', async () => {
createComponent({ createComponent({ state: { providerRepos: [providerRepo] } });
state: {
providerRepos: [providerRepo],
},
});
findImportAllButton().vm.$emit('click'); findImportAllButton().vm.$emit('click');
await nextTick(); await nextTick();
expect(eventHub.$emit).toHaveBeenCalledWith('importAll');
expect(importAllFn).toHaveBeenCalled();
}); });
it('shows loading spinner when import is in progress', () => { it('shows loading spinner when import is in progress', () => {
createComponent({ createComponent({ getters: { isImportingAnyRepo: () => true } });
getters: {
isImportingAnyRepo: () => true,
},
});
expect(findImportAllButton().props().loading).toBe(true); expect(findImportAllButton().props().loading).toBe(true);
}); });
it('renders filtering input field by default', () => { it('renders filtering input field by default', () => {
createComponent(); createComponent();
expect(findFilterField().exists()).toBe(true); expect(findFilterField().exists()).toBe(true);
}); });
it('does not render filtering input field when filterable is false', () => { it('does not render filtering input field when filterable is false', () => {
createComponent({ filterable: false }); createComponent({ filterable: false });
expect(findFilterField().exists()).toBe(false); expect(findFilterField().exists()).toBe(false);
}); });
......
import Vuex from 'vuex'; import { mount } from '@vue/test-utils';
import { createLocalVue, mount } from '@vue/test-utils'; import ImportedProjectTableRow from '~/import_projects/components/imported_project_table_row.vue';
import createStore from '~/import_projects/store'; import ImportStatus from '~/import_projects/components/import_status.vue';
import importedProjectTableRow from '~/import_projects/components/imported_project_table_row.vue'; import { STATUSES } from '~/import_projects/constants';
import STATUS_MAP from '~/import_projects/constants';
describe('ImportedProjectTableRow', () => { describe('ImportedProjectTableRow', () => {
let vm; let wrapper;
const project = { const project = {
id: 1, importSource: {
fullPath: 'fullPath', fullName: 'fullName',
importStatus: 'finished', providerLink: 'providerLink',
providerLink: 'providerLink', },
importSource: 'importSource', importedProject: {
id: 1,
fullPath: 'fullPath',
importSource: 'importSource',
},
importStatus: STATUSES.FINISHED,
}; };
function mountComponent() { function mountComponent() {
const localVue = createLocalVue(); wrapper = mount(ImportedProjectTableRow, { propsData: { project } });
localVue.use(Vuex);
const component = mount(importedProjectTableRow, {
localVue,
store: createStore(),
propsData: {
project: {
...project,
},
},
});
return component.vm;
} }
beforeEach(() => { beforeEach(() => {
vm = mountComponent(); mountComponent();
}); });
afterEach(() => { afterEach(() => {
vm.$destroy(); wrapper.destroy();
}); });
it('renders an imported project table row', () => { it('renders an imported project table row', () => {
const providerLink = vm.$el.querySelector('.js-provider-link'); const providerLink = wrapper.find('[data-testid=providerLink]');
const statusObject = STATUS_MAP[project.importStatus];
expect(providerLink.attributes().href).toMatch(project.importSource.providerLink);
expect(vm.$el.classList.contains('js-imported-project')).toBe(true); expect(providerLink.text()).toMatch(project.importSource.fullName);
expect(providerLink.href).toMatch(project.providerLink); expect(wrapper.find('[data-testid=fullPath]').text()).toMatch(project.importedProject.fullPath);
expect(providerLink.textContent).toMatch(project.importSource); expect(wrapper.find(ImportStatus).props().status).toBe(project.importStatus);
expect(vm.$el.querySelector('.js-full-path').textContent).toMatch(project.fullPath); expect(wrapper.find('[data-testid=goToProject').attributes().href).toMatch(
expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch( project.importedProject.fullPath,
statusObject.text,
); );
expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
expect(vm.$el.querySelector('.js-go-to-project').href).toMatch(project.fullPath);
}); });
}); });
import { nextTick } from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { createLocalVue, mount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import { state, actions, getters, mutations } from '~/import_projects/store'; import ProviderRepoTableRow from '~/import_projects/components/provider_repo_table_row.vue';
import providerRepoTableRow from '~/import_projects/components/provider_repo_table_row.vue'; import ImportStatus from '~/import_projects/components/import_status.vue';
import STATUS_MAP, { STATUSES } from '~/import_projects/constants'; import { STATUSES } from '~/import_projects/constants';
import Select2Select from '~/vue_shared/components/select2_select.vue';
describe('ProviderRepoTableRow', () => { describe('ProviderRepoTableRow', () => {
let vm; let wrapper;
const fetchImport = jest.fn(); const fetchImport = jest.fn();
const importPath = '/import-path'; const setImportTarget = jest.fn();
const defaultTargetNamespace = 'user'; const fakeImportTarget = {
const ciCdOnly = true; targetNamespace: 'target',
newName: 'newName',
};
const ciCdOnly = false;
const repo = { const repo = {
id: 10, importSource: {
sanitizedName: 'sanitizedName', id: 'remote-1',
fullName: 'fullName', fullName: 'fullName',
providerLink: 'providerLink', providerLink: 'providerLink',
},
importedProject: {
id: 1,
fullPath: 'fullPath',
importSource: 'importSource',
},
importStatus: STATUSES.FINISHED,
}; };
function initStore(initialState) { const availableNamespaces = [
const stubbedActions = { ...actions, fetchImport }; { text: 'Groups', children: [{ id: 'test', text: 'test' }] },
{ text: 'Users', children: [{ id: 'root', text: 'root' }] },
];
function initStore(initialState) {
const store = new Vuex.Store({ const store = new Vuex.Store({
state: { ...state(), ...initialState }, state: initialState,
actions: stubbedActions, getters: {
mutations, getImportTarget: () => () => fakeImportTarget,
getters, },
actions: { fetchImport, setImportTarget },
}); });
return store; return store;
} }
const findImportButton = () =>
wrapper
.findAll('button')
.filter(node => node.text() === 'Import')
.at(0);
function mountComponent(initialState) { function mountComponent(initialState) {
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
const store = initStore({ importPath, defaultTargetNamespace, ciCdOnly, ...initialState }); const store = initStore({ ciCdOnly, ...initialState });
const component = mount(providerRepoTableRow, { wrapper = shallowMount(ProviderRepoTableRow, {
localVue, localVue,
store, store,
propsData: { propsData: { repo, availableNamespaces },
repo,
},
}); });
return component.vm;
} }
beforeEach(() => { beforeEach(() => {
vm = mountComponent(); mountComponent();
}); });
afterEach(() => { afterEach(() => {
vm.$destroy(); wrapper.destroy();
}); });
it('renders a provider repo table row', () => { it('renders a provider repo table row', () => {
const providerLink = vm.$el.querySelector('.js-provider-link'); const providerLink = wrapper.find('[data-testid=providerLink]');
const statusObject = STATUS_MAP[STATUSES.NONE];
expect(providerLink.attributes().href).toMatch(repo.importSource.providerLink);
expect(vm.$el.classList.contains('js-provider-repo')).toBe(true); expect(providerLink.text()).toMatch(repo.importSource.fullName);
expect(providerLink.href).toMatch(repo.providerLink); expect(wrapper.find(ImportStatus).props().status).toBe(repo.importStatus);
expect(providerLink.textContent).toMatch(repo.fullName); expect(wrapper.contains('button')).toBe(true);
expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
statusObject.text,
);
expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
expect(vm.$el.querySelector('.js-import-button')).not.toBeNull();
}); });
it('renders a select2 namespace select', () => { it('renders a select2 namespace select', () => {
const dropdownTrigger = vm.$el.querySelector('.js-namespace-select'); expect(wrapper.contains(Select2Select)).toBe(true);
expect(wrapper.find(Select2Select).props().options.data).toBe(availableNamespaces);
expect(dropdownTrigger).not.toBeNull();
expect(dropdownTrigger.classList.contains('select2-container')).toBe(true);
dropdownTrigger.click();
expect(vm.$el.querySelector('.select2-drop')).not.toBeNull();
}); });
it('imports repo when clicking import button', () => { it('imports repo when clicking import button', async () => {
vm.$el.querySelector('.js-import-button').click(); findImportButton().trigger('click');
return vm.$nextTick().then(() => { await nextTick();
const { calls } = fetchImport.mock;
// Not using .toBeCalledWith because it expects const { calls } = fetchImport.mock;
// an unmatchable and undefined 3rd argument.
expect(calls.length).toBe(1); expect(calls).toHaveLength(1);
expect(calls[0][1]).toEqual({ expect(calls[0][1]).toBe(repo.importSource.id);
repo,
newName: repo.sanitizedName,
targetNamespace: defaultTargetNamespace,
});
});
}); });
}); });
...@@ -12,41 +12,76 @@ import { ...@@ -12,41 +12,76 @@ import {
RECEIVE_IMPORT_SUCCESS, RECEIVE_IMPORT_SUCCESS,
RECEIVE_IMPORT_ERROR, RECEIVE_IMPORT_ERROR,
RECEIVE_JOBS_SUCCESS, RECEIVE_JOBS_SUCCESS,
REQUEST_NAMESPACES,
RECEIVE_NAMESPACES_SUCCESS,
RECEIVE_NAMESPACES_ERROR,
} from '~/import_projects/store/mutation_types'; } from '~/import_projects/store/mutation_types';
import { import actionsFactory from '~/import_projects/store/actions';
fetchRepos, import { getImportTarget } from '~/import_projects/store/getters';
fetchImport,
receiveJobsSuccess,
fetchJobs,
clearJobsEtagPoll,
stopJobsPolling,
} from '~/import_projects/store/actions';
import state from '~/import_projects/store/state'; import state from '~/import_projects/store/state';
import { STATUSES } from '~/import_projects/constants';
jest.mock('~/flash'); jest.mock('~/flash');
const MOCK_ENDPOINT = `${TEST_HOST}/endpoint.json`;
const {
clearJobsEtagPoll,
stopJobsPolling,
importAll,
fetchRepos,
fetchImport,
fetchJobs,
fetchNamespaces,
} = actionsFactory({
endpoints: {
reposPath: MOCK_ENDPOINT,
importPath: MOCK_ENDPOINT,
jobsPath: MOCK_ENDPOINT,
namespacesPath: MOCK_ENDPOINT,
},
});
describe('import_projects store actions', () => { describe('import_projects store actions', () => {
let localState; let localState;
const repos = [{ id: 1 }, { id: 2 }]; const importRepoId = 1;
const importPayload = { newName: 'newName', targetNamespace: 'targetNamespace', repo: { id: 1 } }; const otherImportRepoId = 2;
const defaultTargetNamespace = 'default';
const sanitizedName = 'sanitizedName';
const defaultImportTarget = { newName: sanitizedName, targetNamespace: defaultTargetNamespace };
beforeEach(() => { beforeEach(() => {
localState = state(); localState = {
...state(),
defaultTargetNamespace,
repositories: [
{ importSource: { id: importRepoId, sanitizedName }, importStatus: STATUSES.NONE },
{
importSource: { id: otherImportRepoId, sanitizedName: 's2' },
importStatus: STATUSES.NONE,
},
{
importSource: { id: 3, sanitizedName: 's3', incompatible: true },
importStatus: STATUSES.NONE,
},
],
};
localState.getImportTarget = getImportTarget(localState);
}); });
describe('fetchRepos', () => { describe('fetchRepos', () => {
let mock; let mock;
const payload = { imported_projects: [{}], provider_repos: [{}], namespaces: [{}] }; const payload = { imported_projects: [{}], provider_repos: [{}] };
beforeEach(() => { beforeEach(() => {
localState.reposPath = `${TEST_HOST}/endpoint.json`;
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
}); });
afterEach(() => mock.restore()); afterEach(() => mock.restore());
it('dispatches stopJobsPolling actions and commits REQUEST_REPOS, RECEIVE_REPOS_SUCCESS mutations on a successful request', () => { it('dispatches stopJobsPolling actions and commits REQUEST_REPOS, RECEIVE_REPOS_SUCCESS mutations on a successful request', () => {
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, payload); mock.onGet(MOCK_ENDPOINT).reply(200, payload);
return testAction( return testAction(
fetchRepos, fetchRepos,
...@@ -64,7 +99,7 @@ describe('import_projects store actions', () => { ...@@ -64,7 +99,7 @@ describe('import_projects store actions', () => {
}); });
it('dispatches stopJobsPolling action and commits REQUEST_REPOS, RECEIVE_REPOS_ERROR mutations on an unsuccessful request', () => { it('dispatches stopJobsPolling action and commits REQUEST_REPOS, RECEIVE_REPOS_ERROR mutations on an unsuccessful request', () => {
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500); mock.onGet(MOCK_ENDPOINT).reply(500);
return testAction( return testAction(
fetchRepos, fetchRepos,
...@@ -104,7 +139,6 @@ describe('import_projects store actions', () => { ...@@ -104,7 +139,6 @@ describe('import_projects store actions', () => {
let mock; let mock;
beforeEach(() => { beforeEach(() => {
localState.importPath = `${TEST_HOST}/endpoint.json`;
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
}); });
...@@ -112,15 +146,17 @@ describe('import_projects store actions', () => { ...@@ -112,15 +146,17 @@ describe('import_projects store actions', () => {
it('commits REQUEST_IMPORT and REQUEST_IMPORT_SUCCESS mutations on a successful request', () => { it('commits REQUEST_IMPORT and REQUEST_IMPORT_SUCCESS mutations on a successful request', () => {
const importedProject = { name: 'imported/project' }; const importedProject = { name: 'imported/project' };
const importRepoId = importPayload.repo.id; mock.onPost(MOCK_ENDPOINT).reply(200, importedProject);
mock.onPost(`${TEST_HOST}/endpoint.json`).reply(200, importedProject);
return testAction( return testAction(
fetchImport, fetchImport,
importPayload, importRepoId,
localState, localState,
[ [
{ type: REQUEST_IMPORT, payload: importRepoId }, {
type: REQUEST_IMPORT,
payload: { repoId: importRepoId, importTarget: defaultImportTarget },
},
{ {
type: RECEIVE_IMPORT_SUCCESS, type: RECEIVE_IMPORT_SUCCESS,
payload: { payload: {
...@@ -134,15 +170,18 @@ describe('import_projects store actions', () => { ...@@ -134,15 +170,18 @@ describe('import_projects store actions', () => {
}); });
it('commits REQUEST_IMPORT and RECEIVE_IMPORT_ERROR and shows generic error message on an unsuccessful request', async () => { it('commits REQUEST_IMPORT and RECEIVE_IMPORT_ERROR and shows generic error message on an unsuccessful request', async () => {
mock.onPost(`${TEST_HOST}/endpoint.json`).reply(500); mock.onPost(MOCK_ENDPOINT).reply(500);
await testAction( await testAction(
fetchImport, fetchImport,
importPayload, importRepoId,
localState, localState,
[ [
{ type: REQUEST_IMPORT, payload: importPayload.repo.id }, {
{ type: RECEIVE_IMPORT_ERROR, payload: importPayload.repo.id }, type: REQUEST_IMPORT,
payload: { repoId: importRepoId, importTarget: defaultImportTarget },
},
{ type: RECEIVE_IMPORT_ERROR, payload: importRepoId },
], ],
[], [],
); );
...@@ -152,15 +191,18 @@ describe('import_projects store actions', () => { ...@@ -152,15 +191,18 @@ describe('import_projects store actions', () => {
it('commits REQUEST_IMPORT and RECEIVE_IMPORT_ERROR and shows detailed error message on an unsuccessful request with errors fields in response', async () => { it('commits REQUEST_IMPORT and RECEIVE_IMPORT_ERROR and shows detailed error message on an unsuccessful request with errors fields in response', async () => {
const ERROR_MESSAGE = 'dummy'; const ERROR_MESSAGE = 'dummy';
mock.onPost(`${TEST_HOST}/endpoint.json`).reply(500, { errors: ERROR_MESSAGE }); mock.onPost(MOCK_ENDPOINT).reply(500, { errors: ERROR_MESSAGE });
await testAction( await testAction(
fetchImport, fetchImport,
importPayload, importRepoId,
localState, localState,
[ [
{ type: REQUEST_IMPORT, payload: importPayload.repo.id }, {
{ type: RECEIVE_IMPORT_ERROR, payload: importPayload.repo.id }, type: REQUEST_IMPORT,
payload: { repoId: importRepoId, importTarget: defaultImportTarget },
},
{ type: RECEIVE_IMPORT_ERROR, payload: importRepoId },
], ],
[], [],
); );
...@@ -169,24 +211,11 @@ describe('import_projects store actions', () => { ...@@ -169,24 +211,11 @@ describe('import_projects store actions', () => {
}); });
}); });
describe('receiveJobsSuccess', () => {
it(`commits ${RECEIVE_JOBS_SUCCESS} mutation`, () => {
return testAction(
receiveJobsSuccess,
repos,
localState,
[{ type: RECEIVE_JOBS_SUCCESS, payload: repos }],
[],
);
});
});
describe('fetchJobs', () => { describe('fetchJobs', () => {
let mock; let mock;
const updatedProjects = [{ name: 'imported/project' }, { name: 'provider/repo' }]; const updatedProjects = [{ name: 'imported/project' }, { name: 'provider/repo' }];
beforeEach(() => { beforeEach(() => {
localState.jobsPath = `${TEST_HOST}/endpoint.json`;
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
}); });
...@@ -198,7 +227,7 @@ describe('import_projects store actions', () => { ...@@ -198,7 +227,7 @@ describe('import_projects store actions', () => {
afterEach(() => mock.restore()); afterEach(() => mock.restore());
it('commits RECEIVE_JOBS_SUCCESS mutation on a successful request', async () => { it('commits RECEIVE_JOBS_SUCCESS mutation on a successful request', async () => {
mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, updatedProjects); mock.onGet(MOCK_ENDPOINT).reply(200, updatedProjects);
await testAction( await testAction(
fetchJobs, fetchJobs,
...@@ -237,4 +266,62 @@ describe('import_projects store actions', () => { ...@@ -237,4 +266,62 @@ describe('import_projects store actions', () => {
}); });
}); });
}); });
describe('fetchNamespaces', () => {
let mock;
const namespaces = [{ full_name: 'test/ns1' }, { full_name: 'test_ns2' }];
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => mock.restore());
it('commits REQUEST_NAMESPACES and RECEIVE_NAMESPACES_SUCCESS on success', async () => {
mock.onGet(MOCK_ENDPOINT).reply(200, namespaces);
await testAction(
fetchNamespaces,
null,
localState,
[
{ type: REQUEST_NAMESPACES },
{
type: RECEIVE_NAMESPACES_SUCCESS,
payload: convertObjectPropsToCamelCase(namespaces, { deep: true }),
},
],
[],
);
});
it('commits REQUEST_NAMESPACES and RECEIVE_NAMESPACES_ERROR and shows generic error message on an unsuccessful request', async () => {
mock.onGet(MOCK_ENDPOINT).reply(500);
await testAction(
fetchNamespaces,
null,
localState,
[{ type: REQUEST_NAMESPACES }, { type: RECEIVE_NAMESPACES_ERROR }],
[],
);
expect(createFlash).toHaveBeenCalledWith('Requesting namespaces failed');
});
});
describe('importAll', () => {
it('dispatches multiple fetchImport actions', async () => {
await testAction(
importAll,
null,
localState,
[],
[
{ type: 'fetchImport', payload: importRepoId },
{ type: 'fetchImport', payload: otherImportRepoId },
],
);
});
});
}); });
import { import {
namespaceSelectOptions, isLoading,
isImportingAnyRepo, isImportingAnyRepo,
hasProviderRepos,
hasIncompatibleRepos, hasIncompatibleRepos,
hasImportedProjects, hasImportableRepos,
getImportTarget,
} from '~/import_projects/store/getters'; } from '~/import_projects/store/getters';
import { STATUSES } from '~/import_projects/constants';
import state from '~/import_projects/store/state'; import state from '~/import_projects/store/state';
const IMPORTED_REPO = {
importSource: {},
importedProject: { fullPath: 'some/path' },
};
const IMPORTABLE_REPO = {
importSource: { id: 'some-id', sanitizedName: 'sanitized' },
importedProject: null,
importStatus: STATUSES.NONE,
};
const INCOMPATIBLE_REPO = {
importSource: { incompatible: true },
};
describe('import_projects store getters', () => { describe('import_projects store getters', () => {
let localState; let localState;
...@@ -14,85 +30,87 @@ describe('import_projects store getters', () => { ...@@ -14,85 +30,87 @@ describe('import_projects store getters', () => {
localState = state(); localState = state();
}); });
describe('namespaceSelectOptions', () => { it.each`
const namespaces = [{ fullPath: 'namespace-0' }, { fullPath: 'namespace-1' }]; isLoadingRepos | isLoadingNamespaces | isLoadingValue
const defaultTargetNamespace = 'current-user'; ${false} | ${false} | ${false}
${true} | ${false} | ${true}
it('returns an options array with a "Users" and "Groups" optgroups', () => { ${false} | ${true} | ${true}
localState.namespaces = namespaces; ${true} | ${true} | ${true}
localState.defaultTargetNamespace = defaultTargetNamespace; `(
'isLoading returns $isLoadingValue when isLoadingRepos is $isLoadingRepos and isLoadingNamespaces is $isLoadingNamespaces',
const optionsArray = namespaceSelectOptions(localState); ({ isLoadingRepos, isLoadingNamespaces, isLoadingValue }) => {
const groupsGroup = optionsArray[0]; Object.assign(localState, {
const usersGroup = optionsArray[1]; isLoadingRepos,
isLoadingNamespaces,
expect(groupsGroup.text).toBe('Groups');
expect(usersGroup.text).toBe('Users');
groupsGroup.children.forEach((child, index) => {
expect(child.id).toBe(namespaces[index].fullPath);
expect(child.text).toBe(namespaces[index].fullPath);
}); });
expect(usersGroup.children.length).toBe(1); expect(isLoading(localState)).toBe(isLoadingValue);
expect(usersGroup.children[0].id).toBe(defaultTargetNamespace); },
expect(usersGroup.children[0].text).toBe(defaultTargetNamespace); );
});
}); it.each`
importStatus | value
describe('isImportingAnyRepo', () => { ${STATUSES.NONE} | ${false}
it('returns true if there are any reposBeingImported', () => { ${STATUSES.SCHEDULING} | ${true}
localState.reposBeingImported = new Array(1); ${STATUSES.SCHEDULED} | ${true}
${STATUSES.STARTED} | ${true}
expect(isImportingAnyRepo(localState)).toBe(true); ${STATUSES.FINISHED} | ${false}
}); `(
'isImportingAnyRepo returns $value when repo with $importStatus status is available',
({ importStatus, value }) => {
localState.repositories = [{ importStatus }];
expect(isImportingAnyRepo(localState)).toBe(value);
},
);
it('returns false if there are no reposBeingImported', () => { describe('hasIncompatibleRepos', () => {
localState.reposBeingImported = []; it('returns true if there are any incompatible projects', () => {
localState.repositories = [IMPORTABLE_REPO, IMPORTED_REPO, INCOMPATIBLE_REPO];
expect(isImportingAnyRepo(localState)).toBe(false);
});
});
describe('hasProviderRepos', () => {
it('returns true if there are any providerRepos', () => {
localState.providerRepos = new Array(1);
expect(hasProviderRepos(localState)).toBe(true); expect(hasIncompatibleRepos(localState)).toBe(true);
}); });
it('returns false if there are no providerRepos', () => { it('returns false if there are no incompatible projects', () => {
localState.providerRepos = []; localState.repositories = [IMPORTABLE_REPO, IMPORTED_REPO];
expect(hasProviderRepos(localState)).toBe(false); expect(hasIncompatibleRepos(localState)).toBe(false);
}); });
}); });
describe('hasImportedProjects', () => { describe('hasImportableRepos', () => {
it('returns true if there are any importedProjects', () => { it('returns true if there are any importable projects ', () => {
localState.importedProjects = new Array(1); localState.repositories = [IMPORTABLE_REPO, IMPORTED_REPO, INCOMPATIBLE_REPO];
expect(hasImportedProjects(localState)).toBe(true); expect(hasImportableRepos(localState)).toBe(true);
}); });
it('returns false if there are no importedProjects', () => { it('returns false if there are no importable projects', () => {
localState.importedProjects = []; localState.repositories = [IMPORTED_REPO, INCOMPATIBLE_REPO];
expect(hasImportedProjects(localState)).toBe(false); expect(hasImportableRepos(localState)).toBe(false);
}); });
}); });
describe('hasIncompatibleRepos', () => { describe('getImportTarget', () => {
it('returns true if there are any incompatibleProjects', () => { it('returns default value if no custom target available', () => {
localState.incompatibleRepos = new Array(1); localState.defaultTargetNamespace = 'default';
localState.repositories = [IMPORTABLE_REPO];
expect(hasIncompatibleRepos(localState)).toBe(true); expect(getImportTarget(localState)(IMPORTABLE_REPO.importSource.id)).toStrictEqual({
newName: IMPORTABLE_REPO.importSource.sanitizedName,
targetNamespace: localState.defaultTargetNamespace,
});
}); });
it('returns false if there are no incompatibleProjects', () => { it('returns custom import target if available', () => {
localState.incompatibleRepos = []; const fakeTarget = { newName: 'something', targetNamespace: 'ns' };
localState.repositories = [IMPORTABLE_REPO];
localState.customImportTargets[IMPORTABLE_REPO.importSource.id] = fakeTarget;
expect(hasIncompatibleRepos(localState)).toBe(false); expect(getImportTarget(localState)(IMPORTABLE_REPO.importSource.id)).toStrictEqual(
fakeTarget,
);
}); });
}); });
}); });
import * as types from '~/import_projects/store/mutation_types'; import * as types from '~/import_projects/store/mutation_types';
import mutations from '~/import_projects/store/mutations'; import mutations from '~/import_projects/store/mutations';
import { STATUSES } from '~/import_projects/constants';
describe('import_projects store mutations', () => { describe('import_projects store mutations', () => {
describe(`${types.RECEIVE_IMPORT_SUCCESS}`, () => { let state;
it('removes repoId from reposBeingImported and providerRepos, adds to importedProjects', () => { const SOURCE_PROJECT = {
const repoId = 1; id: 1,
const state = { full_name: 'full/name',
reposBeingImported: [repoId], sanitized_name: 'name',
providerRepos: [{ id: repoId }], provider_link: 'https://demo.link/full/name',
};
const IMPORTED_PROJECT = {
name: 'demo',
importSource: 'something',
providerLink: 'custom-link',
importStatus: 'status',
fullName: 'fullName',
};
describe(`${types.SET_FILTER}`, () => {
it('overwrites current filter value', () => {
state = { filter: 'some-value' };
const NEW_VALUE = 'new-value';
mutations[types.SET_FILTER](state, NEW_VALUE);
expect(state.filter).toBe(NEW_VALUE);
});
});
describe(`${types.REQUEST_REPOS}`, () => {
it('sets repos loading flag to true', () => {
state = {};
mutations[types.REQUEST_REPOS](state);
expect(state.isLoadingRepos).toBe(true);
});
});
describe(`${types.RECEIVE_REPOS_SUCCESS}`, () => {
describe('for imported projects', () => {
const response = {
importedProjects: [IMPORTED_PROJECT],
providerRepos: [],
};
it('picks import status from response', () => {
state = {};
mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
expect(state.repositories[0].importStatus).toBe(IMPORTED_PROJECT.importStatus);
});
it('recreates importSource from response', () => {
state = {};
mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
expect(state.repositories[0].importSource).toStrictEqual(
expect.objectContaining({
fullName: IMPORTED_PROJECT.importSource,
sanitizedName: IMPORTED_PROJECT.name,
providerLink: IMPORTED_PROJECT.providerLink,
}),
);
});
it('passes project to importProject', () => {
state = {};
mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
expect(IMPORTED_PROJECT).toStrictEqual(
expect.objectContaining(state.repositories[0].importedProject),
);
});
});
describe('for importable projects', () => {
beforeEach(() => {
state = {};
const response = {
importedProjects: [],
providerRepos: [SOURCE_PROJECT],
};
mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
});
it('sets import status to none', () => {
expect(state.repositories[0].importStatus).toBe(STATUSES.NONE);
});
it('sets importSource to project', () => {
expect(state.repositories[0].importSource).toBe(SOURCE_PROJECT);
});
});
describe('for incompatible projects', () => {
const response = {
importedProjects: [], importedProjects: [],
providerRepos: [],
incompatibleRepos: [SOURCE_PROJECT],
}; };
const importedProject = { id: repoId };
mutations[types.RECEIVE_IMPORT_SUCCESS](state, { importedProject, repoId }); beforeEach(() => {
state = {};
mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
});
expect(state.reposBeingImported.includes(repoId)).toBe(false); it('sets incompatible flag', () => {
expect(state.providerRepos.some(repo => repo.id === repoId)).toBe(false); expect(state.repositories[0].importSource.incompatible).toBe(true);
expect(state.importedProjects.some(repo => repo.id === repoId)).toBe(true); });
it('sets importSource to project', () => {
expect(state.repositories[0].importSource).toStrictEqual(
expect.objectContaining(SOURCE_PROJECT),
);
});
});
it('sets repos loading flag to false', () => {
const response = {
importedProjects: [],
providerRepos: [],
};
state = {};
mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
expect(state.isLoadingRepos).toBe(false);
});
});
describe(`${types.RECEIVE_REPOS_ERROR}`, () => {
it('sets repos loading flag to false', () => {
state = {};
mutations[types.RECEIVE_REPOS_ERROR](state);
expect(state.isLoadingRepos).toBe(false);
});
});
describe(`${types.REQUEST_IMPORT}`, () => {
beforeEach(() => {
const REPO_ID = 1;
const importTarget = { targetNamespace: 'ns', newName: 'name ' };
state = { repositories: [{ importSource: { id: REPO_ID } }] };
mutations[types.REQUEST_IMPORT](state, { repoId: REPO_ID, importTarget });
});
it(`sets status to ${STATUSES.SCHEDULING}`, () => {
expect(state.repositories[0].importStatus).toBe(STATUSES.SCHEDULING);
});
});
describe(`${types.RECEIVE_IMPORT_SUCCESS}`, () => {
beforeEach(() => {
const REPO_ID = 1;
state = { repositories: [{ importSource: { id: REPO_ID } }] };
mutations[types.RECEIVE_IMPORT_SUCCESS](state, {
repoId: REPO_ID,
importedProject: IMPORTED_PROJECT,
});
});
it('sets import status', () => {
expect(state.repositories[0].importStatus).toBe(IMPORTED_PROJECT.importStatus);
});
it('sets imported project', () => {
expect(IMPORTED_PROJECT).toStrictEqual(
expect.objectContaining(state.repositories[0].importedProject),
);
});
});
describe(`${types.RECEIVE_IMPORT_ERROR}`, () => {
beforeEach(() => {
const REPO_ID = 1;
state = { repositories: [{ importSource: { id: REPO_ID } }] };
mutations[types.RECEIVE_IMPORT_ERROR](state, REPO_ID);
});
it(`resets import status to ${STATUSES.NONE}`, () => {
expect(state.repositories[0].importStatus).toBe(STATUSES.NONE);
}); });
}); });
describe(`${types.RECEIVE_JOBS_SUCCESS}`, () => { describe(`${types.RECEIVE_JOBS_SUCCESS}`, () => {
it('updates importStatus of existing importedProjects', () => { it('updates import status of existing project', () => {
const repoId = 1; const repoId = 1;
const state = { importedProjects: [{ id: repoId, importStatus: 'started' }] }; state = {
const updatedProjects = [{ id: repoId, importStatus: 'finished' }]; repositories: [{ importedProject: { id: repoId }, importStatus: STATUSES.STARTED }],
};
const updatedProjects = [{ id: repoId, importStatus: STATUSES.FINISHED }];
mutations[types.RECEIVE_JOBS_SUCCESS](state, updatedProjects); mutations[types.RECEIVE_JOBS_SUCCESS](state, updatedProjects);
expect(state.importedProjects[0].importStatus).toBe(updatedProjects[0].importStatus); expect(state.repositories[0].importStatus).toBe(updatedProjects[0].importStatus);
});
});
describe(`${types.REQUEST_NAMESPACES}`, () => {
it('sets namespaces loading flag to true', () => {
state = {};
mutations[types.REQUEST_NAMESPACES](state);
expect(state.isLoadingNamespaces).toBe(true);
});
});
describe(`${types.RECEIVE_NAMESPACES_SUCCESS}`, () => {
const response = [{ fullPath: 'some/path' }];
beforeEach(() => {
state = {};
mutations[types.RECEIVE_NAMESPACES_SUCCESS](state, response);
});
it('stores namespaces to state', () => {
expect(state.namespaces).toStrictEqual(response);
});
it('sets namespaces loading flag to false', () => {
expect(state.isLoadingNamespaces).toBe(false);
});
});
describe(`${types.RECEIVE_NAMESPACES_ERROR}`, () => {
it('sets namespaces loading flag to false', () => {
state = {};
mutations[types.RECEIVE_NAMESPACES_ERROR](state);
expect(state.isLoadingNamespaces).toBe(false);
});
});
describe(`${types.SET_IMPORT_TARGET}`, () => {
const PROJECT = {
id: 2,
sanitizedName: 'sanitizedName',
};
it('stores custom target if it differs from defaults', () => {
state = { customImportTargets: {}, repositories: [{ importSource: PROJECT }] };
const importTarget = { targetNamespace: 'ns', newName: 'name ' };
mutations[types.SET_IMPORT_TARGET](state, { repoId: PROJECT.id, importTarget });
expect(state.customImportTargets[PROJECT.id]).toBe(importTarget);
});
it('removes custom target if it is equal to defaults', () => {
const importTarget = { targetNamespace: 'ns', newName: 'name ' };
state = {
defaultTargetNamespace: 'default',
customImportTargets: {
[PROJECT.id]: importTarget,
},
repositories: [{ importSource: PROJECT }],
};
mutations[types.SET_IMPORT_TARGET](state, {
repoId: PROJECT.id,
importTarget: {
targetNamespace: state.defaultTargetNamespace,
newName: PROJECT.sanitizedName,
},
});
expect(state.customImportTargets[SOURCE_PROJECT.id]).toBeUndefined();
}); });
}); });
}); });
import { isProjectImportable } from '~/import_projects/utils';
import { STATUSES } from '~/import_projects/constants';
describe('import_projects utils', () => {
describe('isProjectImportable', () => {
it.each`
status | result
${STATUSES.FINISHED} | ${false}
${STATUSES.FAILED} | ${false}
${STATUSES.SCHEDULED} | ${false}
${STATUSES.STARTED} | ${false}
${STATUSES.NONE} | ${true}
${STATUSES.SCHEDULING} | ${false}
`('returns $result when project is compatible and status is $status', ({ status, result }) => {
expect(
isProjectImportable({
importStatus: status,
importSource: { incompatible: false },
}),
).toBe(result);
});
it('returns false if project is not compatible', () => {
expect(
isProjectImportable({
importStatus: STATUSES.NONE,
importSource: { incompatible: true },
}),
).toBe(false);
});
});
});
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