diff options
Diffstat (limited to 'app/assets/javascripts/invite_members')
4 files changed, 172 insertions, 35 deletions
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 d2ea14a658b..b55ef77ae5d 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue @@ -6,13 +6,13 @@ import { GlDatepicker, GlLink, GlSprintf, - GlSearchBoxByType, GlButton, GlFormInput, } from '@gitlab/ui'; import eventHub from '../event_hub'; -import { s__, sprintf } from '~/locale'; +import { s__, __, sprintf } from '~/locale'; import Api from '~/api'; +import MembersTokenSelect from '~/invite_members/components/members_token_select.vue'; export default { name: 'InviteMembersModal', @@ -23,16 +23,20 @@ export default { GlDropdown, GlDropdownItem, GlSprintf, - GlSearchBoxByType, GlButton, GlFormInput, + MembersTokenSelect, }, props: { - groupId: { + id: { type: String, required: true, }, - groupName: { + isProject: { + type: Boolean, + required: true, + }, + name: { type: String, required: true, }, @@ -59,9 +63,16 @@ export default { }; }, computed: { + inviteToName() { + return this.name.toUpperCase(); + }, + inviteToType() { + return this.isProject ? __('project') : __('group'); + }, introText() { - return sprintf(s__("InviteMembersModal|You're inviting members to the %{group_name} group"), { - group_name: this.groupName, + return sprintf(s__("InviteMembersModal|You're inviting members to the %{name} %{type}"), { + name: this.inviteToName, + type: this.inviteToType, }); }, toastOptions() { @@ -110,13 +121,14 @@ export default { this.selectedAccessLevel = item; }, submitForm(formData) { - return Api.inviteGroupMember(this.groupId, formData) - .then(() => { - this.showToastMessageSuccess(); - }) - .catch(error => { - this.showToastMessageError(error); - }); + if (this.isProject) { + return Api.inviteProjectMembers(this.id, formData) + .then(this.showToastMessageSuccess) + .catch(this.showToastMessageError); + } + return Api.inviteGroupMember(this.id, formData) + .then(this.showToastMessageSuccess) + .catch(this.showToastMessageError); }, showToastMessageSuccess() { this.$toast.show(this.$options.labels.toastMessageSuccessful, this.toastOptions); @@ -129,44 +141,45 @@ export default { }, labels: { modalTitle: s__('InviteMembersModal|Invite team members'), - userToInvite: s__('InviteMembersModal|GitLab member or Email address'), + newUsersToInvite: s__('InviteMembersModal|GitLab member or Email address'), userPlaceholder: s__('InviteMembersModal|Search for members to invite'), accessLevel: s__('InviteMembersModal|Choose a role permission'), accessExpireDate: s__('InviteMembersModal|Access expiration date (optional)'), - toastMessageSuccessful: s__('InviteMembersModal|Users were succesfully added'), - toastMessageUnsuccessful: s__('InviteMembersModal|User not invited. Feature coming soon!'), + toastMessageSuccessful: s__('InviteMembersModal|Members were successfully added'), + toastMessageUnsuccessful: s__('InviteMembersModal|Some of the members could not be added'), readMoreText: s__(`InviteMembersModal|%{linkStart}Read more%{linkEnd} about role permissions`), inviteButtonText: s__('InviteMembersModal|Invite'), cancelButtonText: s__('InviteMembersModal|Cancel'), + headerCloseLabel: s__('InviteMembersModal|Close invite team members'), }, + membersTokenSelectLabelId: 'invite-members-input', }; </script> <template> - <gl-modal :modal-id="modalId" size="sm" :title="$options.labels.modalTitle"> + <gl-modal + :modal-id="modalId" + size="sm" + :title="$options.labels.modalTitle" + :header-close-label="$options.labels.headerCloseLabel" + > <div class="gl-ml-5 gl-mr-5"> <div>{{ introText }}</div> - <label class="gl-font-weight-bold gl-mt-5">{{ $options.labels.userToInvite }}</label> + <label :id="$options.membersTokenSelectLabelId" class="gl-font-weight-bold gl-mt-5">{{ + $options.labels.newUsersToInvite + }}</label> <div class="gl-mt-2"> - <gl-search-box-by-type + <members-token-select v-model="newUsersToInvite" + :label="$options.labels.newUsersToInvite" + :aria-labelledby="$options.membersTokenSelectLabelId" :placeholder="$options.labels.userPlaceholder" - type="text" - autocomplete="off" - autocorrect="off" - autocapitalize="off" - spellcheck="false" /> </div> <label class="gl-font-weight-bold gl-mt-5">{{ $options.labels.accessLevel }}</label> <div class="gl-mt-2 gl-w-half gl-xs-w-full"> - <gl-dropdown - menu-class="dropdown-menu-selectable" - class="gl-shadow-none gl-w-full" - v-bind="$attrs" - :text="selectedRoleName" - > + <gl-dropdown class="gl-shadow-none gl-w-full" v-bind="$attrs" :text="selectedRoleName"> <template v-for="(key, item) in accessLevels"> <gl-dropdown-item :key="key" @@ -215,9 +228,13 @@ export default { {{ $options.labels.cancelButtonText }} </gl-button> <div class="gl-mr-3"></div> - <gl-button ref="inviteButton" variant="success" @click="sendInvite">{{ - $options.labels.inviteButtonText - }}</gl-button> + <gl-button + ref="inviteButton" + :disabled="!newUsersToInvite" + variant="success" + @click="sendInvite" + >{{ $options.labels.inviteButtonText }}</gl-button + > </div> </template> </gl-modal> diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue new file mode 100644 index 00000000000..aed2e5e3236 --- /dev/null +++ b/app/assets/javascripts/invite_members/components/members_token_select.vue @@ -0,0 +1,120 @@ +<script> +import { debounce } from 'lodash'; +import { GlTokenSelector, GlAvatar, GlAvatarLabeled } from '@gitlab/ui'; +import { USER_SEARCH_DELAY } from '../constants'; +import Api from '~/api'; + +export default { + components: { + GlTokenSelector, + GlAvatar, + GlAvatarLabeled, + }, + props: { + placeholder: { + type: String, + required: false, + default: '', + }, + ariaLabelledby: { + type: String, + required: true, + }, + }, + data() { + return { + loading: false, + query: '', + users: [], + selectedTokens: [], + hasBeenFocused: false, + hideDropdownWithNoItems: true, + }; + }, + computed: { + newUsersToInvite() { + return this.selectedTokens + .map(obj => { + return obj.id; + }) + .join(','); + }, + placeholderText() { + if (this.selectedTokens.length === 0) { + return this.placeholder; + } + return ''; + }, + }, + methods: { + handleTextInput(query) { + this.hideDropdownWithNoItems = false; + this.query = query; + this.loading = true; + this.retrieveUsers(query); + }, + retrieveUsers: debounce(function debouncedRetrieveUsers() { + return Api.users(this.query, this.$options.queryOptions) + .then(response => { + this.users = response.data.map(token => ({ + id: token.id, + name: token.name, + username: token.username, + avatar_url: token.avatar_url, + })); + this.loading = false; + }) + .catch(() => { + this.loading = false; + }); + }, USER_SEARCH_DELAY), + handleInput() { + this.$emit('input', this.newUsersToInvite); + }, + handleBlur() { + this.hideDropdownWithNoItems = false; + }, + handleFocus() { + // The modal auto-focuses on the input when opened. + // This prevents the dropdown from opening when the modal opens. + if (this.hasBeenFocused) { + this.loading = true; + this.retrieveUsers(); + } + + this.hasBeenFocused = true; + }, + }, + queryOptions: { exclude_internal: true, active: true }, +}; +</script> + +<template> + <gl-token-selector + v-model="selectedTokens" + :dropdown-items="users" + :loading="loading" + :allow-user-defined-tokens="false" + :hide-dropdown-with-no-items="hideDropdownWithNoItems" + :placeholder="placeholderText" + :aria-labelledby="ariaLabelledby" + @blur="handleBlur" + @text-input="handleTextInput" + @input="handleInput" + @focus="handleFocus" + > + <template #token-content="{ token }"> + <gl-avatar v-if="token.avatar_url" :src="token.avatar_url" :size="16" /> + {{ token.name }} + </template> + + <template #dropdown-item-content="{ dropdownItem }"> + <gl-avatar-labeled + :src="dropdownItem.avatar_url" + :size="32" + :label="dropdownItem.name" + :sub-label="dropdownItem.username" + /> + </template> + </gl-token-selector> +</template> diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js new file mode 100644 index 00000000000..1ff2125c292 --- /dev/null +++ b/app/assets/javascripts/invite_members/constants.js @@ -0,0 +1 @@ +export const USER_SEARCH_DELAY = 200; 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 92aa3187fc3..db957ecacfd 100644 --- a/app/assets/javascripts/invite_members/init_invite_members_modal.js +++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js @@ -18,7 +18,6 @@ export default function initInviteMembersModal() { props: { ...el.dataset, accessLevels: JSON.parse(el.dataset.accessLevels), - groupName: el.dataset.groupName.toUpperCase(), }, }), }); |