diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 36d6d8a3973..ccc181e4fbb 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -289,10 +289,6 @@ - "tooling/docs/**/*" - "lib/tasks/gitlab/docs/compile_deprecations.rake" -.bundler-patterns: &bundler-patterns - - '{Gemfile.lock,*/Gemfile.lock,*/*/Gemfile.lock}' - - '{Gemfile.next.lock,*/Gemfile.next.lock,*/*/Gemfile.next.lock}' - .nodejs-patterns: &nodejs-patterns - '{package.json,*/package.json,*/*/package.json}' - '{yarn.lock,*/yarn.lock,*/*/yarn.lock}' @@ -463,12 +459,6 @@ .frontend-predictive-patterns: &frontend-predictive-patterns - "{,ee/,jh/}{app/assets/javascripts,spec/frontend}/**/*" -# Frontend view patterns + .qa-patterns -.frontend-qa-patterns: &frontend-qa-patterns - - "{,ee/,jh/}{app/assets,app/components,app/helpers,app/presenters,app/views}/**/*" - # QA changes - - "{,jh/}qa/**/*" - # Code patterns + .ci-patterns .code-patterns: &code-patterns - ".{eslintrc.yml,eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}" @@ -679,13 +669,6 @@ - "{,jh/}Gemfile.next{,.lock}" - "{,ee/,jh/}config/**/*.rb" -.core-frontend-patterns: &core-frontend-patterns - - "{package.json,yarn.lock}" - - "babel.config.js" - - "jest.config.{base,integration,unit}.js" - - "config/helpers/**/*.js" - - "vendor/assets/javascripts/**/*" - .feature-flag-development-config-patterns: &feature-flag-development-config-patterns - "{,ee/,jh/}config/feature_flags/**/*.yml" diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml index 9b9f27cbfd8..10f8b3cc52a 100644 --- a/.rubocop_todo/layout/line_length.yml +++ b/.rubocop_todo/layout/line_length.yml @@ -2150,7 +2150,6 @@ Layout/LineLength: - 'ee/spec/workers/ci/minutes/update_project_and_namespace_usage_worker_spec.rb' - 'ee/spec/workers/ci/upstream_projects_subscriptions_cleanup_worker_spec.rb' - 'ee/spec/workers/compliance_management/merge_requests/compliance_violations_worker_spec.rb' - - 'ee/spec/workers/concerns/elastic/indexing_control_spec.rb' - 'ee/spec/workers/elastic/migration_worker_spec.rb' - 'ee/spec/workers/geo/destroy_worker_spec.rb' - 'ee/spec/workers/geo/prune_event_log_worker_spec.rb' diff --git a/.rubocop_todo/rspec/expect_in_hook.yml b/.rubocop_todo/rspec/expect_in_hook.yml index a0cc988aadc..d64e2dfab16 100644 --- a/.rubocop_todo/rspec/expect_in_hook.yml +++ b/.rubocop_todo/rspec/expect_in_hook.yml @@ -73,7 +73,6 @@ RSpec/ExpectInHook: - 'ee/spec/tasks/gitlab/license_rake_spec.rb' - 'ee/spec/tasks/gitlab/spdx_rake_spec.rb' - 'ee/spec/workers/analytics/cycle_analytics/consistency_worker_spec.rb' - - 'ee/spec/workers/concerns/elastic/indexing_control_spec.rb' - 'ee/spec/workers/elastic_index_bulk_cron_worker_spec.rb' - 'ee/spec/workers/elastic_indexing_control_worker_spec.rb' - 'ee/spec/workers/geo/secondary/registry_consistency_worker_spec.rb' diff --git a/.rubocop_todo/rspec/named_subject.yml b/.rubocop_todo/rspec/named_subject.yml index 4dcfe10e1dc..1bd4a69e18b 100644 --- a/.rubocop_todo/rspec/named_subject.yml +++ b/.rubocop_todo/rspec/named_subject.yml @@ -3287,7 +3287,6 @@ RSpec/NamedSubject: - 'spec/services/projects/operations/update_service_spec.rb' - 'spec/services/projects/overwrite_project_service_spec.rb' - 'spec/services/projects/prometheus/alerts/notify_service_spec.rb' - - 'spec/services/projects/prometheus/metrics/destroy_service_spec.rb' - 'spec/services/projects/readme_renderer_service_spec.rb' - 'spec/services/projects/transfer_service_spec.rb' - 'spec/services/projects/unlink_fork_service_spec.rb' diff --git a/app/assets/javascripts/admin/broadcast_messages/components/base.stories.js b/app/assets/javascripts/admin/broadcast_messages/components/base.stories.js new file mode 100644 index 00000000000..61fbce0fef9 --- /dev/null +++ b/app/assets/javascripts/admin/broadcast_messages/components/base.stories.js @@ -0,0 +1,27 @@ +import BroadcastMessagesBase from '~/admin/broadcast_messages/components/base.vue'; +import { generateMockMessages } from '../../../../../../spec/frontend/admin/broadcast_messages/mock_data'; + +export default { + title: 'admin/broadcast_messages/base', + component: BroadcastMessagesBase, +}; + +const Template = (args, { argTypes }) => ({ + props: Object.keys(argTypes), + components: { BroadcastMessagesBase }, + template: '', +}); + +export const Default = Template.bind({}); +Default.args = { + page: 1, + messagesCount: 5, + messages: generateMockMessages(5), +}; + +export const Empty = Template.bind({}); +Empty.args = { + page: 1, + messagesCount: 0, + messages: [], +}; diff --git a/app/assets/javascripts/ci/pipeline_details/header/pipeline_header.vue b/app/assets/javascripts/ci/pipeline_details/header/pipeline_header.vue index fe53f5e7681..fb1c52f2959 100644 --- a/app/assets/javascripts/ci/pipeline_details/header/pipeline_header.vue +++ b/app/assets/javascripts/ci/pipeline_details/header/pipeline_header.vue @@ -16,6 +16,7 @@ import deletePipelineMutation from '../graphql/mutations/delete_pipeline.mutatio import retryPipelineMutation from '../graphql/mutations/retry_pipeline.mutation.graphql'; import { getQueryHeaders } from '../graph/utils'; import { POLL_INTERVAL } from '../graph/constants'; +import { MERGE_TRAIN_EVENT_TYPE } from './constants'; import HeaderActions from './components/header_actions.vue'; import HeaderBadges from './components/header_badges.vue'; import getPipelineQuery from './graphql/queries/get_pipeline_header_data.query.graphql'; @@ -40,6 +41,8 @@ export default { TimeAgoTooltip, PipelineAccountVerificationAlert: () => import('ee_component/vue_shared/components/pipeline_account_verification_alert.vue'), + HeaderMergeTrainsLink: () => + import('ee_component/ci/pipeline_details/header/components/header_merge_trains_link.vue'), }, directives: { GlTooltip: GlTooltipDirective, @@ -219,6 +222,9 @@ export default { refText() { return this.pipeline?.refText; }, + isMergeTrainPipeline() { + return this.pipeline.mergeRequestEventType === MERGE_TRAIN_EVENT_TYPE; + }, }, methods: { reportFailure(errorType, errorMessages = []) { @@ -406,6 +412,9 @@ export default { +
+ +
{ - if (alertShown) return; - - editor.state.doc.descendants((node) => { - if (node.type.name === 'table' && node.attrs.isMarkdown && shouldRenderHTMLTable(node)) { - editor.emit('alert', { - message: __( - 'The content editor may change the markdown formatting style of the document, which may not match your original markdown style.', - ), - variant: VARIANT_WARNING, - }); - - alertShown = true; - - return false; - } - - return true; - }); -}, 1000); - export default Table.extend({ addAttributes() { return { @@ -37,7 +16,24 @@ export default Table.extend({ }; }, - onUpdate({ editor }) { - onUpdate(editor); - }, + onUpdate: debounce(function onUpdate({ editor }) { + if (this.options.alertShown) return; + + editor.state.doc.descendants((node) => { + if (node.type.name === 'table' && node.attrs.isMarkdown && shouldRenderHTMLTable(node)) { + this.options.eventHub.$emit(ALERT_EVENT, { + message: __( + 'Tables containing block elements (like multiple paragraphs, lists or blockquotes) are not supported in Markdown and will be converted to HTML.', + ), + variant: VARIANT_WARNING, + }); + + this.options.alertShown = true; + + return false; + } + + return true; + }); + }, 1000), }); diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/cleanup_image_tags.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/cleanup_image_tags.vue index c8e89465d21..92534c65917 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/cleanup_image_tags.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/cleanup_image_tags.vue @@ -11,6 +11,7 @@ import { UNAVAILABLE_ADMIN_FEATURE_TEXT, } from '~/packages_and_registries/settings/project/constants'; import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql'; +import SettingsSection from '~/vue_shared/components/settings/settings_section.vue'; import ContainerExpirationPolicyForm from './container_expiration_policy_form.vue'; @@ -20,6 +21,7 @@ export default { GlSprintf, GlLink, ContainerExpirationPolicyForm, + SettingsSection, }, inject: ['projectPath', 'isAdmin', 'adminSettingsPath', 'enableHistoricEntries', 'helpPagePath'], i18n: { @@ -77,17 +79,18 @@ export default { - + diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy_form.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy_form.vue index 65c86ebfefe..7caf229ad7c 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy_form.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy_form.vue @@ -1,5 +1,5 @@ @@ -218,12 +249,22 @@ export default { supports-quick-actions :autofocus="autofocus" @input="setCommentText" - @keydown.meta.enter="$emit('submitForm', { commentText, isNoteInternal })" - @keydown.ctrl.enter="$emit('submitForm', { commentText, isNoteInternal })" + @keydown.meta.enter="submitForm" + @keydown.ctrl.enter="submitForm" @keydown.esc.stop="cancelEditing" />
+
+ +
{{ commentButtonTextComputed }} { + return { + ...note, + discussion: { + ...note.discussion, + resolved, + resolvedBy, + }, + }; + }); + return { + id: this.discussionId, + notes: { + nodes: [...toggledDiscussionNotes], + }, + }; + }, + async resolveDiscussion() { + this.isResolving = true; + try { + await this.$apollo.mutate({ + mutation: toggleWorkItemNoteResolveDiscussion, + variables: { id: this.discussionId, resolve: !this.isDiscussionResolved }, + optimisticResponse: { + discussionToggleResolve: { + errors: [], + discussion: this.getToggledDiscussion(!this.isDiscussionResolved), + __typename: 'DiscussionToggleResolvePayload', + }, + }, + }); + } catch (error) { + this.$emit('error', error.message); + } finally { + this.isResolving = false; + } + }, }, }; @@ -158,8 +230,12 @@ export default { :class="{ 'gl-mb-4': hasReplies }" :assignees="assignees" :can-set-work-item-metadata="canSetWorkItemMetadata" + :is-discussion-resolved="isDiscussionResolved" + :is-discussion-resolvable="isDiscussionResolvable" :work-item-id="workItemId" :work-item-iid="workItemIid" + :is-resolving="isResolving" + @resolve="resolveDiscussion" @startReplying="showReplyForm" @deleteNote="$emit('deleteNote', note)" @reportAbuse="$emit('reportAbuse', note)" @@ -187,9 +263,13 @@ export default { :work-item-id="workItemId" :work-item-iid="workItemIid" :can-set-work-item-metadata="canSetWorkItemMetadata" + :is-discussion-resolved="isDiscussionResolved" + :is-discussion-resolvable="isDiscussionResolvable" + :is-resolving="isResolving" @startReplying="showReplyForm" @deleteNote="$emit('deleteNote', note)" @reportAbuse="$emit('reportAbuse', note)" + @resolve="resolveDiscussion" @error="$emit('error', $event)" /> @@ -214,6 +294,9 @@ export default { :work-item-id="workItemId" :work-item-iid="workItemIid" :can-set-work-item-metadata="canSetWorkItemMetadata" + :is-discussion-resolved="isDiscussionResolved" + :is-discussion-resolvable="isDiscussionResolvable" + :is-resolving="isResolving" @startReplying="showReplyForm" @deleteNote="$emit('deleteNote', reply)" @reportAbuse="$emit('reportAbuse', reply)" @@ -241,10 +324,15 @@ export default { :is-discussion-locked="isDiscussionLocked" :is-internal-thread="note.internal" :is-work-item-confidential="isWorkItemConfidential" + :is-discussion-resolved="isDiscussionResolved" + :is-discussion-resolvable="isDiscussionResolvable" + :is-resolving="isResolving" + :has-replies="hasReplies" @startReplying="showReplyForm" @cancelEditing="hideReplyForm" @replied="onReplied" @replying="onReplying" + @resolve="resolveDiscussion" @error="$emit('error', $event)" /> diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_note.vue index 3f1d657f9fd..613ae6df2a6 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_note.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_note.vue @@ -91,6 +91,21 @@ export default { required: false, default: false, }, + isDiscussionResolved: { + type: Boolean, + required: false, + default: false, + }, + isDiscussionResolvable: { + type: Boolean, + required: false, + default: false, + }, + isResolving: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -126,6 +141,9 @@ export default { showReply() { return this.note.userPermissions.createNote && this.isFirstNote; }, + canResolve() { + return this.note.userPermissions.resolveNote && this.isFirstNote && this.hasReplies; + }, noteHeaderClass() { return { 'note-header': true, @@ -174,6 +192,9 @@ export default { isWorkItemConfidential() { return this.workItem.confidential; }, + discussionResolvedBy() { + return this.note.discussion.resolvedBy; + }, }, apollo: { workItem: { @@ -326,9 +347,13 @@ export default { :work-item-id="workItemId" :autofocus="isEditing" :is-work-item-confidential="isWorkItemConfidential" + :is-discussion-resolved="isDiscussionResolved" + :is-discussion-resolvable="isDiscussionResolvable" + :has-replies="hasReplies" :full-path="fullPath" class="gl-pl-3 gl-mt-3" @cancelEditing="isEditing = false" + @toggleResolveDiscussion="$emit('resolve')" @submitForm="updateNote" />
@@ -360,8 +385,14 @@ export default { :is-author-contributor="note.authorIsContributor" :max-access-level-of-author="note.maxAccessLevelOfAuthor" :project-name="projectName" + :can-resolve="canResolve" + :resolvable="isDiscussionResolvable" + :is-resolved="isDiscussionResolved" + :is-resolving="isResolving" + :resolved-by="discussionResolvedBy" @startReplying="showReplyForm" @startEditing="startEditing" + @resolve="$emit('resolve')" @error="($event) => $emit('error', $event)" @notifyCopyDone="notifyCopyDone" @deleteNote="$emit('deleteNote')" diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue index 4fcd905d0b8..5bf1600f7f2 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue @@ -21,6 +21,7 @@ export default { assignUserText: __('Assign to commenting user'), unassignUserText: __('Unassign from commenting user'), reportAbuseText: __('Report abuse'), + resolveThreadTitle: __('Resolve thread'), }, components: { EmojiPicker: () => import('~/emoji/components/picker.vue'), @@ -108,6 +109,31 @@ export default { required: false, default: '', }, + canResolve: { + type: Boolean, + required: false, + default: false, + }, + resolvable: { + type: Boolean, + required: false, + default: false, + }, + isResolved: { + type: Boolean, + required: false, + default: false, + }, + isResolving: { + type: Boolean, + required: false, + default: false, + }, + resolvedBy: { + type: Object, + required: false, + default: () => ({}), + }, }, computed: { assignUserActionText() { @@ -131,8 +157,21 @@ export default { name: this.projectName, }); }, + resolveIcon() { + if (!this.isResolving) { + return this.isResolved ? 'check-circle-filled' : 'check-circle'; + } + return null; + }, + resolveVariant() { + return this.isResolved ? 'success' : 'default'; + }, + resolveThreadTitle() { + return this.isResolved + ? __('Resolved by ') + this.resolvedBy.name + : this.$options.i18n.resolveThreadTitle; + }, }, - methods: { async setAwardEmoji(name) { const { mutation, mutationName, errorMessage } = getMutation({ note: this.note, name }); @@ -199,6 +238,20 @@ export default { > {{ __('Contributor') }} + import('ee_component/work_items/components/work_item_weight.vue'), WorkItemProgress: () => import('ee_component/work_items/components/work_item_progress.vue'), WorkItemIteration: () => import('ee_component/work_items/components/work_item_iteration.vue'), @@ -139,6 +142,9 @@ export default { hasParent() { return this.workItemHierarchy?.hasParent; }, + workItemCrmContacts() { + return this.isWidgetPresent(WIDGET_TYPE_CRM_CONTACTS) && this.glFeatures.workItemsAlpha; + }, }, methods: { isWidgetPresent(type) { @@ -168,6 +174,15 @@ export default { " /> +