Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
3e53902ee1
commit
5e555ebcf6
|
@ -456,12 +456,7 @@ export default {
|
|||
<p class="gl-mb-5">
|
||||
{{ $options.i18n.autoCollapsed }}
|
||||
</p>
|
||||
<gl-button
|
||||
data-testid="expand-button"
|
||||
category="secondary"
|
||||
variant="warning"
|
||||
@click.prevent="handleToggle"
|
||||
>
|
||||
<gl-button data-testid="expand-button" @click.prevent="handleToggle">
|
||||
{{ $options.i18n.expand }}
|
||||
</gl-button>
|
||||
</div>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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<Number>} 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<Number>} An array representing the new order of the list
|
||||
* @returns {Array<Number>} 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:
|
||||
*
|
||||
* <pre>
|
||||
* <ul data-sourcepos="3:1-8:0">
|
||||
* <li data-sourcepos="4:1-4:8">
|
||||
* <ul data-sourcepos="3:1-7:8">
|
||||
* <li data-sourcepos="4:1-6:10">
|
||||
* Item 2
|
||||
* <ul data-sourcepos="5:1-6:10">
|
||||
* <li data-sourcepos="5:1-5:10">Item 3</li>
|
||||
* <li data-sourcepos="6:1-6:10">Item 4</li>
|
||||
* <ul data-sourcepos="5:3-6:10">
|
||||
* <li data-sourcepos="5:3-5:10">Item 3</li>
|
||||
* <li data-sourcepos="6:3-6:10">Item 4</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li data-sourcepos="3:1-3:8">Item 1</li>
|
||||
* <li data-sourcepos="7:1-8:0">Item 5</li>
|
||||
* <ul>
|
||||
* <li data-sourcepos="7:1-7:8">Item 5</li>
|
||||
* </ul>
|
||||
* </pre>
|
||||
*
|
||||
* 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])
|
||||
|
|
|
@ -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] }}</gl-link
|
||||
>
|
||||
#{{ pipeline[pipelineKey] }}
|
||||
</gl-link>
|
||||
<!--Commit row-->
|
||||
<div class="icon-container gl-display-inline-block gl-mr-1">
|
||||
<gl-icon
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
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;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -103,7 +107,7 @@ export default {
|
|||
<gl-link
|
||||
:href="assigneeUrl"
|
||||
:title="tooltipTitle"
|
||||
:data-user-id="user.id"
|
||||
:data-user-id="assigneeId"
|
||||
data-placement="left"
|
||||
class="gl-display-inline-block js-user-link"
|
||||
>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 });
|
||||
};
|
|
@ -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('</span>')) {
|
||||
wrapComment = false;
|
||||
}
|
||||
return line;
|
||||
})
|
||||
.join('\n');
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -47,6 +47,8 @@
|
|||
- 1
|
||||
- - audit_events_user_impersonation_event_create
|
||||
- 1
|
||||
- - auth_saml_group_sync
|
||||
- 1
|
||||
- - authorized_keys
|
||||
- 2
|
||||
- - authorized_project_update
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"] },
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"properties" : {
|
||||
"id": { "type": "integer" },
|
||||
"iid": { "type": "integer" },
|
||||
"type": { "type": "string" },
|
||||
"title": { "type": "string" },
|
||||
"confidential": { "type": "boolean" },
|
||||
"closed": { "type": "boolean" },
|
||||
|
|
|
@ -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],
|
||||
];
|
||||
|
||||
|
|
|
@ -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 = `<ul data-sourcepos="3:1-8:0">
|
||||
<li data-sourcepos="4:1-4:8">
|
||||
const html = `<ul data-sourcepos="3:1-7:8">
|
||||
<li data-sourcepos="4:1-6:10">
|
||||
Item 2
|
||||
<ul data-sourcepos="5:1-6:10">
|
||||
<li data-sourcepos="5:1-5:10">Item 3</li>
|
||||
<li data-sourcepos="6:1-6:10">Item 4</li>
|
||||
<ul data-sourcepos="5:3-6:10">
|
||||
<li data-sourcepos="5:3-5:10">Item 3</li>
|
||||
<li data-sourcepos="6:3-6:10">Item 4</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li data-sourcepos="3:1-3:8">Item 1</li>
|
||||
<li data-sourcepos="7:1-8:0">Item 5</li>
|
||||
<ul>`;
|
||||
<li data-sourcepos="7:1-7:8">Item 5</li>
|
||||
</ul>`;
|
||||
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';
|
||||
\`\`\`
|
||||
|
||||
![iii](img.jpg)
|
||||
|
||||
last paragraph
|
||||
|
||||
1. Item 5
|
||||
1. Item 6`;
|
||||
|
||||
// Drag Item 2 + children to Item 5's position
|
||||
const html = `<ol data-sourcepos="3:1-25:7">
|
||||
<li data-sourcepos="3:1-6:0">
|
||||
<p data-sourcepos="3:4-3:7">Item 1</p>
|
||||
<p data-sourcepos="5:4-5:8">Item 1 part 2</p>
|
||||
</li>
|
||||
<li data-sourcepos="13:1-13:7">
|
||||
<p data-sourcepos="13:4-13:7">Item 3</p>
|
||||
</li>
|
||||
<li data-sourcepos="14:1-23:0">
|
||||
<p data-sourcepos="14:4-14:7">Item 4</p>
|
||||
<div>
|
||||
<pre data-sourcepos="16:4-18:6">
|
||||
<code><span lang="plaintext">const variabl = 'string';</span></code>
|
||||
</pre>
|
||||
</div>
|
||||
<p data-sourcepos="20:4-20:32">
|
||||
<a href="href"><img src="img.jpg" alt="description" /></a>
|
||||
</p>
|
||||
<p data-sourcepos="22:4-22:17">last paragraph</p>
|
||||
</li>
|
||||
<li data-sourcepos="24:1-24:7">
|
||||
<p data-sourcepos="24:4-24:7">Item 5</p>
|
||||
</li>
|
||||
<li data-sourcepos="7:1-12:10">
|
||||
<p data-sourcepos="7:4-7:7">Item 2</p>
|
||||
<ul data-sourcepos="8:4-12:10">
|
||||
<li data-sourcepos="8:4-10:15">Item 2.1
|
||||
<ul data-sourcepos="9:6-10:15">
|
||||
<li data-sourcepos="9:6-9:12">Item 2.1.1</li>
|
||||
<li data-sourcepos="10:6-10:15">Item 2.1.2</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li data-sourcepos="11:4-11:10">Item 2.2</li>
|
||||
<li data-sourcepos="12:4-12:10">Item 2.3</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li data-sourcepos="25:1-25:7">
|
||||
<p data-sourcepos="25:4-25:7">Item 6</p>
|
||||
</li>
|
||||
</ol>`;
|
||||
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';
|
||||
\`\`\`
|
||||
|
||||
![iii](img.jpg)
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
});
|
|
@ -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 = `<span class="${HLJS_COMMENT_SELECTOR}">/* Line 1 \n* Line 2 \n*/</span>`;
|
||||
const outputValue = `<span class="${HLJS_COMMENT_SELECTOR}">/* Line 1 \n<span class="${HLJS_COMMENT_SELECTOR}">* Line 2 </span>\n<span class="${HLJS_COMMENT_SELECTOR}">*/</span>`;
|
||||
const hljsResultMock = { value: inputValue };
|
||||
|
||||
wrapComments(hljsResultMock);
|
||||
expect(hljsResultMock.value).toBe(outputValue);
|
||||
});
|
||||
|
||||
it('does not mutate the input value if the hljs comment selector is not present', () => {
|
||||
const inputValue = '<span class="hljs-keyword">const</span>';
|
||||
const hljsResultMock = { value: inputValue };
|
||||
|
||||
expect(hljsResultMock.value).toBe(inputValue);
|
||||
});
|
||||
});
|
|
@ -3,6 +3,7 @@ import Vue from 'vue';
|
|||
import VueRouter from 'vue-router';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
|
||||
import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
|
||||
import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
|
||||
import { ROUGE_TO_HLJS_LANGUAGE_MAP } from '~/vue_shared/components/source_viewer/constants';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
|
@ -11,6 +12,7 @@ import eventHub from '~/notes/event_hub';
|
|||
|
||||
jest.mock('~/blob/line_highlighter');
|
||||
jest.mock('highlight.js/lib/core');
|
||||
jest.mock('~/vue_shared/components/source_viewer/plugins/index');
|
||||
Vue.use(VueRouter);
|
||||
const router = new VueRouter();
|
||||
|
||||
|
@ -59,6 +61,10 @@ describe('Source Viewer component', () => {
|
|||
describe('highlight.js', () => {
|
||||
beforeEach(() => createComponent({ language: mappedLanguage }));
|
||||
|
||||
it('registers our plugins for Highlight.js', () => {
|
||||
expect(registerPlugins).toHaveBeenCalledWith(hljs);
|
||||
});
|
||||
|
||||
it('registers the language definition', async () => {
|
||||
const languageDefinition = await import(`highlight.js/lib/languages/${mappedLanguage}`);
|
||||
|
||||
|
|
|
@ -444,7 +444,7 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do
|
|||
it 'does restore connection hierarchy' do
|
||||
expect_next_instances_of(job_class, 1..) do |job|
|
||||
expect(job).to receive(:perform) do
|
||||
validate_connections!
|
||||
validate_connections_stack!
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -3,8 +3,14 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers do
|
||||
let(:migration_class) do
|
||||
Class.new(ActiveRecord::Migration[6.1])
|
||||
.include(described_class)
|
||||
.include(Gitlab::Database::Migrations::ReestablishedConnectionStack)
|
||||
end
|
||||
|
||||
let(:migration) do
|
||||
ActiveRecord::Migration.new.extend(described_class)
|
||||
migration_class.new
|
||||
end
|
||||
|
||||
describe '#queue_batched_background_migration' do
|
||||
|
@ -221,24 +227,64 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers d
|
|||
end
|
||||
end
|
||||
|
||||
context 'when uses a CI connection', :reestablished_active_record_base do
|
||||
context 'when within transaction' do
|
||||
before do
|
||||
skip_if_multiple_databases_not_setup
|
||||
|
||||
ActiveRecord::Base.establish_connection(:ci) # rubocop:disable Database/EstablishConnection
|
||||
allow(migration).to receive(:transaction_open?).and_return(true)
|
||||
end
|
||||
|
||||
it 'raises an exception' do
|
||||
ci_migration = create(:batched_background_migration, :active)
|
||||
it 'does raise an exception' do
|
||||
expect { migration.finalize_batched_background_migration(job_class_name: 'MyJobClass', table_name: :projects, column_name: :id, job_arguments: []) }
|
||||
.to raise_error /`finalize_batched_background_migration` cannot be run inside a transaction./
|
||||
end
|
||||
end
|
||||
|
||||
expect do
|
||||
migration.finalize_batched_background_migration(
|
||||
job_class_name: ci_migration.job_class_name,
|
||||
table_name: ci_migration.table_name,
|
||||
column_name: ci_migration.column_name,
|
||||
job_arguments: ci_migration.job_arguments
|
||||
)
|
||||
end.to raise_error /is currently not supported when running in decomposed/
|
||||
context 'when running migration in reconfigured ActiveRecord::Base context' do
|
||||
it_behaves_like 'reconfigures connection stack', 'ci' do
|
||||
before do
|
||||
create(:batched_background_migration,
|
||||
job_class_name: 'Ci::MyClass',
|
||||
table_name: :ci_builds,
|
||||
column_name: :id,
|
||||
job_arguments: [],
|
||||
gitlab_schema: :gitlab_ci)
|
||||
end
|
||||
|
||||
context 'when restrict_gitlab_migration is set to gitlab_ci' do
|
||||
it 'finalizes the migration' do
|
||||
migration_class.include(Gitlab::Database::MigrationHelpers::RestrictGitlabSchema)
|
||||
migration_class.restrict_gitlab_migration gitlab_schema: :gitlab_ci
|
||||
|
||||
allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |runner|
|
||||
expect(runner).to receive(:finalize).with('Ci::MyClass', :ci_builds, :id, []) do
|
||||
validate_connections_stack!
|
||||
end
|
||||
end
|
||||
|
||||
migration.finalize_batched_background_migration(
|
||||
job_class_name: 'Ci::MyClass', table_name: :ci_builds, column_name: :id, job_arguments: [])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when restrict_gitlab_migration is set to gitlab_main' do
|
||||
it 'does not find any migrations' do
|
||||
migration_class.include(Gitlab::Database::MigrationHelpers::RestrictGitlabSchema)
|
||||
migration_class.restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
expect do
|
||||
migration.finalize_batched_background_migration(
|
||||
job_class_name: 'Ci::MyClass', table_name: :ci_builds, column_name: :id, job_arguments: [])
|
||||
end.to raise_error /Could not find batched background migration/
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no restrict is set' do
|
||||
it 'does not find any migrations' do
|
||||
expect do
|
||||
migration.finalize_batched_background_migration(
|
||||
job_class_name: 'Ci::MyClass', table_name: :ci_builds, column_name: :id, job_arguments: [])
|
||||
end.to raise_error /Could not find batched background migration/
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ RSpec.describe Gitlab::Database::Migrations::ReestablishedConnectionStack do
|
|||
it_behaves_like "reconfigures connection stack", db_config_name do
|
||||
it 'does restore connection hierarchy' do
|
||||
model.with_restored_connection_stack do
|
||||
validate_connections!
|
||||
validate_connections_stack!
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ RSpec.describe Gitlab::GrapeLogging::Loggers::QueueDurationLogger do
|
|||
describe ".parameters" do
|
||||
let(:start_time) { Time.new(2018, 01, 01) }
|
||||
|
||||
describe 'when no proxy time is available' do
|
||||
describe 'when no proxy duration is available' do
|
||||
let(:mock_request) { double('env', env: {}) }
|
||||
|
||||
it 'returns an empty hash' do
|
||||
|
@ -16,20 +16,18 @@ RSpec.describe Gitlab::GrapeLogging::Loggers::QueueDurationLogger do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'when a proxy time is available' do
|
||||
describe 'when a proxy duration is available' do
|
||||
let(:mock_request) do
|
||||
double('env',
|
||||
env: {
|
||||
'HTTP_GITLAB_WORKHORSE_PROXY_START' => (start_time - 1.hour).to_i * (10**9)
|
||||
'GITLAB_RAILS_QUEUE_DURATION' => 2.seconds
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns the correct duration in seconds' do
|
||||
it 'adds the duration to log parameters' do
|
||||
travel_to(start_time) do
|
||||
subject.before
|
||||
|
||||
expect(subject.parameters(mock_request, nil)).to eq( { 'queue_duration_s': 1.hour.to_f })
|
||||
expect(subject.parameters(mock_request, nil)).to eq( { 'queue_duration_s': 2.seconds.to_f })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -43,6 +43,12 @@ RSpec.describe IssueBoardEntity do
|
|||
expect(subject).to include(labels: array_including(hash_including(:id, :title, :color, :description, :text_color, :priority)))
|
||||
end
|
||||
|
||||
describe 'type' do
|
||||
it 'has an issue type' do
|
||||
expect(subject[:type]).to eq('ISSUE')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'real_path' do
|
||||
it 'has an issue path' do
|
||||
expect(subject[:real_path]).to eq(project_issue_path(project, resource.iid))
|
||||
|
|
|
@ -24,6 +24,12 @@ RSpec.describe IssueEntity do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'type' do
|
||||
it 'has an issue type' do
|
||||
expect(subject[:type]).to eq('ISSUE')
|
||||
end
|
||||
end
|
||||
|
||||
it 'has Issuable attributes' do
|
||||
expect(subject).to include(:id, :iid, :author_id, :description, :lock_version, :milestone_id,
|
||||
:title, :updated_by_id, :created_at, :updated_at, :milestone, :labels)
|
||||
|
|
|
@ -50,6 +50,19 @@ RSpec.describe MergeRequests::CreatePipelineService do
|
|||
expect(response.payload.source).to eq('merge_request_event')
|
||||
end
|
||||
|
||||
context 'when push options contain ci.skip' do
|
||||
let(:params) { { push_options: { ci: { skip: true } } } }
|
||||
|
||||
it 'creates a skipped pipeline' do
|
||||
expect { response }.to change { Ci::Pipeline.count }.by(1)
|
||||
|
||||
expect(response).to be_success
|
||||
expect(response.payload).to be_persisted
|
||||
expect(response.payload.builds).to be_empty
|
||||
expect(response.payload).to be_skipped
|
||||
end
|
||||
end
|
||||
|
||||
context 'with fork merge request' do
|
||||
let_it_be(:forked_project) { fork_project(project, nil, repository: true, target_project: create(:project, :private, :repository)) }
|
||||
|
||||
|
|
|
@ -228,6 +228,21 @@ RSpec.describe MergeRequests::RefreshService do
|
|||
expect(@another_merge_request.has_commits?).to be_falsy
|
||||
end
|
||||
|
||||
context 'when "push_options: nil" is passed' do
|
||||
let(:service_instance) { service.new(project: project, current_user: @user, params: { push_options: nil }) }
|
||||
|
||||
subject { service_instance.execute(@oldrev, @newrev, ref) }
|
||||
|
||||
it 'creates a detached merge request pipeline with commits' do
|
||||
expect { subject }
|
||||
.to change { @merge_request.pipelines_for_merge_request.count }.by(1)
|
||||
.and change { @another_merge_request.pipelines_for_merge_request.count }.by(0)
|
||||
|
||||
expect(@merge_request.has_commits?).to be_truthy
|
||||
expect(@another_merge_request.has_commits?).to be_falsy
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not create detached merge request pipeline for forked project' do
|
||||
expect { subject }
|
||||
.not_to change { @fork_merge_request.pipelines_for_merge_request.count }
|
||||
|
|
|
@ -22,7 +22,7 @@ RSpec.shared_context 'reconfigures connection stack' do |db_config_name|
|
|||
end
|
||||
end
|
||||
|
||||
def validate_connections!
|
||||
def validate_connections_stack!
|
||||
model_connections = Gitlab::Database.database_base_models.to_h do |db_config_name, model_class|
|
||||
[model_class, Gitlab::Database.db_config_name(model_class.connection)]
|
||||
end
|
||||
|
|
|
@ -3,24 +3,50 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe MergeRequests::CreatePipelineWorker do
|
||||
subject(:worker) { described_class.new }
|
||||
|
||||
describe '#perform' do
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project) }
|
||||
let(:merge_request) { create(:merge_request) }
|
||||
let(:worker) { described_class.new }
|
||||
|
||||
subject { worker.perform(project.id, user.id, merge_request.id) }
|
||||
|
||||
context 'when the objects exist' do
|
||||
it 'calls the merge request create pipeline service and calls update head pipeline' do
|
||||
aggregate_failures do
|
||||
expect_next_instance_of(MergeRequests::CreatePipelineService, project: project, current_user: user) do |service|
|
||||
expect_next_instance_of(MergeRequests::CreatePipelineService,
|
||||
project: project,
|
||||
current_user: user,
|
||||
params: { push_options: nil }) do |service|
|
||||
expect(service).to receive(:execute).with(merge_request)
|
||||
end
|
||||
|
||||
expect(MergeRequest).to receive(:find_by_id).with(merge_request.id).and_return(merge_request)
|
||||
expect(merge_request).to receive(:update_head_pipeline)
|
||||
|
||||
subject.perform(project.id, user.id, merge_request.id)
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'when push options are passed as Hash to the worker' do
|
||||
let(:extra_params) { { 'push_options' => { 'ci' => { 'skip' => true } } } }
|
||||
|
||||
subject { worker.perform(project.id, user.id, merge_request.id, extra_params) }
|
||||
|
||||
it 'calls the merge request create pipeline service and calls update head pipeline' do
|
||||
aggregate_failures do
|
||||
expect_next_instance_of(MergeRequests::CreatePipelineService,
|
||||
project: project,
|
||||
current_user: user,
|
||||
params: { push_options: { ci: { skip: true } } }) do |service|
|
||||
expect(service).to receive(:execute).with(merge_request)
|
||||
end
|
||||
|
||||
expect(MergeRequest).to receive(:find_by_id).with(merge_request.id).and_return(merge_request)
|
||||
expect(merge_request).to receive(:update_head_pipeline)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -29,8 +55,7 @@ RSpec.describe MergeRequests::CreatePipelineWorker do
|
|||
it 'does not call the create pipeline service' do
|
||||
expect(MergeRequests::CreatePipelineService).not_to receive(:new)
|
||||
|
||||
expect { subject.perform(project.id, user.id, merge_request.id) }
|
||||
.not_to raise_exception
|
||||
expect { subject }.not_to raise_exception
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -3,28 +3,47 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe UpdateMergeRequestsWorker do
|
||||
include RepoHelpers
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:oldrev) { "123456" }
|
||||
let_it_be(:newrev) { "789012" }
|
||||
let_it_be(:ref) { "refs/heads/test" }
|
||||
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
subject { described_class.new }
|
||||
let(:worker) { described_class.new }
|
||||
|
||||
describe '#perform' do
|
||||
let(:oldrev) { "123456" }
|
||||
let(:newrev) { "789012" }
|
||||
let(:ref) { "refs/heads/test" }
|
||||
|
||||
def perform
|
||||
subject.perform(project.id, user.id, oldrev, newrev, ref)
|
||||
end
|
||||
subject { worker.perform(project.id, user.id, oldrev, newrev, ref) }
|
||||
|
||||
it 'executes MergeRequests::RefreshService with expected values' do
|
||||
expect_next_instance_of(MergeRequests::RefreshService, project: project, current_user: user) do |refresh_service|
|
||||
expect(refresh_service).to receive(:execute).with(oldrev, newrev, ref)
|
||||
expect_next_instance_of(MergeRequests::RefreshService,
|
||||
project: project,
|
||||
current_user: user,
|
||||
params: { push_options: nil }) do |service|
|
||||
expect(service)
|
||||
.to receive(:execute)
|
||||
.with(oldrev, newrev, ref)
|
||||
end
|
||||
|
||||
perform
|
||||
subject
|
||||
end
|
||||
|
||||
context 'when push options are passed as Hash' do
|
||||
let(:extra_params) { { 'push_options' => { 'ci' => { 'skip' => true } } } }
|
||||
|
||||
subject { worker.perform(project.id, user.id, oldrev, newrev, ref, extra_params) }
|
||||
|
||||
it 'executes MergeRequests::RefreshService with expected values' do
|
||||
expect_next_instance_of(MergeRequests::RefreshService,
|
||||
project: project,
|
||||
current_user: user,
|
||||
params: { push_options: { ci: { skip: true } } }) do |service|
|
||||
expect(service)
|
||||
.to receive(:execute)
|
||||
.with(oldrev, newrev, ref)
|
||||
end
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue