diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 0b82be7140c..aec608007d5 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -456,12 +456,7 @@ export default {
{{ $options.i18n.autoCollapsed }}
-
+
{{ $options.i18n.expand }}
diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue
index 9d8b339e813..2da6a049703 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -13,8 +13,6 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql';
-import getIssuesWithoutCrmQuery from 'ee_else_ce/issues/list/queries/get_issues_without_crm.query.graphql';
-import getIssuesCountsWithoutCrmQuery from 'ee_else_ce/issues/list/queries/get_issues_counts_without_crm.query.graphql';
import createFlash, { FLASH_TYPES } from '~/flash';
import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -160,9 +158,7 @@ export default {
},
apollo: {
issues: {
- query() {
- return this.hasCrmParameter ? getIssuesQuery : getIssuesWithoutCrmQuery;
- },
+ query: getIssuesQuery,
variables() {
return this.queryVariables;
},
@@ -186,9 +182,7 @@ export default {
debounce: 200,
},
issuesCounts: {
- query() {
- return this.hasCrmParameter ? getIssuesCountsQuery : getIssuesCountsWithoutCrmQuery;
- },
+ query: getIssuesCountsQuery,
variables() {
return this.queryVariables;
},
@@ -403,12 +397,6 @@ export default {
page_before: this.pageParams.beforeCursor,
};
},
- hasCrmParameter() {
- return (
- window.location.search.includes('crm_contact_id=') ||
- window.location.search.includes('crm_organization_id=')
- );
- },
},
watch: {
$route(newValue, oldValue) {
diff --git a/app/assets/javascripts/issues/list/queries/get_issues_counts_without_crm.query.graphql b/app/assets/javascripts/issues/list/queries/get_issues_counts_without_crm.query.graphql
deleted file mode 100644
index ab91aab1218..00000000000
--- a/app/assets/javascripts/issues/list/queries/get_issues_counts_without_crm.query.graphql
+++ /dev/null
@@ -1,136 +0,0 @@
-query getIssuesCountWithoutCrm(
- $isProject: Boolean = false
- $fullPath: ID!
- $iid: String
- $search: String
- $assigneeId: String
- $assigneeUsernames: [String!]
- $authorUsername: String
- $confidential: Boolean
- $labelName: [String]
- $milestoneTitle: [String]
- $milestoneWildcardId: MilestoneWildcardId
- $myReactionEmoji: String
- $releaseTag: [String!]
- $releaseTagWildcardId: ReleaseTagWildcardId
- $types: [IssueType!]
- $not: NegatedIssueFilterInput
-) {
- group(fullPath: $fullPath) @skip(if: $isProject) {
- id
- openedIssues: issues(
- includeSubgroups: true
- state: opened
- iid: $iid
- search: $search
- assigneeId: $assigneeId
- assigneeUsernames: $assigneeUsernames
- authorUsername: $authorUsername
- confidential: $confidential
- labelName: $labelName
- milestoneTitle: $milestoneTitle
- milestoneWildcardId: $milestoneWildcardId
- myReactionEmoji: $myReactionEmoji
- types: $types
- not: $not
- ) {
- count
- }
- closedIssues: issues(
- includeSubgroups: true
- state: closed
- iid: $iid
- search: $search
- assigneeId: $assigneeId
- assigneeUsernames: $assigneeUsernames
- authorUsername: $authorUsername
- confidential: $confidential
- labelName: $labelName
- milestoneTitle: $milestoneTitle
- milestoneWildcardId: $milestoneWildcardId
- myReactionEmoji: $myReactionEmoji
- types: $types
- not: $not
- ) {
- count
- }
- allIssues: issues(
- includeSubgroups: true
- state: all
- iid: $iid
- search: $search
- assigneeId: $assigneeId
- assigneeUsernames: $assigneeUsernames
- authorUsername: $authorUsername
- confidential: $confidential
- labelName: $labelName
- milestoneTitle: $milestoneTitle
- milestoneWildcardId: $milestoneWildcardId
- myReactionEmoji: $myReactionEmoji
- types: $types
- not: $not
- ) {
- count
- }
- }
- project(fullPath: $fullPath) @include(if: $isProject) {
- id
- openedIssues: issues(
- state: opened
- iid: $iid
- search: $search
- assigneeId: $assigneeId
- assigneeUsernames: $assigneeUsernames
- authorUsername: $authorUsername
- confidential: $confidential
- labelName: $labelName
- milestoneTitle: $milestoneTitle
- milestoneWildcardId: $milestoneWildcardId
- myReactionEmoji: $myReactionEmoji
- releaseTag: $releaseTag
- releaseTagWildcardId: $releaseTagWildcardId
- types: $types
- not: $not
- ) {
- count
- }
- closedIssues: issues(
- state: closed
- iid: $iid
- search: $search
- assigneeId: $assigneeId
- assigneeUsernames: $assigneeUsernames
- authorUsername: $authorUsername
- confidential: $confidential
- labelName: $labelName
- milestoneTitle: $milestoneTitle
- milestoneWildcardId: $milestoneWildcardId
- myReactionEmoji: $myReactionEmoji
- releaseTag: $releaseTag
- releaseTagWildcardId: $releaseTagWildcardId
- types: $types
- not: $not
- ) {
- count
- }
- allIssues: issues(
- state: all
- iid: $iid
- search: $search
- assigneeId: $assigneeId
- assigneeUsernames: $assigneeUsernames
- authorUsername: $authorUsername
- confidential: $confidential
- labelName: $labelName
- milestoneTitle: $milestoneTitle
- milestoneWildcardId: $milestoneWildcardId
- myReactionEmoji: $myReactionEmoji
- releaseTag: $releaseTag
- releaseTagWildcardId: $releaseTagWildcardId
- types: $types
- not: $not
- ) {
- count
- }
- }
-}
diff --git a/app/assets/javascripts/issues/list/queries/get_issues_without_crm.query.graphql b/app/assets/javascripts/issues/list/queries/get_issues_without_crm.query.graphql
deleted file mode 100644
index 4a8b1dfd618..00000000000
--- a/app/assets/javascripts/issues/list/queries/get_issues_without_crm.query.graphql
+++ /dev/null
@@ -1,94 +0,0 @@
-#import "~/graphql_shared/fragments/page_info.fragment.graphql"
-#import "./issue.fragment.graphql"
-
-query getIssuesWithoutCrm(
- $hideUsers: Boolean = false
- $isProject: Boolean = false
- $isSignedIn: Boolean = false
- $fullPath: ID!
- $iid: String
- $search: String
- $sort: IssueSort
- $state: IssuableState
- $assigneeId: String
- $assigneeUsernames: [String!]
- $authorUsername: String
- $confidential: Boolean
- $labelName: [String]
- $milestoneTitle: [String]
- $milestoneWildcardId: MilestoneWildcardId
- $myReactionEmoji: String
- $releaseTag: [String!]
- $releaseTagWildcardId: ReleaseTagWildcardId
- $types: [IssueType!]
- $not: NegatedIssueFilterInput
- $beforeCursor: String
- $afterCursor: String
- $firstPageSize: Int
- $lastPageSize: Int
-) {
- group(fullPath: $fullPath) @skip(if: $isProject) {
- id
- issues(
- includeSubgroups: true
- iid: $iid
- search: $search
- sort: $sort
- state: $state
- assigneeId: $assigneeId
- assigneeUsernames: $assigneeUsernames
- authorUsername: $authorUsername
- confidential: $confidential
- labelName: $labelName
- milestoneTitle: $milestoneTitle
- milestoneWildcardId: $milestoneWildcardId
- myReactionEmoji: $myReactionEmoji
- types: $types
- not: $not
- before: $beforeCursor
- after: $afterCursor
- first: $firstPageSize
- last: $lastPageSize
- ) {
- pageInfo {
- ...PageInfo
- }
- nodes {
- ...IssueFragment
- reference(full: true)
- }
- }
- }
- project(fullPath: $fullPath) @include(if: $isProject) {
- id
- issues(
- iid: $iid
- search: $search
- sort: $sort
- state: $state
- assigneeId: $assigneeId
- assigneeUsernames: $assigneeUsernames
- authorUsername: $authorUsername
- confidential: $confidential
- labelName: $labelName
- milestoneTitle: $milestoneTitle
- milestoneWildcardId: $milestoneWildcardId
- myReactionEmoji: $myReactionEmoji
- releaseTag: $releaseTag
- releaseTagWildcardId: $releaseTagWildcardId
- types: $types
- not: $not
- before: $beforeCursor
- after: $afterCursor
- first: $firstPageSize
- last: $lastPageSize
- ) {
- pageInfo {
- ...PageInfo
- }
- nodes {
- ...IssueFragment
- }
- }
- }
-}
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue
index 4f97458dcd1..daa1632c4aa 100644
--- a/app/assets/javascripts/issues/show/components/description.vue
+++ b/app/assets/javascripts/issues/show/components/description.vue
@@ -12,6 +12,7 @@ import Vue from 'vue';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
import createFlash from '~/flash';
+import { IssuableType } from '~/issues/constants';
import { isPositiveInteger } from '~/lib/utils/number_utils';
import { getParameterByName, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
import { __, s__, sprintf } from '~/locale';
@@ -66,7 +67,7 @@ export default {
issuableType: {
type: String,
required: false,
- default: 'issue',
+ default: IssuableType.Issue,
},
updateUrl: {
type: String,
@@ -177,7 +178,9 @@ export default {
onError: this.taskListUpdateError.bind(this),
});
- this.renderSortableLists();
+ if (this.issuableType === IssuableType.Issue) {
+ this.renderSortableLists();
+ }
}
},
renderSortableLists() {
@@ -185,6 +188,10 @@ export default {
const lists = document.querySelectorAll('.description ul, .description ol');
lists.forEach((list) => {
+ if (list.children.length <= 1) {
+ return;
+ }
+
Array.from(list.children).forEach((listItem) => {
listItem.prepend(this.createDragIconElement());
this.addPointerEventListeners(listItem);
@@ -211,13 +218,18 @@ export default {
},
addPointerEventListeners(listItem) {
const pointeroverListener = (event) => {
- if (isDragging() || this.isUpdating) {
+ const dragIcon = event.target.closest('li').querySelector('.drag-icon');
+ if (!dragIcon || isDragging() || this.isUpdating) {
return;
}
- event.target.closest('li').querySelector('.drag-icon').style.visibility = 'visible'; // eslint-disable-line no-param-reassign
+ dragIcon.style.visibility = 'visible';
};
const pointeroutListener = (event) => {
- event.target.closest('li').querySelector('.drag-icon').style.visibility = 'hidden'; // eslint-disable-line no-param-reassign
+ const dragIcon = event.target.closest('li').querySelector('.drag-icon');
+ if (!dragIcon) {
+ return;
+ }
+ dragIcon.style.visibility = 'hidden';
};
// We use pointerover/pointerout instead of CSS so that when we hover over a
diff --git a/app/assets/javascripts/issues/show/utils.js b/app/assets/javascripts/issues/show/utils.js
index 60e66f59f92..05b06586362 100644
--- a/app/assets/javascripts/issues/show/utils.js
+++ b/app/assets/javascripts/issues/show/utils.js
@@ -1,39 +1,35 @@
import { COLON, HYPHEN, NEWLINE } from '~/lib/utils/text_utility';
/**
- * Get the index from sourcepos that represents the line of
- * the description when the description is split by newline.
+ * Returns the start and end `sourcepos` rows, converted to zero-based numbering.
*
* @param {String} sourcepos Source position in format `23:3-23:14`
- * @returns {Number} Index of description split by newline
+ * @returns {Array} Start and end `sourcepos` rows, zero-based numbered
*/
-const getDescriptionIndex = (sourcepos) => {
- const [startRange] = sourcepos.split(HYPHEN);
+const getSourceposRows = (sourcepos) => {
+ const [startRange, endRange] = sourcepos.split(HYPHEN);
const [startRow] = startRange.split(COLON);
- return startRow - 1;
+ const [endRow] = endRange.split(COLON);
+ return [startRow - 1, endRow - 1];
};
/**
- * Given a `ul` or `ol` element containing a new sort order, this function performs
- * a depth-first search to get the new sort order in the form of sourcepos indices.
+ * Given a `ul` or `ol` element containing a new sort order, this function returns
+ * an array of this new order which is derived from its list items' sourcepos values.
*
* @param {HTMLElement} list A `ul` or `ol` element containing a new sort order
- * @returns {Array} An array representing the new order of the list
+ * @returns {Array} A numerical array representing the new order of the list.
+ * The numbers represent the rows of the original markdown source.
*/
const getNewSourcePositions = (list) => {
const newSourcePositions = [];
- function pushPositionOfChildListItems(el) {
- if (!el) {
- return;
+ Array.from(list.children).forEach((listItem) => {
+ const [start, end] = getSourceposRows(listItem.dataset.sourcepos);
+ for (let i = start; i <= end; i += 1) {
+ newSourcePositions.push(i);
}
- if (el.tagName === 'LI') {
- newSourcePositions.push(getDescriptionIndex(el.dataset.sourcepos));
- }
- Array.from(el.children).forEach(pushPositionOfChildListItems);
- }
-
- pushPositionOfChildListItems(list);
+ });
return newSourcePositions;
};
@@ -56,17 +52,17 @@ const getNewSourcePositions = (list) => {
* And a reordered list (due to dragging Item 2 into Item 1's position) like:
*
*
- *
- * -
+ *
+ * -
* Item 2
- *
- * - Item 3
- * - Item 4
+ *
*
* - Item 1
- * - Item 5
- *
*
*
* This function returns:
@@ -87,7 +83,7 @@ const getNewSourcePositions = (list) => {
*/
export const convertDescriptionWithNewSort = (description, list) => {
const descriptionLines = description.split(NEWLINE);
- const startIndexOfList = getDescriptionIndex(list.dataset.sourcepos);
+ const [startIndexOfList] = getSourceposRows(list.dataset.sourcepos);
getNewSourcePositions(list)
.map((lineIndex) => descriptionLines[lineIndex])
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
index 63c492c8bcd..09d588aaafd 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
@@ -137,9 +137,8 @@ export default {
class="gl-text-decoration-underline gl-text-blue-600! gl-mr-3"
data-testid="pipeline-url-link"
data-qa-selector="pipeline_url_link"
+ >#{{ pipeline[pipelineKey] }}
- #{{ pipeline[pipelineKey] }}
-
import { GlTooltipDirective, GlLink } from '@gitlab/ui';
import { IssuableType } from '~/issues/constants';
+import { isGid, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import { isUserBusy } from '~/set_status_modal/utils';
import AssigneeAvatar from './assignee_avatar.vue';
@@ -94,6 +95,9 @@ export default {
assigneeUrl() {
return this.user.web_url || this.user.webUrl;
},
+ assigneeId() {
+ return isGid(this.user.id) ? getIdFromGraphQLId(this.user.id) : this.user.id;
+ },
},
};
@@ -103,7 +107,7 @@ export default {
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
index bed6dd4d5c6..0d78530d878 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
@@ -134,3 +134,7 @@ export const BIDI_CHARS_CLASS_LIST = 'unicode-bidi has-tooltip';
export const BIDI_CHAR_TOOLTIP = __(
'Potentially unwanted character detected: Unicode BiDi Control',
);
+
+export const HLJS_COMMENT_SELECTOR = 'hljs-comment';
+
+export const HLJS_ON_AFTER_HIGHLIGHT = 'after:highlight';
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js
new file mode 100644
index 00000000000..c9f7e5508be
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js
@@ -0,0 +1,13 @@
+import { HLJS_ON_AFTER_HIGHLIGHT } from '../constants';
+import wrapComments from './wrap_comments';
+
+/**
+ * Registers our plugins for Highlight.js
+ *
+ * Plugin API: https://github.com/highlightjs/highlight.js/blob/main/docs/plugin-api.rst
+ *
+ * @param {Object} hljs - the Highlight.js instance.
+ */
+export const registerPlugins = (hljs) => {
+ hljs.addPlugin({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapComments });
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js
new file mode 100644
index 00000000000..4a18733de94
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js
@@ -0,0 +1,38 @@
+import { HLJS_COMMENT_SELECTOR } from '../constants';
+
+const createWrapper = (content) => {
+ const span = document.createElement('span');
+ span.className = HLJS_COMMENT_SELECTOR;
+ span.innerHTML = content;
+ return span.outerHTML;
+};
+
+/**
+ * Highlight.js plugin for wrapping multi-line comments in the `hljs-comment` class.
+ * This ensures that multi-line comments are rendered correctly in the GitLab UI.
+ *
+ * Plugin API: https://github.com/highlightjs/highlight.js/blob/main/docs/plugin-api.rst
+ *
+ * @param {Object} Result - an object that represents the highlighted result from Highlight.js
+ */
+export default (result) => {
+ if (!result.value.includes(HLJS_COMMENT_SELECTOR)) return;
+
+ let wrapComment = false;
+
+ // eslint-disable-next-line no-param-reassign
+ result.value = result.value // Highlight.js expects the result param to be mutated for plugins to work
+ .split('\n')
+ .map((lineContent) => {
+ if (lineContent.includes(HLJS_COMMENT_SELECTOR)) {
+ wrapComment = true;
+ return lineContent;
+ }
+ const line = wrapComment ? createWrapper(lineContent) : lineContent;
+ if (lineContent.includes('')) {
+ wrapComment = false;
+ }
+ return line;
+ })
+ .join('\n');
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
index ed87a202b15..f819a9e5be2 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
@@ -5,6 +5,7 @@ import eventHub from '~/notes/event_hub';
import languageLoader from '~/content_editor/services/highlight_js_language_loader';
import { ROUGE_TO_HLJS_LANGUAGE_MAP, LINES_PER_CHUNK } from './constants';
import Chunk from './components/chunk.vue';
+import { registerPlugins } from './plugins/index';
/*
* This component is optimized to handle source code with many lines of code by splitting source code into chunks of 70 lines of code,
@@ -111,6 +112,7 @@ export default {
let detectedLanguage = language;
let highlightedContent;
if (this.hljs) {
+ registerPlugins(this.hljs);
if (!detectedLanguage) {
const hljsHighlightAuto = this.hljs.highlightAuto(content);
highlightedContent = hljsHighlightAuto.value;
diff --git a/app/assets/stylesheets/page_bundles/issues_show.scss b/app/assets/stylesheets/page_bundles/issues_show.scss
index 9873a0121c0..63b0bcc0c7f 100644
--- a/app/assets/stylesheets/page_bundles/issues_show.scss
+++ b/app/assets/stylesheets/page_bundles/issues_show.scss
@@ -3,8 +3,8 @@
.description {
ul,
ol {
- /* We're changing list-style-position to inside because the default of outside
- * doesn't move the negative margin to the left of the bullet. */
+ /* We're changing list-style-position to inside because the default of
+ * outside doesn't move negative margin to the left of the bullet. */
list-style-position: inside;
}
@@ -21,6 +21,26 @@
inset-block-start: 0.3rem;
inset-inline-start: 1rem;
}
+
+ /* The inside bullet aligns itself to the bottom, which we see when text to its right wraps.
+ * We fix this by aligning it to the top. Targeting ::marker doesn't seem to work. */
+ > * {
+ vertical-align: top;
+ }
+
+ /* The inside bullet is treated like an element inside the li element, so when we have a
+ * multi-paragraph list item, the text doesn't start on the right of the bullet because
+ * it is a block level p element. We make it inline to fix this. */
+ > p:first-of-type {
+ display: inline-block;
+ max-width: calc(100% - 1.5rem);
+ }
+
+ /* We fix the other paragraphs not indenting to the
+ * right of the bullet due to the inside bullet. */
+ > :not(p:first-of-type):not(.drag-icon):not(.task-list-item-checkbox):not(.gfm-issue):not(.js-add-task) {
+ margin-inline-start: 1rem;
+ }
}
ul.task-list {
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index fada49ca40d..4736d441a4b 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -55,15 +55,6 @@
box-shadow ease-in-out 0.15s;
background-color: $white;
- &.is-focused {
- @include gl-focus;
-
- .comment-toolbar,
- .nav-links {
- border-color: $blue-300;
- }
- }
-
&.is-dropzone-hover {
border-color: $green-500;
box-shadow: 0 0 2px $black-transparent,
@@ -80,6 +71,10 @@
@include gl-shadow-none;
}
}
+
+ .comment-warning-wrapper:focus-within {
+ @include gl-focus;
+ }
}
.md-header .nav-links {
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 47b652431d3..9c3e337c0c2 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -553,7 +553,7 @@ class Namespace < ApplicationRecord
private
def cluster_enabled_granted?
- root_ancestor.cluster_enabled_grant.present? && (Gitlab.com? || Gitlab.dev_or_test_env?)
+ (Gitlab.com? || Gitlab.dev_or_test_env?) && root_ancestor.cluster_enabled_grant.present?
end
def certificate_based_clusters_enabled_ff?
diff --git a/app/serializers/issue_board_entity.rb b/app/serializers/issue_board_entity.rb
index bcad28d6aad..ebd0f037160 100644
--- a/app/serializers/issue_board_entity.rb
+++ b/app/serializers/issue_board_entity.rb
@@ -3,6 +3,10 @@
class IssueBoardEntity < Grape::Entity
include RequestAwareEntity
+ format_with(:upcase) do |item|
+ item.try(:upcase)
+ end
+
expose :id
expose :iid
expose :title
@@ -51,6 +55,11 @@ class IssueBoardEntity < Grape::Entity
expose :assignable_labels_endpoint, if: -> (issue) { issue.project } do |issue|
project_labels_path(issue.project, format: :json, include_ancestor_groups: true)
end
+
+ expose :issue_type,
+ as: :type,
+ format_with: :upcase,
+ documentation: { type: "String", desc: "One of #{::WorkItems::Type.base_types.keys.map(&:upcase)}" }
end
IssueBoardEntity.prepend_mod_with('IssueBoardEntity')
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index 852a2e62b7d..eba2c49bc2e 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -3,6 +3,10 @@
class IssueEntity < IssuableEntity
include TimeTrackableEntity
+ format_with(:upcase) do |item|
+ item.try(:upcase)
+ end
+
expose :state
expose :milestone_id
expose :updated_by_id
@@ -75,6 +79,11 @@ class IssueEntity < IssuableEntity
expose :issue_email_participants do |issue|
issue.issue_email_participants.map { |x| { email: x.email } }
end
+
+ expose :issue_type,
+ as: :type,
+ format_with: :upcase,
+ documentation: { type: "String", desc: "One of #{::WorkItems::Type.base_types.keys.map(&:upcase)}" }
end
IssueEntity.prepend_mod_with('IssueEntity')
diff --git a/app/services/git/branch_push_service.rb b/app/services/git/branch_push_service.rb
index 3c27ad56ebb..91f14251608 100644
--- a/app/services/git/branch_push_service.rb
+++ b/app/services/git/branch_push_service.rb
@@ -39,6 +39,7 @@ module Git
def enqueue_update_mrs
return if params[:merge_request_branches]&.exclude?(branch_name)
+ # TODO: pass params[:push_options] to worker
UpdateMergeRequestsWorker.perform_async(
project.id,
current_user.id,
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 44be254441d..350b37aba68 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -185,9 +185,12 @@ module MergeRequests
def create_pipeline_for(merge_request, user, async: false)
if async
+ # TODO: pass push_options to worker
MergeRequests::CreatePipelineWorker.perform_async(project.id, user.id, merge_request.id)
else
- MergeRequests::CreatePipelineService.new(project: project, current_user: user).execute(merge_request)
+ MergeRequests::CreatePipelineService
+ .new(project: project, current_user: user, params: params.slice(:push_options))
+ .execute(merge_request)
end
end
diff --git a/app/services/merge_requests/create_pipeline_service.rb b/app/services/merge_requests/create_pipeline_service.rb
index 9d7f8393ba5..37c734613e7 100644
--- a/app/services/merge_requests/create_pipeline_service.rb
+++ b/app/services/merge_requests/create_pipeline_service.rb
@@ -9,9 +9,11 @@ module MergeRequests
end
def create_detached_merge_request_pipeline(merge_request)
- Ci::CreatePipelineService.new(pipeline_project(merge_request),
- current_user,
- ref: pipeline_ref_for_detached_merge_request_pipeline(merge_request))
+ Ci::CreatePipelineService
+ .new(pipeline_project(merge_request),
+ current_user,
+ ref: pipeline_ref_for_detached_merge_request_pipeline(merge_request),
+ push_options: params[:push_options])
.execute(:merge_request_event, merge_request: merge_request)
end
diff --git a/app/workers/merge_requests/create_pipeline_worker.rb b/app/workers/merge_requests/create_pipeline_worker.rb
index ee42a3dee08..b40408cf647 100644
--- a/app/workers/merge_requests/create_pipeline_worker.rb
+++ b/app/workers/merge_requests/create_pipeline_worker.rb
@@ -15,7 +15,7 @@ module MergeRequests
worker_resource_boundary :cpu
idempotent!
- def perform(project_id, user_id, merge_request_id)
+ def perform(project_id, user_id, merge_request_id, params = {})
project = Project.find_by_id(project_id)
return unless project
@@ -25,7 +25,12 @@ module MergeRequests
merge_request = MergeRequest.find_by_id(merge_request_id)
return unless merge_request
- MergeRequests::CreatePipelineService.new(project: project, current_user: user).execute(merge_request)
+ push_options = params.with_indifferent_access[:push_options]
+
+ MergeRequests::CreatePipelineService
+ .new(project: project, current_user: user, params: { push_options: push_options })
+ .execute(merge_request)
+
merge_request.update_head_pipeline
end
end
diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb
index 5c96257cb63..eb69c0eaba6 100644
--- a/app/workers/update_merge_requests_worker.rb
+++ b/app/workers/update_merge_requests_worker.rb
@@ -13,13 +13,17 @@ class UpdateMergeRequestsWorker # rubocop:disable Scalability/IdempotentWorker
weight 3
loggable_arguments 2, 3, 4
- def perform(project_id, user_id, oldrev, newrev, ref)
+ def perform(project_id, user_id, oldrev, newrev, ref, params = {})
project = Project.find_by_id(project_id)
return unless project
user = User.find_by_id(user_id)
return unless user
- MergeRequests::RefreshService.new(project: project, current_user: user).execute(oldrev, newrev, ref)
+ push_options = params.with_indifferent_access[:push_options]
+
+ MergeRequests::RefreshService
+ .new(project: project, current_user: user, params: { push_options: push_options })
+ .execute(oldrev, newrev, ref)
end
end
diff --git a/config/initializers/zz_metrics.rb b/config/initializers/zz_metrics.rb
index 88469d2cdef..5e6c1abdda6 100644
--- a/config/initializers/zz_metrics.rb
+++ b/config/initializers/zz_metrics.rb
@@ -32,7 +32,9 @@ if Gitlab::Metrics.enabled? && !Rails.env.test? && !(Rails.env.development? && d
config.middleware.insert_before Gitlab::Database::LoadBalancing::RackMiddleware,
Gitlab::Metrics::RackMiddleware
- config.middleware.use(Gitlab::Middleware::RailsQueueDuration)
+ config.middleware.insert_before Gitlab::Database::LoadBalancing::RackMiddleware,
+ Gitlab::Middleware::RailsQueueDuration
+
config.middleware.use(Gitlab::Metrics::ElasticsearchRackMiddleware)
end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 11c232df4e0..fc75752061f 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -47,6 +47,8 @@
- 1
- - audit_events_user_impersonation_event_create
- 1
+- - auth_saml_group_sync
+ - 1
- - authorized_keys
- 2
- - authorized_project_update
diff --git a/doc/administration/audit_event_streaming.md b/doc/administration/audit_event_streaming.md
index 5ccecfa9c8c..55f3cd7fd7a 100644
--- a/doc/administration/audit_event_streaming.md
+++ b/doc/administration/audit_event_streaming.md
@@ -158,9 +158,11 @@ the destination's value when [listing streaming destinations](#list-streaming-de
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/332747) in GitLab 14.9 [with a flag](../administration/feature_flags.md) named `audit_event_streaming_git_operations`. Disabled by default.
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/357211) in GitLab 15.0.
+> - [Enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/357211) in GitLab 15.1 by default.
FLAG:
-On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](feature_flags.md) named `audit_event_streaming_git_operations`. On GitLab.com, this feature is available.
+On self-managed GitLab, by default this feature is available. To hide the
+feature, ask an administrator to [disable the feature flag](feature_flags.md) named `audit_event_streaming_git_operations`.
Streaming audit events can be sent when signed-in users push or pull a project's remote Git repositories:
diff --git a/doc/user/packages/infrastructure_registry/index.md b/doc/user/packages/infrastructure_registry/index.md
index 47f563fd7e7..551289a575a 100644
--- a/doc/user/packages/infrastructure_registry/index.md
+++ b/doc/user/packages/infrastructure_registry/index.md
@@ -15,19 +15,13 @@ projects.
## View packages
-To view packages within your project or group:
+To view packages within your project:
-1. Go to the project or group.
+1. Go to the project.
1. Go to **Packages & Registries > Infrastructure Registry**.
You can search, sort, and filter packages on this page.
-When you view packages in a group:
-
-- All packages published to the group and its projects are displayed.
-- Only the projects you can access are displayed.
-- If a project is private, or you are not a member of the project, it is not displayed.
-
For information on how to create and upload a package, view the GitLab
documentation for your package type:
@@ -68,7 +62,7 @@ To delete a package, you must have suitable [permissions](../../permissions.md).
You can delete packages by using [the API](../../../api/packages.md#delete-a-project-package) or the UI.
-To delete a package in the UI, from your group or project:
+To delete a package in the UI, from your project:
1. Go to **Packages & Registries > Infrastructure Registry**.
1. Find the name of the package you want to delete.
diff --git a/lib/gitlab/database/migrations/batched_background_migration_helpers.rb b/lib/gitlab/database/migrations/batched_background_migration_helpers.rb
index 079a14824c0..0ba7e1b0fd0 100644
--- a/lib/gitlab/database/migrations/batched_background_migration_helpers.rb
+++ b/lib/gitlab/database/migrations/batched_background_migration_helpers.rb
@@ -148,14 +148,6 @@ module Gitlab
'your migration class.'
end
- database_name = Gitlab::Database.db_config_name(connection)
-
- unless ActiveRecord::Base.configurations.primary?(database_name)
- raise 'The `#finalize_background_migration` is currently not supported when running in decomposed database, ' \
- 'and this database is not `main:`. For more information visit: ' \
- 'https://docs.gitlab.com/ee/development/database/migrations_for_multiple_databases.html'
- end
-
Gitlab::Database::BackgroundMigration::BatchedMigration.reset_column_information
migration = Gitlab::Database::BackgroundMigration::BatchedMigration.find_for_configuration(
@@ -163,7 +155,14 @@ module Gitlab
raise 'Could not find batched background migration' if migration.nil?
- Gitlab::Database::BackgroundMigration::BatchedMigrationRunner.finalize(job_class_name, table_name, column_name, job_arguments, connection: connection)
+ with_restored_connection_stack do |restored_connection|
+ Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas.with_suppressed do
+ Gitlab::Database::BackgroundMigration::BatchedMigrationRunner.finalize(
+ job_class_name, table_name,
+ column_name, job_arguments,
+ connection: restored_connection)
+ end
+ end
end
# Deletes batched background migration for the given configuration.
diff --git a/lib/gitlab/database/migrations/reestablished_connection_stack.rb b/lib/gitlab/database/migrations/reestablished_connection_stack.rb
index d7cf482c32a..addc9d874af 100644
--- a/lib/gitlab/database/migrations/reestablished_connection_stack.rb
+++ b/lib/gitlab/database/migrations/reestablished_connection_stack.rb
@@ -17,7 +17,9 @@ module Gitlab
original_handler = ActiveRecord::Base.connection_handler
original_db_config = ActiveRecord::Base.connection_db_config
- return yield if ActiveRecord::Base.configurations.primary?(original_db_config.name)
+ if ActiveRecord::Base.configurations.primary?(original_db_config.name)
+ return yield(ActiveRecord::Base.connection)
+ end
# If the `ActiveRecord::Base` connection is different than `:main`
# re-establish and configure `SharedModel` context accordingly
@@ -43,7 +45,7 @@ module Gitlab
ActiveRecord::Base.establish_connection :main # rubocop:disable Database/EstablishConnection
Gitlab::Database::SharedModel.using_connection(base_model.connection) do
- yield
+ yield(base_model.connection)
end
ensure
ActiveRecord::Base.connection_handler = original_handler
diff --git a/lib/gitlab/grape_logging/loggers/queue_duration_logger.rb b/lib/gitlab/grape_logging/loggers/queue_duration_logger.rb
index fe741a5bbe8..dde2bdd855e 100644
--- a/lib/gitlab/grape_logging/loggers/queue_duration_logger.rb
+++ b/lib/gitlab/grape_logging/loggers/queue_duration_logger.rb
@@ -6,21 +6,12 @@ module Gitlab
module GrapeLogging
module Loggers
class QueueDurationLogger < ::GrapeLogging::Loggers::Base
- attr_accessor :start_time
-
- def before
- @start_time = Time.now
- end
-
def parameters(request, _)
- proxy_start = request.env['HTTP_GITLAB_WORKHORSE_PROXY_START'].presence
+ duration_s = request.env[Gitlab::Middleware::RailsQueueDuration::GITLAB_RAILS_QUEUE_DURATION_KEY].presence
- return {} unless proxy_start && start_time
+ return {} unless duration_s
- # Time in milliseconds since gitlab-workhorse started the request
- duration = start_time.to_f * 1_000 - proxy_start.to_f / 1_000_000
-
- { 'queue_duration_s': Gitlab::Utils.ms_to_round_sec(duration) }
+ { 'queue_duration_s': duration_s }
end
end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 8a03c1e709b..a85a4b9b3e6 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -145,13 +145,18 @@ RSpec.describe Projects::IssuesController do
project.add_developer(user)
end
- it "returns issue_email_participants" do
+ it "returns issue attributes" do
participants = create_list(:issue_email_participant, 2, issue: issue)
get :show, params: { namespace_id: project.namespace, project_id: project, id: issue.iid }, format: :json
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['issue_email_participants']).to contain_exactly({ "email" => participants[0].email }, { "email" => participants[1].email })
+ expect(json_response).to include(
+ 'issue_email_participants' => contain_exactly(
+ { "email" => participants[0].email }, { "email" => participants[1].email }
+ ),
+ 'type' => 'ISSUE'
+ )
end
end
diff --git a/spec/fixtures/api/schemas/entities/issue.json b/spec/fixtures/api/schemas/entities/issue.json
index 31743b58d98..b4a076780d9 100644
--- a/spec/fixtures/api/schemas/entities/issue.json
+++ b/spec/fixtures/api/schemas/entities/issue.json
@@ -3,6 +3,7 @@
"properties" : {
"id": { "type": "integer" },
"iid": { "type": "integer" },
+ "type": { "type": "string" },
"author_id": { "type": "integer" },
"description": { "type": ["string", "null"] },
"lock_version": { "type": ["integer", "null"] },
diff --git a/spec/fixtures/api/schemas/entities/issue_board.json b/spec/fixtures/api/schemas/entities/issue_board.json
index 58d3832440c..aa29ca08163 100644
--- a/spec/fixtures/api/schemas/entities/issue_board.json
+++ b/spec/fixtures/api/schemas/entities/issue_board.json
@@ -3,6 +3,7 @@
"properties" : {
"id": { "type": "integer" },
"iid": { "type": "integer" },
+ "type": { "type": "string" },
"title": { "type": "string" },
"confidential": { "type": "boolean" },
"closed": { "type": "boolean" },
diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js
index 7de653e3069..4780851e9c1 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -8,8 +8,6 @@ import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql';
-import getIssuesWithoutCrmQuery from 'ee_else_ce/issues/list/queries/get_issues_without_crm.query.graphql';
-import getIssuesCountsWithoutCrmQuery from 'ee_else_ce/issues/list/queries/get_issues_counts_without_crm.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
@@ -121,8 +119,6 @@ describe('CE IssuesListApp component', () => {
const requestHandlers = [
[getIssuesQuery, issuesQueryResponse],
[getIssuesCountsQuery, issuesCountsQueryResponse],
- [getIssuesWithoutCrmQuery, issuesQueryResponse],
- [getIssuesCountsWithoutCrmQuery, issuesCountsQueryResponse],
[setSortPreferenceMutation, sortPreferenceMutationResponse],
];
diff --git a/spec/frontend/issues/show/utils_spec.js b/spec/frontend/issues/show/utils_spec.js
index e5f14cfc01a..603fb5cc2a6 100644
--- a/spec/frontend/issues/show/utils_spec.js
+++ b/spec/frontend/issues/show/utils_spec.js
@@ -2,7 +2,7 @@ import { convertDescriptionWithNewSort } from '~/issues/show/utils';
describe('app/assets/javascripts/issues/show/utils.js', () => {
describe('convertDescriptionWithNewSort', () => {
- it('converts markdown description with new list sort order', () => {
+ it('converts markdown description with nested lists with new list sort order', () => {
const description = `I am text
- Item 1
@@ -12,17 +12,17 @@ describe('app/assets/javascripts/issues/show/utils.js', () => {
- Item 5`;
// Drag Item 2 + children to Item 1's position
- const html = `
- -
+ const html = `
+ -
Item 2
-
- Item 1
- - Item 5
- `;
const list = document.createElement('div');
list.innerHTML = html;
@@ -36,5 +36,105 @@ describe('app/assets/javascripts/issues/show/utils.js', () => {
expect(convertDescriptionWithNewSort(description, list.firstChild)).toBe(expected);
});
+
+ it('converts markdown description with multi-line list items with new list sort order', () => {
+ const description = `Labore ea omnis et officia excepturi.
+
+1. Item 1
+
+ Item 1 part 2
+
+1. Item 2
+ - Item 2.1
+ - Item 2.1.1
+ - Item 2.1.2
+ - Item 2.2
+ - Item 2.3
+1. Item 3
+1. Item 4
+
+ \`\`\`
+ const variable = 'string';
+ \`\`\`
+
+ 
+
+ last paragraph
+
+1. Item 5
+1. Item 6`;
+
+ // Drag Item 2 + children to Item 5's position
+ const html = `
+ -
+
Item 1
+ Item 1 part 2
+
+ -
+
Item 3
+
+ -
+
Item 4
+
+
+ const variabl = 'string';
+
+
+
+
+
+ last paragraph
+
+ -
+
Item 5
+
+ -
+
Item 2
+
+ - Item 2.1
+
+ - Item 2.1.1
+ - Item 2.1.2
+
+
+ - Item 2.2
+ - Item 2.3
+
+
+ -
+
Item 6
+
+
`;
+ const list = document.createElement('div');
+ list.innerHTML = html;
+
+ const expected = `Labore ea omnis et officia excepturi.
+
+1. Item 1
+
+ Item 1 part 2
+
+1. Item 3
+1. Item 4
+
+ \`\`\`
+ const variable = 'string';
+ \`\`\`
+
+ 
+
+ last paragraph
+
+1. Item 5
+1. Item 2
+ - Item 2.1
+ - Item 2.1.1
+ - Item 2.1.2
+ - Item 2.2
+ - Item 2.3
+1. Item 6`;
+
+ expect(convertDescriptionWithNewSort(description, list.firstChild)).toBe(expected);
+ });
});
});
diff --git a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
index 69f6a6e6e04..a286eeef14f 100644
--- a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
@@ -1,5 +1,8 @@
import { shallowMount } from '@vue/test-utils';
+import { GlLink } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
+import { TYPE_USER } from '~/graphql_shared/constants';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
import AssigneeAvatar from '~/sidebar/components/assignees/assignee_avatar.vue';
import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue';
import userDataMock from '../../user_data_mock';
@@ -32,6 +35,7 @@ describe('AssigneeAvatarLink component', () => {
});
const findTooltipText = () => wrapper.attributes('title');
+ const findUserLink = () => wrapper.findComponent(GlLink);
it('has the root url present in the assigneeUrl method', () => {
createComponent();
@@ -112,4 +116,24 @@ describe('AssigneeAvatarLink component', () => {
});
},
);
+
+ it('passes the correct user id for REST API', () => {
+ createComponent({
+ tooltipHasName: true,
+ user: userDataMock(),
+ });
+
+ expect(findUserLink().attributes('data-user-id')).toBe(String(userDataMock().id));
+ });
+
+ it('passes the correct user id for GraphQL API', () => {
+ const userId = userDataMock().id;
+
+ createComponent({
+ tooltipHasName: true,
+ user: { ...userDataMock(), id: convertToGraphQLId(TYPE_USER, userId) },
+ });
+
+ expect(findUserLink().attributes('data-user-id')).toBe(String(userId));
+ });
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js
new file mode 100644
index 00000000000..83fdc5d669d
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js
@@ -0,0 +1,14 @@
+import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
+import { HLJS_ON_AFTER_HIGHLIGHT } from '~/vue_shared/components/source_viewer/constants';
+import wrapComments from '~/vue_shared/components/source_viewer/plugins/wrap_comments';
+
+jest.mock('~/vue_shared/components/source_viewer/plugins/wrap_comments');
+const hljsMock = { addPlugin: jest.fn() };
+
+describe('Highlight.js plugin registration', () => {
+ beforeEach(() => registerPlugins(hljsMock));
+
+ it('registers our plugins', () => {
+ expect(hljsMock.addPlugin).toHaveBeenCalledWith({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapComments });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_comments_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_comments_spec.js
new file mode 100644
index 00000000000..5c0cee7781e
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_comments_spec.js
@@ -0,0 +1,20 @@
+import { HLJS_COMMENT_SELECTOR } from '~/vue_shared/components/source_viewer/constants';
+import wrapComments from '~/vue_shared/components/source_viewer/plugins/wrap_comments';
+
+describe('Highlight.js plugin for wrapping comments', () => {
+ it('mutates the input value by wrapping each line in a span tag', () => {
+ const inputValue = ``;
+ const outputValue = `