diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-20 14:10:13 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-20 14:10:13 +0300 |
commit | 0ea3fcec397b69815975647f5e2aa5fe944a8486 (patch) | |
tree | 7979381b89d26011bcf9bdc989a40fcc2f1ed4ff /app/assets/javascripts/issuable | |
parent | 72123183a20411a36d607d70b12d57c484394c8e (diff) |
Add latest changes from gitlab-org/gitlab@15-1-stable-eev15.1.0-rc42
Diffstat (limited to 'app/assets/javascripts/issuable')
12 files changed, 360 insertions, 20 deletions
diff --git a/app/assets/javascripts/issuable/components/csv_export_modal.vue b/app/assets/javascripts/issuable/components/csv_export_modal.vue index b0af3612e05..736da92fa9f 100644 --- a/app/assets/javascripts/issuable/components/csv_export_modal.vue +++ b/app/assets/javascripts/issuable/components/csv_export_modal.vue @@ -1,16 +1,18 @@ <script> -import { GlButton, GlModal, GlSprintf, GlIcon } from '@gitlab/ui'; +import { GlModal, GlSprintf, GlIcon } from '@gitlab/ui'; import { __, n__ } from '~/locale'; import { ISSUABLE_TYPE } from '../constants'; export default { + actionCancel: { + text: __('Cancel'), + }, i18n: { exportText: __( 'The CSV export will be created in the background. Once finished, it will be sent to %{email} in an attachment.', ), }, components: { - GlButton, GlModal, GlSprintf, GlIcon, @@ -38,6 +40,19 @@ export default { }, }, computed: { + actionPrimary() { + return { + text: this.exportText, + attributes: { + href: this.exportCsvPath, + variant: 'confirm', + 'data-method': 'post', + 'data-qa-selector': `export_${this.issuableType}_button`, + 'data-track-action': 'click_button', + 'data-track-label': `export_${this.issuableType}_csv`, + }, + }; + }, isIssue() { return this.issuableType === ISSUABLE_TYPE.issues; }, @@ -56,6 +71,8 @@ export default { <template> <gl-modal :modal-id="modalId" + :action-primary="actionPrimary" + :action-cancel="$options.actionCancel" body-class="gl-p-0!" :title="exportText" data-qa-selector="export_issuable_modal" @@ -73,18 +90,5 @@ export default { </template> </gl-sprintf> </div> - <template #modal-footer> - <gl-button - category="primary" - variant="confirm" - :href="exportCsvPath" - data-method="post" - :data-qa-selector="`export_${issuableType}_button`" - data-track-action="click_button" - :data-track-label="`export_${issuableType}_csv`" - > - {{ exportText }} - </gl-button> - </template> </gl-modal> </template> diff --git a/app/assets/javascripts/issuable/components/csv_import_modal.vue b/app/assets/javascripts/issuable/components/csv_import_modal.vue index 7e2cbf03801..72293343c48 100644 --- a/app/assets/javascripts/issuable/components/csv_import_modal.vue +++ b/app/assets/javascripts/issuable/components/csv_import_modal.vue @@ -18,6 +18,9 @@ export default { actionPrimary: { text: __('Import issues'), }, + actionCancel: { + text: __('Cancel'), + }, components: { GlModal, GlFormGroup, @@ -55,6 +58,7 @@ export default { :modal-id="modalId" :title="$options.i18n.importIssuesText" :action-primary="$options.actionPrimary" + :action-cancel="$options.actionCancel" @primary="submitForm" > <form ref="form" :action="importCsvIssuesPath" enctype="multipart/form-data" method="post"> diff --git a/app/assets/javascripts/issuable/components/issuable_header_warnings.vue b/app/assets/javascripts/issuable/components/issuable_header_warnings.vue index 06d1a2ee233..543dca0afe1 100644 --- a/app/assets/javascripts/issuable/components/issuable_header_warnings.vue +++ b/app/assets/javascripts/issuable/components/issuable_header_warnings.vue @@ -27,7 +27,7 @@ export default { return this.getNoteableData.confidential; }, isMergeRequest() { - return this.getNoteableData.targetType === 'merge_request' && this.glFeatures.updatedMrHeader; + return this.getNoteableData.targetType === 'merge_request'; }, warningIconsMeta() { return [ diff --git a/app/assets/javascripts/issuable/components/related_issuable_item.vue b/app/assets/javascripts/issuable/components/related_issuable_item.vue index dfe18567608..e6379b35f7a 100644 --- a/app/assets/javascripts/issuable/components/related_issuable_item.vue +++ b/app/assets/javascripts/issuable/components/related_issuable_item.vue @@ -71,8 +71,9 @@ export default { :class="{ 'issuable-info-container': !canReorder, 'card-body': canReorder, + 'gl-pr-2': canRemove, }" - class="item-body d-flex align-items-center py-2 px-3" + class="item-body d-flex align-items-center gl-py-3 gl-px-5" > <div class="item-contents gl-display-flex gl-align-items-center gl-flex-wrap gl-flex-grow-1 flex-xl-nowrap gl-min-h-7" @@ -170,7 +171,7 @@ export default { <issue-assignees v-if="assignees.length !== 0" :assignees="assignees" - class="item-assignees d-flex align-items-center align-self-end flex-shrink-0 d-md-none ml-2" + class="item-assignees d-flex align-items-center align-self-end flex-shrink-0 d-md-none gl-ml-3" /> </div> </div> diff --git a/app/assets/javascripts/issuable/components/status_box.vue b/app/assets/javascripts/issuable/components/status_box.vue index 498dc859186..d72ee5c6757 100644 --- a/app/assets/javascripts/issuable/components/status_box.vue +++ b/app/assets/javascripts/issuable/components/status_box.vue @@ -63,6 +63,8 @@ export default { }, }, data() { + if (!this.iid) return { state: this.initialState }; + if (this.initialState) { badgeState.state = this.initialState; } @@ -74,8 +76,7 @@ export default { return [ CLASSES[this.state], { - 'gl-vertical-align-bottom': - this.issuableType === IssuableType.MergeRequest && this.glFeatures.updatedMrHeader, + 'gl-vertical-align-bottom': this.issuableType === IssuableType.MergeRequest, }, ]; }, diff --git a/app/assets/javascripts/issuable/issuable_form.js b/app/assets/javascripts/issuable/issuable_form.js index 8e76a33c7dd..38453072af8 100644 --- a/app/assets/javascripts/issuable/issuable_form.js +++ b/app/assets/javascripts/issuable/issuable_form.js @@ -46,6 +46,9 @@ function getFallbackKey() { export default class IssuableForm { constructor(form) { + if (form.length === 0) { + return; + } this.form = form; this.toggleWip = this.toggleWip.bind(this); this.renderWipExplanation = this.renderWipExplanation.bind(this); diff --git a/app/assets/javascripts/issuable/popover/components/issue_popover.vue b/app/assets/javascripts/issuable/popover/components/issue_popover.vue new file mode 100644 index 00000000000..0cafaa1e500 --- /dev/null +++ b/app/assets/javascripts/issuable/popover/components/issue_popover.vue @@ -0,0 +1,83 @@ +<script> +import { GlPopover, GlSkeletonLoader } from '@gitlab/ui'; +import StatusBox from '~/issuable/components/status_box.vue'; +import timeagoMixin from '~/vue_shared/mixins/timeago'; +import query from '../queries/issue.query.graphql'; + +export default { + components: { + GlPopover, + GlSkeletonLoader, + StatusBox, + }, + mixins: [timeagoMixin], + props: { + target: { + type: HTMLAnchorElement, + required: true, + }, + projectPath: { + type: String, + required: true, + }, + iid: { + type: String, + required: true, + }, + cachedTitle: { + type: String, + required: true, + }, + }, + data() { + return { + issue: {}, + }; + }, + computed: { + formattedTime() { + return this.timeFormatted(this.issue.createdAt); + }, + title() { + return this.issue?.title || this.cachedTitle; + }, + showDetails() { + return Object.keys(this.issue).length > 0; + }, + }, + apollo: { + issue: { + query, + update: (data) => data.project.issue, + variables() { + const { projectPath, iid } = this; + + return { + projectPath, + iid, + }; + }, + }, + }, +}; +</script> + +<template> + <gl-popover :target="target" boundary="viewport" placement="top" show> + <gl-skeleton-loader v-if="$apollo.queries.issue.loading" :height="15"> + <rect width="250" height="15" rx="4" /> + </gl-skeleton-loader> + <div v-else-if="showDetails" class="gl-display-flex gl-align-items-center"> + <status-box issuable-type="issue" :initial-state="issue.state" /> + <span class="gl-text-secondary"> + {{ __('Opened') }} <time :datetime="issue.createdAt">{{ formattedTime }}</time> + </span> + </div> + <h5 v-if="!$apollo.queries.issue.loading" class="gl-my-3">{{ title }}</h5> + <!-- eslint-disable @gitlab/vue-require-i18n-strings --> + <div class="gl-text-secondary"> + {{ `${projectPath}#${iid}` }} + </div> + <!-- eslint-enable @gitlab/vue-require-i18n-strings --> + </gl-popover> +</template> diff --git a/app/assets/javascripts/issuable/popover/components/mr_popover.vue b/app/assets/javascripts/issuable/popover/components/mr_popover.vue new file mode 100644 index 00000000000..92994809362 --- /dev/null +++ b/app/assets/javascripts/issuable/popover/components/mr_popover.vue @@ -0,0 +1,116 @@ +<script> +/* eslint-disable @gitlab/vue-require-i18n-strings */ +import { GlBadge, GlPopover, GlSkeletonLoader } from '@gitlab/ui'; +import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import timeagoMixin from '~/vue_shared/mixins/timeago'; +import { mrStates, humanMRStates } from '../constants'; +import query from '../queries/merge_request.query.graphql'; + +export default { + // name: 'MRPopover' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25 + name: 'MRPopover', // eslint-disable-line @gitlab/require-i18n-strings + components: { + GlBadge, + GlPopover, + GlSkeletonLoader, + CiIcon, + }, + mixins: [timeagoMixin], + props: { + target: { + type: HTMLAnchorElement, + required: true, + }, + projectPath: { + type: String, + required: true, + }, + iid: { + type: String, + required: true, + }, + cachedTitle: { + type: String, + required: true, + }, + }, + data() { + return { + mergeRequest: {}, + }; + }, + computed: { + detailedStatus() { + return this.mergeRequest.headPipeline && this.mergeRequest.headPipeline.detailedStatus; + }, + formattedTime() { + return this.timeFormatted(this.mergeRequest.createdAt); + }, + badgeVariant() { + switch (this.mergeRequest.state) { + case mrStates.merged: + return 'info'; + case mrStates.closed: + return 'danger'; + default: + return 'success'; + } + }, + stateHumanName() { + switch (this.mergeRequest.state) { + case mrStates.merged: + return humanMRStates.merged; + case mrStates.closed: + return humanMRStates.closed; + default: + return humanMRStates.open; + } + }, + title() { + return this.mergeRequest?.title || this.cachedTitle; + }, + showDetails() { + return Object.keys(this.mergeRequest).length > 0; + }, + }, + apollo: { + mergeRequest: { + query, + update: (data) => data.project.mergeRequest, + variables() { + const { projectPath, iid } = this; + + return { + projectPath, + iid, + }; + }, + }, + }, +}; +</script> + +<template> + <gl-popover :target="target" boundary="viewport" placement="top" show> + <div class="mr-popover"> + <gl-skeleton-loader v-if="$apollo.queries.mergeRequest.loading" :height="15"> + <rect width="250" height="15" rx="4" /> + </gl-skeleton-loader> + <div v-else-if="showDetails" class="d-flex align-items-center justify-content-between"> + <div class="d-inline-flex align-items-center"> + <gl-badge class="gl-mr-3" :variant="badgeVariant"> + {{ stateHumanName }} + </gl-badge> + <span class="gl-text-secondary">Opened <time v-text="formattedTime"></time></span> + </div> + <ci-icon v-if="detailedStatus" :status="detailedStatus" /> + </div> + <h5 v-if="!$apollo.queries.mergeRequest.loading" class="my-2">{{ title }}</h5> + <!-- eslint-disable @gitlab/vue-require-i18n-strings --> + <div class="gl-text-secondary"> + {{ `${projectPath}!${iid}` }} + </div> + <!-- eslint-enable @gitlab/vue-require-i18n-strings --> + </div> + </gl-popover> +</template> diff --git a/app/assets/javascripts/issuable/popover/constants.js b/app/assets/javascripts/issuable/popover/constants.js new file mode 100644 index 00000000000..352bc635293 --- /dev/null +++ b/app/assets/javascripts/issuable/popover/constants.js @@ -0,0 +1,13 @@ +import { __ } from '~/locale'; + +export const mrStates = { + merged: 'merged', + closed: 'closed', + open: 'open', +}; + +export const humanMRStates = { + merged: __('Merged'), + closed: __('Closed'), + open: __('Open'), +}; diff --git a/app/assets/javascripts/issuable/popover/index.js b/app/assets/javascripts/issuable/popover/index.js new file mode 100644 index 00000000000..de3c8160b7a --- /dev/null +++ b/app/assets/javascripts/issuable/popover/index.js @@ -0,0 +1,85 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import IssuePopover from './components/issue_popover.vue'; +import MRPopover from './components/mr_popover.vue'; + +const componentsByReferenceType = { + issue: IssuePopover, + merge_request: MRPopover, +}; + +let renderFn; + +const handleIssuablePopoverMouseOut = ({ target }) => { + target.removeEventListener('mouseleave', handleIssuablePopoverMouseOut); + + if (renderFn) { + clearTimeout(renderFn); + } +}; + +const popoverMountedAttr = 'data-popover-mounted'; + +/** + * Adds a MergeRequestPopover component to the body, hands over as much data as the target element has in data attributes. + * loads based on data-project-path and data-iid more data about an MR from the API and sets it on the popover + */ +const handleIssuablePopoverMount = ({ + apolloProvider, + projectPath, + title, + iid, + referenceType, + target, +}) => { + // Add listener to actually remove it again + target.addEventListener('mouseleave', handleIssuablePopoverMouseOut); + + renderFn = setTimeout(() => { + const PopoverComponent = Vue.extend(componentsByReferenceType[referenceType]); + new PopoverComponent({ + propsData: { + target, + projectPath, + iid, + cachedTitle: title, + }, + apolloProvider, + }).$mount(); + + target.setAttribute(popoverMountedAttr, true); + }, 200); // 200ms delay so not every mouseover triggers Popover + API Call +}; + +export default (elements) => { + if (elements.length > 0) { + Vue.use(VueApollo); + + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + const listenerAddedAttr = 'data-popover-listener-added'; + + elements.forEach((el) => { + const { projectPath, iid, referenceType } = el.dataset; + const title = el.dataset.mrTitle || el.title; + + if (!el.getAttribute(listenerAddedAttr) && projectPath && title && iid && referenceType) { + el.addEventListener('mouseenter', ({ target }) => { + if (!el.getAttribute(popoverMountedAttr)) { + handleIssuablePopoverMount({ + apolloProvider, + projectPath, + title, + iid, + referenceType, + target, + }); + } + }); + el.setAttribute(listenerAddedAttr, true); + } + }); + } +}; diff --git a/app/assets/javascripts/issuable/popover/queries/issue.query.graphql b/app/assets/javascripts/issuable/popover/queries/issue.query.graphql new file mode 100644 index 00000000000..47a62e2b6ea --- /dev/null +++ b/app/assets/javascripts/issuable/popover/queries/issue.query.graphql @@ -0,0 +1,11 @@ +query issue($projectPath: ID!, $iid: String!) { + project(fullPath: $projectPath) { + id + issue(iid: $iid) { + id + title + createdAt + state + } + } +} diff --git a/app/assets/javascripts/issuable/popover/queries/merge_request.query.graphql b/app/assets/javascripts/issuable/popover/queries/merge_request.query.graphql new file mode 100644 index 00000000000..7cd344c1d5e --- /dev/null +++ b/app/assets/javascripts/issuable/popover/queries/merge_request.query.graphql @@ -0,0 +1,19 @@ +query mergeRequest($projectPath: ID!, $iid: String!) { + project(fullPath: $projectPath) { + id + mergeRequest(iid: $iid) { + id + title + createdAt + state + headPipeline { + id + detailedStatus { + id + icon + group + } + } + } + } +} |