diff options
Diffstat (limited to 'app/assets/javascripts/vue_merge_request_widget/components')
8 files changed, 164 insertions, 146 deletions
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 028f5370028..f7c0f960c0e 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 @@ -68,7 +68,7 @@ export default { }, isCollapsible() { if (!this.isLoadingSummary && this.loadingState !== LOADING_STATES.collapsedError) { - if (this.shouldCollapse) { + if ('shouldCollapse' in this) { return this.shouldCollapse(this.collapsedData); } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js index 7e329399957..0b8f5ffa397 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import { markRaw } from '~/lib/utils/vue3compat/mark_raw'; import ExtensionBase from './base.vue'; // Holds all the currently registered extensions @@ -7,45 +8,47 @@ export const registeredExtensions = Vue.observable({ extensions: [] }); export const registerExtension = (extension) => { // Pushes into the extenions array a dynamically created Vue component // that gets exteneded from `base.vue` - registeredExtensions.extensions.push({ - extends: ExtensionBase, - name: extension.name, - props: { - mr: { - type: Object, - required: true, + registeredExtensions.extensions.push( + markRaw({ + extends: ExtensionBase, + name: extension.name, + props: { + mr: { + type: Object, + required: true, + }, }, - }, - telemetry: extension.telemetry, - i18n: extension.i18n, - expandEvent: extension.expandEvent, - enablePolling: extension.enablePolling, - enableExpandedPolling: extension.enableExpandedPolling, - modalComponent: extension.modalComponent, - computed: { - ...extension.props.reduce( - (acc, propKey) => ({ - ...acc, - [propKey]() { - return this.mr[propKey]; - }, - }), - {}, - ), - ...Object.keys(extension.computed).reduce( - (acc, computedKey) => ({ - ...acc, - // Making the computed property a method allows us to pass in arguments - // this allows for each computed property to receive some data - [computedKey]() { - return extension.computed[computedKey]; - }, - }), - {}, - ), - }, - methods: { - ...extension.methods, - }, - }); + telemetry: extension.telemetry, + i18n: extension.i18n, + expandEvent: extension.expandEvent, + enablePolling: extension.enablePolling, + enableExpandedPolling: extension.enableExpandedPolling, + modalComponent: extension.modalComponent, + computed: { + ...extension.props.reduce( + (acc, propKey) => ({ + ...acc, + [propKey]() { + return this.mr[propKey]; + }, + }), + {}, + ), + ...Object.keys(extension.computed).reduce( + (acc, computedKey) => ({ + ...acc, + // Making the computed property a method allows us to pass in arguments + // this allows for each computed property to receive some data + [computedKey]() { + return extension.computed[computedKey]; + }, + }), + {}, + ), + }, + methods: { + ...extension.methods, + }, + }), + ); }; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue index 5baeb309f79..8bf4d8816be 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue @@ -1,10 +1,9 @@ <script> -import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { GlDisclosureDropdown } from '@gitlab/ui'; export default { components: { - GlDropdown, - GlDropdownItem, + GlDisclosureDropdown, }, props: { commits: { @@ -13,28 +12,36 @@ export default { default: () => [], }, }, + computed: { + dropdownItems() { + return this.commits.map((commit) => ({ + text: commit.title, + extraAttrs: { + text: commit.shortId || commit.short_Id, + }, + action: () => { + this.$emit('input', commit.message); + }, + })); + }, + }, }; </script> <template> <div> - <gl-dropdown - right - text="Use an existing commit message" + <gl-disclosure-dropdown + placement="right" + toggle-text="Use an existing commit message" category="tertiary" - variant="confirm" + :items="dropdownItems" size="small" class="mr-commit-dropdown" > - <gl-dropdown-item - v-for="(commit, index) in commits" - :key="index" - class="text-nowrap text-truncate" - @click="$emit('input', commit.message)" - > - <span class="monospace mr-2">{{ commit.shortId || commit.short_id }}</span> - {{ commit.title }} - </gl-dropdown-item> - </gl-dropdown> + <template #list-item="{ item }"> + <span class="gl-mr-2">{{ item.extraAttrs.text }}</span> + {{ item.text }} + </template> + </gl-disclosure-dropdown> </div> </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 52cdafd4717..7071759b8bb 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 @@ -27,7 +27,6 @@ import readyToMergeSubscription from '~/vue_merge_request_widget/queries/states/ import HelpPopover from '~/vue_shared/components/help_popover.vue'; import { AUTO_MERGE_STRATEGIES, - WARNING, MT_MERGE_STRATEGY, PIPELINE_FAILED_STATE, STATE_MACHINE, @@ -42,7 +41,6 @@ import CommitMessageDropdown from './commit_message_dropdown.vue'; import SquashBeforeMerge from './squash_before_merge.vue'; import MergeFailedPipelineConfirmationDialog from './merge_failed_pipeline_confirmation_dialog.vue'; -const PIPELINE_RUNNING_STATE = 'running'; const PIPELINE_PENDING_STATE = 'pending'; const PIPELINE_SUCCESS_STATE = 'success'; @@ -133,8 +131,6 @@ export default { GlFormCheckbox, GlSkeletonLoader, MergeFailedPipelineConfirmationDialog, - MergeTrainHelperIcon: () => - import('ee_component/vue_merge_request_widget/components/merge_train_helper_icon.vue'), MergeImmediatelyConfirmationDialog: () => import( 'ee_component/vue_merge_request_widget/components/merge_immediately_confirmation_dialog.vue' @@ -176,14 +172,11 @@ export default { }; }, computed: { - stateData() { - return this.state; - }, hasCI() { - return this.stateData.hasCI || this.stateData.hasCi; + return this.state.hasCI || this.state.hasCi; }, isAutoMergeAvailable() { - return !isEmpty(this.stateData.availableAutoMergeStrategies); + return !isEmpty(this.state.availableAutoMergeStrategies); }, pipeline() { return this.state.headPipeline; @@ -246,30 +239,11 @@ export default { return PIPELINE_SUCCESS_STATE; }, - iconClass() { - if (this.shouldRenderMergeTrainHelperIcon && !this.mr.preventMerge) { - return PIPELINE_RUNNING_STATE; - } - - if ( - this.status === PIPELINE_FAILED_STATE || - !this.commitMessage.length || - !this.isMergeAllowed || - this.mr.preventMerge - ) { - return WARNING; - } - - return PIPELINE_SUCCESS_STATE; - }, mergeButtonText() { if (this.isMergingImmediately) { return __('Merge in progress'); } - if (this.isAutoMergeAvailable && !this.autoMergeLabelsEnabled) { - return this.autoMergeTextLegacy; - } - if (this.isAutoMergeAvailable && this.autoMergeLabelsEnabled) { + if (this.isAutoMergeAvailable) { return this.autoMergeText; } @@ -279,9 +253,6 @@ export default { return __('Merge'); }, - autoMergeLabelsEnabled() { - return window.gon?.features?.autoMergeLabelsMrWidget; - }, showAutoMergeHelperText() { return ( !(this.status === PIPELINE_FAILED_STATE || this.isPipelineFailed) && @@ -289,7 +260,7 @@ export default { ); }, hasPipelineMustSucceedConflict() { - return !this.hasCI && this.stateData.onlyAllowMergeIfPipelineSucceeds; + return !this.hasCI && this.state.onlyAllowMergeIfPipelineSucceeds; }, isNotClosed() { return this.mr.state !== STATUS_CLOSED; @@ -322,12 +293,7 @@ export default { return this.preferredAutoMergeStrategy === MT_MERGE_STRATEGY && this.isPipelineFailed; }, shouldShowMergeControls() { - return ( - (this.isMergeAllowed || this.isAutoMergeAvailable) && - (this.stateData.userPermissions?.canMerge || this.mr.canMerge) && - !this.mr.mergeOngoing && - !this.mr.autoMergeEnabled - ); + return this.state.userPermissions?.canMerge && this.mr.state === 'readyToMerge'; }, sourceBranchDeletedText() { const isPreMerge = this.mr.state !== STATUS_MERGED; @@ -354,6 +320,11 @@ export default { }; }, }, + watch: { + 'mr.state': function mrStateWatcher() { + this.isMakingRequest = false; + }, + }, mounted() { eventHub.$on('ApprovalUpdated', this.updateGraphqlState); eventHub.$on('MRWidgetUpdateRequested', this.updateGraphqlState); @@ -441,8 +412,6 @@ export default { } this.updateGraphqlState(); - - this.isMakingRequest = false; }) .catch(() => { this.isMakingRequest = false; @@ -511,13 +480,13 @@ export default { }, i18n: { mergeCommitTemplateHintText: s__( - 'mrWidget|To change this default message, edit the template for merge commit messages. %{linkStart}Learn more.%{linkEnd}', + 'mrWidget|To change this default message, edit the template for merge commit messages. %{linkStart}Learn more%{linkEnd}.', ), squashCommitTemplateHintText: s__( - 'mrWidget|To change this default message, edit the template for squash commit messages. %{linkStart}Learn more.%{linkEnd}', + 'mrWidget|To change this default message, edit the template for squash commit messages. %{linkStart}Learn more%{linkEnd}.', ), mergeAndSquashCommitTemplatesHintText: s__( - 'mrWidget|To change these default messages, edit the templates for both the merge and squash commit messages. %{linkStart}Learn more.%{linkEnd}', + 'mrWidget|To change these default messages, edit the templates for both the merge and squash commit messages. %{linkStart}Learn more%{linkEnd}.', ), sourceDivergedFromTargetText: s__('mrWidget|The source branch is %{link} the target branch'), divergedCommits: (count) => n__('%d commit behind', '%d commits behind', count), @@ -619,9 +588,8 @@ export default { :href="commitTemplateHelpPage" class="inline-link" target="_blank" + >{{ content }}</gl-link > - {{ content }} - </gl-link> </template> </gl-sprintf> </p> @@ -692,35 +660,21 @@ export default { > {{ __('Merge immediately') }} </gl-dropdown-item> - <merge-immediately-confirmation-dialog - ref="confirmationDialog" - :docs-url="mr.mergeImmediatelyDocsPath" - @mergeImmediately="onMergeImmediatelyConfirmation" - /> </gl-dropdown> - <merge-train-failed-pipeline-confirmation-dialog - :visible="isPipelineFailedModalVisibleMergeTrain" - @startMergeTrain="onStartMergeTrainConfirmation" - @cancel="isPipelineFailedModalVisibleMergeTrain = false" - /> - <merge-failed-pipeline-confirmation-dialog - :visible="isPipelineFailedModalVisibleNormalMerge" - @mergeWithFailedPipeline="onMergeWithFailedPipelineConfirmation" - @cancel="isPipelineFailedModalVisibleNormalMerge = false" - /> </gl-button-group> - <merge-train-helper-icon - v-if="shouldRenderMergeTrainHelperIcon && !autoMergeLabelsEnabled" - class="gl-mx-3" - /> - <template v-if="showAutoMergeHelperText && autoMergeLabelsEnabled"> + <template v-if="showAutoMergeHelperText"> <div class="gl-ml-4 gl-text-gray-500 gl-font-sm" data-qa-selector="auto_merge_helper_text" + data-testid="auto-merge-helper-text" > {{ autoMergeHelperText }} </div> - <help-popover class="gl-ml-2" :options="autoMergeHelpPopoverOptions"> + <help-popover + class="gl-ml-2" + :options="autoMergeHelpPopoverOptions" + data-testid="auto-merge-helper-text-icon" + > <gl-sprintf :message="autoMergePopoverSettings.bodyText"> <template #link="{ content }"> <gl-link @@ -784,6 +738,21 @@ export default { </div> </div> </div> + <merge-immediately-confirmation-dialog + ref="confirmationDialog" + :docs-url="mr.mergeImmediatelyDocsPath" + @mergeImmediately="onMergeImmediatelyConfirmation" + /> + <merge-train-failed-pipeline-confirmation-dialog + :visible="isPipelineFailedModalVisibleMergeTrain" + @startMergeTrain="onStartMergeTrainConfirmation" + @cancel="isPipelineFailedModalVisibleMergeTrain = false" + /> + <merge-failed-pipeline-confirmation-dialog + :visible="isPipelineFailedModalVisibleNormalMerge" + @mergeWithFailedPipeline="onMergeWithFailedPipelineConfirmation" + @cancel="isPipelineFailedModalVisibleNormalMerge = false" + /> </template> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue index 6655af92a55..c38c253564a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue @@ -23,7 +23,7 @@ export default { default: () => [], }, }, - data: () => { + data() { return { timeout: null, updatingTooltip: false, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue index 334fc01c9f7..258fa4edcda 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue @@ -5,16 +5,24 @@ export default { import( '~/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue' ), + + MrTerraformWidget: () => import('~/vue_merge_request_widget/extensions/terraform/index.vue'), }, + props: { mr: { type: Object, required: true, }, }, + computed: { + terraformPlansWidget() { + return this.mr.terraformReportsPath && 'MrTerraformWidget'; + }, + widgets() { - return ['MrSecurityWidget']; + return [this.terraformPlansWidget, 'MrSecurityWidget'].filter((w) => w); }, }, }; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue index cdce7c6625a..ec979861283 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue @@ -30,6 +30,11 @@ export default { required: false, default: 2, }, + rowIndex: { + type: Number, + required: false, + default: -1, + }, }, computed: { statusIcon() { @@ -44,6 +49,9 @@ export default { generatedSupportingText() { return generateText(this.data.supportingText); }, + shouldShowThirdLevel() { + return this.data.children?.length > 0 && this.level === 2; + }, }, methods: { onClickedAction(action) { @@ -60,16 +68,19 @@ export default { :widget-name="widgetName" :header="data.header" :help-popover="data.helpPopover" + :class="{ 'gl-border-top-0': rowIndex === 0 }" > <template #body> - <div class="gl-display-flex gl-flex-direction-column"> - <div> - <p v-safe-html="generatedText" class="gl-mb-0"></p> - <gl-link v-if="data.link" :href="data.link.href">{{ data.link.text }}</gl-link> - <p v-if="data.supportingText" v-safe-html="generatedSupportingText" class="gl-mb-0"></p> - <gl-badge v-if="data.badge" :variant="data.badge.variant || 'info'"> - {{ data.badge.text }} - </gl-badge> + <div class="gl-w-full gl-display-flex" :class="{ 'gl-flex-direction-column': level === 1 }"> + <div class="gl-display-flex gl-flex-grow-1"> + <div class="gl-display-flex gl-flex-grow-1 gl-flex-direction-column"> + <p v-safe-html="generatedText" class="gl-mb-0 gl-mr-1"></p> + <gl-link v-if="data.link" :href="data.link.href">{{ data.link.text }}</gl-link> + <p v-if="data.supportingText" v-safe-html="generatedSupportingText" class="gl-mb-0"></p> + <gl-badge v-if="data.badge" :variant="data.badge.variant || 'info'"> + {{ data.badge.text }} + </gl-badge> + </div> <actions :widget="widgetName" :tertiary-buttons="data.actions" @@ -78,10 +89,7 @@ export default { /> <p v-if="data.subtext" v-safe-html="generatedSubtext" class="gl-m-0 gl-font-sm"></p> </div> - <ul - v-if="data.children && data.children.length > 0 && level === 2" - class="gl-m-0 gl-p-0 gl-list-style-none" - > + <ul v-if="shouldShowThirdLevel" class="gl-m-0 gl-p-0 gl-list-style-none"> <li v-for="(childData, index) in data.children" :key="childData.id || index"> <dynamic-content :data="childData" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue index 54eb15c8ac8..e327d848d8f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue @@ -10,6 +10,7 @@ import HelpPopover from '~/vue_shared/components/help_popover.vue'; import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller'; import { EXTENSION_ICONS } from '../../constants'; import { createTelemetryHub } from '../extensions/telemetry'; +import { generateText } from '../extensions/utils'; import ContentRow from './widget_content_row.vue'; import DynamicContent from './dynamic_content.vue'; import StatusIcon from './status_icon.vue'; @@ -72,9 +73,12 @@ export default { }, // If the summary slot is not used, this value will be used as a fallback. summary: { - type: String, + type: Object, required: false, default: undefined, + validator: (s) => { + return Boolean(s.title); + }, }, // If the content slot is not used, this value will be used as a fallback. content: { @@ -154,7 +158,7 @@ export default { return { isExpandedForTheFirstTime: true, isCollapsed: true, - isLoading: false, + isLoading: true, isLoadingExpandedContent: false, summaryError: null, contentError: null, @@ -162,6 +166,12 @@ export default { }; }, computed: { + generatedSummary() { + return generateText(this.summary?.title || ''); + }, + generatedSubSummary() { + return generateText(this.summary?.subtitle || ''); + }, collapseButtonLabel() { return sprintf(this.isCollapsed ? __('Show details') : __('Hide details')); }, @@ -171,6 +181,9 @@ export default { hasActionButtons() { return this.actionButtons.length > 0 || Boolean(this.$scopedSlots['action-buttons']); }, + contentWithKeyField() { + return this.content?.map((item, index) => ({ ...item, id: item.id || index })); + }, }, watch: { hasError: { @@ -289,7 +302,7 @@ export default { <template> <section class="media-section" data-testid="widget-extension"> - <div class="gl-px-5 gl-pr-4 gl-py-4 gl-align-items-center gl-display-flex"> + <div class="gl-px-5 gl-pr-4 gl-py-4 gl-display-flex"> <status-icon :level="1" :name="widgetName" @@ -302,7 +315,14 @@ export default { > <div class="gl-flex-grow-1" data-testid="widget-extension-top-level-summary"> <span v-if="summaryError">{{ summaryError }}</span> - <slot v-else name="summary">{{ isLoading ? loadingText : summary }}</slot> + <slot v-else name="summary" + ><div v-safe-html="isLoading ? loadingText : generatedSummary"></div> + <div + v-if="!isLoading && generatedSubSummary" + v-safe-html="generatedSubSummary" + class="gl-font-sm gl-text-gray-700" + ></div + ></slot> </div> <div class="gl-display-flex"> <help-popover @@ -336,7 +356,7 @@ export default { </slot> </div> <div - v-if="isCollapsible" + v-if="isCollapsible && !isLoading" class="gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6" > <gl-button @@ -376,8 +396,8 @@ export default { <div v-else class="gl-w-full"> <slot name="content"> <dynamic-scroller - v-if="content" - :items="content" + v-if="contentWithKeyField" + :items="contentWithKeyField" :min-item-size="32" :style="{ maxHeight: '170px' }" data-testid="dynamic-content-scroller" @@ -390,6 +410,9 @@ export default { :data="item" :widget-name="widgetName" :level="2" + :row-index="index" + data-testid="extension-list-item" + @clickedAction="onActionClick" /> </dynamic-scroller-item> </template> |