diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-22 15:10:30 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-22 15:10:30 +0300 |
commit | 49203bfa3c7eb607a7561ae7da9b5c52aa49fd77 (patch) | |
tree | f33cd54ec9a45d69a3e58fe93735070d3b718913 /app | |
parent | 3c9a2dd62025043448c9ea9a6df86422874ee4be (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
19 files changed, 496 insertions, 103 deletions
diff --git a/app/assets/javascripts/admin/abuse_report/components/abuse_report_notes.vue b/app/assets/javascripts/admin/abuse_report/components/abuse_report_notes.vue index 80af7d7400a..29de7e1ad1d 100644 --- a/app/assets/javascripts/admin/abuse_report/components/abuse_report_notes.vue +++ b/app/assets/javascripts/admin/abuse_report/components/abuse_report_notes.vue @@ -6,6 +6,7 @@ import SkeletonLoadingContainer from '~/vue_shared/components/notes/skeleton_not import { SKELETON_NOTES_COUNT } from '~/admin/abuse_report/constants'; import abuseReportNotesQuery from '../graphql/notes/abuse_report_notes.query.graphql'; import AbuseReportDiscussion from './notes/abuse_report_discussion.vue'; +import AbuseReportAddNote from './notes/abuse_report_add_note.vue'; export default { name: 'AbuseReportNotes', @@ -16,6 +17,7 @@ export default { components: { SkeletonLoadingContainer, AbuseReportDiscussion, + AbuseReportAddNote, }, props: { abuseReportId: { @@ -60,6 +62,9 @@ export default { const discussionId = discussion.notes.nodes[0].id; return discussionId.split('/')[discussionId.split('/').length - 1]; }, + updateKey() { + this.addNoteKey = uniqueId(`abuse-report-add-note-${this.abuseReportId}`); + }, }, }; </script> @@ -86,6 +91,16 @@ export default { :abuse-report-id="abuseReportId" /> </ul> + <div class="js-comment-form"> + <ul class="notes notes-form timeline"> + <abuse-report-add-note + :key="addNoteKey" + :is-new-discussion="true" + :abuse-report-id="abuseReportId" + @cancelEditing="updateKey" + /> + </ul> + </div> </template> </div> </div> diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_add_note.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_add_note.vue new file mode 100644 index 00000000000..3c709fc565f --- /dev/null +++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_add_note.vue @@ -0,0 +1,138 @@ +<script> +import { sprintf, __ } from '~/locale'; +import { createAlert } from '~/alert'; +import { clearDraft } from '~/lib/utils/autosave'; +import createNoteMutation from '../../graphql/notes/create_abuse_report_note.mutation.graphql'; +import AbuseReportCommentForm from './abuse_report_comment_form.vue'; + +export default { + name: 'AbuseReportAddNote', + i18n: { + reply: __('Reply'), + replyToComment: __('Reply to comment'), + commentError: __('Your comment could not be submitted because %{reason}.'), + genericError: __( + 'Your comment could not be submitted! Please check your network connection and try again.', + ), + }, + components: { + AbuseReportCommentForm, + }, + props: { + abuseReportId: { + type: String, + required: true, + }, + discussionId: { + type: String, + required: false, + default: '', + }, + isNewDiscussion: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + isEditing: this.isNewDiscussion, + isSubmitting: false, + }; + }, + computed: { + autosaveKey() { + // eslint-disable-next-line @gitlab/require-i18n-strings + return this.discussionId ? `${this.discussionId}-comment` : `${this.abuseReportId}-comment`; + }, + timelineEntryClasses() { + return this.isNewDiscussion + ? 'timeline-entry note-form' + : // eslint-disable-next-line @gitlab/require-i18n-strings + 'note note-wrapper note-comment discussion-reply-holder gl-border-t-0! clearfix'; + }, + timelineEntryInnerClasses() { + return this.isNewDiscussion ? 'timeline-entry-inner' : ''; + }, + commentFormWrapperClasses() { + return !this.isEditing + ? 'gl-relative gl-display-flex gl-align-items-flex-start gl-flex-nowrap' + : ''; + }, + }, + methods: { + async addNote({ commentText }) { + this.isSubmitting = true; + + this.$apollo + .mutate({ + mutation: createNoteMutation, + variables: { + input: { + noteableId: this.abuseReportId, + body: commentText, + discussionId: this.discussionId || null, + }, + }, + }) + .then(() => { + clearDraft(this.autosaveKey); + this.cancelEditing(); + }) + .catch((error) => { + const errorMessage = error?.message + ? sprintf(this.$options.i18n.commentError, { reason: error.message.toLowerCase() }) + : this.$options.i18n.genericError; + + createAlert({ + message: errorMessage, + parent: this.$el, + captureError: true, + }); + }) + .finally(() => { + this.isSubmitting = false; + }); + }, + cancelEditing() { + this.isEditing = this.isNewDiscussion; + this.$emit('cancelEditing'); + }, + showReplyForm() { + this.isEditing = true; + }, + }, +}; +</script> + +<template> + <li :class="timelineEntryClasses" data-testid="abuse-report-note-timeline-entry"> + <div :class="timelineEntryInnerClasses" data-testid="abuse-report-note-timeline-entry-inner"> + <div class="timeline-content"> + <div class="flash-container"></div> + <div :class="commentFormWrapperClasses" data-testid="abuse-report-comment-form-wrapper"> + <abuse-report-comment-form + v-if="isEditing" + :abuse-report-id="abuseReportId" + :is-submitting="isSubmitting" + :autosave-key="autosaveKey" + :is-new-discussion="isNewDiscussion" + @submitForm="addNote" + @cancelEditing="cancelEditing" + /> + <textarea + v-else + ref="textarea" + rows="1" + class="reply-placeholder-text-field gl-font-regular!" + data-testid="abuse-report-note-reply-textarea" + :placeholder="$options.i18n.reply" + :aria-label="$options.i18n.replyToComment" + @focus="showReplyForm" + @click="showReplyForm" + ></textarea> + </div> + </div> + </div> + </li> +</template> diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_comment_form.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_comment_form.vue new file mode 100644 index 00000000000..646ffc690d0 --- /dev/null +++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_comment_form.vue @@ -0,0 +1,136 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import { __, s__ } from '~/locale'; +import { helpPagePath } from '~/helpers/help_page_helper'; +import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave'; +import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; +import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue'; + +export default { + name: 'AbuseReportCommentForm', + i18n: { + addReplyText: __('Add a reply'), + placeholderText: __('Write a comment or drag your files hereā¦'), + cancelButtonText: __('Cancel'), + confirmText: s__('Notes|Are you sure you want to cancel creating this comment?'), + discardText: __('Discard changes'), + continueEditingText: __('Continue editing'), + }, + components: { + GlButton, + MarkdownEditor, + }, + inject: ['uploadNoteAttachmentPath'], + props: { + abuseReportId: { + type: String, + required: true, + }, + isSubmitting: { + type: Boolean, + required: false, + default: false, + }, + autosaveKey: { + type: String, + required: true, + }, + isNewDiscussion: { + type: Boolean, + required: false, + default: false, + }, + initialValue: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + commentText: getDraft(this.autosaveKey) || this.initialValue || '', + }; + }, + computed: { + formFieldProps() { + return { + 'aria-label': this.$options.i18n.addReplyText, + placeholder: this.$options.i18n.placeholderText, + id: 'abuse-report-add-or-edit-comment', + name: 'abuse-report-add-or-edit-comment', + }; + }, + markdownDocsPath() { + return helpPagePath('user/markdown'); + }, + commentButtonText() { + return this.isNewDiscussion ? __('Comment') : __('Reply'); + }, + }, + methods: { + setCommentText(newText) { + if (!this.isSubmitting) { + this.commentText = newText; + updateDraft(this.autosaveKey, this.commentText); + } + }, + async cancelEditing() { + if (this.commentText && this.commentText !== this.initialValue) { + const confirmed = await confirmAction(this.$options.i18n.confirmText, { + primaryBtnText: this.$options.i18n.discardText, + cancelBtnText: this.$options.i18n.continueEditingText, + primaryBtnVariant: 'danger', + }); + + if (!confirmed) { + return; + } + } + + this.$emit('cancelEditing'); + clearDraft(this.autosaveKey); + }, + }, +}; +</script> + +<template> + <div class="timeline-discussion-body gl-overflow-visible!"> + <div class="note-body gl-p-0! gl-overflow-visible!"> + <form class="common-note-form gfm-form js-main-target-form gl-flex-grow-1 new-note"> + <markdown-editor + :value="commentText" + :enable-content-editor="false" + render-markdown-path="" + :uploads-path="uploadNoteAttachmentPath" + :markdown-docs-path="markdownDocsPath" + :form-field-props="formFieldProps" + :autofocus="true" + @input="setCommentText" + @keydown.meta.enter="$emit('submitForm', { commentText })" + @keydown.ctrl.enter="$emit('submitForm', { commentText })" + @keydown.esc.stop="cancelEditing" + /> + <div class="note-form-actions"> + <gl-button + category="primary" + variant="confirm" + data-testid="comment-button" + :disabled="!commentText.length" + :loading="isSubmitting" + @click="$emit('submitForm', { commentText })" + > + {{ commentButtonText }} + </gl-button> + <gl-button + data-testid="cancel-button" + category="primary" + class="gl-ml-3" + @click="cancelEditing" + >{{ $options.i18n.cancelButtonText }} + </gl-button> + </div> + </form> + </div> + </div> +</template> diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue index 4d24471fa43..49ba777f697 100644 --- a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue +++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue @@ -4,6 +4,7 @@ import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item import DiscussionNotesRepliesWrapper from '~/notes/components/discussion_notes_replies_wrapper.vue'; import ToggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue'; import AbuseReportNote from './abuse_report_note.vue'; +import AbuseReportAddNote from './abuse_report_add_note.vue'; export default { name: 'AbuseReportDiscussion', @@ -12,6 +13,7 @@ export default { DiscussionNotesRepliesWrapper, ToggleRepliesWidget, AbuseReportNote, + AbuseReportAddNote, }, props: { abuseReportId: { @@ -92,6 +94,11 @@ export default { :abuse-report-id="abuseReportId" /> </template> + <abuse-report-add-note + :discussion-id="discussionId" + :is-new-discussion="false" + :abuse-report-id="abuseReportId" + /> </template> </discussion-notes-replies-wrapper> </ul> diff --git a/app/assets/javascripts/admin/abuse_report/index.js b/app/assets/javascripts/admin/abuse_report/index.js index c2117130d26..e4319b3edef 100644 --- a/app/assets/javascripts/admin/abuse_report/index.js +++ b/app/assets/javascripts/admin/abuse_report/index.js @@ -30,6 +30,7 @@ export const initAbuseReportApp = () => { allowScopedLabels: false, updatePath: abuseReport.report.updatePath, listPath: abuseReportsListPath, + uploadNoteAttachmentPath: abuseReport.uploadNoteAttachmentPath, labelsManagePath: '', allowLabelCreate: true, }, diff --git a/app/assets/javascripts/ci/pipeline_details/pipelines_index.js b/app/assets/javascripts/ci/pipeline_details/pipelines_index.js index ea2875713a9..b4528ab895d 100644 --- a/app/assets/javascripts/ci/pipeline_details/pipelines_index.js +++ b/app/assets/javascripts/ci/pipeline_details/pipelines_index.js @@ -44,6 +44,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => { params, fullPath, visibilityPipelineIdType, + showJenkinsCiPrompt, } = el.dataset; return new Vue({ @@ -57,6 +58,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => { pipelineEditorPath, pipelineSchedulesPath, suggestedCiTemplates: JSON.parse(suggestedCiTemplates), + showJenkinsCiPrompt: parseBoolean(showJenkinsCiPrompt), }, data() { return { diff --git a/app/assets/javascripts/ci/pipeline_editor/constants.js b/app/assets/javascripts/ci/pipeline_editor/constants.js index e85138e361f..66725df15f0 100644 --- a/app/assets/javascripts/ci/pipeline_editor/constants.js +++ b/app/assets/javascripts/ci/pipeline_editor/constants.js @@ -1,4 +1,5 @@ import { s__ } from '~/locale'; +import { helpPagePath } from '~/helpers/help_page_helper'; export const EDITOR_APP_DRAWER_HELP = 'HELP'; export const EDITOR_APP_DRAWER_JOB_ASSISTANT = 'JOB_ASSISTANT'; @@ -93,6 +94,9 @@ export const VALIDATE_TAB_FEEDBACK_URL = 'https://gitlab.com/gitlab-org/gitlab/- export const COMMIT_SHA_POLL_INTERVAL = 1000; +export const MIGRATION_PLAN_HELP_PATH = helpPagePath('ci/migration/plan_a_migration'); +export const MIGRATE_FROM_JENKINS_TRACKING_LABEL = 'migrate_from_jenkins_prompt'; + export const I18N = { title: s__('Pipelines|Get started with GitLab CI/CD'), learnBasics: { @@ -107,6 +111,13 @@ export const I18N = { ), cta: s__('Pipelines|Try test template'), }, + migrateFromJenkins: { + title: s__('Pipelines|Migrate to GitLab CI/CD from Jenkins'), + description: s__( + 'Pipelines|Take advantage of simple, scalable pipelines and CI/CD-enabled features. You can view integration results, security scans, tests, code coverage and more directly in merge requests!', + ), + cta: s__('Pipelines|Start with a migration plan'), + }, }, templates: { title: s__('Pipelines|Ready to set up CI/CD for your project?'), diff --git a/app/assets/javascripts/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue b/app/assets/javascripts/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue index a6297213402..c9631d8f36b 100644 --- a/app/assets/javascripts/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue +++ b/app/assets/javascripts/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue @@ -1,7 +1,12 @@ <script> import { GlButton, GlCard, GlSprintf } from '@gitlab/ui'; import { mergeUrlParams } from '~/lib/utils/url_utility'; -import { STARTER_TEMPLATE_NAME, I18N } from '~/ci/pipeline_editor/constants'; +import { + STARTER_TEMPLATE_NAME, + I18N, + MIGRATION_PLAN_HELP_PATH, + MIGRATE_FROM_JENKINS_TRACKING_LABEL, +} from '~/ci/pipeline_editor/constants'; import Tracking from '~/tracking'; import CiTemplates from './ci_templates.vue'; @@ -15,7 +20,7 @@ export default { mixins: [Tracking.mixin()], STARTER_TEMPLATE_NAME, I18N, - inject: ['pipelineEditorPath'], + inject: ['pipelineEditorPath', 'showJenkinsCiPrompt'], data() { return { gettingStartedTemplateUrl: mergeUrlParams( @@ -23,17 +28,23 @@ export default { this.pipelineEditorPath, ), tracker: null, + migrationPlanUrl: MIGRATION_PLAN_HELP_PATH, + migrationPromptTrackingLabel: MIGRATE_FROM_JENKINS_TRACKING_LABEL, }; }, + mounted() { + if (this.showJenkinsCiPrompt) { + this.trackEvent('render', this.migrationPromptTrackingLabel); + } + }, methods: { - trackEvent(template) { - this.track('template_clicked', { - label: template, - }); + trackEvent(action, label) { + this.track(action, { label }); }, }, }; </script> + <template> <div> <h2 class="gl-font-size-h2 gl-text-gray-900">{{ $options.I18N.title }}</h2> @@ -47,28 +58,62 @@ export default { </gl-sprintf> </p> - <div class="gl-lg-w-25p gl-lg-pr-5 gl-mb-8"> - <gl-card> - <div class="gl-flex-direction-row"> - <div class="gl-py-5"><gl-emoji class="gl-font-size-h2-xl" data-name="wave" /></div> - <div class="gl-mb-3"> - <strong class="gl-text-gray-800 gl-mb-2"> - {{ $options.I18N.learnBasics.gettingStarted.title }} - </strong> + <div class="gl-display-flex gl-flex-direction-row gl-flex-wrap"> + <div + v-if="showJenkinsCiPrompt" + class="gl-lg-w-25p gl-md-w-half gl-w-full gl-md-pr-5 gl-pb-8" + data-testid="migrate-from-jenkins-prompt" + > + <gl-card class="gl-bg-blue-50"> + <div class="gl-flex-direction-row"> + <div class="gl-py-5"><gl-emoji class="gl-font-size-h2-xl" data-name="rocket" /></div> + <div class="gl-mb-3"> + <strong class="gl-text-gray-800 gl-mb-2">{{ + $options.I18N.learnBasics.migrateFromJenkins.title + }}</strong> + </div> + <p class="gl-font-sm gl-h-13"> + {{ $options.I18N.learnBasics.migrateFromJenkins.description }} + </p> + </div> + + <gl-button + category="primary" + variant="confirm" + :href="migrationPlanUrl" + target="_blank" + @click="trackEvent('template_clicked', migrationPromptTrackingLabel)" + > + {{ $options.I18N.learnBasics.migrateFromJenkins.cta }} + </gl-button> + </gl-card> + </div> + + <div class="gl-lg-w-25p gl-md-w-half gl-w-full gl-pb-8"> + <gl-card> + <div class="gl-flex-direction-row"> + <div class="gl-py-5"><gl-emoji class="gl-font-size-h2-xl" data-name="wave" /></div> + <div class="gl-mb-3"> + <strong class="gl-text-gray-800 gl-mb-2"> + {{ $options.I18N.learnBasics.gettingStarted.title }} + </strong> + </div> + <p class="gl-font-sm gl-h-13"> + {{ $options.I18N.learnBasics.gettingStarted.description }} + </p> </div> - <p class="gl-font-sm">{{ $options.I18N.learnBasics.gettingStarted.description }}</p> - </div> - <gl-button - category="primary" - variant="confirm" - :href="gettingStartedTemplateUrl" - data-testid="test-template-link" - @click="trackEvent($options.STARTER_TEMPLATE_NAME)" - > - {{ $options.I18N.learnBasics.gettingStarted.cta }} - </gl-button> - </gl-card> + <gl-button + category="primary" + variant="confirm" + :href="gettingStartedTemplateUrl" + data-testid="test-template-link" + @click="trackEvent('template_clicked', $options.STARTER_TEMPLATE_NAME)" + > + {{ $options.I18N.learnBasics.gettingStarted.cta }} + </gl-button> + </gl-card> + </div> </div> <h2 class="gl-font-lg gl-text-gray-900">{{ $options.I18N.templates.title }}</h2> diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue b/app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue index 3021b4a2ef8..634fc2ad447 100644 --- a/app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue +++ b/app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue @@ -70,3 +70,11 @@ export default { :items="items" /> </template> + +<style scoped> +/* TODO: Use max-height prop when gitlab-ui got updated. +See https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2374 */ +::v-deep .gl-new-dropdown-inner { + max-height: 310px; +} +</style> diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js deleted file mode 100644 index 8f3681952c6..00000000000 --- a/app/assets/javascripts/header.js +++ /dev/null @@ -1,50 +0,0 @@ -// TODO: Remove this with the removal of the old navigation. -// See https://gitlab.com/groups/gitlab-org/-/epics/11875. - -import { highCountTrim } from '~/lib/utils/text_utility'; -import Tracking from '~/tracking'; - -/** - * Updates todo counter when todos are toggled. - * When count is 0, we hide the badge. - * - * @param {jQuery.Event} e - * @param {String} count - */ -export default function initTodoToggle() { - document.addEventListener('todo:toggle', (e) => { - const updatedCount = e.detail.count || 0; - const todoPendingCount = document.querySelector('.js-todos-count'); - - if (todoPendingCount) { - todoPendingCount.textContent = highCountTrim(updatedCount); - if (updatedCount === 0) { - todoPendingCount.classList.add('hidden'); - } else { - todoPendingCount.classList.remove('hidden'); - } - } - }); -} - -function trackShowUserDropdownLink(trackEvent, elToTrack, el) { - const { trackLabel, trackProperty } = elToTrack.dataset; - - el.addEventListener('shown.bs.dropdown', () => { - Tracking.event(document.body.dataset.page, trackEvent, { - label: trackLabel, - property: trackProperty, - }); - }); -} - -export function initNavUserDropdownTracking() { - const el = document.querySelector('.js-nav-user-dropdown'); - const buyEl = document.querySelector('.js-buy-pipeline-minutes-link'); - - if (el && buyEl) { - trackShowUserDropdownLink('show_buy_ci_minutes', buyEl, el); - } -} - -requestIdleCallback(initNavUserDropdownTracking); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index b41f306eabd..43ae13915ad 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -15,7 +15,6 @@ import * as tooltips from '~/tooltips'; import { initPrefetchLinks } from '~/lib/utils/navigation_utility'; import { logHelloDeferred } from 'jh_else_ce/lib/logger/hello_deferred'; import initAlertHandler from './alert_handler'; -import initTodoToggle from './header'; import initLayoutNav from './layout_nav'; import { handleLocationHash, addSelectOnFocusBehaviour } from './lib/utils/common_utils'; import { localTimeAgo } from './lib/utils/datetime/timeago_utility'; @@ -86,7 +85,6 @@ function deferredInitialisation() { if (!gon.use_new_navigation) { initTopNav(); - initTodoToggle(); } initBreadcrumbs(); initPrefetchLinks('.js-prefetch-document'); diff --git a/app/assets/javascripts/vue_shared/components/list_selector/constants.js b/app/assets/javascripts/vue_shared/components/list_selector/constants.js index cff9c56a1c0..ad826c6f3e5 100644 --- a/app/assets/javascripts/vue_shared/components/list_selector/constants.js +++ b/app/assets/javascripts/vue_shared/components/list_selector/constants.js @@ -1,6 +1,26 @@ import { __ } from '~/locale'; +import UserItem from './user_item.vue'; +import GroupItem from './group_item.vue'; +import DeployKeyItem from './deploy_key_item.vue'; export const CONFIG = { - users: { title: __('Users'), icon: 'user', filterKey: 'username', showNamespaceDropdown: true }, - groups: { title: __('Groups'), icon: 'group', filterKey: 'name' }, + users: { + title: __('Users'), + icon: 'user', + filterKey: 'username', + showNamespaceDropdown: true, + component: UserItem, + }, + groups: { + title: __('Groups'), + icon: 'group', + filterKey: 'name', + component: GroupItem, + }, + deployKeys: { + title: __('Deploy keys'), + icon: 'key', + filterKey: 'name', + component: DeployKeyItem, + }, }; diff --git a/app/assets/javascripts/vue_shared/components/list_selector/deploy_key_item.vue b/app/assets/javascripts/vue_shared/components/list_selector/deploy_key_item.vue new file mode 100644 index 00000000000..4dbbd44f0b5 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/list_selector/deploy_key_item.vue @@ -0,0 +1,51 @@ +<script> +import { GlButton, GlIcon } from '@gitlab/ui'; +import { sprintf, __ } from '~/locale'; + +export default { + name: 'DeployKeyItem', + components: { GlButton, GlIcon }, + props: { + data: { + type: Object, + required: true, + }, + canDelete: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + const { title, owner, id } = this.data; + return { + deleteButtonLabel: sprintf(__('Delete %{name}'), { name: title }), + title, + owner, + id, + }; + }, +}; +</script> + +<template> + <span + class="gl-display-flex gl-align-items-center gl-gap-3" + data-testid="deploy-key-wrapper" + @click="$emit('select', id)" + > + <gl-icon name="key" /> + <span class="gl-display-flex gl-flex-direction-column gl-flex-grow-1"> + <span class="gl-font-weight-bold">{{ title }}</span> + <span class="gl-text-gray-600">@{{ owner }}</span> + </span> + + <gl-button + v-if="canDelete" + icon="remove" + :aria-label="deleteButtonLabel" + category="tertiary" + @click.stop="$emit('delete', id)" + /> + </span> +</template> diff --git a/app/assets/javascripts/vue_shared/components/list_selector/index.vue b/app/assets/javascripts/vue_shared/components/list_selector/index.vue index b8480a0c496..d79a8d6a00c 100644 --- a/app/assets/javascripts/vue_shared/components/list_selector/index.vue +++ b/app/assets/javascripts/vue_shared/components/list_selector/index.vue @@ -5,8 +5,6 @@ import { createAlert } from '~/alert'; import { __ } from '~/locale'; import groupsAutocompleteQuery from '~/graphql_shared/queries/groups_autocomplete.query.graphql'; import Api from '~/api'; -import UserItem from './user_item.vue'; -import GroupItem from './group_item.vue'; import { CONFIG } from './constants'; const I18N = { @@ -25,10 +23,6 @@ export default { GlCollapsibleListbox, }, props: { - title: { - type: String, - required: true, - }, type: { type: String, required: true, @@ -61,12 +55,6 @@ export default { config() { return CONFIG[this.type]; }, - isUserVariant() { - return this.type === 'users'; - }, - component() { - return this.isUserVariant ? UserItem : GroupItem; - }, namespaceDropdownText() { return parseBoolean(this.isProjectNamespace) ? this.$options.i18n.projectGroups @@ -77,12 +65,14 @@ export default { async handleSearchInput(search) { this.$refs.results.open(); + const searchMethod = { + users: this.fetchUsersBySearchTerm, + groups: this.fetchGroupsBySearchTerm, + deployKeys: this.fetchDeployKeysBySearchTerm, + }; + try { - if (this.isUserVariant) { - this.items = await this.fetchUsersBySearchTerm(search); - } else { - this.items = await this.fetchGroupsBySearchTerm(search); - } + this.items = await searchMethod[this.type](search); } catch (e) { createAlert({ message: this.$options.i18n.apiErrorMessage, @@ -114,6 +104,10 @@ export default { })), ); }, + fetchDeployKeysBySearchTerm() { + // TODO - implement API request (follow-up) + // https://gitlab.com/gitlab-org/gitlab/-/issues/432494 + }, getItemByKey(key) { return this.items.find((item) => item[this.config.filterKey] === key); }, @@ -139,7 +133,7 @@ export default { <gl-card header-class="gl-new-card-header gl-border-none" body-class="gl-card-footer"> <template #header ><strong data-testid="list-selector-title" - >{{ title }} + >{{ config.title }} <span class="gl-text-gray-700 gl-ml-3" ><gl-icon :name="config.icon" /> {{ selectedItems.length }}</span ></strong @@ -166,7 +160,7 @@ export default { </template> <template #list-item="{ item }"> - <component :is="component" :data="item" @select="handleSelectItem" /> + <component :is="config.component" :data="item" @select="handleSelectItem" /> </template> </gl-collapsible-listbox> @@ -180,7 +174,7 @@ export default { </div> <component - :is="component" + :is="config.component" v-for="(item, index) of selectedItems" :key="index" :class="{ 'gl-border-t': index > 0 }" diff --git a/app/helpers/ci/pipelines_helper.rb b/app/helpers/ci/pipelines_helper.rb index 9c4ceaccff1..156df0c4cc4 100644 --- a/app/helpers/ci/pipelines_helper.rb +++ b/app/helpers/ci/pipelines_helper.rb @@ -87,7 +87,8 @@ module Ci pipeline_editor_path: can?(current_user, :create_pipeline, project) && project_ci_pipeline_editor_path(project), suggested_ci_templates: suggested_ci_templates.to_json, full_path: project.full_path, - visibility_pipeline_id_type: visibility_pipeline_id_type + visibility_pipeline_id_type: visibility_pipeline_id_type, + show_jenkins_ci_prompt: show_jenkins_ci_prompt(project).to_s } end @@ -104,5 +105,12 @@ module Ci yield markdown(warning.content) end end + + def show_jenkins_ci_prompt(project) + return false unless can?(current_user, :create_pipeline, project) + return false if project.repository.gitlab_ci_yml.present? + + project.repository.jenkinsfile? + end end end diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb index 1731562d158..19dc0e40564 100644 --- a/app/models/abuse_report.rb +++ b/app/models/abuse_report.rb @@ -124,7 +124,7 @@ class AbuseReport < ApplicationRecord return screenshot.url unless screenshot.upload asset_host = ActionController::Base.asset_host || Gitlab.config.gitlab.base_url - local_path = Gitlab::Routing.url_helpers.abuse_report_upload_path( + local_path = Gitlab::Routing.url_helpers.abuse_report_screenshot_path( filename: screenshot.filename, id: screenshot.upload.model_id, model: 'abuse_report', diff --git a/app/models/ci/catalog/listing.rb b/app/models/ci/catalog/listing.rb index 93cb4830f23..14d043ad156 100644 --- a/app/models/ci/catalog/listing.rb +++ b/app/models/ci/catalog/listing.rb @@ -44,7 +44,8 @@ module Ci attr_reader :current_user def all_resources - Ci::Catalog::Resource.joins(:project).includes(:project) + Ci::Catalog::Resource.published + .joins(:project).includes(:project) .merge(Project.public_or_visible_to_user(current_user)) end diff --git a/app/models/repository.rb b/app/models/repository.rb index f9e680edf37..a6160a4c3b4 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -679,6 +679,10 @@ class Repository end cache_method :gitlab_ci_yml + def jenkinsfile? + file_on_head(:jenkinsfile).present? + end + def xcode_project? file_on_head(:xcode_config, :tree).present? end diff --git a/app/serializers/admin/abuse_report_details_entity.rb b/app/serializers/admin/abuse_report_details_entity.rb index 77b85f239f7..a654482b989 100644 --- a/app/serializers/admin/abuse_report_details_entity.rb +++ b/app/serializers/admin/abuse_report_details_entity.rb @@ -76,5 +76,9 @@ module Admin expose :report do |report| ReportedContentEntity.represent(report) end + + expose :upload_note_attachment_path do |report| + upload_path('abuse_report', id: report.id) + end end end |