diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-16 21:18:33 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-16 21:18:33 +0300 |
commit | f64a639bcfa1fc2bc89ca7db268f594306edfd7c (patch) | |
tree | a2c3c2ebcc3b45e596949db485d6ed18ffaacfa1 /app/assets/javascripts/invite_members | |
parent | bfbc3e0d6583ea1a91f627528bedc3d65ba4b10f (diff) |
Add latest changes from gitlab-org/gitlab@13-10-stable-eev13.10.0-rc40
Diffstat (limited to 'app/assets/javascripts/invite_members')
9 files changed, 300 insertions, 48 deletions
diff --git a/app/assets/javascripts/invite_members/components/group_select.vue b/app/assets/javascripts/invite_members/components/group_select.vue new file mode 100644 index 00000000000..4a72e97db8c --- /dev/null +++ b/app/assets/javascripts/invite_members/components/group_select.vue @@ -0,0 +1,103 @@ +<script> +import { GlDropdown, GlDropdownItem, GlDropdownText, GlSearchBoxByType } from '@gitlab/ui'; +import { debounce } from 'lodash'; +import Api from '~/api'; +import { s__ } from '~/locale'; +import { SEARCH_DELAY } from '../constants'; + +export default { + name: 'GroupSelect', + components: { + GlDropdown, + GlDropdownItem, + GlDropdownText, + GlSearchBoxByType, + }, + model: { + prop: 'selectedGroup', + }, + data() { + return { + isFetching: false, + groups: [], + selectedGroup: {}, + searchTerm: '', + }; + }, + computed: { + selectedGroupName() { + return this.selectedGroup.name || this.$options.i18n.dropdownText; + }, + isFetchResultEmpty() { + return this.groups.length === 0; + }, + }, + watch: { + searchTerm() { + this.retrieveGroups(); + }, + }, + mounted() { + this.retrieveGroups(); + }, + methods: { + retrieveGroups: debounce(function debouncedRetrieveGroups() { + this.isFetching = true; + return Api.groups(this.searchTerm, this.$options.defaultFetchOptions) + .then((response) => { + this.groups = response.map((group) => ({ + id: group.id, + name: group.full_name, + path: group.path, + })); + this.isFetching = false; + }) + .catch(() => { + this.isFetching = false; + }); + }, SEARCH_DELAY), + selectGroup(group) { + this.selectedGroup = group; + + this.$emit('input', this.selectedGroup); + }, + }, + i18n: { + dropdownText: s__('GroupSelect|Select a group'), + searchPlaceholder: s__('GroupSelect|Search groups'), + emptySearchResult: s__('GroupSelect|No matching results'), + }, + defaultFetchOptions: { + exclude_internal: true, + active: true, + }, +}; +</script> +<template> + <div> + <gl-dropdown + data-testid="group-select-dropdown" + :text="selectedGroupName" + block + menu-class="gl-w-full!" + > + <gl-search-box-by-type + v-model.trim="searchTerm" + :is-loading="isFetching" + :placeholder="$options.i18n.searchPlaceholder" + data-qa-selector="group_select_dropdown_search_field" + /> + <gl-dropdown-item + v-for="group in groups" + :key="group.id" + :name="group.name" + @click="selectGroup(group)" + > + {{ group.name }} + </gl-dropdown-item> + <gl-dropdown-text v-if="isFetchResultEmpty && !isFetching" data-testid="empty-result-message"> + <span class="gl-text-gray-500">{{ $options.i18n.emptySearchResult }}</span> + </gl-dropdown-text> + </gl-dropdown> + </div> +</template> diff --git a/app/assets/javascripts/invite_members/components/invite_group_trigger.vue b/app/assets/javascripts/invite_members/components/invite_group_trigger.vue new file mode 100644 index 00000000000..c9de078319a --- /dev/null +++ b/app/assets/javascripts/invite_members/components/invite_group_trigger.vue @@ -0,0 +1,34 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import eventHub from '../event_hub'; + +export default { + components: { + GlButton, + }, + props: { + displayText: { + type: String, + required: false, + default: s__('InviteMembers|Invite a group'), + }, + classes: { + type: String, + required: false, + default: '', + }, + }, + methods: { + openModal() { + eventHub.$emit('openModal', { inviteeType: 'group' }); + }, + }, +}; +</script> + +<template> + <gl-button :class="classes" data-qa-selector="invite_a_group_button" @click="openModal"> + {{ displayText }} + </gl-button> +</template> diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue index f5a65882fba..47f1405c980 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue @@ -11,9 +11,10 @@ import { } from '@gitlab/ui'; import { partition, isString } from 'lodash'; import Api from '~/api'; +import GroupSelect from '~/invite_members/components/group_select.vue'; import MembersTokenSelect from '~/invite_members/components/members_token_select.vue'; import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants'; -import { s__, __, sprintf } from '~/locale'; +import { s__, sprintf } from '~/locale'; import eventHub from '../event_hub'; export default { @@ -28,6 +29,7 @@ export default { GlButton, GlFormInput, MembersTokenSelect, + GroupSelect, }, props: { id: { @@ -47,7 +49,7 @@ export default { required: true, }, defaultAccessLevel: { - type: String, + type: Number, required: true, }, helpLink: { @@ -60,21 +62,21 @@ export default { visible: true, modalId: 'invite-members-modal', selectedAccessLevel: this.defaultAccessLevel, + inviteeType: 'members', newUsersToInvite: [], selectedDate: undefined, + groupToBeSharedWith: {}, }; }, computed: { - inviteToName() { - return this.name.toUpperCase(); - }, - inviteToType() { - return this.isProject ? __('project') : __('group'); + isInviteGroup() { + return this.inviteeType === 'group'; }, introText() { - return sprintf(s__("InviteMembersModal|You're inviting members to the %{name} %{type}"), { - name: this.inviteToName, - type: this.inviteToType, + const inviteTo = this.isProject ? 'toProject' : 'toGroup'; + + return sprintf(this.$options.labels[this.inviteeType][inviteTo].introText, { + name: this.name, }); }, toastOptions() { @@ -82,12 +84,12 @@ export default { onComplete: () => { this.selectedAccessLevel = this.defaultAccessLevel; this.newUsersToInvite = []; + this.groupToBeSharedWith = {}; }, }; }, basePostData() { return { - access_level: this.selectedAccessLevel, expires_at: this.selectedDate, format: 'json', }; @@ -97,9 +99,16 @@ export default { (key) => this.accessLevels[key] === Number(this.selectedAccessLevel), ); }, + inviteDisabled() { + return ( + this.newUsersToInvite.length === 0 && Object.keys(this.groupToBeSharedWith).length === 0 + ); + }, }, mounted() { - eventHub.$on('openModal', this.openModal); + eventHub.$on('openModal', (options) => { + this.openModal(options); + }); }, methods: { partitionNewUsersToInvite() { @@ -113,26 +122,42 @@ export default { usersToAddById.map((user) => user.id).join(','), ]; }, - openModal() { + openModal({ inviteeType }) { + this.inviteeType = inviteeType; + this.$root.$emit(BV_SHOW_MODAL, this.modalId); }, closeModal() { this.$root.$emit(BV_HIDE_MODAL, this.modalId); }, sendInvite() { - this.submitForm(); + if (this.isInviteGroup) { + this.submitShareWithGroup(); + } else { + this.submitInviteMembers(); + } this.closeModal(); }, cancelInvite() { this.selectedAccessLevel = this.defaultAccessLevel; this.selectedDate = undefined; - this.newUsersToInvite = ''; + this.newUsersToInvite = []; + this.groupToBeSharedWith = {}; this.closeModal(); }, changeSelectedItem(item) { this.selectedAccessLevel = item; }, - submitForm() { + submitShareWithGroup() { + const apiShareWithGroup = this.isProject + ? Api.projectShareWithGroup.bind(Api) + : Api.groupShareWithGroup.bind(Api); + + apiShareWithGroup(this.id, this.shareWithGroupPostData(this.groupToBeSharedWith.id)) + .then(this.showToastMessageSuccess) + .catch(this.showToastMessageError); + }, + submitInviteMembers() { const [usersToInviteByEmail, usersToAddById] = this.partitionNewUsersToInvite(); const promises = []; @@ -155,10 +180,25 @@ export default { Promise.all(promises).then(this.showToastMessageSuccess).catch(this.showToastMessageError); }, inviteByEmailPostData(usersToInviteByEmail) { - return { ...this.basePostData, email: usersToInviteByEmail }; + return { + ...this.basePostData, + email: usersToInviteByEmail, + access_level: this.selectedAccessLevel, + }; }, addByUserIdPostData(usersToAddById) { - return { ...this.basePostData, user_id: usersToAddById }; + return { + ...this.basePostData, + user_id: usersToAddById, + access_level: this.selectedAccessLevel, + }; + }, + shareWithGroupPostData(groupToBeSharedWith) { + return { + ...this.basePostData, + group_id: groupToBeSharedWith, + group_access: this.selectedAccessLevel, + }; }, showToastMessageSuccess() { this.$toast.show(this.$options.labels.toastMessageSuccessful, this.toastOptions); @@ -170,9 +210,36 @@ export default { }, }, labels: { - modalTitle: s__('InviteMembersModal|Invite team members'), - newUsersToInvite: s__('InviteMembersModal|GitLab member or Email address'), - userPlaceholder: s__('InviteMembersModal|Search for members to invite'), + members: { + modalTitle: s__('InviteMembersModal|Invite team members'), + searchField: s__('InviteMembersModal|GitLab member or Email address'), + placeHolder: s__('InviteMembersModal|Search for members to invite'), + toGroup: { + introText: s__( + "InviteMembersModal|You're inviting members to the %{strongStart}%{name}%{strongEnd} group.", + ), + }, + toProject: { + introText: s__( + "InviteMembersModal|You're inviting members to the %{strongStart}%{name}%{strongEnd} project.", + ), + }, + }, + group: { + modalTitle: s__('InviteMembersModal|Invite a group'), + searchField: s__('InviteMembersModal|Select a group to invite'), + placeHolder: s__('InviteMembersModal|Search for a group to invite'), + toGroup: { + introText: s__( + "InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} group.", + ), + }, + toProject: { + introText: s__( + "InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} project.", + ), + }, + }, accessLevel: s__('InviteMembersModal|Choose a role permission'), accessExpireDate: s__('InviteMembersModal|Access expiration date (optional)'), toastMessageSuccessful: s__('InviteMembersModal|Members were successfully added'), @@ -189,31 +256,45 @@ export default { <gl-modal :modal-id="modalId" size="sm" - :title="$options.labels.modalTitle" + data-qa-selector="invite_members_modal_content" + :title="$options.labels[inviteeType].modalTitle" :header-close-label="$options.labels.headerCloseLabel" > - <div class="gl-ml-5 gl-mr-5"> - <div>{{ introText }}</div> + <div> + <p ref="introText"> + <gl-sprintf :message="introText"> + <template #strong="{ content }"> + <strong>{{ content }}</strong> + </template> + </gl-sprintf> + </p> <label :id="$options.membersTokenSelectLabelId" class="gl-font-weight-bold gl-mt-5">{{ - $options.labels.newUsersToInvite + $options.labels[inviteeType].searchField }}</label> <div class="gl-mt-2"> <members-token-select + v-if="!isInviteGroup" v-model="newUsersToInvite" - :label="$options.labels.newUsersToInvite" :aria-labelledby="$options.membersTokenSelectLabelId" - :placeholder="$options.labels.userPlaceholder" + :placeholder="$options.labels[inviteeType].placeHolder" /> + <group-select v-if="isInviteGroup" v-model="groupToBeSharedWith" /> </div> - <label class="gl-font-weight-bold gl-mt-5">{{ $options.labels.accessLevel }}</label> + <label class="gl-font-weight-bold gl-mt-3">{{ $options.labels.accessLevel }}</label> <div class="gl-mt-2 gl-w-half gl-xs-w-full"> - <gl-dropdown class="gl-shadow-none gl-w-full" v-bind="$attrs" :text="selectedRoleName"> + <gl-dropdown + class="gl-shadow-none gl-w-full" + data-qa-selector="access_level_dropdown" + v-bind="$attrs" + :text="selectedRoleName" + > <template v-for="(key, item) in accessLevels"> <gl-dropdown-item :key="key" active-class="is-active" + is-check-item :is-checked="key === selectedAccessLevel" @click="changeSelectedItem(key)" > @@ -223,7 +304,7 @@ export default { </gl-dropdown> </div> - <div class="gl-mt-2"> + <div class="gl-mt-2 gl-w-half gl-xs-w-full"> <gl-sprintf :message="$options.labels.readMoreText"> <template #link="{ content }"> <gl-link :href="helpLink" target="_blank">{{ content }}</gl-link> @@ -231,7 +312,7 @@ export default { </gl-sprintf> </div> - <label class="gl-font-weight-bold gl-mt-5" for="expires_at">{{ + <label class="gl-font-weight-bold gl-mt-5 gl-display-block" for="expires_at">{{ $options.labels.accessExpireDate }}</label> <div class="gl-mt-2 gl-w-half gl-xs-w-full gl-display-inline-block"> @@ -253,15 +334,16 @@ export default { </div> <template #modal-footer> - <div class="gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-p-3"> + <div class="gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-m-0"> <gl-button ref="cancelButton" @click="cancelInvite"> {{ $options.labels.cancelButtonText }} </gl-button> <div class="gl-mr-3"></div> <gl-button ref="inviteButton" - :disabled="!newUsersToInvite" + :disabled="inviteDisabled" variant="success" + data-qa-selector="invite_button" @click="sendInvite" >{{ $options.labels.inviteButtonText }}</gl-button > diff --git a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue index eb97c458f88..666693e934f 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue @@ -1,13 +1,10 @@ <script> -import { GlLink, GlIcon } from '@gitlab/ui'; +import { GlButton } from '@gitlab/ui'; import { s__ } from '~/locale'; import eventHub from '../event_hub'; export default { - components: { - GlLink, - GlIcon, - }, + components: { GlButton }, props: { displayText: { type: String, @@ -24,20 +21,28 @@ export default { required: false, default: '', }, + variant: { + type: String, + required: false, + default: undefined, + }, }, methods: { openModal() { - eventHub.$emit('openModal'); + eventHub.$emit('openModal', { inviteeType: 'members' }); }, }, }; </script> <template> - <gl-link :class="classes" @click="openModal"> - <div v-if="icon" class="nav-icon-container"> - <gl-icon :size="16" :name="icon" /> - </div> - <span class="nav-item-name"> {{ displayText }} </span> - </gl-link> + <gl-button + :class="classes" + :icon="icon" + :variant="variant" + data-qa-selector="invite_members_button" + @click="openModal" + > + {{ displayText }} + </gl-button> </template> diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue index 233a214013b..db6a7888786 100644 --- a/app/assets/javascripts/invite_members/components/members_token_select.vue +++ b/app/assets/javascripts/invite_members/components/members_token_select.vue @@ -3,7 +3,7 @@ import { GlTokenSelector, GlAvatar, GlAvatarLabeled, GlSprintf } from '@gitlab/u import { debounce } from 'lodash'; import { __ } from '~/locale'; import { getUsers } from '~/rest_api'; -import { USER_SEARCH_DELAY } from '../constants'; +import { SEARCH_DELAY } from '../constants'; export default { components: { @@ -67,7 +67,7 @@ export default { .catch(() => { this.loading = false; }); - }, USER_SEARCH_DELAY), + }, SEARCH_DELAY), handleInput() { this.$emit('input', this.selectedTokens); }, diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js index 1ff2125c292..2044dad896f 100644 --- a/app/assets/javascripts/invite_members/constants.js +++ b/app/assets/javascripts/invite_members/constants.js @@ -1 +1 @@ -export const USER_SEARCH_DELAY = 200; +export const SEARCH_DELAY = 200; diff --git a/app/assets/javascripts/invite_members/init_invite_group_trigger.js b/app/assets/javascripts/invite_members/init_invite_group_trigger.js new file mode 100644 index 00000000000..c01bb1bae28 --- /dev/null +++ b/app/assets/javascripts/invite_members/init_invite_group_trigger.js @@ -0,0 +1,20 @@ +import Vue from 'vue'; +import InviteGroupTrigger from '~/invite_members/components/invite_group_trigger.vue'; + +export default function initInviteGroupTrigger() { + const el = document.querySelector('.js-invite-group-trigger'); + + if (!el) { + return false; + } + + return new Vue({ + el, + render: (createElement) => + createElement(InviteGroupTrigger, { + props: { + ...el.dataset, + }, + }), + }); +} diff --git a/app/assets/javascripts/invite_members/init_invite_members_form.js b/app/assets/javascripts/invite_members/init_invite_members_form.js new file mode 100644 index 00000000000..5f8688755ba --- /dev/null +++ b/app/assets/javascripts/invite_members/init_invite_members_form.js @@ -0,0 +1,7 @@ +import { disableButtonIfEmptyField } from '~/lib/utils/common_utils'; + +// This is only used when `invite_members_group_modal` feature flag is disabled. +// This file can be removed when `invite_members_group_modal` feature flag is removed +export default () => { + disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change'); +}; diff --git a/app/assets/javascripts/invite_members/init_invite_members_modal.js b/app/assets/javascripts/invite_members/init_invite_members_modal.js index 3de99dcc546..fc77bd53ba4 100644 --- a/app/assets/javascripts/invite_members/init_invite_members_modal.js +++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js @@ -20,6 +20,7 @@ export default function initInviteMembersModal() { ...el.dataset, isProject: parseBoolean(el.dataset.isProject), accessLevels: JSON.parse(el.dataset.accessLevels), + defaultAccessLevel: parseInt(el.dataset.defaultAccessLevel, 10), }, }), }); |