diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-08 15:08:46 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-08 15:08:46 +0300 |
commit | cdda3d117c99cadf295f26abc92cb2456033b762 (patch) | |
tree | 30315b1ea79ee4639f44a407978ed719c62a1653 /app/assets/javascripts/groups | |
parent | f4ea1f8998fd64bcd02280514b91f103f830d5ce (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/groups')
4 files changed, 249 insertions, 0 deletions
diff --git a/app/assets/javascripts/groups/settings/api/access_dropdown_api.js b/app/assets/javascripts/groups/settings/api/access_dropdown_api.js new file mode 100644 index 00000000000..5560d10d179 --- /dev/null +++ b/app/assets/javascripts/groups/settings/api/access_dropdown_api.js @@ -0,0 +1,16 @@ +import axios from '~/lib/utils/axios_utils'; +import { joinPaths } from '~/lib/utils/url_utility'; + +const GROUP_SUBGROUPS_PATH = '/-/autocomplete/group_subgroups.json'; + +const buildUrl = (urlRoot, url) => { + return joinPaths(urlRoot, url); +}; + +export const getSubGroups = () => { + return axios.get(buildUrl(gon.relative_url_root || '', GROUP_SUBGROUPS_PATH), { + params: { + group_id: gon.current_group_id, + }, + }); +}; diff --git a/app/assets/javascripts/groups/settings/components/access_dropdown.vue b/app/assets/javascripts/groups/settings/components/access_dropdown.vue new file mode 100644 index 00000000000..b8a269de98a --- /dev/null +++ b/app/assets/javascripts/groups/settings/components/access_dropdown.vue @@ -0,0 +1,194 @@ +<script> +import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlSearchBoxByType } from '@gitlab/ui'; +import { debounce, intersectionWith, groupBy, differenceBy, intersectionBy } from 'lodash'; +import createFlash from '~/flash'; +import { __, s__, n__ } from '~/locale'; +import { getSubGroups } from '../api/access_dropdown_api'; +import { LEVEL_TYPES } from '../constants'; + +export const i18n = { + selectUsers: s__('ProtectedEnvironment|Select groups'), + groupsSectionHeader: s__('AccessDropdown|Groups'), +}; + +export default { + i18n, + components: { + GlDropdown, + GlDropdownItem, + GlDropdownSectionHeader, + GlSearchBoxByType, + }, + props: { + hasLicense: { + required: false, + type: Boolean, + default: true, + }, + label: { + type: String, + required: false, + default: i18n.selectUsers, + }, + disabled: { + type: Boolean, + required: false, + default: false, + }, + preselectedItems: { + type: Array, + required: false, + default: () => [], + }, + }, + data() { + return { + loading: false, + initialLoading: false, + query: '', + groups: [], + selected: { + [LEVEL_TYPES.GROUP]: [], + }, + }; + }, + computed: { + preselected() { + return groupBy(this.preselectedItems, 'type'); + }, + toggleLabel() { + const counts = Object.fromEntries( + Object.entries(this.selected).map(([key, value]) => [key, value.length]), + ); + + const labelPieces = []; + + if (counts[LEVEL_TYPES.GROUP] > 0) { + labelPieces.push(n__('1 group', '%d groups', counts[LEVEL_TYPES.GROUP])); + } + + return labelPieces.join(', ') || this.label; + }, + toggleClass() { + return this.toggleLabel === this.label ? 'gl-text-gray-500!' : ''; + }, + selection() { + return [...this.getDataForSave(LEVEL_TYPES.GROUP, 'group_id')]; + }, + }, + watch: { + query: debounce(function debouncedSearch() { + return this.getData(); + }, 500), + }, + created() { + this.getData({ initial: true }); + }, + methods: { + focusInput() { + this.$refs.search.focusInput(); + }, + getData({ initial = false } = {}) { + this.initialLoading = initial; + this.loading = true; + + if (this.hasLicense) { + Promise.all([this.groups.length ? Promise.resolve({ data: this.groups }) : getSubGroups()]) + .then(([groupsResponse]) => { + this.consolidateData(groupsResponse.data); + this.setSelected({ initial }); + }) + .catch(() => createFlash({ message: __('Failed to load groups.') })) + .finally(() => { + this.initialLoading = false; + this.loading = false; + }); + } + }, + consolidateData(groupsResponse = []) { + if (this.hasLicense) { + this.groups = groupsResponse.map((group) => ({ ...group, type: LEVEL_TYPES.GROUP })); + } + }, + setSelected({ initial } = {}) { + if (initial) { + const selectedGroups = intersectionWith( + this.groups, + this.preselectedItems, + (group, selected) => { + return selected.type === LEVEL_TYPES.GROUP && group.id === selected.group_id; + }, + ); + this.selected[LEVEL_TYPES.GROUP] = selectedGroups; + } + }, + getDataForSave(accessType, key) { + const selected = this.selected[accessType].map(({ id }) => ({ [key]: id })); + const preselected = this.preselected[accessType]; + const added = differenceBy(selected, preselected, key); + const preserved = intersectionBy(preselected, selected, key).map(({ id, [key]: keyId }) => ({ + id, + [key]: keyId, + })); + const removed = differenceBy(preselected, selected, key).map(({ id, [key]: keyId }) => ({ + id, + [key]: keyId, + _destroy: true, + })); + return [...added, ...removed, ...preserved]; + }, + onItemClick(item) { + this.toggleSelection(this.selected[item.type], item); + this.emitUpdate(); + }, + toggleSelection(arr, item) { + const itemIndex = arr.findIndex(({ id }) => id === item.id); + if (itemIndex > -1) { + arr.splice(itemIndex, 1); + } else arr.push(item); + }, + isSelected(item) { + return this.selected[item.type].some((selected) => selected.id === item.id); + }, + emitUpdate() { + this.$emit('select', this.selection); + }, + onHide() { + this.$emit('hidden', this.selection); + }, + }, +}; +</script> + +<template> + <gl-dropdown + :disabled="disabled || initialLoading" + :text="toggleLabel" + class="gl-min-w-20" + :toggle-class="toggleClass" + aria-labelledby="allowed-users-label" + @shown="focusInput" + @hidden="onHide" + > + <template #header> + <gl-search-box-by-type ref="search" v-model.trim="query" :is-loading="loading" /> + </template> + <template v-if="groups.length"> + <gl-dropdown-section-header>{{ + $options.i18n.groupsSectionHeader + }}</gl-dropdown-section-header> + <gl-dropdown-item + v-for="group in groups" + :key="`${group.id}${group.name}`" + fingerprint + data-testid="group-dropdown-item" + :avatar-url="group.avatar_url" + is-check-item + :is-checked="isSelected(group)" + @click.native.capture.stop="onItemClick(group)" + > + {{ group.name }} + </gl-dropdown-item> + </template> + </gl-dropdown> +</template> diff --git a/app/assets/javascripts/groups/settings/constants.js b/app/assets/javascripts/groups/settings/constants.js new file mode 100644 index 00000000000..c91c2a20529 --- /dev/null +++ b/app/assets/javascripts/groups/settings/constants.js @@ -0,0 +1,3 @@ +export const LEVEL_TYPES = { + GROUP: 'group', +}; diff --git a/app/assets/javascripts/groups/settings/init_access_dropdown.js b/app/assets/javascripts/groups/settings/init_access_dropdown.js new file mode 100644 index 00000000000..24419280fc0 --- /dev/null +++ b/app/assets/javascripts/groups/settings/init_access_dropdown.js @@ -0,0 +1,36 @@ +import * as Sentry from '@sentry/browser'; +import Vue from 'vue'; +import AccessDropdown from './components/access_dropdown.vue'; + +export const initAccessDropdown = (el) => { + if (!el) { + return false; + } + + const { label, disabled, preselectedItems } = el.dataset; + let preselected = []; + try { + preselected = JSON.parse(preselectedItems); + } catch (e) { + Sentry.captureException(e); + } + + return new Vue({ + el, + render(createElement) { + const vm = this; + return createElement(AccessDropdown, { + props: { + preselectedItems: preselected, + label, + disabled, + }, + on: { + select(selected) { + vm.$emit('select', selected); + }, + }, + }); + }, + }); +}; |