Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-02-15 21:08:59 +00:00
parent fb994e98ec
commit 367e7db836
101 changed files with 1730 additions and 463 deletions

View File

@ -42,48 +42,50 @@ rules:
no-jquery/no-serialize: error
promise/always-return: off
promise/no-callback-in-promise: off
"@gitlab/no-global-event-off": error
'@gitlab/no-global-event-off': error
import/order:
- error
- groups:
- builtin
- external
- internal
- parent
- sibling
- index
pathGroups:
- pattern: ~/**
group: internal
- pattern: emojis/**
group: internal
- pattern: "{ee_,}empty_states/**"
group: internal
- pattern: "{ee_,}icons/**"
group: internal
- pattern: "{ee_,}images/**"
group: internal
- pattern: vendor/**
group: internal
- pattern: shared_queries/**
group: internal
- pattern: "{ee_,}spec/**"
group: internal
- pattern: "{ee_,}jest/**"
group: internal
- pattern: ee_else_ce/**
group: internal
- pattern: ee/**
group: internal
- pattern: ee_component/**
group: internal
- pattern: "{test_,}helpers/**"
group: internal
- pattern: test_fixtures/**
group: internal
- error
- groups:
- builtin
- external
- internal
- parent
- sibling
- index
pathGroups:
- pattern: ~/**
group: internal
- pattern: emojis/**
group: internal
- pattern: '{ee_,}empty_states/**'
group: internal
- pattern: '{ee_,}icons/**'
group: internal
- pattern: '{ee_,}images/**'
group: internal
- pattern: vendor/**
group: internal
- pattern: shared_queries/**
group: internal
- pattern: '{ee_,}spec/**'
group: internal
- pattern: '{ee_,}jest/**'
group: internal
- pattern: ee_else_ce/**
group: internal
- pattern: ee/**
group: internal
- pattern: ee_component/**
group: internal
- pattern: '{test_,}helpers/**'
group: internal
- pattern: test_fixtures/**
group: internal
alphabetize:
order: asc
overrides:
- files:
- '**/spec/**/*'
rules:
"@gitlab/require-i18n-strings": off
"@gitlab/no-runtime-template-compiler": off
'@gitlab/require-i18n-strings': off
'@gitlab/no-runtime-template-compiler': off

View File

@ -22,27 +22,24 @@ import Heading from './nodes/heading';
import HorizontalRule from './nodes/horizontal_rule';
import Image from './nodes/image';
import Reference from './nodes/reference';
import Table from './nodes/table';
import TableHead from './nodes/table_head';
import TableBody from './nodes/table_body';
import TableHeaderRow from './nodes/table_header_row';
import TableRow from './nodes/table_row';
import TableCell from './nodes/table_cell';
import TableOfContents from './nodes/table_of_contents';
import TaskList from './nodes/task_list';
import TaskListItem from './nodes/task_list_item';
import Video from './nodes/video';
import OrderedList from './nodes/ordered_list';
import ListItem from './nodes/list_item';
import OrderedList from './nodes/ordered_list';
import OrderedTaskList from './nodes/ordered_task_list';
import Paragraph from './nodes/paragraph';
import Reference from './nodes/reference';
import Summary from './nodes/summary';
import Table from './nodes/table';
import TableBody from './nodes/table_body';
import TableCell from './nodes/table_cell';
import TableHead from './nodes/table_head';
import TableHeaderRow from './nodes/table_header_row';
import TableOfContents from './nodes/table_of_contents';
import TableRow from './nodes/table_row';
import TaskList from './nodes/task_list';
import TaskListItem from './nodes/task_list_item';
import Text from './nodes/text';
import Video from './nodes/video';
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb transform
// GitLab Flavored Markdown (GFM) to HTML.

View File

@ -1,9 +1,9 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mapActions, mapGetters } from 'vuex';
import 'ee_else_ce/boards/models/issue';
import 'ee_else_ce/boards/models/list';
import VueApollo from 'vue-apollo';
import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar';
import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown';
import boardConfigToggle from 'ee_else_ce/boards/config_toggle';
@ -15,33 +15,31 @@ import {
} from 'ee_else_ce/boards/ee_functions';
import toggleEpicsSwimlanes from 'ee_else_ce/boards/toggle_epics_swimlanes';
import toggleLabels from 'ee_else_ce/boards/toggle_labels';
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
import BoardContent from '~/boards/components/board_content.vue';
import BoardExtraActions from '~/boards/components/board_extra_actions.vue';
import { deprecatedCreateFlash as Flash } from '~/flash';
import createDefaultClient from '~/lib/graphql';
import { __ } from '~/locale';
import './models/label';
import './models/assignee';
import toggleFocusMode from '~/boards/toggle_focus';
import FilteredSearchBoards from '~/boards/filtered_search_boards';
import eventHub from '~/boards/eventhub';
import sidebarEventHub from '~/sidebar/event_hub';
import '~/boards/models/milestone';
import '~/boards/models/project';
import '~/boards/filters/due_date_filters';
import BoardAddIssuesModal from '~/boards/components/modal/index.vue';
import eventHub from '~/boards/eventhub';
import FilteredSearchBoards from '~/boards/filtered_search_boards';
import modalMixin from '~/boards/mixins/modal_mixins';
import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
import ModalStore from '~/boards/stores/modal_store';
import modalMixin from '~/boards/mixins/modal_mixins';
import '~/boards/filters/due_date_filters';
import BoardAddIssuesModal from '~/boards/components/modal/index.vue';
import toggleFocusMode from '~/boards/toggle_focus';
import { deprecatedCreateFlash as Flash } from '~/flash';
import createDefaultClient from '~/lib/graphql';
import {
NavigationType,
convertObjectPropsToCamelCase,
parseBoolean,
} from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import sidebarEventHub from '~/sidebar/event_hub';
import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher';
Vue.use(VueApollo);

View File

@ -1,13 +1,13 @@
<script>
import { GlButton, GlLoadingIcon, GlModal, GlLink } from '@gitlab/ui';
import { getParameterByName } from '~/lib/utils/common_utils';
import SvgBlankState from '~/pipelines/components/pipelines_list/blank_state.vue';
import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import eventHub from '~/pipelines/event_hub';
import PipelinesMixin from '~/pipelines/mixins/pipelines_mixin';
import PipelinesService from '~/pipelines/services/pipelines_service';
import PipelineStore from '~/pipelines/stores/pipelines_store';
import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import SvgBlankState from '~/pipelines/components/pipelines_list/blank_state.vue';
export default {
components: {

View File

@ -1,4 +1,5 @@
/* global $ */
/* eslint-disable import/order */
import jQuery from 'jquery';
import Cookies from 'js-cookie';

View File

@ -10,13 +10,13 @@ class-methods-use-this */
old_notes_spec.js is the spec for the legacy, jQuery notes application. It has nothing to do with the new, fancy Vue notes app.
*/
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import Autosize from 'autosize';
import $ from 'jquery';
import '~/lib/utils/jquery_at_who';
import Cookies from 'js-cookie';
import { escape, uniqueId } from 'lodash';
import Autosize from 'autosize';
import Vue from 'vue';
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import '~/lib/utils/jquery_at_who';
import AjaxCache from '~/lib/utils/ajax_cache';
import syntaxHighlight from '~/syntax_highlight';
import Autosave from './autosave';

View File

@ -1,11 +1,11 @@
import Vue from 'vue';
import PipelineTourSuccessModal from '~/blob/pipeline_tour_success_modal.vue';
import BlobViewer from '~/blob/viewer/index';
import GpgBadges from '~/gpg_badges';
import initBlob from '~/pages/projects/init_blob';
import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import '~/sourcegraph/load';
import PipelineTourSuccessModal from '~/blob/pipeline_tour_success_modal.vue';
document.addEventListener('DOMContentLoaded', () => {
new BlobViewer(); // eslint-disable-line no-new

View File

@ -1,6 +1,6 @@
/* eslint-disable no-new */
import $ from 'jquery';
import loadAwardsHandler from '~/awards_handler';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import Diff from '~/diff';
import flash from '~/flash';
@ -14,7 +14,6 @@ import { initCommitBoxInfo } from '~/projects/commit_box/info';
import syntaxHighlight from '~/syntax_highlight';
import ZenMode from '~/zen_mode';
import '~/sourcegraph/load';
import loadAwardsHandler from '~/awards_handler';
const hasPerfBar = document.querySelector('.with-performance-bar');
const performanceHeight = hasPerfBar ? 35 : 0;

View File

@ -141,7 +141,15 @@ export default class UserTabs {
this.loadOverviewTab();
}
const loadableActions = ['groups', 'contributed', 'projects', 'starred', 'snippets'];
const loadableActions = [
'groups',
'contributed',
'projects',
'starred',
'snippets',
'followers',
'following',
];
if (loadableActions.indexOf(action) > -1) {
this.loadTab(action, endpoint);
}

View File

@ -10,11 +10,11 @@ import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, FILTER_TAG_IDENTIFIER } from '../
import PipelinesMixin from '../../mixins/pipelines_mixin';
import PipelinesService from '../../services/pipelines_service';
import { validateParams } from '../../utils';
import SvgBlankState from './blank_state.vue';
import EmptyState from './empty_state.vue';
import NavigationControls from './nav_controls.vue';
import PipelinesFilteredSearch from './pipelines_filtered_search.vue';
import PipelinesTableComponent from './pipelines_table.vue';
import SvgBlankState from './blank_state.vue';
export default {
components: {

View File

@ -1,9 +1,9 @@
import Visibility from 'visibilityjs';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import Poll from '~/lib/utils/poll';
import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
import { validateParams } from '~/pipelines/utils';
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
import { validateParams } from '~/pipelines/utils';
import { CANCEL_REQUEST } from '../constants';
import eventHub from '../event_hub';

View File

@ -1,31 +1,92 @@
<script>
import { s__ } from '~/locale';
import { GlButton } from '@gitlab/ui';
import createFlash, { FLASH_TYPES } from '~/flash';
import { INTEGRATION_VIEW_CONFIGS, i18n } from '../constants';
import IntegrationView from './integration_view.vue';
const INTEGRATION_VIEW_CONFIGS = {
sourcegraph: {
title: s__('ProfilePreferences|Sourcegraph'),
label: s__('ProfilePreferences|Enable integrated code intelligence on code views'),
formName: 'sourcegraph_enabled',
},
gitpod: {
title: s__('ProfilePreferences|Gitpod'),
label: s__('ProfilePreferences|Enable Gitpod integration'),
formName: 'gitpod_enabled',
},
};
function updateClasses(bodyClasses = '', applicationTheme, layout) {
// Remove body class for any previous theme, re-add current one
document.body.classList.remove(...bodyClasses.split(' '));
document.body.classList.add(applicationTheme);
// Toggle container-fluid class
if (layout === 'fluid') {
document
.querySelector('.content-wrapper .container-fluid')
.classList.remove('container-limited');
} else {
document.querySelector('.content-wrapper .container-fluid').classList.add('container-limited');
}
}
export default {
name: 'ProfilePreferences',
components: {
IntegrationView,
GlButton,
},
inject: {
integrationViews: {
default: [],
},
themes: {
default: [],
},
userFields: {
default: {},
},
formEl: 'formEl',
profilePreferencesPath: 'profilePreferencesPath',
bodyClasses: 'bodyClasses',
},
integrationViewConfigs: INTEGRATION_VIEW_CONFIGS,
i18n,
data() {
return {
isSubmitEnabled: true,
};
},
computed: {
applicationThemes() {
return this.themes.reduce((themes, theme) => {
const { id, ...rest } = theme;
return { ...themes, [id]: rest };
}, {});
},
},
created() {
this.formEl.addEventListener('ajax:beforeSend', this.handleLoading);
this.formEl.addEventListener('ajax:success', this.handleSuccess);
this.formEl.addEventListener('ajax:error', this.handleError);
},
beforeDestroy() {
this.formEl.removeEventListener('ajax:beforeSend', this.handleLoading);
this.formEl.removeEventListener('ajax:success', this.handleSuccess);
this.formEl.removeEventListener('ajax:error', this.handleError);
},
methods: {
handleLoading() {
this.isSubmitEnabled = false;
},
handleSuccess(customEvent) {
const formData = new FormData(this.formEl);
updateClasses(
this.bodyClasses,
this.applicationThemes[formData.get('user[theme_id]')].css_class,
this.selectedLayout,
);
const { message = this.$options.i18n.defaultSuccess, type = FLASH_TYPES.NOTICE } =
customEvent?.detail?.[0] || {};
createFlash({ message, type });
this.isSubmitEnabled = true;
},
handleError(customEvent) {
const { message = this.$options.i18n.defaultError, type = FLASH_TYPES.ALERT } =
customEvent?.detail?.[0] || {};
createFlash({ message, type });
this.isSubmitEnabled = true;
},
},
};
</script>
@ -36,10 +97,10 @@ export default {
</div>
<div v-if="integrationViews.length" class="col-lg-4 profile-settings-sidebar">
<h4 class="gl-mt-0" data-testid="profile-preferences-integrations-heading">
{{ s__('ProfilePreferences|Integrations') }}
{{ $options.i18n.integrations }}
</h4>
<p>
{{ s__('ProfilePreferences|Customize integrations with third party services.') }}
{{ $options.i18n.integrationsDescription }}
</p>
</div>
<div v-if="integrationViews.length" class="col-lg-8">
@ -52,5 +113,19 @@ export default {
:config="$options.integrationViewConfigs[view.name]"
/>
</div>
<div class="col-lg-4 profile-settings-sidebar"></div>
<div class="col-lg-8">
<div class="form-group">
<gl-button
variant="success"
name="commit"
type="submit"
:disabled="!isSubmitEnabled"
:value="$options.i18n.saveChanges"
>
{{ $options.i18n.saveChanges }}
</gl-button>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,22 @@
import { s__, __ } from '~/locale';
export const INTEGRATION_VIEW_CONFIGS = {
sourcegraph: {
title: s__('Preferences|Sourcegraph'),
label: s__('Preferences|Enable integrated code intelligence on code views'),
formName: 'sourcegraph_enabled',
},
gitpod: {
title: s__('Preferences|Gitpod'),
label: s__('Preferences|Enable Gitpod integration'),
formName: 'gitpod_enabled',
},
};
export const i18n = {
saveChanges: __('Save changes'),
defaultSuccess: __('Preferences saved.'),
defaultError: s__('Preferences|Failed to save preferences.'),
integrations: s__('Preferences|Integrations'),
integrationsDescription: s__('Preferences|Customize integrations with third party services.'),
};

View File

@ -3,16 +3,20 @@ import ProfilePreferences from './components/profile_preferences.vue';
export default () => {
const el = document.querySelector('#js-profile-preferences-app');
const shouldParse = ['integrationViews', 'userFields'];
const formEl = document.querySelector('#profile-preferences-form');
const shouldParse = ['integrationViews', 'themes', 'userFields'];
const provide = Object.keys(el.dataset).reduce((memo, key) => {
let value = el.dataset[key];
if (shouldParse.includes(key)) {
value = JSON.parse(value);
}
const provide = Object.keys(el.dataset).reduce(
(memo, key) => {
let value = el.dataset[key];
if (shouldParse.includes(key)) {
value = JSON.parse(value);
}
return { ...memo, [key]: value };
}, {});
return { ...memo, [key]: value };
},
{ formEl },
);
return new Vue({
el,

View File

@ -1,8 +1,8 @@
<script>
/* eslint-disable vue/no-v-html */
import { GlIcon, GlLink, GlLoadingIcon } from '@gitlab/ui';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
import { GlIcon, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { handleLocationHash } from '~/lib/utils/common_utils';
import readmeQuery from '../../queries/readme.query.graphql';

View File

@ -1,5 +1,5 @@
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import { GlToast } from '@gitlab/ui';
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';

View File

@ -1,9 +1,8 @@
<script>
/* eslint-disable vue/no-v-html */
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { forEach, escape } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';

View File

@ -1,9 +1,9 @@
<script>
/* eslint-disable vue/no-v-html */
import { GlIcon } from '@gitlab/ui';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
import { unescape } from 'lodash';
import { GlIcon } from '@gitlab/ui';
import { deprecatedCreateFlash as Flash } from '~/flash';
import GLForm from '~/gl_form';
import axios from '~/lib/utils/axios_utils';

View File

@ -33,6 +33,21 @@ class DashboardController < Dashboard::ApplicationController
protected
def load_events
@events =
if params[:filter] == "followed"
load_user_events
else
load_project_events
end
Events::RenderService.new(current_user).execute(@events)
end
def load_user_events
UserRecentEventsFinder.new(current_user, current_user.followees, event_filter, params).execute
end
def load_project_events
projects =
if params[:filter] == "starred"
ProjectsFinder.new(current_user: current_user, params: { starred: true }).execute
@ -40,12 +55,10 @@ class DashboardController < Dashboard::ApplicationController
current_user.authorized_projects
end
@events = EventCollection
EventCollection
.new(projects, offset: params[:offset].to_i, filter: event_filter)
.to_a
.map(&:present)
Events::RenderService.new(current_user).execute(@events)
end
def set_show_full_reference

View File

@ -9,23 +9,18 @@ class Profiles::PreferencesController < Profiles::ApplicationController
end
def update
begin
result = Users::UpdateService.new(current_user, preferences_params.merge(user: user)).execute
result = Users::UpdateService.new(current_user, preferences_params.merge(user: user)).execute
if result[:status] == :success
message = _('Preferences saved.')
if result[:status] == :success
flash[:notice] = _('Preferences saved.')
else
flash[:alert] = _('Failed to save preferences.')
end
rescue ArgumentError => e
# Raised when `dashboard` is given an invalid value.
flash[:alert] = _("Failed to save preferences (%{error_message}).") % { error_message: e.message }
end
respond_to do |format|
format.html { redirect_to profile_preferences_path }
format.js
render json: { type: :notice, message: message }
else
render status: :bad_request, json: { type: :alert, message: _('Failed to save preferences.') }
end
rescue ArgumentError => e
# Raised when `dashboard` is given an invalid value.
message = _("Failed to save preferences (%{error_message}).") % { error_message: e.message }
render status: :bad_request, json: { type: :alert, message: message }
end
private

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true
class UsersController < ApplicationController
include InternalRedirect
include RoutableActions
include RendersMemberAccess
include RendersProjectsList
@ -13,13 +14,15 @@ class UsersController < ApplicationController
contributed: false,
snippets: true,
calendar: false,
followers: false,
following: false,
calendar_activities: true
skip_before_action :authenticate_user!
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
before_action :user, except: [:exists, :suggests, :ssh_keys]
before_action :authorize_read_user_profile!,
only: [:calendar, :calendar_activities, :groups, :projects, :contributed, :starred, :snippets]
only: [:calendar, :calendar_activities, :groups, :projects, :contributed, :starred, :snippets, :followers, :following]
feature_category :users
@ -97,6 +100,18 @@ class UsersController < ApplicationController
present_projects(@starred_projects)
end
def followers
@user_followers = user.followers.page(params[:page])
present_users(@user_followers)
end
def following
@user_following = user.followees.page(params[:page])
present_users(@user_following)
end
def present_projects(projects)
skip_pagination = Gitlab::Utils.to_boolean(params[:skip_pagination])
skip_namespace = Gitlab::Utils.to_boolean(params[:skip_namespace])
@ -146,6 +161,22 @@ class UsersController < ApplicationController
render json: { exists: exists, suggests: suggestions }
end
def follow
current_user.follow(user)
redirect_path = referer_path(request) || @user
redirect_to redirect_path
end
def unfollow
current_user.unfollow(user)
redirect_path = referer_path(request) || @user
redirect_to redirect_path
end
private
def user
@ -169,7 +200,7 @@ class UsersController < ApplicationController
end
def load_events
@events = UserRecentEventsFinder.new(current_user, user, params).execute
@events = UserRecentEventsFinder.new(current_user, user, nil, params).execute
Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end
@ -216,6 +247,17 @@ class UsersController < ApplicationController
def authorize_read_user_profile!
access_denied! unless can?(current_user, :read_user_profile, user)
end
def present_users(users)
respond_to do |format|
format.html { render 'show' }
format.json do
render json: {
html: view_to_html_string("shared/users/index", users: users)
}
end
end
end
end
UsersController.prepend_if_ee('EE::UsersController')

View File

@ -15,28 +15,50 @@ class UserRecentEventsFinder
requires_cross_project_access
attr_reader :current_user, :target_user, :params
attr_reader :current_user, :target_user, :params, :event_filter
DEFAULT_LIMIT = 20
MAX_LIMIT = 100
def initialize(current_user, target_user, params = {})
def initialize(current_user, target_user, event_filter, params = {})
@current_user = current_user
@target_user = target_user
@params = params
@event_filter = event_filter || EventFilter.new(EventFilter::ALL)
end
def execute
return Event.none unless can?(current_user, :read_user_profile, target_user)
target_events
.with_associations
.limit_recent(limit, params[:offset])
.order_created_desc
if target_user.is_a? User
execute_single
else
execute_multi
end
end
private
def execute_single
return Event.none unless can?(current_user, :read_user_profile, target_user)
event_filter.apply_filter(target_events
.with_associations
.limit_recent(limit, params[:offset])
.order_created_desc)
end
# rubocop: disable CodeReuse/ActiveRecord
def execute_multi
users = []
@target_user.each do |user|
users.append(user.id) if can?(current_user, :read_user_profile, user)
end
return Event.none if users.empty?
event_filter.apply_filter(Event.where(author: users).limit_recent(limit, params[:offset] || 0))
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def target_events
Event.where(author: target_user)

View File

@ -242,7 +242,7 @@ module UsersHelper
tabs = []
if can?(current_user, :read_user_profile, @user)
tabs += [:overview, :activity, :groups, :contributed, :projects, :starred, :snippets]
tabs += [:overview, :activity, :groups, :contributed, :projects, :starred, :snippets, :followers, :following]
end
tabs

View File

@ -116,6 +116,13 @@ class User < ApplicationRecord
has_one :user_synced_attributes_metadata, autosave: true
has_one :aws_role, class_name: 'Aws::Role'
# Followers
has_many :followed_users, foreign_key: :follower_id, class_name: 'Users::UserFollowUser'
has_many :followees, through: :followed_users
has_many :following_users, foreign_key: :followee_id, class_name: 'Users::UserFollowUser'
has_many :followers, through: :following_users
# Groups
has_many :members
has_many :group_members, -> { where(requested_at: nil).where("access_level >= ?", Gitlab::Access::GUEST) }, source: 'GroupMember'
@ -1442,6 +1449,29 @@ class User < ApplicationRecord
end
end
def following?(user)
self.followees.exists?(user.id)
end
def follow(user)
return false if self.id == user.id
begin
followee = Users::UserFollowUser.create(follower_id: self.id, followee_id: user.id)
self.followees.reset if followee.persisted?
rescue ActiveRecord::RecordNotUnique
false
end
end
def unfollow(user)
if Users::UserFollowUser.where(follower_id: self.id, followee_id: user.id).delete_all > 0
self.followees.reset
else
false
end
end
def manageable_namespaces
@manageable_namespaces ||= [namespace] + manageable_groups
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
module Users
class UserFollowUser < ApplicationRecord
belongs_to :follower, class_name: 'User'
belongs_to :followee, class_name: 'User'
end
end

View File

@ -5,7 +5,10 @@
%ul.nav-links.nav.nav-tabs
%li{ class: active_when(params[:filter].nil?) }>
= link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your projects
= _('Your projects')
%li{ class: active_when(params[:filter] == 'starred') }>
= link_to activity_dashboard_path(filter: 'starred'), data: {placement: 'right'} do
Starred projects
= _('Starred projects')
%li{ class: active_when(params[:filter] == 'followed') }>
= link_to activity_dashboard_path(filter: 'followed'), data: {placement: 'right'} do
= _('Followed users')

View File

@ -1,13 +1,14 @@
- page_title _('Preferences')
- @content_class = "limit-container-width" unless fluid_layout
- user_fields = { gitpod_enabled: @user.gitpod_enabled, sourcegraph_enabled: @user.sourcegraph_enabled }
- user_theme_id = Gitlab::Themes.for_user(@user).id
- data_attributes = { integration_views: integration_views.to_json, user_fields: user_fields.to_json }
- user_fields = { theme: user_theme_id, gitpod_enabled: @user.gitpod_enabled, sourcegraph_enabled: @user.sourcegraph_enabled }.to_json
- @themes = Gitlab::Themes::THEMES.to_json
- data_attributes = { themes: @themes, integration_views: integration_views.to_json, user_fields: user_fields, body_classes: Gitlab::Themes.body_classes, profile_preferences_path: profile_preferences_path }
- Gitlab::Themes.each do |theme|
= stylesheet_link_tag "themes/#{theme.css_filename}" if theme.css_filename
= form_for @user, url: profile_preferences_path, remote: true, method: :put do |f|
= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { id: "profile-preferences-form" } do |f|
.row.gl-mt-3.js-preferences-form.js-search-settings-section
.col-lg-4.application-theme#navigation-theme
%h4.gl-mt-0
@ -143,10 +144,4 @@
.form-text.text-muted
= s_('Preferences|For example: 30 mins ago.')
#js-profile-preferences-app{ data: data_attributes, user_fields: user_fields.to_json }
.row.gl-mt-3.js-preferences-form
.col-lg-4.profile-settings-sidebar
.col-lg-8
.form-group
= f.submit _('Save changes'), class: 'gl-button btn btn-success'
#js-profile-preferences-app{ data: data_attributes }

View File

@ -1,20 +0,0 @@
// Remove body class for any previous theme, re-add current one
$('body').removeClass('<%= Gitlab::Themes.body_classes %>')
$('body').addClass('<%= user_application_theme %>')
// Toggle container-fluid class
if ('<%= current_user.layout %>' === 'fluid') {
$('.content-wrapper .container-fluid').removeClass('container-limited')
} else {
$('.content-wrapper .container-fluid').addClass('container-limited')
}
// Re-enable the "Save" button
$('input[type=submit]').enable()
// Show flash messages
<% if flash.notice %>
new Flash({ message: '<%= flash.discard(:notice) %>', type: 'notice'})
<% elsif flash.alert %>
new Flash({ message: '<%= flash.discard(:alert) %>', type: 'alert'})
<% end %>

View File

@ -1,5 +1,6 @@
- current_user_empty_message_description = local_assigns.fetch(:current_user_empty_message_description, nil)
- secondary_button_link = local_assigns.fetch(:secondary_button_link, nil)
- primary_button_link = local_assigns.fetch(:primary_button_link, nil)
.nothing-here-block
.svg-content

View File

@ -0,0 +1,13 @@
- user = local_assigns.fetch(:user)
.col-lg-3.col-md-4.col-sm-12
.gl-card.gl-mb-5
.gl-card-body
= image_tag avatar_icon_for_user(user, 40), class: "avatar s40", alt: ''
.user-info
.block-truncated
= link_to user.name, user_path(user), class: 'user js-user-link', data: { user_id: user.id }
.block-truncated
%span.gl-text-gray-900= user.to_reference

View File

@ -0,0 +1,20 @@
- followers_illustration_path = 'illustrations/starred_empty.svg'
- followers_visitor_empty_message = s_('UserProfile|This user doesn\'t have any followers.')
- followers_current_user_empty_message_header = s_('UserProfile|You do not have any followers.')
- following_illustration_path = 'illustrations/starred_empty.svg'
- following_visitor_empty_message = s_('UserProfile|This user isn\'t following other users.')
- following_current_user_empty_message_header = s_('UserProfile|You are not following other users.')
- if users.size > 0
.row.gl-mt-3
= render partial: 'shared/users/user', collection: users, as: :user
= paginate users, theme: 'gitlab'
- else
- if @user_followers
= render partial: 'shared/empty_states/profile_tabs', locals: { illustration_path: followers_illustration_path,
visitor_empty_message: followers_visitor_empty_message,
current_user_empty_message_header: followers_current_user_empty_message_header}
- elsif @user_following
= render partial: 'shared/empty_states/profile_tabs', locals: { illustration_path: following_illustration_path,
visitor_empty_message: following_visitor_empty_message,
current_user_empty_message_header: following_current_user_empty_message_header}

View File

@ -26,6 +26,13 @@
= link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: link_classes + 'btn gl-button btn-default btn-icon',
title: s_('UserProfile|Report abuse'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= sprite_icon('error')
- if current_user && current_user.id != @user.id
- if current_user.following?(@user)
= link_to user_unfollow_path(@user, :json) , class: link_classes + 'btn gl-button btn-default', method: :post do
= _('Unfollow')
- else
= link_to user_follow_path(@user, :json) , class: link_classes + 'btn gl-button btn-default', method: :post do
= _('Follow')
- if can?(current_user, :read_user_profile, @user)
= link_to user_path(@user, rss_url_options), class: link_classes + 'btn gl-button btn-default btn-icon has-tooltip',
title: s_('UserProfile|Subscribe'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
@ -89,6 +96,16 @@
- unless @user.public_email.blank?
.profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline.mt-1.mt-sm-0
= link_to @user.public_email, "mailto:#{@user.public_email}", class: 'text-link', itemprop: 'email'
.cover-desc.gl-text-gray-900.gl-mb-2.mb-sm-2
= sprite_icon('users', css_class: 'gl-vertical-align-middle gl-text-gray-500')
.profile-link-holder.middle-dot-divider
= link_to user_followers_path, class: 'text-link' do
- count = @user.followers.count
= n_('1 follower', '%{count} followers', count) % { count: count }
.profile-link-holder.middle-dot-divider
= link_to user_following_path, class: 'text-link' do
= @user.followees.count
= _('following')
- if @user.bio.present?
.cover-desc.cgray
.profile-user-bio
@ -129,6 +146,14 @@
%li.js-snippets-tab
= link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do
= s_('UserProfile|Snippets')
- if profile_tab?(:followers)
%li.js-followers-tab
= link_to user_followers_path, data: { target: 'div#followers', action: 'followers', toggle: 'tab', endpoint: user_followers_path(format: :json) } do
= s_('UserProfile|Followers')
- if profile_tab?(:following)
%li.js-following-tab
= link_to user_following_path, data: { target: 'div#following', action: 'following', toggle: 'tab', endpoint: user_following_path(format: :json) } do
= s_('UserProfile|Following')
%div{ class: container_class }
.tab-content
@ -165,6 +190,14 @@
#snippets.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:followers)
#followers.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:following)
#following.tab-pane
-# This tab is always loaded via AJAX
.loading.hide
.spinner.spinner-md

View File

@ -0,0 +1,5 @@
---
title: Add follow each other model, API and UI(profile, activity view)
merge_request: 45451
author: Roger Meier
type: added

View File

@ -0,0 +1,5 @@
---
title: Import epic award emojis when using Bulk Import
merge_request: 53747
author:
type: added

View File

@ -46,9 +46,13 @@ scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) d
get :contributed, as: :contributed_projects
get :starred, as: :starred_projects
get :snippets
get :followers
get :following
get :exists
get :suggests
get :activity
post :follow
post :unfollow
get '/', to: redirect('%{username}'), as: nil
end
end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
class CreateUserFollowUsers < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
execute <<~SQL
CREATE TABLE user_follow_users (
follower_id integer not null references users (id) on delete cascade,
followee_id integer not null references users (id) on delete cascade,
PRIMARY KEY (follower_id, followee_id)
);
CREATE INDEX ON user_follow_users (followee_id);
SQL
end
end
def down
drop_table :user_follow_users
end
end

View File

@ -0,0 +1 @@
d6b324e808265c4ba8b6216c77b7abfa96b4b8b4c9fbd8d0a15240548526c4f3

View File

@ -17751,6 +17751,11 @@ CREATE SEQUENCE user_details_user_id_seq
ALTER SEQUENCE user_details_user_id_seq OWNED BY user_details.user_id;
CREATE TABLE user_follow_users (
follower_id integer NOT NULL,
followee_id integer NOT NULL
);
CREATE TABLE user_highest_roles (
user_id bigint NOT NULL,
updated_at timestamp with time zone NOT NULL,
@ -20917,6 +20922,9 @@ ALTER TABLE ONLY user_custom_attributes
ALTER TABLE ONLY user_details
ADD CONSTRAINT user_details_pkey PRIMARY KEY (user_id);
ALTER TABLE ONLY user_follow_users
ADD CONSTRAINT user_follow_users_pkey PRIMARY KEY (follower_id, followee_id);
ALTER TABLE ONLY user_highest_roles
ADD CONSTRAINT user_highest_roles_pkey PRIMARY KEY (user_id);
@ -23774,6 +23782,8 @@ CREATE UNIQUE INDEX uniq_pkgs_debian_project_distributions_project_id_and_suite
CREATE UNIQUE INDEX unique_merge_request_metrics_by_merge_request_id ON merge_request_metrics USING btree (merge_request_id);
CREATE INDEX user_follow_users_followee_id_idx ON user_follow_users USING btree (followee_id);
CREATE UNIQUE INDEX vulnerability_feedback_unique_idx ON vulnerability_feedback USING btree (project_id, category, feedback_type, project_fingerprint);
CREATE UNIQUE INDEX vulnerability_occurrence_pipelines_on_unique_keys ON vulnerability_occurrence_pipelines USING btree (occurrence_id, pipeline_id);
@ -26194,6 +26204,12 @@ ALTER TABLE ONLY u2f_registrations
ADD CONSTRAINT fk_u2f_registrations_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE product_analytics_events_experimental
ADD CONSTRAINT product_analytics_events_experimental_project_id_fkey FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;-- schema_migrations.version information is no longer stored in this file,
ADD CONSTRAINT product_analytics_events_experimental_project_id_fkey FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY user_follow_users
ADD CONSTRAINT user_follow_users_followee_id_fkey FOREIGN KEY (followee_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY user_follow_users
ADD CONSTRAINT user_follow_users_follower_id_fkey FOREIGN KEY (follower_id) REFERENCES users(id) ON DELETE CASCADE;-- schema_migrations.version information is no longer stored in this file,
-- but instead tracked in the db/schema_migrations directory
-- see https://gitlab.com/gitlab-org/gitlab/-/issues/218590 for details

View File

@ -274,7 +274,9 @@ Parameters:
"twitter": "",
"website_url": "",
"organization": "",
"job_title": "Operations Specialist"
"job_title": "Operations Specialist",
"followers": 1,
"following": 1
}
```
@ -685,6 +687,88 @@ Example responses
}
```
## User Follow
### Follow and unfollow users
Follow a user.
```plaintext
POST /users/:id/follow
```
Unfollow a user.
```plaintext
POST /users/:id/unfollow
```
| Attribute | Type | Required | Description |
| --------- | ------- | -------- | ---------------------------- |
| `id` | integer | yes | The ID of the user to follow |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/users/3/follow"
```
Example response:
```json
{
"id": 1,
"username": "john_smith",
"name": "John Smith",
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg",
"web_url": "http://localhost:3000/john_smith"
}
```
### Followers and following
Get the followers of a user.
```plaintext
GET /users/:id/followers
```
Get the list of users being followed.
```plaintext
GET /users/:id/following
```
| Attribute | Type | Required | Description |
| --------- | ------- | -------- | ---------------------------- |
| `id` | integer | yes | The ID of the user to follow |
```shell
curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/users/3/followers"
```
Example response:
```json
[
{
"id": 2,
"name": "Lennie Donnelly",
"username": "evette.kilback",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/7955171a55ac4997ed81e5976287890a?s=80&d=identicon",
"web_url": "http://127.0.0.1:3000/evette.kilback"
},
{
"id": 4,
"name": "Serena Bradtke",
"username": "cammy",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/a2daad869a7b60d3090b7b9bef4baf57?s=80&d=identicon",
"web_url": "http://127.0.0.1:3000/cammy"
}
]
```
## User counts
Get the counts (same as in top right menu) of the currently signed in user.

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

View File

@ -83,6 +83,14 @@ There are several types of users in GitLab:
self-managed instances' features and settings.
- [Internal users](../development/internal_users.md).
## User activity
You can follow or unfollow other users from their [user profiles](profile/index.md#user-profile).
To see their activity in the top-level Activity view, select Follow or Unfollow, and select
the Followed Users tab:
![Follow users](img/activity_followed_users_v13_9.png)
## Projects
In GitLab, you can create [projects](project/index.md) to host

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 KiB

View File

@ -41,6 +41,12 @@ On your profile page, you can see the following information:
- Personal projects: your personal projects (respecting the project's visibility level)
- Starred projects: projects you starred
- Snippets: your personal code [snippets](../snippets.md#personal-snippets)
- Followers: people following you
- Following: people you are following
Profile page with active Following view:
![Follow users](img/profile_following_v13_9.png)
## User settings

View File

@ -59,21 +59,25 @@ and edit labels.
> Showing all inherited labels [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/241990) in GitLab 13.5.
To view the project labels list, navigate to the project and click **Issues > Labels**.
The list includes all labels that are defined at the project level, as well as all
labels defined by its ancestor groups.
For each label, you can see the project or group path from where it was created.
You can filter the list by entering a search query at the top and clicking search (**{search}**).
To view a project's available labels, in the project, go to **Issues > Labels**.
Its list of labels includes both the labels defined at the project level, and
all labels defined by its ancestor groups. For each label, you can see the
project or group path from where it was created. You can filter the list by
entering a search query in the **Filter** field, and then clicking its search
icon (**{search}**).
To create a new project label:
1. Navigate to **Issues > Labels** in the project.
1. Click the **New label** button.
- Enter the title.
- (Optional) Enter a description.
- (Optional) Select a background color by clicking on the available colors, or input
a hex color value for a specific color.
1. Click **Create label** to create the label.
1. In your project, go to **Issues > Labels**.
1. Select the **New label** button.
1. In the **Title** field, enter a short, descriptive name for the label. You
can also use this field to create [scoped, mutually exclusive labels](#scoped-labels).
1. (Optional) In the **Description** field, you can enter additional
information about how and when to use this label.
1. (Optional) Select a background color for the label by selecting one of the
available colors, or by entering a hex color value in the **Background color**
field.
1. Select **Create label**.
You can also create a new project label from within an issue or merge request. In the
label section of the right sidebar of an issue or a merge request:

View File

@ -86,9 +86,10 @@ To improve your project's security, we recommend the following:
- [Enable Akismet](../../integration/akismet.md) on your GitLab instance to add spam checking to this service.
Unblocked email spam can result in many spam issues being created.
The unique internal email address is visible to all project members in your GitLab instance.
However, when using an email alias externally, an end user (issue creator) cannot see the internal
email address displayed in the information note.
The unique internal email address is visible to project members with Maintainer (or higher)
[permission level](../permissions.md)
in your GitLab instance. However, when using an email alias externally, an end user
(issue creator) cannot see the internal email address displayed in the information note.
### Using customized email templates

View File

@ -10,6 +10,12 @@ module API
expose :work_information do |user|
work_information(user)
end
expose :followers, if: ->(user, opts) { Ability.allowed?(opts[:current_user], :read_user_profile, user) } do |user|
user.followers.count
end
expose :following, if: ->(user, opts) { Ability.allowed?(opts[:current_user], :read_user_profile, user) } do |user|
user.followees.count
end
end
end
end

View File

@ -159,6 +159,68 @@ module API
present user.status || {}, with: Entities::UserStatus
end
desc 'Follow a user' do
success Entities::User
end
params do
requires :id, type: Integer, desc: 'The ID of the user'
end
post ':id/follow', feature_category: :users do
user = find_user(params[:id])
not_found!('User') unless user
if current_user.follow(user)
present user, with: Entities::UserBasic
else
not_modified!
end
end
desc 'Unfollow a user' do
success Entities::User
end
params do
requires :id, type: Integer, desc: 'The ID of the user'
end
post ':id/unfollow', feature_category: :users do
user = find_user(params[:id])
not_found!('User') unless user
if current_user.unfollow(user)
present user, with: Entities::UserBasic
else
not_modified!
end
end
desc 'Get the users who follow a user' do
success Entities::UserBasic
end
params do
requires :id, type: Integer, desc: 'The ID of the user'
use :pagination
end
get ':id/following', feature_category: :users do
user = find_user(params[:id])
not_found!('User') unless user && can?(current_user, :read_user_profile, user)
present paginate(user.followees), with: Entities::UserBasic
end
desc 'Get the followers of a user' do
success Entities::UserBasic
end
params do
requires :id, type: Integer, desc: 'The ID of the user'
use :pagination
end
get ':id/followers', feature_category: :users do
user = find_user(params[:id])
not_found!('User') unless user && can?(current_user, :read_user_profile, user)
present paginate(user.followers), with: Entities::UserBasic
end
desc 'Create a user. Available only for admins.' do
success Entities::UserWithAdmin
end

View File

@ -13,7 +13,7 @@ module BulkImports
response = client.execute(
client.parse(query.to_s),
query.variables(context.entity)
query.variables(context)
).original_hash.deep_dup
BulkImports::Pipeline::ExtractedData.new(

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
module BulkImports
module Common
module Transformers
class AwardEmojiTransformer
def initialize(*args); end
def transform(context, data)
user = find_user(context, data&.dig('user', 'public_email')) || context.current_user
data
.except('user')
.merge('user_id' => user.id)
end
private
def find_user(context, email)
return if email.blank?
context.group.users.find_by_any_email(email, confirmed: true) # rubocop: disable CodeReuse/ActiveRecord
end
end
end
end
end

View File

@ -29,8 +29,8 @@ module BulkImports
GRAPHQL
end
def variables(entity)
{ full_path: entity.source_full_path }
def variables(context)
{ full_path: context.entity.source_full_path }
end
def base_path

View File

@ -26,10 +26,10 @@ module BulkImports
GRAPHQL
end
def variables(entity)
def variables(context)
{
full_path: entity.source_full_path,
cursor: entity.next_page_for(:labels)
full_path: context.entity.source_full_path,
cursor: context.entity.next_page_for(:labels)
}
end

View File

@ -31,10 +31,10 @@ module BulkImports
GRAPHQL
end
def variables(entity)
def variables(context)
{
full_path: entity.source_full_path,
cursor: entity.next_page_for(:group_members)
full_path: context.entity.source_full_path,
cursor: context.entity.next_page_for(:group_members)
}
end

View File

@ -4,10 +4,12 @@ module BulkImports
module Pipeline
class Context
attr_reader :entity, :bulk_import
attr_accessor :extra
def initialize(entity)
def initialize(entity, extra = {})
@entity = entity
@bulk_import = entity.bulk_import
@extra = extra
end
def group

View File

@ -3,16 +3,11 @@
module Gitlab
module Database
module Migrations
Observation = Struct.new(
:migration,
:walltime,
:success
)
class Instrumentation
attr_reader :observations
def initialize
def initialize(observers = ::Gitlab::Database::Migrations::Observers.all_observers)
@observers = observers
@observations = []
end
@ -22,6 +17,8 @@ module Gitlab
exception = nil
on_each_observer { |observer| observer.before }
observation.walltime = Benchmark.realtime do
yield
rescue => e
@ -29,6 +26,9 @@ module Gitlab
observation.success = false
end
on_each_observer { |observer| observer.after }
on_each_observer { |observer| observer.record(observation) }
record_observation(observation)
raise exception if exception
@ -38,9 +38,19 @@ module Gitlab
private
attr_reader :observers
def record_observation(observation)
@observations << observation
end
def on_each_observer(&block)
observers.each do |observer|
yield observer
rescue => e
Gitlab::AppLogger.error("Migration observer #{observer.class} failed with: #{e}")
end
end
end
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
module Gitlab
module Database
module Migrations
Observation = Struct.new(
:migration,
:walltime,
:success,
:total_database_size_change
)
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module Gitlab
module Database
module Migrations
module Observers
def self.all_observers
[
TotalDatabaseSizeChange.new
]
end
end
end
end
end

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
module Gitlab
module Database
module Migrations
module Observers
class MigrationObserver
attr_reader :connection
def initialize
@connection = ActiveRecord::Base.connection
end
def before
# implement in subclass
end
def after
# implement in subclass
end
def record(observation)
raise NotImplementedError, 'implement in subclass'
end
end
end
end
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
module Gitlab
module Database
module Migrations
module Observers
class TotalDatabaseSizeChange < MigrationObserver
def before
@size_before = get_total_database_size
end
def after
@size_after = get_total_database_size
end
def record(observation)
return unless @size_after && @size_before
observation.total_database_size_change = @size_after - @size_before
end
private
def get_total_database_size
connection.execute("select pg_database_size(current_database())").first['pg_database_size']
end
end
end
end
end
end

View File

@ -1123,6 +1123,11 @@ msgid_plural "%d deploy keys"
msgstr[0] ""
msgstr[1] ""
msgid "1 follower"
msgid_plural "%{count} followers"
msgstr[0] ""
msgstr[1] ""
msgid "1 group"
msgid_plural "%d groups"
msgstr[0] ""
@ -12971,6 +12976,12 @@ msgstr ""
msgid "Folder/%{name}"
msgstr ""
msgid "Follow"
msgstr ""
msgid "Followed users"
msgstr ""
msgid "Font Color"
msgstr ""
@ -22224,21 +22235,39 @@ msgstr ""
msgid "Preferences|Choose what content you want to see on your homepage."
msgstr ""
msgid "Preferences|Customize integrations with third party services."
msgstr ""
msgid "Preferences|Customize the appearance of the application header and navigation sidebar."
msgstr ""
msgid "Preferences|Display time in 24-hour format"
msgstr ""
msgid "Preferences|Enable Gitpod integration"
msgstr ""
msgid "Preferences|Enable integrated code intelligence on code views"
msgstr ""
msgid "Preferences|Failed to save preferences."
msgstr ""
msgid "Preferences|For example: 30 mins ago."
msgstr ""
msgid "Preferences|Gitpod"
msgstr ""
msgid "Preferences|Homepage content"
msgstr ""
msgid "Preferences|Instead of all the files changed, show only one file at a time. To switch between files, use the file browser."
msgstr ""
msgid "Preferences|Integrations"
msgstr ""
msgid "Preferences|Layout width"
msgstr ""
@ -22260,6 +22289,9 @@ msgstr ""
msgid "Preferences|Show whitespace changes in diffs"
msgstr ""
msgid "Preferences|Sourcegraph"
msgstr ""
msgid "Preferences|Syntax highlighting theme"
msgstr ""
@ -22452,24 +22484,6 @@ msgstr ""
msgid "Profile Settings"
msgstr ""
msgid "ProfilePreferences|Customize integrations with third party services."
msgstr ""
msgid "ProfilePreferences|Enable Gitpod integration"
msgstr ""
msgid "ProfilePreferences|Enable integrated code intelligence on code views"
msgstr ""
msgid "ProfilePreferences|Gitpod"
msgstr ""
msgid "ProfilePreferences|Integrations"
msgstr ""
msgid "ProfilePreferences|Sourcegraph"
msgstr ""
msgid "ProfileSession|on"
msgstr ""
@ -31381,6 +31395,9 @@ msgstr ""
msgid "Unexpected error"
msgstr ""
msgid "Unfollow"
msgstr ""
msgid "Unfortunately, your email message to GitLab could not be processed."
msgstr ""
@ -32038,6 +32055,12 @@ msgstr ""
msgid "UserProfile|Explore public groups to find projects to contribute to."
msgstr ""
msgid "UserProfile|Followers"
msgstr ""
msgid "UserProfile|Following"
msgstr ""
msgid "UserProfile|Groups"
msgstr ""
@ -32080,6 +32103,9 @@ msgstr ""
msgid "UserProfile|Subscribe"
msgstr ""
msgid "UserProfile|This user doesn't have any followers."
msgstr ""
msgid "UserProfile|This user doesn't have any personal projects"
msgstr ""
@ -32095,6 +32121,9 @@ msgstr ""
msgid "UserProfile|This user is blocked"
msgstr ""
msgid "UserProfile|This user isn't following other users."
msgstr ""
msgid "UserProfile|Unconfirmed user"
msgstr ""
@ -32104,9 +32133,15 @@ msgstr ""
msgid "UserProfile|View user in admin area"
msgstr ""
msgid "UserProfile|You are not following other users."
msgstr ""
msgid "UserProfile|You can create a group for several dependent projects."
msgstr ""
msgid "UserProfile|You do not have any followers."
msgstr ""
msgid "UserProfile|You haven't created any personal projects."
msgstr ""
@ -34672,6 +34707,9 @@ msgstr[1] ""
msgid "finding is not found or is already attached to a vulnerability"
msgstr ""
msgid "following"
msgstr ""
msgid "for %{link_to_merge_request} with %{link_to_merge_request_source_branch}"
msgstr ""

View File

@ -24,7 +24,7 @@ RSpec.describe Profiles::PreferencesController do
end
describe 'PATCH update' do
def go(params: {}, format: :js)
def go(params: {}, format: :json)
params.reverse_merge!(
color_scheme_id: '1',
dashboard: 'stars',
@ -35,9 +35,12 @@ RSpec.describe Profiles::PreferencesController do
end
context 'on successful update' do
it 'sets the flash' do
it 'responds with success' do
go
expect(flash[:notice]).to eq _('Preferences saved.')
expect(response).to have_gitlab_http_status(:ok)
expect(response.parsed_body['message']).to eq _('Preferences saved.')
expect(response.parsed_body['type']).to eq('notice')
end
it "changes the user's preferences" do
@ -59,36 +62,26 @@ RSpec.describe Profiles::PreferencesController do
end
context 'on failed update' do
it 'sets the flash' do
it 'responds with error' do
expect(user).to receive(:save).and_return(false)
go
expect(flash[:alert]).to eq(_('Failed to save preferences.'))
expect(response).to have_gitlab_http_status(:bad_request)
expect(response.parsed_body['message']).to eq _('Failed to save preferences.')
expect(response.parsed_body['type']).to eq('alert')
end
end
context 'on invalid dashboard setting' do
it 'sets the flash' do
it 'responds with error' do
prefs = { dashboard: 'invalid' }
go params: prefs
expect(flash[:alert]).to match(/\AFailed to save preferences \(.+\)\.\z/)
end
end
context 'as js' do
it 'renders' do
go
expect(response).to render_template :update
end
end
context 'as html' do
it 'redirects' do
go format: :html
expect(response).to redirect_to(profile_preferences_path)
expect(response).to have_gitlab_http_status(:bad_request)
expect(response.parsed_body['message']).to match(/\AFailed to save preferences \(.+\)\.\z/)
expect(response.parsed_body['type']).to eq('alert')
end
end
end

View File

@ -9,6 +9,26 @@ RSpec.describe 'Dashboard > Activity' do
sign_in(user)
end
context 'tabs' do
it 'shows Your Projects' do
visit activity_dashboard_path
expect(find('.top-area .nav-tabs li.active')).to have_content('Your projects')
end
it 'shows Starred Projects' do
visit activity_dashboard_path(filter: 'starred')
expect(find('.top-area .nav-tabs li.active')).to have_content('Starred projects')
end
it 'shows Followed Projects' do
visit activity_dashboard_path(filter: 'followed')
expect(find('.top-area .nav-tabs li.active')).to have_content('Followed users')
end
end
context 'rss' do
before do
visit activity_dashboard_path

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'User edit preferences profile' do
RSpec.describe 'User edit preferences profile', :js do
let(:user) { create(:user) }
before do
@ -53,7 +53,14 @@ RSpec.describe 'User edit preferences profile' do
fill_in 'Tab width', with: -1
click_button 'Save changes'
expect(page).to have_content('Failed to save preferences')
field = page.find_field('user[tab_width]')
message = field.native.attribute("validationMessage")
expect(message).to eq "Value must be greater than or equal to 1."
# User trying to hack an invalid value
page.execute_script("document.querySelector('#user_tab_width').setAttribute('min', '-1')")
click_button 'Save changes'
expect(page).to have_content('Failed to save preferences.')
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'User visits the profile preferences page' do
RSpec.describe 'User visits the profile preferences page', :js do
include Select2Helper
let(:user) { create(:user) }
@ -39,7 +39,7 @@ RSpec.describe 'User visits the profile preferences page' do
describe 'User changes their default dashboard', :js do
it 'creates a flash message' do
select2('stars', from: '#user_dashboard')
click_button 'Save'
click_button 'Save changes'
wait_for_requests
@ -48,7 +48,7 @@ RSpec.describe 'User visits the profile preferences page' do
it 'updates their preference' do
select2('stars', from: '#user_dashboard')
click_button 'Save'
click_button 'Save changes'
wait_for_requests
@ -67,7 +67,7 @@ RSpec.describe 'User visits the profile preferences page' do
describe 'User changes their language', :js do
it 'creates a flash message', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/31404' do
select2('en', from: '#user_preferred_language')
click_button 'Save'
click_button 'Save changes'
wait_for_requests
@ -77,7 +77,7 @@ RSpec.describe 'User visits the profile preferences page' do
it 'updates their preference' do
wait_for_requests
select2('pt_BR', from: '#user_preferred_language')
click_button 'Save'
click_button 'Save changes'
wait_for_requests
refresh
@ -94,6 +94,8 @@ RSpec.describe 'User visits the profile preferences page' do
click_button 'Save changes'
wait_for_requests
expect(user.reload.render_whitespace_in_code).to be(true)
expect(render_whitespace_field).to be_checked
end

View File

@ -151,6 +151,132 @@ RSpec.describe 'Overview tab on a user profile', :js do
end
end
describe 'followers section' do
describe 'user has no followers' do
before do
visit user.username
page.find('.js-followers-tab a').click
wait_for_requests
end
it 'shows an empty followers list with an info message' do
page.within('#followers') do
expect(page).to have_content('You do not have any followers')
expect(page).not_to have_selector('.gl-card.gl-mb-5')
expect(page).not_to have_selector('.gl-pagination')
end
end
end
describe 'user has less then 20 followers' do
let(:follower) { create(:user) }
before do
follower.follow(user)
visit user.username
page.find('.js-followers-tab a').click
wait_for_requests
end
it 'shows followers' do
page.within('#followers') do
expect(page).to have_content(follower.name)
expect(page).to have_selector('.gl-card.gl-mb-5')
expect(page).not_to have_selector('.gl-pagination')
end
end
end
describe 'user has more then 20 followers' do
let(:other_users) { create_list(:user, 21) }
before do
other_users.each do |follower|
follower.follow(user)
end
visit user.username
page.find('.js-followers-tab a').click
wait_for_requests
end
it 'shows paginated followers' do
page.within('#followers') do
other_users.each_with_index do |follower, i|
break if i == 20
expect(page).to have_content(follower.name)
end
expect(page).to have_selector('.gl-card.gl-mb-5')
expect(page).to have_selector('.gl-pagination')
expect(page).to have_selector('.gl-pagination .js-pagination-page', count: 2)
end
end
end
end
describe 'following section' do
describe 'user is not following others' do
before do
visit user.username
page.find('.js-following-tab a').click
wait_for_requests
end
it 'shows an empty following list with an info message' do
page.within('#following') do
expect(page).to have_content('You are not following other users')
expect(page).not_to have_selector('.gl-card.gl-mb-5')
expect(page).not_to have_selector('.gl-pagination')
end
end
end
describe 'user is following less then 20 people' do
let(:followee) { create(:user) }
before do
user.follow(followee)
visit user.username
page.find('.js-following-tab a').click
wait_for_requests
end
it 'shows following user' do
page.within('#following') do
expect(page).to have_content(followee.name)
expect(page).to have_selector('.gl-card.gl-mb-5')
expect(page).not_to have_selector('.gl-pagination')
end
end
end
describe 'user is following more then 20 people' do
let(:other_users) { create_list(:user, 21) }
before do
other_users.each do |followee|
user.follow(followee)
end
visit user.username
page.find('.js-following-tab a').click
wait_for_requests
end
it 'shows paginated following' do
page.within('#following') do
other_users.each_with_index do |followee, i|
break if i == 20
expect(page).to have_content(followee.name)
end
expect(page).to have_selector('.gl-card.gl-mb-5')
expect(page).to have_selector('.gl-pagination')
expect(page).to have_selector('.gl-pagination .js-pagination-page', count: 2)
end
end
end
end
describe 'bot user' do
let(:bot_user) { create(:user, user_type: :security_bot) }

View File

@ -20,6 +20,8 @@ RSpec.describe 'User page' do
expect(page).to have_link('Contributed projects')
expect(page).to have_link('Personal projects')
expect(page).to have_link('Snippets')
expect(page).to have_link('Followers')
expect(page).to have_link('Following')
end
end
@ -54,6 +56,50 @@ RSpec.describe 'User page' do
expect(page).to have_content('GitLab - work info test')
end
end
context 'follow/unfollow and followers/following' do
let_it_be(:followee) { create(:user) }
let_it_be(:follower) { create(:user) }
it 'does not show link to follow' do
subject
expect(page).not_to have_link(text: 'Follow', class: 'gl-button')
end
it 'shows 0 followers and 0 following' do
subject
expect(page).to have_content('0 followers')
expect(page).to have_content('0 following')
end
it 'shows 1 followers and 1 following' do
follower.follow(user)
user.follow(followee)
subject
expect(page).to have_content('1 follower')
expect(page).to have_content('1 following')
end
it 'does show link to follow' do
sign_in(user)
visit user_path(followee)
expect(page).to have_link(text: 'Follow', class: 'gl-button')
end
it 'does show link to unfollow' do
sign_in(user)
user.follow(followee)
visit user_path(followee)
expect(page).to have_link(text: 'Unfollow', class: 'gl-button')
end
end
end
context 'with private profile' do
@ -83,6 +129,8 @@ RSpec.describe 'User page' do
expect(page).to have_link('Contributed projects')
expect(page).to have_link('Personal projects')
expect(page).to have_link('Snippets')
expect(page).to have_link('Followers')
expect(page).to have_link('Following')
end
end
end
@ -242,6 +290,8 @@ RSpec.describe 'User page' do
expect(page).not_to have_link('Contributed projects')
expect(page).not_to have_link('Personal projects')
expect(page).not_to have_link('Snippets')
expect(page).not_to have_link('Followers')
expect(page).not_to have_link('Following')
end
end
end
@ -261,6 +311,8 @@ RSpec.describe 'User page' do
expect(page).to have_link('Contributed projects')
expect(page).to have_link('Personal projects')
expect(page).to have_link('Snippets')
expect(page).to have_link('Followers')
expect(page).to have_link('Following')
end
end
end

View File

@ -5,16 +5,17 @@ require 'spec_helper'
RSpec.describe UserRecentEventsFinder do
let_it_be(:project_owner, reload: true) { create(:user) }
let_it_be(:current_user, reload: true) { create(:user) }
let(:private_project) { create(:project, :private, creator: project_owner) }
let(:internal_project) { create(:project, :internal, creator: project_owner) }
let(:public_project) { create(:project, :public, creator: project_owner) }
let_it_be(:private_project) { create(:project, :private, creator: project_owner) }
let_it_be(:internal_project) { create(:project, :internal, creator: project_owner) }
let_it_be(:public_project) { create(:project, :public, creator: project_owner) }
let!(:private_event) { create(:event, project: private_project, author: project_owner) }
let!(:internal_event) { create(:event, project: internal_project, author: project_owner) }
let!(:public_event) { create(:event, project: public_project, author: project_owner) }
let_it_be(:issue) { create(:issue, project: public_project) }
let(:limit) { nil }
let(:params) { { limit: limit } }
subject(:finder) { described_class.new(current_user, project_owner, params) }
subject(:finder) { described_class.new(current_user, project_owner, nil, params) }
describe '#execute' do
context 'when profile is public' do
@ -39,15 +40,106 @@ RSpec.describe UserRecentEventsFinder do
expect(finder.execute).to be_empty
end
describe 'design activity events' do
let_it_be(:event_a) { create(:design_event, author: project_owner) }
let_it_be(:event_b) { create(:design_event, author: project_owner) }
context 'events from multiple users' do
let_it_be(:second_user, reload: true) { create(:user) }
let_it_be(:private_project_second_user) { create(:project, :private, creator: second_user) }
let(:internal_project_second_user) { create(:project, :internal, creator: second_user) }
let(:public_project_second_user) { create(:project, :public, creator: second_user) }
let!(:private_event_second_user) { create(:event, project: private_project_second_user, author: second_user) }
let!(:internal_event_second_user) { create(:event, project: internal_project_second_user, author: second_user) }
let!(:public_event_second_user) { create(:event, project: public_project_second_user, author: second_user) }
it 'includes events from all users', :aggregate_failures do
events = described_class.new(current_user, [project_owner, second_user], nil, params).execute
expect(events).to include(private_event, internal_event, public_event)
expect(events).to include(private_event_second_user, internal_event_second_user, public_event_second_user)
expect(events.size).to eq(6)
end
it 'does not include events from users with private profile', :aggregate_failures do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?).with(current_user, :read_user_profile, second_user).and_return(false)
events = described_class.new(current_user, [project_owner, second_user], nil, params).execute
expect(events).to include(private_event, internal_event, public_event)
expect(events.size).to eq(3)
end
end
context 'filter activity events' do
let!(:push_event) { create(:push_event, project: public_project, author: project_owner) }
let!(:merge_event) { create(:event, :merged, project: public_project, author: project_owner) }
let!(:issue_event) { create(:event, :closed, project: public_project, target: issue, author: project_owner) }
let!(:comment_event) { create(:event, :commented, project: public_project, author: project_owner) }
let!(:wiki_event) { create(:wiki_page_event, project: public_project, author: project_owner) }
let!(:design_event) { create(:design_event, project: public_project, author: project_owner) }
let!(:team_event) { create(:event, :joined, project: public_project, author: project_owner) }
it 'includes all events', :aggregate_failures do
event_filter = EventFilter.new(EventFilter::ALL)
events = described_class.new(current_user, project_owner, event_filter, params).execute
expect(events).to include(private_event, internal_event, public_event)
expect(events).to include(push_event, merge_event, issue_event, comment_event, wiki_event, design_event, team_event)
expect(events.size).to eq(10)
end
it 'only includes push events', :aggregate_failures do
event_filter = EventFilter.new(EventFilter::PUSH)
events = described_class.new(current_user, project_owner, event_filter, params).execute
expect(events).to include(push_event)
expect(events.size).to eq(1)
end
it 'only includes merge events', :aggregate_failures do
event_filter = EventFilter.new(EventFilter::MERGED)
events = described_class.new(current_user, project_owner, event_filter, params).execute
expect(events).to include(merge_event)
expect(events.size).to eq(1)
end
it 'only includes issue events', :aggregate_failures do
event_filter = EventFilter.new(EventFilter::ISSUE)
events = described_class.new(current_user, project_owner, event_filter, params).execute
expect(events).to include(issue_event)
expect(events.size).to eq(1)
end
it 'only includes comments events', :aggregate_failures do
event_filter = EventFilter.new(EventFilter::COMMENTS)
events = described_class.new(current_user, project_owner, event_filter, params).execute
expect(events).to include(comment_event)
expect(events.size).to eq(1)
end
it 'only includes wiki events', :aggregate_failures do
event_filter = EventFilter.new(EventFilter::WIKI)
events = described_class.new(current_user, project_owner, event_filter, params).execute
expect(events).to include(wiki_event)
expect(events.size).to eq(1)
end
it 'only includes design events', :aggregate_failures do
events = finder.execute
event_filter = EventFilter.new(EventFilter::DESIGNS)
events = described_class.new(current_user, project_owner, event_filter, params).execute
expect(events).to include(event_a)
expect(events).to include(event_b)
expect(events).to include(design_event)
expect(events.size).to eq(1)
end
it 'only includes team events', :aggregate_failures do
event_filter = EventFilter.new(EventFilter::TEAM)
events = described_class.new(current_user, project_owner, event_filter, params).execute
expect(events).to include(private_event, internal_event, public_event, team_event)
expect(events.size).to eq(4)
end
end

View File

@ -1,16 +1,15 @@
/* global List */
/* global ListIssue */
import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import BoardList from '~/boards/components/board_list_deprecated.vue';
import eventHub from '~/boards/eventhub';
import '~/boards/models/issue';
import '~/boards/models/list';
import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
import axios from '~/lib/utils/axios_utils';
import '~/boards/models/issue';
import '~/boards/models/list';
import { listObj, boardsMockInterceptor } from './mock_data';
const createComponent = ({ done, listIssueProps = {}, componentProps = {}, listProps = {} }) => {

View File

@ -1,16 +1,14 @@
/* global List */
/* global ListIssue */
import MockAdapter from 'axios-mock-adapter';
import Sortable from 'sortablejs';
import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
import BoardList from '~/boards/components/board_list_deprecated.vue';
import '~/boards/models/issue';
import '~/boards/models/list';
import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
import axios from '~/lib/utils/axios_utils';
import { listObj, boardsMockInterceptor } from './mock_data';
window.Sortable = Sortable;

View File

@ -1,5 +1,5 @@
import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { listObj } from 'jest/boards/mock_data';
import BoardColumn from '~/boards/components/board_column.vue';

View File

@ -1,11 +1,11 @@
/* global ListAssignee, ListLabel, ListIssue */
import { GlLabel } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { range } from 'lodash';
import '~/boards/models/label';
import '~/boards/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
import { GlLabel } from '@gitlab/ui';
import IssueCardInner from '~/boards/components/issue_card_inner_deprecated.vue';
import store from '~/boards/stores';
import { listObj } from './mock_data';

View File

@ -2,16 +2,15 @@
/* global ListAssignee */
/* global ListIssue */
/* global ListLabel */
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import '~/boards/models/label';
import '~/boards/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
import { ListType } from '~/boards/constants';
import boardsStore from '~/boards/stores/boards_store';
import axios from '~/lib/utils/axios_utils';
import { listObj, listObjDuplicate, boardsMockInterceptor } from './mock_data';
describe('List model', () => {

View File

@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import 'vendor/jquery.endless-scroll';
import MockAdapter from 'axios-mock-adapter';
import CommitsList from '~/commits';
import axios from '~/lib/utils/axios_utils';
import Pager from '~/pager';

View File

@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { dismiss } from '~/feature_highlight/feature_highlight_helper';
import { deprecatedCreateFlash as Flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
jest.mock('~/flash');

View File

@ -1,4 +1,7 @@
import DropdownUtils from '~/filtered_search/dropdown_utils';
// TODO: Moving this line up throws an error about `FilteredSearchDropdown`
// being undefined in test. See gitlab-org/gitlab#321476 for more info.
// eslint-disable-next-line import/order
import DropdownUser from '~/filtered_search/dropdown_user';
import FilteredSearchTokenizer from '~/filtered_search/filtered_search_tokenizer';
import IssuableFilteredTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';

View File

@ -1,10 +1,9 @@
/* eslint no-param-reassign: "off" */
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import GfmAutoComplete, { membersBeforeSave } from 'ee_else_ce/gfm_auto_complete';
import { initEmojiMock } from 'helpers/emoji';
import '~/lib/utils/jquery_at_who';
import GfmAutoComplete, { membersBeforeSave } from 'ee_else_ce/gfm_auto_complete';
import { getJSONFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';

View File

@ -1,8 +1,8 @@
import Vuex from 'vuex';
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import '~/behaviors/markdown/render_gfm';
import { Range } from 'monaco-editor';
import Vue from 'vue';
import Vuex from 'vuex';
import '~/behaviors/markdown/render_gfm';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import waitForPromises from 'helpers/wait_for_promises';
import waitUsingRealTimer from 'helpers/wait_using_real_timer';

View File

@ -1,8 +1,8 @@
import $ from 'jquery';
import timezoneMock from 'timezone-mock';
import * as datetimeUtility from '~/lib/utils/datetime_utility';
import { __, s__ } from '~/locale';
import '~/commons/bootstrap';
import * as datetimeUtility from '~/lib/utils/datetime_utility';
describe('Date time utils', () => {
describe('timeFor', () => {

View File

@ -1,10 +1,10 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import Todos from '~/pages/dashboard/todos/index/todos';
import '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
import { addDelimiter } from '~/lib/utils/text_utility';
import { visitUrl } from '~/lib/utils/url_utility';
import Todos from '~/pages/dashboard/todos/index/todos';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn().mockName('visitUrl'),

View File

@ -1,67 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`IntegrationView component should render IntegrationView properly 1`] = `
<div
name="sourcegraph"
>
<label
class="label-bold"
>
Foo
</label>
<gl-link-stub
class="has-tooltip"
href="http://foo.com/help"
title="More information"
>
<gl-icon-stub
class="vertical-align-middle"
name="question-o"
size="16"
/>
</gl-link-stub>
<div
class="form-group form-check"
data-testid="profile-preferences-integration-form-group"
>
<input
data-testid="profile-preferences-integration-hidden-field"
name="user[foo_enabled]"
type="hidden"
value="0"
/>
<input
class="form-check-input"
data-testid="profile-preferences-integration-checkbox"
id="user_foo_enabled"
name="user[foo_enabled]"
type="checkbox"
value="1"
/>
<label
class="form-check-label"
for="user_foo_enabled"
>
Enable foo
</label>
<gl-form-text-stub
tag="div"
textvariant="muted"
>
<integration-help-text-stub
message="Click %{linkStart}Foo%{linkEnd}!"
messageurl="http://foo.com"
/>
</gl-form-text-stub>
</div>
</div>
`;

View File

@ -1,51 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ProfilePreferences component should render ProfilePreferences properly 1`] = `
<div
class="row gl-mt-3 js-preferences-form"
>
<div
class="col-sm-12"
>
<hr
data-testid="profile-preferences-integrations-rule"
/>
</div>
<div
class="col-lg-4 profile-settings-sidebar"
>
<h4
class="gl-mt-0"
data-testid="profile-preferences-integrations-heading"
>
Integrations
</h4>
<p>
Customize integrations with third party services.
</p>
</div>
<div
class="col-lg-8"
>
<integration-view-stub
config="[object Object]"
helplink="http://foo.com/help"
message="Click %{linkStart}Foo%{linkEnd}!"
messageurl="http://foo.com"
/>
<integration-view-stub
config="[object Object]"
helplink="http://bar.com/help"
message="Click %{linkStart}Bar%{linkEnd}!"
messageurl="http://bar.com"
/>
</div>
</div>
`;

View File

@ -115,10 +115,4 @@ describe('IntegrationView component', () => {
expect(findFormGroupLabel().text()).toBe('Enable foo');
});
it('should render IntegrationView properly', () => {
wrapper = createComponent();
expect(wrapper.element).toMatchSnapshot();
});
});

View File

@ -1,27 +1,58 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import IntegrationView from '~/profile/preferences/components/integration_view.vue';
import ProfilePreferences from '~/profile/preferences/components/profile_preferences.vue';
import { integrationViews, userFields } from '../mock_data';
import { i18n } from '~/profile/preferences/constants';
import { integrationViews, userFields, bodyClasses } from '../mock_data';
const expectedUrl = '/foo';
describe('ProfilePreferences component', () => {
let wrapper;
const defaultProvide = {
integrationViews: [],
userFields,
bodyClasses,
themes: [{ id: 1, css_class: 'foo' }],
profilePreferencesPath: '/update-profile',
formEl: document.createElement('form'),
};
function createComponent(options = {}) {
const { props = {}, provide = {} } = options;
return shallowMount(ProfilePreferences, {
provide: {
...defaultProvide,
...provide,
},
propsData: props,
});
const { props = {}, provide = {}, attachTo } = options;
return extendedWrapper(
shallowMount(ProfilePreferences, {
provide: {
...defaultProvide,
...provide,
},
propsData: props,
attachTo,
}),
);
}
function findIntegrationsDivider() {
return wrapper.findByTestId('profile-preferences-integrations-rule');
}
function findIntegrationsHeading() {
return wrapper.findByTestId('profile-preferences-integrations-heading');
}
function findSubmitButton() {
return wrapper.findComponent(GlButton);
}
function findFlashError() {
return document.querySelector('.flash-container .flash-text');
}
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
@ -30,8 +61,8 @@ describe('ProfilePreferences component', () => {
it('should not render Integrations section', () => {
wrapper = createComponent();
const views = wrapper.findAll(IntegrationView);
const divider = wrapper.find('[data-testid="profile-preferences-integrations-rule"]');
const heading = wrapper.find('[data-testid="profile-preferences-integrations-heading"]');
const divider = findIntegrationsDivider();
const heading = findIntegrationsHeading();
expect(divider.exists()).toBe(false);
expect(heading.exists()).toBe(false);
@ -40,8 +71,8 @@ describe('ProfilePreferences component', () => {
it('should render Integration section', () => {
wrapper = createComponent({ provide: { integrationViews } });
const divider = wrapper.find('[data-testid="profile-preferences-integrations-rule"]');
const heading = wrapper.find('[data-testid="profile-preferences-integrations-heading"]');
const divider = findIntegrationsDivider();
const heading = findIntegrationsHeading();
const views = wrapper.findAll(IntegrationView);
expect(divider.exists()).toBe(true);
@ -49,9 +80,84 @@ describe('ProfilePreferences component', () => {
expect(views).toHaveLength(integrationViews.length);
});
it('should render ProfilePreferences properly', () => {
wrapper = createComponent({ provide: { integrationViews } });
describe('form submit', () => {
let form;
expect(wrapper.element).toMatchSnapshot();
beforeEach(() => {
const div = document.createElement('div');
div.classList.add('container-fluid');
document.body.appendChild(div);
document.body.classList.add('content-wrapper');
form = document.createElement('form');
form.setAttribute('url', expectedUrl);
form.setAttribute('method', 'put');
const input = document.createElement('input');
input.setAttribute('name', 'user[theme_id]');
input.setAttribute('type', 'radio');
input.setAttribute('value', '1');
input.setAttribute('checked', 'checked');
form.appendChild(input);
wrapper = createComponent({ provide: { formEl: form }, attachTo: document.body });
const beforeSendEvent = new CustomEvent('ajax:beforeSend');
form.dispatchEvent(beforeSendEvent);
});
it('disables the submit button', async () => {
await wrapper.vm.$nextTick();
const button = findSubmitButton();
expect(button.props('disabled')).toBe(true);
});
it('success re-enables the submit button', async () => {
const successEvent = new CustomEvent('ajax:success');
form.dispatchEvent(successEvent);
await wrapper.vm.$nextTick();
const button = findSubmitButton();
expect(button.props('disabled')).toBe(false);
});
it('error re-enables the submit button', async () => {
const errorEvent = new CustomEvent('ajax:error');
form.dispatchEvent(errorEvent);
await wrapper.vm.$nextTick();
const button = findSubmitButton();
expect(button.props('disabled')).toBe(false);
});
it('displays the default success message', () => {
const successEvent = new CustomEvent('ajax:success');
form.dispatchEvent(successEvent);
expect(findFlashError().innerText.trim()).toEqual(i18n.defaultSuccess);
});
it('displays the custom success message', () => {
const message = 'foo';
const successEvent = new CustomEvent('ajax:success', { detail: [{ message }] });
form.dispatchEvent(successEvent);
expect(findFlashError().innerText.trim()).toEqual(message);
});
it('displays the default error message', () => {
const errorEvent = new CustomEvent('ajax:error');
form.dispatchEvent(errorEvent);
expect(findFlashError().innerText.trim()).toEqual(i18n.defaultError);
});
it('displays the custom error message', () => {
const message = 'bar';
const errorEvent = new CustomEvent('ajax:error', { detail: [{ message }] });
form.dispatchEvent(errorEvent);
expect(findFlashError().innerText.trim()).toEqual(message);
});
});
});

View File

@ -16,3 +16,5 @@ export const integrationViews = [
export const userFields = {
foo_enabled: true,
};
export const bodyClasses = 'ui-light-indigo ui-light gl-dark';

View File

@ -1,11 +1,10 @@
/* eslint-disable no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign */
import AxiosMockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import axios from '~/lib/utils/axios_utils';
import initSearchAutocomplete from '~/search_autocomplete';
import '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
describe('Search autocomplete dropdown', () => {
let widget = null;

View File

@ -1,8 +1,7 @@
import { config as testUtilsConfig } from '@vue/test-utils';
import * as jqueryMatchers from 'custom-jquery-matchers';
import Vue from 'vue';
import 'jquery';
import * as jqueryMatchers from 'custom-jquery-matchers';
import { config as testUtilsConfig } from '@vue/test-utils';
import { setGlobalDateToFakeDate } from 'helpers/fake_date';
import Translate from '~/vue_shared/translate';
import { getJSONFixture, loadHTMLFixture, setHTMLFixture } from './__helpers__/fixtures';

View File

@ -1,7 +1,7 @@
import { TEST_HOST } from 'helpers/test_constants';
import extendStore from '~/ide/stores/extend';
import { initIde } from '~/ide';
import Editor from '~/ide/lib/editor';
import extendStore from '~/ide/stores/extend';
import { IDE_DATASET } from './mock_data';
export default (container, { isRepoEmpty = false, path = '' } = {}) => {

View File

@ -1,8 +1,8 @@
import { waitForText } from 'helpers/wait_for_text';
import waitForPromises from 'helpers/wait_for_promises';
import { setTestTimeout } from 'helpers/timeout';
import { useOverclockTimers } from 'test_helpers/utils/overclock_timers';
import waitForPromises from 'helpers/wait_for_promises';
import { waitForText } from 'helpers/wait_for_text';
import { createCommitId } from 'test_helpers/factories/commit_id';
import { useOverclockTimers } from 'test_helpers/utils/overclock_timers';
import * as ideHelper from './helpers/ide_helper';
import startWebIDE from './helpers/start';

View File

@ -1,5 +1,5 @@
import { withValues } from '../utils/obj';
import { getCommit } from '../fixtures';
import { withValues } from '../utils/obj';
import { createCommitId } from './commit_id';
export const createNewCommit = ({ id = createCommitId(), message }, orig = getCommit()) => {

View File

@ -3,6 +3,7 @@
// see: https://gitlab.com/groups/gitlab-org/-/epics/895#what-if-theres-a-karma-spec-which-is-simply-unmovable-to-jest-ie-it-is-dependent-on-a-running-browser-environment
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { SIDEBAR_COLLAPSED_CLASS } from '~/contextual_sidebar';
import {
calculateTop,
showSubLevelItems,
@ -19,7 +20,6 @@ import {
setSidebar,
subItemsMouseLeave,
} from '~/fly_out_nav';
import { SIDEBAR_COLLAPSED_CLASS } from '~/contextual_sidebar';
describe('Fly out sidebar navigation', () => {
let el;

View File

@ -5,14 +5,14 @@
* https://gitlab.com/groups/gitlab-org/-/epics/895#what-if-theres-a-karma-spec-which-is-simply-unmovable-to-jest-ie-it-is-dependent-on-a-running-browser-environment
*/
import Vue from 'vue';
import { createLocalVue } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
import Dashboard from '~/monitoring/components/dashboard.vue';
import { createStore } from '~/monitoring/stores';
import axios from '~/lib/utils/axios_utils';
import { mockApiEndpoint } from '../mock_data';
import { metricsDashboardPayload, dashboardProps } from '../fixture_data';
import { mockApiEndpoint } from '../mock_data';
import { setupStoreWithData } from '../store_utils';
const localVue = createLocalVue();

View File

@ -2,19 +2,18 @@
jasmine/no-global-setup, no-underscore-dangle, no-console
*/
import { config as testUtilsConfig } from '@vue/test-utils';
import jasmineDiff from 'jasmine-diff';
import $ from 'jquery';
import 'core-js/features/set-immediate';
import 'vendor/jasmine-jquery';
import '~/commons';
import Vue from 'vue';
import jasmineDiff from 'jasmine-diff';
import { config as testUtilsConfig } from '@vue/test-utils';
import { getDefaultAdapter } from '~/lib/utils/axios_utils';
import Translate from '~/vue_shared/translate';
import { getDefaultAdapter } from '~/lib/utils/axios_utils';
import { FIXTURES_PATH, TEST_HOST } from './test_constants';
import customMatchers from './matchers';
import { FIXTURES_PATH, TEST_HOST } from './test_constants';
// Tech debt issue TBD
testUtilsConfig.logModifiedComponents = false;

View File

@ -0,0 +1,48 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe BulkImports::Common::Transformers::AwardEmojiTransformer do
describe '#transform' do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:bulk_import) { create(:bulk_import) }
let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import, group: group) }
let_it_be(:context) { BulkImports::Pipeline::Context.new(entity) }
let(:hash) do
{
'name' => 'thumbs up',
'user' => {
'public_email' => email
}
}
end
before do
group.add_developer(user)
end
shared_examples 'sets user_id and removes user key' do
it 'sets found user_id and removes user key' do
transformed_hash = subject.transform(context, hash)
expect(transformed_hash['user']).to be_nil
expect(transformed_hash['user_id']).to eq(user.id)
end
end
context 'when user can be found by email' do
let(:email) { user.email }
include_examples 'sets user_id and removes user key'
end
context 'when user cannot be found by email' do
let(:user) { bulk_import.user }
let(:email) { nil }
include_examples 'sets user_id and removes user key'
end
end
end

View File

@ -4,12 +4,13 @@ require 'spec_helper'
RSpec.describe BulkImports::Groups::Graphql::GetGroupQuery do
describe '#variables' do
let(:entity) { double(source_full_path: 'test') }
let(:entity) { double(source_full_path: 'test', bulk_import: nil) }
let(:context) { BulkImports::Pipeline::Context.new(entity) }
it 'returns query variables based on entity information' do
expected = { full_path: entity.source_full_path }
expect(described_class.variables(entity)).to eq(expected)
expect(described_class.variables(context)).to eq(expected)
end
end

View File

@ -4,12 +4,13 @@ require 'spec_helper'
RSpec.describe BulkImports::Groups::Graphql::GetLabelsQuery do
describe '#variables' do
let(:entity) { double(source_full_path: 'test', next_page_for: 'next_page') }
let(:entity) { double(source_full_path: 'test', next_page_for: 'next_page', bulk_import: nil) }
let(:context) { BulkImports::Pipeline::Context.new(entity) }
it 'returns query variables based on entity information' do
expected = { full_path: entity.source_full_path, cursor: entity.next_page_for }
expect(described_class.variables(entity)).to eq(expected)
expect(described_class.variables(context)).to eq(expected)
end
end

View File

@ -5,11 +5,12 @@ require 'spec_helper'
RSpec.describe BulkImports::Groups::Graphql::GetMembersQuery do
it 'has a valid query' do
entity = create(:bulk_import_entity)
context = BulkImports::Pipeline::Context.new(entity)
query = GraphQL::Query.new(
GitlabSchema,
described_class.to_s,
variables: described_class.variables(entity)
variables: described_class.variables(context)
)
result = GitlabSchema.static_validator.validate(query)

View File

@ -4,8 +4,9 @@ require 'spec_helper'
RSpec.describe BulkImports::Importers::GroupImporter do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:bulk_import) { create(:bulk_import) }
let(:bulk_import_entity) { create(:bulk_import_entity, :started, bulk_import: bulk_import) }
let(:bulk_import_entity) { create(:bulk_import_entity, :started, bulk_import: bulk_import, group: group) }
let(:bulk_import_configuration) { create(:bulk_import_configuration, bulk_import: bulk_import) }
let(:context) { BulkImports::Pipeline::Context.new(bulk_import_entity) }
@ -21,7 +22,11 @@ RSpec.describe BulkImports::Importers::GroupImporter do
expect_to_run_pipeline BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline, context: context
expect_to_run_pipeline BulkImports::Groups::Pipelines::MembersPipeline, context: context
expect_to_run_pipeline BulkImports::Groups::Pipelines::LabelsPipeline, context: context
expect_to_run_pipeline('EE::BulkImports::Groups::Pipelines::EpicsPipeline'.constantize, context: context) if Gitlab.ee?
if Gitlab.ee?
expect_to_run_pipeline('EE::BulkImports::Groups::Pipelines::EpicsPipeline'.constantize, context: context)
expect_to_run_pipeline('EE::BulkImports::Groups::Pipelines::EpicAwardEmojiPipeline'.constantize, context: context)
end
subject.execute
@ -29,7 +34,7 @@ RSpec.describe BulkImports::Importers::GroupImporter do
end
context 'when failed' do
let(:bulk_import_entity) { create(:bulk_import_entity, :failed, bulk_import: bulk_import) }
let(:bulk_import_entity) { create(:bulk_import_entity, :failed, bulk_import: bulk_import, group: group) }
it 'does not transition entity to finished state' do
allow(bulk_import_entity).to receive(:start!)

View File

@ -11,6 +11,41 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do
expect { |b| subject.observe(migration, &b) }.to yield_control
end
context 'behavior with observers' do
subject { described_class.new(observers).observe(migration) {} }
let(:observers) { [observer] }
let(:observer) { instance_double('Gitlab::Database::Migrations::Observers::MigrationObserver', before: nil, after: nil, record: nil) }
it 'calls #before, #after, #record on given observers' do
expect(observer).to receive(:before).ordered
expect(observer).to receive(:after).ordered
expect(observer).to receive(:record).ordered do |observation|
expect(observation.migration).to eq(migration)
end
subject
end
it 'ignores errors coming from observers #before' do
expect(observer).to receive(:before).and_raise('some error')
subject
end
it 'ignores errors coming from observers #after' do
expect(observer).to receive(:after).and_raise('some error')
subject
end
it 'ignores errors coming from observers #record' do
expect(observer).to receive(:record).and_raise('some error')
subject
end
end
context 'on successful execution' do
subject { described_class.new.observe(migration) {} }

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Database::Migrations::Observers::TotalDatabaseSizeChange do
subject { described_class.new }
let(:observation) { Gitlab::Database::Migrations::Observation.new }
let(:connection) { ActiveRecord::Base.connection }
let(:query) { 'select pg_database_size(current_database())' }
it 'records the size change' do
expect(connection).to receive(:execute).with(query).once.and_return([{ 'pg_database_size' => 1024 }])
expect(connection).to receive(:execute).with(query).once.and_return([{ 'pg_database_size' => 256 }])
subject.before
subject.after
subject.record(observation)
expect(observation.total_database_size_change).to eq(256 - 1024)
end
context 'out of order calls' do
before do
allow(connection).to receive(:execute).with(query).and_return([{ 'pg_database_size' => 1024 }])
end
it 'does not record anything if before size is unknown' do
subject.after
expect { subject.record(observation) }.not_to change { observation.total_database_size_change }
end
it 'does not record anything if after size is unknown' do
subject.before
expect { subject.record(observation) }.not_to change { observation.total_database_size_change }
end
end
end

View File

@ -2831,6 +2831,79 @@ RSpec.describe User do
end
end
describe '#following?' do
it 'check if following another user' do
user = create :user
followee1 = create :user
expect(user.follow(followee1)).to be_truthy
expect(user.following?(followee1)).to be_truthy
expect(user.unfollow(followee1)).to be_truthy
expect(user.following?(followee1)).to be_falsey
end
end
describe '#follow' do
it 'follow another user' do
user = create :user
followee1 = create :user
followee2 = create :user
expect(user.followees).to be_empty
expect(user.follow(followee1)).to be_truthy
expect(user.follow(followee1)).to be_falsey
expect(user.followees).to contain_exactly(followee1)
expect(user.follow(followee2)).to be_truthy
expect(user.follow(followee2)).to be_falsey
expect(user.followees).to contain_exactly(followee1, followee2)
end
it 'follow itself is not possible' do
user = create :user
expect(user.followees).to be_empty
expect(user.follow(user)).to be_falsey
expect(user.followees).to be_empty
end
end
describe '#unfollow' do
it 'unfollow another user' do
user = create :user
followee1 = create :user
followee2 = create :user
expect(user.followees).to be_empty
expect(user.follow(followee1)).to be_truthy
expect(user.follow(followee1)).to be_falsey
expect(user.follow(followee2)).to be_truthy
expect(user.follow(followee2)).to be_falsey
expect(user.followees).to contain_exactly(followee1, followee2)
expect(user.unfollow(followee1)).to be_truthy
expect(user.unfollow(followee1)).to be_falsey
expect(user.followees).to contain_exactly(followee2)
expect(user.unfollow(followee2)).to be_truthy
expect(user.unfollow(followee2)).to be_falsey
expect(user.followees).to be_empty
end
end
describe '.find_by_private_commit_email' do
context 'with email' do
let_it_be(:user) { create(:user) }

View File

@ -652,6 +652,34 @@ RSpec.describe API::Users do
expect(response).to match_response_schema('public_api/v4/user/basic')
expect(json_response.keys).not_to include 'created_at'
end
it "returns the `followers` field for public users" do
get api("/users/#{user.id}")
expect(response).to match_response_schema('public_api/v4/user/basic')
expect(json_response.keys).to include 'followers'
end
it "does not return the `followers` field for private users" do
get api("/users/#{private_user.id}")
expect(response).to match_response_schema('public_api/v4/user/basic')
expect(json_response.keys).not_to include 'followers'
end
it "returns the `following` field for public users" do
get api("/users/#{user.id}")
expect(response).to match_response_schema('public_api/v4/user/basic')
expect(json_response.keys).to include 'following'
end
it "does not return the `following` field for private users" do
get api("/users/#{private_user.id}")
expect(response).to match_response_schema('public_api/v4/user/basic')
expect(json_response.keys).not_to include 'following'
end
end
it "returns a 404 error if user id not found" do
@ -688,6 +716,128 @@ RSpec.describe API::Users do
end
end
describe 'POST /users/:id/follow' do
let(:followee) { create(:user) }
context 'on an unfollowed user' do
it 'follows the user' do
post api("/users/#{followee.id}/follow", user)
expect(user.followees).to contain_exactly(followee)
expect(response).to have_gitlab_http_status(:created)
end
end
context 'on a followed user' do
before do
user.follow(followee)
end
it 'does not change following' do
post api("/users/#{followee.id}/follow", user)
expect(user.followees).to contain_exactly(followee)
expect(response).to have_gitlab_http_status(:not_modified)
end
end
end
describe 'POST /users/:id/unfollow' do
let(:followee) { create(:user) }
context 'on a followed user' do
before do
user.follow(followee)
end
it 'unfollow the user' do
post api("/users/#{followee.id}/unfollow", user)
expect(user.followees).to be_empty
expect(response).to have_gitlab_http_status(:created)
end
end
context 'on an unfollowed user' do
it 'does not change following' do
post api("/users/#{followee.id}/unfollow", user)
expect(user.followees).to be_empty
expect(response).to have_gitlab_http_status(:not_modified)
end
end
end
describe 'GET /users/:id/followers' do
let(:follower) { create(:user) }
context 'user has followers' do
it 'lists followers' do
follower.follow(user)
get api("/users/#{user.id}/followers", user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
end
it 'do not lists followers if profile is private' do
follower.follow(private_user)
get api("/users/#{private_user.id}/followers", user)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 User Not Found')
end
end
context 'user does not have any follower' do
it 'does list nothing' do
get api("/users/#{user.id}/followers", user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_empty
end
end
end
describe 'GET /users/:id/following' do
let(:followee) { create(:user) }
context 'user has followers' do
it 'lists following user' do
user.follow(followee)
get api("/users/#{user.id}/following", user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
end
it 'do not lists following user if profile is private' do
user.follow(private_user)
get api("/users/#{private_user.id}/following", user)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 User Not Found')
end
end
context 'user does not have any follower' do
it 'does list nothing' do
get api("/users/#{user.id}/following", user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_empty
end
end
end
describe "POST /users" do
it "creates user" do
expect do

Some files were not shown because too many files have changed in this diff Show More