diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-04 00:11:13 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-04 00:11:13 +0300 |
commit | 63b3a14f15ee5c202d78b7bd72030f4f437ef982 (patch) | |
tree | 7994560f5a6bc1d421ce0fe395031e52d2b4719a /app/assets/javascripts/access_tokens | |
parent | 9578c9f9e88421a5dc4d9215f40d932bd30cbabc (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/access_tokens')
4 files changed, 223 insertions, 11 deletions
diff --git a/app/assets/javascripts/access_tokens/components/projects_field.vue b/app/assets/javascripts/access_tokens/components/projects_field.vue index e58f74b6ad4..dfe4e07662b 100644 --- a/app/assets/javascripts/access_tokens/components/projects_field.vue +++ b/app/assets/javascripts/access_tokens/components/projects_field.vue @@ -1,11 +1,12 @@ <script> import { GlFormGroup, GlFormRadio, GlFormText } from '@gitlab/ui'; +import ProjectsTokenSelector from './projects_token_selector.vue'; export default { name: 'ProjectsField', ALL_PROJECTS: 'ALL_PROJECTS', SELECTED_PROJECTS: 'SELECTED_PROJECTS', - components: { GlFormGroup, GlFormRadio, GlFormText }, + components: { GlFormGroup, GlFormRadio, GlFormText, ProjectsTokenSelector }, props: { inputAttrs: { type: Object, @@ -15,8 +16,24 @@ export default { data() { return { selectedRadio: this.$options.ALL_PROJECTS, + selectedProjects: [], }; }, + computed: { + allProjectsRadioSelected() { + return this.selectedRadio === this.$options.ALL_PROJECTS; + }, + hiddenInputValue() { + return this.allProjectsRadioSelected + ? null + : this.selectedProjects.map((project) => project.id).join(','); + }, + }, + methods: { + handleTokenSelectorFocus() { + this.selectedRadio = this.$options.SELECTED_PROJECTS; + }, + }, }; </script> @@ -32,7 +49,8 @@ export default { <gl-form-radio v-model="selectedRadio" :value="$options.SELECTED_PROJECTS">{{ __('Selected projects') }}</gl-form-radio> - <input :id="inputAttrs.id" type="hidden" :name="inputAttrs.name" /> + <input :id="inputAttrs.id" type="hidden" :name="inputAttrs.name" :value="hiddenInputValue" /> + <projects-token-selector v-model="selectedProjects" @focus="handleTokenSelectorFocus" /> </gl-form-group> </div> </template> diff --git a/app/assets/javascripts/access_tokens/components/projects_token_selector.vue b/app/assets/javascripts/access_tokens/components/projects_token_selector.vue new file mode 100644 index 00000000000..839f32bd488 --- /dev/null +++ b/app/assets/javascripts/access_tokens/components/projects_token_selector.vue @@ -0,0 +1,141 @@ +<script> +import { + GlTokenSelector, + GlAvatar, + GlAvatarLabeled, + GlIntersectionObserver, + GlLoadingIcon, +} from '@gitlab/ui'; +import produce from 'immer'; + +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; + +import getProjectsQuery from '../graphql/queries/get_projects.query.graphql'; + +const DEBOUNCE_DELAY = 250; +const PROJECTS_PER_PAGE = 20; + +export default { + name: 'ProjectsTokenSelector', + components: { + GlTokenSelector, + GlAvatar, + GlAvatarLabeled, + GlIntersectionObserver, + GlLoadingIcon, + }, + model: { + prop: 'selectedProjects', + }, + props: { + selectedProjects: { + type: Array, + required: true, + }, + }, + apollo: { + projects: { + query: getProjectsQuery, + debounce: DEBOUNCE_DELAY, + variables() { + return { + search: this.searchQuery, + after: null, + first: PROJECTS_PER_PAGE, + }; + }, + update({ projects }) { + return { + list: projects.nodes.map((project) => ({ + ...project, + id: getIdFromGraphQLId(project.id), + })), + pageInfo: projects.pageInfo, + }; + }, + result() { + this.isLoadingMoreProjects = false; + this.isSearching = false; + }, + }, + }, + data() { + return { + projects: { + list: [], + pageInfo: {}, + }, + searchQuery: '', + isLoadingMoreProjects: false, + isSearching: false, + }; + }, + methods: { + handleSearch(query) { + this.isSearching = true; + this.searchQuery = query; + }, + loadMoreProjects() { + this.isLoadingMoreProjects = true; + + this.$apollo.queries.projects.fetchMore({ + variables: { + after: this.projects.pageInfo.endCursor, + first: PROJECTS_PER_PAGE, + }, + updateQuery(previousResult, { fetchMoreResult: { projects: newProjects } }) { + const { projects: previousProjects } = previousResult; + + return produce(previousResult, (draftData) => { + /* eslint-disable no-param-reassign */ + draftData.projects.nodes = [...previousProjects.nodes, ...newProjects.nodes]; + draftData.projects.pageInfo = newProjects.pageInfo; + /* eslint-enable no-param-reassign */ + }); + }, + }); + }, + }, +}; +</script> + +<template> + <div class="gl-relative"> + <gl-token-selector + :selected-tokens="selectedProjects" + :dropdown-items="projects.list" + :loading="isSearching" + :placeholder="__('Select projects')" + menu-class="gl-w-full! gl-max-w-full!" + @input="$emit('input', $event)" + @focus="$emit('focus', $event)" + @text-input="handleSearch" + @keydown.enter.prevent + > + <template #token-content="{ token: project }"> + <gl-avatar + :entity-id="project.id" + :entity-name="project.name" + :src="project.avatarUrl" + :size="16" + /> + {{ project.nameWithNamespace }} + </template> + <template #dropdown-item-content="{ dropdownItem: project }"> + <gl-avatar-labeled + :entity-id="project.id" + :entity-name="project.name" + :size="32" + :src="project.avatarUrl" + :label="project.name" + :sub-label="project.nameWithNamespace" + /> + </template> + <template #dropdown-footer> + <gl-intersection-observer v-if="projects.pageInfo.hasNextPage" @appear="loadMoreProjects"> + <gl-loading-icon v-if="isLoadingMoreProjects" size="md" /> + </gl-intersection-observer> + </template> + </gl-token-selector> + </div> +</template> diff --git a/app/assets/javascripts/access_tokens/graphql/queries/get_projects.query.graphql b/app/assets/javascripts/access_tokens/graphql/queries/get_projects.query.graphql new file mode 100644 index 00000000000..40d475cb571 --- /dev/null +++ b/app/assets/javascripts/access_tokens/graphql/queries/get_projects.query.graphql @@ -0,0 +1,22 @@ +#import "~/graphql_shared/fragments/pageInfo.fragment.graphql" + +query getProjects($search: String!, $after: String = "", $first: Int!) { + projects( + search: $search + after: $after + first: $first + membership: true + searchNamespaces: true + sort: "UPDATED_ASC" + ) { + nodes { + id + name + nameWithNamespace + avatarUrl + } + pageInfo { + ...PageInfo + } + } +} diff --git a/app/assets/javascripts/access_tokens/index.js b/app/assets/javascripts/access_tokens/index.js index e29ec5adb42..6ab17092999 100644 --- a/app/assets/javascripts/access_tokens/index.js +++ b/app/assets/javascripts/access_tokens/index.js @@ -1,4 +1,6 @@ import Vue from 'vue'; +import createFlash from '~/flash'; +import { __ } from '~/locale'; import ExpiresAtField from './components/expires_at_field.vue'; @@ -43,17 +45,46 @@ export const initProjectsField = () => { const inputAttrs = getInputAttrs(el); if (window.gon.features.personalAccessTokensScopedToProjects) { - const ProjectsField = () => import('./components/projects_field.vue'); - - return new Vue({ - el, - render(h) { - return h(ProjectsField, { - props: { - inputAttrs, + return new Promise((resolve) => { + Promise.all([ + import('./components/projects_field.vue'), + import('vue-apollo'), + import('~/lib/graphql'), + ]) + .then( + ([ + { default: ProjectsField }, + { default: VueApollo }, + { default: createDefaultClient }, + ]) => { + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + + Vue.use(VueApollo); + + resolve( + new Vue({ + el, + apolloProvider, + render(h) { + return h(ProjectsField, { + props: { + inputAttrs, + }, + }); + }, + }), + ); }, + ) + .catch(() => { + createFlash({ + message: __( + 'An error occurred while loading the access tokens form, please try again.', + ), + }); }); - }, }); } |