diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-15 18:09:53 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-15 18:09:53 +0300 |
commit | 51d5328e82229d69456d3a43dd2cf10518bf64c7 (patch) | |
tree | cca8c7aaea10c92398e03ae182b76c5cc514185c /app | |
parent | 14771dc276c9cfdeed1a3915ee29301cd848b475 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
40 files changed, 707 insertions, 320 deletions
diff --git a/app/assets/javascripts/content_editor/extensions/image.js b/app/assets/javascripts/content_editor/extensions/image.js index 25f976f524f..65849ec4d0d 100644 --- a/app/assets/javascripts/content_editor/extensions/image.js +++ b/app/assets/javascripts/content_editor/extensions/image.js @@ -34,6 +34,7 @@ export default Image.extend({ canonicalSrc: { default: null, parseHTML: (element) => element.dataset.canonicalSrc, + renderHTML: () => '', }, alt: { default: null, @@ -51,6 +52,10 @@ export default Image.extend({ return img.getAttribute('title'); }, }, + isReference: { + default: false, + renderHTML: () => '', + }, }; }, parseHTML() { @@ -71,7 +76,6 @@ export default Image.extend({ src: HTMLAttributes.src, alt: HTMLAttributes.alt, title: HTMLAttributes.title, - 'data-canonical-src': HTMLAttributes.canonicalSrc, }, ]; }, diff --git a/app/assets/javascripts/content_editor/extensions/link.js b/app/assets/javascripts/content_editor/extensions/link.js index f9b12f631fe..e985e561fda 100644 --- a/app/assets/javascripts/content_editor/extensions/link.js +++ b/app/assets/javascripts/content_editor/extensions/link.js @@ -56,6 +56,11 @@ export default Link.extend({ canonicalSrc: { default: null, parseHTML: (element) => element.dataset.canonicalSrc, + renderHTML: () => '', + }, + isReference: { + default: false, + renderHTML: () => '', }, }; }, diff --git a/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js b/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js index 66923a4eb4d..d9c60ade00a 100644 --- a/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js +++ b/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js @@ -126,7 +126,8 @@ const factorySpecs = { selector: 'img', getAttrs: (hastNode) => ({ src: hastNode.properties.src, - canonicalSrc: hastNode.properties.src, + canonicalSrc: hastNode.properties.identifier ?? hastNode.properties.src, + isReference: hastNode.properties.isReference === 'true', title: hastNode.properties.title, alt: hastNode.properties.alt, }), @@ -156,8 +157,9 @@ const factorySpecs = { type: 'mark', selector: 'a', getAttrs: (hastNode) => ({ - canonicalSrc: hastNode.properties.href, + canonicalSrc: hastNode.properties.identifier ?? hastNode.properties.href, href: hastNode.properties.href, + isReference: hastNode.properties.isReference === 'true', title: hastNode.properties.title, }), }, @@ -185,7 +187,7 @@ const factorySpecs = { }, }; -const SANITIZE_ALLOWLIST = ['level', 'identifier', 'numeric', 'language', 'url']; +const SANITIZE_ALLOWLIST = ['level', 'identifier', 'numeric', 'language', 'url', 'isReference']; const sanitizeAttribute = (attributeName, attributeValue, hastNode) => { if (!attributeValue || SANITIZE_ALLOWLIST.includes(attributeName)) { @@ -230,7 +232,14 @@ export default () => { attributeTransformer, markdown, }), - skipRendering: ['footnoteReference', 'footnoteDefinition', 'code', 'definition'], + skipRendering: [ + 'footnoteReference', + 'footnoteDefinition', + 'code', + 'definition', + 'linkReference', + 'imageReference', + ], }); return { document }; diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js index f9ab1c1d959..41114571df7 100644 --- a/app/assets/javascripts/content_editor/services/serialization_helpers.js +++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js @@ -307,12 +307,15 @@ export function renderHardBreak(state, node, parent, index) { } export function renderImage(state, node) { - const { alt, canonicalSrc, src, title } = node.attrs; + const { alt, canonicalSrc, src, title, isReference } = node.attrs; if (isString(src) || isString(canonicalSrc)) { const quotedTitle = title ? ` ${state.quote(title)}` : ''; + const sourceExpression = isReference + ? `[${canonicalSrc}]` + : `(${state.esc(canonicalSrc || src)}${quotedTitle})`; - state.write(`![${state.esc(alt || '')}](${state.esc(canonicalSrc || src)}${quotedTitle})`); + state.write(`![${state.esc(alt || '')}]${sourceExpression}`); } } @@ -587,7 +590,11 @@ export const link = { return isBracketAutoLink(mark.attrs.sourceMarkdown) ? '>' : ''; } - const { canonicalSrc, href, title, sourceMarkdown } = mark.attrs; + const { canonicalSrc, href, title, sourceMarkdown, isReference } = mark.attrs; + + if (isReference) { + return `][${state.esc(canonicalSrc || href)}]`; + } if (linkType(sourceMarkdown) === LINK_HTML) { return closeTag('a'); diff --git a/app/assets/javascripts/issuable/popover/components/issue_popover.vue b/app/assets/javascripts/issuable/popover/components/issue_popover.vue index 4ae6de4783c..945a3782642 100644 --- a/app/assets/javascripts/issuable/popover/components/issue_popover.vue +++ b/app/assets/javascripts/issuable/popover/components/issue_popover.vue @@ -6,6 +6,7 @@ import IssueMilestone from '~/issuable/components/issue_milestone.vue'; import StatusBox from '~/issuable/components/status_box.vue'; import { IssuableStatus } from '~/issues/constants'; import timeagoMixin from '~/vue_shared/mixins/timeago'; +import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue'; export default { components: { @@ -16,6 +17,7 @@ export default { IssueMilestone, IssueWeight: () => import('ee_component/boards/components/issue_card_weight.vue'), StatusBox, + WorkItemTypeIcon, }, directives: { GlTooltip: GlTooltipDirective, @@ -96,8 +98,9 @@ export default { </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> + <work-item-type-icon v-if="!$apollo.queries.issue.loading" :work-item-type="issue.type" /> + <span class="gl-text-secondary">{{ `${projectPath}#${iid}` }}</span> </div> <!-- eslint-enable @gitlab/vue-require-i18n-strings --> diff --git a/app/assets/javascripts/issuable/popover/queries/issue.query.graphql b/app/assets/javascripts/issuable/popover/queries/issue.query.graphql index 5a07d11c579..1bbe6cb6eac 100644 --- a/app/assets/javascripts/issuable/popover/queries/issue.query.graphql +++ b/app/assets/javascripts/issuable/popover/queries/issue.query.graphql @@ -14,6 +14,7 @@ query issue($projectPath: ID!, $iid: String!) { startDate dueDate } + type } } } diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue index d6acd700148..11911adb401 100644 --- a/app/assets/javascripts/issues/list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue @@ -774,6 +774,7 @@ export default { :show-page-size-change-controls="showPageSizeControls" :has-next-page="pageInfo.hasNextPage" :has-previous-page="pageInfo.hasPreviousPage" + show-work-item-type-icon @click-tab="handleClickTab" @dismiss-alert="handleDismissAlert" @filter="handleFilter" diff --git a/app/assets/javascripts/issues/list/queries/issue.fragment.graphql b/app/assets/javascripts/issues/list/queries/issue.fragment.graphql index 35762120f71..99c3b6b11bf 100644 --- a/app/assets/javascripts/issues/list/queries/issue.fragment.graphql +++ b/app/assets/javascripts/issues/list/queries/issue.fragment.graphql @@ -18,6 +18,7 @@ fragment IssueFragment on Issue { userDiscussionsCount @include(if: $isSignedIn) webPath webUrl + type assignees @skip(if: $hideUsers) { nodes { __typename diff --git a/app/assets/javascripts/lib/gfm/index.js b/app/assets/javascripts/lib/gfm/index.js index 05534401a2a..1ab9fc22705 100644 --- a/app/assets/javascripts/lib/gfm/index.js +++ b/app/assets/javascripts/lib/gfm/index.js @@ -1,4 +1,5 @@ import { pick } from 'lodash'; +import normalize from 'mdurl/encode'; import { unified } from 'unified'; import remarkParse from 'remark-parse'; import remarkGfm from 'remark-gfm'; @@ -29,6 +30,37 @@ const skipRenderingHandlers = { [{ type: 'text', value: `[${node.identifier}]: ${node.url}${title}` }], ); }, + linkReference: (h, node) => { + const definition = h.definition(node.identifier); + + return h( + node.position, + 'a', + { + href: normalize(definition.url ?? ''), + identifier: node.identifier, + isReference: 'true', + title: definition.title, + }, + all(h, node), + ); + }, + imageReference: (h, node) => { + const definition = h.definition(node.identifier); + + return h( + node.position, + 'img', + { + src: normalize(definition.url ?? ''), + alt: node.alt, + identifier: node.identifier, + isReference: 'true', + title: definition.title, + }, + all(h, node), + ); + }, }; const createParser = ({ skipRendering = [] }) => { diff --git a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue index c66a1eecfc5..777a332333d 100644 --- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue +++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue @@ -17,6 +17,7 @@ import allRunnersCountQuery from 'ee_else_ce/runner/graphql/list/all_runners_cou import RegistrationDropdown from '../components/registration/registration_dropdown.vue'; import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue'; import RunnerBulkDelete from '../components/runner_bulk_delete.vue'; +import RunnerBulkDeleteCheckbox from '../components/runner_bulk_delete_checkbox.vue'; import RunnerList from '../components/runner_list.vue'; import RunnerListEmptyState from '../components/runner_list_empty_state.vue'; import RunnerName from '../components/runner_name.vue'; @@ -38,6 +39,7 @@ export default { RegistrationDropdown, RunnerFilteredSearchBar, RunnerBulkDelete, + RunnerBulkDeleteCheckbox, RunnerList, RunnerListEmptyState, RunnerName, @@ -203,13 +205,20 @@ export default { :filtered-svg-path="emptyStateFilteredSvgPath" /> <template v-else> - <runner-bulk-delete v-if="isBulkDeleteEnabled" @deleted="onDeleted" /> + <runner-bulk-delete + v-if="isBulkDeleteEnabled" + :runners="runners.items" + @deleted="onDeleted" + /> <runner-list :runners="runners.items" :loading="runnersLoading" :checkable="isBulkDeleteEnabled" @checked="onChecked" > + <template v-if="isBulkDeleteEnabled" #head-checkbox> + <runner-bulk-delete-checkbox :runners="runners.items" /> + </template> <template #runner-name="{ runner }"> <gl-link :href="runner.adminUrl"> <runner-name :runner="runner" /> diff --git a/app/assets/javascripts/runner/components/runner_bulk_delete.vue b/app/assets/javascripts/runner/components/runner_bulk_delete.vue index fd55487c458..703da01d9c8 100644 --- a/app/assets/javascripts/runner/components/runner_bulk_delete.vue +++ b/app/assets/javascripts/runner/components/runner_bulk_delete.vue @@ -16,6 +16,13 @@ export default { GlModal: GlModalDirective, }, inject: ['localMutations'], + props: { + runners: { + type: Array, + default: () => [], + required: false, + }, + }, data() { return { isDeleting: false, @@ -28,8 +35,13 @@ export default { }, }, computed: { + currentCheckedRunnerIds() { + return this.runners + .map(({ id }) => id) + .filter((id) => this.checkedRunnerIds.indexOf(id) >= 0); + }, checkedCount() { - return this.checkedRunnerIds.length || 0; + return this.currentCheckedRunnerIds.length || 0; }, bannerMessage() { return sprintf( @@ -98,7 +110,7 @@ export default { mutation: BulkRunnerDelete, variables: { input: { - ids: this.checkedRunnerIds, + ids: this.currentCheckedRunnerIds, }, }, update: (cache, { data }) => { diff --git a/app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue b/app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue new file mode 100644 index 00000000000..dde5a5a4a05 --- /dev/null +++ b/app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue @@ -0,0 +1,59 @@ +<script> +import { GlFormCheckbox } from '@gitlab/ui'; +import checkedRunnerIdsQuery from '../graphql/list/checked_runner_ids.query.graphql'; + +export default { + components: { + GlFormCheckbox, + }, + inject: ['localMutations'], + props: { + runners: { + type: Array, + default: () => [], + required: false, + }, + }, + data() { + return { + checkedRunnerIds: [], + }; + }, + apollo: { + checkedRunnerIds: { + query: checkedRunnerIdsQuery, + }, + }, + computed: { + disabled() { + return !this.runners.length; + }, + checked() { + return Boolean(this.runners.length) && this.runners.every(this.isChecked); + }, + indeterminate() { + return !this.checked && this.runners.some(this.isChecked); + }, + }, + methods: { + isChecked({ id }) { + return this.checkedRunnerIds.indexOf(id) >= 0; + }, + onChange($event) { + this.localMutations.setRunnersChecked({ + runners: this.runners, + isChecked: $event, + }); + }, + }, +}; +</script> + +<template> + <gl-form-checkbox + :indeterminate="indeterminate" + :checked="checked" + :disabled="disabled" + @change="onChange" + /> +</template> diff --git a/app/assets/javascripts/runner/components/runner_list.vue b/app/assets/javascripts/runner/components/runner_list.vue index f1f99c728c5..2e406f71792 100644 --- a/app/assets/javascripts/runner/components/runner_list.vue +++ b/app/assets/javascripts/runner/components/runner_list.vue @@ -1,5 +1,5 @@ <script> -import { GlTableLite, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui'; +import { GlFormCheckbox, GlTableLite, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { __, s__ } from '~/locale'; @@ -23,6 +23,7 @@ const defaultFields = [ export default { components: { + GlFormCheckbox, GlTableLite, GlSkeletonLoader, TooltipOnTruncate, @@ -123,19 +124,11 @@ export default { fixed > <template #head(checkbox)> - <!-- - Checkbox to select all to be added here - See https://gitlab.com/gitlab-org/gitlab/-/issues/339525/ - --> - <span></span> + <slot name="head-checkbox"></slot> </template> <template #cell(checkbox)="{ item }"> - <input - type="checkbox" - :checked="isChecked(item)" - @change="onCheckboxChange(item, $event.target.checked)" - /> + <gl-form-checkbox :checked="isChecked(item)" @change="onCheckboxChange(item, $event)" /> </template> <template #head(status)="{ label }"> diff --git a/app/assets/javascripts/runner/graphql/list/local_state.js b/app/assets/javascripts/runner/graphql/list/local_state.js index ce6d125311c..154af261bba 100644 --- a/app/assets/javascripts/runner/graphql/list/local_state.js +++ b/app/assets/javascripts/runner/graphql/list/local_state.js @@ -57,6 +57,13 @@ export const createLocalState = () => { [runner.id]: isChecked, }); }, + setRunnersChecked({ runners, isChecked }) { + const newVal = runners.reduce( + (acc, { id }) => ({ ...acc, [id]: isChecked }), + checkedRunnerIdsVar(), + ); + checkedRunnerIdsVar(newVal); + }, clearChecked() { checkedRunnerIdsVar({}); }, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue b/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue index b76d5d90ead..38f40e8a3c8 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue @@ -14,7 +14,8 @@ export default { props: { widget: { type: String, - required: true, + required: false, + default: '', }, tertiaryButtons: { type: Array, @@ -30,6 +31,8 @@ export default { }, computed: { dropdownLabel() { + if (!this.widget) return undefined; + return sprintf(__('%{widget} options'), { widget: this.widget }); }, }, @@ -85,6 +88,7 @@ export default { :href="btn.href" :target="btn.target" :data-clipboard-text="btn.dataClipboardText" + :data-method="btn.dataMethod" @click="onClickAction(btn)" > {{ btn.text }} @@ -99,11 +103,15 @@ export default { :title="setTooltip(btn)" :href="btn.href" :target="btn.target" - :class="{ 'gl-mr-3': index !== tertiaryButtons.length - 1 }" + :class="[{ 'gl-mr-3': index !== tertiaryButtons.length - 1 }, btn.class]" :data-clipboard-text="btn.dataClipboardText" + :data-qa-selector="btn.dataQaSelector" + :data-method="btn.dataMethod" :icon="btn.icon" :data-testid="btn.testId || 'extension-actions-button'" :variant="btn.variant || 'confirm'" + :loading="btn.loading" + :disabled="btn.loading" category="tertiary" size="small" class="gl-display-none gl-md-display-block gl-float-left" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue b/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue index 266f1fb594b..254b280bf14 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue @@ -2,9 +2,9 @@ import { GlSprintf } from '@gitlab/ui'; import { escape } from 'lodash'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { n__, s__ } from '~/locale'; +import { n__, s__, sprintf } from '~/locale'; -const mergeCommitCount = s__('mrWidgetCommitsAdded|1 merge commit'); +const mergeCommitCount = s__('mrWidgetCommitsAdded|%{strongStart}1%{strongEnd} merge commit'); export default { components: { @@ -49,7 +49,16 @@ export default { return escape(this.targetBranch); }, commitsCountMessage() { - return n__('%d commit', '%d commits', this.isSquashEnabled ? 1 : this.commitsCount); + const count = this.isSquashEnabled ? 1 : this.commitsCount; + + return sprintf( + n__( + '%{strongStart}%{count}%{strongEnd} commit', + '%{strongStart}%{count}%{strongEnd} commits', + count, + ), + { count }, + ); }, message() { if (this.state === 'closed') { @@ -68,10 +77,17 @@ export default { }, squashCommitMessage() { if (this.isMerged) { - return s__('mergedCommitsAdded|(commits were squashed)'); + return s__('mergedCommitsAdded| (commits were squashed)'); } - return n__('(squashes %d commit)', '(squashes %d commits)', this.commitsCount); + return sprintf( + n__( + ' (squashes %{strongStart}%{count}%{strongEnd} commit)', + ' (squashes %{strongStart}%{count}%{strongEnd} commits)', + this.commitsCount, + ), + { count: this.commitsCount }, + ); }, }, mergeCommitCount, @@ -82,16 +98,30 @@ export default { <span> <gl-sprintf :message="message"> <template #commitCount> - <span class="commits-count-message">{{ commitsCountMessage }}</span> + <gl-sprintf :message="commitsCountMessage"> + <template #strong="{ content }"> + <span class="gl-font-weight-bold">{{ content }}</span> + </template> + </gl-sprintf> </template> <template #mergeCommitCount> - <span>{{ $options.mergeCommitCount }}</span> + <gl-sprintf :message="$options.mergeCommitCount"> + <template #strong="{ content }"> + <span class="gl-font-weight-bold">{{ content }}</span> + </template> + </gl-sprintf> </template> <template #targetBranch> - <span class="label-branch">{{ targetBranchEscaped }}</span> + <span class="label-branch gl-font-weight-bold">{{ targetBranchEscaped }}</span> </template> <template #squashedCommits> - <template v-if="isSquashEnabled"> {{ squashCommitMessage }}</template> + <template v-if="isSquashEnabled"> + <gl-sprintf :message="squashCommitMessage"> + <template #strong="{ content }"> + <span class="gl-font-weight-bold">{{ content }}</span> + </template> + </gl-sprintf> + </template> </template> <template #mergeCommitSha> <span class="label-branch">{{ mergeCommitSha }}</span> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue index 410331004e4..414c5bf9691 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue @@ -12,8 +12,8 @@ import { sprintf, s__, __ } from '~/locale'; import Poll from '~/lib/utils/poll'; import { normalizeHeaders } from '~/lib/utils/common_utils'; import { EXTENSION_ICON_CLASS, EXTENSION_ICONS } from '../../constants'; +import Actions from '../action_buttons.vue'; import StatusIcon from './status_icon.vue'; -import Actions from './actions.vue'; import ChildContent from './child_content.vue'; import { createTelemetryHub } from './telemetry'; import { generateText } from './utils'; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue index 38f83a61b30..1eccc7de660 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue @@ -1,7 +1,7 @@ <script> import { GlBadge, GlLink, GlSafeHtmlDirective, GlModalDirective } from '@gitlab/ui'; +import Actions from '../action_buttons.vue'; import StatusIcon from './status_icon.vue'; -import Actions from './actions.vue'; import { generateText } from './utils'; export default { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue new file mode 100644 index 00000000000..4a5a03fb598 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue @@ -0,0 +1,55 @@ +<script> +import StatusIcon from './mr_widget_status_icon.vue'; +import Actions from './action_buttons.vue'; + +export default { + components: { + StatusIcon, + Actions, + }, + props: { + isLoading: { + type: Boolean, + required: false, + default: false, + }, + status: { + type: String, + required: false, + default: '', + }, + actions: { + type: Array, + required: false, + default: () => [], + }, + }, +}; +</script> + +<template> + <div class="mr-widget-body media"> + <div v-if="isLoading" class="gl-w-full mr-conflict-loader"> + <slot name="loading"></slot> + </div> + <template v-else> + <slot name="icon"> + <status-icon :status="status" /> + </slot> + <div + :class="{ 'gl-display-flex': actions.length, 'gl-md-display-flex': !actions.length }" + class="media-body" + > + <slot></slot> + <div + :class="{ 'gl-flex-direction-column-reverse': !actions.length }" + class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto gl-mt-1" + > + <slot name="actions"> + <actions v-if="actions.length" :tertiary-buttons="actions" /> + </slot> + </div> + </div> + </template> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue index 63bd8f3d8dc..690acc9a6dc 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue @@ -1,5 +1,5 @@ <script> -import { GlSkeletonLoader, GlIcon, GlButton, GlSprintf } from '@gitlab/ui'; +import { GlSkeletonLoader, GlIcon, GlSprintf } from '@gitlab/ui'; import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merge'; import autoMergeEnabledQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql'; import createFlash from '~/flash'; @@ -9,6 +9,7 @@ import { AUTO_MERGE_STRATEGIES } from '../../constants'; import eventHub from '../../event_hub'; import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables'; import MrWidgetAuthor from '../mr_widget_author.vue'; +import StateContainer from '../state_container.vue'; export default { name: 'MRWidgetAutoMergeEnabled', @@ -28,8 +29,8 @@ export default { MrWidgetAuthor, GlSkeletonLoader, GlIcon, - GlButton, GlSprintf, + StateContainer, }, mixins: [autoMergeMixin, glFeatureFlagMixin(), mergeRequestQueryVariablesMixin], props: { @@ -77,6 +78,26 @@ export default { autoMergeStrategy() { return (this.glFeatures.mergeRequestWidgetGraphql ? this.state : this.mr).autoMergeStrategy; }, + actions() { + const actions = []; + + if (this.loading) { + return actions; + } + + if (this.mr.canCancelAutomaticMerge) { + actions.push({ + text: this.cancelButtonText, + loading: this.isCancellingAutoMerge, + dataQaSelector: 'cancel_auto_merge_button', + class: 'js-cancel-auto-merge', + testId: 'cancelAutomaticMergeButton', + onClick: () => this.cancelAutomaticMerge(), + }); + } + + return actions; + }, }, methods: { cancelAutomaticMerge() { @@ -130,38 +151,25 @@ export default { }; </script> <template> - <div class="mr-widget-body media"> - <div v-if="loading" class="gl-w-full mr-conflict-loader"> + <state-container status="scheduled" :is-loading="loading" :actions="actions"> + <template #loading> <gl-skeleton-loader :width="334" :height="30"> <rect x="0" y="3" width="24" height="24" rx="4" /> <rect x="32" y="7" width="150" height="16" rx="4" /> <rect x="190" y="7" width="144" height="16" rx="4" /> </gl-skeleton-loader> - </div> - <template v-else> + </template> + <template v-if="!loading"> + <h4 class="gl-mr-3" data-testid="statusText"> + <gl-sprintf :message="statusText" data-testid="statusText"> + <template #merge_author> + <mr-widget-author :author="mergeUser" /> + </template> + </gl-sprintf> + </h4> + </template> + <template v-if="!loading" #icon> <gl-icon name="status_scheduled" :size="24" class="gl-text-blue-500 gl-mr-3 gl-mt-1" /> - <div class="media-body"> - <h4 class="gl-display-flex"> - <span class="gl-mr-3"> - <gl-sprintf :message="statusText" data-testid="statusText"> - <template #merge_author> - <mr-widget-author :author="mergeUser" /> - </template> - </gl-sprintf> - </span> - <gl-button - v-if="mr.canCancelAutomaticMerge" - :loading="isCancellingAutoMerge" - size="small" - class="js-cancel-auto-merge" - data-qa-selector="cancel_auto_merge_button" - data-testid="cancelAutomaticMergeButton" - @click="cancelAutomaticMerge" - > - {{ cancelButtonText }} - </gl-button> - </h4> - </div> </template> - </div> + </state-container> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue index 1a764d3d091..b0cda85f361 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue @@ -1,17 +1,15 @@ <script> -import { GlLoadingIcon, GlButton } from '@gitlab/ui'; +import { s__ } from '~/locale'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import eventHub from '../../event_hub'; import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables'; import autoMergeFailedQuery from '../../queries/states/auto_merge_failed.query.graphql'; -import statusIcon from '../mr_widget_status_icon.vue'; +import StateContainer from '../state_container.vue'; export default { name: 'MRWidgetAutoMergeFailed', components: { - statusIcon, - GlLoadingIcon, - GlButton, + StateContainer, }, mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin], apollo: { @@ -38,6 +36,17 @@ export default { isRefreshing: false, }; }, + computed: { + actions() { + return [ + { + text: s__('mrWidget|Refresh'), + loading: this.isRefreshing, + onClick: () => this.refreshWidget(), + }, + ]; + }, + }, methods: { refreshWidget() { this.isRefreshing = true; @@ -49,23 +58,10 @@ export default { }; </script> <template> - <div class="mr-widget-body media"> - <status-icon status="warning" /> - <div class="media-body space-children gl-display-flex gl-flex-wrap gl-align-items-center"> - <span class="bold"> - <template v-if="mergeError">{{ mergeError }}</template> - {{ s__('mrWidget|This merge request failed to be merged automatically') }} - </span> - <gl-button - :disabled="isRefreshing" - category="secondary" - variant="default" - size="small" - @click="refreshWidget" - > - <gl-loading-icon v-if="isRefreshing" size="sm" :inline="true" /> - {{ s__('mrWidget|Refresh') }} - </gl-button> - </div> - </div> + <state-container status="warning" :actions="actions"> + <span class="bold gl-ml-0!"> + <template v-if="mergeError">{{ mergeError }}</template> + {{ s__('mrWidget|This merge request failed to be merged automatically') }} + </span> + </state-container> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue index 7652d7049f2..8abd915b93e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue @@ -4,14 +4,14 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables'; import userPermissionsQuery from '../../queries/permissions.query.graphql'; import conflictsStateQuery from '../../queries/states/conflicts.query.graphql'; -import StatusIcon from '../mr_widget_status_icon.vue'; +import StateContainer from '../state_container.vue'; export default { name: 'MRWidgetConflicts', components: { GlSkeletonLoader, - StatusIcon, GlButton, + StateContainer, }, mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin], apollo: { @@ -86,25 +86,23 @@ export default { }; </script> <template> - <div class="mr-widget-body media"> - <status-icon :show-disabled-button="true" status="warning" /> - - <div v-if="isLoading" class="gl-ml-4 gl-w-full mr-conflict-loader"> + <state-container status="warning" :is-loading="isLoading"> + <template #loading> <gl-skeleton-loader :width="334" :height="30"> <rect x="0" y="7" width="150" height="16" rx="4" /> <rect x="158" y="7" width="84" height="16" rx="4" /> <rect x="250" y="7" width="84" height="16" rx="4" /> </gl-skeleton-loader> - </div> - <div v-else class="media-body space-children gl-display-flex gl-align-items-center"> - <span v-if="shouldBeRebased" class="gl-ml-0! gl-text-body! bold"> + </template> + <template v-if="!isLoading"> + <span v-if="shouldBeRebased" class="bold gl-ml-0! gl-text-body!"> {{ s__(`mrWidget|Merge blocked: fast-forward merge is not possible. To merge this request, first rebase locally.`) }} </span> <template v-else> - <span class="gl-ml-0! gl-text-body! bold"> + <span class="bold gl-ml-0! gl-text-body! gl-flex-grow-1 gl-w-full gl-md-w-auto gl-mr-2"> {{ s__('mrWidget|Merge blocked: merge conflicts must be resolved.') }} <span v-if="!canMerge"> {{ @@ -114,23 +112,30 @@ export default { }} </span> </span> - <gl-button - v-if="showResolveButton" - :href="mr.conflictResolutionPath" - size="small" - data-testid="resolve-conflicts-button" - > - {{ s__('mrWidget|Resolve conflicts') }} - </gl-button> - <gl-button - v-if="canMerge" - size="small" - data-testid="merge-locally-button" - class="js-check-out-modal-trigger" - > - {{ s__('mrWidget|Resolve locally') }} - </gl-button> </template> - </div> - </div> + </template> + <template v-if="!isLoading && !shouldBeRebased" #actions> + <gl-button + v-if="canMerge" + size="small" + variant="confirm" + category="secondary" + data-testid="merge-locally-button" + class="js-check-out-modal-trigger gl-align-self-start" + :class="{ 'gl-mr-2': showResolveButton }" + > + {{ s__('mrWidget|Resolve locally') }} + </gl-button> + <gl-button + v-if="showResolveButton" + :href="mr.conflictResolutionPath" + size="small" + variant="confirm" + class="gl-mb-2 gl-md-mb-0 gl-align-self-start" + data-testid="resolve-conflicts-button" + > + {{ s__('mrWidget|Resolve conflicts') }} + </gl-button> + </template> + </state-container> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue index 19df346759f..4416123cd51 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue @@ -1,5 +1,5 @@ <script> -import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui'; +import { GlTooltipDirective, GlIcon } from '@gitlab/ui'; import api from '~/api'; import createFlash from '~/flash'; import { s__, __ } from '~/locale'; @@ -7,6 +7,7 @@ import { OPEN_REVERT_MODAL, OPEN_CHERRY_PICK_MODAL } from '~/projects/commit/con import modalEventHub from '~/projects/commit/event_hub'; import eventHub from '../../event_hub'; import MrWidgetAuthorTime from '../mr_widget_author_time.vue'; +import StateContainer from '../state_container.vue'; export default { name: 'MRWidgetMerged', @@ -16,7 +17,7 @@ export default { components: { MrWidgetAuthorTime, GlIcon, - GlButton, + StateContainer, }, props: { mr: { @@ -72,6 +73,53 @@ export default { cherryPickLabel() { return s__('mrWidget|Cherry-pick'); }, + actions() { + const actions = []; + + if (this.mr.canRevertInCurrentMR) { + actions.push({ + text: this.revertLabel, + tooltipText: this.revertTitle, + dataQaSelector: 'revert_button', + onClick: () => this.openRevertModal(), + }); + } else if (this.mr.revertInForkPath) { + actions.push({ + text: this.revertLabel, + tooltipText: this.revertTitle, + href: this.mr.revertInForkPath, + dataQaSelector: 'revert_button', + dataMethod: 'post', + }); + } + + if (this.mr.canCherryPickInCurrentMR) { + actions.push({ + text: this.cherryPickLabel, + tooltipText: this.cherryPickTitle, + dataQaSelector: 'cherry_pick_button', + onClick: () => this.openCherryPickModal(), + }); + } else if (this.mr.cherryPickInForkPath) { + actions.push({ + text: this.cherryPickLabel, + tooltipText: this.cherryPickTitle, + href: this.mr.cherryPickInForkPath, + dataQaSelector: 'cherry_pick_button', + dataMethod: 'post', + }); + } + + if (this.shouldShowRemoveSourceBranch) { + actions.push({ + text: s__('mrWidget|Delete source branch'), + class: 'js-remove-branch-button', + onClick: () => this.removeSourceBranch(), + }); + } + + return actions; + }, }, mounted() { document.dispatchEvent(new CustomEvent('merged:UpdateActions')); @@ -115,68 +163,15 @@ export default { }; </script> <template> - <div class="mr-widget-body media"> - <gl-icon name="merge" :size="24" class="gl-text-blue-500 gl-mr-3 gl-mt-1" /> - <div class="media-body"> - <div class="space-children"> - <mr-widget-author-time - :action-text="s__('mrWidget|Merged by')" - :author="mr.metrics.mergedBy" - :date-title="mr.metrics.mergedAt" - :date-readable="mr.metrics.readableMergedAt" - /> - <gl-button - v-if="mr.canRevertInCurrentMR" - v-gl-tooltip.hover - :title="revertTitle" - size="small" - category="secondary" - data-qa-selector="revert_button" - @click="openRevertModal" - > - {{ revertLabel }} - </gl-button> - <gl-button - v-else-if="mr.revertInForkPath" - v-gl-tooltip.hover - :href="mr.revertInForkPath" - :title="revertTitle" - size="small" - category="secondary" - data-method="post" - > - {{ revertLabel }} - </gl-button> - <gl-button - v-if="mr.canCherryPickInCurrentMR" - v-gl-tooltip.hover - :title="cherryPickTitle" - size="small" - data-qa-selector="cherry_pick_button" - @click="openCherryPickModal" - > - {{ cherryPickLabel }} - </gl-button> - <gl-button - v-else-if="mr.cherryPickInForkPath" - v-gl-tooltip.hover - :href="mr.cherryPickInForkPath" - :title="cherryPickTitle" - size="small" - data-method="post" - > - {{ cherryPickLabel }} - </gl-button> - <gl-button - v-if="shouldShowRemoveSourceBranch" - :disabled="isMakingRequest" - size="small" - class="js-remove-branch-button" - @click="removeSourceBranch" - > - {{ s__('mrWidget|Delete source branch') }} - </gl-button> - </div> - </div> - </div> + <state-container :actions="actions"> + <template #icon> + <gl-icon name="merge" :size="24" class="gl-text-blue-500 gl-mr-3 gl-mt-1" /> + </template> + <mr-widget-author-time + :action-text="s__('mrWidget|Merged by')" + :author="mr.metrics.mergedBy" + :date-title="mr.metrics.mergedAt" + :date-readable="mr.metrics.readableMergedAt" + /> + </state-container> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue index 0880fd81500..d25d1ade7eb 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -8,7 +8,7 @@ import simplePoll from '~/lib/utils/simple_poll'; import eventHub from '../../event_hub'; import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables'; import rebaseQuery from '../../queries/states/rebase.query.graphql'; -import statusIcon from '../mr_widget_status_icon.vue'; +import StateContainer from '../state_container.vue'; export default { name: 'MRWidgetRebase', @@ -25,9 +25,9 @@ export default { }, }, components: { - statusIcon, GlSkeletonLoader, GlButton, + StateContainer, }, mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin], props: { @@ -76,6 +76,10 @@ export default { return this.mr.targetBranch; }, status() { + if (this.isLoading) { + return undefined; + } + if (this.rebaseInProgress || this.isMakingRequest) { return 'loading'; } @@ -148,69 +152,71 @@ export default { }; </script> <template> - <div class="mr-widget-body media"> - <div v-if="isLoading" class="gl-w-full mr-conflict-loader"> + <state-container :status="status" :is-loading="isLoading"> + <template #loading> <gl-skeleton-loader :width="334" :height="30"> <rect x="0" y="3" width="24" height="24" rx="4" /> <rect x="32" y="5" width="302" height="20" rx="4" /> </gl-skeleton-loader> - </div> - <template v-else> - <status-icon :status="status" :show-disabled-button="showDisabledButton" /> - - <div class="rebase-state-find-class-convention media media-body space-children"> + </template> + <template v-if="!isLoading"> + <span + v-if="rebaseInProgress || isMakingRequest" + class="gl-ml-0! gl-text-body! gl-font-weight-bold" + data-testid="rebase-message" + >{{ __('Rebase in progress') }}</span + > + <span + v-if="!rebaseInProgress && !canPushToSourceBranch" + class="gl-text-body! gl-font-weight-bold gl-ml-0!" + data-testid="rebase-message" + >{{ fastForwardMergeText }}</span + > + <div + v-if="!rebaseInProgress && canPushToSourceBranch && !isMakingRequest" + class="accept-merge-holder clearfix js-toggle-container media gl-md-display-flex gl-flex-wrap gl-flex-grow-1" + > <span - v-if="rebaseInProgress || isMakingRequest" - class="gl-ml-0! gl-text-body! gl-font-weight-bold" + v-if="!rebasingError" + class="gl-font-weight-bold gl-w-100 gl-md-w-auto gl-flex-grow-1 gl-ml-0! gl-text-body! gl-md-mr-3" data-testid="rebase-message" - >{{ __('Rebase in progress') }}</span + data-qa-selector="no_fast_forward_message_content" + >{{ + __('Merge blocked: the source branch must be rebased onto the target branch.') + }}</span > <span - v-if="!rebaseInProgress && !canPushToSourceBranch" - class="gl-text-body! gl-font-weight-bold gl-ml-0!" + v-else + class="gl-font-weight-bold danger gl-w-100 gl-md-w-auto gl-flex-grow-1 gl-md-mr-3" data-testid="rebase-message" - >{{ fastForwardMergeText }}</span - > - <div - v-if="!rebaseInProgress && canPushToSourceBranch && !isMakingRequest" - class="accept-merge-holder clearfix js-toggle-container accept-action media space-children gl-align-items-center" + >{{ rebasingError }}</span > - <span - v-if="!rebasingError" - class="gl-ml-0! gl-text-body! gl-font-weight-bold" - data-testid="rebase-message" - data-qa-selector="no_fast_forward_message_content" - >{{ - __('Merge blocked: the source branch must be rebased onto the target branch.') - }}</span - > - <span v-else class="gl-font-weight-bold danger" data-testid="rebase-message">{{ - rebasingError - }}</span> - <gl-button - :loading="isMakingRequest" - variant="confirm" - size="small" - data-qa-selector="mr_rebase_button" - data-testid="standard-rebase-button" - class="gl-ml-3!" - @click="rebase" - > - {{ __('Rebase') }} - </gl-button> - <gl-button - v-if="showRebaseWithoutCi" - :loading="isMakingRequest" - variant="confirm" - size="small" - category="secondary" - data-testid="rebase-without-ci-button" - @click="rebaseWithoutCi" - > - {{ __('Rebase without pipeline') }} - </gl-button> - </div> </div> </template> - </div> + <template v-if="!isLoading" #actions> + <gl-button + v-if="showRebaseWithoutCi" + :loading="isMakingRequest" + variant="confirm" + size="small" + category="secondary" + data-testid="rebase-without-ci-button" + class="gl-align-self-start gl-mr-2" + @click="rebaseWithoutCi" + > + {{ __('Rebase without pipeline') }} + </gl-button> + <gl-button + :loading="isMakingRequest" + variant="confirm" + size="small" + data-qa-selector="mr_rebase_button" + data-testid="standard-rebase-button" + class="gl-mb-2 gl-md-mb-0 gl-align-self-start" + @click="rebase" + > + {{ __('Rebase') }} + </gl-button> + </template> + </state-container> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index 884fc666276..d2c85b14999 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -335,6 +335,9 @@ export default { ? __('Did not delete the source branch.') : __('Source branch will not be deleted.'); }, + showMergeDetailsHeader() { + return ['readyToMerge'].indexOf(this.mr.state) >= 0; + }, }, mounted() { if (this.glFeatures.mergeRequestWidgetGraphql) { @@ -524,7 +527,7 @@ export default { </div> </div> <template v-else> - <div class="mr-widget-body media mr-widget-body-line-height-1"> + <div class="mr-widget-body mr-widget-body-ready-merge media mr-widget-body-line-height-1"> <div class="media-body"> <div class="mr-widget-body-controls gl-display-flex gl-align-items-center gl-flex-wrap"> <gl-button-group v-if="shouldShowMergeControls" class="gl-align-self-start"> @@ -651,13 +654,13 @@ export default { </div> <div v-if="!shouldShowMergeControls" - class="gl-w-full gl-order-n1 gl-text-gray-500" + class="gl-w-full gl-order-n1 mr-widget-merge-details" data-qa-selector="merged_status_content" > - <strong v-if="mr.state !== 'closed'"> + <p v-if="showMergeDetailsHeader" class="gl-mb-3 gl-text-gray-900"> {{ __('Merge details') }} - </strong> - <ul class="gl-pl-4 gl-m-0"> + </p> + <ul class="gl-pl-4 gl-mb-0 gl-ml-3 gl-text-gray-600"> <li v-if="mr.divergedCommitsCount > 0" class="gl-line-height-normal"> <gl-sprintf :message="s__('mrWidget|The source branch is %{link} the target branch')" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue index 06f70bc456e..d149f5208fc 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue @@ -1,13 +1,13 @@ <script> import { GlButton } from '@gitlab/ui'; import { I18N_SHA_MISMATCH } from '../../i18n'; -import statusIcon from '../mr_widget_status_icon.vue'; +import StateContainer from '../state_container.vue'; export default { name: 'ShaMismatch', components: { - statusIcon, GlButton, + StateContainer, }, i18n: { I18N_SHA_MISMATCH, @@ -22,24 +22,24 @@ export default { </script> <template> - <div class="mr-widget-body media"> - <status-icon :show-disabled-button="false" status="warning" /> - <div class="media-body"> - <span - class="gl-ml-0! gl-text-body! gl-font-weight-bold" - data-qa-selector="head_mismatch_content" - > - {{ $options.i18n.I18N_SHA_MISMATCH.warningMessage }} - </span> + <state-container status="warning"> + <span + class="gl-font-weight-bold gl-md-mr-3 gl-flex-grow-1 gl-ml-0! gl-text-body!" + data-qa-selector="head_mismatch_content" + > + {{ $options.i18n.I18N_SHA_MISMATCH.warningMessage }} + </span> + <template #actions> <gl-button - class="gl-ml-3" data-testid="action-button" size="small" category="primary" variant="confirm" + class="gl-align-self-start" :href="mr.mergeRequestDiffsPath" - >{{ $options.i18n.I18N_SHA_MISMATCH.actionButtonLabel }}</gl-button > - </div> - </div> + {{ $options.i18n.I18N_SHA_MISMATCH.actionButtonLabel }} + </gl-button> + </template> + </state-container> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue index 7e9ebcf0b2e..035d62eaa59 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue @@ -1,13 +1,13 @@ <script> import { GlButton } from '@gitlab/ui'; import notesEventHub from '~/notes/event_hub'; -import statusIcon from '../mr_widget_status_icon.vue'; +import StateContainer from '../state_container.vue'; export default { name: 'UnresolvedDiscussions', components: { - statusIcon, GlButton, + StateContainer, }, props: { mr: { @@ -24,30 +24,33 @@ export default { </script> <template> - <div class="mr-widget-body media gl-flex-wrap"> - <status-icon show-disabled-button status="warning" /> - <div class="media-body"> - <span class="gl-ml-0! gl-text-body! gl-ml-3 gl-font-weight-bold gl-w-100"> - {{ s__('mrWidget|Merge blocked: all threads must be resolved.') }} - </span> + <state-container status="warning"> + <span + class="gl-ml-3 gl-font-weight-bold gl-w-100 gl-flex-grow-1 gl-md-mr-3 gl-ml-0! gl-text-body!" + > + {{ s__('mrWidget|Merge blocked: all threads must be resolved.') }} + </span> + <template #actions> <gl-button - data-testid="jump-to-first" - class="gl-ml-3" + v-if="mr.createIssueToResolveDiscussionsPath" + :href="mr.createIssueToResolveDiscussionsPath" + class="js-create-issue gl-align-self-start gl-vertical-align-top gl-mr-2" size="small" variant="confirm" category="secondary" - @click="jumpToFirstUnresolvedDiscussion" > - {{ s__('mrWidget|Jump to first unresolved thread') }} + {{ s__('mrWidget|Create issue to resolve all threads') }} </gl-button> <gl-button - v-if="mr.createIssueToResolveDiscussionsPath" - :href="mr.createIssueToResolveDiscussionsPath" - class="js-create-issue gl-ml-3" + data-testid="jump-to-first" + class="gl-mb-2 gl-md-mb-0 gl-align-self-start gl-vertical-align-top" size="small" + variant="confirm" + category="primary" + @click="jumpToFirstUnresolvedDiscussion" > - {{ s__('mrWidget|Create issue to resolve all threads') }} + {{ s__('mrWidget|Jump to first unresolved thread') }} </gl-button> - </div> - </div> + </template> + </state-container> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue index 501aee1a18d..cf7f83c014a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue @@ -12,13 +12,13 @@ import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_va import getStateQuery from '../../queries/get_state.query.graphql'; import draftQuery from '../../queries/states/draft.query.graphql'; import removeDraftMutation from '../../queries/toggle_draft.mutation.graphql'; -import StatusIcon from '../mr_widget_status_icon.vue'; +import StateContainer from '../state_container.vue'; export default { name: 'WorkInProgress', components: { - StatusIcon, GlButton, + StateContainer, }, mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin], apollo: { @@ -163,26 +163,22 @@ export default { </script> <template> - <div class="mr-widget-body media"> - <status-icon :show-disabled-button="canUpdate" status="warning" /> - <div class="media-body"> - <div class="float-left"> - <span class="gl-ml-0! gl-text-body! gl-font-weight-bold"> - {{ - __("Merge blocked: merge request must be marked as ready. It's still marked as draft.") - }} - </span> - </div> + <state-container status="warning"> + <span class="gl-font-weight-bold gl-ml-0! gl-text-body! gl-flex-grow-1"> + {{ __("Merge blocked: merge request must be marked as ready. It's still marked as draft.") }} + </span> + <template #actions> <gl-button v-if="canUpdate" size="small" :disabled="isMakingRequest" :loading="isMakingRequest" - class="js-remove-draft gl-ml-3" + variant="confirm" + class="js-remove-draft gl-md-ml-3 gl-align-self-start" @click="handleRemoveDraft" > {{ s__('mrWidget|Mark as ready') }} </gl-button> - </div> - </div> + </template> + </state-container> </template> diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue index 59b49a4e10c..38083327593 100644 --- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue +++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue @@ -7,6 +7,7 @@ import { differenceInSeconds, getTimeago, SECONDS_IN_DAY } from '~/lib/utils/dat import { isExternal, setUrlFragment } from '~/lib/utils/url_utility'; import { __, n__, sprintf } from '~/locale'; import IssuableAssignees from '~/issuable/components/issue_assignees.vue'; +import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue'; import timeagoMixin from '~/vue_shared/mixins/timeago'; export default { @@ -17,6 +18,7 @@ export default { GlFormCheckbox, GlSprintf, IssuableAssignees, + WorkItemTypeIcon, }, directives: { GlTooltip: GlTooltipDirective, @@ -50,6 +52,11 @@ export default { required: false, default: false, }, + showWorkItemTypeIcon: { + type: Boolean, + required: false, + default: false, + }, }, computed: { issuableId() { @@ -225,6 +232,7 @@ export default { </span> </div> <div class="issuable-info"> + <work-item-type-icon v-if="showWorkItemTypeIcon" :work-item-type="issuable.type" /> <slot v-if="hasSlotContents('reference')" name="reference"></slot> <span v-else data-testid="issuable-reference" class="issuable-reference"> {{ reference }} diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue index 189bbb56432..bc10f84b819 100644 --- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue +++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue @@ -182,6 +182,11 @@ export default { required: false, default: false, }, + showWorkItemTypeIcon: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -344,6 +349,7 @@ export default { :label-filter-param="labelFilterParam" :show-checkbox="showBulkEditSidebar" :checked="issuableChecked(issuable)" + :show-work-item-type-icon="showWorkItemTypeIcon" @checked-input="handleIssuableCheckedInput(issuable, $event)" > <template #reference> diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue index 63ab1eb17d4..1f23fdfaafd 100644 --- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue +++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue @@ -12,6 +12,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { isExternal } from '~/lib/utils/url_utility'; import { n__, sprintf } from '~/locale'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue'; import { IssuableStates } from '~/vue_shared/issuable/list/constants'; export default { @@ -22,6 +23,7 @@ export default { GlAvatarLink, GlAvatarLabeled, TimeAgoTooltip, + WorkItemTypeIcon, }, directives: { GlTooltip: GlTooltipDirective, @@ -65,6 +67,16 @@ export default { required: false, default: null, }, + issuableType: { + type: String, + required: false, + default: '', + }, + showWorkItemTypeIcon: { + type: Boolean, + required: false, + default: false, + }, }, computed: { badgeVariant() { @@ -122,7 +134,13 @@ export default { </div> </div> <span> - {{ __('Created') }} + <template v-if="showWorkItemTypeIcon"> + <work-item-type-icon :work-item-type="issuableType" show-text /> + {{ __('created') }} + </template> + <template v-else> + {{ __('Created') }} + </template> <time-ago-tooltip data-testid="startTimeItem" :time="createdAt" /> {{ __('by') }} </span> diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue index 7ed93c042f8..2bc57ecba55 100644 --- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue +++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue @@ -87,6 +87,11 @@ export default { required: false, default: 0, }, + showWorkItemTypeIcon: { + type: Boolean, + required: false, + default: false, + }, }, methods: { handleKeydownTitle(e, issuableMeta) { @@ -110,6 +115,8 @@ export default { :created-at="issuable.createdAt" :author="issuable.author" :task-completion-status="taskCompletionStatus" + :issuable-type="issuable.type" + :show-work-item-type-icon="showWorkItemTypeIcon" > <template #status-badge> <slot name="status-badge"></slot> diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index 44c9ef9e44b..7b3eeab649b 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -10,6 +10,7 @@ import { } from '@gitlab/ui'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; +import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue'; import { i18n, WIDGET_TYPE_ASSIGNEES, @@ -55,6 +56,7 @@ export default { WorkItemWeight, WorkItemInformation, LocalStorageSync, + WorkItemTypeIcon, }, mixins: [glFeatureFlagMixin()], props: { @@ -146,6 +148,9 @@ export default { parentUrl() { return `../../issues/${this.parentWorkItem?.iid}`; }, + workItemIconName() { + return this.workItem?.workItemType?.iconName; + }, }, beforeDestroy() { /** make sure that if the user has not even dismissed the alert , @@ -243,16 +248,21 @@ export default { <li class="gl-px-4 gl-py-3 gl-line-height-0 gl-display-flex gl-align-items-center gl-overflow-hidden gl-flex-shrink-0" > - <gl-icon name="task-done" class="gl-mr-2 gl-flex-shrink-0" /> + <work-item-type-icon + :work-item-icon-name="workItemIconName" + :work-item-type="workItemType && workItemType.toUpperCase()" + /> {{ workItemType }} </li> </ul> - <span + <work-item-type-icon v-else + :work-item-icon-name="workItemIconName" + :work-item-type="workItemType && workItemType.toUpperCase()" + show-text class="gl-font-weight-bold gl-text-secondary gl-mr-auto" data-testid="work-item-type" - >{{ workItemType }}</span - > + /> <gl-loading-icon v-if="updateInProgress" :inline="true" class="gl-mr-3" /> <gl-badge v-if="workItem.confidential" variant="warning" icon="eye-slash" class="gl-mr-3">{{ __('Confidential') @@ -317,16 +327,16 @@ export default { :can-update="canUpdate" @error="error = $event" /> - <work-item-weight - v-if="workItemWeight" - class="gl-mb-5" - :can-update="canUpdate" - :weight="workItemWeight.weight" - :work-item-id="workItem.id" - :work-item-type="workItemType" - @error="error = $event" - /> </template> + <work-item-weight + v-if="workItemWeight" + class="gl-mb-5" + :can-update="canUpdate" + :weight="workItemWeight.weight" + :work-item-id="workItem.id" + :work-item-type="workItemType" + @error="error = $event" + /> <work-item-description v-if="hasDescriptionWidget" :work-item-id="workItem.id" diff --git a/app/assets/javascripts/work_items/components/work_item_type_icon.vue b/app/assets/javascripts/work_items/components/work_item_type_icon.vue new file mode 100644 index 00000000000..fd914fa350b --- /dev/null +++ b/app/assets/javascripts/work_items/components/work_item_type_icon.vue @@ -0,0 +1,44 @@ +<script> +import { GlIcon } from '@gitlab/ui'; +import { WORK_ITEMS_TYPE_MAP } from '../constants'; + +export default { + components: { + GlIcon, + }, + props: { + workItemType: { + type: String, + required: false, + default: '', + }, + showText: { + type: Boolean, + required: false, + default: false, + }, + workItemIconName: { + type: String, + required: false, + default: '', + }, + }, + computed: { + iconName() { + return ( + this.workItemIconName || WORK_ITEMS_TYPE_MAP[this.workItemType]?.icon || 'issue-type-issue' + ); + }, + workItemTypeName() { + return WORK_ITEMS_TYPE_MAP[this.workItemType]?.name; + }, + }, +}; +</script> + +<template> + <span> + <gl-icon :name="iconName" class="gl-mr-2" /> + <span v-if="workItemTypeName" :class="{ 'gl-sr-only': !showText }">{{ workItemTypeName }}</span> + </span> +</template> diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js index b0ade1fbe45..d89978bff82 100644 --- a/app/assets/javascripts/work_items/constants.js +++ b/app/assets/javascripts/work_items/constants.js @@ -21,6 +21,7 @@ export const WORK_ITEM_TYPE_ENUM_INCIDENT = 'INCIDENT'; export const WORK_ITEM_TYPE_ENUM_ISSUE = 'ISSUE'; export const WORK_ITEM_TYPE_ENUM_TASK = 'TASK'; export const WORK_ITEM_TYPE_ENUM_TEST_CASE = 'TEST_CASE'; +export const WORK_ITEM_TYPE_ENUM_REQUIREMENTS = 'REQUIREMENTS'; export const i18n = { fetchError: s__('WorkItem|Something went wrong when fetching the work item. Please try again.'), @@ -39,3 +40,26 @@ export const WORK_ITEM_STATUS_TEXT = { export const WORK_ITEM_TYPE_IDS = { TASK: 'gid://gitlab/WorkItems::Type/5', }; + +export const WORK_ITEMS_TYPE_MAP = { + [WORK_ITEM_TYPE_ENUM_INCIDENT]: { + icon: `issue-type-incident`, + name: s__('WorkItem|Incident'), + }, + [WORK_ITEM_TYPE_ENUM_ISSUE]: { + icon: `issue-type-issue`, + name: s__('WorkItem|Issue'), + }, + [WORK_ITEM_TYPE_ENUM_TASK]: { + icon: `issue-type-task`, + name: s__('WorkItem|Task'), + }, + [WORK_ITEM_TYPE_ENUM_TEST_CASE]: { + icon: `issue-type-test-case`, + name: s__('WorkItem|Test case'), + }, + [WORK_ITEM_TYPE_ENUM_REQUIREMENTS]: { + icon: `issue-type-requirements`, + name: s__('WorkItem|Requirements'), + }, +}; diff --git a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql index dacc1527a43..fd6a71ae9b4 100644 --- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql +++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql @@ -9,6 +9,7 @@ fragment WorkItem on WorkItem { workItemType { id name + iconName } userPermissions { deleteWorkItem diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss index 1b27e51e793..b7a75884425 100644 --- a/app/assets/stylesheets/page_bundles/merge_requests.scss +++ b/app/assets/stylesheets/page_bundles/merge_requests.scss @@ -416,8 +416,6 @@ $tabs-holder-z-index: 250; .label-branch { @include gl-font-monospace; font-size: 95%; - color: var(--gl-text-color, $gl-text-color); - font-weight: normal; overflow: hidden; word-break: break-all; } @@ -477,8 +475,7 @@ $tabs-holder-z-index: 250; margin: 0 0 0 10px; } - .bold, - .gl-font-weight-bold { + .bold { font-weight: $gl-font-weight-bold; color: var(--gray-600, $gray-600); margin-left: 10px; @@ -494,8 +491,7 @@ $tabs-holder-z-index: 250; } .spacing, - .bold, - .gl-font-weight-bold { + .bold { vertical-align: middle; } @@ -602,6 +598,12 @@ $tabs-holder-z-index: 250; padding: $gl-padding; } +.mr-widget-body-ready-merge { + @include media-breakpoint-down(sm) { + @include gl-p-3; + } +} + .mr-widget-border-top { border-top: 1px solid var(--border-color, $border-color); } @@ -820,3 +822,21 @@ $tabs-holder-z-index: 250; height: 180px; } } + +.mr-widget-merge-details { + li:not(:last-child) { + @include gl-mb-3; + } +} + +.mr-ready-merge-related-links, +.mr-widget-merge-details { + a { + @include gl-text-decoration-underline; + + &:hover, + &:focus { + @include gl-text-decoration-none; + } + } +} diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 069e7316ac1..8fd004233e2 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -154,7 +154,13 @@ module IssuablesHelper def issuable_meta(issuable, project) output = [] - output << "Created #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe + + if issuable.respond_to?(:work_item_type) && WorkItems::Type::WI_TYPES_WITH_CREATED_HEADER.include?(issuable.work_item_type.base_type) + output << content_tag(:span, sprite_icon("#{issuable.work_item_type.icon_name}", css_class: 'gl-icon gl-vertical-align-middle'), class: 'gl-mr-2', aria: { hidden: 'true' }) + output << s_('IssuableStatus|%{wi_type} created %{created_at} by ').html_safe % { wi_type: issuable.issue_type.capitalize, created_at: time_ago_with_tooltip(issuable.created_at) } + else + output << s_('IssuableStatus|Created %{created_at} by').html_safe % { created_at: time_ago_with_tooltip(issuable.created_at) } + end if issuable.is_a?(Issue) && issuable.service_desk_reply_to output << "#{html_escape(issuable.service_desk_reply_to)} via " diff --git a/app/helpers/nav/top_nav_helper.rb b/app/helpers/nav/top_nav_helper.rb index 3ceb60251c2..efec6f2d0d8 100644 --- a/app/helpers/nav/top_nav_helper.rb +++ b/app/helpers/nav/top_nav_helper.rb @@ -81,13 +81,6 @@ module Nav **snippets_menu_item_attrs ) end - - builder.add_secondary_menu_item( - id: 'help', - title: _('Help'), - icon: 'question-o', - href: help_path - ) end def build_view_model(builder:, project:, group:) diff --git a/app/models/work_items/type.rb b/app/models/work_items/type.rb index d715dfe041e..753fcbcb8f9 100644 --- a/app/models/work_items/type.rb +++ b/app/models/work_items/type.rb @@ -28,6 +28,8 @@ module WorkItems task: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::Hierarchy, Widgets::StartAndDueDate] }.freeze + WI_TYPES_WITH_CREATED_HEADER = %w[issue incident].freeze + cache_markdown_field :description, pipeline: :single_line enum base_type: BASE_TYPES.transform_values { |value| value[:enum_value] } |