Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
fb994e98ec
commit
367e7db836
|
@ -42,48 +42,50 @@ rules:
|
||||||
no-jquery/no-serialize: error
|
no-jquery/no-serialize: error
|
||||||
promise/always-return: off
|
promise/always-return: off
|
||||||
promise/no-callback-in-promise: off
|
promise/no-callback-in-promise: off
|
||||||
"@gitlab/no-global-event-off": error
|
'@gitlab/no-global-event-off': error
|
||||||
import/order:
|
import/order:
|
||||||
- error
|
- error
|
||||||
- groups:
|
- groups:
|
||||||
- builtin
|
- builtin
|
||||||
- external
|
- external
|
||||||
- internal
|
- internal
|
||||||
- parent
|
- parent
|
||||||
- sibling
|
- sibling
|
||||||
- index
|
- index
|
||||||
pathGroups:
|
pathGroups:
|
||||||
- pattern: ~/**
|
- pattern: ~/**
|
||||||
group: internal
|
group: internal
|
||||||
- pattern: emojis/**
|
- pattern: emojis/**
|
||||||
group: internal
|
group: internal
|
||||||
- pattern: "{ee_,}empty_states/**"
|
- pattern: '{ee_,}empty_states/**'
|
||||||
group: internal
|
group: internal
|
||||||
- pattern: "{ee_,}icons/**"
|
- pattern: '{ee_,}icons/**'
|
||||||
group: internal
|
group: internal
|
||||||
- pattern: "{ee_,}images/**"
|
- pattern: '{ee_,}images/**'
|
||||||
group: internal
|
group: internal
|
||||||
- pattern: vendor/**
|
- pattern: vendor/**
|
||||||
group: internal
|
group: internal
|
||||||
- pattern: shared_queries/**
|
- pattern: shared_queries/**
|
||||||
group: internal
|
group: internal
|
||||||
- pattern: "{ee_,}spec/**"
|
- pattern: '{ee_,}spec/**'
|
||||||
group: internal
|
group: internal
|
||||||
- pattern: "{ee_,}jest/**"
|
- pattern: '{ee_,}jest/**'
|
||||||
group: internal
|
group: internal
|
||||||
- pattern: ee_else_ce/**
|
- pattern: ee_else_ce/**
|
||||||
group: internal
|
group: internal
|
||||||
- pattern: ee/**
|
- pattern: ee/**
|
||||||
group: internal
|
group: internal
|
||||||
- pattern: ee_component/**
|
- pattern: ee_component/**
|
||||||
group: internal
|
group: internal
|
||||||
- pattern: "{test_,}helpers/**"
|
- pattern: '{test_,}helpers/**'
|
||||||
group: internal
|
group: internal
|
||||||
- pattern: test_fixtures/**
|
- pattern: test_fixtures/**
|
||||||
group: internal
|
group: internal
|
||||||
|
alphabetize:
|
||||||
|
order: asc
|
||||||
overrides:
|
overrides:
|
||||||
- files:
|
- files:
|
||||||
- '**/spec/**/*'
|
- '**/spec/**/*'
|
||||||
rules:
|
rules:
|
||||||
"@gitlab/require-i18n-strings": off
|
'@gitlab/require-i18n-strings': off
|
||||||
"@gitlab/no-runtime-template-compiler": off
|
'@gitlab/no-runtime-template-compiler': off
|
||||||
|
|
|
@ -22,27 +22,24 @@ import Heading from './nodes/heading';
|
||||||
import HorizontalRule from './nodes/horizontal_rule';
|
import HorizontalRule from './nodes/horizontal_rule';
|
||||||
import Image from './nodes/image';
|
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 ListItem from './nodes/list_item';
|
||||||
|
import OrderedList from './nodes/ordered_list';
|
||||||
import OrderedTaskList from './nodes/ordered_task_list';
|
import OrderedTaskList from './nodes/ordered_task_list';
|
||||||
import Paragraph from './nodes/paragraph';
|
import Paragraph from './nodes/paragraph';
|
||||||
|
import Reference from './nodes/reference';
|
||||||
import Summary from './nodes/summary';
|
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 Text from './nodes/text';
|
||||||
|
import Video from './nodes/video';
|
||||||
|
|
||||||
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb transform
|
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb transform
|
||||||
// GitLab Flavored Markdown (GFM) to HTML.
|
// GitLab Flavored Markdown (GFM) to HTML.
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
import VueApollo from 'vue-apollo';
|
||||||
import { mapActions, mapGetters } from 'vuex';
|
import { mapActions, mapGetters } from 'vuex';
|
||||||
|
|
||||||
import 'ee_else_ce/boards/models/issue';
|
import 'ee_else_ce/boards/models/issue';
|
||||||
import 'ee_else_ce/boards/models/list';
|
import 'ee_else_ce/boards/models/list';
|
||||||
import VueApollo from 'vue-apollo';
|
|
||||||
import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar';
|
import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar';
|
||||||
import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown';
|
import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown';
|
||||||
import boardConfigToggle from 'ee_else_ce/boards/config_toggle';
|
import boardConfigToggle from 'ee_else_ce/boards/config_toggle';
|
||||||
|
@ -15,33 +15,31 @@ import {
|
||||||
} from 'ee_else_ce/boards/ee_functions';
|
} from 'ee_else_ce/boards/ee_functions';
|
||||||
import toggleEpicsSwimlanes from 'ee_else_ce/boards/toggle_epics_swimlanes';
|
import toggleEpicsSwimlanes from 'ee_else_ce/boards/toggle_epics_swimlanes';
|
||||||
import toggleLabels from 'ee_else_ce/boards/toggle_labels';
|
import toggleLabels from 'ee_else_ce/boards/toggle_labels';
|
||||||
|
|
||||||
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
|
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
|
||||||
import BoardContent from '~/boards/components/board_content.vue';
|
import BoardContent from '~/boards/components/board_content.vue';
|
||||||
import BoardExtraActions from '~/boards/components/board_extra_actions.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/label';
|
||||||
import './models/assignee';
|
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/milestone';
|
||||||
import '~/boards/models/project';
|
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 store from '~/boards/stores';
|
||||||
import boardsStore from '~/boards/stores/boards_store';
|
import boardsStore from '~/boards/stores/boards_store';
|
||||||
import ModalStore from '~/boards/stores/modal_store';
|
import ModalStore from '~/boards/stores/modal_store';
|
||||||
import modalMixin from '~/boards/mixins/modal_mixins';
|
import toggleFocusMode from '~/boards/toggle_focus';
|
||||||
import '~/boards/filters/due_date_filters';
|
import { deprecatedCreateFlash as Flash } from '~/flash';
|
||||||
import BoardAddIssuesModal from '~/boards/components/modal/index.vue';
|
import createDefaultClient from '~/lib/graphql';
|
||||||
import {
|
import {
|
||||||
NavigationType,
|
NavigationType,
|
||||||
convertObjectPropsToCamelCase,
|
convertObjectPropsToCamelCase,
|
||||||
parseBoolean,
|
parseBoolean,
|
||||||
} from '~/lib/utils/common_utils';
|
} from '~/lib/utils/common_utils';
|
||||||
|
import { __ } from '~/locale';
|
||||||
|
import sidebarEventHub from '~/sidebar/event_hub';
|
||||||
import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher';
|
import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher';
|
||||||
|
|
||||||
Vue.use(VueApollo);
|
Vue.use(VueApollo);
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlButton, GlLoadingIcon, GlModal, GlLink } from '@gitlab/ui';
|
import { GlButton, GlLoadingIcon, GlModal, GlLink } from '@gitlab/ui';
|
||||||
import { getParameterByName } from '~/lib/utils/common_utils';
|
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 eventHub from '~/pipelines/event_hub';
|
||||||
import PipelinesMixin from '~/pipelines/mixins/pipelines_mixin';
|
import PipelinesMixin from '~/pipelines/mixins/pipelines_mixin';
|
||||||
import PipelinesService from '~/pipelines/services/pipelines_service';
|
import PipelinesService from '~/pipelines/services/pipelines_service';
|
||||||
import PipelineStore from '~/pipelines/stores/pipelines_store';
|
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 TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
|
||||||
import SvgBlankState from '~/pipelines/components/pipelines_list/blank_state.vue';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* global $ */
|
/* global $ */
|
||||||
|
/* eslint-disable import/order */
|
||||||
|
|
||||||
import jQuery from 'jquery';
|
import jQuery from 'jquery';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
|
|
|
@ -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.
|
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 $ from 'jquery';
|
||||||
import '~/lib/utils/jquery_at_who';
|
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import { escape, uniqueId } from 'lodash';
|
import { escape, uniqueId } from 'lodash';
|
||||||
import Autosize from 'autosize';
|
|
||||||
import Vue from 'vue';
|
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 AjaxCache from '~/lib/utils/ajax_cache';
|
||||||
import syntaxHighlight from '~/syntax_highlight';
|
import syntaxHighlight from '~/syntax_highlight';
|
||||||
import Autosave from './autosave';
|
import Autosave from './autosave';
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
import PipelineTourSuccessModal from '~/blob/pipeline_tour_success_modal.vue';
|
||||||
import BlobViewer from '~/blob/viewer/index';
|
import BlobViewer from '~/blob/viewer/index';
|
||||||
import GpgBadges from '~/gpg_badges';
|
import GpgBadges from '~/gpg_badges';
|
||||||
import initBlob from '~/pages/projects/init_blob';
|
import initBlob from '~/pages/projects/init_blob';
|
||||||
import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
|
import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
|
||||||
import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
|
import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
|
||||||
import '~/sourcegraph/load';
|
import '~/sourcegraph/load';
|
||||||
import PipelineTourSuccessModal from '~/blob/pipeline_tour_success_modal.vue';
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
new BlobViewer(); // eslint-disable-line no-new
|
new BlobViewer(); // eslint-disable-line no-new
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* eslint-disable no-new */
|
/* eslint-disable no-new */
|
||||||
|
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
|
import loadAwardsHandler from '~/awards_handler';
|
||||||
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
|
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
|
||||||
import Diff from '~/diff';
|
import Diff from '~/diff';
|
||||||
import flash from '~/flash';
|
import flash from '~/flash';
|
||||||
|
@ -14,7 +14,6 @@ import { initCommitBoxInfo } from '~/projects/commit_box/info';
|
||||||
import syntaxHighlight from '~/syntax_highlight';
|
import syntaxHighlight from '~/syntax_highlight';
|
||||||
import ZenMode from '~/zen_mode';
|
import ZenMode from '~/zen_mode';
|
||||||
import '~/sourcegraph/load';
|
import '~/sourcegraph/load';
|
||||||
import loadAwardsHandler from '~/awards_handler';
|
|
||||||
|
|
||||||
const hasPerfBar = document.querySelector('.with-performance-bar');
|
const hasPerfBar = document.querySelector('.with-performance-bar');
|
||||||
const performanceHeight = hasPerfBar ? 35 : 0;
|
const performanceHeight = hasPerfBar ? 35 : 0;
|
||||||
|
|
|
@ -141,7 +141,15 @@ export default class UserTabs {
|
||||||
this.loadOverviewTab();
|
this.loadOverviewTab();
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadableActions = ['groups', 'contributed', 'projects', 'starred', 'snippets'];
|
const loadableActions = [
|
||||||
|
'groups',
|
||||||
|
'contributed',
|
||||||
|
'projects',
|
||||||
|
'starred',
|
||||||
|
'snippets',
|
||||||
|
'followers',
|
||||||
|
'following',
|
||||||
|
];
|
||||||
if (loadableActions.indexOf(action) > -1) {
|
if (loadableActions.indexOf(action) > -1) {
|
||||||
this.loadTab(action, endpoint);
|
this.loadTab(action, endpoint);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,11 +10,11 @@ import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, FILTER_TAG_IDENTIFIER } from '../
|
||||||
import PipelinesMixin from '../../mixins/pipelines_mixin';
|
import PipelinesMixin from '../../mixins/pipelines_mixin';
|
||||||
import PipelinesService from '../../services/pipelines_service';
|
import PipelinesService from '../../services/pipelines_service';
|
||||||
import { validateParams } from '../../utils';
|
import { validateParams } from '../../utils';
|
||||||
|
import SvgBlankState from './blank_state.vue';
|
||||||
import EmptyState from './empty_state.vue';
|
import EmptyState from './empty_state.vue';
|
||||||
import NavigationControls from './nav_controls.vue';
|
import NavigationControls from './nav_controls.vue';
|
||||||
import PipelinesFilteredSearch from './pipelines_filtered_search.vue';
|
import PipelinesFilteredSearch from './pipelines_filtered_search.vue';
|
||||||
import PipelinesTableComponent from './pipelines_table.vue';
|
import PipelinesTableComponent from './pipelines_table.vue';
|
||||||
import SvgBlankState from './blank_state.vue';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import Visibility from 'visibilityjs';
|
import Visibility from 'visibilityjs';
|
||||||
import { deprecatedCreateFlash as createFlash } from '~/flash';
|
import { deprecatedCreateFlash as createFlash } from '~/flash';
|
||||||
import Poll from '~/lib/utils/poll';
|
|
||||||
import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
|
import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
|
||||||
import { validateParams } from '~/pipelines/utils';
|
import Poll from '~/lib/utils/poll';
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
|
import { validateParams } from '~/pipelines/utils';
|
||||||
import { CANCEL_REQUEST } from '../constants';
|
import { CANCEL_REQUEST } from '../constants';
|
||||||
import eventHub from '../event_hub';
|
import eventHub from '../event_hub';
|
||||||
|
|
||||||
|
|
|
@ -1,31 +1,92 @@
|
||||||
<script>
|
<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';
|
import IntegrationView from './integration_view.vue';
|
||||||
|
|
||||||
const INTEGRATION_VIEW_CONFIGS = {
|
function updateClasses(bodyClasses = '', applicationTheme, layout) {
|
||||||
sourcegraph: {
|
// Remove body class for any previous theme, re-add current one
|
||||||
title: s__('ProfilePreferences|Sourcegraph'),
|
document.body.classList.remove(...bodyClasses.split(' '));
|
||||||
label: s__('ProfilePreferences|Enable integrated code intelligence on code views'),
|
document.body.classList.add(applicationTheme);
|
||||||
formName: 'sourcegraph_enabled',
|
|
||||||
},
|
// Toggle container-fluid class
|
||||||
gitpod: {
|
if (layout === 'fluid') {
|
||||||
title: s__('ProfilePreferences|Gitpod'),
|
document
|
||||||
label: s__('ProfilePreferences|Enable Gitpod integration'),
|
.querySelector('.content-wrapper .container-fluid')
|
||||||
formName: 'gitpod_enabled',
|
.classList.remove('container-limited');
|
||||||
},
|
} else {
|
||||||
};
|
document.querySelector('.content-wrapper .container-fluid').classList.add('container-limited');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ProfilePreferences',
|
name: 'ProfilePreferences',
|
||||||
components: {
|
components: {
|
||||||
IntegrationView,
|
IntegrationView,
|
||||||
|
GlButton,
|
||||||
},
|
},
|
||||||
inject: {
|
inject: {
|
||||||
integrationViews: {
|
integrationViews: {
|
||||||
default: [],
|
default: [],
|
||||||
},
|
},
|
||||||
|
themes: {
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
userFields: {
|
||||||
|
default: {},
|
||||||
|
},
|
||||||
|
formEl: 'formEl',
|
||||||
|
profilePreferencesPath: 'profilePreferencesPath',
|
||||||
|
bodyClasses: 'bodyClasses',
|
||||||
},
|
},
|
||||||
integrationViewConfigs: INTEGRATION_VIEW_CONFIGS,
|
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>
|
</script>
|
||||||
|
|
||||||
|
@ -36,10 +97,10 @@ export default {
|
||||||
</div>
|
</div>
|
||||||
<div v-if="integrationViews.length" class="col-lg-4 profile-settings-sidebar">
|
<div v-if="integrationViews.length" class="col-lg-4 profile-settings-sidebar">
|
||||||
<h4 class="gl-mt-0" data-testid="profile-preferences-integrations-heading">
|
<h4 class="gl-mt-0" data-testid="profile-preferences-integrations-heading">
|
||||||
{{ s__('ProfilePreferences|Integrations') }}
|
{{ $options.i18n.integrations }}
|
||||||
</h4>
|
</h4>
|
||||||
<p>
|
<p>
|
||||||
{{ s__('ProfilePreferences|Customize integrations with third party services.') }}
|
{{ $options.i18n.integrationsDescription }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="integrationViews.length" class="col-lg-8">
|
<div v-if="integrationViews.length" class="col-lg-8">
|
||||||
|
@ -52,5 +113,19 @@ export default {
|
||||||
:config="$options.integrationViewConfigs[view.name]"
|
:config="$options.integrationViewConfigs[view.name]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -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.'),
|
||||||
|
};
|
|
@ -3,16 +3,20 @@ import ProfilePreferences from './components/profile_preferences.vue';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const el = document.querySelector('#js-profile-preferences-app');
|
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) => {
|
const provide = Object.keys(el.dataset).reduce(
|
||||||
let value = el.dataset[key];
|
(memo, key) => {
|
||||||
if (shouldParse.includes(key)) {
|
let value = el.dataset[key];
|
||||||
value = JSON.parse(value);
|
if (shouldParse.includes(key)) {
|
||||||
}
|
value = JSON.parse(value);
|
||||||
|
}
|
||||||
|
|
||||||
return { ...memo, [key]: value };
|
return { ...memo, [key]: value };
|
||||||
}, {});
|
},
|
||||||
|
{ formEl },
|
||||||
|
);
|
||||||
|
|
||||||
return new Vue({
|
return new Vue({
|
||||||
el,
|
el,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
/* eslint-disable vue/no-v-html */
|
/* eslint-disable vue/no-v-html */
|
||||||
|
import { GlIcon, GlLink, GlLoadingIcon } from '@gitlab/ui';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import '~/behaviors/markdown/render_gfm';
|
import '~/behaviors/markdown/render_gfm';
|
||||||
import { GlIcon, GlLink, GlLoadingIcon } from '@gitlab/ui';
|
|
||||||
import { handleLocationHash } from '~/lib/utils/common_utils';
|
import { handleLocationHash } from '~/lib/utils/common_utils';
|
||||||
import readmeQuery from '../../queries/readme.query.graphql';
|
import readmeQuery from '../../queries/readme.query.graphql';
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
|
|
||||||
import { GlToast } from '@gitlab/ui';
|
import { GlToast } from '@gitlab/ui';
|
||||||
|
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import VueApollo from 'vue-apollo';
|
import VueApollo from 'vue-apollo';
|
||||||
import createDefaultClient from '~/lib/graphql';
|
import createDefaultClient from '~/lib/graphql';
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
/* eslint-disable vue/no-v-html */
|
/* eslint-disable vue/no-v-html */
|
||||||
|
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import '~/behaviors/markdown/render_gfm';
|
import '~/behaviors/markdown/render_gfm';
|
||||||
|
|
||||||
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
|
|
||||||
import { forEach, escape } from 'lodash';
|
import { forEach, escape } from 'lodash';
|
||||||
import axios from '~/lib/utils/axios_utils';
|
import axios from '~/lib/utils/axios_utils';
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
/* eslint-disable vue/no-v-html */
|
/* eslint-disable vue/no-v-html */
|
||||||
|
import { GlIcon } from '@gitlab/ui';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import '~/behaviors/markdown/render_gfm';
|
import '~/behaviors/markdown/render_gfm';
|
||||||
import { unescape } from 'lodash';
|
import { unescape } from 'lodash';
|
||||||
import { GlIcon } from '@gitlab/ui';
|
|
||||||
import { deprecatedCreateFlash as Flash } from '~/flash';
|
import { deprecatedCreateFlash as Flash } from '~/flash';
|
||||||
import GLForm from '~/gl_form';
|
import GLForm from '~/gl_form';
|
||||||
import axios from '~/lib/utils/axios_utils';
|
import axios from '~/lib/utils/axios_utils';
|
||||||
|
|
|
@ -33,6 +33,21 @@ class DashboardController < Dashboard::ApplicationController
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def load_events
|
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 =
|
projects =
|
||||||
if params[:filter] == "starred"
|
if params[:filter] == "starred"
|
||||||
ProjectsFinder.new(current_user: current_user, params: { starred: true }).execute
|
ProjectsFinder.new(current_user: current_user, params: { starred: true }).execute
|
||||||
|
@ -40,12 +55,10 @@ class DashboardController < Dashboard::ApplicationController
|
||||||
current_user.authorized_projects
|
current_user.authorized_projects
|
||||||
end
|
end
|
||||||
|
|
||||||
@events = EventCollection
|
EventCollection
|
||||||
.new(projects, offset: params[:offset].to_i, filter: event_filter)
|
.new(projects, offset: params[:offset].to_i, filter: event_filter)
|
||||||
.to_a
|
.to_a
|
||||||
.map(&:present)
|
.map(&:present)
|
||||||
|
|
||||||
Events::RenderService.new(current_user).execute(@events)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_show_full_reference
|
def set_show_full_reference
|
||||||
|
|
|
@ -9,23 +9,18 @@ class Profiles::PreferencesController < Profiles::ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
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
|
render json: { type: :notice, message: message }
|
||||||
flash[:notice] = _('Preferences saved.')
|
else
|
||||||
else
|
render status: :bad_request, json: { type: :alert, message: _('Failed to save preferences.') }
|
||||||
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
|
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class UsersController < ApplicationController
|
class UsersController < ApplicationController
|
||||||
|
include InternalRedirect
|
||||||
include RoutableActions
|
include RoutableActions
|
||||||
include RendersMemberAccess
|
include RendersMemberAccess
|
||||||
include RendersProjectsList
|
include RendersProjectsList
|
||||||
|
@ -13,13 +14,15 @@ class UsersController < ApplicationController
|
||||||
contributed: false,
|
contributed: false,
|
||||||
snippets: true,
|
snippets: true,
|
||||||
calendar: false,
|
calendar: false,
|
||||||
|
followers: false,
|
||||||
|
following: false,
|
||||||
calendar_activities: true
|
calendar_activities: true
|
||||||
|
|
||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
|
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
|
||||||
before_action :user, except: [:exists, :suggests, :ssh_keys]
|
before_action :user, except: [:exists, :suggests, :ssh_keys]
|
||||||
before_action :authorize_read_user_profile!,
|
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
|
feature_category :users
|
||||||
|
|
||||||
|
@ -97,6 +100,18 @@ class UsersController < ApplicationController
|
||||||
present_projects(@starred_projects)
|
present_projects(@starred_projects)
|
||||||
end
|
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)
|
def present_projects(projects)
|
||||||
skip_pagination = Gitlab::Utils.to_boolean(params[:skip_pagination])
|
skip_pagination = Gitlab::Utils.to_boolean(params[:skip_pagination])
|
||||||
skip_namespace = Gitlab::Utils.to_boolean(params[:skip_namespace])
|
skip_namespace = Gitlab::Utils.to_boolean(params[:skip_namespace])
|
||||||
|
@ -146,6 +161,22 @@ class UsersController < ApplicationController
|
||||||
render json: { exists: exists, suggests: suggestions }
|
render json: { exists: exists, suggests: suggestions }
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def user
|
def user
|
||||||
|
@ -169,7 +200,7 @@ class UsersController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_events
|
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?)
|
Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
|
||||||
end
|
end
|
||||||
|
@ -216,6 +247,17 @@ class UsersController < ApplicationController
|
||||||
def authorize_read_user_profile!
|
def authorize_read_user_profile!
|
||||||
access_denied! unless can?(current_user, :read_user_profile, user)
|
access_denied! unless can?(current_user, :read_user_profile, user)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
UsersController.prepend_if_ee('EE::UsersController')
|
UsersController.prepend_if_ee('EE::UsersController')
|
||||||
|
|
|
@ -15,28 +15,50 @@ class UserRecentEventsFinder
|
||||||
|
|
||||||
requires_cross_project_access
|
requires_cross_project_access
|
||||||
|
|
||||||
attr_reader :current_user, :target_user, :params
|
attr_reader :current_user, :target_user, :params, :event_filter
|
||||||
|
|
||||||
DEFAULT_LIMIT = 20
|
DEFAULT_LIMIT = 20
|
||||||
MAX_LIMIT = 100
|
MAX_LIMIT = 100
|
||||||
|
|
||||||
def initialize(current_user, target_user, params = {})
|
def initialize(current_user, target_user, event_filter, params = {})
|
||||||
@current_user = current_user
|
@current_user = current_user
|
||||||
@target_user = target_user
|
@target_user = target_user
|
||||||
@params = params
|
@params = params
|
||||||
|
@event_filter = event_filter || EventFilter.new(EventFilter::ALL)
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
return Event.none unless can?(current_user, :read_user_profile, target_user)
|
if target_user.is_a? User
|
||||||
|
execute_single
|
||||||
target_events
|
else
|
||||||
.with_associations
|
execute_multi
|
||||||
.limit_recent(limit, params[:offset])
|
end
|
||||||
.order_created_desc
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
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
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
def target_events
|
def target_events
|
||||||
Event.where(author: target_user)
|
Event.where(author: target_user)
|
||||||
|
|
|
@ -242,7 +242,7 @@ module UsersHelper
|
||||||
tabs = []
|
tabs = []
|
||||||
|
|
||||||
if can?(current_user, :read_user_profile, @user)
|
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
|
end
|
||||||
|
|
||||||
tabs
|
tabs
|
||||||
|
|
|
@ -116,6 +116,13 @@ class User < ApplicationRecord
|
||||||
has_one :user_synced_attributes_metadata, autosave: true
|
has_one :user_synced_attributes_metadata, autosave: true
|
||||||
has_one :aws_role, class_name: 'Aws::Role'
|
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
|
# Groups
|
||||||
has_many :members
|
has_many :members
|
||||||
has_many :group_members, -> { where(requested_at: nil).where("access_level >= ?", Gitlab::Access::GUEST) }, source: 'GroupMember'
|
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
|
||||||
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
|
def manageable_namespaces
|
||||||
@manageable_namespaces ||= [namespace] + manageable_groups
|
@manageable_namespaces ||= [namespace] + manageable_groups
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
|
@ -5,7 +5,10 @@
|
||||||
%ul.nav-links.nav.nav-tabs
|
%ul.nav-links.nav.nav-tabs
|
||||||
%li{ class: active_when(params[:filter].nil?) }>
|
%li{ class: active_when(params[:filter].nil?) }>
|
||||||
= link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do
|
= link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do
|
||||||
Your projects
|
= _('Your projects')
|
||||||
%li{ class: active_when(params[:filter] == 'starred') }>
|
%li{ class: active_when(params[:filter] == 'starred') }>
|
||||||
= link_to activity_dashboard_path(filter: 'starred'), data: {placement: 'right'} do
|
= 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')
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
- page_title _('Preferences')
|
- page_title _('Preferences')
|
||||||
- @content_class = "limit-container-width" unless fluid_layout
|
- @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
|
- 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|
|
- Gitlab::Themes.each do |theme|
|
||||||
= stylesheet_link_tag "themes/#{theme.css_filename}" if theme.css_filename
|
= 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
|
.row.gl-mt-3.js-preferences-form.js-search-settings-section
|
||||||
.col-lg-4.application-theme#navigation-theme
|
.col-lg-4.application-theme#navigation-theme
|
||||||
%h4.gl-mt-0
|
%h4.gl-mt-0
|
||||||
|
@ -143,10 +144,4 @@
|
||||||
.form-text.text-muted
|
.form-text.text-muted
|
||||||
= s_('Preferences|For example: 30 mins ago.')
|
= s_('Preferences|For example: 30 mins ago.')
|
||||||
|
|
||||||
#js-profile-preferences-app{ data: data_attributes, user_fields: user_fields.to_json }
|
#js-profile-preferences-app{ data: data_attributes }
|
||||||
|
|
||||||
.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'
|
|
||||||
|
|
|
@ -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 %>
|
|
|
@ -1,5 +1,6 @@
|
||||||
- current_user_empty_message_description = local_assigns.fetch(:current_user_empty_message_description, nil)
|
- current_user_empty_message_description = local_assigns.fetch(:current_user_empty_message_description, nil)
|
||||||
- secondary_button_link = local_assigns.fetch(:secondary_button_link, nil)
|
- secondary_button_link = local_assigns.fetch(:secondary_button_link, nil)
|
||||||
|
- primary_button_link = local_assigns.fetch(:primary_button_link, nil)
|
||||||
|
|
||||||
.nothing-here-block
|
.nothing-here-block
|
||||||
.svg-content
|
.svg-content
|
||||||
|
|
|
@ -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
|
|
@ -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}
|
|
@ -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',
|
= 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
|
title: s_('UserProfile|Report abuse'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
|
||||||
= sprite_icon('error')
|
= 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)
|
- 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',
|
= 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
|
title: s_('UserProfile|Subscribe'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
|
||||||
|
@ -89,6 +96,16 @@
|
||||||
- unless @user.public_email.blank?
|
- unless @user.public_email.blank?
|
||||||
.profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline.mt-1.mt-sm-0
|
.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'
|
= 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?
|
- if @user.bio.present?
|
||||||
.cover-desc.cgray
|
.cover-desc.cgray
|
||||||
.profile-user-bio
|
.profile-user-bio
|
||||||
|
@ -129,6 +146,14 @@
|
||||||
%li.js-snippets-tab
|
%li.js-snippets-tab
|
||||||
= link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do
|
= link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do
|
||||||
= s_('UserProfile|Snippets')
|
= 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 }
|
%div{ class: container_class }
|
||||||
.tab-content
|
.tab-content
|
||||||
|
@ -165,6 +190,14 @@
|
||||||
#snippets.tab-pane
|
#snippets.tab-pane
|
||||||
-# This tab is always loaded via AJAX
|
-# 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
|
.loading.hide
|
||||||
.spinner.spinner-md
|
.spinner.spinner-md
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add follow each other model, API and UI(profile, activity view)
|
||||||
|
merge_request: 45451
|
||||||
|
author: Roger Meier
|
||||||
|
type: added
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Import epic award emojis when using Bulk Import
|
||||||
|
merge_request: 53747
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -46,9 +46,13 @@ scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) d
|
||||||
get :contributed, as: :contributed_projects
|
get :contributed, as: :contributed_projects
|
||||||
get :starred, as: :starred_projects
|
get :starred, as: :starred_projects
|
||||||
get :snippets
|
get :snippets
|
||||||
|
get :followers
|
||||||
|
get :following
|
||||||
get :exists
|
get :exists
|
||||||
get :suggests
|
get :suggests
|
||||||
get :activity
|
get :activity
|
||||||
|
post :follow
|
||||||
|
post :unfollow
|
||||||
get '/', to: redirect('%{username}'), as: nil
|
get '/', to: redirect('%{username}'), as: nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
|
@ -0,0 +1 @@
|
||||||
|
d6b324e808265c4ba8b6216c77b7abfa96b4b8b4c9fbd8d0a15240548526c4f3
|
|
@ -17751,6 +17751,11 @@ CREATE SEQUENCE user_details_user_id_seq
|
||||||
|
|
||||||
ALTER SEQUENCE user_details_user_id_seq OWNED BY user_details.user_id;
|
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 (
|
CREATE TABLE user_highest_roles (
|
||||||
user_id bigint NOT NULL,
|
user_id bigint NOT NULL,
|
||||||
updated_at timestamp with time zone 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
|
ALTER TABLE ONLY user_details
|
||||||
ADD CONSTRAINT user_details_pkey PRIMARY KEY (user_id);
|
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
|
ALTER TABLE ONLY user_highest_roles
|
||||||
ADD CONSTRAINT user_highest_roles_pkey PRIMARY KEY (user_id);
|
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 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_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);
|
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;
|
ADD CONSTRAINT fk_u2f_registrations_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
ALTER TABLE product_analytics_events_experimental
|
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
|
-- but instead tracked in the db/schema_migrations directory
|
||||||
-- see https://gitlab.com/gitlab-org/gitlab/-/issues/218590 for details
|
-- see https://gitlab.com/gitlab-org/gitlab/-/issues/218590 for details
|
||||||
|
|
|
@ -274,7 +274,9 @@ Parameters:
|
||||||
"twitter": "",
|
"twitter": "",
|
||||||
"website_url": "",
|
"website_url": "",
|
||||||
"organization": "",
|
"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
|
## User counts
|
||||||
|
|
||||||
Get the counts (same as in top right menu) of the currently signed in user.
|
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 |
|
@ -83,6 +83,14 @@ There are several types of users in GitLab:
|
||||||
self-managed instances' features and settings.
|
self-managed instances' features and settings.
|
||||||
- [Internal users](../development/internal_users.md).
|
- [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
|
## Projects
|
||||||
|
|
||||||
In GitLab, you can create [projects](project/index.md) to host
|
In GitLab, you can create [projects](project/index.md) to host
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 637 KiB |
|
@ -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)
|
- Personal projects: your personal projects (respecting the project's visibility level)
|
||||||
- Starred projects: projects you starred
|
- Starred projects: projects you starred
|
||||||
- Snippets: your personal code [snippets](../snippets.md#personal-snippets)
|
- 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
|
## User settings
|
||||||
|
|
||||||
|
|
|
@ -59,21 +59,25 @@ and edit labels.
|
||||||
|
|
||||||
> Showing all inherited labels [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/241990) in GitLab 13.5.
|
> 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**.
|
To view a project's available labels, in the project, go to **Issues > Labels**.
|
||||||
The list includes all labels that are defined at the project level, as well as all
|
Its list of labels includes both the labels defined at the project level, and
|
||||||
labels defined by its ancestor groups.
|
all labels defined by its ancestor groups. For each label, you can see the
|
||||||
For each label, you can see the project or group path from where it was created.
|
project or group path from where it was created. You can filter the list by
|
||||||
You can filter the list by entering a search query at the top and clicking search (**{search}**).
|
entering a search query in the **Filter** field, and then clicking its search
|
||||||
|
icon (**{search}**).
|
||||||
|
|
||||||
To create a new project label:
|
To create a new project label:
|
||||||
|
|
||||||
1. Navigate to **Issues > Labels** in the project.
|
1. In your project, go to **Issues > Labels**.
|
||||||
1. Click the **New label** button.
|
1. Select the **New label** button.
|
||||||
- Enter the title.
|
1. In the **Title** field, enter a short, descriptive name for the label. You
|
||||||
- (Optional) Enter a description.
|
can also use this field to create [scoped, mutually exclusive labels](#scoped-labels).
|
||||||
- (Optional) Select a background color by clicking on the available colors, or input
|
1. (Optional) In the **Description** field, you can enter additional
|
||||||
a hex color value for a specific color.
|
information about how and when to use this label.
|
||||||
1. Click **Create label** to create the 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
|
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:
|
label section of the right sidebar of an issue or a merge request:
|
||||||
|
|
|
@ -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.
|
- [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.
|
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.
|
The unique internal email address is visible to project members with Maintainer (or higher)
|
||||||
However, when using an email alias externally, an end user (issue creator) cannot see the internal
|
[permission level](../permissions.md)
|
||||||
email address displayed in the information note.
|
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
|
### Using customized email templates
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,12 @@ module API
|
||||||
expose :work_information do |user|
|
expose :work_information do |user|
|
||||||
work_information(user)
|
work_information(user)
|
||||||
end
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -159,6 +159,68 @@ module API
|
||||||
present user.status || {}, with: Entities::UserStatus
|
present user.status || {}, with: Entities::UserStatus
|
||||||
end
|
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
|
desc 'Create a user. Available only for admins.' do
|
||||||
success Entities::UserWithAdmin
|
success Entities::UserWithAdmin
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,7 +13,7 @@ module BulkImports
|
||||||
|
|
||||||
response = client.execute(
|
response = client.execute(
|
||||||
client.parse(query.to_s),
|
client.parse(query.to_s),
|
||||||
query.variables(context.entity)
|
query.variables(context)
|
||||||
).original_hash.deep_dup
|
).original_hash.deep_dup
|
||||||
|
|
||||||
BulkImports::Pipeline::ExtractedData.new(
|
BulkImports::Pipeline::ExtractedData.new(
|
||||||
|
|
|
@ -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
|
|
@ -29,8 +29,8 @@ module BulkImports
|
||||||
GRAPHQL
|
GRAPHQL
|
||||||
end
|
end
|
||||||
|
|
||||||
def variables(entity)
|
def variables(context)
|
||||||
{ full_path: entity.source_full_path }
|
{ full_path: context.entity.source_full_path }
|
||||||
end
|
end
|
||||||
|
|
||||||
def base_path
|
def base_path
|
||||||
|
|
|
@ -26,10 +26,10 @@ module BulkImports
|
||||||
GRAPHQL
|
GRAPHQL
|
||||||
end
|
end
|
||||||
|
|
||||||
def variables(entity)
|
def variables(context)
|
||||||
{
|
{
|
||||||
full_path: entity.source_full_path,
|
full_path: context.entity.source_full_path,
|
||||||
cursor: entity.next_page_for(:labels)
|
cursor: context.entity.next_page_for(:labels)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -31,10 +31,10 @@ module BulkImports
|
||||||
GRAPHQL
|
GRAPHQL
|
||||||
end
|
end
|
||||||
|
|
||||||
def variables(entity)
|
def variables(context)
|
||||||
{
|
{
|
||||||
full_path: entity.source_full_path,
|
full_path: context.entity.source_full_path,
|
||||||
cursor: entity.next_page_for(:group_members)
|
cursor: context.entity.next_page_for(:group_members)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,12 @@ module BulkImports
|
||||||
module Pipeline
|
module Pipeline
|
||||||
class Context
|
class Context
|
||||||
attr_reader :entity, :bulk_import
|
attr_reader :entity, :bulk_import
|
||||||
|
attr_accessor :extra
|
||||||
|
|
||||||
def initialize(entity)
|
def initialize(entity, extra = {})
|
||||||
@entity = entity
|
@entity = entity
|
||||||
@bulk_import = entity.bulk_import
|
@bulk_import = entity.bulk_import
|
||||||
|
@extra = extra
|
||||||
end
|
end
|
||||||
|
|
||||||
def group
|
def group
|
||||||
|
|
|
@ -3,16 +3,11 @@
|
||||||
module Gitlab
|
module Gitlab
|
||||||
module Database
|
module Database
|
||||||
module Migrations
|
module Migrations
|
||||||
Observation = Struct.new(
|
|
||||||
:migration,
|
|
||||||
:walltime,
|
|
||||||
:success
|
|
||||||
)
|
|
||||||
|
|
||||||
class Instrumentation
|
class Instrumentation
|
||||||
attr_reader :observations
|
attr_reader :observations
|
||||||
|
|
||||||
def initialize
|
def initialize(observers = ::Gitlab::Database::Migrations::Observers.all_observers)
|
||||||
|
@observers = observers
|
||||||
@observations = []
|
@observations = []
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -22,6 +17,8 @@ module Gitlab
|
||||||
|
|
||||||
exception = nil
|
exception = nil
|
||||||
|
|
||||||
|
on_each_observer { |observer| observer.before }
|
||||||
|
|
||||||
observation.walltime = Benchmark.realtime do
|
observation.walltime = Benchmark.realtime do
|
||||||
yield
|
yield
|
||||||
rescue => e
|
rescue => e
|
||||||
|
@ -29,6 +26,9 @@ module Gitlab
|
||||||
observation.success = false
|
observation.success = false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
on_each_observer { |observer| observer.after }
|
||||||
|
on_each_observer { |observer| observer.record(observation) }
|
||||||
|
|
||||||
record_observation(observation)
|
record_observation(observation)
|
||||||
|
|
||||||
raise exception if exception
|
raise exception if exception
|
||||||
|
@ -38,9 +38,19 @@ module Gitlab
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
attr_reader :observers
|
||||||
|
|
||||||
def record_observation(observation)
|
def record_observation(observation)
|
||||||
@observations << observation
|
@observations << observation
|
||||||
end
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1123,6 +1123,11 @@ msgid_plural "%d deploy keys"
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
|
msgid "1 follower"
|
||||||
|
msgid_plural "%{count} followers"
|
||||||
|
msgstr[0] ""
|
||||||
|
msgstr[1] ""
|
||||||
|
|
||||||
msgid "1 group"
|
msgid "1 group"
|
||||||
msgid_plural "%d groups"
|
msgid_plural "%d groups"
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
|
@ -12971,6 +12976,12 @@ msgstr ""
|
||||||
msgid "Folder/%{name}"
|
msgid "Folder/%{name}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Follow"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Followed users"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Font Color"
|
msgid "Font Color"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -22224,21 +22235,39 @@ msgstr ""
|
||||||
msgid "Preferences|Choose what content you want to see on your homepage."
|
msgid "Preferences|Choose what content you want to see on your homepage."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Preferences|Customize integrations with third party services."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Preferences|Customize the appearance of the application header and navigation sidebar."
|
msgid "Preferences|Customize the appearance of the application header and navigation sidebar."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Preferences|Display time in 24-hour format"
|
msgid "Preferences|Display time in 24-hour format"
|
||||||
msgstr ""
|
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."
|
msgid "Preferences|For example: 30 mins ago."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Preferences|Gitpod"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Preferences|Homepage content"
|
msgid "Preferences|Homepage content"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Preferences|Instead of all the files changed, show only one file at a time. To switch between files, use the file browser."
|
msgid "Preferences|Instead of all the files changed, show only one file at a time. To switch between files, use the file browser."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Preferences|Integrations"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Preferences|Layout width"
|
msgid "Preferences|Layout width"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -22260,6 +22289,9 @@ msgstr ""
|
||||||
msgid "Preferences|Show whitespace changes in diffs"
|
msgid "Preferences|Show whitespace changes in diffs"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Preferences|Sourcegraph"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Preferences|Syntax highlighting theme"
|
msgid "Preferences|Syntax highlighting theme"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -22452,24 +22484,6 @@ msgstr ""
|
||||||
msgid "Profile Settings"
|
msgid "Profile Settings"
|
||||||
msgstr ""
|
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"
|
msgid "ProfileSession|on"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -31381,6 +31395,9 @@ msgstr ""
|
||||||
msgid "Unexpected error"
|
msgid "Unexpected error"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Unfollow"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Unfortunately, your email message to GitLab could not be processed."
|
msgid "Unfortunately, your email message to GitLab could not be processed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -32038,6 +32055,12 @@ msgstr ""
|
||||||
msgid "UserProfile|Explore public groups to find projects to contribute to."
|
msgid "UserProfile|Explore public groups to find projects to contribute to."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "UserProfile|Followers"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "UserProfile|Following"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "UserProfile|Groups"
|
msgid "UserProfile|Groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -32080,6 +32103,9 @@ msgstr ""
|
||||||
msgid "UserProfile|Subscribe"
|
msgid "UserProfile|Subscribe"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "UserProfile|This user doesn't have any followers."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "UserProfile|This user doesn't have any personal projects"
|
msgid "UserProfile|This user doesn't have any personal projects"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -32095,6 +32121,9 @@ msgstr ""
|
||||||
msgid "UserProfile|This user is blocked"
|
msgid "UserProfile|This user is blocked"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "UserProfile|This user isn't following other users."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "UserProfile|Unconfirmed user"
|
msgid "UserProfile|Unconfirmed user"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -32104,9 +32133,15 @@ msgstr ""
|
||||||
msgid "UserProfile|View user in admin area"
|
msgid "UserProfile|View user in admin area"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "UserProfile|You are not following other users."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "UserProfile|You can create a group for several dependent projects."
|
msgid "UserProfile|You can create a group for several dependent projects."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "UserProfile|You do not have any followers."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "UserProfile|You haven't created any personal projects."
|
msgid "UserProfile|You haven't created any personal projects."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -34672,6 +34707,9 @@ msgstr[1] ""
|
||||||
msgid "finding is not found or is already attached to a vulnerability"
|
msgid "finding is not found or is already attached to a vulnerability"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "following"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "for %{link_to_merge_request} with %{link_to_merge_request_source_branch}"
|
msgid "for %{link_to_merge_request} with %{link_to_merge_request_source_branch}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ RSpec.describe Profiles::PreferencesController do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'PATCH update' do
|
describe 'PATCH update' do
|
||||||
def go(params: {}, format: :js)
|
def go(params: {}, format: :json)
|
||||||
params.reverse_merge!(
|
params.reverse_merge!(
|
||||||
color_scheme_id: '1',
|
color_scheme_id: '1',
|
||||||
dashboard: 'stars',
|
dashboard: 'stars',
|
||||||
|
@ -35,9 +35,12 @@ RSpec.describe Profiles::PreferencesController do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'on successful update' do
|
context 'on successful update' do
|
||||||
it 'sets the flash' do
|
it 'responds with success' do
|
||||||
go
|
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
|
end
|
||||||
|
|
||||||
it "changes the user's preferences" do
|
it "changes the user's preferences" do
|
||||||
|
@ -59,36 +62,26 @@ RSpec.describe Profiles::PreferencesController do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'on failed update' do
|
context 'on failed update' do
|
||||||
it 'sets the flash' do
|
it 'responds with error' do
|
||||||
expect(user).to receive(:save).and_return(false)
|
expect(user).to receive(:save).and_return(false)
|
||||||
|
|
||||||
go
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'on invalid dashboard setting' do
|
context 'on invalid dashboard setting' do
|
||||||
it 'sets the flash' do
|
it 'responds with error' do
|
||||||
prefs = { dashboard: 'invalid' }
|
prefs = { dashboard: 'invalid' }
|
||||||
|
|
||||||
go params: prefs
|
go params: prefs
|
||||||
|
|
||||||
expect(flash[:alert]).to match(/\AFailed to save preferences \(.+\)\.\z/)
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
end
|
expect(response.parsed_body['message']).to match(/\AFailed to save preferences \(.+\)\.\z/)
|
||||||
end
|
expect(response.parsed_body['type']).to eq('alert')
|
||||||
|
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,6 +9,26 @@ RSpec.describe 'Dashboard > Activity' do
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
end
|
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
|
context 'rss' do
|
||||||
before do
|
before do
|
||||||
visit activity_dashboard_path
|
visit activity_dashboard_path
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe 'User edit preferences profile' do
|
RSpec.describe 'User edit preferences profile', :js do
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
@ -53,7 +53,14 @@ RSpec.describe 'User edit preferences profile' do
|
||||||
fill_in 'Tab width', with: -1
|
fill_in 'Tab width', with: -1
|
||||||
click_button 'Save changes'
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe 'User visits the profile preferences page' do
|
RSpec.describe 'User visits the profile preferences page', :js do
|
||||||
include Select2Helper
|
include Select2Helper
|
||||||
|
|
||||||
let(:user) { create(:user) }
|
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
|
describe 'User changes their default dashboard', :js do
|
||||||
it 'creates a flash message' do
|
it 'creates a flash message' do
|
||||||
select2('stars', from: '#user_dashboard')
|
select2('stars', from: '#user_dashboard')
|
||||||
click_button 'Save'
|
click_button 'Save changes'
|
||||||
|
|
||||||
wait_for_requests
|
wait_for_requests
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ RSpec.describe 'User visits the profile preferences page' do
|
||||||
|
|
||||||
it 'updates their preference' do
|
it 'updates their preference' do
|
||||||
select2('stars', from: '#user_dashboard')
|
select2('stars', from: '#user_dashboard')
|
||||||
click_button 'Save'
|
click_button 'Save changes'
|
||||||
|
|
||||||
wait_for_requests
|
wait_for_requests
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ RSpec.describe 'User visits the profile preferences page' do
|
||||||
describe 'User changes their language', :js do
|
describe 'User changes their language', :js do
|
||||||
it 'creates a flash message', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/31404' do
|
it 'creates a flash message', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/31404' do
|
||||||
select2('en', from: '#user_preferred_language')
|
select2('en', from: '#user_preferred_language')
|
||||||
click_button 'Save'
|
click_button 'Save changes'
|
||||||
|
|
||||||
wait_for_requests
|
wait_for_requests
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ RSpec.describe 'User visits the profile preferences page' do
|
||||||
it 'updates their preference' do
|
it 'updates their preference' do
|
||||||
wait_for_requests
|
wait_for_requests
|
||||||
select2('pt_BR', from: '#user_preferred_language')
|
select2('pt_BR', from: '#user_preferred_language')
|
||||||
click_button 'Save'
|
click_button 'Save changes'
|
||||||
|
|
||||||
wait_for_requests
|
wait_for_requests
|
||||||
refresh
|
refresh
|
||||||
|
@ -94,6 +94,8 @@ RSpec.describe 'User visits the profile preferences page' do
|
||||||
|
|
||||||
click_button 'Save changes'
|
click_button 'Save changes'
|
||||||
|
|
||||||
|
wait_for_requests
|
||||||
|
|
||||||
expect(user.reload.render_whitespace_in_code).to be(true)
|
expect(user.reload.render_whitespace_in_code).to be(true)
|
||||||
expect(render_whitespace_field).to be_checked
|
expect(render_whitespace_field).to be_checked
|
||||||
end
|
end
|
||||||
|
|
|
@ -151,6 +151,132 @@ RSpec.describe 'Overview tab on a user profile', :js do
|
||||||
end
|
end
|
||||||
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
|
describe 'bot user' do
|
||||||
let(:bot_user) { create(:user, user_type: :security_bot) }
|
let(:bot_user) { create(:user, user_type: :security_bot) }
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,8 @@ RSpec.describe 'User page' do
|
||||||
expect(page).to have_link('Contributed projects')
|
expect(page).to have_link('Contributed projects')
|
||||||
expect(page).to have_link('Personal projects')
|
expect(page).to have_link('Personal projects')
|
||||||
expect(page).to have_link('Snippets')
|
expect(page).to have_link('Snippets')
|
||||||
|
expect(page).to have_link('Followers')
|
||||||
|
expect(page).to have_link('Following')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -54,6 +56,50 @@ RSpec.describe 'User page' do
|
||||||
expect(page).to have_content('GitLab - work info test')
|
expect(page).to have_content('GitLab - work info test')
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
context 'with private profile' do
|
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('Contributed projects')
|
||||||
expect(page).to have_link('Personal projects')
|
expect(page).to have_link('Personal projects')
|
||||||
expect(page).to have_link('Snippets')
|
expect(page).to have_link('Snippets')
|
||||||
|
expect(page).to have_link('Followers')
|
||||||
|
expect(page).to have_link('Following')
|
||||||
end
|
end
|
||||||
end
|
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('Contributed projects')
|
||||||
expect(page).not_to have_link('Personal projects')
|
expect(page).not_to have_link('Personal projects')
|
||||||
expect(page).not_to have_link('Snippets')
|
expect(page).not_to have_link('Snippets')
|
||||||
|
expect(page).not_to have_link('Followers')
|
||||||
|
expect(page).not_to have_link('Following')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -261,6 +311,8 @@ RSpec.describe 'User page' do
|
||||||
expect(page).to have_link('Contributed projects')
|
expect(page).to have_link('Contributed projects')
|
||||||
expect(page).to have_link('Personal projects')
|
expect(page).to have_link('Personal projects')
|
||||||
expect(page).to have_link('Snippets')
|
expect(page).to have_link('Snippets')
|
||||||
|
expect(page).to have_link('Followers')
|
||||||
|
expect(page).to have_link('Following')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,16 +5,17 @@ require 'spec_helper'
|
||||||
RSpec.describe UserRecentEventsFinder do
|
RSpec.describe UserRecentEventsFinder do
|
||||||
let_it_be(:project_owner, reload: true) { create(:user) }
|
let_it_be(:project_owner, reload: true) { create(:user) }
|
||||||
let_it_be(:current_user, reload: true) { create(:user) }
|
let_it_be(:current_user, reload: true) { create(:user) }
|
||||||
let(:private_project) { create(:project, :private, creator: project_owner) }
|
let_it_be(:private_project) { create(:project, :private, creator: project_owner) }
|
||||||
let(:internal_project) { create(:project, :internal, creator: project_owner) }
|
let_it_be(:internal_project) { create(:project, :internal, creator: project_owner) }
|
||||||
let(:public_project) { create(:project, :public, 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!(:private_event) { create(:event, project: private_project, author: project_owner) }
|
||||||
let!(:internal_event) { create(:event, project: internal_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!(:public_event) { create(:event, project: public_project, author: project_owner) }
|
||||||
|
let_it_be(:issue) { create(:issue, project: public_project) }
|
||||||
let(:limit) { nil }
|
let(:limit) { nil }
|
||||||
let(:params) { { limit: limit } }
|
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
|
describe '#execute' do
|
||||||
context 'when profile is public' do
|
context 'when profile is public' do
|
||||||
|
@ -39,15 +40,106 @@ RSpec.describe UserRecentEventsFinder do
|
||||||
expect(finder.execute).to be_empty
|
expect(finder.execute).to be_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'design activity events' do
|
context 'events from multiple users' do
|
||||||
let_it_be(:event_a) { create(:design_event, author: project_owner) }
|
let_it_be(:second_user, reload: true) { create(:user) }
|
||||||
let_it_be(:event_b) { create(:design_event, author: project_owner) }
|
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
|
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(design_event)
|
||||||
expect(events).to include(event_b)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
/* global List */
|
/* global List */
|
||||||
/* global ListIssue */
|
/* global ListIssue */
|
||||||
|
|
||||||
import MockAdapter from 'axios-mock-adapter';
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import waitForPromises from 'helpers/wait_for_promises';
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
import axios from '~/lib/utils/axios_utils';
|
|
||||||
import BoardList from '~/boards/components/board_list_deprecated.vue';
|
import BoardList from '~/boards/components/board_list_deprecated.vue';
|
||||||
import eventHub from '~/boards/eventhub';
|
import eventHub from '~/boards/eventhub';
|
||||||
import '~/boards/models/issue';
|
|
||||||
import '~/boards/models/list';
|
|
||||||
import store from '~/boards/stores';
|
import store from '~/boards/stores';
|
||||||
import boardsStore from '~/boards/stores/boards_store';
|
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';
|
import { listObj, boardsMockInterceptor } from './mock_data';
|
||||||
|
|
||||||
const createComponent = ({ done, listIssueProps = {}, componentProps = {}, listProps = {} }) => {
|
const createComponent = ({ done, listIssueProps = {}, componentProps = {}, listProps = {} }) => {
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
/* global List */
|
/* global List */
|
||||||
/* global ListIssue */
|
/* global ListIssue */
|
||||||
|
|
||||||
import MockAdapter from 'axios-mock-adapter';
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
import Sortable from 'sortablejs';
|
import Sortable from 'sortablejs';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import axios from '~/lib/utils/axios_utils';
|
|
||||||
import BoardList from '~/boards/components/board_list_deprecated.vue';
|
import BoardList from '~/boards/components/board_list_deprecated.vue';
|
||||||
|
|
||||||
import '~/boards/models/issue';
|
import '~/boards/models/issue';
|
||||||
import '~/boards/models/list';
|
import '~/boards/models/list';
|
||||||
import store from '~/boards/stores';
|
import store from '~/boards/stores';
|
||||||
import boardsStore from '~/boards/stores/boards_store';
|
import boardsStore from '~/boards/stores/boards_store';
|
||||||
|
import axios from '~/lib/utils/axios_utils';
|
||||||
import { listObj, boardsMockInterceptor } from './mock_data';
|
import { listObj, boardsMockInterceptor } from './mock_data';
|
||||||
|
|
||||||
window.Sortable = Sortable;
|
window.Sortable = Sortable;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { nextTick } from 'vue';
|
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import { nextTick } from 'vue';
|
||||||
|
|
||||||
import { listObj } from 'jest/boards/mock_data';
|
import { listObj } from 'jest/boards/mock_data';
|
||||||
import BoardColumn from '~/boards/components/board_column.vue';
|
import BoardColumn from '~/boards/components/board_column.vue';
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
/* global ListAssignee, ListLabel, ListIssue */
|
/* global ListAssignee, ListLabel, ListIssue */
|
||||||
|
import { GlLabel } from '@gitlab/ui';
|
||||||
import { mount } from '@vue/test-utils';
|
import { mount } from '@vue/test-utils';
|
||||||
import { range } from 'lodash';
|
import { range } from 'lodash';
|
||||||
import '~/boards/models/label';
|
import '~/boards/models/label';
|
||||||
import '~/boards/models/assignee';
|
import '~/boards/models/assignee';
|
||||||
import '~/boards/models/issue';
|
import '~/boards/models/issue';
|
||||||
import '~/boards/models/list';
|
import '~/boards/models/list';
|
||||||
import { GlLabel } from '@gitlab/ui';
|
|
||||||
import IssueCardInner from '~/boards/components/issue_card_inner_deprecated.vue';
|
import IssueCardInner from '~/boards/components/issue_card_inner_deprecated.vue';
|
||||||
import store from '~/boards/stores';
|
import store from '~/boards/stores';
|
||||||
import { listObj } from './mock_data';
|
import { listObj } from './mock_data';
|
||||||
|
|
|
@ -2,16 +2,15 @@
|
||||||
/* global ListAssignee */
|
/* global ListAssignee */
|
||||||
/* global ListIssue */
|
/* global ListIssue */
|
||||||
/* global ListLabel */
|
/* global ListLabel */
|
||||||
|
|
||||||
import MockAdapter from 'axios-mock-adapter';
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
import waitForPromises from 'helpers/wait_for_promises';
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
import axios from '~/lib/utils/axios_utils';
|
|
||||||
import '~/boards/models/label';
|
import '~/boards/models/label';
|
||||||
import '~/boards/models/assignee';
|
import '~/boards/models/assignee';
|
||||||
import '~/boards/models/issue';
|
import '~/boards/models/issue';
|
||||||
import '~/boards/models/list';
|
import '~/boards/models/list';
|
||||||
import { ListType } from '~/boards/constants';
|
import { ListType } from '~/boards/constants';
|
||||||
import boardsStore from '~/boards/stores/boards_store';
|
import boardsStore from '~/boards/stores/boards_store';
|
||||||
|
import axios from '~/lib/utils/axios_utils';
|
||||||
import { listObj, listObjDuplicate, boardsMockInterceptor } from './mock_data';
|
import { listObj, listObjDuplicate, boardsMockInterceptor } from './mock_data';
|
||||||
|
|
||||||
describe('List model', () => {
|
describe('List model', () => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import 'vendor/jquery.endless-scroll';
|
import 'vendor/jquery.endless-scroll';
|
||||||
import MockAdapter from 'axios-mock-adapter';
|
|
||||||
import CommitsList from '~/commits';
|
import CommitsList from '~/commits';
|
||||||
import axios from '~/lib/utils/axios_utils';
|
import axios from '~/lib/utils/axios_utils';
|
||||||
import Pager from '~/pager';
|
import Pager from '~/pager';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import MockAdapter from 'axios-mock-adapter';
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
import axios from '~/lib/utils/axios_utils';
|
|
||||||
import { dismiss } from '~/feature_highlight/feature_highlight_helper';
|
import { dismiss } from '~/feature_highlight/feature_highlight_helper';
|
||||||
import { deprecatedCreateFlash as Flash } from '~/flash';
|
import { deprecatedCreateFlash as Flash } from '~/flash';
|
||||||
|
import axios from '~/lib/utils/axios_utils';
|
||||||
import httpStatusCodes from '~/lib/utils/http_status';
|
import httpStatusCodes from '~/lib/utils/http_status';
|
||||||
|
|
||||||
jest.mock('~/flash');
|
jest.mock('~/flash');
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
import DropdownUtils from '~/filtered_search/dropdown_utils';
|
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 DropdownUser from '~/filtered_search/dropdown_user';
|
||||||
import FilteredSearchTokenizer from '~/filtered_search/filtered_search_tokenizer';
|
import FilteredSearchTokenizer from '~/filtered_search/filtered_search_tokenizer';
|
||||||
import IssuableFilteredTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
|
import IssuableFilteredTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
/* eslint no-param-reassign: "off" */
|
/* eslint no-param-reassign: "off" */
|
||||||
|
|
||||||
import MockAdapter from 'axios-mock-adapter';
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
|
import GfmAutoComplete, { membersBeforeSave } from 'ee_else_ce/gfm_auto_complete';
|
||||||
import { initEmojiMock } from 'helpers/emoji';
|
import { initEmojiMock } from 'helpers/emoji';
|
||||||
import '~/lib/utils/jquery_at_who';
|
import '~/lib/utils/jquery_at_who';
|
||||||
import GfmAutoComplete, { membersBeforeSave } from 'ee_else_ce/gfm_auto_complete';
|
|
||||||
import { getJSONFixture } from 'helpers/fixtures';
|
import { getJSONFixture } from 'helpers/fixtures';
|
||||||
import { TEST_HOST } from 'helpers/test_constants';
|
import { TEST_HOST } from 'helpers/test_constants';
|
||||||
import waitForPromises from 'helpers/wait_for_promises';
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import Vuex from 'vuex';
|
|
||||||
import Vue from 'vue';
|
|
||||||
import MockAdapter from 'axios-mock-adapter';
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
import '~/behaviors/markdown/render_gfm';
|
|
||||||
import { Range } from 'monaco-editor';
|
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 { createComponentWithStore } from 'helpers/vue_mount_component_helper';
|
||||||
import waitForPromises from 'helpers/wait_for_promises';
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
import waitUsingRealTimer from 'helpers/wait_using_real_timer';
|
import waitUsingRealTimer from 'helpers/wait_using_real_timer';
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import timezoneMock from 'timezone-mock';
|
import timezoneMock from 'timezone-mock';
|
||||||
|
import * as datetimeUtility from '~/lib/utils/datetime_utility';
|
||||||
import { __, s__ } from '~/locale';
|
import { __, s__ } from '~/locale';
|
||||||
import '~/commons/bootstrap';
|
import '~/commons/bootstrap';
|
||||||
import * as datetimeUtility from '~/lib/utils/datetime_utility';
|
|
||||||
|
|
||||||
describe('Date time utils', () => {
|
describe('Date time utils', () => {
|
||||||
describe('timeFor', () => {
|
describe('timeFor', () => {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import MockAdapter from 'axios-mock-adapter';
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import Todos from '~/pages/dashboard/todos/index/todos';
|
|
||||||
import '~/lib/utils/common_utils';
|
import '~/lib/utils/common_utils';
|
||||||
import axios from '~/lib/utils/axios_utils';
|
import axios from '~/lib/utils/axios_utils';
|
||||||
import { addDelimiter } from '~/lib/utils/text_utility';
|
import { addDelimiter } from '~/lib/utils/text_utility';
|
||||||
import { visitUrl } from '~/lib/utils/url_utility';
|
import { visitUrl } from '~/lib/utils/url_utility';
|
||||||
|
import Todos from '~/pages/dashboard/todos/index/todos';
|
||||||
|
|
||||||
jest.mock('~/lib/utils/url_utility', () => ({
|
jest.mock('~/lib/utils/url_utility', () => ({
|
||||||
visitUrl: jest.fn().mockName('visitUrl'),
|
visitUrl: jest.fn().mockName('visitUrl'),
|
||||||
|
|
|
@ -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>
|
|
||||||
`;
|
|
|
@ -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>
|
|
||||||
`;
|
|
|
@ -115,10 +115,4 @@ describe('IntegrationView component', () => {
|
||||||
|
|
||||||
expect(findFormGroupLabel().text()).toBe('Enable foo');
|
expect(findFormGroupLabel().text()).toBe('Enable foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render IntegrationView properly', () => {
|
|
||||||
wrapper = createComponent();
|
|
||||||
|
|
||||||
expect(wrapper.element).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,27 +1,58 @@
|
||||||
|
import { GlButton } from '@gitlab/ui';
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||||
import IntegrationView from '~/profile/preferences/components/integration_view.vue';
|
import IntegrationView from '~/profile/preferences/components/integration_view.vue';
|
||||||
import ProfilePreferences from '~/profile/preferences/components/profile_preferences.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', () => {
|
describe('ProfilePreferences component', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
const defaultProvide = {
|
const defaultProvide = {
|
||||||
integrationViews: [],
|
integrationViews: [],
|
||||||
userFields,
|
userFields,
|
||||||
|
bodyClasses,
|
||||||
|
themes: [{ id: 1, css_class: 'foo' }],
|
||||||
|
profilePreferencesPath: '/update-profile',
|
||||||
|
formEl: document.createElement('form'),
|
||||||
};
|
};
|
||||||
|
|
||||||
function createComponent(options = {}) {
|
function createComponent(options = {}) {
|
||||||
const { props = {}, provide = {} } = options;
|
const { props = {}, provide = {}, attachTo } = options;
|
||||||
return shallowMount(ProfilePreferences, {
|
return extendedWrapper(
|
||||||
provide: {
|
shallowMount(ProfilePreferences, {
|
||||||
...defaultProvide,
|
provide: {
|
||||||
...provide,
|
...defaultProvide,
|
||||||
},
|
...provide,
|
||||||
propsData: props,
|
},
|
||||||
});
|
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(() => {
|
afterEach(() => {
|
||||||
wrapper.destroy();
|
wrapper.destroy();
|
||||||
wrapper = null;
|
wrapper = null;
|
||||||
|
@ -30,8 +61,8 @@ describe('ProfilePreferences component', () => {
|
||||||
it('should not render Integrations section', () => {
|
it('should not render Integrations section', () => {
|
||||||
wrapper = createComponent();
|
wrapper = createComponent();
|
||||||
const views = wrapper.findAll(IntegrationView);
|
const views = wrapper.findAll(IntegrationView);
|
||||||
const divider = wrapper.find('[data-testid="profile-preferences-integrations-rule"]');
|
const divider = findIntegrationsDivider();
|
||||||
const heading = wrapper.find('[data-testid="profile-preferences-integrations-heading"]');
|
const heading = findIntegrationsHeading();
|
||||||
|
|
||||||
expect(divider.exists()).toBe(false);
|
expect(divider.exists()).toBe(false);
|
||||||
expect(heading.exists()).toBe(false);
|
expect(heading.exists()).toBe(false);
|
||||||
|
@ -40,8 +71,8 @@ describe('ProfilePreferences component', () => {
|
||||||
|
|
||||||
it('should render Integration section', () => {
|
it('should render Integration section', () => {
|
||||||
wrapper = createComponent({ provide: { integrationViews } });
|
wrapper = createComponent({ provide: { integrationViews } });
|
||||||
const divider = wrapper.find('[data-testid="profile-preferences-integrations-rule"]');
|
const divider = findIntegrationsDivider();
|
||||||
const heading = wrapper.find('[data-testid="profile-preferences-integrations-heading"]');
|
const heading = findIntegrationsHeading();
|
||||||
const views = wrapper.findAll(IntegrationView);
|
const views = wrapper.findAll(IntegrationView);
|
||||||
|
|
||||||
expect(divider.exists()).toBe(true);
|
expect(divider.exists()).toBe(true);
|
||||||
|
@ -49,9 +80,84 @@ describe('ProfilePreferences component', () => {
|
||||||
expect(views).toHaveLength(integrationViews.length);
|
expect(views).toHaveLength(integrationViews.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render ProfilePreferences properly', () => {
|
describe('form submit', () => {
|
||||||
wrapper = createComponent({ provide: { integrationViews } });
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,3 +16,5 @@ export const integrationViews = [
|
||||||
export const userFields = {
|
export const userFields = {
|
||||||
foo_enabled: true,
|
foo_enabled: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const bodyClasses = 'ui-light-indigo ui-light gl-dark';
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
/* eslint-disable no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign */
|
/* eslint-disable no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign */
|
||||||
|
|
||||||
import AxiosMockAdapter from 'axios-mock-adapter';
|
import AxiosMockAdapter from 'axios-mock-adapter';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
|
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
|
||||||
|
import axios from '~/lib/utils/axios_utils';
|
||||||
import initSearchAutocomplete from '~/search_autocomplete';
|
import initSearchAutocomplete from '~/search_autocomplete';
|
||||||
import '~/lib/utils/common_utils';
|
import '~/lib/utils/common_utils';
|
||||||
import axios from '~/lib/utils/axios_utils';
|
|
||||||
|
|
||||||
describe('Search autocomplete dropdown', () => {
|
describe('Search autocomplete dropdown', () => {
|
||||||
let widget = null;
|
let widget = null;
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
|
import { config as testUtilsConfig } from '@vue/test-utils';
|
||||||
|
import * as jqueryMatchers from 'custom-jquery-matchers';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import 'jquery';
|
import 'jquery';
|
||||||
|
|
||||||
import * as jqueryMatchers from 'custom-jquery-matchers';
|
|
||||||
import { config as testUtilsConfig } from '@vue/test-utils';
|
|
||||||
import { setGlobalDateToFakeDate } from 'helpers/fake_date';
|
import { setGlobalDateToFakeDate } from 'helpers/fake_date';
|
||||||
import Translate from '~/vue_shared/translate';
|
import Translate from '~/vue_shared/translate';
|
||||||
import { getJSONFixture, loadHTMLFixture, setHTMLFixture } from './__helpers__/fixtures';
|
import { getJSONFixture, loadHTMLFixture, setHTMLFixture } from './__helpers__/fixtures';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { TEST_HOST } from 'helpers/test_constants';
|
import { TEST_HOST } from 'helpers/test_constants';
|
||||||
import extendStore from '~/ide/stores/extend';
|
|
||||||
import { initIde } from '~/ide';
|
import { initIde } from '~/ide';
|
||||||
import Editor from '~/ide/lib/editor';
|
import Editor from '~/ide/lib/editor';
|
||||||
|
import extendStore from '~/ide/stores/extend';
|
||||||
import { IDE_DATASET } from './mock_data';
|
import { IDE_DATASET } from './mock_data';
|
||||||
|
|
||||||
export default (container, { isRepoEmpty = false, path = '' } = {}) => {
|
export default (container, { isRepoEmpty = false, path = '' } = {}) => {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { waitForText } from 'helpers/wait_for_text';
|
|
||||||
import waitForPromises from 'helpers/wait_for_promises';
|
|
||||||
import { setTestTimeout } from 'helpers/timeout';
|
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 { createCommitId } from 'test_helpers/factories/commit_id';
|
||||||
|
import { useOverclockTimers } from 'test_helpers/utils/overclock_timers';
|
||||||
import * as ideHelper from './helpers/ide_helper';
|
import * as ideHelper from './helpers/ide_helper';
|
||||||
import startWebIDE from './helpers/start';
|
import startWebIDE from './helpers/start';
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { withValues } from '../utils/obj';
|
|
||||||
import { getCommit } from '../fixtures';
|
import { getCommit } from '../fixtures';
|
||||||
|
import { withValues } from '../utils/obj';
|
||||||
import { createCommitId } from './commit_id';
|
import { createCommitId } from './commit_id';
|
||||||
|
|
||||||
export const createNewCommit = ({ id = createCommitId(), message }, orig = getCommit()) => {
|
export const createNewCommit = ({ id = createCommitId(), message }, orig = getCommit()) => {
|
||||||
|
|
|
@ -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
|
// 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 { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
|
||||||
|
import { SIDEBAR_COLLAPSED_CLASS } from '~/contextual_sidebar';
|
||||||
import {
|
import {
|
||||||
calculateTop,
|
calculateTop,
|
||||||
showSubLevelItems,
|
showSubLevelItems,
|
||||||
|
@ -19,7 +20,6 @@ import {
|
||||||
setSidebar,
|
setSidebar,
|
||||||
subItemsMouseLeave,
|
subItemsMouseLeave,
|
||||||
} from '~/fly_out_nav';
|
} from '~/fly_out_nav';
|
||||||
import { SIDEBAR_COLLAPSED_CLASS } from '~/contextual_sidebar';
|
|
||||||
|
|
||||||
describe('Fly out sidebar navigation', () => {
|
describe('Fly out sidebar navigation', () => {
|
||||||
let el;
|
let el;
|
||||||
|
|
|
@ -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
|
* 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 { createLocalVue } from '@vue/test-utils';
|
||||||
import MockAdapter from 'axios-mock-adapter';
|
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 Dashboard from '~/monitoring/components/dashboard.vue';
|
||||||
import { createStore } from '~/monitoring/stores';
|
import { createStore } from '~/monitoring/stores';
|
||||||
import axios from '~/lib/utils/axios_utils';
|
|
||||||
import { mockApiEndpoint } from '../mock_data';
|
|
||||||
import { metricsDashboardPayload, dashboardProps } from '../fixture_data';
|
import { metricsDashboardPayload, dashboardProps } from '../fixture_data';
|
||||||
|
import { mockApiEndpoint } from '../mock_data';
|
||||||
import { setupStoreWithData } from '../store_utils';
|
import { setupStoreWithData } from '../store_utils';
|
||||||
|
|
||||||
const localVue = createLocalVue();
|
const localVue = createLocalVue();
|
||||||
|
|
|
@ -2,19 +2,18 @@
|
||||||
jasmine/no-global-setup, no-underscore-dangle, no-console
|
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 $ from 'jquery';
|
||||||
import 'core-js/features/set-immediate';
|
import 'core-js/features/set-immediate';
|
||||||
import 'vendor/jasmine-jquery';
|
import 'vendor/jasmine-jquery';
|
||||||
import '~/commons';
|
import '~/commons';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import jasmineDiff from 'jasmine-diff';
|
import { getDefaultAdapter } from '~/lib/utils/axios_utils';
|
||||||
import { config as testUtilsConfig } from '@vue/test-utils';
|
|
||||||
import Translate from '~/vue_shared/translate';
|
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 customMatchers from './matchers';
|
||||||
|
import { FIXTURES_PATH, TEST_HOST } from './test_constants';
|
||||||
|
|
||||||
// Tech debt issue TBD
|
// Tech debt issue TBD
|
||||||
testUtilsConfig.logModifiedComponents = false;
|
testUtilsConfig.logModifiedComponents = false;
|
||||||
|
|
|
@ -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
|
|
@ -4,12 +4,13 @@ require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe BulkImports::Groups::Graphql::GetGroupQuery do
|
RSpec.describe BulkImports::Groups::Graphql::GetGroupQuery do
|
||||||
describe '#variables' 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
|
it 'returns query variables based on entity information' do
|
||||||
expected = { full_path: entity.source_full_path }
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,13 @@ require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe BulkImports::Groups::Graphql::GetLabelsQuery do
|
RSpec.describe BulkImports::Groups::Graphql::GetLabelsQuery do
|
||||||
describe '#variables' 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
|
it 'returns query variables based on entity information' do
|
||||||
expected = { full_path: entity.source_full_path, cursor: entity.next_page_for }
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -5,11 +5,12 @@ require 'spec_helper'
|
||||||
RSpec.describe BulkImports::Groups::Graphql::GetMembersQuery do
|
RSpec.describe BulkImports::Groups::Graphql::GetMembersQuery do
|
||||||
it 'has a valid query' do
|
it 'has a valid query' do
|
||||||
entity = create(:bulk_import_entity)
|
entity = create(:bulk_import_entity)
|
||||||
|
context = BulkImports::Pipeline::Context.new(entity)
|
||||||
|
|
||||||
query = GraphQL::Query.new(
|
query = GraphQL::Query.new(
|
||||||
GitlabSchema,
|
GitlabSchema,
|
||||||
described_class.to_s,
|
described_class.to_s,
|
||||||
variables: described_class.variables(entity)
|
variables: described_class.variables(context)
|
||||||
)
|
)
|
||||||
result = GitlabSchema.static_validator.validate(query)
|
result = GitlabSchema.static_validator.validate(query)
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,9 @@ require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe BulkImports::Importers::GroupImporter do
|
RSpec.describe BulkImports::Importers::GroupImporter do
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
|
let(:group) { create(:group) }
|
||||||
let(:bulk_import) { create(:bulk_import) }
|
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(:bulk_import_configuration) { create(:bulk_import_configuration, bulk_import: bulk_import) }
|
||||||
let(:context) { BulkImports::Pipeline::Context.new(bulk_import_entity) }
|
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::SubgroupEntitiesPipeline, context: context
|
||||||
expect_to_run_pipeline BulkImports::Groups::Pipelines::MembersPipeline, 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 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
|
subject.execute
|
||||||
|
|
||||||
|
@ -29,7 +34,7 @@ RSpec.describe BulkImports::Importers::GroupImporter do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when failed' do
|
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
|
it 'does not transition entity to finished state' do
|
||||||
allow(bulk_import_entity).to receive(:start!)
|
allow(bulk_import_entity).to receive(:start!)
|
||||||
|
|
|
@ -11,6 +11,41 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do
|
||||||
expect { |b| subject.observe(migration, &b) }.to yield_control
|
expect { |b| subject.observe(migration, &b) }.to yield_control
|
||||||
end
|
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
|
context 'on successful execution' do
|
||||||
subject { described_class.new.observe(migration) {} }
|
subject { described_class.new.observe(migration) {} }
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -2831,6 +2831,79 @@ RSpec.describe User do
|
||||||
end
|
end
|
||||||
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
|
describe '.find_by_private_commit_email' do
|
||||||
context 'with email' do
|
context 'with email' do
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
|
@ -652,6 +652,34 @@ RSpec.describe API::Users do
|
||||||
expect(response).to match_response_schema('public_api/v4/user/basic')
|
expect(response).to match_response_schema('public_api/v4/user/basic')
|
||||||
expect(json_response.keys).not_to include 'created_at'
|
expect(json_response.keys).not_to include 'created_at'
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
it "returns a 404 error if user id not found" do
|
it "returns a 404 error if user id not found" do
|
||||||
|
@ -688,6 +716,128 @@ RSpec.describe API::Users do
|
||||||
end
|
end
|
||||||
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
|
describe "POST /users" do
|
||||||
it "creates user" do
|
it "creates user" do
|
||||||
expect do
|
expect do
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue