Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-05-04 12:09:02 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-05-04 12:09:02 +0300
commit48640cf76a1ee0cd515e259d8f3eb2de25ba01c3 (patch)
tree84719b9b1f23a396298b0774ed8920cb401426d6 /app/assets/javascripts
parent7d4987ae65374a40ab540ad825da3c33d8bab6df (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/api/user_api.js12
-rw-r--r--app/assets/javascripts/boards/components/config_toggle.vue4
-rw-r--r--app/assets/javascripts/boards/index.js1
-rw-r--r--app/assets/javascripts/boards/stores/getters.js14
-rw-r--r--app/assets/javascripts/graphql_shared/possible_types.json4
-rw-r--r--app/assets/javascripts/lib/utils/users_cache.js11
-rw-r--r--app/assets/javascripts/runner/components/registration/registration_dropdown.vue2
-rw-r--r--app/assets/javascripts/user_popovers.js15
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue89
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"