diff options
Diffstat (limited to 'app')
13 files changed, 173 insertions, 24 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" diff --git a/app/controllers/repositories/lfs_storage_controller.rb b/app/controllers/repositories/lfs_storage_controller.rb index 252b604dcb0..d54b51b463a 100644 --- a/app/controllers/repositories/lfs_storage_controller.rb +++ b/app/controllers/repositories/lfs_storage_controller.rb @@ -6,6 +6,8 @@ module Repositories include WorkhorseRequest include SendFileUpload + InvalidUploadedFile = Class.new(StandardError) + skip_before_action :verify_workhorse_api!, only: :download # added here as a part of the refactor, will be removed @@ -44,6 +46,8 @@ module Repositories end def upload_finalize + validate_uploaded_file! + if store_file!(oid, size) head 200, content_type: LfsRequest::CONTENT_TYPE else @@ -55,6 +59,8 @@ module Repositories render_lfs_forbidden rescue ObjectStorage::RemoteStoreError render_lfs_forbidden + rescue InvalidUploadedFile + render plain: 'SHA256 or size mismatch', status: :bad_request end private @@ -117,5 +123,13 @@ module Repositories lfs_object: object ) end + + def validate_uploaded_file! + return unless uploaded_file + + if size != uploaded_file.size || oid != uploaded_file.sha256 + raise InvalidUploadedFile + end + end end end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 456a678ce77..dbdfa0c1eab 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -97,24 +97,18 @@ module ApplicationSettingsHelper end end - def oauth_providers_checkboxes + def oauth_providers_checkboxes(form) button_based_providers.map do |source| - disabled = @application_setting.disabled_oauth_sign_in_sources.include?(source.to_s) + checked = !@application_setting.disabled_oauth_sign_in_sources.include?(source.to_s) name = Gitlab::Auth::OAuth::Provider.label_for(source) - checkbox_name = 'application_setting[enabled_oauth_sign_in_sources][]' - checkbox_id = "application_setting_enabled_oauth_sign_in_sources_#{name.parameterize(separator: '_')}" - - content_tag :div, class: 'form-check' do - check_box_tag( - checkbox_name, - source, - !disabled, - autocomplete: 'off', - id: checkbox_id, - class: 'form-check-input' - ) + - label_tag(checkbox_id, name, class: 'form-check-label') - end + + form.gitlab_ui_checkbox_component( + :enabled_oauth_sign_in_sources, + name, + checkbox_options: { checked: checked, multiple: true, autocomplete: 'off' }, + checked_value: source, + unchecked_value: nil + ) end end diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index f849f36bf84..f98e70e41d8 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -25,7 +25,6 @@ module BoardsHelper labels_manage_path: labels_manage_path, releases_fetch_path: releases_fetch_path, board_type: board.to_type, - has_scope: board.scoped?.to_s, has_missing_boards: has_missing_boards?.to_s, multiple_boards_available: multiple_boards_available?.to_s, board_base_url: board_base_url diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml index bce210d28d3..28e0ee25a5d 100644 --- a/app/views/admin/application_settings/_signin.html.haml +++ b/app/views/admin/application_settings/_signin.html.haml @@ -1,4 +1,4 @@ -= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-signin-settings'), html: { class: 'fieldset-form', id: 'signin-settings' } do |f| += gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-signin-settings'), html: { class: 'fieldset-form', id: 'signin-settings' } do |f| = form_errors(@application_setting) %fieldset @@ -23,7 +23,7 @@ %fieldset.form-group %legend.gl-font-base.gl-mb-3.gl-border-none.gl-font-weight-bold= _('Enabled OAuth authentication sources') = hidden_field_tag 'application_setting[enabled_oauth_sign_in_sources][]' - - oauth_providers_checkboxes.each do |source| + - oauth_providers_checkboxes(f).each do |source| = source .form-group = f.label :two_factor_authentication, _('Two-factor authentication'), class: 'label-bold' |