diff options
Diffstat (limited to 'app/assets')
20 files changed, 238 insertions, 29 deletions
diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js index de4566bb119..05de970e387 100644 --- a/app/assets/javascripts/activities.js +++ b/app/assets/javascripts/activities.js @@ -6,10 +6,12 @@ import Pager from './pager'; import { localTimeAgo } from './lib/utils/datetime_utility'; export default class Activities { - constructor() { - Pager.init(20, true, false, data => data, this.updateTooltips); + constructor(container = '') { + this.container = container; - $('.event-filter-link').on('click', (e) => { + Pager.init(20, true, false, data => data, this.updateTooltips, this.container); + + $('.event-filter-link').on('click', e => { e.preventDefault(); this.toggleFilter(e.currentTarget); this.reloadActivities(); @@ -22,7 +24,7 @@ export default class Activities { reloadActivities() { $('.content_list').html(''); - Pager.init(20, true, false, data => data, this.updateTooltips); + Pager.init(20, true, false, data => data, this.updateTooltips, this.container); } toggleFilter(sender) { diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js index c4f0c41d3a8..b70125c80ca 100644 --- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js @@ -68,6 +68,11 @@ export const conditions = [ value: 'none', }, { + url: 'milestone_title=Any+Milestone', + tokenKey: 'milestone', + value: 'any', + }, + { url: 'milestone_title=%23upcoming', tokenKey: 'milestone', value: 'upcoming', diff --git a/app/assets/javascripts/jobs/components/job_container_item.vue b/app/assets/javascripts/jobs/components/job_container_item.vue index 81cc0823792..6486b25c8a7 100644 --- a/app/assets/javascripts/jobs/components/job_container_item.vue +++ b/app/assets/javascripts/jobs/components/job_container_item.vue @@ -1,5 +1,4 @@ <script> -import _ from 'underscore'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import Icon from '~/vue_shared/components/icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; @@ -9,11 +8,9 @@ export default { CiIcon, Icon, }, - directives: { tooltip, }, - props: { job: { type: Object, @@ -24,10 +21,9 @@ export default { required: true, }, }, - computed: { tooltipText() { - return `${_.escape(this.job.name)} - ${this.job.status.tooltip}`; + return `${this.job.name} - ${this.job.status.tooltip}`; }, }, }; @@ -36,7 +32,10 @@ export default { <template> <div class="build-job" - :class="{ retried: job.retried, active: isActive }" + :class="{ + retried: job.retried, + active: isActive + }" > <a v-tooltip diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js index 8aabb840847..1c98683c597 100644 --- a/app/assets/javascripts/mr_notes/index.js +++ b/app/assets/javascripts/mr_notes/index.js @@ -4,6 +4,7 @@ import { mapActions, mapState, mapGetters } from 'vuex'; import initDiffsApp from '../diffs'; import notesApp from '../notes/components/notes_app.vue'; import discussionCounter from '../notes/components/discussion_counter.vue'; +import initDiscussionFilters from '../notes/discussion_filters'; import store from './stores'; import MergeRequest from '../merge_request'; @@ -88,5 +89,6 @@ export default function initMrNotes() { }, }); + initDiscussionFilters(store); initDiffsApp(store); } diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue index ad6e7cf501d..1f80f24e045 100644 --- a/app/assets/javascripts/notes/components/discussion_counter.vue +++ b/app/assets/javascripts/notes/components/discussion_counter.vue @@ -56,10 +56,11 @@ export default { </script> <template> - <div class="line-resolve-all-container prepend-top-10"> + <div + v-if="discussionCount > 0" + class="line-resolve-all-container prepend-top-8"> <div> <div - v-if="discussionCount > 0" :class="{ 'has-next-btn': hasNextButton }" class="line-resolve-all"> <span diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue new file mode 100644 index 00000000000..27972682ca1 --- /dev/null +++ b/app/assets/javascripts/notes/components/discussion_filter.vue @@ -0,0 +1,82 @@ +<script> +import $ from 'jquery'; +import Icon from '~/vue_shared/components/icon.vue'; +import { mapGetters, mapActions } from 'vuex'; + +export default { + components: { + Icon, + }, + props: { + filters: { + type: Array, + required: true, + }, + defaultValue: { + type: Number, + default: null, + required: false, + }, + }, + data() { + return { currentValue: this.defaultValue }; + }, + computed: { + ...mapGetters([ + 'getNotesDataByProp', + ]), + currentFilter() { + if (!this.currentValue) return this.filters[0]; + return this.filters.find(filter => filter.value === this.currentValue); + }, + }, + methods: { + ...mapActions(['filterDiscussion']), + selectFilter(value) { + const filter = parseInt(value, 10); + + // close dropdown + $(this.$refs.dropdownToggle).dropdown('toggle'); + + if (filter === this.currentValue) return; + this.currentValue = filter; + this.filterDiscussion({ path: this.getNotesDataByProp('discussionsPath'), filter }); + }, + }, +}; +</script> + +<template> + <div class="discussion-filter-container d-inline-block align-bottom"> + <button + id="discussion-filter-dropdown" + ref="dropdownToggle" + class="btn btn-default" + data-toggle="dropdown" + aria-expanded="false" + > + {{ currentFilter.title }} + <icon name="chevron-down" /> + </button> + <div + class="dropdown-menu dropdown-menu-selectable dropdown-menu-right" + aria-labelledby="discussion-filter-dropdown"> + <div class="dropdown-content"> + <ul> + <li + v-for="filter in filters" + :key="filter.value" + > + <button + :class="{ 'is-active': filter.value === currentValue }" + type="button" + @click="selectFilter(filter.value)" + > + {{ filter.title }} + </button> + </li> + </ul> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 618a1581d8f..b0faa443a18 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -50,11 +50,11 @@ export default { }, data() { return { - isLoading: true, + currentFilter: null, }; }, computed: { - ...mapGetters(['isNotesFetched', 'discussions', 'getNotesDataByProp', 'discussionCount']), + ...mapGetters(['isNotesFetched', 'discussions', 'getNotesDataByProp', 'discussionCount', 'isLoading']), noteableType() { return this.noteableData.noteableType; }, @@ -102,6 +102,7 @@ export default { }, methods: { ...mapActions({ + setLoadingState: 'setLoadingState', fetchDiscussions: 'fetchDiscussions', poll: 'poll', actionToggleAward: 'toggleAward', @@ -133,19 +134,19 @@ export default { return discussion.individual_note ? { note: discussion.notes[0] } : { discussion }; }, fetchNotes() { - return this.fetchDiscussions(this.getNotesDataByProp('discussionsPath')) + return this.fetchDiscussions({ path: this.getNotesDataByProp('discussionsPath') }) .then(() => { this.initPolling(); }) .then(() => { - this.isLoading = false; + this.setLoadingState(false); this.setNotesFetchedState(true); eventHub.$emit('fetchedNotesData'); }) .then(() => this.$nextTick()) .then(() => this.checkLocationHash()) .catch(() => { - this.isLoading = false; + this.setLoadingState(false); this.setNotesFetchedState(true); Flash('Something went wrong while fetching comments. Please try again.'); }); diff --git a/app/assets/javascripts/notes/discussion_filters.js b/app/assets/javascripts/notes/discussion_filters.js new file mode 100644 index 00000000000..012ffc4093e --- /dev/null +++ b/app/assets/javascripts/notes/discussion_filters.js @@ -0,0 +1,33 @@ +import Vue from 'vue'; +import DiscussionFilter from './components/discussion_filter.vue'; + +export default (store) => { + const discussionFilterEl = document.getElementById('js-vue-discussion-filter'); + + if (discussionFilterEl) { + const { defaultFilter, notesFilters } = discussionFilterEl.dataset; + const defaultValue = defaultFilter ? parseInt(defaultFilter, 10) : null; + const filterValues = notesFilters ? JSON.parse(notesFilters) : {}; + const filters = Object.keys(filterValues).map(entry => + ({ title: entry, value: filterValues[entry] })); + + return new Vue({ + el: discussionFilterEl, + name: 'DiscussionFilter', + components: { + DiscussionFilter, + }, + store, + render(createElement) { + return createElement('discussion-filter', { + props: { + filters, + defaultValue, + }, + }); + }, + }); + } + + return null; +}; diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 3aef30c608c..2f715c85fa6 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -1,10 +1,13 @@ import Vue from 'vue'; import notesApp from './components/notes_app.vue'; +import initDiscussionFilters from './discussion_filters'; import createStore from './stores'; document.addEventListener('DOMContentLoaded', () => { const store = createStore(); + initDiscussionFilters(store); + return new Vue({ el: '#js-vue-notes', components: { diff --git a/app/assets/javascripts/notes/services/notes_service.js b/app/assets/javascripts/notes/services/notes_service.js index f5dce94caad..47a6f07cce2 100644 --- a/app/assets/javascripts/notes/services/notes_service.js +++ b/app/assets/javascripts/notes/services/notes_service.js @@ -5,8 +5,9 @@ import * as constants from '../constants'; Vue.use(VueResource); export default { - fetchDiscussions(endpoint) { - return Vue.http.get(endpoint); + fetchDiscussions(endpoint, filter) { + const config = filter !== undefined ? { params: { notes_filter: filter } } : null; + return Vue.http.get(endpoint, config); }, deleteNote(endpoint) { return Vue.http.delete(endpoint); diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 7ab7e5a9abb..b5dd49bc6c9 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -11,6 +11,7 @@ import loadAwardsHandler from '../../awards_handler'; import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; import { isInViewport, scrollToElement } from '../../lib/utils/common_utils'; import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub'; +import { __ } from '~/locale'; let eTagPoll; @@ -36,9 +37,9 @@ export const setNotesFetchedState = ({ commit }, state) => export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data); -export const fetchDiscussions = ({ commit }, path) => +export const fetchDiscussions = ({ commit }, { path, filter }) => service - .fetchDiscussions(path) + .fetchDiscussions(path, filter) .then(res => res.json()) .then(discussions => { commit(types.SET_INITIAL_DISCUSSIONS, discussions); @@ -251,7 +252,7 @@ const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => { if (discussion) { commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note); } else if (note.type === constants.DIFF_NOTE) { - dispatch('fetchDiscussions', state.notesData.discussionsPath); + dispatch('fetchDiscussions', { path: state.notesData.discussionsPath }); } else { commit(types.ADD_NEW_NOTE, note); } @@ -345,5 +346,23 @@ export const updateMergeRequestWidget = () => { mrWidgetEventHub.$emit('mr.discussion.updated'); }; +export const setLoadingState = ({ commit }, data) => { + commit(types.SET_NOTES_LOADING_STATE, data); +}; + +export const filterDiscussion = ({ dispatch }, { path, filter }) => { + dispatch('setLoadingState', true); + dispatch('fetchDiscussions', { path, filter }) + .then(() => { + dispatch('setLoadingState', false); + dispatch('setNotesFetchedState', true); + }) + .catch(() => { + dispatch('setLoadingState', false); + dispatch('setNotesFetchedState', true); + Flash(__('Something went wrong while fetching comments. Please try again.')); + }); +}; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index a829149a17e..21c334a9d33 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -11,6 +11,8 @@ export const getNotesData = state => state.notesData; export const isNotesFetched = state => state.isNotesFetched; +export const isLoading = state => state.isLoading; + export const getNotesDataByProp = state => prop => state.notesData[prop]; export const getNoteableData = state => state.noteableData; diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js index 61dbb075586..400142668ea 100644 --- a/app/assets/javascripts/notes/stores/modules/index.js +++ b/app/assets/javascripts/notes/stores/modules/index.js @@ -11,6 +11,7 @@ export default () => ({ // View layer isToggleStateButtonLoading: false, isNotesFetched: false, + isLoading: true, // holds endpoints and permissions provided through haml notesData: { diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index 6f374f78691..2fa53aef1d4 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -14,6 +14,7 @@ export const UPDATE_NOTE = 'UPDATE_NOTE'; export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION'; export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES'; export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE'; +export const SET_NOTES_LOADING_STATE = 'SET_NOTES_LOADING_STATE'; // DISCUSSION export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index 73e55705f39..65085452139 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -216,6 +216,10 @@ export default { Object.assign(state, { isNotesFetched: value }); }, + [types.SET_NOTES_LOADING_STATE](state, value) { + state.isLoading = value; + }, + [types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) { const discussion = utils.findNoteObjectById(state.discussions, discussionId); diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js index 3b58c54b3f4..386a9b2c740 100644 --- a/app/assets/javascripts/pager.js +++ b/app/assets/javascripts/pager.js @@ -7,14 +7,21 @@ const ENDLESS_SCROLL_BOTTOM_PX = 400; const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000; export default { - init(limit = 0, preload = false, disable = false, prepareData = $.noop, callback = $.noop) { + init( + limit = 0, + preload = false, + disable = false, + prepareData = $.noop, + callback = $.noop, + container = '', + ) { this.url = $('.content_list').data('href') || removeParams(['limit', 'offset']); this.limit = limit; this.offset = parseInt(getParameterByName('offset'), 10) || this.limit; this.disable = disable; this.prepareData = prepareData; this.callback = callback; - this.loading = $('.loading').first(); + this.loading = $(`${container} .loading`).first(); if (preload) { this.offset = 0; this.getOld(); diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js index 1de9945baad..04bcb16f036 100644 --- a/app/assets/javascripts/pages/users/user_tabs.js +++ b/app/assets/javascripts/pages/users/user_tabs.js @@ -170,7 +170,7 @@ export default class UserTabs { this.loadActivityCalendar('activity'); // eslint-disable-next-line no-new - new Activities(); + new Activities('#activity'); this.loaded.activity = true; } diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 0f95fb911e1..8ea34f5d19d 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -185,7 +185,17 @@ ul.related-merge-requests > li { } .new-branch-col { - padding-top: 10px; + font-size: 0; + + .discussion-filter-container { + &:not(:only-child) { + margin-right: $gl-padding-8; + } + + @include media-breakpoint-down(md) { + margin-top: $gl-padding-8; + } + } } .create-mr-dropdown-wrap { @@ -205,6 +215,10 @@ ul.related-merge-requests > li { .btn-group:not(.hidden) { display: flex; + + @include media-breakpoint-down(md) { + margin-top: $gl-padding-8; + } } .js-create-merge-request { @@ -251,7 +265,6 @@ ul.related-merge-requests > li { .new-branch-col { padding-top: 0; - text-align: right; align-self: center; } @@ -262,3 +275,9 @@ ul.related-merge-requests > li { } } } + +@include media-breakpoint-up(lg) { + .new-branch-col { + text-align: right; + } +} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 2feb7464ecb..fa6afbf81de 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -818,9 +818,17 @@ display: flex; justify-content: space-between; - @include media-breakpoint-down(xs) { + @include media-breakpoint-down(md) { flex-direction: column-reverse; } + + .discussion-filter-container { + margin-top: $gl-padding-8; + + &:not(:only-child) { + padding-right: $gl-padding-8; + } + } } .limit-container-width:not(.container-limited) { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index bfba1bf1b2b..be535ade0a6 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -618,7 +618,6 @@ ul.notes { .line-resolve-all-container { @include notes-media('min', map-get($grid-breakpoints, sm)) { margin-right: 0; - padding-left: $gl-padding; } > div { @@ -756,3 +755,23 @@ ul.notes { margin-top: 4px; } } + +.discussion-filter-container { + + .btn > svg { + width: $gl-col-padding; + height: $gl-col-padding; + } + + .dropdown-menu { + margin-bottom: $gl-padding-4; + + @include media-breakpoint-down(md) { + margin-left: $btn-side-margin + $contextual-sidebar-collapsed-width; + } + + @include media-breakpoint-down(xs) { + margin-left: $btn-side-margin; + } + } +} |