diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-20 16:37:47 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-20 16:37:47 +0300 |
commit | aee0a117a889461ce8ced6fcf73207fe017f1d99 (patch) | |
tree | 891d9ef189227a8445d83f35c1b0fc99573f4380 /app/assets/javascripts/milestones | |
parent | 8d46af3258650d305f53b819eabf7ab18d22f59e (diff) |
Add latest changes from gitlab-org/gitlab@14-6-stable-eev14.6.0-rc42
Diffstat (limited to 'app/assets/javascripts/milestones')
-rw-r--r-- | app/assets/javascripts/milestones/components/delete_milestone_modal.vue | 137 | ||||
-rw-r--r-- | app/assets/javascripts/milestones/components/promote_milestone_modal.vue | 104 | ||||
-rw-r--r-- | app/assets/javascripts/milestones/event_hub.js | 3 | ||||
-rw-r--r-- | app/assets/javascripts/milestones/index.js | 123 | ||||
-rw-r--r-- | app/assets/javascripts/milestones/milestone.js | 49 | ||||
-rw-r--r-- | app/assets/javascripts/milestones/milestone_select.js | 273 | ||||
-rw-r--r-- | app/assets/javascripts/milestones/utils.js (renamed from app/assets/javascripts/milestones/milestone_utils.js) | 0 |
7 files changed, 689 insertions, 0 deletions
diff --git a/app/assets/javascripts/milestones/components/delete_milestone_modal.vue b/app/assets/javascripts/milestones/components/delete_milestone_modal.vue new file mode 100644 index 00000000000..34f9fe778ea --- /dev/null +++ b/app/assets/javascripts/milestones/components/delete_milestone_modal.vue @@ -0,0 +1,137 @@ +<script> +import { GlSafeHtmlDirective as SafeHtml, GlModal } from '@gitlab/ui'; +import createFlash from '~/flash'; +import axios from '~/lib/utils/axios_utils'; + +import { redirectTo } from '~/lib/utils/url_utility'; +import { __, n__, s__, sprintf } from '~/locale'; +import eventHub from '../event_hub'; + +export default { + components: { + GlModal, + }, + directives: { + SafeHtml, + }, + props: { + issueCount: { + type: Number, + required: true, + }, + mergeRequestCount: { + type: Number, + required: true, + }, + milestoneId: { + type: Number, + required: true, + }, + milestoneTitle: { + type: String, + required: true, + }, + milestoneUrl: { + type: String, + required: true, + }, + }, + computed: { + text() { + const milestoneTitle = sprintf('<strong>%{milestoneTitle}</strong>', { + milestoneTitle: this.milestoneTitle, + }); + + if (this.issueCount === 0 && this.mergeRequestCount === 0) { + return sprintf( + s__(`Milestones| +You’re about to permanently delete the milestone %{milestoneTitle}. +This milestone is not currently used in any issues or merge requests.`), + { + milestoneTitle, + }, + false, + ); + } + + return sprintf( + s__(`Milestones| +You’re about to permanently delete the milestone %{milestoneTitle} and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}. +Once deleted, it cannot be undone or recovered.`), + { + milestoneTitle, + issuesWithCount: n__('%d issue', '%d issues', this.issueCount), + mergeRequestsWithCount: n__( + '%d merge request', + '%d merge requests', + this.mergeRequestCount, + ), + }, + false, + ); + }, + title() { + return sprintf(s__('Milestones|Delete milestone %{milestoneTitle}?'), { + milestoneTitle: this.milestoneTitle, + }); + }, + }, + methods: { + onSubmit() { + eventHub.$emit('deleteMilestoneModal.requestStarted', this.milestoneUrl); + + return axios + .delete(this.milestoneUrl) + .then((response) => { + eventHub.$emit('deleteMilestoneModal.requestFinished', { + milestoneUrl: this.milestoneUrl, + successful: true, + }); + + // follow the rediect to milestones overview page + redirectTo(response.request.responseURL); + }) + .catch((error) => { + eventHub.$emit('deleteMilestoneModal.requestFinished', { + milestoneUrl: this.milestoneUrl, + successful: false, + }); + + if (error.response && error.response.status === 404) { + createFlash({ + message: sprintf(s__('Milestones|Milestone %{milestoneTitle} was not found'), { + milestoneTitle: this.milestoneTitle, + }), + }); + } else { + createFlash({ + message: sprintf(s__('Milestones|Failed to delete milestone %{milestoneTitle}'), { + milestoneTitle: this.milestoneTitle, + }), + }); + } + throw error; + }); + }, + }, + primaryProps: { + text: s__('Milestones|Delete milestone'), + attributes: [{ variant: 'danger' }, { category: 'primary' }], + }, + cancelProps: { + text: __('Cancel'), + }, +}; +</script> + +<template> + <gl-modal + modal-id="delete-milestone-modal" + :title="title" + :action-primary="$options.primaryProps" + :action-cancel="$options.cancelProps" + @primary="onSubmit" + > + <p v-safe-html="text"></p> + </gl-modal> +</template> diff --git a/app/assets/javascripts/milestones/components/promote_milestone_modal.vue b/app/assets/javascripts/milestones/components/promote_milestone_modal.vue new file mode 100644 index 00000000000..b41611001ab --- /dev/null +++ b/app/assets/javascripts/milestones/components/promote_milestone_modal.vue @@ -0,0 +1,104 @@ +<script> +import { GlModal } from '@gitlab/ui'; +import createFlash from '~/flash'; +import axios from '~/lib/utils/axios_utils'; +import { visitUrl } from '~/lib/utils/url_utility'; +import { __, s__, sprintf } from '~/locale'; + +export default { + components: { + GlModal, + }, + data() { + return { + milestoneTitle: '', + url: '', + groupName: '', + currentButton: null, + visible: false, + }; + }, + computed: { + title() { + return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), { + milestoneTitle: this.milestoneTitle, + }); + }, + text() { + return sprintf( + s__(`Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}. + Existing project milestones with the same title will be merged.`), + { milestoneTitle: this.milestoneTitle, groupName: this.groupName }, + ); + }, + }, + mounted() { + this.getButtons().forEach((button) => { + button.addEventListener('click', this.onPromoteButtonClick); + button.removeAttribute('disabled'); + }); + }, + beforeDestroy() { + this.getButtons().forEach((button) => { + button.removeEventListener('click', this.onPromoteButtonClick); + }); + }, + methods: { + onPromoteButtonClick({ currentTarget }) { + const { milestoneTitle, url, groupName } = currentTarget.dataset; + currentTarget.setAttribute('disabled', ''); + this.visible = true; + this.milestoneTitle = milestoneTitle; + this.url = url; + this.groupName = groupName; + this.currentButton = currentTarget; + }, + getButtons() { + return document.querySelectorAll('.js-promote-project-milestone-button'); + }, + onSubmit() { + return axios + .post(this.url, { params: { format: 'json' } }) + .then((response) => { + visitUrl(response.data.url); + }) + .catch((error) => { + createFlash({ + message: error, + }); + }) + .finally(() => { + this.visible = false; + }); + }, + onClose() { + this.visible = false; + if (this.currentButton) { + this.currentButton.removeAttribute('disabled'); + } + }, + }, + primaryAction: { + text: s__('Milestones|Promote Milestone'), + attributes: [{ variant: 'warning' }], + }, + cancelAction: { + text: __('Cancel'), + attributes: [], + }, +}; +</script> +<template> + <gl-modal + :visible="visible" + modal-id="promote-milestone-modal" + :action-primary="$options.primaryAction" + :action-cancel="$options.cancelAction" + :title="title" + @primary="onSubmit" + @hide="onClose" + > + <p>{{ text }}</p> + <p>{{ s__('Milestones|This action cannot be reversed.') }}</p> + </gl-modal> +</template> diff --git a/app/assets/javascripts/milestones/event_hub.js b/app/assets/javascripts/milestones/event_hub.js new file mode 100644 index 00000000000..e31806ad199 --- /dev/null +++ b/app/assets/javascripts/milestones/event_hub.js @@ -0,0 +1,3 @@ +import createEventHub from '~/helpers/event_hub_factory'; + +export default createEventHub(); diff --git a/app/assets/javascripts/milestones/index.js b/app/assets/javascripts/milestones/index.js new file mode 100644 index 00000000000..2ca5f104b4f --- /dev/null +++ b/app/assets/javascripts/milestones/index.js @@ -0,0 +1,123 @@ +import $ from 'jquery'; +import Vue from 'vue'; +import initDatePicker from '~/behaviors/date_picker'; +import GLForm from '~/gl_form'; +import { BV_SHOW_MODAL } from '~/lib/utils/constants'; +import Milestone from '~/milestones/milestone'; +import Sidebar from '~/right_sidebar'; +import MountMilestoneSidebar from '~/sidebar/mount_milestone_sidebar'; +import Translate from '~/vue_shared/translate'; +import ZenMode from '~/zen_mode'; +import DeleteMilestoneModal from './components/delete_milestone_modal.vue'; +import PromoteMilestoneModal from './components/promote_milestone_modal.vue'; +import eventHub from './event_hub'; + +export function initForm(initGFM = true) { + new ZenMode(); // eslint-disable-line no-new + initDatePicker(); + + // eslint-disable-next-line no-new + new GLForm($('.milestone-form'), { + emojis: true, + members: initGFM, + issues: initGFM, + mergeRequests: initGFM, + epics: initGFM, + milestones: initGFM, + labels: initGFM, + snippets: initGFM, + vulnerabilities: initGFM, + }); +} + +export function initShow() { + new Milestone(); // eslint-disable-line no-new + new Sidebar(); // eslint-disable-line no-new + new MountMilestoneSidebar(); // eslint-disable-line no-new +} + +export function initPromoteMilestoneModal() { + Vue.use(Translate); + + const promoteMilestoneModal = document.getElementById('promote-milestone-modal'); + if (!promoteMilestoneModal) { + return null; + } + + return new Vue({ + el: promoteMilestoneModal, + render(createElement) { + return createElement(PromoteMilestoneModal); + }, + }); +} + +export function initDeleteMilestoneModal() { + Vue.use(Translate); + + const onRequestFinished = ({ milestoneUrl, successful }) => { + const button = document.querySelector( + `.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`, + ); + + if (!successful) { + button.removeAttribute('disabled'); + } + + button.querySelector('.js-loading-icon').classList.add('hidden'); + }; + + const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button'); + + const onRequestStarted = (milestoneUrl) => { + const button = document.querySelector( + `.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`, + ); + button.setAttribute('disabled', ''); + button.querySelector('.js-loading-icon').classList.remove('hidden'); + eventHub.$once('deleteMilestoneModal.requestFinished', onRequestFinished); + }; + + return new Vue({ + el: '#js-delete-milestone-modal', + data() { + return { + modalProps: { + milestoneId: -1, + milestoneTitle: '', + milestoneUrl: '', + issueCount: -1, + mergeRequestCount: -1, + }, + }; + }, + mounted() { + eventHub.$on('deleteMilestoneModal.props', this.setModalProps); + deleteMilestoneButtons.forEach((button) => { + button.removeAttribute('disabled'); + button.addEventListener('click', () => { + this.$root.$emit(BV_SHOW_MODAL, 'delete-milestone-modal'); + eventHub.$once('deleteMilestoneModal.requestStarted', onRequestStarted); + + this.setModalProps({ + milestoneId: parseInt(button.dataset.milestoneId, 10), + milestoneTitle: button.dataset.milestoneTitle, + milestoneUrl: button.dataset.milestoneUrl, + issueCount: parseInt(button.dataset.milestoneIssueCount, 10), + mergeRequestCount: parseInt(button.dataset.milestoneMergeRequestCount, 10), + }); + }); + }); + }, + methods: { + setModalProps(modalProps) { + this.modalProps = modalProps; + }, + }, + render(createElement) { + return createElement(DeleteMilestoneModal, { + props: this.modalProps, + }); + }, + }); +} diff --git a/app/assets/javascripts/milestones/milestone.js b/app/assets/javascripts/milestones/milestone.js new file mode 100644 index 00000000000..05102f73f92 --- /dev/null +++ b/app/assets/javascripts/milestones/milestone.js @@ -0,0 +1,49 @@ +import createFlash from '~/flash'; +import { sanitize } from '~/lib/dompurify'; +import axios from '~/lib/utils/axios_utils'; +import { historyPushState } from '~/lib/utils/common_utils'; +import { __ } from '~/locale'; +import { GlTabsBehavior, TAB_SHOWN_EVENT } from '~/tabs'; + +export default class Milestone { + constructor() { + this.tabsEl = document.querySelector('.js-milestone-tabs'); + this.glTabs = new GlTabsBehavior(this.tabsEl); + this.loadedTabs = new WeakSet(); + + this.bindTabsSwitching(); + this.loadInitialTab(); + } + + bindTabsSwitching() { + this.tabsEl.addEventListener(TAB_SHOWN_EVENT, (event) => { + const tab = event.target; + const { activeTabPanel } = event.detail; + historyPushState(tab.getAttribute('href')); + this.loadTab(tab, activeTabPanel); + }); + } + + loadInitialTab() { + const tab = this.tabsEl.querySelector(`a[href="${window.location.hash}"]`); + this.glTabs.activateTab(tab || this.glTabs.activeTab); + } + loadTab(tab, tabPanel) { + const { endpoint } = tab.dataset; + + if (endpoint && !this.loadedTabs.has(tab)) { + axios + .get(endpoint) + .then(({ data }) => { + // eslint-disable-next-line no-param-reassign + tabPanel.innerHTML = sanitize(data.html); + this.loadedTabs.add(tab); + }) + .catch(() => + createFlash({ + message: __('Error loading milestone tab'), + }), + ); + } + } +} diff --git a/app/assets/javascripts/milestones/milestone_select.js b/app/assets/javascripts/milestones/milestone_select.js new file mode 100644 index 00000000000..c95ec3dd10b --- /dev/null +++ b/app/assets/javascripts/milestones/milestone_select.js @@ -0,0 +1,273 @@ +/* eslint-disable one-var, no-self-compare, consistent-return, no-param-reassign, no-shadow */ +/* global Issuable */ + +import $ from 'jquery'; +import { template, escape } from 'lodash'; +import Api from '~/api'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; +import { __, sprintf } from '~/locale'; +import { sortMilestonesByDueDate } from '~/milestones/utils'; +import axios from '~/lib/utils/axios_utils'; +import { timeFor, parsePikadayDate, dateInWords } from '~/lib/utils/datetime_utility'; + +export default class MilestoneSelect { + constructor(currentProject, els, options = {}) { + if (currentProject !== null) { + this.currentProject = + typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject; + } + + MilestoneSelect.init(els, options); + } + + static init(els, options) { + let $els = $(els); + + if (!els) { + $els = $('.js-milestone-select'); + } + + $els.each((i, dropdown) => { + let milestoneLinkNoneTemplate, + milestoneLinkTemplate, + milestoneExpiredLinkTemplate, + selectedMilestone, + selectedMilestoneDefault; + const $dropdown = $(dropdown); + const issueUpdateURL = $dropdown.data('issueUpdate'); + const showNo = $dropdown.data('showNo'); + const showAny = $dropdown.data('showAny'); + const showMenuAbove = $dropdown.data('showMenuAbove'); + const showUpcoming = $dropdown.data('showUpcoming'); + const showStarted = $dropdown.data('showStarted'); + const useId = $dropdown.data('useId'); + const defaultLabel = $dropdown.data('defaultLabel'); + const defaultNo = $dropdown.data('defaultNo'); + const abilityName = $dropdown.data('abilityName'); + const $selectBox = $dropdown.closest('.selectbox'); + const $block = $selectBox.closest('.block'); + const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon'); + const $value = $block.find('.value'); + const $loading = $block.find('.block-loading').addClass('gl-display-none'); + selectedMilestoneDefault = showAny ? '' : null; + selectedMilestoneDefault = + showNo && defaultNo ? __('No milestone') : selectedMilestoneDefault; + selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault; + + if (issueUpdateURL) { + milestoneLinkTemplate = template( + '<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>', + ); + milestoneExpiredLinkTemplate = template( + '<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %> (Past due)</a>', + ); + milestoneLinkNoneTemplate = `<span class="no-value">${__('None')}</span>`; + } + return initDeprecatedJQueryDropdown($dropdown, { + showMenuAbove, + data: (term, callback) => { + let contextId = parseInt($dropdown.get(0).dataset.projectId, 10); + let getMilestones = Api.projectMilestones.bind(Api); + const reqParams = { state: 'active', include_parent_milestones: true }; + + if (term) { + reqParams.search = term.trim(); + } + + if (!contextId) { + contextId = $dropdown.get(0).dataset.groupId; + delete reqParams.include_parent_milestones; + getMilestones = Api.groupMilestones.bind(Api); + } + + // We don't use $.data() as it caches initial value and never updates! + return getMilestones(contextId, reqParams) + .then(({ data }) => + data + .map((m) => ({ + ...m, + // Public API includes `title` instead of `name`. + name: m.title, + })) + .sort(sortMilestonesByDueDate), + ) + .then((data) => { + const extraOptions = []; + if (showAny) { + extraOptions.push({ + id: null, + name: null, + title: __('Any milestone'), + }); + } + if (showNo && term.trim() === '') { + extraOptions.push({ + id: -1, + name: __('No milestone'), + title: __('No milestone'), + }); + } + if (showUpcoming) { + extraOptions.push({ + id: -2, + name: '#upcoming', + title: __('Upcoming'), + }); + } + if (showStarted) { + extraOptions.push({ + id: -3, + name: '#started', + title: __('Started'), + }); + } + if (extraOptions.length) { + extraOptions.push({ type: 'divider' }); + } + + callback(extraOptions.concat(data)); + if (showMenuAbove) { + $dropdown.data('deprecatedJQueryDropdown').positionMenuAbove(); + } + $(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active'); + }); + }, + renderRow: (milestone) => { + const milestoneName = milestone.title || milestone.name; + let milestoneDisplayName = escape(milestoneName); + + if (milestone.expired) { + milestoneDisplayName = sprintf(__('%{milestone} (expired)'), { + milestone: milestoneDisplayName, + }); + } + + return ` + <li data-milestone-id="${escape(milestoneName)}"> + <a href='#' class='dropdown-menu-milestone-link'> + ${milestoneDisplayName} + </a> + </li> + `; + }, + filterable: true, + filterRemote: true, + search: { + fields: ['title'], + }, + selectable: true, + toggleLabel: (selected, el) => { + if (selected && 'id' in selected && $(el).hasClass('is-active')) { + return selected.title; + } + return defaultLabel; + }, + defaultLabel, + fieldName: $dropdown.data('fieldName'), + text: (milestone) => escape(milestone.title), + id: (milestone) => { + if (milestone !== undefined) { + if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) { + return milestone.name; + } + + return milestone.id; + } + }, + hidden: () => { + $selectBox.hide(); + // display:block overrides the hide-collapse rule + return $value.css('display', ''); + }, + opened: (e) => { + const $el = $(e.currentTarget); + if (options.handleClick) { + selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault; + } + $('a.is-active', $el).removeClass('is-active'); + $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active'); + }, + vue: false, + clicked: (clickEvent) => { + const { e } = clickEvent; + let selected = clickEvent.selectedObj; + + if (!selected) return; + + if (options.handleClick) { + e.preventDefault(); + options.handleClick(selected); + return; + } + + const page = $('body').attr('data-page'); + const isIssueIndex = page === 'projects:issues:index'; + const isMRIndex = page === page && page === 'projects:merge_requests:index'; + const isSelecting = selected.name !== selectedMilestone; + selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault; + + if ( + $dropdown.hasClass('js-filter-bulk-update') || + $dropdown.hasClass('js-issuable-form-dropdown') + ) { + e.preventDefault(); + return; + } + + if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { + return Issuable.filterResults($dropdown.closest('form')); + } else if ($dropdown.hasClass('js-filter-submit')) { + return $dropdown.closest('form').submit(); + } + + selected = $selectBox.find('input[type="hidden"]').val(); + + const data = {}; + data[abilityName] = {}; + data[abilityName].milestone_id = selected != null ? selected : null; + $loading.removeClass('gl-display-none'); + $dropdown.trigger('loading.gl.dropdown'); + return axios + .put(issueUpdateURL, data) + .then(({ data }) => { + $dropdown.trigger('loaded.gl.dropdown'); + $loading.addClass('gl-display-none'); + $selectBox.hide(); + $value.css('display', ''); + if (data.milestone != null) { + data.milestone.remaining = timeFor(data.milestone.due_date); + data.milestone.name = data.milestone.title; + $value.html( + data.milestone.expired + ? milestoneExpiredLinkTemplate({ + ...data.milestone, + remaining: sprintf(__('%{due_date} (Past due)'), { + due_date: dateInWords(parsePikadayDate(data.milestone.due_date)), + }), + }) + : milestoneLinkTemplate(data.milestone), + ); + return $sidebarCollapsedValue + .attr( + 'data-original-title', + `${data.milestone.name}<br />${data.milestone.remaining}`, + ) + .find('span') + .text(data.milestone.title); + } + $value.html(milestoneLinkNoneTemplate); + return $sidebarCollapsedValue + .attr('data-original-title', __('Milestone')) + .find('span') + .text(__('None')); + }) + .catch(() => { + $loading.addClass('gl-display-none'); + }); + }, + }); + }); + } +} + +window.MilestoneSelect = MilestoneSelect; diff --git a/app/assets/javascripts/milestones/milestone_utils.js b/app/assets/javascripts/milestones/utils.js index 3ae5e676138..3ae5e676138 100644 --- a/app/assets/javascripts/milestones/milestone_utils.js +++ b/app/assets/javascripts/milestones/utils.js |