diff options
author | Shinya Maeda <shinya@gitlab.com> | 2018-10-05 10:13:06 +0900 |
---|---|---|
committer | Shinya Maeda <shinya@gitlab.com> | 2018-10-05 10:13:06 +0900 |
commit | af2c51d7ca73e16cdb9a11fff6d097116c7c5324 (patch) | |
tree | cd7f07f36e09a357902a262cbb932f25d254a16c /app/assets/javascripts | |
parent | acfe7ec65c75f65f2f87bf3e8598c1725c9150f4 (diff) | |
parent | f71c497f5da791a35876206255e342a9bb5e49c5 (diff) | |
download | gitlab-ce-af2c51d7ca73e16cdb9a11fff6d097116c7c5324.tar.gz |
Merge branch 'master-ce' into scheduled-manual-jobs
Diffstat (limited to 'app/assets/javascripts')
21 files changed, 568 insertions, 56 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index cd800d75f7a..ecc2440c7e6 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -22,6 +22,7 @@ const Api = { dockerfilePath: '/api/:version/templates/dockerfiles/:key', issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', usersPath: '/api/:version/users.json', + userStatusPath: '/api/:version/user/status', commitPath: '/api/:version/projects/:id/repository/commits', commitPipelinesPath: '/:project_id/commit/:sha/pipelines', branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch', @@ -266,6 +267,15 @@ const Api = { }); }, + postUserStatus({ emoji, message }) { + const url = Api.buildUrl(this.userStatusPath); + + return axios.put(url, { + emoji, + message, + }); + }, + templates(key, params = {}) { const url = Api.buildUrl(this.templatesPath).replace(':key', key); diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 5b0c4285339..cace8bb9dba 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -42,10 +42,11 @@ export class AwardsHandler { } bindEvents() { + const $parentEl = this.targetContainerEl ? $(this.targetContainerEl) : $(document); // If the user shows intent let's pre-build the menu this.registerEventListener( 'one', - $(document), + $parentEl, 'mouseenter focus', this.toggleButtonSelector, 'mouseenter focus', @@ -58,7 +59,7 @@ export class AwardsHandler { } }, ); - this.registerEventListener('on', $(document), 'click', this.toggleButtonSelector, e => { + this.registerEventListener('on', $parentEl, 'click', this.toggleButtonSelector, e => { e.stopPropagation(); e.preventDefault(); this.showEmojiMenu($(e.currentTarget)); @@ -76,7 +77,7 @@ export class AwardsHandler { }); const emojiButtonSelector = `.js-awards-block .js-emoji-btn, .${this.menuClass} .js-emoji-btn`; - this.registerEventListener('on', $(document), 'click', emojiButtonSelector, e => { + this.registerEventListener('on', $parentEl, 'click', emojiButtonSelector, e => { e.preventDefault(); const $target = $(e.currentTarget); const $glEmojiElement = $target.find('gl-emoji'); @@ -168,7 +169,8 @@ export class AwardsHandler { </div> `; - document.body.insertAdjacentHTML('beforeend', emojiMenuMarkup); + const targetEl = this.targetContainerEl ? this.targetContainerEl : document.body; + targetEl.insertAdjacentHTML('beforeend', emojiMenuMarkup); this.addRemainingEmojiMenuCategories(); this.setupSearch(); @@ -250,6 +252,12 @@ export class AwardsHandler { } positionMenu($menu, $addBtn) { + if (this.targetContainerEl) { + return $menu.css({ + top: `${$addBtn.outerHeight()}px`, + }); + } + const position = $addBtn.data('position'); // The menu could potentially be off-screen or in a hidden overflow element // So we position the element absolute in the body @@ -424,9 +432,7 @@ export class AwardsHandler { users = origTitle.trim().split(FROM_SENTENCE_REGEX); } users.unshift('You'); - return awardBlock - .attr('title', this.toSentence(users)) - .tooltip('_fixTitle'); + return awardBlock.attr('title', this.toSentence(users)).tooltip('_fixTitle'); } createAwardButtonForVotesBlock(votesBlock, emojiName) { @@ -609,13 +615,11 @@ export class AwardsHandler { let awardsHandlerPromise = null; export default function loadAwardsHandler(reload = false) { if (!awardsHandlerPromise || reload) { - awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then( - Emoji => { - const awardsHandler = new AwardsHandler(Emoji); - awardsHandler.bindEvents(); - return awardsHandler; - }, - ); + awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then(Emoji => { + const awardsHandler = new AwardsHandler(Emoji); + awardsHandler.bindEvents(); + return awardsHandler; + }); } return awardsHandlerPromise; } diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue index 5758588e82e..993206b2e73 100644 --- a/app/assets/javascripts/diffs/components/commit_item.vue +++ b/app/assets/javascripts/diffs/components/commit_item.vue @@ -5,6 +5,7 @@ import Icon from '~/vue_shared/components/icon.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import CIIcon from '~/vue_shared/components/ci_icon.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; /** * CommitItem @@ -29,6 +30,7 @@ export default { ClipboardButton, CIIcon, TimeAgoTooltip, + CommitPipelineStatus, }, props: { commit: { @@ -102,6 +104,14 @@ export default { ></pre> </div> <div class="commit-actions flex-row d-none d-sm-flex"> + <div + v-if="commit.signatureHtml" + v-html="commit.signatureHtml" + ></div> + <commit-pipeline-status + v-if="commit.pipelineStatusPath" + :endpoint="commit.pipelineStatusPath" + /> <div class="commit-sha-group"> <div class="label label-monospace" diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index 4ae588042e4..c39403f1021 100644 --- a/app/assets/javascripts/diffs/store/utils.js +++ b/app/assets/javascripts/diffs/store/utils.js @@ -244,6 +244,7 @@ export function getDiffPositionByLineCode(diffFiles) { oldLine, newLine, lineCode, + positionType: 'text', }; } }); @@ -259,8 +260,8 @@ export function isDiscussionApplicableToLine({ discussion, diffPosition, latestD const { lineCode, ...diffPositionCopy } = diffPosition; if (discussion.original_position && discussion.position) { - const originalRefs = convertObjectPropsToCamelCase(discussion.original_position.formatter); - const refs = convertObjectPropsToCamelCase(discussion.position.formatter); + const originalRefs = convertObjectPropsToCamelCase(discussion.original_position); + const refs = convertObjectPropsToCamelCase(discussion.position); return _.isEqual(refs, diffPositionCopy) || _.isEqual(originalRefs, diffPositionCopy); } diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index 11e3b781e5a..a1d8e531940 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -466,7 +466,9 @@ export default { class="gl-responsive-table-row" role="row"> <div - class="table-section section-wrap section-15" + v-tooltip + :title="model.name" + class="table-section section-wrap section-15 text-truncate" role="gridcell" > <div @@ -480,9 +482,7 @@ export default { v-if="!model.isFolder" class="environment-name table-mobile-content"> <a - v-tooltip :href="environmentPath" - :title="model.name" > {{ model.name }} </a> diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index 4ae3a714bee..175d0b8498b 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -1,5 +1,9 @@ import $ from 'jquery'; +import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; import { highCountTrim } from '~/lib/utils/text_utility'; +import SetStatusModalTrigger from './set_status_modal/set_status_modal_trigger.vue'; +import SetStatusModalWrapper from './set_status_modal/set_status_modal_wrapper.vue'; /** * Updates todo counter when todos are toggled. @@ -17,3 +21,54 @@ export default function initTodoToggle() { $todoPendingCount.toggleClass('hidden', parsedCount === 0); }); } + +document.addEventListener('DOMContentLoaded', () => { + const setStatusModalTriggerEl = document.querySelector('.js-set-status-modal-trigger'); + const setStatusModalWrapperEl = document.querySelector('.js-set-status-modal-wrapper'); + + if (setStatusModalTriggerEl || setStatusModalWrapperEl) { + Vue.use(Translate); + + // eslint-disable-next-line no-new + new Vue({ + el: setStatusModalTriggerEl, + data() { + const { hasStatus } = this.$options.el.dataset; + + return { + hasStatus: hasStatus === 'true', + }; + }, + render(createElement) { + return createElement(SetStatusModalTrigger, { + props: { + hasStatus: this.hasStatus, + }, + }); + }, + }); + + // eslint-disable-next-line no-new + new Vue({ + el: setStatusModalWrapperEl, + data() { + const { currentEmoji, currentMessage } = this.$options.el.dataset; + + return { + currentEmoji, + currentMessage, + }; + }, + render(createElement) { + const { currentEmoji, currentMessage } = this; + + return createElement(SetStatusModalWrapper, { + props: { + currentEmoji, + currentMessage, + }, + }); + }, + }); + } +}); diff --git a/app/assets/javascripts/jobs/components/commit_block.vue b/app/assets/javascripts/jobs/components/commit_block.vue index 39a4ff159e2..4b1788a1c16 100644 --- a/app/assets/javascripts/jobs/components/commit_block.vue +++ b/app/assets/javascripts/jobs/components/commit_block.vue @@ -46,7 +46,7 @@ v-if="mergeRequest" :href="mergeRequest.path" class="js-link-commit link-commit" - >{{ mergeRequest.iid }}</a> + >!{{ mergeRequest.iid }}</a> </p> <p class="build-light-text append-bottom-0"> diff --git a/app/assets/javascripts/jobs/components/empty_state.vue b/app/assets/javascripts/jobs/components/empty_state.vue index ff45a5b05f8..ff500eddddb 100644 --- a/app/assets/javascripts/jobs/components/empty_state.vue +++ b/app/assets/javascripts/jobs/components/empty_state.vue @@ -27,7 +27,7 @@ value === null || (Object.prototype.hasOwnProperty.call(value, 'path') && Object.prototype.hasOwnProperty.call(value, 'method') && - Object.prototype.hasOwnProperty.call(value, 'title')) + Object.prototype.hasOwnProperty.call(value, 'button_title')) ); }, }, @@ -67,7 +67,7 @@ :data-method="action.method" class="js-job-empty-state-action btn btn-primary" > - {{ action.title }} + {{ action.button_title }} </a> </div> </div> diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index bac8bd71d64..047e55866ce 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -2,6 +2,7 @@ import { mapGetters, mapState } from 'vuex'; import CiHeader from '~/vue_shared/components/header_ci_component.vue'; import Callout from '~/vue_shared/components/callout.vue'; + import EmptyState from './empty_state.vue'; import EnvironmentsBlock from './environments_block.vue'; import ErasedBlock from './erased_block.vue'; import StuckBlock from './stuck_block.vue'; @@ -11,6 +12,7 @@ components: { CiHeader, Callout, + EmptyState, EnvironmentsBlock, ErasedBlock, StuckBlock, @@ -31,6 +33,8 @@ 'jobHasStarted', 'hasEnvironment', 'isJobStuck', + 'hasTrace', + 'emptyStateIllustration', ]), }, }; @@ -77,12 +81,14 @@ <environments-block v-if="hasEnvironment" + class="js-job-environment" :deployment-status="job.deployment_status" :icon-status="job.status" /> <erased-block v-if="job.erased" + class="js-job-erased" :user="job.erased_by" :erased-at="job.erased_at" /> @@ -91,6 +97,15 @@ <!-- EO job log --> <!--empty state --> + <empty-state + v-if="!hasTrace" + class="js-job-empty-state" + :illustration-path="emptyStateIllustration.image" + :illustration-size-class="emptyStateIllustration.size" + :title="emptyStateIllustration.title" + :content="emptyStateIllustration.content" + :action="job.status.action" + /> <!-- EO empty state --> <!-- EO Body Section --> diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js index 62d154ff584..afe5f88b292 100644 --- a/app/assets/javascripts/jobs/store/getters.js +++ b/app/assets/javascripts/jobs/store/getters.js @@ -30,13 +30,24 @@ export const jobHasStarted = state => !(state.job.started === false); export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status); /** + * Checks if it the job has trace. + * Used to check if it should render the job log or the empty state + * @returns {Boolean} + */ +export const hasTrace = state => state.job.has_trace || state.job.status.group === 'running'; + +export const emptyStateIllustration = state => + (state.job && state.job.status && state.job.status.illustration) || {}; + +/** * When the job is pending and there are no available runners * we need to render the stuck block; * * @returns {Boolean} */ export const isJobStuck = state => - state.job.status.group === 'pending' && state.job.runners && state.job.runners.available === false; + state.job.status.group === 'pending' && + (!_.isEmpty(state.job.runners) && state.job.runners.available === false); // 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 d4babf1fab2..75832884711 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -126,8 +126,8 @@ export const unresolvedDiscussionsIdsByDiff = (state, getters) => const filenameComparison = a.diff_file.file_path.localeCompare(b.diff_file.file_path); // Get the line numbers, to compare within the same file - const aLines = [a.position.formatter.new_line, a.position.formatter.old_line]; - const bLines = [b.position.formatter.new_line, b.position.formatter.old_line]; + const aLines = [a.position.new_line, a.position.old_line]; + const bLines = [b.position.new_line, b.position.old_line]; return filenameComparison < 0 || (filenameComparison === 0 && diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js index 9aa83ce6269..72f3f70b98f 100644 --- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js +++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js @@ -79,10 +79,13 @@ export default class Todos { .then(({ data }) => { this.updateRowState(target); this.updateBadges(data); - }).catch(() => flash(__('Error updating todo status.'))); + }).catch(() => { + this.updateRowState(target, true); + return flash(__('Error updating todo status.')); + }); } - updateRowState(target) { + updateRowState(target, isInactive = false) { const row = target.closest('li'); const restoreBtn = row.querySelector('.js-undo-todo'); const doneBtn = row.querySelector('.js-done-todo'); @@ -91,7 +94,10 @@ export default class Todos { target.removeAttribute('disabled'); target.classList.remove('disabled'); - if (target === doneBtn) { + if (isInactive === true) { + restoreBtn.classList.add('hidden'); + doneBtn.classList.remove('hidden'); + } else if (target === doneBtn) { row.classList.add('done-reversible'); restoreBtn.classList.remove('hidden'); } else if (target === restoreBtn) { diff --git a/app/assets/javascripts/pages/profiles/show/index.js b/app/assets/javascripts/pages/profiles/show/index.js index aea7b649c20..c7ce4675573 100644 --- a/app/assets/javascripts/pages/profiles/show/index.js +++ b/app/assets/javascripts/pages/profiles/show/index.js @@ -11,7 +11,7 @@ document.addEventListener('DOMContentLoaded', () => { const statusEmojiField = document.getElementById('js-status-emoji-field'); const statusMessageField = document.getElementById('js-status-message-field'); - const toggleNoEmojiPlaceholder = (isVisible) => { + const toggleNoEmojiPlaceholder = isVisible => { const placeholderElement = document.getElementById('js-no-emoji-placeholder'); placeholderElement.classList.toggle('hidden', !isVisible); }; @@ -69,5 +69,5 @@ document.addEventListener('DOMContentLoaded', () => { } }); }) - .catch(() => createFlash('Failed to load emoji list!')); + .catch(() => createFlash('Failed to load emoji list.')); }); diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js index 9892a039941..bf592ba7a3c 100644 --- a/app/assets/javascripts/pages/users/activity_calendar.js +++ b/app/assets/javascripts/pages/users/activity_calendar.js @@ -43,7 +43,15 @@ const initColorKey = () => .domain([0, 3]); export default class ActivityCalendar { - constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0, firstDayOfWeek = 0) { + constructor( + container, + activitiesContainer, + timestamps, + calendarActivitiesPath, + utcOffset = 0, + firstDayOfWeek = 0, + monthsAgo = 12, + ) { this.calendarActivitiesPath = calendarActivitiesPath; this.clickDay = this.clickDay.bind(this); this.currentSelectedDate = ''; @@ -66,6 +74,8 @@ export default class ActivityCalendar { ]; this.months = []; this.firstDayOfWeek = firstDayOfWeek; + this.activitiesContainer = activitiesContainer; + this.container = container; // Loop through the timestamps to create a group of objects // The group of objects will be grouped based on the day of the week they are @@ -75,13 +85,13 @@ export default class ActivityCalendar { const today = getSystemDate(utcOffset); today.setHours(0, 0, 0, 0, 0); - const oneYearAgo = new Date(today); - oneYearAgo.setFullYear(today.getFullYear() - 1); + const timeAgo = new Date(today); + timeAgo.setMonth(today.getMonth() - monthsAgo); - const days = getDayDifference(oneYearAgo, today); + const days = getDayDifference(timeAgo, today); for (let i = 0; i <= days; i += 1) { - const date = new Date(oneYearAgo); + const date = new Date(timeAgo); date.setDate(date.getDate() + i); const day = date.getDay(); @@ -280,7 +290,7 @@ export default class ActivityCalendar { this.currentSelectedDate.getDate(), ].join('-'); - $('.user-calendar-activities').html(LOADING_HTML); + $(this.activitiesContainer).html(LOADING_HTML); axios .get(this.calendarActivitiesPath, { @@ -289,11 +299,11 @@ export default class ActivityCalendar { }, responseType: 'text', }) - .then(({ data }) => $('.user-calendar-activities').html(data)) + .then(({ data }) => $(this.activitiesContainer).html(data)) .catch(() => flash(__('An error occurred while retrieving calendar activity'))); } else { this.currentSelectedDate = ''; - $('.user-calendar-activities').html(''); + $(this.activitiesContainer).html(''); } } } diff --git a/app/assets/javascripts/pages/users/user_overview_block.js b/app/assets/javascripts/pages/users/user_overview_block.js new file mode 100644 index 00000000000..0009419cd0c --- /dev/null +++ b/app/assets/javascripts/pages/users/user_overview_block.js @@ -0,0 +1,42 @@ +import axios from '~/lib/utils/axios_utils'; + +export default class UserOverviewBlock { + constructor(options = {}) { + this.container = options.container; + this.url = options.url; + this.limit = options.limit || 20; + this.loadData(); + } + + loadData() { + const loadingEl = document.querySelector(`${this.container} .loading`); + + loadingEl.classList.remove('hide'); + + axios + .get(this.url, { + params: { + limit: this.limit, + }, + }) + .then(({ data }) => this.render(data)) + .catch(() => loadingEl.classList.add('hide')); + } + + render(data) { + const { html, count } = data; + const contentList = document.querySelector(`${this.container} .overview-content-list`); + + contentList.innerHTML += html; + + const loadingEl = document.querySelector(`${this.container} .loading`); + + if (count && count > 0) { + document.querySelector(`${this.container} .js-view-all`).classList.remove('hide'); + } else { + document.querySelector(`${this.container} .nothing-here-block`).classList.add('text-left', 'p-0'); + } + + loadingEl.classList.add('hide'); + } +} diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js index a2ca03536f2..23b0348a99f 100644 --- a/app/assets/javascripts/pages/users/user_tabs.js +++ b/app/assets/javascripts/pages/users/user_tabs.js @@ -2,9 +2,10 @@ import $ from 'jquery'; import axios from '~/lib/utils/axios_utils'; import Activities from '~/activities'; import { localTimeAgo } from '~/lib/utils/datetime_utility'; -import { __ } from '~/locale'; +import { __, sprintf } from '~/locale'; import flash from '~/flash'; import ActivityCalendar from './activity_calendar'; +import UserOverviewBlock from './user_overview_block'; /** * UserTabs @@ -61,19 +62,28 @@ import ActivityCalendar from './activity_calendar'; * </div> */ -const CALENDAR_TEMPLATE = ` - <div class="clearfix calendar"> - <div class="js-contrib-calendar"></div> - <div class="calendar-hint"> - Summary of issues, merge requests, push events, and comments +const CALENDAR_TEMPLATES = { + activity: ` + <div class="clearfix calendar"> + <div class="js-contrib-calendar"></div> + <div class="calendar-hint bottom-right"></div> </div> - </div> -`; + `, + overview: ` + <div class="clearfix calendar"> + <div class="calendar-hint"></div> + <div class="js-contrib-calendar prepend-top-20"></div> + </div> + `, +}; + +const CALENDAR_PERIOD_6_MONTHS = 6; +const CALENDAR_PERIOD_12_MONTHS = 12; export default class UserTabs { constructor({ defaultAction, action, parentEl }) { this.loaded = {}; - this.defaultAction = defaultAction || 'activity'; + this.defaultAction = defaultAction || 'overview'; this.action = action || this.defaultAction; this.$parentEl = $(parentEl) || $(document); this.windowLocation = window.location; @@ -124,6 +134,8 @@ export default class UserTabs { } if (action === 'activity') { this.loadActivities(); + } else if (action === 'overview') { + this.loadOverviewTab(); } const loadableActions = ['groups', 'contributed', 'projects', 'snippets']; @@ -154,7 +166,40 @@ export default class UserTabs { if (this.loaded.activity) { return; } - const $calendarWrap = this.$parentEl.find('.user-calendar'); + + this.loadActivityCalendar('activity'); + + // eslint-disable-next-line no-new + new Activities(); + + this.loaded.activity = true; + } + + loadOverviewTab() { + if (this.loaded.overview) { + return; + } + + this.loadActivityCalendar('overview'); + + UserTabs.renderMostRecentBlocks('#js-overview .activities-block', 5); + UserTabs.renderMostRecentBlocks('#js-overview .projects-block', 10); + + this.loaded.overview = true; + } + + static renderMostRecentBlocks(container, limit) { + // eslint-disable-next-line no-new + new UserOverviewBlock({ + container, + url: $(`${container} .overview-content-list`).data('href'), + limit, + }); + } + + loadActivityCalendar(action) { + const monthsAgo = action === 'overview' ? CALENDAR_PERIOD_6_MONTHS : CALENDAR_PERIOD_12_MONTHS; + const $calendarWrap = this.$parentEl.find('.tab-pane.active .user-calendar'); const calendarPath = $calendarWrap.data('calendarPath'); const calendarActivitiesPath = $calendarWrap.data('calendarActivitiesPath'); const utcOffset = $calendarWrap.data('utcOffset'); @@ -166,17 +211,22 @@ export default class UserTabs { axios .get(calendarPath) .then(({ data }) => { - $calendarWrap.html(CALENDAR_TEMPLATE); - $calendarWrap.find('.calendar-hint').append(`(Timezone: ${utcFormatted})`); + $calendarWrap.html(CALENDAR_TEMPLATES[action]); + + let calendarHint = ''; + + if (action === 'activity') { + calendarHint = sprintf(__('Summary of issues, merge requests, push events, and comments (Timezone: %{utcFormatted})'), { utcFormatted }); + } else if (action === 'overview') { + calendarHint = __('Issues, merge requests, pushes and comments.'); + } + + $calendarWrap.find('.calendar-hint').text(calendarHint); // eslint-disable-next-line no-new - new ActivityCalendar('.js-contrib-calendar', data, calendarActivitiesPath, utcOffset); + new ActivityCalendar('.tab-pane.active .js-contrib-calendar', '.tab-pane.active .user-calendar-activities', data, calendarActivitiesPath, utcOffset, 0, monthsAgo); }) .catch(() => flash(__('There was an error loading users activity calendar.'))); - - // eslint-disable-next-line no-new - new Activities(); - this.loaded.activity = true; } toggleLoading(status) { diff --git a/app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js b/app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js new file mode 100644 index 00000000000..14a89ef9293 --- /dev/null +++ b/app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js @@ -0,0 +1,21 @@ +import { AwardsHandler } from '~/awards_handler'; + +class EmojiMenuInModal extends AwardsHandler { + constructor(emoji, toggleButtonSelector, menuClass, selectEmojiCallback, targetContainerEl) { + super(emoji); + + this.selectEmojiCallback = selectEmojiCallback; + this.toggleButtonSelector = toggleButtonSelector; + this.menuClass = menuClass; + this.targetContainerEl = targetContainerEl; + + this.bindEvents(); + } + + postEmoji($emojiButton, awardUrl, selectedEmoji, callback) { + this.selectEmojiCallback(selectedEmoji, this.emoji.glEmojiTag(selectedEmoji)); + callback(); + } +} + +export default EmojiMenuInModal; diff --git a/app/assets/javascripts/set_status_modal/event_hub.js b/app/assets/javascripts/set_status_modal/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/set_status_modal/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue b/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue new file mode 100644 index 00000000000..48e5ede80f2 --- /dev/null +++ b/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue @@ -0,0 +1,33 @@ +<script> +import { s__ } from '~/locale'; +import eventHub from './event_hub'; + +export default { + props: { + hasStatus: { + type: Boolean, + required: true, + }, + }, + computed: { + buttonText() { + return this.hasStatus ? s__('SetStatusModal|Edit status') : s__('SetStatusModal|Set status'); + }, + }, + methods: { + openModal() { + eventHub.$emit('openModal'); + }, + }, +}; +</script> + +<template> + <button + type="button" + class="btn menu-item" + @click="openModal" + > + {{ buttonText }} + </button> +</template> diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue new file mode 100644 index 00000000000..43f0b6651b9 --- /dev/null +++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue @@ -0,0 +1,241 @@ +<script> +import $ from 'jquery'; +import createFlash from '~/flash'; +import Icon from '~/vue_shared/components/icon.vue'; +import GfmAutoComplete from '~/gfm_auto_complete'; +import { __, s__ } from '~/locale'; +import Api from '~/api'; +import eventHub from './event_hub'; +import EmojiMenuInModal from './emoji_menu_in_modal'; + +const emojiMenuClass = 'js-modal-status-emoji-menu'; + +export default { + components: { + Icon, + }, + props: { + currentEmoji: { + type: String, + required: true, + }, + currentMessage: { + type: String, + required: true, + }, + }, + data() { + return { + defaultEmojiTag: '', + emoji: this.currentEmoji, + emojiMenu: null, + emojiTag: '', + isEmojiMenuVisible: false, + message: this.currentMessage, + modalId: 'set-user-status-modal', + noEmoji: true, + }; + }, + computed: { + isDirty() { + return this.message.length || this.emoji.length; + }, + }, + mounted() { + eventHub.$on('openModal', this.openModal); + }, + beforeDestroy() { + this.emojiMenu.destroy(); + }, + methods: { + openModal() { + this.$root.$emit('bv::show::modal', this.modalId); + }, + closeModal() { + this.$root.$emit('bv::hide::modal', this.modalId); + }, + setupEmojiListAndAutocomplete() { + const toggleEmojiMenuButtonSelector = '#set-user-status-modal .js-toggle-emoji-menu'; + const emojiAutocomplete = new GfmAutoComplete(); + emojiAutocomplete.setup($(this.$refs.statusMessageField), { emojis: true }); + + import(/* webpackChunkName: 'emoji' */ '~/emoji') + .then(Emoji => { + if (this.emoji) { + this.emojiTag = Emoji.glEmojiTag(this.emoji); + } + this.noEmoji = this.emoji === ''; + this.defaultEmojiTag = Emoji.glEmojiTag('speech_balloon'); + + this.emojiMenu = new EmojiMenuInModal( + Emoji, + toggleEmojiMenuButtonSelector, + emojiMenuClass, + this.setEmoji, + this.$refs.userStatusForm, + ); + }) + .catch(() => createFlash(__('Failed to load emoji list.'))); + }, + showEmojiMenu() { + this.isEmojiMenuVisible = true; + this.emojiMenu.showEmojiMenu($(this.$refs.toggleEmojiMenuButton)); + }, + hideEmojiMenu() { + if (!this.isEmojiMenuVisible) { + return; + } + + this.isEmojiMenuVisible = false; + this.emojiMenu.hideMenuElement($(`.${emojiMenuClass}`)); + }, + setDefaultEmoji() { + const { emojiTag } = this; + const hasStatusMessage = this.message; + if (hasStatusMessage && emojiTag) { + return; + } + + if (hasStatusMessage) { + this.noEmoji = false; + this.emojiTag = this.defaultEmojiTag; + } else if (emojiTag === this.defaultEmojiTag) { + this.noEmoji = true; + this.clearEmoji(); + } + }, + setEmoji(emoji, emojiTag) { + this.emoji = emoji; + this.noEmoji = false; + this.clearEmoji(); + this.emojiTag = emojiTag; + }, + clearEmoji() { + if (this.emojiTag) { + this.emojiTag = ''; + } + }, + clearStatusInputs() { + this.emoji = ''; + this.message = ''; + this.noEmoji = true; + this.clearEmoji(); + this.hideEmojiMenu(); + }, + removeStatus() { + this.clearStatusInputs(); + this.setStatus(); + }, + setStatus() { + const { emoji, message } = this; + + Api.postUserStatus({ + emoji, + message, + }) + .then(this.onUpdateSuccess) + .catch(this.onUpdateFail); + }, + onUpdateSuccess() { + this.closeModal(); + window.location.reload(); + }, + onUpdateFail() { + createFlash( + s__("SetStatusModal|Sorry, we weren't able to set your status. Please try again later."), + ); + + this.closeModal(); + }, + }, +}; +</script> + +<template> + <gl-ui-modal + :title="s__('SetStatusModal|Set a status')" + :modal-id="modalId" + :ok-title="s__('SetStatusModal|Set status')" + :cancel-title="s__('SetStatusModal|Remove status')" + ok-variant="success" + class="set-user-status-modal" + @shown="setupEmojiListAndAutocomplete" + @hide="hideEmojiMenu" + @ok="setStatus" + @cancel="removeStatus" + > + <div> + <input + v-model="emoji" + class="js-status-emoji-field" + type="hidden" + name="user[status][emoji]" + /> + <div + ref="userStatusForm" + class="form-group position-relative m-0" + > + <div class="input-group"> + <span class="input-group-btn"> + <button + ref="toggleEmojiMenuButton" + v-gl-tooltip.bottom + :title="s__('SetStatusModal|Add status emoji')" + :aria-label="s__('SetStatusModal|Add status emoji')" + name="button" + type="button" + class="js-toggle-emoji-menu emoji-menu-toggle-button btn" + @click="showEmojiMenu" + > + <span v-html="emojiTag"></span> + <span + v-show="noEmoji" + class="js-no-emoji-placeholder no-emoji-placeholder position-relative" + > + <icon + name="emoji_slightly_smiling_face" + css-classes="award-control-icon-neutral" + /> + <icon + name="emoji_smiley" + css-classes="award-control-icon-positive" + /> + <icon + name="emoji_smile" + css-classes="award-control-icon-super-positive" + /> + </span> + </button> + </span> + <input + ref="statusMessageField" + v-model="message" + :placeholder="s__('SetStatusModal|What\'s your status?')" + type="text" + class="form-control form-control input-lg js-status-message-field" + name="user[status][message]" + @keyup="setDefaultEmoji" + @keyup.enter.prevent + @click="hideEmojiMenu" + /> + <span + v-show="isDirty" + class="input-group-btn" + > + <button + v-gl-tooltip.bottom + :title="s__('SetStatusModal|Clear status')" + :aria-label="s__('SetStatusModal|Clear status')" + name="button" + type="button" + class="js-clear-user-status-button clear-user-status btn" + @click="clearStatusInputs()" + > + <icon name="close" /> + </button> + </span> + </div> + </div> + </div> + </gl-ui-modal> +</template> diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index e19bbbacf4d..b9dfa22dd49 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -592,7 +592,7 @@ function UsersSelect(currentUser, els, options = {}) { if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) { var trimmed = query.term.trim(); emailUser = { - name: "Invite \"" + query.term + "\" by email", + name: "Invite \"" + trimmed + "\" by email", username: trimmed, id: trimmed, invite: true |