diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-18 09:09:24 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-18 09:09:24 +0000 |
commit | 4720b569f0fcbb47e9f1a60e95172ae63b6f065a (patch) | |
tree | 5c6bcecbca227e608753a57a9aad19ccfe0567b6 /app | |
parent | cefe554b7ce2d0b52f9de855be832a47c2bc24ab (diff) | |
download | gitlab-ce-4720b569f0fcbb47e9f1a60e95172ae63b6f065a.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
26 files changed, 204 insertions, 72 deletions
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index b973316b3b9..218bf41cd58 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -1,3 +1,4 @@ +/* eslint-disable max-classes-per-file */ import $ from 'jquery'; import Pikaday from 'pikaday'; import dateFormat from 'dateformat'; diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 3d5c03440ea..918276ce329 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -1,4 +1,4 @@ -/* eslint-disable one-var, consistent-return */ +/* eslint-disable max-classes-per-file, one-var, consistent-return */ import $ from 'jquery'; import _ from 'underscore'; diff --git a/app/assets/javascripts/jobs/components/environments_block.vue b/app/assets/javascripts/jobs/components/environments_block.vue index 797aa15a2b9..d9168f57cc7 100644 --- a/app/assets/javascripts/jobs/components/environments_block.vue +++ b/app/assets/javascripts/jobs/components/environments_block.vue @@ -1,5 +1,5 @@ <script> -import _ from 'underscore'; +import { escape as esc, isEmpty } from 'lodash'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import { sprintf, __ } from '../../locale'; @@ -43,7 +43,7 @@ export default { '%{startLink}%{name}%{endLink}', { startLink: `<a href="${this.deploymentStatus.environment.environment_path}" class="js-environment-link">`, - name: _.escape(this.deploymentStatus.environment.name), + name: esc(this.deploymentStatus.environment.name), endLink: '</a>', }, false, @@ -58,10 +58,10 @@ export default { return this.hasLastDeployment ? this.deploymentStatus.environment.last_deployment : {}; }, hasEnvironment() { - return !_.isEmpty(this.deploymentStatus.environment); + return !isEmpty(this.deploymentStatus.environment); }, lastDeploymentPath() { - return !_.isEmpty(this.lastDeployment.deployable) + return !isEmpty(this.lastDeployment.deployable) ? this.lastDeployment.deployable.build_path : ''; }, @@ -74,8 +74,8 @@ export default { } const { name, path } = this.deploymentCluster; - const escapedName = _.escape(name); - const escapedPath = _.escape(path); + const escapedName = esc(name); + const escapedPath = esc(path); if (!escapedPath) { return escapedName; diff --git a/app/assets/javascripts/jobs/components/erased_block.vue b/app/assets/javascripts/jobs/components/erased_block.vue index 8437ad89301..fc5e022f44a 100644 --- a/app/assets/javascripts/jobs/components/erased_block.vue +++ b/app/assets/javascripts/jobs/components/erased_block.vue @@ -1,5 +1,5 @@ <script> -import _ from 'underscore'; +import { isEmpty } from 'lodash'; import { GlLink } from '@gitlab/ui'; import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; @@ -21,7 +21,7 @@ export default { }, computed: { isErasedByUser() { - return !_.isEmpty(this.user); + return !isEmpty(this.user); }, }, }; diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index bc310f77a58..0783d1157be 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -1,5 +1,5 @@ <script> -import _ from 'underscore'; +import { throttle, isEmpty } from 'lodash'; import { mapGetters, mapState, mapActions } from 'vuex'; import { GlLoadingIcon } from '@gitlab/ui'; import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; @@ -125,7 +125,7 @@ export default { // Once the job log is loaded, // fetch the stages for the dropdown on the sidebar job(newVal, oldVal) { - if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) { + if (isEmpty(oldVal) && !isEmpty(newVal.pipeline)) { const stages = this.job.pipeline.details.stages || []; const defaultStage = stages.find(stage => stage && stage.name === this.selectedStage); @@ -145,7 +145,7 @@ export default { }, }, created() { - this.throttled = _.throttle(this.toggleScrollButtons, 100); + this.throttled = throttle(this.toggleScrollButtons, 100); window.addEventListener('resize', this.onResize); window.addEventListener('scroll', this.updateScroll); diff --git a/app/assets/javascripts/jobs/components/manual_variables_form.vue b/app/assets/javascripts/jobs/components/manual_variables_form.vue index c32a3cac7be..a23f30d571a 100644 --- a/app/assets/javascripts/jobs/components/manual_variables_form.vue +++ b/app/assets/javascripts/jobs/components/manual_variables_form.vue @@ -1,5 +1,5 @@ <script> -import _ from 'underscore'; +import { uniqueId } from 'lodash'; import { mapActions } from 'vuex'; import { GlButton } from '@gitlab/ui'; import { s__, sprintf } from '~/locale'; @@ -19,7 +19,9 @@ export default { validator(value) { return ( value === null || - (_.has(value, 'path') && _.has(value, 'method') && _.has(value, 'button_title')) + (Object.prototype.hasOwnProperty.call(value, 'path') && + Object.prototype.hasOwnProperty.call(value, 'method') && + Object.prototype.hasOwnProperty.call(value, 'button_title')) ); }, }, @@ -78,7 +80,7 @@ export default { const newVariable = { key: this.key, secret_value: this.secretValue, - id: _.uniqueId(), + id: uniqueId(), }; this.variables.push(newVariable); diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue index a61acf2f6eb..f1683bc2195 100644 --- a/app/assets/javascripts/jobs/components/sidebar.vue +++ b/app/assets/javascripts/jobs/components/sidebar.vue @@ -1,5 +1,5 @@ <script> -import _ from 'underscore'; +import { isEmpty } from 'lodash'; import { mapActions, mapState } from 'vuex'; import { GlLink, GlButton } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; @@ -84,10 +84,10 @@ export default { ); }, hasArtifact() { - return !_.isEmpty(this.job.artifact); + return !isEmpty(this.job.artifact); }, hasTriggers() { - return !_.isEmpty(this.job.trigger); + return !isEmpty(this.job.trigger); }, hasStages() { return ( diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue index 09f9647a680..ddcfc3d6db6 100644 --- a/app/assets/javascripts/jobs/components/stages_dropdown.vue +++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue @@ -1,5 +1,5 @@ <script> -import _ from 'underscore'; +import { isEmpty } from 'lodash'; import { GlLink } from '@gitlab/ui'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; @@ -24,7 +24,7 @@ export default { }, computed: { hasRef() { - return !_.isEmpty(this.pipeline.ref); + return !isEmpty(this.pipeline.ref); }, isTriggeredByMergeRequest() { return Boolean(this.pipeline.merge_request); diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js index 406b1a2e375..3f02f924eed 100644 --- a/app/assets/javascripts/jobs/store/getters.js +++ b/app/assets/javascripts/jobs/store/getters.js @@ -1,4 +1,4 @@ -import _ from 'underscore'; +import { isEmpty, isString } from 'lodash'; import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; export const headerTime = state => (state.job.started ? state.job.started : state.job.created_at); @@ -7,15 +7,15 @@ export const hasUnmetPrerequisitesFailure = state => state.job && state.job.failure_reason && state.job.failure_reason === 'unmet_prerequisites'; export const shouldRenderCalloutMessage = state => - !_.isEmpty(state.job.status) && !_.isEmpty(state.job.callout_message); + !isEmpty(state.job.status) && !isEmpty(state.job.callout_message); /** * When job has not started the key will be null * When job started the key will be a string with a date. */ -export const shouldRenderTriggeredLabel = state => _.isString(state.job.started); +export const shouldRenderTriggeredLabel = state => isString(state.job.started); -export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status); +export const hasEnvironment = state => !isEmpty(state.job.deployment_status); /** * Checks if it the job has trace. @@ -23,7 +23,7 @@ export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status); * @returns {Boolean} */ export const hasTrace = state => - state.job.has_trace || (!_.isEmpty(state.job.status) && state.job.status.group === 'running'); + state.job.has_trace || (!isEmpty(state.job.status) && state.job.status.group === 'running'); export const emptyStateIllustration = state => (state.job && state.job.status && state.job.status.illustration) || {}; @@ -38,8 +38,8 @@ export const emptyStateAction = state => * @returns {Boolean} */ export const shouldRenderSharedRunnerLimitWarning = state => - !_.isEmpty(state.job.runners) && - !_.isEmpty(state.job.runners.quota) && + !isEmpty(state.job.runners) && + !isEmpty(state.job.runners.quota) && state.job.runners.quota.used >= state.job.runners.quota.limit; export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceComplete; diff --git a/app/assets/javascripts/monitoring/components/charts/anomaly.vue b/app/assets/javascripts/monitoring/components/charts/anomaly.vue index bcbc1dad89d..447f8845506 100644 --- a/app/assets/javascripts/monitoring/components/charts/anomaly.vue +++ b/app/assets/javascripts/monitoring/components/charts/anomaly.vue @@ -127,7 +127,6 @@ export default { }); const yAxisWithOffset = { - name: this.yAxisLabel, axisLabel: { formatter: num => roundOffFloat(num - this.yOffset, 3).toString(), }, @@ -162,6 +161,7 @@ export default { }), ); } + return { yAxis: yAxisWithOffset, series: boundarySeries }; }, }, diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue index e6dbc38402e..d2b1e4da3fd 100644 --- a/app/assets/javascripts/monitoring/components/charts/time_series.vue +++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue @@ -162,7 +162,8 @@ export default { ); }, chartOptions() { - const option = omit(this.option, 'series'); + const { yAxis, xAxis } = this.option; + const option = omit(this.option, ['series', 'yAxis', 'xAxis']); const dataYAxis = { name: this.yAxisLabel, @@ -173,7 +174,9 @@ export default { axisLabel: { formatter: num => roundOffFloat(num, 3).toString(), }, + ...yAxis, }; + const deploymentsYAxis = { show: false, min: deploymentYAxisCoords.min, @@ -184,18 +187,21 @@ export default { }, }; + const timeXAxis = { + name: __('Time'), + type: 'time', + axisLabel: { + formatter: date => dateFormat(date, dateFormats.timeOfDay), + }, + axisPointer: { + snap: true, + }, + ...xAxis, + }; + return { series: this.chartOptionSeries, - xAxis: { - name: __('Time'), - type: 'time', - axisLabel: { - formatter: date => dateFormat(date, dateFormats.timeOfDay), - }, - axisPointer: { - snap: true, - }, - }, + xAxis: timeXAxis, yAxis: [dataYAxis, deploymentsYAxis], dataZoom: [this.dataZoomConfig], ...option, diff --git a/app/assets/javascripts/pages/groups/registry/repositories/index.js b/app/assets/javascripts/pages/groups/registry/repositories/index.js index 52fb839e3fd..47fea2be189 100644 --- a/app/assets/javascripts/pages/groups/registry/repositories/index.js +++ b/app/assets/javascripts/pages/groups/registry/repositories/index.js @@ -3,5 +3,7 @@ import registryExplorer from '~/registry/explorer/index'; document.addEventListener('DOMContentLoaded', () => { initRegistryImages(); - registryExplorer(); + const { attachMainComponent, attachBreadcrumb } = registryExplorer(); + attachBreadcrumb(); + attachMainComponent(); }); diff --git a/app/assets/javascripts/pages/projects/registry/repositories/index.js b/app/assets/javascripts/pages/projects/registry/repositories/index.js index 52fb839e3fd..47fea2be189 100644 --- a/app/assets/javascripts/pages/projects/registry/repositories/index.js +++ b/app/assets/javascripts/pages/projects/registry/repositories/index.js @@ -3,5 +3,7 @@ import registryExplorer from '~/registry/explorer/index'; document.addEventListener('DOMContentLoaded', () => { initRegistryImages(); - registryExplorer(); + const { attachMainComponent, attachBreadcrumb } = registryExplorer(); + attachBreadcrumb(); + attachMainComponent(); }); diff --git a/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue b/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue new file mode 100644 index 00000000000..f51948da8cc --- /dev/null +++ b/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue @@ -0,0 +1,59 @@ +<script> +import { initial, first, last } from 'lodash'; + +export default { + props: { + crumbs: { + type: Array, + required: true, + }, + }, + computed: { + rootRoute() { + return this.$router.options.routes.find(r => r.meta.root); + }, + isRootRoute() { + return this.$route.name === this.rootRoute.name; + }, + rootCrumbs() { + return initial(this.crumbs); + }, + divider() { + const { classList, tagName, innerHTML } = first(this.crumbs).querySelector('svg'); + return { classList: [...classList], tagName, innerHTML }; + }, + lastCrumb() { + const { children } = last(this.crumbs); + const { tagName, classList } = first(children); + return { + tagName, + classList: [...classList], + text: this.$route.meta.nameGenerator(this.$route), + path: { to: this.$route.name }, + }; + }, + }, +}; +</script> + +<template> + <ul> + <li + v-for="(crumb, index) in rootCrumbs" + :key="index" + :class="crumb.classList" + v-html="crumb.innerHTML" + ></li> + <li v-if="!isRootRoute"> + <router-link ref="rootRouteLink" :to="rootRoute.path"> + {{ rootRoute.meta.nameGenerator(rootRoute) }} + </router-link> + <component :is="divider.tagName" :class="divider.classList" v-html="divider.innerHTML" /> + </li> + <li> + <component :is="lastCrumb.tagName" ref="lastCrumb" :class="lastCrumb.classList"> + <router-link ref="childRouteLink" :to="lastCrumb.path">{{ lastCrumb.text }}</router-link> + </component> + </li> + </ul> +</template> diff --git a/app/assets/javascripts/registry/explorer/index.js b/app/assets/javascripts/registry/explorer/index.js index daa2e4fb109..a36978303c6 100644 --- a/app/assets/javascripts/registry/explorer/index.js +++ b/app/assets/javascripts/registry/explorer/index.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import Translate from '~/vue_shared/translate'; import RegistryExplorer from './pages/index.vue'; +import RegistryBreadcrumb from './components/registry_breadcrumb.vue'; import { createStore } from './stores'; import createRouter from './router'; @@ -19,15 +20,39 @@ export default () => { const router = createRouter(endpoint, store); store.dispatch('setInitialState', el.dataset); - return new Vue({ - el, - store, - router, - components: { - RegistryExplorer, - }, - render(createElement) { - return createElement('registry-explorer'); - }, - }); + const attachMainComponent = () => + new Vue({ + el, + store, + router, + components: { + RegistryExplorer, + }, + render(createElement) { + return createElement('registry-explorer'); + }, + }); + + const attachBreadcrumb = () => { + const breadCrumbEl = document.querySelector('nav .js-breadcrumbs-list'); + const crumbs = [...document.querySelectorAll('.js-breadcrumbs-list li')]; + return new Vue({ + el: breadCrumbEl, + store, + router, + components: { + RegistryBreadcrumb, + }, + render(createElement) { + return createElement('registry-breadcrumb', { + class: breadCrumbEl.className, + props: { + crumbs, + }, + }); + }, + }); + }; + + return { attachBreadcrumb, attachMainComponent }; }; diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/registry/explorer/pages/details.vue index bff67bb8376..bc613db8672 100644 --- a/app/assets/javascripts/registry/explorer/pages/details.vue +++ b/app/assets/javascripts/registry/explorer/pages/details.vue @@ -19,6 +19,7 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import { numberToHumanSize } from '~/lib/utils/number_utils'; import timeagoMixin from '~/vue_shared/mixins/timeago'; import Tracking from '~/tracking'; +import { decodeAndParse } from '../utils'; import { LIST_KEY_TAG, LIST_KEY_IMAGE_ID, @@ -62,7 +63,7 @@ export default { computed: { ...mapState(['tags', 'tagsPagination', 'isLoading', 'config']), imageName() { - const { name } = JSON.parse(window.atob(this.$route.params.id)); + const { name } = decodeAndParse(this.$route.params.id); return name; }, fields() { @@ -169,7 +170,7 @@ export default { }, handleSingleDelete(itemToDelete) { this.itemsToBeDeleted = []; - this.requestDeleteTag({ tag: itemToDelete, imageId: this.$route.params.id }); + this.requestDeleteTag({ tag: itemToDelete, params: this.$route.params.id }); }, handleMultipleDelete() { const { itemsToBeDeleted } = this; @@ -178,7 +179,7 @@ export default { this.requestDeleteTags({ ids: itemsToBeDeleted.map(x => this.tags[x].name), - imageId: this.$route.params.id, + params: this.$route.params.id, }); }, onDeletionConfirmed() { diff --git a/app/assets/javascripts/registry/explorer/pages/list.vue b/app/assets/javascripts/registry/explorer/pages/list.vue index dc730ac2828..1dbc7cc2242 100644 --- a/app/assets/javascripts/registry/explorer/pages/list.vue +++ b/app/assets/javascripts/registry/explorer/pages/list.vue @@ -70,7 +70,7 @@ export default { this.itemToDelete = {}; }, encodeListItem(item) { - const params = JSON.stringify({ name: item.path, tags_path: item.tags_path }); + const params = JSON.stringify({ name: item.path, tags_path: item.tags_path, id: item.id }); return window.btoa(params); }, }, diff --git a/app/assets/javascripts/registry/explorer/router.js b/app/assets/javascripts/registry/explorer/router.js index 8cf35b8f245..7e4c3d28623 100644 --- a/app/assets/javascripts/registry/explorer/router.js +++ b/app/assets/javascripts/registry/explorer/router.js @@ -1,8 +1,9 @@ import Vue from 'vue'; import VueRouter from 'vue-router'; -import { __ } from '~/locale'; +import { s__ } from '~/locale'; import List from './pages/list.vue'; import Details from './pages/details.vue'; +import { decodeAndParse } from './utils'; Vue.use(VueRouter); @@ -16,7 +17,8 @@ export default function createRouter(base, store) { path: '/', component: List, meta: { - name: __('Container Registry'), + nameGenerator: () => s__('ContainerRegistry|Container Registry'), + root: true, }, beforeEnter: (to, from, next) => { store.dispatch('requestImagesList'); @@ -28,10 +30,10 @@ export default function createRouter(base, store) { path: '/:id', component: Details, meta: { - name: __('Tags'), + nameGenerator: route => decodeAndParse(route.params.id).name, }, beforeEnter: (to, from, next) => { - store.dispatch('requestTagsList', { id: to.params.id }); + store.dispatch('requestTagsList', { params: to.params.id }); next(); }, }, diff --git a/app/assets/javascripts/registry/explorer/stores/actions.js b/app/assets/javascripts/registry/explorer/stores/actions.js index 25ff105ac53..86d00d4fca9 100644 --- a/app/assets/javascripts/registry/explorer/stores/actions.js +++ b/app/assets/javascripts/registry/explorer/stores/actions.js @@ -13,6 +13,7 @@ import { DELETE_IMAGE_ERROR_MESSAGE, DELETE_IMAGE_SUCCESS_MESSAGE, } from '../constants'; +import { decodeAndParse } from '../utils'; export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data); @@ -43,9 +44,9 @@ export const requestImagesList = ({ commit, dispatch, state }, pagination = {}) }); }; -export const requestTagsList = ({ commit, dispatch }, { pagination = {}, id }) => { +export const requestTagsList = ({ commit, dispatch }, { pagination = {}, params }) => { commit(types.SET_MAIN_LOADING, true); - const { tags_path } = JSON.parse(window.atob(id)); + const { tags_path } = decodeAndParse(params); const { page = DEFAULT_PAGE, perPage = DEFAULT_PAGE_SIZE } = pagination; return axios @@ -61,13 +62,13 @@ export const requestTagsList = ({ commit, dispatch }, { pagination = {}, id }) = }); }; -export const requestDeleteTag = ({ commit, dispatch, state }, { tag, imageId }) => { +export const requestDeleteTag = ({ commit, dispatch, state }, { tag, params }) => { commit(types.SET_MAIN_LOADING, true); return axios .delete(tag.destroy_path) .then(() => { createFlash(DELETE_TAG_SUCCESS_MESSAGE, 'success'); - dispatch('requestTagsList', { pagination: state.tagsPagination, id: imageId }); + dispatch('requestTagsList', { pagination: state.tagsPagination, params }); }) .catch(() => { createFlash(DELETE_TAG_ERROR_MESSAGE); @@ -77,15 +78,16 @@ export const requestDeleteTag = ({ commit, dispatch, state }, { tag, imageId }) }); }; -export const requestDeleteTags = ({ commit, dispatch, state }, { ids, imageId }) => { +export const requestDeleteTags = ({ commit, dispatch, state }, { ids, params }) => { commit(types.SET_MAIN_LOADING, true); - const url = `/${state.config.projectPath}/registry/repository/${imageId}/tags/bulk_destroy`; + const { id } = decodeAndParse(params); + const url = `/${state.config.projectPath}/registry/repository/${id}/tags/bulk_destroy`; return axios .delete(url, { params: { ids } }) .then(() => { createFlash(DELETE_TAGS_SUCCESS_MESSAGE, 'success'); - dispatch('requestTagsList', { pagination: state.tagsPagination, id: imageId }); + dispatch('requestTagsList', { pagination: state.tagsPagination, params }); }) .catch(() => { createFlash(DELETE_TAGS_ERROR_MESSAGE); diff --git a/app/assets/javascripts/registry/explorer/utils.js b/app/assets/javascripts/registry/explorer/utils.js new file mode 100644 index 00000000000..b1df87c6993 --- /dev/null +++ b/app/assets/javascripts/registry/explorer/utils.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export const decodeAndParse = param => JSON.parse(window.atob(param)); diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue index 75d1e5865b0..9df0c045fe4 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue @@ -61,7 +61,7 @@ export default { eventHub.$emit('EnablePolling'); }, updateTimer() { - this.timer = this.timer - 1; + this.timer -= 1; if (this.timer === 0) { this.refresh(); diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 21b23feb57f..89b673397a2 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -192,7 +192,7 @@ .stage-events { width: 60%; overflow: scroll; - height: 467px; + min-height: 467px; } .stage-event-list { diff --git a/app/graphql/mutations/issues/update.rb b/app/graphql/mutations/issues/update.rb new file mode 100644 index 00000000000..119bc51e4a4 --- /dev/null +++ b/app/graphql/mutations/issues/update.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Mutations + module Issues + class Update < Base + graphql_name 'UpdateIssue' + + # Add arguments here instead of creating separate mutations + + def resolve(project_path:, iid:, **args) + issue = authorized_find!(project_path: project_path, iid: iid) + project = issue.project + + ::Issues::UpdateService.new(project, current_user, args).execute(issue) + + { + issue: issue, + errors: issue.errors.full_messages + } + end + end + end +end + +Mutations::Issues::Update.prepend_if_ee('::EE::Mutations::Issues::Update') diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index ee0f4dbb05f..90e9e1ec0b9 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -11,6 +11,7 @@ module Types mount_mutation Mutations::AwardEmojis::Toggle mount_mutation Mutations::Issues::SetConfidential mount_mutation Mutations::Issues::SetDueDate + mount_mutation Mutations::Issues::Update mount_mutation Mutations::MergeRequests::SetLabels mount_mutation Mutations::MergeRequests::SetLocked mount_mutation Mutations::MergeRequests::SetMilestone diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index c502b392384..744aef3cad4 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -2,6 +2,7 @@ - current_text ||= nil - supports_autocomplete = local_assigns.fetch(:supports_autocomplete, true) - supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false) +- qa_selector = local_assigns.fetch(:qa_selector, '') .zen-backdrop - classes << ' js-gfm-input js-autosize markdown-area' - if defined?(f) && f @@ -10,7 +11,8 @@ placeholder: placeholder, dir: 'auto', data: { supports_quick_actions: supports_quick_actions, - supports_autocomplete: supports_autocomplete } + supports_autocomplete: supports_autocomplete, + qa_selector: qa_selector } - else = text_area_tag attr, current_text, class: classes, placeholder: placeholder %a.zen-control.zen-control-leave.js-zen-leave{ href: "#" } diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index f867fb2b6f7..3c2c751c579 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -16,10 +16,10 @@ = f.label :description, s_("Snippets|Description (optional)"), class: 'label-bold' .js-collapsible-input .js-collapsed{ class: ('d-none' if is_expanded) } - = text_field_tag nil, nil, class: 'form-control', placeholder: description_placeholder + = text_field_tag nil, nil, class: 'form-control', placeholder: description_placeholder, data: { qa_selector: 'description_placeholder' } .js-expanded{ class: ('d-none' if !is_expanded) } = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do - = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: description_placeholder + = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: description_placeholder, qa_selector: 'description_field' = render 'shared/notes/hints' .form-group.file-editor |