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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/behaviors/markdown/editor_extensions.js21
-rw-r--r--app/assets/javascripts/boards/index.js24
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue4
-rw-r--r--app/assets/javascripts/main.js1
-rw-r--r--app/assets/javascripts/notes.js6
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/commit/show/index.js3
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js10
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue2
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines_mixin.js4
-rw-r--r--app/assets/javascripts/profile/preferences/components/profile_preferences.vue105
-rw-r--r--app/assets/javascripts/profile/preferences/constants.js22
-rw-r--r--app/assets/javascripts/profile/preferences/profile_preferences_bundle.js20
-rw-r--r--app/assets/javascripts/repository/components/preview/index.vue2
-rw-r--r--app/assets/javascripts/terraform/index.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue2
-rw-r--r--app/controllers/dashboard_controller.rb19
-rw-r--r--app/controllers/profiles/preferences_controller.rb25
-rw-r--r--app/controllers/users_controller.rb46
-rw-r--r--app/finders/user_recent_events_finder.rb32
-rw-r--r--app/helpers/users_helper.rb2
-rw-r--r--app/models/user.rb30
-rw-r--r--app/models/users/user_follow_user.rb7
-rw-r--r--app/views/dashboard/_activity_head.html.haml7
-rw-r--r--app/views/profiles/preferences/show.html.haml15
-rw-r--r--app/views/profiles/preferences/update.js.erb20
-rw-r--r--app/views/shared/empty_states/_profile_tabs.html.haml1
-rw-r--r--app/views/shared/users/_user.html.haml13
-rw-r--r--app/views/shared/users/index.html.haml20
-rw-r--r--app/views/users/show.html.haml33
31 files changed, 380 insertions, 123 deletions
diff --git a/app/assets/javascripts/behaviors/markdown/editor_extensions.js b/app/assets/javascripts/behaviors/markdown/editor_extensions.js
index ee50075862a..b512e4dbc8b 100644
--- a/app/assets/javascripts/behaviors/markdown/editor_extensions.js
+++ b/app/assets/javascripts/behaviors/markdown/editor_extensions.js
@@ -22,27 +22,24 @@ import Heading from './nodes/heading';
import HorizontalRule from './nodes/horizontal_rule';
import Image from './nodes/image';
+import ListItem from './nodes/list_item';
+import OrderedList from './nodes/ordered_list';
+import OrderedTaskList from './nodes/ordered_task_list';
+import Paragraph from './nodes/paragraph';
import Reference from './nodes/reference';
+import Summary from './nodes/summary';
import Table from './nodes/table';
-import TableHead from './nodes/table_head';
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 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 OrderedTaskList from './nodes/ordered_task_list';
-import Paragraph from './nodes/paragraph';
-
-import Summary from './nodes/summary';
import Text from './nodes/text';
+import Video from './nodes/video';
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb transform
// GitLab Flavored Markdown (GFM) to HTML.
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index ac7d8e542c4..15f7e2191a2 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { mapActions, mapGetters } from 'vuex';
import 'ee_else_ce/boards/models/issue';
import 'ee_else_ce/boards/models/list';
-import VueApollo from 'vue-apollo';
import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar';
import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown';
import boardConfigToggle from 'ee_else_ce/boards/config_toggle';
@@ -15,33 +15,31 @@ import {
} from 'ee_else_ce/boards/ee_functions';
import toggleEpicsSwimlanes from 'ee_else_ce/boards/toggle_epics_swimlanes';
import toggleLabels from 'ee_else_ce/boards/toggle_labels';
-
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
import BoardContent from '~/boards/components/board_content.vue';
import BoardExtraActions from '~/boards/components/board_extra_actions.vue';
-import { deprecatedCreateFlash as Flash } from '~/flash';
-import createDefaultClient from '~/lib/graphql';
-import { __ } from '~/locale';
import './models/label';
import './models/assignee';
-
-import toggleFocusMode from '~/boards/toggle_focus';
-import FilteredSearchBoards from '~/boards/filtered_search_boards';
-import eventHub from '~/boards/eventhub';
-import sidebarEventHub from '~/sidebar/event_hub';
import '~/boards/models/milestone';
import '~/boards/models/project';
+import '~/boards/filters/due_date_filters';
+import BoardAddIssuesModal from '~/boards/components/modal/index.vue';
+import eventHub from '~/boards/eventhub';
+import FilteredSearchBoards from '~/boards/filtered_search_boards';
+import modalMixin from '~/boards/mixins/modal_mixins';
import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
import ModalStore from '~/boards/stores/modal_store';
-import modalMixin from '~/boards/mixins/modal_mixins';
-import '~/boards/filters/due_date_filters';
-import BoardAddIssuesModal from '~/boards/components/modal/index.vue';
+import toggleFocusMode from '~/boards/toggle_focus';
+import { deprecatedCreateFlash as Flash } from '~/flash';
+import createDefaultClient from '~/lib/graphql';
import {
NavigationType,
convertObjectPropsToCamelCase,
parseBoolean,
} from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
+import sidebarEventHub from '~/sidebar/event_hub';
import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher';
Vue.use(VueApollo);
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index 4362263b0dd..787152d00ef 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -1,13 +1,13 @@
<script>
import { GlButton, GlLoadingIcon, GlModal, GlLink } from '@gitlab/ui';
import { getParameterByName } from '~/lib/utils/common_utils';
+import SvgBlankState from '~/pipelines/components/pipelines_list/blank_state.vue';
+import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import eventHub from '~/pipelines/event_hub';
import PipelinesMixin from '~/pipelines/mixins/pipelines_mixin';
import PipelinesService from '~/pipelines/services/pipelines_service';
import PipelineStore from '~/pipelines/stores/pipelines_store';
-import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
-import SvgBlankState from '~/pipelines/components/pipelines_list/blank_state.vue';
export default {
components: {
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 376d93a9783..32ecc0036bd 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -1,4 +1,5 @@
/* global $ */
+/* eslint-disable import/order */
import jQuery from 'jquery';
import Cookies from 'js-cookie';
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 072983b4285..8ed40f36103 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -10,13 +10,13 @@ class-methods-use-this */
old_notes_spec.js is the spec for the legacy, jQuery notes application. It has nothing to do with the new, fancy Vue notes app.
*/
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import Autosize from 'autosize';
import $ from 'jquery';
-import '~/lib/utils/jquery_at_who';
import Cookies from 'js-cookie';
import { escape, uniqueId } from 'lodash';
-import Autosize from 'autosize';
import Vue from 'vue';
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import '~/lib/utils/jquery_at_who';
import AjaxCache from '~/lib/utils/ajax_cache';
import syntaxHighlight from '~/syntax_highlight';
import Autosave from './autosave';
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index 610c643c805..61ff1c95a38 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -1,11 +1,11 @@
import Vue from 'vue';
+import PipelineTourSuccessModal from '~/blob/pipeline_tour_success_modal.vue';
import BlobViewer from '~/blob/viewer/index';
import GpgBadges from '~/gpg_badges';
import initBlob from '~/pages/projects/init_blob';
import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import '~/sourcegraph/load';
-import PipelineTourSuccessModal from '~/blob/pipeline_tour_success_modal.vue';
document.addEventListener('DOMContentLoaded', () => {
new BlobViewer(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js
index 6baf8c6595e..90a663802d2 100644
--- a/app/assets/javascripts/pages/projects/commit/show/index.js
+++ b/app/assets/javascripts/pages/projects/commit/show/index.js
@@ -1,6 +1,6 @@
/* eslint-disable no-new */
-
import $ from 'jquery';
+import loadAwardsHandler from '~/awards_handler';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import Diff from '~/diff';
import flash from '~/flash';
@@ -14,7 +14,6 @@ import { initCommitBoxInfo } from '~/projects/commit_box/info';
import syntaxHighlight from '~/syntax_highlight';
import ZenMode from '~/zen_mode';
import '~/sourcegraph/load';
-import loadAwardsHandler from '~/awards_handler';
const hasPerfBar = document.querySelector('.with-performance-bar');
const performanceHeight = hasPerfBar ? 35 : 0;
diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index e3aac07097b..80e14842f51 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -141,7 +141,15 @@ export default class UserTabs {
this.loadOverviewTab();
}
- const loadableActions = ['groups', 'contributed', 'projects', 'starred', 'snippets'];
+ const loadableActions = [
+ 'groups',
+ 'contributed',
+ 'projects',
+ 'starred',
+ 'snippets',
+ 'followers',
+ 'following',
+ ];
if (loadableActions.indexOf(action) > -1) {
this.loadTab(action, endpoint);
}
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
index a45adc766e3..48009a9fcb8 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
@@ -10,11 +10,11 @@ import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, FILTER_TAG_IDENTIFIER } from '../
import PipelinesMixin from '../../mixins/pipelines_mixin';
import PipelinesService from '../../services/pipelines_service';
import { validateParams } from '../../utils';
+import SvgBlankState from './blank_state.vue';
import EmptyState from './empty_state.vue';
import NavigationControls from './nav_controls.vue';
import PipelinesFilteredSearch from './pipelines_filtered_search.vue';
import PipelinesTableComponent from './pipelines_table.vue';
-import SvgBlankState from './blank_state.vue';
export default {
components: {
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
index 8444d43513b..2321728e30c 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
@@ -1,9 +1,9 @@
import Visibility from 'visibilityjs';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import Poll from '~/lib/utils/poll';
import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
-import { validateParams } from '~/pipelines/utils';
+import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
+import { validateParams } from '~/pipelines/utils';
import { CANCEL_REQUEST } from '../constants';
import eventHub from '../event_hub';
diff --git a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
index 8b2006b7c5b..184ee3810ac 100644
--- a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
+++ b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
@@ -1,31 +1,92 @@
<script>
-import { s__ } from '~/locale';
+import { GlButton } from '@gitlab/ui';
+import createFlash, { FLASH_TYPES } from '~/flash';
+import { INTEGRATION_VIEW_CONFIGS, i18n } from '../constants';
import IntegrationView from './integration_view.vue';
-const INTEGRATION_VIEW_CONFIGS = {
- sourcegraph: {
- title: s__('ProfilePreferences|Sourcegraph'),
- label: s__('ProfilePreferences|Enable integrated code intelligence on code views'),
- formName: 'sourcegraph_enabled',
- },
- gitpod: {
- title: s__('ProfilePreferences|Gitpod'),
- label: s__('ProfilePreferences|Enable Gitpod integration'),
- formName: 'gitpod_enabled',
- },
-};
+function updateClasses(bodyClasses = '', applicationTheme, layout) {
+ // Remove body class for any previous theme, re-add current one
+ document.body.classList.remove(...bodyClasses.split(' '));
+ document.body.classList.add(applicationTheme);
+
+ // Toggle container-fluid class
+ if (layout === 'fluid') {
+ document
+ .querySelector('.content-wrapper .container-fluid')
+ .classList.remove('container-limited');
+ } else {
+ document.querySelector('.content-wrapper .container-fluid').classList.add('container-limited');
+ }
+}
export default {
name: 'ProfilePreferences',
components: {
IntegrationView,
+ GlButton,
},
inject: {
integrationViews: {
default: [],
},
+ themes: {
+ default: [],
+ },
+ userFields: {
+ default: {},
+ },
+ formEl: 'formEl',
+ profilePreferencesPath: 'profilePreferencesPath',
+ bodyClasses: 'bodyClasses',
},
integrationViewConfigs: INTEGRATION_VIEW_CONFIGS,
+ i18n,
+ data() {
+ return {
+ isSubmitEnabled: true,
+ };
+ },
+ computed: {
+ applicationThemes() {
+ return this.themes.reduce((themes, theme) => {
+ const { id, ...rest } = theme;
+ return { ...themes, [id]: rest };
+ }, {});
+ },
+ },
+ created() {
+ this.formEl.addEventListener('ajax:beforeSend', this.handleLoading);
+ this.formEl.addEventListener('ajax:success', this.handleSuccess);
+ this.formEl.addEventListener('ajax:error', this.handleError);
+ },
+ beforeDestroy() {
+ this.formEl.removeEventListener('ajax:beforeSend', this.handleLoading);
+ this.formEl.removeEventListener('ajax:success', this.handleSuccess);
+ this.formEl.removeEventListener('ajax:error', this.handleError);
+ },
+ methods: {
+ handleLoading() {
+ this.isSubmitEnabled = false;
+ },
+ handleSuccess(customEvent) {
+ const formData = new FormData(this.formEl);
+ updateClasses(
+ this.bodyClasses,
+ this.applicationThemes[formData.get('user[theme_id]')].css_class,
+ this.selectedLayout,
+ );
+ const { message = this.$options.i18n.defaultSuccess, type = FLASH_TYPES.NOTICE } =
+ customEvent?.detail?.[0] || {};
+ createFlash({ message, type });
+ this.isSubmitEnabled = true;
+ },
+ handleError(customEvent) {
+ const { message = this.$options.i18n.defaultError, type = FLASH_TYPES.ALERT } =
+ customEvent?.detail?.[0] || {};
+ createFlash({ message, type });
+ this.isSubmitEnabled = true;
+ },
+ },
};
</script>
@@ -36,10 +97,10 @@ export default {
</div>
<div v-if="integrationViews.length" class="col-lg-4 profile-settings-sidebar">
<h4 class="gl-mt-0" data-testid="profile-preferences-integrations-heading">
- {{ s__('ProfilePreferences|Integrations') }}
+ {{ $options.i18n.integrations }}
</h4>
<p>
- {{ s__('ProfilePreferences|Customize integrations with third party services.') }}
+ {{ $options.i18n.integrationsDescription }}
</p>
</div>
<div v-if="integrationViews.length" class="col-lg-8">
@@ -52,5 +113,19 @@ export default {
:config="$options.integrationViewConfigs[view.name]"
/>
</div>
+ <div class="col-lg-4 profile-settings-sidebar"></div>
+ <div class="col-lg-8">
+ <div class="form-group">
+ <gl-button
+ variant="success"
+ name="commit"
+ type="submit"
+ :disabled="!isSubmitEnabled"
+ :value="$options.i18n.saveChanges"
+ >
+ {{ $options.i18n.saveChanges }}
+ </gl-button>
+ </div>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/profile/preferences/constants.js b/app/assets/javascripts/profile/preferences/constants.js
new file mode 100644
index 00000000000..ea8464ba065
--- /dev/null
+++ b/app/assets/javascripts/profile/preferences/constants.js
@@ -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.'),
+};
diff --git a/app/assets/javascripts/profile/preferences/profile_preferences_bundle.js b/app/assets/javascripts/profile/preferences/profile_preferences_bundle.js
index 744e0174a4e..6520e68d41c 100644
--- a/app/assets/javascripts/profile/preferences/profile_preferences_bundle.js
+++ b/app/assets/javascripts/profile/preferences/profile_preferences_bundle.js
@@ -3,16 +3,20 @@ import ProfilePreferences from './components/profile_preferences.vue';
export default () => {
const el = document.querySelector('#js-profile-preferences-app');
- const shouldParse = ['integrationViews', 'userFields'];
+ const formEl = document.querySelector('#profile-preferences-form');
+ const shouldParse = ['integrationViews', 'themes', 'userFields'];
- const provide = Object.keys(el.dataset).reduce((memo, key) => {
- let value = el.dataset[key];
- if (shouldParse.includes(key)) {
- value = JSON.parse(value);
- }
+ const provide = Object.keys(el.dataset).reduce(
+ (memo, key) => {
+ let value = el.dataset[key];
+ if (shouldParse.includes(key)) {
+ value = JSON.parse(value);
+ }
- return { ...memo, [key]: value };
- }, {});
+ return { ...memo, [key]: value };
+ },
+ { formEl },
+ );
return new Vue({
el,
diff --git a/app/assets/javascripts/repository/components/preview/index.vue b/app/assets/javascripts/repository/components/preview/index.vue
index e2c3f3b81ee..b74c2333148 100644
--- a/app/assets/javascripts/repository/components/preview/index.vue
+++ b/app/assets/javascripts/repository/components/preview/index.vue
@@ -1,8 +1,8 @@
<script>
/* eslint-disable vue/no-v-html */
+import { GlIcon, GlLink, GlLoadingIcon } from '@gitlab/ui';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
-import { GlIcon, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { handleLocationHash } from '~/lib/utils/common_utils';
import readmeQuery from '../../queries/readme.query.graphql';
diff --git a/app/assets/javascripts/terraform/index.js b/app/assets/javascripts/terraform/index.js
index 2288bab2fa8..3f986423836 100644
--- a/app/assets/javascripts/terraform/index.js
+++ b/app/assets/javascripts/terraform/index.js
@@ -1,5 +1,5 @@
-import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import { GlToast } from '@gitlab/ui';
+import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
index 24386c90954..3790a509f26 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
@@ -1,9 +1,8 @@
<script>
/* eslint-disable vue/no-v-html */
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
-
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { forEach, escape } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 1699329bf4d..25d01dc550f 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -1,9 +1,9 @@
<script>
/* eslint-disable vue/no-v-html */
+import { GlIcon } from '@gitlab/ui';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
import { unescape } from 'lodash';
-import { GlIcon } from '@gitlab/ui';
import { deprecatedCreateFlash as Flash } from '~/flash';
import GLForm from '~/gl_form';
import axios from '~/lib/utils/axios_utils';
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index a88cf64d842..29cb60ad3cc 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -33,6 +33,21 @@ class DashboardController < Dashboard::ApplicationController
protected
def load_events
+ @events =
+ if params[:filter] == "followed"
+ load_user_events
+ else
+ load_project_events
+ end
+
+ Events::RenderService.new(current_user).execute(@events)
+ end
+
+ def load_user_events
+ UserRecentEventsFinder.new(current_user, current_user.followees, event_filter, params).execute
+ end
+
+ def load_project_events
projects =
if params[:filter] == "starred"
ProjectsFinder.new(current_user: current_user, params: { starred: true }).execute
@@ -40,12 +55,10 @@ class DashboardController < Dashboard::ApplicationController
current_user.authorized_projects
end
- @events = EventCollection
+ EventCollection
.new(projects, offset: params[:offset].to_i, filter: event_filter)
.to_a
.map(&:present)
-
- Events::RenderService.new(current_user).execute(@events)
end
def set_show_full_reference
diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index 4d88491e9a8..add5046e213 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -9,23 +9,18 @@ class Profiles::PreferencesController < Profiles::ApplicationController
end
def update
- begin
- result = Users::UpdateService.new(current_user, preferences_params.merge(user: user)).execute
-
- if result[:status] == :success
- flash[:notice] = _('Preferences saved.')
- else
- flash[:alert] = _('Failed to save preferences.')
- end
- rescue ArgumentError => e
- # Raised when `dashboard` is given an invalid value.
- flash[:alert] = _("Failed to save preferences (%{error_message}).") % { error_message: e.message }
- end
+ result = Users::UpdateService.new(current_user, preferences_params.merge(user: user)).execute
+ if result[:status] == :success
+ message = _('Preferences saved.')
- respond_to do |format|
- format.html { redirect_to profile_preferences_path }
- format.js
+ render json: { type: :notice, message: message }
+ else
+ render status: :bad_request, json: { type: :alert, message: _('Failed to save preferences.') }
end
+ rescue ArgumentError => e
+ # Raised when `dashboard` is given an invalid value.
+ message = _("Failed to save preferences (%{error_message}).") % { error_message: e.message }
+ render status: :bad_request, json: { type: :alert, message: message }
end
private
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index c9152128638..54d97f588fc 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class UsersController < ApplicationController
+ include InternalRedirect
include RoutableActions
include RendersMemberAccess
include RendersProjectsList
@@ -13,13 +14,15 @@ class UsersController < ApplicationController
contributed: false,
snippets: true,
calendar: false,
+ followers: false,
+ following: false,
calendar_activities: true
skip_before_action :authenticate_user!
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
before_action :user, except: [:exists, :suggests, :ssh_keys]
before_action :authorize_read_user_profile!,
- only: [:calendar, :calendar_activities, :groups, :projects, :contributed, :starred, :snippets]
+ only: [:calendar, :calendar_activities, :groups, :projects, :contributed, :starred, :snippets, :followers, :following]
feature_category :users
@@ -97,6 +100,18 @@ class UsersController < ApplicationController
present_projects(@starred_projects)
end
+ def followers
+ @user_followers = user.followers.page(params[:page])
+
+ present_users(@user_followers)
+ end
+
+ def following
+ @user_following = user.followees.page(params[:page])
+
+ present_users(@user_following)
+ end
+
def present_projects(projects)
skip_pagination = Gitlab::Utils.to_boolean(params[:skip_pagination])
skip_namespace = Gitlab::Utils.to_boolean(params[:skip_namespace])
@@ -146,6 +161,22 @@ class UsersController < ApplicationController
render json: { exists: exists, suggests: suggestions }
end
+ def follow
+ current_user.follow(user)
+
+ redirect_path = referer_path(request) || @user
+
+ redirect_to redirect_path
+ end
+
+ def unfollow
+ current_user.unfollow(user)
+
+ redirect_path = referer_path(request) || @user
+
+ redirect_to redirect_path
+ end
+
private
def user
@@ -169,7 +200,7 @@ class UsersController < ApplicationController
end
def load_events
- @events = UserRecentEventsFinder.new(current_user, user, params).execute
+ @events = UserRecentEventsFinder.new(current_user, user, nil, params).execute
Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end
@@ -216,6 +247,17 @@ class UsersController < ApplicationController
def authorize_read_user_profile!
access_denied! unless can?(current_user, :read_user_profile, user)
end
+
+ def present_users(users)
+ respond_to do |format|
+ format.html { render 'show' }
+ format.json do
+ render json: {
+ html: view_to_html_string("shared/users/index", users: users)
+ }
+ end
+ end
+ end
end
UsersController.prepend_if_ee('EE::UsersController')
diff --git a/app/finders/user_recent_events_finder.rb b/app/finders/user_recent_events_finder.rb
index c9a1c918365..596a413782e 100644
--- a/app/finders/user_recent_events_finder.rb
+++ b/app/finders/user_recent_events_finder.rb
@@ -15,27 +15,49 @@ class UserRecentEventsFinder
requires_cross_project_access
- attr_reader :current_user, :target_user, :params
+ attr_reader :current_user, :target_user, :params, :event_filter
DEFAULT_LIMIT = 20
MAX_LIMIT = 100
- def initialize(current_user, target_user, params = {})
+ def initialize(current_user, target_user, event_filter, params = {})
@current_user = current_user
@target_user = target_user
@params = params
+ @event_filter = event_filter || EventFilter.new(EventFilter::ALL)
end
def execute
+ if target_user.is_a? User
+ execute_single
+ else
+ execute_multi
+ end
+ end
+
+ private
+
+ def execute_single
return Event.none unless can?(current_user, :read_user_profile, target_user)
- target_events
+ event_filter.apply_filter(target_events
.with_associations
.limit_recent(limit, params[:offset])
- .order_created_desc
+ .order_created_desc)
end
- private
+ # rubocop: disable CodeReuse/ActiveRecord
+ def execute_multi
+ users = []
+ @target_user.each do |user|
+ users.append(user.id) if can?(current_user, :read_user_profile, user)
+ end
+
+ return Event.none if users.empty?
+
+ event_filter.apply_filter(Event.where(author: users).limit_recent(limit, params[:offset] || 0))
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def target_events
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 1ea2d4412b1..1979426f844 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -242,7 +242,7 @@ module UsersHelper
tabs = []
if can?(current_user, :read_user_profile, @user)
- tabs += [:overview, :activity, :groups, :contributed, :projects, :starred, :snippets]
+ tabs += [:overview, :activity, :groups, :contributed, :projects, :starred, :snippets, :followers, :following]
end
tabs
diff --git a/app/models/user.rb b/app/models/user.rb
index 4a2ca64fbe9..1f8b680c7e5 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -116,6 +116,13 @@ class User < ApplicationRecord
has_one :user_synced_attributes_metadata, autosave: true
has_one :aws_role, class_name: 'Aws::Role'
+ # Followers
+ has_many :followed_users, foreign_key: :follower_id, class_name: 'Users::UserFollowUser'
+ has_many :followees, through: :followed_users
+
+ has_many :following_users, foreign_key: :followee_id, class_name: 'Users::UserFollowUser'
+ has_many :followers, through: :following_users
+
# Groups
has_many :members
has_many :group_members, -> { where(requested_at: nil).where("access_level >= ?", Gitlab::Access::GUEST) }, source: 'GroupMember'
@@ -1442,6 +1449,29 @@ class User < ApplicationRecord
end
end
+ def following?(user)
+ self.followees.exists?(user.id)
+ end
+
+ def follow(user)
+ return false if self.id == user.id
+
+ begin
+ followee = Users::UserFollowUser.create(follower_id: self.id, followee_id: user.id)
+ self.followees.reset if followee.persisted?
+ rescue ActiveRecord::RecordNotUnique
+ false
+ end
+ end
+
+ def unfollow(user)
+ if Users::UserFollowUser.where(follower_id: self.id, followee_id: user.id).delete_all > 0
+ self.followees.reset
+ else
+ false
+ end
+ end
+
def manageable_namespaces
@manageable_namespaces ||= [namespace] + manageable_groups
end
diff --git a/app/models/users/user_follow_user.rb b/app/models/users/user_follow_user.rb
new file mode 100644
index 00000000000..a94239a746c
--- /dev/null
+++ b/app/models/users/user_follow_user.rb
@@ -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
diff --git a/app/views/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml
index 3f39555a1d4..0daadd20f54 100644
--- a/app/views/dashboard/_activity_head.html.haml
+++ b/app/views/dashboard/_activity_head.html.haml
@@ -5,7 +5,10 @@
%ul.nav-links.nav.nav-tabs
%li{ class: active_when(params[:filter].nil?) }>
= link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do
- Your projects
+ = _('Your projects')
%li{ class: active_when(params[:filter] == 'starred') }>
= link_to activity_dashboard_path(filter: 'starred'), data: {placement: 'right'} do
- Starred projects
+ = _('Starred projects')
+ %li{ class: active_when(params[:filter] == 'followed') }>
+ = link_to activity_dashboard_path(filter: 'followed'), data: {placement: 'right'} do
+ = _('Followed users')
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 169eef1fa9b..cd76a67b692 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -1,13 +1,14 @@
- page_title _('Preferences')
- @content_class = "limit-container-width" unless fluid_layout
-- user_fields = { gitpod_enabled: @user.gitpod_enabled, sourcegraph_enabled: @user.sourcegraph_enabled }
- user_theme_id = Gitlab::Themes.for_user(@user).id
-- data_attributes = { integration_views: integration_views.to_json, user_fields: user_fields.to_json }
+- user_fields = { theme: user_theme_id, gitpod_enabled: @user.gitpod_enabled, sourcegraph_enabled: @user.sourcegraph_enabled }.to_json
+- @themes = Gitlab::Themes::THEMES.to_json
+- data_attributes = { themes: @themes, integration_views: integration_views.to_json, user_fields: user_fields, body_classes: Gitlab::Themes.body_classes, profile_preferences_path: profile_preferences_path }
- Gitlab::Themes.each do |theme|
= stylesheet_link_tag "themes/#{theme.css_filename}" if theme.css_filename
-= form_for @user, url: profile_preferences_path, remote: true, method: :put do |f|
+= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { id: "profile-preferences-form" } do |f|
.row.gl-mt-3.js-preferences-form.js-search-settings-section
.col-lg-4.application-theme#navigation-theme
%h4.gl-mt-0
@@ -143,10 +144,4 @@
.form-text.text-muted
= s_('Preferences|For example: 30 mins ago.')
- #js-profile-preferences-app{ data: data_attributes, user_fields: user_fields.to_json }
-
- .row.gl-mt-3.js-preferences-form
- .col-lg-4.profile-settings-sidebar
- .col-lg-8
- .form-group
- = f.submit _('Save changes'), class: 'gl-button btn btn-success'
+ #js-profile-preferences-app{ data: data_attributes }
diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb
deleted file mode 100644
index 241262880c1..00000000000
--- a/app/views/profiles/preferences/update.js.erb
+++ /dev/null
@@ -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 %>
diff --git a/app/views/shared/empty_states/_profile_tabs.html.haml b/app/views/shared/empty_states/_profile_tabs.html.haml
index 7780a144a26..abcf9740200 100644
--- a/app/views/shared/empty_states/_profile_tabs.html.haml
+++ b/app/views/shared/empty_states/_profile_tabs.html.haml
@@ -1,5 +1,6 @@
- current_user_empty_message_description = local_assigns.fetch(:current_user_empty_message_description, nil)
- secondary_button_link = local_assigns.fetch(:secondary_button_link, nil)
+- primary_button_link = local_assigns.fetch(:primary_button_link, nil)
.nothing-here-block
.svg-content
diff --git a/app/views/shared/users/_user.html.haml b/app/views/shared/users/_user.html.haml
new file mode 100644
index 00000000000..f92c12102bb
--- /dev/null
+++ b/app/views/shared/users/_user.html.haml
@@ -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
diff --git a/app/views/shared/users/index.html.haml b/app/views/shared/users/index.html.haml
new file mode 100644
index 00000000000..dd6b14d6be2
--- /dev/null
+++ b/app/views/shared/users/index.html.haml
@@ -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}
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index debfe57dbd7..cdaa739a7b3 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -26,6 +26,13 @@
= link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: link_classes + 'btn gl-button btn-default btn-icon',
title: s_('UserProfile|Report abuse'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= sprite_icon('error')
+ - if current_user && current_user.id != @user.id
+ - if current_user.following?(@user)
+ = link_to user_unfollow_path(@user, :json) , class: link_classes + 'btn gl-button btn-default', method: :post do
+ = _('Unfollow')
+ - else
+ = link_to user_follow_path(@user, :json) , class: link_classes + 'btn gl-button btn-default', method: :post do
+ = _('Follow')
- if can?(current_user, :read_user_profile, @user)
= link_to user_path(@user, rss_url_options), class: link_classes + 'btn gl-button btn-default btn-icon has-tooltip',
title: s_('UserProfile|Subscribe'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
@@ -89,6 +96,16 @@
- unless @user.public_email.blank?
.profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline.mt-1.mt-sm-0
= link_to @user.public_email, "mailto:#{@user.public_email}", class: 'text-link', itemprop: 'email'
+ .cover-desc.gl-text-gray-900.gl-mb-2.mb-sm-2
+ = sprite_icon('users', css_class: 'gl-vertical-align-middle gl-text-gray-500')
+ .profile-link-holder.middle-dot-divider
+ = link_to user_followers_path, class: 'text-link' do
+ - count = @user.followers.count
+ = n_('1 follower', '%{count} followers', count) % { count: count }
+ .profile-link-holder.middle-dot-divider
+ = link_to user_following_path, class: 'text-link' do
+ = @user.followees.count
+ = _('following')
- if @user.bio.present?
.cover-desc.cgray
.profile-user-bio
@@ -129,6 +146,14 @@
%li.js-snippets-tab
= link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do
= s_('UserProfile|Snippets')
+ - if profile_tab?(:followers)
+ %li.js-followers-tab
+ = link_to user_followers_path, data: { target: 'div#followers', action: 'followers', toggle: 'tab', endpoint: user_followers_path(format: :json) } do
+ = s_('UserProfile|Followers')
+ - if profile_tab?(:following)
+ %li.js-following-tab
+ = link_to user_following_path, data: { target: 'div#following', action: 'following', toggle: 'tab', endpoint: user_following_path(format: :json) } do
+ = s_('UserProfile|Following')
%div{ class: container_class }
.tab-content
@@ -165,6 +190,14 @@
#snippets.tab-pane
-# This tab is always loaded via AJAX
+ - if profile_tab?(:followers)
+ #followers.tab-pane
+ -# This tab is always loaded via AJAX
+
+ - if profile_tab?(:following)
+ #following.tab-pane
+ -# This tab is always loaded via AJAX
+
.loading.hide
.spinner.spinner-md