diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-21 02:50:22 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-21 02:50:22 +0300 |
commit | 9dc93a4519d9d5d7be48ff274127136236a3adb3 (patch) | |
tree | 70467ae3692a0e35e5ea56bcb803eb512a10bedb /app/assets/javascripts/vue_shared | |
parent | 4b0f34b6d759d6299322b3a54453e930c6121ff0 (diff) |
Add latest changes from gitlab-org/gitlab@13-11-stable-eev13.11.0-rc43
Diffstat (limited to 'app/assets/javascripts/vue_shared')
56 files changed, 1233 insertions, 731 deletions
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue index f7b49a85b83..3905ce2596c 100644 --- a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue +++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue @@ -21,7 +21,7 @@ import Tracking from '~/tracking'; import initUserPopovers from '~/user_popovers'; import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; -import { SEVERITY_LEVELS } from '../constants'; +import { PAGE_CONFIG, SEVERITY_LEVELS } from '../constants'; import createIssueMutation from '../graphql/mutations/alert_issue_create.mutation.graphql'; import toggleSidebarStatusMutation from '../graphql/mutations/alert_sidebar_status.mutation.graphql'; import alertQuery from '../graphql/queries/alert_details.query.graphql'; @@ -92,6 +92,9 @@ export default { projectIssuesPath: { default: '', }, + statuses: { + default: PAGE_CONFIG.OPERATIONS.STATUSES, + }, trackAlertsDetailsViewsOptions: { default: null, }, @@ -367,7 +370,7 @@ export default { > {{ alert.runbook }} </alert-summary-row> - <alert-details-table :alert="alert" :loading="loading" /> + <alert-details-table :alert="alert" :loading="loading" :statuses="statuses" /> </gl-tab> <gl-tab v-if="!isThreatMonitoringPage" diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue index a01bd462196..554c7a573fe 100644 --- a/app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue +++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue @@ -19,10 +19,6 @@ export default { projectId: { default: '', }, - // TODO remove this limitation in https://gitlab.com/gitlab-org/gitlab/-/issues/296717 - isThreatMonitoringPage: { - default: false, - }, }, props: { alert: { @@ -66,7 +62,6 @@ export default { @alert-error="$emit('alert-error', $event)" /> <sidebar-status - v-if="!isThreatMonitoringPage" :project-path="projectPath" :alert="alert" @toggle-sidebar="$emit('toggle-sidebar')" diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue index 8d5eb24ed1d..672761af1cf 100644 --- a/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue +++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue @@ -3,6 +3,7 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import updateAlertStatusMutation from '~/graphql_shared/mutations/alert_status_update.mutation.graphql'; import { s__ } from '~/locale'; import Tracking from '~/tracking'; +import { PAGE_CONFIG } from '../constants'; export default { i18n: { @@ -11,11 +12,6 @@ export default { ), UPDATE_ALERT_STATUS_INSTRUCTION: s__('AlertManagement|Please try again.'), }, - statuses: { - TRIGGERED: s__('AlertManagement|Triggered'), - ACKNOWLEDGED: s__('AlertManagement|Acknowledged'), - RESOLVED: s__('AlertManagement|Resolved'), - }, components: { GlDropdown, GlDropdownItem, @@ -42,6 +38,11 @@ export default { type: Boolean, required: true, }, + statuses: { + type: Object, + required: false, + default: () => PAGE_CONFIG.OPERATIONS.STATUSES, + }, }, computed: { dropdownClass() { @@ -57,13 +58,13 @@ export default { mutation: updateAlertStatusMutation, variables: { iid: this.alert.iid, - status: status.toUpperCase(), + status, projectPath: this.projectPath, }, }) .then((resp) => { if (this.trackAlertStatusUpdateOptions) { - this.trackStatusUpdate(status); + this.trackStatusUpdate(this.statuses[status]); } const errors = resp.data?.updateAlertStatus?.errors || []; @@ -99,7 +100,7 @@ export default { <gl-dropdown ref="dropdown" right - :text="$options.statuses[alert.status]" + :text="statuses[alert.status]" class="w-100" toggle-class="dropdown-menu-toggle" @keydown.esc.native="$emit('hide-dropdown')" @@ -110,12 +111,12 @@ export default { </p> <div class="dropdown-content dropdown-body"> <gl-dropdown-item - v-for="(label, field) in $options.statuses" + v-for="(label, field) in statuses" :key="field" data-testid="statusDropdownItem" - :active="label.toUpperCase() === alert.status" + :active="field === alert.status" :active-class="'is-active'" - @click="updateAlertStatus(label)" + @click="updateAlertStatus(field)" > {{ label }} </gl-dropdown-item> diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue index 0a2bad5510b..3822b9153a4 100644 --- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue +++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue @@ -1,14 +1,9 @@ <script> import { GlIcon, GlLoadingIcon, GlTooltip, GlSprintf } from '@gitlab/ui'; -import { s__ } from '~/locale'; +import { PAGE_CONFIG } from '../../constants'; import AlertStatus from '../alert_status.vue'; export default { - statuses: { - TRIGGERED: s__('AlertManagement|Triggered'), - ACKNOWLEDGED: s__('AlertManagement|Acknowledged'), - RESOLVED: s__('AlertManagement|Resolved'), - }, components: { GlIcon, GlLoadingIcon, @@ -16,6 +11,11 @@ export default { GlSprintf, AlertStatus, }, + inject: { + statuses: { + default: PAGE_CONFIG.OPERATIONS.STATUSES, + }, + }, props: { projectPath: { type: String, @@ -94,6 +94,7 @@ export default { :project-path="projectPath" :is-dropdown-showing="isDropdownShowing" :is-sidebar="true" + :statuses="statuses" @alert-error="$emit('alert-error', $event)" @hide-dropdown="hideDropdown" @handle-updating="handleUpdating" @@ -103,14 +104,11 @@ export default { <p v-else-if="!isDropdownShowing" class="value gl-m-0" - :class="{ 'no-value': !$options.statuses[alert.status] }" + :class="{ 'no-value': !statuses[alert.status] }" > - <span - v-if="$options.statuses[alert.status]" - class="gl-text-gray-500" - data-testid="status" - >{{ $options.statuses[alert.status] }}</span - > + <span v-if="statuses[alert.status]" class="gl-text-gray-500" data-testid="status"> + {{ statuses[alert.status] }} + </span> <span v-else> {{ s__('AlertManagement|None') }} </span> diff --git a/app/assets/javascripts/vue_shared/alert_details/constants.js b/app/assets/javascripts/vue_shared/alert_details/constants.js index 2ab5160534c..6cc70739eaa 100644 --- a/app/assets/javascripts/vue_shared/alert_details/constants.js +++ b/app/assets/javascripts/vue_shared/alert_details/constants.js @@ -13,6 +13,11 @@ export const SEVERITY_LEVELS = { export const PAGE_CONFIG = { OPERATIONS: { TITLE: 'OPERATIONS', + STATUSES: { + TRIGGERED: s__('AlertManagement|Triggered'), + ACKNOWLEDGED: s__('AlertManagement|Acknowledged'), + RESOLVED: s__('AlertManagement|Resolved'), + }, // Tracks snowplow event when user views alert details TRACK_ALERTS_DETAILS_VIEWS_OPTIONS: { category: 'Alert Management', @@ -27,5 +32,11 @@ export const PAGE_CONFIG = { }, THREAT_MONITORING: { TITLE: 'THREAT_MONITORING', + STATUSES: { + TRIGGERED: s__('ThreatMonitoring|Unreviewed'), + ACKNOWLEDGED: s__('ThreatMonitoring|In review'), + RESOLVED: s__('ThreatMonitoring|Resolved'), + IGNORED: s__('ThreatMonitoring|Dismissed'), + }, }, }; diff --git a/app/assets/javascripts/vue_shared/alert_details/index.js b/app/assets/javascripts/vue_shared/alert_details/index.js index 50f2e63702b..fda405c0fa5 100644 --- a/app/assets/javascripts/vue_shared/alert_details/index.js +++ b/app/assets/javascripts/vue_shared/alert_details/index.js @@ -42,7 +42,8 @@ export default (selector) => { }), }); - apolloProvider.clients.defaultClient.cache.writeData({ + apolloProvider.clients.defaultClient.cache.writeQuery({ + query: sidebarStatusQuery, data: { sidebarStatus: false, }, @@ -54,6 +55,7 @@ export default (selector) => { page, projectIssuesPath, projectId, + statuses: PAGE_CONFIG[page].STATUSES, }; if (page === PAGE_CONFIG.OPERATIONS.TITLE) { diff --git a/app/assets/javascripts/vue_shared/components/alert_details_table.vue b/app/assets/javascripts/vue_shared/components/alert_details_table.vue index 3d49a1cb1c5..a74e9d97143 100644 --- a/app/assets/javascripts/vue_shared/components/alert_details_table.vue +++ b/app/assets/javascripts/vue_shared/components/alert_details_table.vue @@ -7,6 +7,7 @@ import { splitCamelCase, } from '~/lib/utils/text_utility'; import { s__ } from '~/locale'; +import { PAGE_CONFIG } from '~/vue_shared/alert_details/constants'; const thClass = 'gl-bg-transparent! gl-border-1! gl-border-b-solid! gl-border-gray-200!'; const tdClass = 'gl-border-gray-100! gl-p-5!'; @@ -42,6 +43,11 @@ export default { type: Boolean, required: true, }, + statuses: { + type: Object, + required: false, + default: () => PAGE_CONFIG.OPERATIONS.STATUSES, + }, }, fields: [ { @@ -71,6 +77,8 @@ export default { let value; if (fieldName === 'environment') { value = fieldValue?.name; + } else if (fieldName === 'status') { + value = this.statuses[fieldValue] || fieldValue; } else { value = fieldValue; } diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue index 82b3545117f..08d3e163257 100644 --- a/app/assets/javascripts/vue_shared/components/awards_list.vue +++ b/app/assets/javascripts/vue_shared/components/awards_list.vue @@ -44,6 +44,16 @@ export default { required: false, default: () => [], }, + selectedClass: { + type: String, + required: false, + default: 'selected', + }, + }, + data() { + return { + isMenuOpen: false, + }; }, computed: { groupedDefaultAwards() { @@ -68,7 +78,7 @@ export default { methods: { getAwardClassBindings(awardList) { return { - selected: this.hasReactionByCurrentUser(awardList), + [this.selectedClass]: this.hasReactionByCurrentUser(awardList), disabled: this.currentUserId === NO_USER_ID, }; }, @@ -147,6 +157,11 @@ export default { const parsedName = /^[0-9]+$/.test(awardName) ? Number(awardName) : awardName; this.$emit('award', parsedName); + + if (document.activeElement) document.activeElement.blur(); + }, + setIsMenuOpen(menuOpen) { + this.isMenuOpen = menuOpen; }, }, }; @@ -172,8 +187,10 @@ export default { <div v-if="canAwardEmoji" class="award-menu-holder"> <emoji-picker v-if="glFeatures.improvedEmojiPicker" - toggle-class="add-reaction-button gl-relative!" + :toggle-class="['add-reaction-button gl-relative!', { 'is-active': isMenuOpen }]" @click="handleAward" + @shown="setIsMenuOpen(true)" + @hidden="setIsMenuOpen(false)" > <template #button-content> <span class="reaction-control-icon reaction-control-icon-neutral"> diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js b/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js index db61d0f6b05..9c2ed5abf04 100644 --- a/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js +++ b/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js @@ -11,6 +11,16 @@ export default { type: String, required: true, }, + isRawContent: { + type: Boolean, + default: false, + required: false, + }, + fileName: { + type: String, + required: false, + default: '', + }, }, mounted() { eventHub.$emit(SNIPPET_MEASURE_BLOBS_CONTENT); diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue index 5bb31f55e6c..f477610ff1d 100644 --- a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue @@ -1,14 +1,17 @@ <script> /* eslint-disable vue/no-v-html */ import { GlIcon } from '@gitlab/ui'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { HIGHLIGHT_CLASS_NAME } from './constants'; import ViewerMixin from './mixins'; export default { components: { GlIcon, + EditorLite: () => + import(/* webpackChunkName: 'EditorLite' */ '~/vue_shared/components/editor_lite.vue'), }, - mixins: [ViewerMixin], + mixins: [ViewerMixin, glFeatureFlagsMixin()], inject: ['blobHash'], data() { return { @@ -19,6 +22,9 @@ export default { lineNumbers() { return this.content.split('\n').length; }, + refactorBlobViewerEnabled() { + return this.glFeatures.refactorBlobViewer; + }, }, mounted() { const { hash } = window.location; @@ -45,27 +51,31 @@ export default { }; </script> <template> - <div - class="file-content code js-syntax-highlight" - data-qa-selector="file_content" - :class="$options.userColorScheme" - > - <div class="line-numbers"> - <a - v-for="line in lineNumbers" - :id="`L${line}`" - :key="line" - class="diff-line-num js-line-number" - :href="`#LC${line}`" - :data-line-number="line" - @click="scrollToLine(`#LC${line}`)" - > - <gl-icon :size="12" name="link" /> - {{ line }} - </a> - </div> - <div class="blob-content"> - <pre class="code highlight"><code :data-blob-hash="blobHash" v-html="content"></code></pre> + <div> + <editor-lite + v-if="isRawContent && refactorBlobViewerEnabled" + :value="content" + :file-name="fileName" + :editor-options="{ readOnly: true }" + /> + <div v-else class="file-content code js-syntax-highlight" :class="$options.userColorScheme"> + <div class="line-numbers"> + <a + v-for="line in lineNumbers" + :id="`L${line}`" + :key="line" + class="diff-line-num js-line-number" + :href="`#LC${line}`" + :data-line-number="line" + @click="scrollToLine(`#LC${line}`)" + > + <gl-icon :size="12" name="link" /> + {{ line }} + </a> + </div> + <div class="blob-content"> + <pre class="code highlight"><code :data-blob-hash="blobHash" v-html="content"></code></pre> + </div> </div> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue index cd5f63afc79..f14e1992901 100644 --- a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue @@ -56,6 +56,7 @@ export default { <gl-button v-gl-tooltip.hover :title="$options.copyURLTooltip" + :aria-label="$options.copyURLTooltip" :data-clipboard-text="sshLink" data-qa-selector="copy_ssh_url_button" icon="copy-to-clipboard" @@ -75,6 +76,7 @@ export default { <gl-button v-gl-tooltip.hover :title="$options.copyURLTooltip" + :aria-label="$options.copyURLTooltip" :data-clipboard-text="httpLink" data-qa-selector="copy_http_url_button" icon="copy-to-clipboard" diff --git a/app/assets/javascripts/vue_shared/components/delete_label_modal.vue b/app/assets/javascripts/vue_shared/components/delete_label_modal.vue new file mode 100644 index 00000000000..1ff0938d086 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/delete_label_modal.vue @@ -0,0 +1,81 @@ +<script> +import { GlModal, GlSprintf, GlButton } from '@gitlab/ui'; +import { uniqueId } from 'lodash'; + +export default { + components: { + GlModal, + GlSprintf, + GlButton, + }, + props: { + selector: { + type: String, + required: true, + }, + }, + data() { + return { + labelName: '', + subjectName: '', + destroyPath: '', + modalId: uniqueId('modal-delete-label-'), + }; + }, + mounted() { + document.querySelectorAll(this.selector).forEach((button) => { + button.addEventListener('click', (e) => { + e.preventDefault(); + + const { labelName, subjectName, destroyPath } = button.dataset; + this.labelName = labelName; + this.subjectName = subjectName; + this.destroyPath = destroyPath; + this.openModal(); + }); + }); + }, + methods: { + openModal() { + this.$refs.modal.show(); + }, + closeModal() { + this.$refs.modal.hide(); + }, + }, +}; +</script> + +<template> + <gl-modal ref="modal" :modal-id="modalId"> + <template #modal-title> + <gl-sprintf :message="__('Delete label: %{labelName}')"> + <template #labelName> + {{ labelName }} + </template> + </gl-sprintf> + </template> + <gl-sprintf + :message=" + __( + `%{strongStart}${labelName}%{strongEnd} will be permanently deleted from ${subjectName}. This cannot be undone.`, + ) + " + > + <template #strong="{ content }"> + <strong>{{ content }}</strong> + </template> + </gl-sprintf> + <template #modal-footer> + <gl-button category="secondary" @click="closeModal">{{ __('Cancel') }}</gl-button> + <gl-button + category="primary" + variant="danger" + :href="destroyPath" + data-method="delete" + data-testid="delete-button" + >{{ __('Delete label') }}</gl-button + > + </template> + </gl-modal> +</template> diff --git a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue deleted file mode 100644 index 3f55f43edbb..00000000000 --- a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue +++ /dev/null @@ -1,146 +0,0 @@ -<script> -/* eslint-disable vue/require-default-prop */ -import { __ } from '~/locale'; - -export default { - name: 'DeprecatedModal', // use GlModal instead - - props: { - id: { - type: String, - required: false, - }, - title: { - type: String, - required: false, - }, - text: { - type: String, - required: false, - }, - hideFooter: { - type: Boolean, - required: false, - default: false, - }, - kind: { - type: String, - required: false, - default: 'primary', - }, - modalDialogClass: { - type: String, - required: false, - default: '', - }, - closeKind: { - type: String, - required: false, - default: 'default', - }, - closeButtonLabel: { - type: String, - required: false, - default: __('Cancel'), - }, - primaryButtonLabel: { - type: String, - required: false, - default: '', - }, - secondaryButtonLabel: { - type: String, - required: false, - default: '', - }, - submitDisabled: { - type: Boolean, - required: false, - default: false, - }, - }, - - computed: { - btnKindClass() { - return { - [`btn-${this.kind}`]: true, - }; - }, - btnCancelKindClass() { - return { - [`btn-${this.closeKind}`]: true, - }; - }, - }, - - methods: { - emitCancel(event) { - this.$emit('cancel', event); - }, - emitSubmit(event) { - this.$emit('submit', event); - }, - }, -}; -</script> - -<template> - <div class="modal-open"> - <div :id="id" :class="id ? '' : 'd-block'" class="modal" role="dialog" tabindex="-1"> - <div :class="modalDialogClass" class="modal-dialog" role="document"> - <div class="modal-content"> - <div class="modal-header"> - <slot name="header"> - <h4 class="modal-title float-left">{{ title }}</h4> - <button - type="button" - class="close float-right" - data-dismiss="modal" - :aria-label="__('Close')" - @click="emitCancel($event)" - > - <span aria-hidden="true">×</span> - </button> - </slot> - </div> - <div class="modal-body"> - <slot :text="text" name="body"> - <p>{{ text }}</p> - </slot> - </div> - <div v-if="!hideFooter" class="modal-footer"> - <button - :class="btnCancelKindClass" - type="button" - class="btn" - data-dismiss="modal" - @click="emitCancel($event)" - > - {{ closeButtonLabel }} - </button> - - <slot v-if="secondaryButtonLabel" name="secondary-button"> - <button v-if="secondaryButtonLabel" type="button" class="btn" data-dismiss="modal"> - {{ secondaryButtonLabel }} - </button> - </slot> - - <button - v-if="primaryButtonLabel" - :disabled="submitDisabled" - :class="btnKindClass" - type="button" - class="btn js-primary-button" - data-dismiss="modal" - data-qa-selector="save_changes_button" - @click="emitSubmit($event)" - > - {{ primaryButtonLabel }} - </button> - </div> - </div> - </div> - </div> - <div v-if="!id" class="modal-backdrop fade show"></div> - </div> -</template> diff --git a/app/assets/javascripts/vue_shared/components/file_finder/index.vue b/app/assets/javascripts/vue_shared/components/file_finder/index.vue index 4ec54b33bce..fbadb202d51 100644 --- a/app/assets/javascripts/vue_shared/components/file_finder/index.vue +++ b/app/assets/javascripts/vue_shared/components/file_finder/index.vue @@ -3,6 +3,7 @@ import { GlIcon } from '@gitlab/ui'; import fuzzaldrinPlus from 'fuzzaldrin-plus'; import Mousetrap from 'mousetrap'; import VirtualList from 'vue-virtual-scroll-list'; +import { keysFor, MR_GO_TO_FILE } from '~/behaviors/shortcuts/keybindings'; import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; import Item from './item.vue'; @@ -128,7 +129,7 @@ export default { this.focusedIndex = 0; } - Mousetrap.bind(['t', 'mod+p'], (e) => { + Mousetrap.bind(keysFor(MR_GO_TO_FILE), (e) => { if (e.preventDefault) { e.preventDefault(); } diff --git a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js index f7cfb59be01..e622b505570 100644 --- a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js +++ b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js @@ -128,6 +128,7 @@ const fileExtensionIcons = { c: 'c', m: 'c', h: 'h', + 'c++': 'cpp', cc: 'cpp', cpp: 'cpp', mm: 'cpp', @@ -402,14 +403,15 @@ const fileNameIcons = { 'gradle.properties': 'gradle', gradlew: 'gradle', 'gradle-wrapper.properties': 'gradle', - license: 'certificate', - 'license.md': 'certificate', - 'license.md.rendered': 'certificate', - 'license.txt': 'certificate', - licence: 'certificate', - 'licence.md': 'certificate', - 'licence.md.rendered': 'certificate', - 'licence.txt': 'certificate', + COPYING: 'certificate', + 'COPYING.LESSER': 'certificate', + LICENSE: 'certificate', + LICENCE: 'certificate', + 'LICENSE.md': 'certificate', + 'LICENCE.md': 'certificate', + 'LICENSE.txt': 'certificate', + 'LICENCE.txt': 'certificate', + '.gitlab-license': 'certificate', dockerfile: 'docker', 'docker-compose.yml': 'docker', '.mailmap': 'email', diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue index 97a8f681faf..107ced550c1 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue @@ -58,7 +58,7 @@ export default { type: String, required: false, default: '', - validator: (value) => value === '' || /(_desc)|(_asc)/g.test(value), + validator: (value) => value === '' || /(_desc)|(_asc)/gi.test(value), }, showCheckbox: { type: Boolean, @@ -363,6 +363,7 @@ export default { <gl-button v-gl-tooltip :title="sortDirectionTooltip" + :aria-label="sortDirectionTooltip" :icon="sortDirectionIcon" class="flex-shrink-1" @click="handleSortDirectionClick" diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue index d53c829a48e..aeb698a3adb 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue @@ -45,6 +45,9 @@ export default { activeAuthor() { return this.authors.find((author) => author.username.toLowerCase() === this.currentValue); }, + activeAuthorAvatar() { + return this.avatarUrl(this.activeAuthor); + }, }, watch: { active: { @@ -74,6 +77,9 @@ export default { this.loading = false; }); }, + avatarUrl(author) { + return author.avatarUrl || author.avatar_url; + }, searchAuthors: debounce(function debouncedSearch({ data }) { this.fetchAuthorBySearchTerm(data); }, DEBOUNCE_DELAY), @@ -92,7 +98,7 @@ export default { <gl-avatar v-if="activeAuthor" :size="16" - :src="activeAuthor.avatar_url" + :src="activeAuthorAvatar" shape="circle" class="gl-mr-2" /> @@ -115,7 +121,7 @@ export default { :value="author.username" > <div class="d-flex"> - <gl-avatar :size="32" :src="author.avatar_url" /> + <gl-avatar :size="32" :src="avatarUrl(author)" /> <div> <div>{{ author.name }}</div> <div>@{{ author.username }}</div> diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue new file mode 100644 index 00000000000..98190d716c9 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue @@ -0,0 +1,105 @@ +<script> +import { + GlFilteredSearchToken, + GlFilteredSearchSuggestion, + GlDropdownDivider, + GlLoadingIcon, +} from '@gitlab/ui'; +import { debounce } from 'lodash'; + +import { deprecatedCreateFlash as createFlash } from '~/flash'; +import { __ } from '~/locale'; + +import { DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY, DEBOUNCE_DELAY } from '../constants'; +import { stripQuotes } from '../filtered_search_utils'; + +export default { + components: { + GlFilteredSearchToken, + GlFilteredSearchSuggestion, + GlDropdownDivider, + GlLoadingIcon, + }, + props: { + config: { + type: Object, + required: true, + }, + value: { + type: Object, + required: true, + }, + }, + data() { + return { + emojis: this.config.initialEmojis || [], + defaultEmojis: this.config.defaultEmojis || [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY], + loading: true, + }; + }, + computed: { + currentValue() { + return this.value.data.toLowerCase(); + }, + activeEmoji() { + return this.emojis.find( + (emoji) => emoji.name.toLowerCase() === stripQuotes(this.currentValue), + ); + }, + }, + methods: { + fetchEmojiBySearchTerm(searchTerm) { + this.loading = true; + this.config + .fetchEmojis(searchTerm) + .then((res) => { + this.emojis = Array.isArray(res) ? res : res.data; + }) + .catch(() => createFlash(__('There was a problem fetching emojis.'))) + .finally(() => { + this.loading = false; + }); + }, + searchEmojis: debounce(function debouncedSearch({ data }) { + this.fetchEmojiBySearchTerm(data); + }, DEBOUNCE_DELAY), + }, +}; +</script> + +<template> + <gl-filtered-search-token + :config="config" + v-bind="{ ...$props, ...$attrs }" + v-on="$listeners" + @input="searchEmojis" + > + <template #view="{ inputValue }"> + <gl-emoji v-if="activeEmoji" :data-name="activeEmoji.name" /> + <span v-else>{{ inputValue }}</span> + </template> + <template #suggestions> + <gl-filtered-search-suggestion + v-for="emoji in defaultEmojis" + :key="emoji.value" + :value="emoji.value" + > + {{ emoji.value }} + </gl-filtered-search-suggestion> + <gl-dropdown-divider v-if="defaultEmojis.length" /> + <gl-loading-icon v-if="loading" /> + <template v-else> + <gl-filtered-search-suggestion + v-for="emoji in emojis" + :key="emoji.name" + :value="emoji.name" + > + <div class="gl-display-flex"> + <gl-emoji :data-name="emoji.name" /> + <span class="gl-ml-3">{{ emoji.name }}</span> + </div> + </gl-filtered-search-suggestion> + </template> + </template> + </gl-filtered-search-token> +</template> diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue new file mode 100644 index 00000000000..101c7150c55 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue @@ -0,0 +1,133 @@ +<script> +import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui'; +import { debounce } from 'lodash'; + +import createFlash from '~/flash'; +import { isNumeric } from '~/lib/utils/number_utils'; +import { __ } from '~/locale'; +import { DEBOUNCE_DELAY } from '../constants'; +import { stripQuotes } from '../filtered_search_utils'; + +export default { + components: { + GlFilteredSearchToken, + GlFilteredSearchSuggestion, + GlLoadingIcon, + }, + props: { + config: { + type: Object, + required: true, + }, + value: { + type: Object, + required: true, + }, + }, + data() { + return { + epics: this.config.initialEpics || [], + loading: true, + }; + }, + computed: { + currentValue() { + /* + * When the URL contains the epic_iid, we'd get: '123' + */ + if (isNumeric(this.value.data)) { + return parseInt(this.value.data, 10); + } + + /* + * When the token is added in current session it'd be: 'Foo::&123' + */ + const id = this.value.data.split('::&')[1]; + + if (id) { + return parseInt(id, 10); + } + + return this.value.data; + }, + activeEpic() { + const currentValueIsString = typeof this.currentValue === 'string'; + return this.epics.find( + (epic) => epic[currentValueIsString ? 'title' : 'iid'] === this.currentValue, + ); + }, + }, + watch: { + active: { + immediate: true, + handler(newValue) { + if (!newValue && !this.epics.length) { + this.searchEpics({ data: this.currentValue }); + } + }, + }, + }, + methods: { + fetchEpicsBySearchTerm(searchTerm = '') { + this.loading = true; + this.config + .fetchEpics(searchTerm) + .then(({ data }) => { + this.epics = data; + }) + .catch(() => createFlash({ message: __('There was a problem fetching epics.') })) + .finally(() => { + this.loading = false; + }); + }, + fetchSingleEpic(iid) { + this.loading = true; + this.config + .fetchSingleEpic(iid) + .then(({ data }) => { + this.epics = [data]; + }) + .catch(() => createFlash({ message: __('There was a problem fetching epics.') })) + .finally(() => { + this.loading = false; + }); + }, + searchEpics: debounce(function debouncedSearch({ data }) { + if (isNumeric(data)) { + return this.fetchSingleEpic(data); + } + return this.fetchEpicsBySearchTerm(data); + }, DEBOUNCE_DELAY), + + getEpicValue(epic) { + return `${epic.title}::&${epic.iid}`; + }, + }, + stripQuotes, +}; +</script> + +<template> + <gl-filtered-search-token + :config="config" + v-bind="{ ...$props, ...$attrs }" + v-on="$listeners" + @input="searchEpics" + > + <template #view="{ inputValue }"> + <span>{{ activeEpic ? getEpicValue(activeEpic) : $options.stripQuotes(inputValue) }}</span> + </template> + <template #suggestions> + <gl-loading-icon v-if="loading" /> + <template v-else> + <gl-filtered-search-suggestion + v-for="epic in epics" + :key="epic.id" + :value="getEpicValue(epic)" + > + <div>{{ epic.title }}</div> + </gl-filtered-search-suggestion> + </template> + </template> + </gl-filtered-search-token> +</template> diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue index 9c2a644b7a9..76b005772ec 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue @@ -46,7 +46,7 @@ export default { }, activeLabel() { return this.labels.find( - (label) => label.title.toLowerCase() === stripQuotes(this.currentValue), + (label) => this.getLabelName(label).toLowerCase() === stripQuotes(this.currentValue), ); }, containerStyle() { @@ -69,6 +69,21 @@ export default { }, }, methods: { + /** + * There's an inconsistency between private and public API + * for labels where label name is included in a different + * property; + * + * Private API => `label.title` + * Public API => `label.name` + * + * This method allows compatibility as there may be instances + * where `config.fetchLabels` provided externally may still be + * using either of the two APIs. + */ + getLabelName(label) { + return label.name || label.title; + }, fetchLabelBySearchTerm(searchTerm) { this.loading = true; this.config @@ -85,7 +100,7 @@ export default { }); }, searchLabels: debounce(function debouncedSearch({ data }) { - this.fetchLabelBySearchTerm(data); + if (!this.loading) this.fetchLabelBySearchTerm(data); }, DEBOUNCE_DELAY), }, }; @@ -100,7 +115,7 @@ export default { > <template #view-token="{ inputValue, cssClasses, listeners }"> <gl-token variant="search-value" :class="cssClasses" :style="containerStyle" v-on="listeners" - >~{{ activeLabel ? activeLabel.title : inputValue }}</gl-token + >~{{ activeLabel ? getLabelName(activeLabel) : inputValue }}</gl-token > </template> <template #suggestions> @@ -114,13 +129,17 @@ export default { <gl-dropdown-divider v-if="defaultLabels.length" /> <gl-loading-icon v-if="loading" /> <template v-else> - <gl-filtered-search-suggestion v-for="label in labels" :key="label.id" :value="label.title"> - <div class="gl-display-flex"> + <gl-filtered-search-suggestion + v-for="label in labels" + :key="label.id" + :value="getLabelName(label)" + > + <div class="gl-display-flex gl-align-items-center"> <span :style="{ backgroundColor: label.color }" class="gl-display-inline-block mr-2 p-2" ></span> - <div>{{ label.title }}</div> + <div>{{ getLabelName(label) }}</div> </div> </gl-filtered-search-suggestion> </template> diff --git a/app/assets/javascripts/vue_shared/components/gl_toggle_vuex.vue b/app/assets/javascripts/vue_shared/components/gl_toggle_vuex.vue deleted file mode 100644 index b649dac029a..00000000000 --- a/app/assets/javascripts/vue_shared/components/gl_toggle_vuex.vue +++ /dev/null @@ -1,49 +0,0 @@ -<script> -import { GlToggle } from '@gitlab/ui'; -import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; - -export default { - name: 'GlToggleVuex', - components: { - GlToggle, - }, - props: { - stateProperty: { - type: String, - required: true, - }, - storeModule: { - type: String, - required: false, - default: null, - }, - setAction: { - type: String, - required: false, - default() { - return `set${capitalizeFirstCharacter(this.stateProperty)}`; - }, - }, - }, - computed: { - value: { - get() { - const { state } = this.$store; - const { stateProperty, storeModule } = this; - return storeModule ? state[storeModule][stateProperty] : state[stateProperty]; - }, - set(value) { - const { stateProperty, storeModule, setAction } = this; - const action = storeModule ? `${storeModule}/${setAction}` : setAction; - this.$store.dispatch(action, { key: stateProperty, value }); - }, - }, - }, -}; -</script> - -<template> - <gl-toggle v-model="value"> - <slot v-bind="{ value }"></slot> - </gl-toggle> -</template> diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index b4cac13168a..f169921d8a6 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -121,13 +121,7 @@ export default { :title="user.email" class="js-user-link commit-committer-link" > - <user-avatar-image - :img-src="avatarUrl" - :img-alt="userAvatarAltText" - :tooltip-text="user.name" - :img-size="24" - /> - + <user-avatar-image :img-src="avatarUrl" :img-alt="userAvatarAltText" :size="24" /> {{ user.name }} </gl-link> <gl-tooltip v-if="message" :target="() => $refs[$options.EMOJI_REF]"> diff --git a/app/assets/javascripts/vue_shared/components/help_popover.vue b/app/assets/javascripts/vue_shared/components/help_popover.vue index 051c65bae70..f36b9107a6e 100644 --- a/app/assets/javascripts/vue_shared/components/help_popover.vue +++ b/app/assets/javascripts/vue_shared/components/help_popover.vue @@ -1,5 +1,5 @@ <script> -import { GlButton, GlPopover } from '@gitlab/ui'; +import { GlButton, GlPopover, GlSafeHtmlDirective } from '@gitlab/ui'; /** * Render a button with a question mark icon @@ -11,6 +11,9 @@ export default { GlButton, GlPopover, }, + directives: { + SafeHtml: GlSafeHtmlDirective, + }, props: { options: { type: Object, @@ -22,15 +25,13 @@ export default { </script> <template> <span> - <gl-button ref="popoverTrigger" variant="link" icon="question" tabindex="0" /> - <gl-popover triggers="hover focus" :target="() => $refs.popoverTrigger.$el" v-bind="options"> - <template #title> - <!-- eslint-disable-next-line vue/no-v-html --> - <span v-html="options.title"></span> + <gl-button ref="popoverTrigger" variant="link" icon="question" :aria-label="__('Help')" /> + <gl-popover :target="() => $refs.popoverTrigger.$el" v-bind="options"> + <template v-if="options.title" #title> + <span v-safe-html="options.title"></span> </template> <template #default> - <!-- eslint-disable-next-line vue/no-v-html --> - <div v-html="options.content"></div> + <div v-safe-html="options.content"></div> </template> </gl-popover> </span> diff --git a/app/assets/javascripts/vue_shared/components/lib/utils/props_utils.js b/app/assets/javascripts/vue_shared/components/lib/utils/props_utils.js new file mode 100644 index 00000000000..b115b1fb34b --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/lib/utils/props_utils.js @@ -0,0 +1,35 @@ +/** + * Return the union of the given components' props options. Required props take + * precendence over non-required props of the same name. + * + * This makes two assumptions: + * - All given components define their props in verbose object format. + * - The components all agree on the `type` of a common prop. + * + * @param {object[]} components The components to derive the union from. + * @returns {object} The union of the props of the given components. + */ +export const propsUnion = (components) => + components.reduce((acc, component) => { + Object.entries(component.props ?? {}).forEach(([propName, propOptions]) => { + if (process.env.NODE_ENV !== 'production') { + if (typeof propOptions !== 'object' || !('type' in propOptions)) { + throw new Error( + `Cannot create props union: expected verbose prop options for prop "${propName}"`, + ); + } + + if (propName in acc && acc[propName]?.type !== propOptions?.type) { + throw new Error( + `Cannot create props union: incompatible prop types for prop "${propName}"`, + ); + } + } + + if (!(propName in acc) || propOptions.required) { + acc[propName] = propOptions; + } + }); + + return acc; + }, {}); diff --git a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue index 10887aee689..90ac20fe748 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue @@ -34,6 +34,7 @@ export default { boundary="window" right menu-class="gl-w-full!" + data-qa-selector="apply_suggestion_button" @shown="$refs.commitMessage.$el.focus()" > <gl-dropdown-form class="gl-px-4! gl-m-0!"> @@ -44,12 +45,14 @@ export default { v-model="message" :placeholder="defaultCommitMessage" submit-on-enter + data-qa-selector="commit_message_textbox" @submit="onApply" /> <gl-button class="gl-w-auto! gl-mt-3 gl-text-center! gl-hover-text-white! gl-transition-medium! float-right" category="primary" variant="success" + data-qa-selector="commit_with_custom_message_button" @click="onApply" > {{ __('Apply') }} diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 25d01dc550f..80b7a9b7d05 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -62,6 +62,11 @@ export default { required: false, default: true, }, + uploadsPath: { + type: String, + required: false, + default: '', + }, enableAutocomplete: { type: Boolean, required: false, @@ -72,6 +77,11 @@ export default { required: false, default: null, }, + lines: { + type: Array, + required: false, + default: () => [], + }, note: { type: Object, required: false, @@ -110,6 +120,20 @@ export default { return this.referencedUsers.length >= referencedUsersThreshold; }, lineContent() { + if (this.lines.length) { + return this.lines + .map((line) => { + const { rich_text: richText, text } = line; + + if (text) { + return text; + } + + return unescape(stripHtml(richText).replace(/\n/g, '')); + }) + .join('\\n'); + } + if (this.line) { const { rich_text: richText, text } = this.line; @@ -144,6 +168,9 @@ export default { false, ); }, + suggestionsStartIndex() { + return Math.max(this.lines.length - 1, 0); + }, }, watch: { isSubmitting(isSubmitting) { @@ -229,12 +256,14 @@ export default { ref="gl-form" :class="{ 'gl-mt-3 gl-mb-3': addSpacingClasses }" class="js-vue-markdown-field md-area position-relative gfm-form" + :data-uploads-path="uploadsPath" > <markdown-header :preview-markdown="previewMarkdown" :line-content="lineContent" :can-suggest="canSuggest" :show-suggest-popover="showSuggestPopover" + :suggestion-start-index="suggestionsStartIndex" @preview-markdown="showPreviewTab" @write-markdown="showWriteTab" @handleSuggestDismissed="() => $emit('handleSuggestDismissed')" diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 5bc1786d692..01cf0beea3a 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -1,6 +1,7 @@ <script> import { GlPopover, GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import $ from 'jquery'; +import { keysFor, BOLD_TEXT, ITALIC_TEXT, LINK_TEXT } from '~/behaviors/shortcuts/keybindings'; import { getSelectedFragment } from '~/lib/utils/common_utils'; import { s__ } from '~/locale'; import { CopyAsGFM } from '../../../behaviors/markdown/copy_as_gfm'; @@ -36,6 +37,11 @@ export default { required: false, default: false, }, + suggestionStartIndex: { + type: Number, + required: false, + default: 0, + }, }, data() { return { @@ -53,7 +59,9 @@ export default { ].join('\n'); }, mdSuggestion() { - return ['```suggestion:-0+0', `{text}`, '```'].join('\n'); + return [['```', `suggestion:-${this.suggestionStartIndex}+0`].join(''), `{text}`, '```'].join( + '\n', + ); }, isMac() { // Accessing properties using ?. to allow tests to use @@ -116,6 +124,11 @@ export default { .catch(() => {}); }, }, + shortcuts: { + bold: keysFor(BOLD_TEXT), + italic: keysFor(ITALIC_TEXT), + link: keysFor(LINK_TEXT), + }, }; </script> @@ -143,7 +156,7 @@ export default { :button-title=" sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { modifierKey }) " - shortcuts="mod+b" + :shortcuts="$options.shortcuts.bold" icon="bold" /> <toolbar-button @@ -151,7 +164,7 @@ export default { :button-title=" sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { modifierKey }) " - shortcuts="mod+i" + :shortcuts="$options.shortcuts.italic" icon="italic" /> <toolbar-button @@ -208,7 +221,7 @@ export default { :button-title=" sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { modifierKey }) " - shortcuts="mod+k" + :shortcuts="$options.shortcuts.link" icon="link" /> </div> diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue index 7c28e74e256..83b8a6ae562 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue @@ -1,13 +1,11 @@ <script> import { GlButton, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import ApplySuggestion from './apply_suggestion.vue'; export default { components: { GlIcon, GlButton, GlLoadingIcon, ApplySuggestion }, directives: { 'gl-tooltip': GlTooltipDirective }, - mixins: [glFeatureFlagsMixin()], props: { batchSuggestionsCount: { type: Number, @@ -59,9 +57,6 @@ export default { }; }, computed: { - canBeBatched() { - return Boolean(this.glFeatures.batchSuggestions); - }, isApplying() { return this.isApplyingSingle || this.isApplyingBatch; }, @@ -118,7 +113,7 @@ export default { <gl-loading-icon class="d-flex-center mr-2" /> <span>{{ applyingSuggestionsMessage }}</span> </div> - <div v-else-if="canApply && canBeBatched && isBatched" class="d-flex align-items-center"> + <div v-else-if="canApply && isBatched" class="d-flex align-items-center"> <gl-button class="btn-inverted js-remove-from-batch-btn btn-grouped" :disabled="isApplying" @@ -142,7 +137,7 @@ export default { </div> <div v-else class="d-flex align-items-center"> <gl-button - v-if="suggestionsCount > 1 && canBeBatched && !isDisableButton" + v-if="suggestionsCount > 1 && !isDisableButton" class="btn-inverted js-add-to-batch-btn btn-grouped" data-qa-selector="add_suggestion_batch_button" :disabled="isDisableButton" @@ -152,6 +147,7 @@ export default { </gl-button> <apply-suggestion v-if="isLoggedIn" + v-gl-tooltip.viewport="tooltipMessage" :disabled="isDisableButton" :default-commit-message="defaultCommitMessage" class="gl-ml-3" diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue index 387b100a04f..7393a8791b7 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue @@ -1,13 +1,18 @@ <script> import { GlButton, GlLink, GlLoadingIcon, GlSprintf, GlIcon } from '@gitlab/ui'; +import { isExperimentVariant } from '~/experimentation/utils'; +import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue'; +import { INVITE_MEMBERS_IN_COMMENT } from '~/invite_members/constants'; export default { + inviteMembersInComment: INVITE_MEMBERS_IN_COMMENT, components: { GlButton, GlLink, GlLoadingIcon, GlSprintf, GlIcon, + InviteMembersTrigger, }, props: { markdownDocsPath: { @@ -29,6 +34,9 @@ export default { hasQuickActionsDocsPath() { return this.quickActionsDocsPath !== ''; }, + inviteCommentEnabled() { + return isExperimentVariant(INVITE_MEMBERS_IN_COMMENT, 'invite_member_link'); + }, }, }; </script> @@ -37,9 +45,9 @@ export default { <div class="comment-toolbar clearfix"> <div class="toolbar-text"> <template v-if="!hasQuickActionsDocsPath && markdownDocsPath"> - <gl-link :href="markdownDocsPath" target="_blank">{{ - __('Markdown is supported') - }}</gl-link> + <gl-link :href="markdownDocsPath" target="_blank"> + {{ __('Markdown is supported') }} + </gl-link> </template> <template v-if="hasQuickActionsDocsPath && markdownDocsPath"> <gl-sprintf @@ -59,6 +67,16 @@ export default { </template> </div> <span v-if="canAttachFile" class="uploading-container"> + <invite-members-trigger + v-if="inviteCommentEnabled" + classes="gl-mr-3 gl-vertical-align-text-bottom" + :display-text="s__('InviteMember|Invite Member')" + icon="assignee" + variant="link" + :track-experiment="$options.inviteMembersInComment" + :trigger-source="$options.inviteMembersInComment" + data-track-event="comment_invite_click" + /> <span class="uploading-progress-container hide"> <gl-icon name="media" /> <span class="attaching-file-message"></span> diff --git a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue index 7b36d57dfbf..38afd56bae6 100644 --- a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue +++ b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue @@ -101,6 +101,7 @@ export default { :data-clipboard-target="target" :data-clipboard-text="text" :title="title" + :aria-label="title" :category="category" icon="copy-to-clipboard" /> diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index 50972a8c32c..149909d263e 100644 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -28,6 +28,7 @@ import { import $ from 'jquery'; import { mapGetters, mapActions, mapState } from 'vuex'; import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history'; +import { __ } from '~/locale'; import initMRPopovers from '~/mr_popover/'; import noteHeader from '~/notes/components/note_header.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -37,6 +38,9 @@ import TimelineEntryItem from './timeline_entry_item.vue'; const MAX_VISIBLE_COMMIT_LIST_COUNT = 3; export default { + i18n: { + deleteButtonLabel: __('Remove description history'), + }, name: 'SystemNote', components: { GlIcon, @@ -139,7 +143,8 @@ export default { <gl-button v-if="displayDeleteButton" v-gl-tooltip - :title="__('Remove description history')" + :title="$options.i18n.deleteButtonLabel" + :aria-label="$options.i18n.deleteButtonLabel" variant="default" category="tertiary" icon="remove" diff --git a/app/assets/javascripts/vue_shared/components/oncall_schedules_list.vue b/app/assets/javascripts/vue_shared/components/oncall_schedules_list.vue new file mode 100644 index 00000000000..ff2847624c5 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/oncall_schedules_list.vue @@ -0,0 +1,70 @@ +<script> +import { GlSprintf, GlLink } from '@gitlab/ui'; +import { sprintf, s__ } from '~/locale'; + +export default { + components: { + GlSprintf, + GlLink, + }, + props: { + schedules: { + type: Array, + required: true, + }, + userName: { + type: String, + required: false, + default: null, + }, + isCurrentUser: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + title() { + return this.isCurrentUser + ? s__('OnCallSchedules|You are currently a part of:') + : sprintf(s__('OnCallSchedules|User %{name} is currently part of:'), { + name: this.userName, + }); + }, + footer() { + return this.isCurrentUser + ? s__( + 'OnCallSchedules|Removing yourself may put your on-call team at risk of missing a notification.', + ) + : s__( + 'OnCallSchedules|Removing this user may put their on-call team at risk of missing a notification.', + ); + }, + }, +}; +</script> + +<template> + <div> + <p data-testid="title">{{ title }}</p> + + <ul data-testid="schedules-list"> + <li v-for="(schedule, index) in schedules" :key="`${schedule.name}-${index}`"> + <gl-sprintf + :message="s__('OnCallSchedules|On-call schedule %{schedule} in Project %{project}')" + > + <template #schedule> + <gl-link :href="schedule.scheduleUrl" target="_blank">{{ schedule.name }}</gl-link> + </template> + <template #project> + <gl-link :href="schedule.projectUrl" target="_blank">{{ + schedule.projectName + }}</gl-link> + </template> + </gl-sprintf> + </li> + </ul> + + <p data-testid="footer">{{ footer }}</p> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_eventhub.js b/app/assets/javascripts/vue_shared/components/recaptcha_eventhub.js deleted file mode 100644 index e193883b6e9..00000000000 --- a/app/assets/javascripts/vue_shared/components/recaptcha_eventhub.js +++ /dev/null @@ -1,21 +0,0 @@ -import createEventHub from '~/helpers/event_hub_factory'; - -// see recaptcha_tags in app/views/shared/_recaptcha_form.html.haml -export const callbackName = 'recaptchaDialogCallback'; - -export const eventHub = createEventHub(); - -const throwDuplicateCallbackError = () => { - throw new Error(`${callbackName} is already defined!`); -}; - -if (window[callbackName]) { - throwDuplicateCallbackError(); -} - -const callback = () => eventHub.$emit('submit'); - -Object.defineProperty(window, callbackName, { - get: () => callback, - set: throwDuplicateCallbackError, -}); diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue deleted file mode 100644 index fc1f3675a3d..00000000000 --- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue +++ /dev/null @@ -1,90 +0,0 @@ -<script> -/* eslint-disable vue/no-v-html */ -import DeprecatedModal from './deprecated_modal.vue'; -import { eventHub } from './recaptcha_eventhub'; - -export default { - name: 'RecaptchaModal', - - components: { - DeprecatedModal, - }, - - props: { - html: { - type: String, - required: false, - default: '', - }, - }, - - data() { - return { - script: {}, - scriptSrc: 'https://www.recaptcha.net/recaptcha/api.js', - }; - }, - - watch: { - html() { - this.appendRecaptchaScript(); - }, - }, - - mounted() { - eventHub.$on('submit', this.submit); - - if (this.html) { - this.appendRecaptchaScript(); - } - }, - - beforeDestroy() { - eventHub.$off('submit', this.submit); - }, - - methods: { - appendRecaptchaScript() { - this.removeRecaptchaScript(); - - const script = document.createElement('script'); - script.src = this.scriptSrc; - script.classList.add('js-recaptcha-script'); - script.async = true; - script.defer = true; - - this.script = script; - - document.body.appendChild(script); - }, - - removeRecaptchaScript() { - if (this.script instanceof Element) this.script.remove(); - }, - - close() { - this.removeRecaptchaScript(); - this.$emit('close'); - }, - - submit() { - this.$el.querySelector('form').submit(); - }, - }, -}; -</script> - -<template> - <deprecated-modal - :hide-footer="true" - :title="__('Please solve the reCAPTCHA')" - kind="warning" - class="recaptcha-modal js-recaptcha-modal" - @cancel="close" - > - <div slot="body"> - <p>{{ __('We want to be sure it is you, please confirm you are not a robot.') }}</p> - <div ref="recaptcha" v-html="html"></div> - </div> - </deprecated-modal> -</template> diff --git a/app/assets/javascripts/vue_shared/components/registry/registry_search.vue b/app/assets/javascripts/vue_shared/components/registry/registry_search.vue index 62453a25f62..0825c3a76ea 100644 --- a/app/assets/javascripts/vue_shared/components/registry/registry_search.vue +++ b/app/assets/javascripts/vue_shared/components/registry/registry_search.vue @@ -1,5 +1,6 @@ <script> import { GlSorting, GlSortingItem, GlFilteredSearch } from '@gitlab/ui'; +import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants'; const ASCENDING_ORDER = 'asc'; const DESCENDING_ORDER = 'desc'; @@ -45,18 +46,60 @@ export default { isSortAscending() { return this.sorting.sort === ASCENDING_ORDER; }, + baselineQueryStringFilters() { + return this.tokens.reduce((acc, curr) => { + acc[curr.type] = ''; + return acc; + }, {}); + }, }, methods: { + generateQueryData({ sorting = {}, filter = [] } = {}) { + // Ensure that we clean up the query when we remove a token from the search + const result = { ...this.baselineQueryStringFilters, ...sorting, search: [] }; + + filter.forEach((f) => { + if (f.type === FILTERED_SEARCH_TERM) { + result.search.push(f.value.data); + } else { + result[f.type] = f.value.data; + } + }); + return result; + }, onDirectionChange() { const sort = this.isSortAscending ? DESCENDING_ORDER : ASCENDING_ORDER; + const newQueryString = this.generateQueryData({ + sorting: { ...this.sorting, sort }, + filter: this.filter, + }); this.$emit('sorting:changed', { sort }); + this.$emit('query:changed', newQueryString); }, onSortItemClick(item) { + const newQueryString = this.generateQueryData({ + sorting: { ...this.sorting, orderBy: item }, + filter: this.filter, + }); this.$emit('sorting:changed', { orderBy: item }); + this.$emit('query:changed', newQueryString); + }, + submitSearch() { + const newQueryString = this.generateQueryData({ + sorting: this.sorting, + filter: this.filter, + }); + this.$emit('filter:submit'); + this.$emit('query:changed', newQueryString); }, clearSearch() { + const newQueryString = this.generateQueryData({ + sorting: this.sorting, + }); + this.$emit('filter:changed', []); this.$emit('filter:submit'); + this.$emit('query:changed', newQueryString); }, }, }; @@ -69,7 +112,7 @@ export default { class="gl-mr-4 gl-flex-fill-1" :placeholder="__('Filter results')" :available-tokens="tokens" - @submit="$emit('filter:submit')" + @submit="submitSearch" @clear="clearSearch" /> <gl-sorting diff --git a/app/assets/javascripts/vue_shared/components/remove_member_modal.vue b/app/assets/javascripts/vue_shared/components/remove_member_modal.vue index 88d1b15aee3..dff3a6a8c3f 100644 --- a/app/assets/javascripts/vue_shared/components/remove_member_modal.vue +++ b/app/assets/javascripts/vue_shared/components/remove_member_modal.vue @@ -1,8 +1,10 @@ <script> import { GlFormCheckbox, GlModal } from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; import { parseBoolean } from '~/lib/utils/common_utils'; import csrf from '~/lib/utils/csrf'; -import { __ } from '~/locale'; +import { s__, __ } from '~/locale'; +import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue'; export default { actionCancel: { @@ -12,6 +14,7 @@ export default { components: { GlFormCheckbox, GlModal, + OncallSchedulesList, }, data() { return { @@ -22,8 +25,20 @@ export default { isAccessRequest() { return parseBoolean(this.modalData.isAccessRequest); }, + isInvite() { + return parseBoolean(this.modalData.isInvite); + }, + isGroupMember() { + return this.modalData.memberType === 'GroupMember'; + }, actionText() { - return this.isAccessRequest ? __('Deny access request') : __('Remove member'); + if (this.isAccessRequest) { + return __('Deny access request'); + } else if (this.isInvite) { + return s__('Member|Revoke invite'); + } + + return __('Remove member'); }, actionPrimary() { return { @@ -33,6 +48,21 @@ export default { }, }; }, + showUnassignIssuablesCheckbox() { + return !this.isAccessRequest && !this.isInvite; + }, + isPartOfOncallSchedules() { + return !this.isAccessRequest && this.oncallSchedules.schedules?.length; + }, + oncallSchedules() { + let schedules = {}; + try { + schedules = JSON.parse(this.modalData.oncallSchedules); + } catch (e) { + Sentry.captureException(e); + } + return schedules; + }, }, mounted() { document.addEventListener('click', this.handleClick); @@ -68,9 +98,18 @@ export default { <form ref="form" :action="modalData.memberPath" method="post"> <p data-testid="modal-message">{{ modalData.message }}</p> + <oncall-schedules-list + v-if="isPartOfOncallSchedules" + :schedules="oncallSchedules.schedules" + :user-name="oncallSchedules.name" + /> + <input ref="method" type="hidden" name="_method" value="delete" /> <input :value="$options.csrf.token" type="hidden" name="authenticity_token" /> - <gl-form-checkbox v-if="!isAccessRequest" name="unassign_issuables"> + <gl-form-checkbox v-if="isGroupMember" name="remove_sub_memberships"> + {{ __('Also remove direct user membership from subgroups and projects') }} + </gl-form-checkbox> + <gl-form-checkbox v-if="showUnassignIssuablesCheckbox" name="unassign_issuables"> {{ __('Also unassign this user from related issues and merge requests') }} </gl-form-checkbox> </form> diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_item.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_item.vue index 4271f6053ed..85a67c087bb 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_item.vue +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_item.vue @@ -21,7 +21,11 @@ export default { }; </script> <template> - <button v-gl-tooltip="{ title: tooltip }" class="p-0 gl-display-flex toolbar-button"> + <button + v-gl-tooltip="{ title: tooltip }" + :aria-label="tooltip" + class="p-0 gl-display-flex toolbar-button" + > <gl-icon class="gl-mx-auto gl-align-self-center" :name="icon" /> </button> </template> diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql index ff0626167a9..76f152e5453 100644 --- a/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql +++ b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql @@ -1,4 +1,4 @@ -query getRunnerPlatforms($projectPath: ID!, $groupPath: ID!) { +query getRunnerPlatforms { runnerPlatforms { nodes { name @@ -11,10 +11,4 @@ query getRunnerPlatforms($projectPath: ID!, $groupPath: ID!) { } } } - project(fullPath: $projectPath) { - id - } - group(fullPath: $groupPath) { - id - } } diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql index 643c1991807..c0248a35e3f 100644 --- a/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql +++ b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql @@ -1,15 +1,5 @@ -query runnerSetupInstructions( - $platform: String! - $architecture: String! - $projectId: ID! - $groupId: ID! -) { - runnerSetup( - platform: $platform - architecture: $architecture - projectId: $projectId - groupId: $groupId - ) { +query runnerSetupInstructions($platform: String!, $architecture: String!) { + runnerSetup(platform: $platform, architecture: $architecture) { installInstructions registerInstructions } diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue index 1d6db576942..d886a67fff7 100644 --- a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue +++ b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue @@ -1,155 +1,31 @@ <script> -import { - GlAlert, - GlButton, - GlModal, - GlModalDirective, - GlButtonGroup, - GlDropdown, - GlDropdownItem, - GlIcon, -} from '@gitlab/ui'; -import { __, s__ } from '~/locale'; -import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue'; -import { - PLATFORMS_WITHOUT_ARCHITECTURES, - INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES, -} from './constants'; -import getRunnerPlatforms from './graphql/queries/get_runner_platforms.query.graphql'; -import getRunnerSetupInstructions from './graphql/queries/get_runner_setup.query.graphql'; +import { GlButton, GlModalDirective } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import RunnerInstructionsModal from './runner_instructions_modal.vue'; export default { components: { - GlAlert, GlButton, - GlButtonGroup, - GlDropdown, - GlDropdownItem, - GlModal, - GlIcon, - ModalCopyButton, + RunnerInstructionsModal, }, directives: { GlModalDirective, }, - inject: { - projectPath: { - default: '', - }, - groupPath: { - default: '', - }, - }, - apollo: { - runnerPlatforms: { - query: getRunnerPlatforms, - variables() { - return { - projectPath: this.projectPath, - groupPath: this.groupPath, - }; - }, - error() { - this.showAlert = true; - }, - result({ data }) { - this.project = data?.project; - this.group = data?.group; - - this.selectPlatform(this.platforms[0].name); - }, - }, + modalId: 'runner-instructions-modal', + i18n: { + buttonText: s__('Runners|Show Runner installation instructions'), }, data() { return { - showAlert: false, - selectedPlatformArchitectures: [], - selectedPlatform: { - name: '', - }, - selectedArchitecture: {}, - runnerPlatforms: {}, - instructions: {}, - project: {}, - group: {}, + opened: false, }; }, - computed: { - isPlatformSelected() { - return Object.keys(this.selectedPlatform).length > 0; - }, - instructionsEmpty() { - return Object.keys(this.instructions).length === 0; - }, - groupId() { - return this.group?.id ?? ''; - }, - projectId() { - return this.project?.id ?? ''; - }, - platforms() { - return this.runnerPlatforms?.nodes; - }, - hasArchitecureList() { - return !PLATFORMS_WITHOUT_ARCHITECTURES.includes(this.selectedPlatform?.name); - }, - instructionsWithoutArchitecture() { - return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatform.name]?.instructions; - }, - runnerInstallationLink() { - return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatform.name]?.link; - }, - }, methods: { - selectPlatform(name) { - this.selectedPlatform = this.platforms.find((platform) => platform.name === name); - if (this.hasArchitecureList) { - this.selectedPlatformArchitectures = this.selectedPlatform?.architectures?.nodes; - [this.selectedArchitecture] = this.selectedPlatformArchitectures; - this.selectArchitecture(this.selectedArchitecture); - } - }, - selectArchitecture(architecture) { - this.selectedArchitecture = architecture; - - this.$apollo.addSmartQuery('instructions', { - variables() { - return { - platform: this.selectedPlatform.name, - architecture: this.selectedArchitecture.name, - projectId: this.projectId, - groupId: this.groupId, - }; - }, - query: getRunnerSetupInstructions, - update(data) { - return data?.runnerSetup; - }, - error() { - this.showAlert = true; - }, - }); - }, - toggleAlert(state) { - this.showAlert = state; + onClick() { + // lazily mount modal to prevent premature instructions requests + this.opened = true; }, }, - modalId: 'installation-instructions-modal', - i18n: { - installARunner: s__('Runners|Install a Runner'), - architecture: s__('Runners|Architecture'), - downloadInstallBinary: s__('Runners|Download and Install Binary'), - downloadLatestBinary: s__('Runners|Download Latest Binary'), - registerRunner: s__('Runners|Register Runner'), - method: __('Method'), - fetchError: s__('Runners|An error has occurred fetching instructions'), - instructions: s__('Runners|Show Runner installation instructions'), - copyInstructions: s__('Runners|Copy instructions'), - }, - closeButton: { - text: __('Close'), - attributes: [{ variant: 'default' }], - }, }; </script> <template> @@ -158,104 +34,10 @@ export default { v-gl-modal-directive="$options.modalId" class="gl-mt-4" data-testid="show-modal-button" + @click="onClick" > - {{ $options.i18n.instructions }} + {{ $options.i18n.buttonText }} </gl-button> - <gl-modal - :modal-id="$options.modalId" - :title="$options.i18n.installARunner" - :action-secondary="$options.closeButton" - > - <gl-alert v-if="showAlert" variant="danger" @dismiss="toggleAlert(false)"> - {{ $options.i18n.fetchError }} - </gl-alert> - <h5>{{ __('Environment') }}</h5> - <gl-button-group class="gl-mb-5"> - <gl-button - v-for="platform in platforms" - :key="platform.name" - data-testid="platform-button" - @click="selectPlatform(platform.name)" - > - {{ platform.humanReadableName }} - </gl-button> - </gl-button-group> - <template v-if="hasArchitecureList"> - <template v-if="isPlatformSelected"> - <h5> - {{ $options.i18n.architecture }} - </h5> - <gl-dropdown class="gl-mb-5" :text="selectedArchitecture.name"> - <gl-dropdown-item - v-for="architecture in selectedPlatformArchitectures" - :key="architecture.name" - data-testid="architecture-dropdown-item" - @click="selectArchitecture(architecture)" - > - {{ architecture.name }} - </gl-dropdown-item> - </gl-dropdown> - <div class="gl-display-flex gl-align-items-center gl-mb-5"> - <h5>{{ $options.i18n.downloadInstallBinary }}</h5> - <gl-button - class="gl-ml-auto" - :href="selectedArchitecture.downloadLocation" - download - data-testid="binary-download-button" - > - {{ $options.i18n.downloadLatestBinary }} - </gl-button> - </div> - </template> - <template v-if="!instructionsEmpty"> - <div class="gl-display-flex"> - <pre - class="gl-bg-gray gl-flex-fill-1 gl-white-space-pre-line" - data-testid="binary-instructions" - > - - {{ instructions.installInstructions }} - </pre - > - <modal-copy-button - :title="$options.i18n.copyInstructions" - :text="instructions.installInstructions" - :modal-id="$options.modalId" - css-classes="gl-align-self-start gl-ml-2 gl-mt-2" - category="tertiary" - /> - </div> - - <hr /> - <h5 class="gl-mb-5">{{ $options.i18n.registerRunner }}</h5> - <h5 class="gl-mb-5">{{ $options.i18n.method }}</h5> - <div class="gl-display-flex"> - <pre - class="gl-bg-gray gl-flex-fill-1 gl-white-space-pre-line" - data-testid="runner-instructions" - > - {{ instructions.registerInstructions }} - </pre - > - <modal-copy-button - :title="$options.i18n.copyInstructions" - :text="instructions.registerInstructions" - :modal-id="$options.modalId" - css-classes="gl-align-self-start gl-ml-2 gl-mt-2" - category="tertiary" - /> - </div> - </template> - </template> - <template v-else> - <div> - <p>{{ instructionsWithoutArchitecture }}</p> - <gl-button :href="runnerInstallationLink"> - <gl-icon name="external-link" /> - {{ s__('Runners|View installation instructions') }} - </gl-button> - </div> - </template> - </gl-modal> + <runner-instructions-modal v-if="opened" :modal-id="$options.modalId" /> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue new file mode 100644 index 00000000000..795b4f58ac5 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue @@ -0,0 +1,249 @@ +<script> +import { + GlAlert, + GlButton, + GlModal, + GlButtonGroup, + GlDropdown, + GlDropdownItem, + GlIcon, + GlLoadingIcon, + GlSkeletonLoader, +} from '@gitlab/ui'; +import { isEmpty } from 'lodash'; +import { __, s__ } from '~/locale'; +import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue'; +import { + PLATFORMS_WITHOUT_ARCHITECTURES, + INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES, +} from './constants'; +import getRunnerPlatformsQuery from './graphql/queries/get_runner_platforms.query.graphql'; +import getRunnerSetupInstructionsQuery from './graphql/queries/get_runner_setup.query.graphql'; + +export default { + components: { + GlAlert, + GlButton, + GlButtonGroup, + GlDropdown, + GlDropdownItem, + GlModal, + GlIcon, + GlLoadingIcon, + GlSkeletonLoader, + ModalCopyButton, + }, + props: { + modalId: { + type: String, + required: true, + }, + }, + apollo: { + platforms: { + query: getRunnerPlatformsQuery, + update(data) { + return data?.runnerPlatforms?.nodes.map(({ name, humanReadableName, architectures }) => { + return { + name, + humanReadableName, + architectures: architectures?.nodes || [], + }; + }); + }, + result() { + // Select first platform by default + if (this.platforms?.[0]) { + this.selectPlatform(this.platforms[0]); + } + }, + error() { + this.toggleAlert(true); + }, + }, + instructions: { + query: getRunnerSetupInstructionsQuery, + skip() { + return !this.selectedPlatform; + }, + variables() { + return { + platform: this.selectedPlatformName, + architecture: this.selectedArchitectureName || '', + }; + }, + update(data) { + return data?.runnerSetup; + }, + error() { + this.toggleAlert(true); + }, + }, + }, + data() { + return { + platforms: [], + selectedPlatform: null, + selectedArchitecture: null, + showAlert: false, + instructions: {}, + }; + }, + computed: { + platformsEmpty() { + return isEmpty(this.platforms); + }, + instructionsEmpty() { + return isEmpty(this.instructions); + }, + selectedPlatformName() { + return this.selectedPlatform?.name; + }, + selectedArchitectureName() { + return this.selectedArchitecture?.name; + }, + hasArchitecureList() { + return !PLATFORMS_WITHOUT_ARCHITECTURES.includes(this.selectedPlatformName); + }, + instructionsWithoutArchitecture() { + return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatformName]?.instructions; + }, + runnerInstallationLink() { + return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatformName]?.link; + }, + }, + methods: { + selectPlatform(platform) { + this.selectedPlatform = platform; + + if (!platform.architectures?.some(({ name }) => name === this.selectedArchitectureName)) { + // Select first architecture when current value is not available + this.selectArchitecture(platform.architectures[0]); + } + }, + selectArchitecture(architecture) { + this.selectedArchitecture = architecture; + }, + toggleAlert(state) { + this.showAlert = state; + }, + }, + i18n: { + installARunner: s__('Runners|Install a runner'), + architecture: s__('Runners|Architecture'), + downloadInstallBinary: s__('Runners|Download and install binary'), + downloadLatestBinary: s__('Runners|Download latest binary'), + registerRunnerCommand: s__('Runners|Command to register runner'), + fetchError: s__('Runners|An error has occurred fetching instructions'), + copyInstructions: s__('Runners|Copy instructions'), + }, + closeButton: { + text: __('Close'), + attributes: [{ variant: 'default' }], + }, +}; +</script> +<template> + <gl-modal + :modal-id="modalId" + :title="$options.i18n.installARunner" + :action-secondary="$options.closeButton" + > + <gl-alert v-if="showAlert" variant="danger" @dismiss="toggleAlert(false)"> + {{ $options.i18n.fetchError }} + </gl-alert> + + <gl-skeleton-loader v-if="platformsEmpty && $apollo.loading" /> + + <template v-if="!platformsEmpty"> + <h5> + {{ __('Environment') }} + </h5> + <gl-button-group class="gl-mb-3"> + <gl-button + v-for="platform in platforms" + :key="platform.name" + :selected="selectedPlatform && selectedPlatform.name === platform.name" + data-testid="platform-button" + @click="selectPlatform(platform)" + > + {{ platform.humanReadableName }} + </gl-button> + </gl-button-group> + </template> + <template v-if="hasArchitecureList"> + <template v-if="selectedPlatform"> + <h5> + {{ $options.i18n.architecture }} + <gl-loading-icon v-if="$apollo.loading" inline /> + </h5> + + <gl-dropdown class="gl-mb-3" :text="selectedArchitectureName"> + <gl-dropdown-item + v-for="architecture in selectedPlatform.architectures" + :key="architecture.name" + :is-check-item="true" + :is-checked="selectedArchitectureName === architecture.name" + data-testid="architecture-dropdown-item" + @click="selectArchitecture(architecture)" + > + {{ architecture.name }} + </gl-dropdown-item> + </gl-dropdown> + <div class="gl-display-flex gl-align-items-center gl-mb-3"> + <h5>{{ $options.i18n.downloadInstallBinary }}</h5> + <gl-button + class="gl-ml-auto" + :href="selectedArchitecture.downloadLocation" + download + icon="download" + data-testid="binary-download-button" + > + {{ $options.i18n.downloadLatestBinary }} + </gl-button> + </div> + </template> + <template v-if="!instructionsEmpty"> + <div class="gl-display-flex"> + <pre + class="gl-bg-gray gl-flex-fill-1 gl-white-space-pre-line" + data-testid="binary-instructions" + >{{ instructions.installInstructions }}</pre + > + <modal-copy-button + :title="$options.i18n.copyInstructions" + :text="instructions.installInstructions" + :modal-id="$options.modalId" + css-classes="gl-align-self-start gl-ml-2 gl-mt-2" + category="tertiary" + /> + </div> + + <h5 class="gl-mb-3">{{ $options.i18n.registerRunnerCommand }}</h5> + <div class="gl-display-flex"> + <pre + class="gl-bg-gray gl-flex-fill-1 gl-white-space-pre-line" + data-testid="register-command" + >{{ instructions.registerInstructions }}</pre + > + <modal-copy-button + :title="$options.i18n.copyInstructions" + :text="instructions.registerInstructions" + :modal-id="$options.modalId" + css-classes="gl-align-self-start gl-ml-2 gl-mt-2" + category="tertiary" + /> + </div> + </template> + </template> + <template v-else> + <div> + <p>{{ instructionsWithoutArchitecture }}</p> + <gl-button :href="runnerInstallationLink"> + <gl-icon name="external-link" /> + {{ s__('Runners|View installation instructions') }} + </gl-button> + </div> + </template> + </gl-modal> +</template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/copyable_field.vue b/app/assets/javascripts/vue_shared/components/sidebar/copyable_field.vue new file mode 100644 index 00000000000..bbc7e6e7a6e --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/copyable_field.vue @@ -0,0 +1,88 @@ +<script> +import { GlLoadingIcon } from '@gitlab/ui'; +import { s__, __, sprintf } from '~/locale'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; + +/** + * Renders an inline field, whose value can be copied to the clipboard, + * for use in the GitLab sidebar (issues, MRs, etc.). + */ +export default { + name: 'CopyableField', + components: { + GlLoadingIcon, + ClipboardButton, + }, + props: { + value: { + type: String, + required: true, + }, + name: { + type: String, + required: true, + }, + isLoading: { + type: Boolean, + required: false, + default: false, + }, + clipboardTooltipText: { + type: String, + required: false, + default: undefined, + }, + }, + computed: { + clipboardProps() { + return { + category: 'tertiary', + tooltipBoundary: 'viewport', + tooltipPlacement: 'left', + text: this.value, + title: + this.clipboardTooltipText || + sprintf(this.$options.i18n.clipboardTooltip, { name: this.name }), + }; + }, + loadingIconLabel() { + return sprintf(this.$options.i18n.loadingIconLabel, { name: this.name }); + }, + templateText() { + return sprintf(this.$options.i18n.templateText, { + name: this.name, + value: this.value, + }); + }, + }, + i18n: { + loadingIconLabel: __('Loading %{name}'), + clipboardTooltip: __('Copy %{name}'), + templateText: s__('Sidebar|%{name}: %{value}'), + }, +}; +</script> + +<template> + <div> + <clipboard-button + v-if="!isLoading" + css-class="sidebar-collapsed-icon dont-change-state gl-rounded-0! gl-hover-bg-transparent" + v-bind="clipboardProps" + /> + + <div + class="gl-display-flex gl-align-items-center gl-justify-content-space-between hide-collapsed" + > + <span + class="gl-overflow-hidden gl-text-overflow-ellipsis gl-white-space-nowrap" + :title="value" + > + {{ templateText }} + </span> + + <gl-loading-icon v-if="isLoading" inline :label="loadingIconLabel" /> + <clipboard-button v-else size="small" v-bind="clipboardProps" /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue b/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue index 1d3bd312b09..320e2048f1c 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue @@ -164,6 +164,7 @@ export default { variant="link" icon="close" class="gl-mr-2 gl-w-auto! gl-p-2!" + :aria-label="__('Close')" @click.prevent="handleDropdownCloseClick" /> </div> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue index 426ae430ce7..f547433f322 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue @@ -172,9 +172,11 @@ export default { after: this.handleVuexActionDispatch, }); + document.addEventListener('mousedown', this.handleDocumentMousedown); document.addEventListener('click', this.handleDocumentClick); }, beforeDestroy() { + document.removeEventListener('mousedown', this.handleDocumentMousedown); document.removeEventListener('click', this.handleDocumentClick); }, methods: { @@ -197,11 +199,36 @@ export default { } }, /** + * This method stores a mousedown event's target. + * Required by the click listener because the click + * event itself has no reference to this element. + */ + handleDocumentMousedown({ target }) { + this.mousedownTarget = target; + }, + /** * This method listens for document-wide click event * and toggle dropdown if user clicks anywhere outside * the dropdown while dropdown is visible. */ handleDocumentClick({ target }) { + // We also perform the toggle exception check for the + // last mousedown event's target to avoid hiding the + // box when the mousedown happened inside the box and + // only the mouseup did not. + if ( + this.showDropdownContents && + !this.preventDropdownToggleOnClick(target) && + !this.preventDropdownToggleOnClick(this.mousedownTarget) + ) { + this.toggleDropdownContents(); + } + }, + /** + * This method checks whether a given click target + * should prevent the dropdown from being toggled. + */ + preventDropdownToggleOnClick(target) { // This approach of element detection is needed // as the dropdown wrapper is not using `GlDropdown` as // it will also require us to use `BDropdownForm` @@ -216,19 +243,20 @@ export default { target?.parentElement?.classList.contains(className), ); - const hadExceptionParent = ['.js-btn-back', '.js-labels-list'].some( + const hasExceptionParent = ['.js-btn-back', '.js-labels-list'].some( (className) => $(target).parents(className).length, ); - if ( - this.showDropdownContents && - !hadExceptionParent && - !hasExceptionClass && - !this.$refs.dropdownButtonCollapsed?.$el.contains(target) && - !this.$refs.dropdownContents?.$el.contains(target) - ) { - this.toggleDropdownContents(); - } + const isInDropdownButtonCollapsed = this.$refs.dropdownButtonCollapsed?.$el.contains(target); + + const isInDropdownContents = this.$refs.dropdownContents?.$el.contains(target); + + return ( + hasExceptionClass || + hasExceptionParent || + isInDropdownButtonCollapsed || + isInDropdownContents + ); }, handleDropdownClose(labels) { // Only emit label updates if there are any labels to update diff --git a/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue b/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue index ef5f052527b..17904f20341 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/multiselect_dropdown.vue @@ -30,5 +30,8 @@ export default { <gl-dropdown-form> <slot name="items"></slot> </gl-dropdown-form> + <template #footer> + <slot name="footer"></slot> + </template> </gl-dropdown> </template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql index 459ea27e9cd..3885127fa8e 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql +++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql @@ -1,4 +1,5 @@ #import "~/graphql_shared/fragments/user.fragment.graphql" +#import "~/graphql_shared/fragments/user_availability.fragment.graphql" query issueParticipants($fullPath: ID!, $iid: String!) { workspace: project(fullPath: $fullPath) { @@ -9,11 +10,13 @@ query issueParticipants($fullPath: ID!, $iid: String!) { participants { nodes { ...User + ...UserAvailability } } assignees { nodes { ...User + ...UserAvailability } } } diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql index 43bd9f17e9a..63482873b69 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql +++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql @@ -1,4 +1,5 @@ #import "~/graphql_shared/fragments/user.fragment.graphql" +#import "~/graphql_shared/fragments/user_availability.fragment.graphql" query getMrParticipants($fullPath: ID!, $iid: String!) { workspace: project(fullPath: $fullPath) { @@ -7,11 +8,13 @@ query getMrParticipants($fullPath: ID!, $iid: String!) { participants { nodes { ...User + ...UserAvailability } } assignees { nodes { ...User + ...UserAvailability } } } diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql index 8ee8de2cb5c..3f40c0368d7 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql +++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql @@ -1,4 +1,5 @@ #import "~/graphql_shared/fragments/user.fragment.graphql" +#import "~/graphql_shared/fragments/user_availability.fragment.graphql" mutation issueSetAssignees($iid: String!, $assigneeUsernames: [String!]!, $fullPath: ID!) { issuableSetAssignees: issueSetAssignees( @@ -9,11 +10,13 @@ mutation issueSetAssignees($iid: String!, $assigneeUsernames: [String!]!, $fullP assignees { nodes { ...User + ...UserAvailability } } participants { nodes { ...User + ...UserAvailability } } } diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql index a0f15a07692..77140ea36d8 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql +++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql @@ -1,4 +1,5 @@ #import "~/graphql_shared/fragments/user.fragment.graphql" +#import "~/graphql_shared/fragments/user_availability.fragment.graphql" mutation mergeRequestSetAssignees($iid: String!, $assigneeUsernames: [String!]!, $fullPath: ID!) { mergeRequestSetAssignees( @@ -9,11 +10,13 @@ mutation mergeRequestSetAssignees($iid: String!, $assigneeUsernames: [String!]!, assignees { nodes { ...User + ...UserAvailability } } participants { nodes { ...User + ...UserAvailability } } } diff --git a/app/assets/javascripts/vue_shared/components/url_sync.vue b/app/assets/javascripts/vue_shared/components/url_sync.vue index 2844d9e9e94..925c6008836 100644 --- a/app/assets/javascripts/vue_shared/components/url_sync.vue +++ b/app/assets/javascripts/vue_shared/components/url_sync.vue @@ -2,11 +2,18 @@ import { historyPushState } from '~/lib/utils/common_utils'; import { mergeUrlParams } from '~/lib/utils/url_utility'; +/** + * Renderless component to update the query string, + * the update is done by updating the query property or + * by using updateQuery method in the scoped slot. + * note: do not use both prop and updateQuery method. + */ export default { props: { query: { type: Object, - required: true, + required: false, + default: null, }, }, watch: { @@ -14,12 +21,19 @@ export default { immediate: true, deep: true, handler(newQuery) { - historyPushState(mergeUrlParams(newQuery, window.location.href, { spreadArrays: true })); + if (newQuery) { + this.updateQuery(newQuery); + } }, }, }, + methods: { + updateQuery(newQuery) { + historyPushState(mergeUrlParams(newQuery, window.location.href, { spreadArrays: true })); + }, + }, render() { - return this.$slots.default; + return this.$scopedSlots.default?.({ updateQuery: this.updateQuery }); }, }; </script> diff --git a/app/assets/javascripts/vue_shared/components/user_date.vue b/app/assets/javascripts/vue_shared/components/user_date.vue new file mode 100644 index 00000000000..38dddbf72c2 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/user_date.vue @@ -0,0 +1,29 @@ +<script> +import { formatDate } from '~/lib/utils/datetime_utility'; +import { __ } from '~/locale'; +import { SHORT_DATE_FORMAT } from '../constants'; + +export default { + props: { + date: { + type: String, + required: false, + default: null, + }, + }, + computed: { + formattedDate() { + const { date } = this; + if (date === null) { + return __('Never'); + } + return formatDate(new Date(date), SHORT_DATE_FORMAT); + }, + }, +}; +</script> +<template> + <span> + {{ formattedDate }} + </span> +</template> diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue index dbd8efec948..11f484b2cdf 100644 --- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue +++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue @@ -1,11 +1,6 @@ <script> /* eslint-disable vue/no-v-html */ -import { - GlPopover, - GlLink, - GlDeprecatedSkeletonLoading as GlSkeletonLoading, - GlIcon, -} from '@gitlab/ui'; +import { GlPopover, GlLink, GlSkeletonLoader, GlIcon } from '@gitlab/ui'; import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue'; import { glEmojiTag } from '../../../emoji'; import UserAvatarImage from '../user_avatar/user_avatar_image.vue'; @@ -19,7 +14,7 @@ export default { GlIcon, GlLink, GlPopover, - GlSkeletonLoading, + GlSkeletonLoader, UserAvatarImage, UserNameWithStatus, }, @@ -60,20 +55,18 @@ export default { <template> <!-- 200ms delay so not every mouseover triggers Popover --> - <gl-popover :target="target" :delay="200" boundary="viewport" triggers="hover" placement="top"> + <gl-popover :target="target" :delay="200" boundary="viewport" placement="top"> <div class="gl-p-3 gl-line-height-normal gl-display-flex" data-testid="user-popover"> <div class="gl-p-2 flex-shrink-1"> <user-avatar-image :img-src="user.avatarUrl" :size="60" css-classes="gl-mr-3!" /> </div> - <div class="gl-p-2 gl-w-full"> + <div class="gl-p-2 gl-w-full gl-min-w-0"> <template v-if="userIsLoading"> - <!-- `gl-skeleton-loading` does not support equal length lines --> - <!-- This can be migrated to `gl-skeleton-loader` when https://gitlab.com/gitlab-org/gitlab-ui/-/issues/872 is completed --> - <gl-skeleton-loading - v-for="n in $options.maxSkeletonLines" - :key="n" - :lines="1" - class="animation-container-small gl-mb-2" + <gl-skeleton-loader + :lines="$options.maxSkeletonLines" + preserve-aspect-ratio="none" + equal-width-lines + :height="52" /> </template> <template v-else> diff --git a/app/assets/javascripts/vue_shared/constants.js b/app/assets/javascripts/vue_shared/constants.js index 5262a15136b..9a5ad195de9 100644 --- a/app/assets/javascripts/vue_shared/constants.js +++ b/app/assets/javascripts/vue_shared/constants.js @@ -8,6 +8,8 @@ const INTERVALS = { export const FILE_SYMLINK_MODE = '120000'; +export const SHORT_DATE_FORMAT = 'd mmm, yyyy'; + export const timeRanges = [ { label: __('30 minutes'), diff --git a/app/assets/javascripts/vue_shared/mixins/recaptcha_modal_implementor.js b/app/assets/javascripts/vue_shared/mixins/recaptcha_modal_implementor.js deleted file mode 100644 index ff1f565e79a..00000000000 --- a/app/assets/javascripts/vue_shared/mixins/recaptcha_modal_implementor.js +++ /dev/null @@ -1,36 +0,0 @@ -import recaptchaModal from '../components/recaptcha_modal.vue'; - -export default { - data() { - return { - showRecaptcha: false, - recaptchaHTML: '', - }; - }, - - components: { - recaptchaModal, - }, - - methods: { - openRecaptcha() { - this.showRecaptcha = true; - }, - - closeRecaptcha() { - this.showRecaptcha = false; - }, - - checkForSpam(data) { - if (!data.recaptcha_html) return data; - - this.recaptchaHTML = data.recaptcha_html; - - const spamError = new Error(data.error_message); - spamError.name = 'SpamError'; - spamError.message = 'SpamError'; - - throw spamError; - }, - }, -}; diff --git a/app/assets/javascripts/vue_shared/security_reports/queries/security_report_download_paths.query.graphql b/app/assets/javascripts/vue_shared/security_reports/queries/security_report_download_paths.query.graphql index 310d8d88904..4ce13827da2 100644 --- a/app/assets/javascripts/vue_shared/security_reports/queries/security_report_download_paths.query.graphql +++ b/app/assets/javascripts/vue_shared/security_reports/queries/security_report_download_paths.query.graphql @@ -6,6 +6,7 @@ query securityReportDownloadPaths( project(fullPath: $projectPath) { mergeRequest(iid: $iid) { headPipeline { + id jobs(securityReportTypes: $reportTypes) { nodes { name diff --git a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue index b27dd33835f..1151cffa76f 100644 --- a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue +++ b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue @@ -184,6 +184,7 @@ export default { :has-issues="false" class="mr-widget-border-top mr-report" data-testid="security-mr-widget" + track-action="users_expanding_secure_security_report" > <template v-for="slot in $options.summarySlots" #[slot]> <span :key="slot"> @@ -212,6 +213,7 @@ export default { :has-issues="false" class="mr-widget-border-top mr-report" data-testid="security-mr-widget" + track-action="users_expanding_secure_security_report" > <template #error> {{ $options.i18n.scansHaveRun }} |