diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-04 12:09:02 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-04 12:09:02 +0300 |
commit | 48640cf76a1ee0cd515e259d8f3eb2de25ba01c3 (patch) | |
tree | 84719b9b1f23a396298b0774ed8920cb401426d6 /app/assets/javascripts | |
parent | 7d4987ae65374a40ab540ad825da3c33d8bab6df (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts')
9 files changed, 147 insertions, 5 deletions
diff --git a/app/assets/javascripts/api/user_api.js b/app/assets/javascripts/api/user_api.js index 09995fad628..c743b18d572 100644 --- a/app/assets/javascripts/api/user_api.js +++ b/app/assets/javascripts/api/user_api.js @@ -10,6 +10,8 @@ const USER_PATH = '/api/:version/users/:id'; const USER_STATUS_PATH = '/api/:version/users/:id/status'; const USER_PROJECTS_PATH = '/api/:version/users/:id/projects'; const USER_POST_STATUS_PATH = '/api/:version/user/status'; +const USER_FOLLOW_PATH = '/api/:version/users/:id/follow'; +const USER_UNFOLLOW_PATH = '/api/:version/users/:id/unfollow'; export function getUsers(query, options) { const url = buildApiUrl(USERS_PATH); @@ -69,3 +71,13 @@ export function updateUserStatus({ emoji, message, availability, clearStatusAfte clear_status_after: clearStatusAfter, }); } + +export function followUser(userId) { + const url = buildApiUrl(USER_FOLLOW_PATH).replace(':id', encodeURIComponent(userId)); + return axios.post(url); +} + +export function unfollowUser(userId) { + const url = buildApiUrl(USER_UNFOLLOW_PATH).replace(':id', encodeURIComponent(userId)); + return axios.post(url); +} diff --git a/app/assets/javascripts/boards/components/config_toggle.vue b/app/assets/javascripts/boards/components/config_toggle.vue index 4746f598ab7..7002fd44294 100644 --- a/app/assets/javascripts/boards/components/config_toggle.vue +++ b/app/assets/javascripts/boards/components/config_toggle.vue @@ -1,5 +1,6 @@ <script> import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui'; +import { mapGetters } from 'vuex'; import { formType } from '~/boards/constants'; import eventHub from '~/boards/eventhub'; import { s__, __ } from '~/locale'; @@ -14,8 +15,9 @@ export default { GlModalDirective, }, mixins: [Tracking.mixin()], - inject: ['canAdminList', 'hasScope'], + inject: ['canAdminList'], computed: { + ...mapGetters(['hasScope']), buttonText() { return this.canAdminList ? s__('Boards|Edit board') : s__('Boards|View scope'); }, diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 77c5994b5a1..8af7da1e0aa 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -69,7 +69,6 @@ function mountBoardApp(el) { timeTrackingLimitToHours: parseBoolean(el.dataset.timeTrackingLimitToHours), issuableType: issuableTypes.issue, emailsDisabled: parseBoolean(el.dataset.emailsDisabled), - hasScope: parseBoolean(el.dataset.hasScope), hasMissingBoards: parseBoolean(el.dataset.hasMissingBoards), weights: el.dataset.weights ? JSON.parse(el.dataset.weights) : [], // Permissions diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js index cb31eb4b008..e1891a4d954 100644 --- a/app/assets/javascripts/boards/stores/getters.js +++ b/app/assets/javascripts/boards/stores/getters.js @@ -51,4 +51,18 @@ export default { isEpicBoard: () => { return false; }, + + hasScope: (state) => { + const { boardConfig } = state; + if (boardConfig.labels?.length > 0) { + return true; + } + let hasScope = false; + ['assigneeId', 'iterationCadenceId', 'iterationId', 'milestoneId', 'weight'].forEach((attr) => { + if (boardConfig[attr] !== null && boardConfig[attr] !== undefined) { + hasScope = true; + } + }); + return hasScope; + }, }; diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json index 3d6360fc4f8..7ca3f20ec1c 100644 --- a/app/assets/javascripts/graphql_shared/possible_types.json +++ b/app/assets/javascripts/graphql_shared/possible_types.json @@ -76,6 +76,10 @@ "Discussion", "Note" ], + "SecurityPolicySource": [ + "GroupSecurityPolicySource", + "ProjectSecurityPolicySource" + ], "Service": [ "BaseService", "JiraService" diff --git a/app/assets/javascripts/lib/utils/users_cache.js b/app/assets/javascripts/lib/utils/users_cache.js index 54f69ef8e1b..bd000bb26fe 100644 --- a/app/assets/javascripts/lib/utils/users_cache.js +++ b/app/assets/javascripts/lib/utils/users_cache.js @@ -35,6 +35,17 @@ class UsersCache extends Cache { // missing catch is intentional, error handling depends on use case } + updateById(userId, data) { + if (!this.hasData(userId)) { + return; + } + + this.internalStorage[userId] = { + ...this.internalStorage[userId], + ...data, + }; + } + retrieveStatusById(userId) { if (this.hasData(userId) && this.get(userId).status) { return Promise.resolve(this.get(userId).status); diff --git a/app/assets/javascripts/runner/components/registration/registration_dropdown.vue b/app/assets/javascripts/runner/components/registration/registration_dropdown.vue index 3fbe3c1be74..bb2a8ddf151 100644 --- a/app/assets/javascripts/runner/components/registration/registration_dropdown.vue +++ b/app/assets/javascripts/runner/components/registration/registration_dropdown.vue @@ -96,7 +96,7 @@ export default { <runner-instructions-modal v-if="instructionsModalOpened" ref="runnerInstructionsModal" - :registration-token="registrationToken" + :registration-token="currentRegistrationToken" data-testid="runner-instructions-modal" /> </gl-dropdown-item> diff --git a/app/assets/javascripts/user_popovers.js b/app/assets/javascripts/user_popovers.js index 4413be384e5..438ae2bc1bc 100644 --- a/app/assets/javascripts/user_popovers.js +++ b/app/assets/javascripts/user_popovers.js @@ -32,6 +32,7 @@ const populateUserInfo = (user) => { ([userData, status]) => { if (userData) { Object.assign(user, { + id: userId, avatarUrl: userData.avatar_url, bot: userData.bot, username: userData.username, @@ -42,6 +43,7 @@ const populateUserInfo = (user) => { websiteUrl: userData.website_url, pronouns: userData.pronouns, localTime: userData.local_time, + isFollowed: userData.is_followed, loaded: true, }); } @@ -97,6 +99,7 @@ export default function addPopovers(elements = document.querySelectorAll('.js-us bio: null, workInformation: null, status: null, + isFollowed: false, loaded: false, }; const renderedPopover = new UserPopoverComponent({ @@ -107,6 +110,18 @@ export default function addPopovers(elements = document.querySelectorAll('.js-us }, }); + const { userId } = el.dataset; + + renderedPopover.$on('follow', () => { + UsersCache.updateById(userId, { is_followed: true }); + user.isFollowed = true; + }); + + renderedPopover.$on('unfollow', () => { + UsersCache.updateById(userId, { is_followed: false }); + user.isFollowed = false; + }); + initializedPopovers.set(el, renderedPopover); renderedPopover.$mount(); diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue index 2c09fa71230..01a0b134b7f 100644 --- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue +++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue @@ -6,9 +6,13 @@ import { GlIcon, GlSafeHtmlDirective, GlSprintf, + GlButton, } from '@gitlab/ui'; +import { __ } from '~/locale'; import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue'; import { glEmojiTag } from '~/emoji'; +import createFlash from '~/flash'; +import { followUser, unfollowUser } from '~/rest_api'; import UserAvatarImage from '../user_avatar/user_avatar_image.vue'; const MAX_SKELETON_LINES = 4; @@ -24,6 +28,7 @@ export default { UserAvatarImage, UserNameWithStatus, GlSprintf, + GlButton, }, directives: { SafeHtml: GlSafeHtmlDirective, @@ -44,6 +49,11 @@ export default { default: 'top', }, }, + data() { + return { + toggleFollowLoading: false, + }; + }, computed: { statusHtml() { if (!this.user.status) { @@ -64,6 +74,69 @@ export default { availabilityStatus() { return this.user?.status?.availability || ''; }, + isNotCurrentUser() { + return !this.userIsLoading && this.user.username !== gon.current_username; + }, + shouldRenderToggleFollowButton() { + return ( + /* + * We're using `gon` to access feature flag because this component + * gets initialized dynamically multiple times from `user_popovers.js` + * for each user link present on the page, and using `glFeatureFlagMixin()` + * doesn't inject available feature flags into the component during init. + */ + gon?.features?.followInUserPopover && + this.isNotCurrentUser && + typeof this.user?.isFollowed !== 'undefined' + ); + }, + toggleFollowButtonText() { + if (this.toggleFollowLoading) return null; + + return this.user?.isFollowed ? __('Unfollow') : __('Follow'); + }, + toggleFollowButtonVariant() { + return this.user?.isFollowed ? 'default' : 'confirm'; + }, + }, + methods: { + async toggleFollow() { + if (this.user.isFollowed) { + this.unfollow(); + } else { + this.follow(); + } + }, + async follow() { + this.toggleFollowLoading = true; + try { + await followUser(this.user.id); + this.$emit('follow'); + } catch (error) { + createFlash({ + message: __('An error occurred while trying to follow this user, please try again.'), + error, + captureError: true, + }); + } finally { + this.toggleFollowLoading = false; + } + }, + async unfollow() { + this.toggleFollowLoading = true; + try { + await unfollowUser(this.user.id); + this.$emit('unfollow'); + } catch (error) { + createFlash({ + message: __('An error occurred while trying to unfollow this user, please try again.'), + error, + captureError: true, + }); + } finally { + this.toggleFollowLoading = false; + } + }, }, safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] }, }; @@ -73,10 +146,22 @@ export default { <!-- 200ms delay so not every mouseover triggers Popover --> <gl-popover :target="target" :delay="200" :placement="placement" boundary="viewport"> <div class="gl-p-3 gl-line-height-normal gl-display-flex" data-testid="user-popover"> - <div class="gl-p-2 flex-shrink-1"> + <div + class="gl-p-2 flex-shrink-1 gl-display-flex gl-flex-direction-column align-items-center gl-w-70p" + > <user-avatar-image :img-src="user.avatarUrl" :size="64" css-classes="gl-mr-3!" /> + <div v-if="shouldRenderToggleFollowButton" class="gl-mt-3"> + <gl-button + :variant="toggleFollowButtonVariant" + :loading="toggleFollowLoading" + size="small" + data-testid="toggle-follow-button" + @click="toggleFollow" + >{{ toggleFollowButtonText }}</gl-button + > + </div> </div> - <div class="gl-p-2 gl-w-full gl-min-w-0"> + <div class="gl-w-full gl-min-w-0"> <template v-if="userIsLoading"> <gl-skeleton-loader :lines="$options.maxSkeletonLines" |