diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-08-18 00:09:56 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-08-18 00:09:56 +0300 |
commit | 9facfe5cb194005894467c06e4ea1a9800034ab4 (patch) | |
tree | 5ceaecd5f89725250bbf4119558715fa14576c4c /app/assets/javascripts/import_projects | |
parent | 18da92341dac366b7bcfd13f2d3c443ffa315af0 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/import_projects')
9 files changed, 194 insertions, 70 deletions
diff --git a/app/assets/javascripts/import_projects/components/bitbucket_status_table.vue b/app/assets/javascripts/import_projects/components/bitbucket_status_table.vue index f673a0e42dc..bc8aa522596 100644 --- a/app/assets/javascripts/import_projects/components/bitbucket_status_table.vue +++ b/app/assets/javascripts/import_projects/components/bitbucket_status_table.vue @@ -9,6 +9,7 @@ export default { GlSprintf, GlLink, }, + inheritAttrs: false, props: { providerTitle: { type: String, @@ -28,7 +29,7 @@ export default { }; </script> <template> - <import-projects-table :provider-title="providerTitle"> + <import-projects-table :provider-title="providerTitle" v-bind="$attrs"> <template #actions> <slot name="actions"></slot> </template> diff --git a/app/assets/javascripts/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_projects/components/import_projects_table.vue index 3ae12363f39..72fdaca7e24 100644 --- a/app/assets/javascripts/import_projects/components/import_projects_table.vue +++ b/app/assets/javascripts/import_projects/components/import_projects_table.vue @@ -3,9 +3,11 @@ import { throttle } from 'lodash'; import { mapActions, mapState, mapGetters } from 'vuex'; import { GlButton, GlLoadingIcon } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; +import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; import ImportedProjectTableRow from './imported_project_table_row.vue'; import ProviderRepoTableRow from './provider_repo_table_row.vue'; import IncompatibleRepoTableRow from './incompatible_repo_table_row.vue'; +import PageQueryParamSync from './page_query_param_sync.vue'; import { isProjectImportable } from '../utils'; const reposFetchThrottleDelay = 1000; @@ -16,8 +18,10 @@ export default { ImportedProjectTableRow, ProviderRepoTableRow, IncompatibleRepoTableRow, + PageQueryParamSync, GlLoadingIcon, GlButton, + PaginationLinks, }, props: { providerTitle: { @@ -29,10 +33,15 @@ export default { required: false, default: true, }, + paginatable: { + type: Boolean, + required: false, + default: false, + }, }, computed: { - ...mapState(['filter', 'repositories', 'namespaces', 'defaultTargetNamespace']), + ...mapState(['filter', 'repositories', 'namespaces', 'defaultTargetNamespace', 'pageInfo']), ...mapGetters([ 'isLoading', 'isImportingAnyRepo', @@ -90,6 +99,7 @@ export default { 'clearJobsEtagPoll', 'setFilter', 'importAll', + 'setPage', ]), handleFilterInput({ target }) { @@ -107,69 +117,82 @@ export default { <template> <div> + <page-query-param-sync :page="pageInfo.page" @popstate="setPage" /> + <p class="light text-nowrap mt-2"> {{ s__('ImportProjects|Select the projects you want to import') }} </p> <template v-if="hasIncompatibleRepos"> <slot name="incompatible-repos-warning"></slot> </template> - <div v-if="!isLoading" class="d-flex justify-content-between align-items-end flex-wrap mb-3"> - <gl-button - variant="success" - :loading="isImportingAnyRepo" - :disabled="!hasImportableRepos" - type="button" - @click="importAll" - >{{ importAllButtonText }}</gl-button - > - <slot name="actions"></slot> - <form v-if="filterable" class="gl-ml-auto" novalidate @submit.prevent> - <input - :value="filter" - data-qa-selector="githubish_import_filter_field" - class="form-control" - name="filter" - :placeholder="__('Filter your projects by name')" - autofocus - size="40" - @input="handleFilterInput($event)" - @keyup.enter="throttledFetchRepos" - /> - </form> - </div> <gl-loading-icon v-if="isLoading" class="js-loading-button-icon import-projects-loading-icon" size="md" /> - <div v-else-if="repositories.length" class="table-responsive"> - <table class="table import-table"> - <thead> - <th class="import-jobs-from-col">{{ fromHeaderText }}</th> - <th class="import-jobs-to-col">{{ __('To GitLab') }}</th> - <th class="import-jobs-status-col">{{ __('Status') }}</th> - <th class="import-jobs-cta-col"></th> - </thead> - <tbody> - <template v-for="repo in repositories"> - <incompatible-repo-table-row - v-if="repo.importSource.incompatible" - :key="repo.importSource.id" - :repo="repo" - /> - <provider-repo-table-row - v-else-if="isProjectImportable(repo)" - :key="repo.importSource.id" - :repo="repo" - :available-namespaces="availableNamespaces" - /> - <imported-project-table-row v-else :key="repo.importSource.id" :project="repo" /> - </template> - </tbody> - </table> - </div> - <div v-else class="text-center"> - <strong>{{ emptyStateText }}</strong> - </div> + <template v-if="!isLoading"> + <div class="d-flex justify-content-between align-items-end flex-wrap mb-3"> + <gl-button + variant="success" + :loading="isImportingAnyRepo" + :disabled="!hasImportableRepos" + type="button" + @click="importAll" + >{{ importAllButtonText }}</gl-button + > + <slot name="actions"></slot> + <form v-if="filterable" class="gl-ml-auto" novalidate @submit.prevent> + <input + :value="filter" + data-qa-selector="githubish_import_filter_field" + class="form-control" + name="filter" + :placeholder="__('Filter your projects by name')" + autofocus + size="40" + @input="handleFilterInput($event)" + @keyup.enter="throttledFetchRepos" + /> + </form> + </div> + <div v-if="repositories.length" class="table-responsive"> + <table class="table import-table"> + <thead> + <th class="import-jobs-from-col">{{ fromHeaderText }}</th> + <th class="import-jobs-to-col">{{ __('To GitLab') }}</th> + <th class="import-jobs-status-col">{{ __('Status') }}</th> + <th class="import-jobs-cta-col"></th> + </thead> + <tbody> + <template v-for="repo in repositories"> + <incompatible-repo-table-row + v-if="repo.importSource.incompatible" + :key="repo.importSource.id" + :repo="repo" + /> + <provider-repo-table-row + v-else-if="isProjectImportable(repo)" + :key="repo.importSource.id" + :repo="repo" + :available-namespaces="availableNamespaces" + /> + <imported-project-table-row v-else :key="repo.importSource.id" :project="repo" /> + </template> + </tbody> + </table> + </div> + <div v-else class="text-center"> + <strong>{{ emptyStateText }}</strong> + </div> + <pagination-links + v-if="paginatable" + align="center" + class="gl-mt-3" + :page-info="pageInfo" + :prev-page="pageInfo.page - 1" + :next-page="repositories.length && pageInfo.page + 1" + :change="setPage" + /> + </template> </div> </template> diff --git a/app/assets/javascripts/import_projects/components/page_query_param_sync.vue b/app/assets/javascripts/import_projects/components/page_query_param_sync.vue new file mode 100644 index 00000000000..5ba3d70f5d0 --- /dev/null +++ b/app/assets/javascripts/import_projects/components/page_query_param_sync.vue @@ -0,0 +1,39 @@ +<script> +import { queryToObject, setUrlParams, updateHistory } from '~/lib/utils/url_utility'; + +export default { + props: { + page: { + type: Number, + required: true, + }, + }, + + watch: { + page(newPage) { + updateHistory({ + url: setUrlParams({ + page: newPage === 1 ? null : newPage, + }), + }); + }, + }, + + created() { + window.addEventListener('popstate', this.updatePage); + }, + + beforeDestroy() { + window.removeEventListener('popstate', this.updatePage); + }, + + methods: { + updatePage() { + const page = parseInt(queryToObject(window.location.search).page, 10) || 1; + this.$emit('popstate', page); + }, + }, + + render: () => null, +}; +</script> diff --git a/app/assets/javascripts/import_projects/index.js b/app/assets/javascripts/import_projects/index.js index e616acdb310..79fbd58e355 100644 --- a/app/assets/javascripts/import_projects/index.js +++ b/app/assets/javascripts/import_projects/index.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import Translate from '../vue_shared/translate'; import ImportProjectsTable from './components/import_projects_table.vue'; import { parseBoolean } from '../lib/utils/common_utils'; +import { queryToObject } from '../lib/utils/url_utility'; import createStore from './store'; Vue.use(Translate); @@ -16,14 +17,21 @@ export function initStoreFromElement(element) { jobsPath, importPath, namespacesPath, + paginatable, } = element.dataset; + const params = queryToObject(document.location.search); + const page = parseInt(params.page ?? 1, 10); + return createStore({ initialState: { defaultTargetNamespace: gon.current_username, ciCdOnly: parseBoolean(ciCdOnly), canSelectNamespace: parseBoolean(canSelectNamespace), provider, + pageInfo: { + page, + }, }, endpoints: { reposPath, @@ -31,6 +39,7 @@ export function initStoreFromElement(element) { importPath, namespacesPath, }, + hasPagination: parseBoolean(paginatable), }); } @@ -38,6 +47,7 @@ export function initPropsFromElement(element) { return { providerTitle: element.dataset.providerTitle, filterable: parseBoolean(element.dataset.filterable), + paginatable: parseBoolean(element.dataset.paginatable), }; } diff --git a/app/assets/javascripts/import_projects/store/actions.js b/app/assets/javascripts/import_projects/store/actions.js index 5765dea51ac..a592bde6373 100644 --- a/app/assets/javascripts/import_projects/store/actions.js +++ b/app/assets/javascripts/import_projects/store/actions.js @@ -1,9 +1,13 @@ import Visibility from 'visibilityjs'; import * as types from './mutation_types'; import { isProjectImportable } from '../utils'; -import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import { + convertObjectPropsToCamelCase, + normalizeHeaders, + parseIntPagination, +} from '~/lib/utils/common_utils'; import Poll from '~/lib/utils/poll'; -import { visitUrl } from '~/lib/utils/url_utility'; +import { visitUrl, objectToQuery } from '~/lib/utils/url_utility'; import createFlash from '~/flash'; import { s__, sprintf } from '~/locale'; import axios from '~/lib/utils/axios_utils'; @@ -12,7 +16,13 @@ let eTagPoll; const hasRedirectInError = e => e?.response?.data?.error?.redirect; const redirectToUrlInError = e => visitUrl(e.response.data.error.redirect); -const pathWithFilter = ({ path, filter = '' }) => (filter ? `${path}?filter=${filter}` : path); +const pathWithParams = ({ path, ...params }) => { + const filteredParams = Object.fromEntries( + Object.entries(params).filter(([, value]) => value !== ''), + ); + const queryString = objectToQuery(filteredParams); + return queryString ? `${path}?${queryString}` : path; +}; const isRequired = () => { // eslint-disable-next-line @gitlab/require-i18n-strings @@ -44,17 +54,33 @@ const importAll = ({ state, dispatch }) => { ); }; -const fetchReposFactory = (reposPath = isRequired()) => ({ state, dispatch, commit }) => { +const fetchReposFactory = ({ reposPath = isRequired(), hasPagination }) => ({ + state, + dispatch, + commit, +}) => { dispatch('stopJobsPolling'); commit(types.REQUEST_REPOS); const { provider, filter } = state; return axios - .get(pathWithFilter({ path: reposPath, filter })) - .then(({ data }) => - commit(types.RECEIVE_REPOS_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true })), + .get( + pathWithParams({ + path: reposPath, + filter, + page: hasPagination ? state.pageInfo.page.toString() : '', + }), ) + .then(({ data, headers }) => { + const normalizedHeaders = normalizeHeaders(headers); + + if ('X-PAGE' in normalizedHeaders) { + commit(types.SET_PAGE_INFO, parseIntPagination(normalizedHeaders)); + } + + commit(types.RECEIVE_REPOS_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true })); + }) .then(() => dispatch('fetchJobs')) .catch(e => { if (hasRedirectInError(e)) { @@ -85,12 +111,12 @@ const fetchImportFactory = (importPath = isRequired()) => ({ state, commit, gett new_name: newName, target_namespace: targetNamespace, }) - .then(({ data }) => + .then(({ data }) => { commit(types.RECEIVE_IMPORT_SUCCESS, { importedProject: convertObjectPropsToCamelCase(data, { deep: true }), repoId, - }), - ) + }); + }) .catch(e => { const serverErrorMessage = e?.response?.data?.errors; const flashMessage = serverErrorMessage @@ -119,7 +145,7 @@ export const fetchJobsFactory = (jobsPath = isRequired()) => ({ state, commit, d eTagPoll = new Poll({ resource: { - fetchJobs: () => axios.get(pathWithFilter({ path: jobsPath, filter })), + fetchJobs: () => axios.get(pathWithParams({ path: jobsPath, filter })), }, method: 'fetchJobs', successCallback: ({ data }) => @@ -161,14 +187,24 @@ const fetchNamespacesFactory = (namespacesPath = isRequired()) => ({ commit }) = }); }; -export default ({ endpoints = isRequired() }) => ({ +const setPage = ({ state, commit, dispatch }, page) => { + if (page === state.pageInfo.page) { + return null; + } + + commit(types.SET_PAGE, page); + return dispatch('fetchRepos'); +}; + +export default ({ endpoints = isRequired(), hasPagination }) => ({ clearJobsEtagPoll, stopJobsPolling, restartJobsPolling, setFilter, setImportTarget, importAll, - fetchRepos: fetchReposFactory(endpoints.reposPath), + setPage, + fetchRepos: fetchReposFactory({ reposPath: endpoints.reposPath, hasPagination }), fetchImport: fetchImportFactory(endpoints.importPath), fetchJobs: fetchJobsFactory(endpoints.jobsPath), fetchNamespaces: fetchNamespacesFactory(endpoints.namespacesPath), diff --git a/app/assets/javascripts/import_projects/store/index.js b/app/assets/javascripts/import_projects/store/index.js index 1419de877fb..7ba12f81eb9 100644 --- a/app/assets/javascripts/import_projects/store/index.js +++ b/app/assets/javascripts/import_projects/store/index.js @@ -7,10 +7,10 @@ import mutations from './mutations'; Vue.use(Vuex); -export default ({ initialState, endpoints }) => +export default ({ initialState, endpoints, hasPagination }) => new Vuex.Store({ state: { ...state(), ...initialState }, - actions: actionsFactory({ endpoints }), + actions: actionsFactory({ endpoints, hasPagination }), mutations, getters, }); diff --git a/app/assets/javascripts/import_projects/store/mutation_types.js b/app/assets/javascripts/import_projects/store/mutation_types.js index 1bcf27d7ba4..6adf5e59cff 100644 --- a/app/assets/javascripts/import_projects/store/mutation_types.js +++ b/app/assets/javascripts/import_projects/store/mutation_types.js @@ -15,3 +15,7 @@ export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS'; export const SET_FILTER = 'SET_FILTER'; export const SET_IMPORT_TARGET = 'SET_IMPORT_TARGET'; + +export const SET_PAGE = 'SET_PAGE'; + +export const SET_PAGE_INFO = 'SET_PAGE_INFO'; diff --git a/app/assets/javascripts/import_projects/store/mutations.js b/app/assets/javascripts/import_projects/store/mutations.js index 44fc3da4de5..b3dbef896a6 100644 --- a/app/assets/javascripts/import_projects/store/mutations.js +++ b/app/assets/javascripts/import_projects/store/mutations.js @@ -104,4 +104,12 @@ export default { Vue.set(state.customImportTargets, repoId, importTarget); } }, + + [types.SET_PAGE_INFO](state, pageInfo) { + state.pageInfo = pageInfo; + }, + + [types.SET_PAGE](state, page) { + state.pageInfo.page = page; + }, }; diff --git a/app/assets/javascripts/import_projects/store/state.js b/app/assets/javascripts/import_projects/store/state.js index 8432a210955..3318181e4af 100644 --- a/app/assets/javascripts/import_projects/store/state.js +++ b/app/assets/javascripts/import_projects/store/state.js @@ -7,4 +7,7 @@ export default () => ({ isLoadingNamespaces: false, ciCdOnly: false, filter: '', + pageInfo: { + page: 1, + }, }); |