diff options
Diffstat (limited to 'app/assets/javascripts/access_tokens/components')
-rw-r--r-- | app/assets/javascripts/access_tokens/components/projects_field.vue | 69 | ||||
-rw-r--r-- | app/assets/javascripts/access_tokens/components/projects_token_selector.vue | 156 |
2 files changed, 225 insertions, 0 deletions
diff --git a/app/assets/javascripts/access_tokens/components/projects_field.vue b/app/assets/javascripts/access_tokens/components/projects_field.vue new file mode 100644 index 00000000000..066cea5e90c --- /dev/null +++ b/app/assets/javascripts/access_tokens/components/projects_field.vue @@ -0,0 +1,69 @@ +<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, ProjectsTokenSelector }, + props: { + inputAttrs: { + type: Object, + required: true, + }, + }, + data() { + return { + selectedRadio: !this.inputAttrs.value + ? this.$options.ALL_PROJECTS + : this.$options.SELECTED_PROJECTS, + selectedProjects: [], + }; + }, + computed: { + allProjectsRadioSelected() { + return this.selectedRadio === this.$options.ALL_PROJECTS; + }, + hiddenInputValue() { + return this.allProjectsRadioSelected + ? null + : this.selectedProjects.map((project) => project.id).join(','); + }, + initialProjectIds() { + if (!this.inputAttrs.value) { + return []; + } + + return this.inputAttrs.value.split(','); + }, + }, + methods: { + handleTokenSelectorFocus() { + this.selectedRadio = this.$options.SELECTED_PROJECTS; + }, + }, +}; +</script> + +<template> + <div> + <gl-form-group :label="__('Projects')" label-class="gl-pb-0!"> + <gl-form-text class="gl-pb-3">{{ + __('Set access permissions for this token.') + }}</gl-form-text> + <gl-form-radio v-model="selectedRadio" :value="$options.ALL_PROJECTS">{{ + __('All projects') + }}</gl-form-radio> + <gl-form-radio v-model="selectedRadio" :value="$options.SELECTED_PROJECTS">{{ + __('Selected projects') + }}</gl-form-radio> + <input :id="inputAttrs.id" type="hidden" :name="inputAttrs.name" :value="hiddenInputValue" /> + <projects-token-selector + v-model="selectedProjects" + :initial-project-ids="initialProjectIds" + @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..a746f62b3a1 --- /dev/null +++ b/app/assets/javascripts/access_tokens/components/projects_token_selector.vue @@ -0,0 +1,156 @@ +<script> +import { + GlTokenSelector, + GlAvatar, + GlAvatarLabeled, + GlIntersectionObserver, + GlLoadingIcon, +} from '@gitlab/ui'; +import produce from 'immer'; + +import { convertToGraphQLIds, convertNodeIdsFromGraphQLIds } from '~/graphql_shared/utils'; + +import getProjectsQuery from '../graphql/queries/get_projects.query.graphql'; + +const DEBOUNCE_DELAY = 250; +const PROJECTS_PER_PAGE = 20; +const GRAPHQL_ENTITY_TYPE = 'Project'; + +export default { + name: 'ProjectsTokenSelector', + components: { + GlTokenSelector, + GlAvatar, + GlAvatarLabeled, + GlIntersectionObserver, + GlLoadingIcon, + }, + model: { + prop: 'selectedProjects', + }, + props: { + selectedProjects: { + type: Array, + required: true, + }, + initialProjectIds: { + 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: convertNodeIdsFromGraphQLIds(projects.nodes), + pageInfo: projects.pageInfo, + }; + }, + result() { + this.isLoadingMoreProjects = false; + this.isSearching = false; + }, + }, + initialProjects: { + query: getProjectsQuery, + variables() { + return { + ids: convertToGraphQLIds(GRAPHQL_ENTITY_TYPE, this.initialProjectIds), + }; + }, + manual: true, + skip() { + return !this.initialProjectIds.length; + }, + result({ data: { projects } }) { + this.$emit('input', convertNodeIdsFromGraphQLIds(projects.nodes)); + }, + }, + }, + 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) => { + draftData.projects.nodes = [...previousProjects.nodes, ...newProjects.nodes]; + draftData.projects.pageInfo = newProjects.pageInfo; + }); + }, + }); + }, + }, +}; +</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> |