Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
960b02f579
commit
79c469c065
|
|
@ -1041,7 +1041,7 @@
|
|||
rules:
|
||||
- if: '$GITLABCOM_DATABASE_TESTING_TRIGGER_TOKEN == null'
|
||||
when: never
|
||||
- <<: *if-fork-merge-request
|
||||
- if: '$CI_MERGE_REQUEST_SOURCE_PROJECT_PATH !~ /^gitlab(-org|-cn)?\//'
|
||||
when: never
|
||||
- <<: *if-merge-request
|
||||
changes: *db-patterns
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
398249273423b358d1c226d6379391f5bf0c927c
|
||||
60cb736d21dff98e753dd49b873840cf665d8c91
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
2e27a8a56fb51a7359742453a6102e0cf20711de
|
||||
42fc100c92311d4989681df8c62b91cd18edb886
|
||||
|
|
|
|||
|
|
@ -82,7 +82,6 @@ export default {
|
|||
);
|
||||
},
|
||||
},
|
||||
colorScheme: gon?.user_color_scheme,
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -103,8 +102,7 @@ export default {
|
|||
<pre
|
||||
v-if="hover.language"
|
||||
ref="code-output"
|
||||
:class="$options.colorScheme"
|
||||
class="border-0 bg-transparent m-0 code highlight text-wrap"
|
||||
class="border-0 bg-transparent m-0 code code-syntax-highlight-theme highlight text-wrap"
|
||||
><doc-line v-for="(tokens, tokenIndex) in hover.tokens" :key="tokenIndex" :language="hover.language" :tokens="tokens" /></pre>
|
||||
<markdown v-else ref="doc-output" class="gl-p-3" :markdown="hover.value" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -59,9 +59,6 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
themeClass() {
|
||||
return window.gon?.user_color_scheme;
|
||||
},
|
||||
isCodeSuggestion() {
|
||||
return (
|
||||
this.node.attrs.isCodeSuggestion &&
|
||||
|
|
@ -72,7 +69,7 @@ export default {
|
|||
classList() {
|
||||
return this.isCodeSuggestion
|
||||
? '!gl-p-0 suggestion-added-input'
|
||||
: `gl-p-3 code highlight ${this.$options.userColorScheme}`;
|
||||
: `gl-p-3 code highlight code-syntax-highlight-theme`;
|
||||
},
|
||||
lineOffset() {
|
||||
return langParamsToLineOffset(this.node.attrs.langParams);
|
||||
|
|
@ -160,7 +157,6 @@ export default {
|
|||
.run();
|
||||
},
|
||||
},
|
||||
userColorScheme: gon.user_color_scheme,
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
|
|
@ -260,7 +256,10 @@ export default {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="suggestion-deleted code" :class="themeClass" data-testid="suggestion-deleted">
|
||||
<div
|
||||
class="suggestion-deleted code code-syntax-highlight-theme"
|
||||
data-testid="suggestion-deleted"
|
||||
>
|
||||
<code
|
||||
v-for="(line, i) in deletedLines"
|
||||
:key="i"
|
||||
|
|
@ -272,8 +271,7 @@ export default {
|
|||
>
|
||||
</div>
|
||||
<div
|
||||
class="suggestion-added code gl-absolute"
|
||||
:class="themeClass"
|
||||
class="suggestion-added code code-syntax-highlight-theme gl-absolute"
|
||||
data-testid="suggestion-added"
|
||||
>
|
||||
<code
|
||||
|
|
@ -292,8 +290,7 @@ export default {
|
|||
as="code"
|
||||
class="gl-relative gl-z-1 !gl-break-words"
|
||||
:class="{
|
||||
'line_content new code': isCodeSuggestion,
|
||||
[themeClass]: isCodeSuggestion,
|
||||
'line_content new code code-syntax-highlight-theme': isCodeSuggestion,
|
||||
}"
|
||||
:spellcheck="false"
|
||||
data-testid="suggestion-field"
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ export default CodeBlockLowlight.extend({
|
|||
'pre',
|
||||
{
|
||||
...mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
||||
class: `content-editor-code-block ${gon.user_color_scheme} ${HTMLAttributes.class}`,
|
||||
class: `content-editor-code-block code-syntax-highlight-theme ${HTMLAttributes.class}`,
|
||||
},
|
||||
['code', {}, 0],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -188,18 +188,14 @@ export default {
|
|||
return this.lineDrafts(line, side).length > 0;
|
||||
},
|
||||
},
|
||||
userColorScheme: window.gon.user_color_scheme,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
$options.userColorScheme,
|
||||
{ 'inline-diff-view': inline, 'with-inline-findings': hasInlineFindingsChanges },
|
||||
]"
|
||||
:class="[{ 'inline-diff-view': inline, 'with-inline-findings': hasInlineFindingsChanges }]"
|
||||
:data-commit-id="commitId"
|
||||
class="diff-grid diff-table code diff-wrap-lines js-syntax-highlight text-file"
|
||||
class="diff-grid diff-table code code-syntax-highlight-theme diff-wrap-lines js-syntax-highlight text-file"
|
||||
@mousedown="handleParallelLineMouseDown"
|
||||
>
|
||||
<template v-for="(line, index) in diffLines">
|
||||
|
|
|
|||
|
|
@ -74,7 +74,6 @@ export default {
|
|||
return line[1] ?? line.line;
|
||||
},
|
||||
},
|
||||
userColorScheme: window.gon.user_color_scheme,
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -122,7 +121,7 @@ export default {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<table v-if="isExpanded" :class="$options.userColorScheme" class="code js-syntax-highlight">
|
||||
<table v-if="isExpanded" class="code code-syntax-highlight-theme js-syntax-highlight">
|
||||
<tbody>
|
||||
<tr v-for="(line, index) in lines" :key="`stacktrace-line-${index}`" class="line_holder">
|
||||
<td class="diff-line-num" :class="{ old: isHighlighted(lineNum(line)) }">
|
||||
|
|
|
|||
|
|
@ -53,8 +53,7 @@ export default {
|
|||
action: null,
|
||||
},
|
||||
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
preClasses: `code highlight ${gon.user_color_scheme}`,
|
||||
preClasses: 'code highlight code-syntax-highlight-theme',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
|
|||
|
|
@ -93,10 +93,15 @@ import { DEFAULT_PAGE_SIZE, issuableListTabs } from '~/vue_shared/issuable/list/
|
|||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import NewResourceDropdown from '~/vue_shared/components/new_resource_dropdown/new_resource_dropdown.vue';
|
||||
import {
|
||||
WORK_ITEM_TYPE_ENUM_OBJECTIVE,
|
||||
BASE_ALLOWED_CREATE_TYPES,
|
||||
DETAIL_VIEW_QUERY_PARAM_NAME,
|
||||
INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION,
|
||||
WORK_ITEM_TYPE_ENUM_OBJECTIVE,
|
||||
WORK_ITEM_TYPE_NAME_ISSUE,
|
||||
WORK_ITEM_TYPE_NAME_KEY_RESULT,
|
||||
WORK_ITEM_TYPE_NAME_OBJECTIVE,
|
||||
} from '~/work_items/constants';
|
||||
import CreateWorkItemModal from '~/work_items/components/create_work_item_modal.vue';
|
||||
import WorkItemDrawer from '~/work_items/components/work_item_drawer.vue';
|
||||
import { makeDrawerUrlParam } from '~/work_items/utils';
|
||||
import {
|
||||
|
|
@ -148,7 +153,9 @@ export default {
|
|||
ISSUES_VIEW_TYPE_KEY,
|
||||
ISSUES_GRID_VIEW_KEY,
|
||||
ISSUES_LIST_VIEW_KEY,
|
||||
WORK_ITEM_TYPE_NAME_ISSUE,
|
||||
components: {
|
||||
CreateWorkItemModal,
|
||||
CsvImportExportButtons,
|
||||
GlDisclosureDropdown,
|
||||
GlDisclosureDropdownGroup,
|
||||
|
|
@ -295,6 +302,16 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
allowedWorkItemTypes() {
|
||||
if (this.glFeatures.okrsMvc && this.hasOkrsFeature) {
|
||||
return BASE_ALLOWED_CREATE_TYPES.concat(
|
||||
WORK_ITEM_TYPE_NAME_KEY_RESULT,
|
||||
WORK_ITEM_TYPE_NAME_OBJECTIVE,
|
||||
);
|
||||
}
|
||||
|
||||
return BASE_ALLOWED_CREATE_TYPES;
|
||||
},
|
||||
dropdownTooltip() {
|
||||
return !this.showTooltip ? this.$options.i18n.actionsLabel : '';
|
||||
},
|
||||
|
|
@ -1106,23 +1123,35 @@ export default {
|
|||
>
|
||||
{{ __('Bulk edit') }}
|
||||
</gl-button>
|
||||
<slot name="new-issuable-button">
|
||||
<gl-button
|
||||
v-if="showNewIssueLink"
|
||||
:href="newIssuePath"
|
||||
variant="confirm"
|
||||
class="gl-grow"
|
||||
>
|
||||
{{ __('New issue') }}
|
||||
</gl-button>
|
||||
</slot>
|
||||
<new-resource-dropdown
|
||||
v-if="showNewIssueDropdown"
|
||||
:query="$options.searchProjectsQuery"
|
||||
:query-variables="newIssueDropdownQueryVariables"
|
||||
:extract-projects="extractProjects"
|
||||
:group-id="groupId"
|
||||
<create-work-item-modal
|
||||
v-if="glFeatures.issuesListCreateModal"
|
||||
:allowed-work-item-types="allowedWorkItemTypes"
|
||||
always-show-work-item-type-select
|
||||
:full-path="fullPath"
|
||||
:is-group="!isProject"
|
||||
:preselected-work-item-type="$options.WORK_ITEM_TYPE_NAME_ISSUE"
|
||||
:show-project-selector="!isProject"
|
||||
@workItemCreated="refetchIssuables"
|
||||
/>
|
||||
<template v-else>
|
||||
<slot name="new-issuable-button">
|
||||
<gl-button
|
||||
v-if="showNewIssueLink"
|
||||
:href="newIssuePath"
|
||||
variant="confirm"
|
||||
class="gl-grow"
|
||||
>
|
||||
{{ __('New issue') }}
|
||||
</gl-button>
|
||||
</slot>
|
||||
<new-resource-dropdown
|
||||
v-if="showNewIssueDropdown"
|
||||
:query="$options.searchProjectsQuery"
|
||||
:query-variables="newIssueDropdownQueryVariables"
|
||||
:extract-projects="extractProjects"
|
||||
:group-id="groupId"
|
||||
/>
|
||||
</template>
|
||||
<gl-disclosure-dropdown
|
||||
v-gl-tooltip
|
||||
category="tertiary"
|
||||
|
|
@ -1166,10 +1195,6 @@ export default {
|
|||
<empty-state-with-any-issues :has-search="hasSearch" :is-open-tab="isOpenTab" />
|
||||
</template>
|
||||
|
||||
<template #list-body>
|
||||
<slot name="list-body"></slot>
|
||||
</template>
|
||||
|
||||
<template #custom-status="{ issuable = {} }">
|
||||
<slot name="custom-status" v-bind="{ issuable }"></slot>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -129,7 +129,6 @@ export default {
|
|||
return line.replace(FIRST_CHAR_REGEX, '');
|
||||
},
|
||||
},
|
||||
userColorSchemeClass: window.gon.user_color_scheme,
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -144,7 +143,7 @@ export default {
|
|||
:expanded="!isCollapsed"
|
||||
/>
|
||||
<div v-if="isTextFile" class="diff-content">
|
||||
<table class="code js-syntax-highlight" :class="$options.userColorSchemeClass">
|
||||
<table class="code js-syntax-highlight code-syntax-highlight-theme">
|
||||
<template v-if="!isFileDiscussion">
|
||||
<template v-if="hasTruncatedDiffLines">
|
||||
<tr
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
|
|||
import CompactCodeDropdown from 'ee_else_ce/repository/components/code_dropdown/compact_code_dropdown.vue';
|
||||
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
|
||||
import apolloProvider from '~/repository/graphql';
|
||||
import { renderGFM } from '~/behaviors/markdown/render_gfm';
|
||||
import { initHomePanel } from '../home_panel';
|
||||
|
||||
// Project show page loads different overview content based on user preferences
|
||||
|
|
@ -125,8 +126,16 @@ const initEmptyProjectTabs = () => {
|
|||
new EmptyProject(); // eslint-disable-line no-new
|
||||
};
|
||||
|
||||
const initWikiContent = () => {
|
||||
const el = document.querySelector('.js-wiki-content');
|
||||
if (!el) return;
|
||||
|
||||
renderGFM(el);
|
||||
};
|
||||
|
||||
initCodeDropdown();
|
||||
initSourceCodeDropdowns();
|
||||
initFindFileShortcut();
|
||||
initEmptyProjectTabs();
|
||||
initWebIdeLink({ el: document.getElementById('js-tree-web-ide-link') });
|
||||
initWikiContent();
|
||||
|
|
|
|||
|
|
@ -2,11 +2,6 @@
|
|||
import { s__ } from '~/locale';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
themeClass() {
|
||||
return window.gon?.user_color_scheme;
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
previewLabel: s__('Preferences|Preview'),
|
||||
},
|
||||
|
|
@ -16,7 +11,7 @@ export default {
|
|||
<div class="form-group">
|
||||
<label>{{ $options.i18n.previewLabel }}</label>
|
||||
<!-- eslint-disable @gitlab/vue-require-i18n-strings -->
|
||||
<table :class="themeClass" class="code">
|
||||
<table class="code code-syntax-highlight-theme">
|
||||
<tbody>
|
||||
<tr class="line_holder parallel">
|
||||
<td class="old_line diff-line-num old">
|
||||
|
|
|
|||
|
|
@ -58,11 +58,6 @@ export default {
|
|||
})),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
codeTheme() {
|
||||
return gon.user_color_scheme || 'white';
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.chunk.lines.forEach(this.processLine);
|
||||
},
|
||||
|
|
@ -97,8 +92,7 @@ export default {
|
|||
<template>
|
||||
<div
|
||||
id="search-blob-content"
|
||||
class="file-content code gl-rounded-none !gl-border-0 !gl-border-transparent"
|
||||
:class="codeTheme"
|
||||
class="file-content code code-syntax-highlight-theme gl-rounded-none !gl-border-0 !gl-border-transparent"
|
||||
>
|
||||
<div class="blob-content">
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export default function syntaxHighlight($els = null) {
|
|||
|
||||
if (el.classList.contains('js-syntax-highlight')) {
|
||||
// Given the element itself, apply highlighting
|
||||
return el.classList.add(gon.user_color_scheme);
|
||||
return el.classList.add('code-syntax-highlight-theme');
|
||||
}
|
||||
// Given a parent element, recurse to any of its applicable children
|
||||
const children = el.querySelectorAll('.js-syntax-highlight');
|
||||
|
|
|
|||
|
|
@ -100,7 +100,6 @@ export default {
|
|||
}
|
||||
});
|
||||
},
|
||||
userColorScheme: window.gon.user_color_scheme,
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -122,8 +121,7 @@ export default {
|
|||
</p>
|
||||
<pre
|
||||
:class="[
|
||||
$options.userColorScheme,
|
||||
'code highlight js-syntax-highlight gl-rounded-base',
|
||||
'code code-syntax-highlight-theme highlight js-syntax-highlight gl-rounded-base',
|
||||
{ 'gl-rounded-b-none': reviewingDocsPath },
|
||||
]"
|
||||
data-testid="how-to-merge-instructions"
|
||||
|
|
@ -169,7 +167,6 @@ export default {
|
|||
{{ $options.i18n.steps.step4.help }}
|
||||
</p>
|
||||
<pre
|
||||
:class="$options.userColorScheme"
|
||||
class="code highlight js-syntax-highlight language-shell gl-rounded-base"
|
||||
data-testid="how-to-merge-instructions"
|
||||
>{{ mergeInfo2 }}</pre
|
||||
|
|
|
|||
|
|
@ -140,12 +140,11 @@ export default {
|
|||
}
|
||||
},
|
||||
},
|
||||
userColorScheme: window.gon.user_color_scheme,
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div class="file-content code js-syntax-highlight gl-flex" :class="$options.userColorScheme">
|
||||
<div class="file-content code code-syntax-highlight-theme js-syntax-highlight gl-flex">
|
||||
<blame v-if="showBlame && blameInfoForRange.length" :blame-info="blameInfoForRange" />
|
||||
<div class="line-numbers !gl-px-0">
|
||||
<div v-for="line in lineNumbers" :key="line" class="diff-line-num line-links gl-flex">
|
||||
|
|
|
|||
|
|
@ -25,13 +25,11 @@ export default {
|
|||
return isScrollable ? scrollableStyles : null;
|
||||
},
|
||||
},
|
||||
userColorScheme: window.gon.user_color_scheme,
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<pre
|
||||
class="code-block rounded code"
|
||||
:class="$options.userColorScheme"
|
||||
class="code-block rounded code code-syntax-highlight-theme"
|
||||
:style="styleObject"
|
||||
><slot><code class="gl-block">{{ code }}</code></slot></pre>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -139,7 +139,6 @@ export default {
|
|||
}
|
||||
},
|
||||
},
|
||||
userColorSchemeClass: window.gon.user_color_scheme,
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -259,8 +258,7 @@ export default {
|
|||
class="gl-my-2 gl-mr-5 gl-overflow-hidden gl-overflow-visible gl-rounded-small gl-border-1 gl-border-solid gl-border-strong gl-pl-0"
|
||||
>
|
||||
<table
|
||||
:class="$options.userColorSchemeClass"
|
||||
class="code js-syntax-highlight"
|
||||
class="code code-syntax-highlight-theme js-syntax-highlight"
|
||||
data-testid="outdated-lines"
|
||||
>
|
||||
<tr v-for="line in lines" v-once :key="line.line_code" class="line_holder">
|
||||
|
|
|
|||
|
|
@ -136,7 +136,6 @@ export default {
|
|||
this.lineHighlighter.highlightHash(this.$route.hash);
|
||||
},
|
||||
},
|
||||
userColorScheme: window.gon.user_color_scheme,
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -145,8 +144,7 @@ export default {
|
|||
<blame v-if="showBlame && blameInfo.length" :blame-info="blameInfo" />
|
||||
|
||||
<div
|
||||
class="file-content code js-syntax-highlight blob-content blob-viewer gl-flex gl-w-full gl-flex-col gl-overflow-auto"
|
||||
:class="$options.userColorScheme"
|
||||
class="file-content code code-syntax-highlight-theme js-syntax-highlight blob-content blob-viewer gl-flex gl-w-full gl-flex-col gl-overflow-auto"
|
||||
data-type="simple"
|
||||
:data-path="blob.path"
|
||||
data-testid="blob-viewer-file-content"
|
||||
|
|
|
|||
|
|
@ -300,6 +300,7 @@ export default {
|
|||
workItemType: workItemType.name,
|
||||
workItemTypeId: workItemType.id,
|
||||
workItemTypeIconName: workItemType.iconName,
|
||||
relatedItemId: this.relatedItemId,
|
||||
workItemTitle,
|
||||
workItemDescription,
|
||||
confidential: this.isConfidential,
|
||||
|
|
@ -311,6 +312,7 @@ export default {
|
|||
if (selectedWorkItemType) {
|
||||
updateDraftWorkItemType({
|
||||
fullPath: this.selectedProjectFullPath,
|
||||
relatedItemId: this.relatedItemId,
|
||||
workItemType: {
|
||||
id: selectedWorkItemType.id,
|
||||
name: selectedWorkItemType.name,
|
||||
|
|
@ -354,6 +356,9 @@ export default {
|
|||
hasWidgets() {
|
||||
return this.workItem?.widgets?.length > 0;
|
||||
},
|
||||
relatedItemId() {
|
||||
return this.relatedItem?.id;
|
||||
},
|
||||
relatedItemReference() {
|
||||
return getDisplayReference(this.selectedProjectFullPath, this.relatedItem.reference);
|
||||
},
|
||||
|
|
@ -540,7 +545,7 @@ export default {
|
|||
return (
|
||||
this.isWidgetSupported(WIDGET_TYPE_LINKED_ITEMS) &&
|
||||
this.isRelatedToItem &&
|
||||
this.relatedItem?.id
|
||||
this.relatedItemId
|
||||
);
|
||||
},
|
||||
resolvingMRDiscussionLink() {
|
||||
|
|
@ -706,10 +711,12 @@ export default {
|
|||
workItemType: this.selectedWorkItemTypeName,
|
||||
workItemTypeId: this.selectedWorkItemTypeId,
|
||||
workItemTypeIconName: this.selectedWorkItemTypeIconName,
|
||||
relatedItemId: this.relatedItemId,
|
||||
});
|
||||
|
||||
updateDraftWorkItemType({
|
||||
fullPath: this.selectedProjectFullPath,
|
||||
relatedItemId: this.relatedItemId,
|
||||
workItemType: {
|
||||
id: this.selectedWorkItemTypeId,
|
||||
name: this.selectedWorkItemTypeName,
|
||||
|
|
@ -731,6 +738,7 @@ export default {
|
|||
input: {
|
||||
fullPath: this.selectedProjectFullPath,
|
||||
workItemType: this.selectedWorkItemTypeName,
|
||||
relatedItemId: this.relatedItemId,
|
||||
[type]: value,
|
||||
},
|
||||
},
|
||||
|
|
@ -929,6 +937,22 @@ export default {
|
|||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async handleUpdateWidgetDraft(input) {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: updateNewWorkItemMutation,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
relatedItemId: this.relatedItemId,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
this.error = this.createErrorText;
|
||||
Sentry.captureException(e);
|
||||
}
|
||||
},
|
||||
handleCancelClick() {
|
||||
/*
|
||||
If any form field is filled or has a non-default value, ask user to confirm
|
||||
|
|
@ -955,6 +979,7 @@ export default {
|
|||
workItemType: this.selectedWorkItemTypeName,
|
||||
workItemTypeId: this.selectedWorkItemTypeId,
|
||||
workItemTypeIconName: this.selectedWorkItemTypeIconName,
|
||||
relatedItemId: this.relatedItemId,
|
||||
});
|
||||
},
|
||||
onParentMilestone(parentMilestone) {
|
||||
|
|
@ -1108,6 +1133,7 @@ export default {
|
|||
:work-item-id="workItemId"
|
||||
:work-item-iid="workItemIid"
|
||||
:work-item-type="selectedWorkItemTypeName"
|
||||
@updateWidgetDraft="handleUpdateWidgetDraft"
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
<work-item-assignees
|
||||
|
|
@ -1122,6 +1148,7 @@ export default {
|
|||
:allows-multiple-assignees="workItemAssignees.allowsMultipleAssignees"
|
||||
:work-item-type="selectedWorkItemTypeName"
|
||||
:can-invite-members="workItemAssignees.canInviteMembers"
|
||||
@updateWidgetDraft="handleUpdateWidgetDraft"
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
<work-item-labels
|
||||
|
|
@ -1133,6 +1160,7 @@ export default {
|
|||
:work-item-id="workItemId"
|
||||
:work-item-iid="workItemIid"
|
||||
:work-item-type="selectedWorkItemTypeName"
|
||||
@updateWidgetDraft="handleUpdateWidgetDraft"
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
<work-item-parent
|
||||
|
|
@ -1146,6 +1174,7 @@ export default {
|
|||
:parent="workItemParent"
|
||||
:is-group="isGroup"
|
||||
:allowed-parent-types-for-new-work-item="allowedParentTypesForSelectedType"
|
||||
@updateWidgetDraft="handleUpdateWidgetDraft"
|
||||
@error="$emit('error', $event)"
|
||||
@parentMilestone="onParentMilestone"
|
||||
/>
|
||||
|
|
@ -1158,6 +1187,7 @@ export default {
|
|||
:work-item-id="workItemId"
|
||||
:work-item-iid="workItemIid"
|
||||
:work-item-type="selectedWorkItemTypeName"
|
||||
@updateWidgetDraft="handleUpdateWidgetDraft"
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
<work-item-milestone
|
||||
|
|
@ -1170,6 +1200,7 @@ export default {
|
|||
:work-item-milestone="workItemMilestone.milestone || selectedParentMilestone"
|
||||
:work-item-type="selectedWorkItemTypeName"
|
||||
:can-update="canUpdate"
|
||||
@updateWidgetDraft="handleUpdateWidgetDraft"
|
||||
@error="$emit('error', $event)"
|
||||
@parentMilestone="onParentMilestone"
|
||||
/>
|
||||
|
|
@ -1183,6 +1214,7 @@ export default {
|
|||
:work-item-id="workItemId"
|
||||
:work-item-iid="workItemIid"
|
||||
:work-item-type="selectedWorkItemTypeName"
|
||||
@updateWidgetDraft="handleUpdateWidgetDraft"
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
<work-item-dates
|
||||
|
|
@ -1196,6 +1228,7 @@ export default {
|
|||
:should-roll-up="shouldDatesRollup"
|
||||
:work-item-type="selectedWorkItemTypeName"
|
||||
:work-item="workItem"
|
||||
@updateWidgetDraft="handleUpdateWidgetDraft"
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
<work-item-health-status
|
||||
|
|
@ -1205,6 +1238,7 @@ export default {
|
|||
:work-item-iid="workItemIid"
|
||||
:work-item-type="selectedWorkItemTypeName"
|
||||
:full-path="selectedProjectFullPath"
|
||||
@updateWidgetDraft="handleUpdateWidgetDraft"
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
<work-item-color
|
||||
|
|
@ -1213,6 +1247,7 @@ export default {
|
|||
:work-item="workItem"
|
||||
:full-path="selectedProjectFullPath"
|
||||
:can-update="canUpdate"
|
||||
@updateWidgetDraft="handleUpdateWidgetDraft"
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
<work-item-custom-fields
|
||||
|
|
@ -1222,6 +1257,7 @@ export default {
|
|||
:custom-fields="workItemCustomFields"
|
||||
:full-path="selectedProjectFullPath"
|
||||
:can-update="canUpdate"
|
||||
@updateWidgetDraft="handleUpdateWidgetDraft"
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
<work-item-crm-contacts
|
||||
|
|
@ -1231,6 +1267,7 @@ export default {
|
|||
:work-item-id="workItemId"
|
||||
:work-item-iid="workItemIid"
|
||||
:work-item-type="selectedWorkItemTypeName"
|
||||
@updateWidgetDraft="handleUpdateWidgetDraft"
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
</aside>
|
||||
|
|
|
|||
|
|
@ -99,7 +99,10 @@ export default {
|
|||
},
|
||||
},
|
||||
data() {
|
||||
const draftWorkItemType = getDraftWorkItemType({ fullPath: this.fullPath })?.name;
|
||||
const draftWorkItemType = getDraftWorkItemType({
|
||||
fullPath: this.fullPath,
|
||||
relatedItemId: this.relatedItem?.id,
|
||||
})?.name;
|
||||
|
||||
return {
|
||||
isCreateModalVisible: false,
|
||||
|
|
@ -175,6 +178,11 @@ export default {
|
|||
this.isCreateModalVisible = false;
|
||||
},
|
||||
showCreateModal(event) {
|
||||
if (!gon?.current_user_id) {
|
||||
// If user is signed out, don't show modal, but allow them to click on the button to sign in
|
||||
return;
|
||||
}
|
||||
|
||||
if (Boolean(event) && isMetaClick(event)) {
|
||||
// opening in a new tab
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -108,7 +108,6 @@ export default {
|
|||
safeHtmlConfig: {
|
||||
ADD_TAGS: ['use'], // to support icon SVGs
|
||||
},
|
||||
userColorSchemeClass: window.gon.user_color_scheme,
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import { s__, sprintf, __ } from '~/locale';
|
|||
import Tracking from '~/tracking';
|
||||
import { ISSUE_MR_CHANGE_ASSIGNEE } from '~/behaviors/shortcuts/keybindings';
|
||||
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
|
||||
import updateNewWorkItemMutation from '../graphql/update_new_work_item.mutation.graphql';
|
||||
import { i18n, TRACKING_CATEGORY_SHOW } from '../constants';
|
||||
|
||||
export default {
|
||||
|
|
@ -268,15 +267,10 @@ export default {
|
|||
const { localAssigneeIds } = this;
|
||||
|
||||
if (this.workItemId === newWorkItemId(this.workItemType)) {
|
||||
this.$apollo.mutate({
|
||||
mutation: updateNewWorkItemMutation,
|
||||
variables: {
|
||||
input: {
|
||||
workItemType: this.workItemType,
|
||||
fullPath: this.fullPath,
|
||||
assignees: this.localAssignees,
|
||||
},
|
||||
},
|
||||
this.$emit('updateWidgetDraft', {
|
||||
workItemType: this.workItemType,
|
||||
fullPath: this.fullPath,
|
||||
assignees: this.localAssignees,
|
||||
});
|
||||
|
||||
this.updateInProgress = false;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import { camelCase } from 'lodash';
|
||||
import { GlForm } from '@gitlab/ui';
|
||||
import { createAlert } from '~/alert';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
|
|
@ -98,6 +99,9 @@ export default {
|
|||
shouldUseGraphQLBulkEdit() {
|
||||
return this.isEpicsList || this.glFeatures.workItemsBulkEdit;
|
||||
},
|
||||
isEditableUnlessEpicList() {
|
||||
return !this.shouldUseGraphQLBulkEdit || (this.shouldUseGraphQLBulkEdit && !this.isEpicsList);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async handleFormSubmitted() {
|
||||
|
|
@ -121,16 +125,36 @@ export default {
|
|||
}
|
||||
},
|
||||
performBulkEdit() {
|
||||
let assigneeIds;
|
||||
if (this.assigneeId === BULK_UPDATE_UNASSIGNED) {
|
||||
assigneeIds = [null];
|
||||
} else if (this.assigneeId) {
|
||||
assigneeIds = [this.assigneeId];
|
||||
}
|
||||
const hasLabelsToUpdate = this.addLabelIds.length > 0 || this.removeLabelIds.length > 0;
|
||||
return this.$apollo.mutate({
|
||||
mutation: workItemBulkUpdateMutation,
|
||||
variables: {
|
||||
input: {
|
||||
parentId: this.parentId,
|
||||
ids: this.checkedItems.map((item) => item.id),
|
||||
labelsWidget: {
|
||||
addLabelIds: this.addLabelIds,
|
||||
removeLabelIds: this.removeLabelIds,
|
||||
},
|
||||
labelsWidget: hasLabelsToUpdate
|
||||
? {
|
||||
addLabelIds: this.addLabelIds,
|
||||
removeLabelIds: this.removeLabelIds,
|
||||
}
|
||||
: undefined,
|
||||
assigneesWidget: assigneeIds
|
||||
? {
|
||||
assigneeIds,
|
||||
}
|
||||
: undefined,
|
||||
confidential: this.confidentiality ? this.confidentiality === 'true' : undefined,
|
||||
healthStatusWidget: this.healthStatus
|
||||
? {
|
||||
healthStatus: camelCase(this.healthStatus),
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -171,7 +195,7 @@ export default {
|
|||
data-testid="bulk-edit-state"
|
||||
/>
|
||||
<work-item-bulk-edit-assignee
|
||||
v-if="!shouldUseGraphQLBulkEdit"
|
||||
v-if="isEditableUnlessEpicList"
|
||||
v-model="assigneeId"
|
||||
:full-path="fullPath"
|
||||
:is-group="isGroup"
|
||||
|
|
@ -192,7 +216,7 @@ export default {
|
|||
@select="removeLabelIds = $event"
|
||||
/>
|
||||
<work-item-bulk-edit-dropdown
|
||||
v-if="!shouldUseGraphQLBulkEdit && hasIssuableHealthStatusFeature"
|
||||
v-if="hasIssuableHealthStatusFeature && isEditableUnlessEpicList"
|
||||
v-model="healthStatus"
|
||||
:header-text="__('Select health status')"
|
||||
:items="$options.healthStatusItems"
|
||||
|
|
@ -208,7 +232,7 @@ export default {
|
|||
data-testid="bulk-edit-subscription"
|
||||
/>
|
||||
<work-item-bulk-edit-dropdown
|
||||
v-if="!shouldUseGraphQLBulkEdit"
|
||||
v-if="isEditableUnlessEpicList"
|
||||
v-model="confidentiality"
|
||||
:header-text="__('Select confidentiality')"
|
||||
:items="$options.confidentialityItems"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import Tracking from '~/tracking';
|
|||
import getGroupContactsQuery from '~/crm/contacts/components/graphql/get_group_contacts.query.graphql';
|
||||
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
|
||||
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
|
||||
import updateNewWorkItemMutation from '../graphql/update_new_work_item.mutation.graphql';
|
||||
import { i18n, TRACKING_CATEGORY_SHOW } from '../constants';
|
||||
import { findCrmContactsWidget, newWorkItemFullPath, newWorkItemId } from '../utils';
|
||||
|
||||
|
|
@ -211,17 +210,11 @@ export default {
|
|||
}
|
||||
|
||||
if (this.createFlow) {
|
||||
this.$apollo.mutate({
|
||||
mutation: updateNewWorkItemMutation,
|
||||
variables: {
|
||||
input: {
|
||||
workItemType: this.workItemType,
|
||||
fullPath: this.fullPath,
|
||||
crmContacts: newSelectedItems,
|
||||
},
|
||||
},
|
||||
this.$emit('updateWidgetDraft', {
|
||||
workItemType: this.workItemType,
|
||||
fullPath: this.fullPath,
|
||||
crmContacts: newSelectedItems,
|
||||
});
|
||||
|
||||
this.updateInProgress = false;
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import {
|
|||
WIDGET_TYPE_START_AND_DUE_DATE,
|
||||
} from '../constants';
|
||||
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
|
||||
import updateNewWorkItemMutation from '../graphql/update_new_work_item.mutation.graphql';
|
||||
import WorkItemSidebarWidget from './shared/work_item_sidebar_widget.vue';
|
||||
|
||||
const nullObjectDate = new Date(0);
|
||||
|
|
@ -175,19 +174,14 @@ export default {
|
|||
this.rollupType = ROLLUP_TYPE_FIXED;
|
||||
|
||||
if (this.workItemId === newWorkItemId(this.workItemType)) {
|
||||
this.$apollo.mutate({
|
||||
mutation: updateNewWorkItemMutation,
|
||||
variables: {
|
||||
input: {
|
||||
workItemType: this.workItemType,
|
||||
fullPath: this.fullPath,
|
||||
rolledUpDates: {
|
||||
isFixed: true,
|
||||
dueDate: this.localDueDate ? toISODateFormat(this.localDueDate) : null,
|
||||
startDate: this.localStartDate ? toISODateFormat(this.localStartDate) : null,
|
||||
rollUp: this.shouldRollUp,
|
||||
},
|
||||
},
|
||||
this.$emit('updateWidgetDraft', {
|
||||
workItemType: this.workItemType,
|
||||
fullPath: this.fullPath,
|
||||
rolledUpDates: {
|
||||
isFixed: true,
|
||||
dueDate: this.localDueDate ? toISODateFormat(this.localDueDate) : null,
|
||||
startDate: this.localStartDate ? toISODateFormat(this.localStartDate) : null,
|
||||
rollUp: this.shouldRollUp,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import Tracking from '~/tracking';
|
|||
import { ISSUABLE_CHANGE_LABEL } from '~/behaviors/shortcuts/keybindings';
|
||||
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
|
||||
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
|
||||
import updateNewWorkItemMutation from '../graphql/update_new_work_item.mutation.graphql';
|
||||
import { i18n, TRACKING_CATEGORY_SHOW, WORK_ITEM_TYPE_NAME_EPIC } from '../constants';
|
||||
import {
|
||||
findLabelsWidget,
|
||||
|
|
@ -257,16 +256,11 @@ export default {
|
|||
await this.updateLabels({ addLabelIds, removeLabelIds });
|
||||
}
|
||||
},
|
||||
async updateDraftCache() {
|
||||
await this.$apollo.mutate({
|
||||
mutation: updateNewWorkItemMutation,
|
||||
variables: {
|
||||
input: {
|
||||
workItemType: this.workItemType,
|
||||
fullPath: this.fullPath,
|
||||
labels: this.labelsCache.filter(({ id }) => this.selectedLabelsIds.includes(id)),
|
||||
},
|
||||
},
|
||||
updateDraftCache() {
|
||||
this.$emit('updateWidgetDraft', {
|
||||
workItemType: this.workItemType,
|
||||
fullPath: this.fullPath,
|
||||
labels: this.labelsCache.filter(({ id }) => this.selectedLabelsIds.includes(id)),
|
||||
});
|
||||
},
|
||||
async updateLabels({ addLabelIds = [], removeLabelIds = [] }) {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import { ISSUE_MR_CHANGE_MILESTONE } from '~/behaviors/shortcuts/keybindings';
|
|||
import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
|
||||
import groupMilestonesQuery from '~/sidebar/queries/group_milestones.query.graphql';
|
||||
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
|
||||
import updateNewWorkItemMutation from '~/work_items/graphql/update_new_work_item.mutation.graphql';
|
||||
import {
|
||||
I18N_WORK_ITEM_ERROR_UPDATING,
|
||||
NAME_TO_TEXT_LOWERCASE_MAP,
|
||||
|
|
@ -169,32 +168,21 @@ export default {
|
|||
this.updateInProgress = true;
|
||||
|
||||
if (this.workItemId === newWorkItemId(this.workItemType)) {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: updateNewWorkItemMutation,
|
||||
variables: {
|
||||
input: {
|
||||
fullPath: this.fullPath,
|
||||
milestone: this.localMilestone
|
||||
? {
|
||||
...this.localMilestone,
|
||||
webPath: this.localMilestone.webUrl,
|
||||
startDate: '',
|
||||
projectMilestone: false,
|
||||
}
|
||||
: null,
|
||||
workItemType: this.workItemType,
|
||||
},
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
Sentry.captureException(error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.updateInProgress = false;
|
||||
this.searchTerm = '';
|
||||
this.shouldFetch = false;
|
||||
});
|
||||
this.$emit('updateWidgetDraft', {
|
||||
fullPath: this.fullPath,
|
||||
milestone: this.localMilestone
|
||||
? {
|
||||
...this.localMilestone,
|
||||
webPath: this.localMilestone.webUrl,
|
||||
startDate: '',
|
||||
projectMilestone: false,
|
||||
}
|
||||
: null,
|
||||
workItemType: this.workItemType,
|
||||
});
|
||||
this.updateInProgress = false;
|
||||
this.searchTerm = '';
|
||||
this.shouldFetch = false;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import WorkItemSidebarDropdownWidget from '~/work_items/components/shared/work_i
|
|||
import updateParentMutation from '~/work_items/graphql/update_parent.mutation.graphql';
|
||||
import { isValidURL } from '~/lib/utils/url_utility';
|
||||
|
||||
import updateNewWorkItemMutation from '~/work_items/graphql/update_new_work_item.mutation.graphql';
|
||||
import {
|
||||
findMilestoneWidget,
|
||||
findHierarchyWidgetDefinition,
|
||||
|
|
@ -237,35 +236,19 @@ export default {
|
|||
this.updateInProgress = true;
|
||||
|
||||
if (this.workItemId === newWorkItemId(this.workItemType)) {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: updateNewWorkItemMutation,
|
||||
variables: {
|
||||
input: {
|
||||
fullPath: this.fullPath,
|
||||
parent: this.isSelectedParentAvailable
|
||||
? {
|
||||
...this.visibleWorkItems.find(({ id }) => id === this.localSelectedItem),
|
||||
webUrl: this.parentWebUrl ?? null,
|
||||
}
|
||||
: null,
|
||||
workItemType: this.workItemType,
|
||||
},
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$emit(
|
||||
'error',
|
||||
sprintf(I18N_WORK_ITEM_ERROR_UPDATING, {
|
||||
workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.workItemType],
|
||||
}),
|
||||
);
|
||||
Sentry.captureException(error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.searchStarted = false;
|
||||
this.updateInProgress = false;
|
||||
});
|
||||
this.$emit('updateWidgetDraft', {
|
||||
fullPath: this.fullPath,
|
||||
parent: this.isSelectedParentAvailable
|
||||
? {
|
||||
...this.visibleWorkItems.find(({ id }) => id === this.localSelectedItem),
|
||||
webUrl: this.parentWebUrl ?? null,
|
||||
}
|
||||
: null,
|
||||
workItemType: this.workItemType,
|
||||
});
|
||||
|
||||
this.searchStarted = false;
|
||||
this.updateInProgress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -321,11 +321,12 @@ export const getNewWorkItemSharedCache = ({
|
|||
widgetDefinitions,
|
||||
fullPath,
|
||||
workItemType,
|
||||
relatedItemId,
|
||||
isValidWorkItemDescription,
|
||||
workItemDescription = '',
|
||||
}) => {
|
||||
const widgetsAutosaveKey = getNewWorkItemWidgetsAutoSaveKey({ fullPath });
|
||||
const fullDraftAutosaveKey = getNewWorkItemAutoSaveKey({ fullPath, workItemType });
|
||||
const widgetsAutosaveKey = getNewWorkItemWidgetsAutoSaveKey({ fullPath, relatedItemId });
|
||||
const fullDraftAutosaveKey = getNewWorkItemAutoSaveKey({ fullPath, workItemType, relatedItemId });
|
||||
const workItemTypeSpecificWidgets =
|
||||
getWorkItemWidgets(JSON.parse(getDraft(fullDraftAutosaveKey))) || {};
|
||||
const sharedCacheWidgets = JSON.parse(getDraft(widgetsAutosaveKey)) || {};
|
||||
|
|
@ -627,6 +628,7 @@ export const setNewWorkItemCache = async ({
|
|||
workItemType,
|
||||
workItemTypeId,
|
||||
workItemTypeIconName,
|
||||
relatedItemId,
|
||||
workItemTitle = '',
|
||||
workItemDescription = '',
|
||||
confidential = false,
|
||||
|
|
@ -661,7 +663,7 @@ export const setNewWorkItemCache = async ({
|
|||
const isValidWorkItemTitle = workItemTitle.trim().length > 0;
|
||||
const isValidWorkItemDescription = workItemDescription.trim().length > 0;
|
||||
|
||||
const autosaveKey = getNewWorkItemAutoSaveKey({ fullPath, workItemType });
|
||||
const autosaveKey = getNewWorkItemAutoSaveKey({ fullPath, workItemType, relatedItemId });
|
||||
const getStorageDraftString = getDraft(autosaveKey);
|
||||
|
||||
const draftData = JSON.parse(getDraft(autosaveKey));
|
||||
|
|
@ -678,6 +680,7 @@ export const setNewWorkItemCache = async ({
|
|||
workItemType,
|
||||
isValidWorkItemDescription,
|
||||
workItemDescription,
|
||||
relatedItemId,
|
||||
});
|
||||
|
||||
draftTitle = sharedCache.draftTitle;
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ const updateCustomFieldsWidget = (sourceData, draftData, customField) => {
|
|||
|
||||
export const updateNewWorkItemCache = (input, cache) => {
|
||||
const {
|
||||
relatedItemId,
|
||||
healthStatus,
|
||||
fullPath,
|
||||
workItemType,
|
||||
|
|
@ -198,14 +199,14 @@ export const updateNewWorkItemCache = (input, cache) => {
|
|||
|
||||
const newData = cache.readQuery({ query, variables });
|
||||
|
||||
const autosaveKey = getNewWorkItemAutoSaveKey({ fullPath, workItemType });
|
||||
const autosaveKey = getNewWorkItemAutoSaveKey({ fullPath, workItemType, relatedItemId });
|
||||
|
||||
const isQueryDataValid = !isEmpty(newData) && newData?.workspace?.workItem;
|
||||
|
||||
if (isQueryDataValid && autosaveKey) {
|
||||
updateDraft(autosaveKey, JSON.stringify(newData));
|
||||
updateDraft(
|
||||
getNewWorkItemWidgetsAutoSaveKey({ fullPath }),
|
||||
getNewWorkItemWidgetsAutoSaveKey({ fullPath, relatedItemId }),
|
||||
JSON.stringify(getWorkItemWidgets(newData)),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import {
|
|||
WORK_ITEM_TYPE_NAME_INCIDENT,
|
||||
WORK_ITEM_TYPE_NAME_ISSUE,
|
||||
WORK_ITEM_TYPE_NAME_TASK,
|
||||
WORK_ITEM_TYPE_ROUTE_WORK_ITEM,
|
||||
WORK_ITEM_TYPE_ROUTE_ISSUE,
|
||||
} from '../constants';
|
||||
import workItemRelatedItemQuery from '../graphql/work_item_related_item.query.graphql';
|
||||
import { convertTypeEnumToName } from '../utils';
|
||||
|
|
@ -122,14 +124,15 @@ export default {
|
|||
const isWorkItemRoute = this.$route.params?.type === 'work_items';
|
||||
const isGroupWorkItemRoute = isWorkItemRoute && this.$router.history.base.includes('groups');
|
||||
|
||||
/*
|
||||
If the route is epics, issues or work items on the group level
|
||||
(because work items on the project level is not yet available)
|
||||
we redirect to the list page when the user clicks on cancel,
|
||||
otherwise we go back to the previous page.
|
||||
*/
|
||||
|
||||
if (Boolean(listPath) && (!isWorkItemRoute || isGroupWorkItemRoute)) {
|
||||
/**
|
||||
* If the route is epics, issues or work items on the group level
|
||||
* (because work items on the project level is not yet available)
|
||||
* we redirect to the list page when the user clicks on cancel,
|
||||
* otherwise we go back to the previous page.
|
||||
*/
|
||||
if (Boolean(listPath) && isWorkItemRoute && isGroupWorkItemRoute) {
|
||||
visitUrl(listPath.replaceAll(WORK_ITEM_TYPE_ROUTE_WORK_ITEM, WORK_ITEM_TYPE_ROUTE_ISSUE));
|
||||
} else if (Boolean(listPath) && (!isWorkItemRoute || isGroupWorkItemRoute)) {
|
||||
visitUrl(listPath);
|
||||
} else {
|
||||
this.$router.go(-1);
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import {
|
|||
WIDGET_TYPE_TIME_TRACKING,
|
||||
WIDGET_TYPE_VULNERABILITIES,
|
||||
WIDGET_TYPE_WEIGHT,
|
||||
WORK_ITEM_TYPE_NAME_ISSUE,
|
||||
WORK_ITEM_TYPE_ROUTE_WORK_ITEM,
|
||||
} from './constants';
|
||||
|
||||
|
|
@ -284,7 +285,12 @@ export const markdownPreviewPath = ({ fullPath, iid, isGroup = false }) => {
|
|||
export const newWorkItemPath = ({ fullPath, isGroup = false, workItemType, query = '' }) => {
|
||||
const domain = gon.relative_url_root || '';
|
||||
const basePath = isGroup ? `groups/${fullPath}` : fullPath;
|
||||
const type = NAME_TO_ROUTE_MAP[workItemType] || WORK_ITEM_TYPE_ROUTE_WORK_ITEM;
|
||||
// We have a special case to redirect to /groups/my-group/-/work_items/new
|
||||
// instead of /groups/my-group/-/issues/new
|
||||
const type =
|
||||
isGroup && workItemType === WORK_ITEM_TYPE_NAME_ISSUE
|
||||
? WORK_ITEM_TYPE_ROUTE_WORK_ITEM
|
||||
: NAME_TO_ROUTE_MAP[workItemType] || WORK_ITEM_TYPE_ROUTE_WORK_ITEM;
|
||||
return `${domain}/${basePath}/-/${type}/new${query}`;
|
||||
};
|
||||
|
||||
|
|
@ -419,26 +425,41 @@ export const getAutosaveKeyQueryParamString = () => {
|
|||
return queryParams.toString();
|
||||
};
|
||||
|
||||
export const getNewWorkItemAutoSaveKey = ({ fullPath, workItemType }) => {
|
||||
export const getNewWorkItemAutoSaveKey = ({ fullPath, workItemType, relatedItemId }) => {
|
||||
if (!workItemType || !fullPath) return '';
|
||||
|
||||
const relatedId = getIdFromGraphQLId(relatedItemId);
|
||||
const queryParamString = getAutosaveKeyQueryParamString();
|
||||
let initialKey = `new-${fullPath}-${workItemType.toLowerCase()}`;
|
||||
|
||||
if (relatedId) {
|
||||
initialKey = `${initialKey}-related-${relatedId}`;
|
||||
}
|
||||
|
||||
if (queryParamString) {
|
||||
return `new-${fullPath}-${workItemType.toLowerCase()}-${queryParamString}-draft`;
|
||||
initialKey = `${initialKey}-${queryParamString}`;
|
||||
}
|
||||
return `new-${fullPath}-${workItemType.toLowerCase()}-draft`;
|
||||
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
return `${initialKey}-draft`;
|
||||
};
|
||||
|
||||
export const getNewWorkItemWidgetsAutoSaveKey = ({ fullPath }) => {
|
||||
export const getNewWorkItemWidgetsAutoSaveKey = ({ fullPath, relatedItemId }) => {
|
||||
if (!fullPath) return '';
|
||||
|
||||
const relatedId = getIdFromGraphQLId(relatedItemId);
|
||||
const queryParamString = getAutosaveKeyQueryParamString();
|
||||
let initialKey = `new-${fullPath}`;
|
||||
|
||||
if (relatedId) {
|
||||
initialKey = `${initialKey}-related-${relatedId}`;
|
||||
}
|
||||
|
||||
if (queryParamString) {
|
||||
return `new-${fullPath}-widgets-${queryParamString}-draft`;
|
||||
initialKey = `${initialKey}-${queryParamString}`;
|
||||
}
|
||||
return `new-${fullPath}-widgets-draft`;
|
||||
|
||||
return `${initialKey}-widgets-draft`;
|
||||
};
|
||||
|
||||
export const getWorkItemWidgets = (draftData) => {
|
||||
|
|
@ -456,18 +477,20 @@ export const getWorkItemWidgets = (draftData) => {
|
|||
return widgets;
|
||||
};
|
||||
|
||||
export const updateDraftWorkItemType = ({ fullPath, workItemType }) => {
|
||||
export const updateDraftWorkItemType = ({ fullPath, workItemType, relatedItemId }) => {
|
||||
const widgetsAutosaveKey = getNewWorkItemWidgetsAutoSaveKey({
|
||||
fullPath,
|
||||
relatedItemId,
|
||||
});
|
||||
const sharedCacheWidgets = JSON.parse(getDraft(widgetsAutosaveKey)) || {};
|
||||
sharedCacheWidgets.TYPE = workItemType;
|
||||
updateDraft(widgetsAutosaveKey, JSON.stringify(sharedCacheWidgets));
|
||||
};
|
||||
|
||||
export const getDraftWorkItemType = ({ fullPath }) => {
|
||||
export const getDraftWorkItemType = ({ fullPath, relatedItemId }) => {
|
||||
const widgetsAutosaveKey = getNewWorkItemWidgetsAutoSaveKey({
|
||||
fullPath,
|
||||
relatedItemId,
|
||||
});
|
||||
const sharedCacheWidgets = JSON.parse(getDraft(widgetsAutosaveKey)) || {};
|
||||
return sharedCacheWidgets.TYPE;
|
||||
|
|
|
|||
|
|
@ -127,7 +127,8 @@ $dark-il: #de935f;
|
|||
--diff-deletion-color: #{$dark-old-bg};
|
||||
}
|
||||
|
||||
.code.dark {
|
||||
.code.dark,
|
||||
.code.code-syntax-highlight-theme {
|
||||
--code-dark-theme: 1;
|
||||
--code-border-lightness-adjust: 0.1;
|
||||
--diff-expansion-background-color: #{$gl-color-neutral-600};
|
||||
|
|
|
|||
|
|
@ -98,7 +98,8 @@ $monokai-gh: #75715e;
|
|||
--diff-deletion-color: #{$monokai-old-bg};
|
||||
}
|
||||
|
||||
.code.monokai {
|
||||
.code.monokai,
|
||||
.code.code-syntax-highlight-theme {
|
||||
--code-dark-theme: 1;
|
||||
--code-border-lightness-adjust: 0.1;
|
||||
--diff-expansion-background-color: #{$gl-color-neutral-600};
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ $none-code-mark: #d3e3f4;
|
|||
--diff-deletion-color: #{$gl-color-neutral-50};
|
||||
}
|
||||
|
||||
.code.none {
|
||||
.code.none,
|
||||
.code.code-syntax-highlight-theme {
|
||||
--code-light-theme: 1;
|
||||
--diff-expansion-background-color: #{$gl-color-neutral-100};
|
||||
|
||||
|
|
|
|||
|
|
@ -101,7 +101,8 @@ $solarized-dark-il: #2aa198;
|
|||
--diff-deletion-color: #{$solarized-dark-old-bg};
|
||||
}
|
||||
|
||||
.code.solarized-dark {
|
||||
.code.solarized-dark,
|
||||
.code.code-syntax-highlight-theme {
|
||||
--code-dark-theme: 1;
|
||||
--code-border-lightness-adjust: 0.1;
|
||||
--diff-expansion-background-color: #{lighten($solarized-dark-pre-bg, 10%)};
|
||||
|
|
|
|||
|
|
@ -108,7 +108,8 @@ $solarized-light-il: #2aa198;
|
|||
background: $solarized-light-matchline-bg;
|
||||
}
|
||||
|
||||
.code.solarized-light {
|
||||
.code.solarized-light,
|
||||
.code.code-syntax-highlight-theme {
|
||||
--code-light-theme: 1;
|
||||
--diff-expansion-background-color: #{$gl-color-neutral-100};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
@import '../white_base';
|
||||
|
||||
.code.white {
|
||||
.code.white,
|
||||
.code.code-syntax-highlight-theme {
|
||||
@include white-base;
|
||||
@include conflict-colors('white');
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
.rd-app-content-header{ data: { hidden_files_warning: true } }
|
||||
- if empty_diff? && !lazy?
|
||||
= render RapidDiffs::EmptyStateComponent.new
|
||||
.rd-app-code-theme.code{ class: helpers.user_color_scheme }
|
||||
.rd-app-code-theme.code.code-syntax-highlight-theme
|
||||
.rd-app-diffs-list
|
||||
-# performance optimization: using a sibling element to cover diffs list is faster than changing opacity on the parent
|
||||
.rd-app-diffs-list-loading-overlay{ data: { diffs_overlay: true } }
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ class GroupsController < Groups::ApplicationController
|
|||
push_force_frontend_feature_flag(:work_items_beta, group.work_items_beta_feature_flag_enabled?)
|
||||
push_force_frontend_feature_flag(:work_items_alpha, group.work_items_alpha_feature_flag_enabled?)
|
||||
push_frontend_feature_flag(:issues_grid_view)
|
||||
push_frontend_feature_flag(:issues_list_create_modal, group)
|
||||
push_frontend_feature_flag(:issues_list_drawer, group)
|
||||
push_frontend_feature_flag(:work_item_status_feature_flag, group&.root_ancestor)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
push_frontend_feature_flag(:preserve_markdown, project)
|
||||
push_frontend_feature_flag(:issues_grid_view)
|
||||
push_frontend_feature_flag(:service_desk_ticket)
|
||||
push_frontend_feature_flag(:issues_list_create_modal, project)
|
||||
push_frontend_feature_flag(:issues_list_drawer, project)
|
||||
push_frontend_feature_flag(:notifications_todos_buttons, current_user)
|
||||
push_frontend_feature_flag(:work_item_planning_view, project&.group)
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ module Ci
|
|||
# The purpose of this class is to store Build related data that can be disposed.
|
||||
# Data that should be persisted forever, should be stored with Ci::Build model.
|
||||
class BuildMetadata < Ci::ApplicationRecord
|
||||
BuildTimeout = Struct.new(:value, :source)
|
||||
|
||||
include Ci::Partitionable
|
||||
include Presentable
|
||||
include ChronicDurationAttribute
|
||||
|
|
@ -51,8 +49,7 @@ module Ci
|
|||
}
|
||||
|
||||
def update_timeout_state
|
||||
timeout = timeout_with_highest_precedence
|
||||
|
||||
timeout = ::Ci::Builds::TimeoutCalculator.new(build).applicable_timeout
|
||||
return unless timeout
|
||||
|
||||
update(timeout: timeout.value, timeout_source: timeout.source)
|
||||
|
|
@ -69,37 +66,5 @@ module Ci
|
|||
def set_build_project
|
||||
self.project_id ||= build.project_id
|
||||
end
|
||||
|
||||
def timeout_with_highest_precedence
|
||||
[(job_timeout || project_timeout), runner_timeout].compact.min_by(&:value)
|
||||
end
|
||||
|
||||
def project_timeout
|
||||
strong_memoize(:project_timeout) do
|
||||
BuildTimeout.new(project&.build_timeout, :project_timeout_source)
|
||||
end
|
||||
end
|
||||
|
||||
def job_timeout
|
||||
return unless build.options
|
||||
|
||||
strong_memoize(:job_timeout) do
|
||||
if timeout_from_options = build.options[:job_timeout]
|
||||
BuildTimeout.new(timeout_from_options, :job_timeout_source)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def runner_timeout
|
||||
return unless runner_timeout_set?
|
||||
|
||||
strong_memoize(:runner_timeout) do
|
||||
BuildTimeout.new(build.runner.maximum_timeout, :runner_timeout_source)
|
||||
end
|
||||
end
|
||||
|
||||
def runner_timeout_set?
|
||||
build.runner&.maximum_timeout.to_i > 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ module WebHooks
|
|||
included do
|
||||
delegate :auto_disabling_enabled?, to: :class
|
||||
|
||||
ignore_column :backoff_count, remove_with: '18.1', remove_after: '2025-05-20'
|
||||
ignore_column :backoff_count, remove_with: '18.3', remove_after: '2025-07-20'
|
||||
|
||||
# A webhook is disabled if:
|
||||
#
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
},
|
||||
"project_security_status": {
|
||||
"type": [
|
||||
"array",
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"description": "Data for rendering the project grades summary in PDF reports"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
- if @wiki_home.present?
|
||||
%div{ class: container_class }
|
||||
.md.gl-mt-3.gl-mb-3
|
||||
.md.gl-mt-3.gl-mb-3.js-wiki-content
|
||||
= render_wiki_content(@wiki_home)
|
||||
- else
|
||||
- can_create_wiki = can?(current_user, :create_wiki, @project)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
- current_line = @blame.first_line
|
||||
|
||||
.file-content.blame.code{ class: user_color_scheme }
|
||||
.file-content.blame.code.code-syntax-highlight-theme
|
||||
- groups_length = @blame.groups.size - 1
|
||||
- @blame.groups.each_with_index do |blame_group, index|
|
||||
- commit_data = @blame.commit_data(blame_group[:commit])
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
name: issues_list_create_modal
|
||||
description:
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/514577
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/190721
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/551355
|
||||
milestone: '18.2'
|
||||
group: group::project management
|
||||
type: beta
|
||||
default_enabled: false
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveWebHooksBackoffCountColumn < Gitlab::Database::Migration[2.3]
|
||||
milestone '18.2'
|
||||
|
||||
TABLE_NAME = :web_hooks
|
||||
COLUMN_NAME = :backoff_count
|
||||
|
||||
def up
|
||||
remove_column TABLE_NAME, COLUMN_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
add_column TABLE_NAME, COLUMN_NAME, :smallint, default: 0, null: false
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
80357a3afe319e27bd583abd215cc23494b00ab6ef789d590e36171d95309c9d
|
||||
|
|
@ -25996,7 +25996,6 @@ CREATE TABLE web_hooks (
|
|||
member_events boolean DEFAULT false NOT NULL,
|
||||
subgroup_events boolean DEFAULT false NOT NULL,
|
||||
recent_failures smallint DEFAULT 0 NOT NULL,
|
||||
backoff_count smallint DEFAULT 0 NOT NULL,
|
||||
disabled_until timestamp with time zone,
|
||||
encrypted_url_variables bytea,
|
||||
encrypted_url_variables_iv bytea,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ Auditor users:
|
|||
- Cannot view the Admin area or perform any administration actions.
|
||||
- Cannot access group or projects settings.
|
||||
- Cannot view job logs when [debug logging](../ci/variables/variables_troubleshooting.md#enable-debug-logging) is enabled.
|
||||
- Cannot access areas designed for editing, including the [pipeline editor](../ci/pipeline_editor/_index.md).
|
||||
|
||||
Auditor users are sometimes used in situations where:
|
||||
|
||||
|
|
|
|||
|
|
@ -20,9 +20,8 @@ title: Rate limits on Git HTTP
|
|||
{{< /history >}}
|
||||
|
||||
If you use Git HTTP in your repository, common Git operations can generate many Git HTTP requests.
|
||||
Some of these Git HTTP requests do not contain authentication parameters, and are considered
|
||||
unauthenticated requests. Enforcing rate limits on Git HTTP requests can improve the security and
|
||||
durability of your web application.
|
||||
GitLab can enforce rate limits on both authenticated and unauthenticated Git HTTP requests to improve
|
||||
the security and durability of your web application.
|
||||
|
||||
{{< alert type="note" >}}
|
||||
|
||||
|
|
@ -30,10 +29,12 @@ durability of your web application.
|
|||
|
||||
{{< /alert >}}
|
||||
|
||||
## Configure Git HTTP rate limits
|
||||
## Configure unauthenticated Git HTTP rate limits
|
||||
|
||||
GitLab disables rate limits on Git HTTP requests by default. If you enable and configure these limits,
|
||||
GitLab applies them to Git HTTP requests:
|
||||
GitLab disables rate limits on unauthenticated Git HTTP requests by default.
|
||||
|
||||
To apply rate limits to Git HTTP requests that do not contain authentication
|
||||
parameters, enable and configure these limits:
|
||||
|
||||
1. On the left sidebar, at the bottom, select **Admin**.
|
||||
1. Select **Settings > Network**.
|
||||
|
|
@ -43,6 +44,34 @@ GitLab applies them to Git HTTP requests:
|
|||
1. Enter a value for **Unauthenticated Git HTTP rate limit period in seconds**.
|
||||
1. Select **Save changes**.
|
||||
|
||||
## Configure authenticated Git HTTP rate limits
|
||||
|
||||
{{< history >}}
|
||||
|
||||
- Authenticated Git HTTP rate limits [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/191552) in GitLab 18.1 [with a flag](../../administration/feature_flags/_index.md) named `git_authenticated_http_limit`. Disabled by default.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
{{< alert type="flag" >}}
|
||||
|
||||
The availability of this feature is controlled by a feature flag.
|
||||
For more information, see the history.
|
||||
|
||||
{{< /alert >}}
|
||||
|
||||
GitLab disables rate limits on authenticated Git HTTP requests by default.
|
||||
|
||||
To apply rate limits to Git HTTP requests that contain authentication
|
||||
parameters, enable and configure these limits:
|
||||
|
||||
1. On the left sidebar, at the bottom, select **Admin**.
|
||||
1. Select **Settings > Network**.
|
||||
1. Expand **Git HTTP rate limits**.
|
||||
1. Select **Enable authenticated Git HTTP request rate limit**.
|
||||
1. Enter a value for **Max authenticated Git HTTP requests per period per user**.
|
||||
1. Enter a value for **Authenticated Git HTTP rate limit period in seconds**.
|
||||
1. Select **Save changes**.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Rate limiting](../../security/rate_limits.md)
|
||||
|
|
|
|||
|
|
@ -930,7 +930,7 @@ Example response:
|
|||
|
||||
{{< alert type="warning" >}}
|
||||
|
||||
This endpoint is scheduled for removal in GitLab 18.5.
|
||||
This endpoint is scheduled for removal in GitLab 18.3 (August 11th, 2025).
|
||||
Use [`GET /groups/:id/saml_users`](#list-all-saml-users) and [`GET /groups/:id/service_accounts`](group_service_accounts.md#list-all-service-account-users) instead.
|
||||
|
||||
{{< /alert >}}
|
||||
|
|
|
|||
|
|
@ -1355,6 +1355,7 @@ To emphasize an area in a screenshot, use an arrow.
|
|||
For an MR added to 11.1's milestone, a valid name for an illustration is `devops_diagram_v11_1.png`.
|
||||
- Place images in a separate directory named `img/` in the same directory where
|
||||
the `.md` document that you're working on is located.
|
||||
- Do not link to externally-hosted images. Download a copy and store it in the appropriate `img` directory within the docs directory.
|
||||
- Consider PNG images instead of JPEG.
|
||||
- Compress GIFs with <https://ezgif.com/optimize> or similar tool.
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ GitLab can check your application for security vulnerabilities and that it meets
|
|||
| [Create a compliance pipeline](compliance_pipeline/_index.md) | Learn how to create compliance pipelines for your groups. | {{< icon name="star" >}} |
|
||||
| [Set up a merge request approval policy](scan_result_policy/_index.md) | Learn how to configure a merge request approval policy that takes action based on scan results. | {{< icon name="star" >}} |
|
||||
| [Set up a scan execution policy](scan_execution_policy/_index.md) | Learn how to create a scan execution policy to enforce security scanning of your project. | {{< icon name="star" >}} |
|
||||
| [Set up a pipeline execution policy](pipeline_execution_policy/_index.md) | Learn how to create a pipeline execution policy to enforce security scanning across projects as part of the pipeline. | {{< icon name="star" >}} |
|
||||
| [Scan a Docker container for vulnerabilities](container_scanning/_index.md) | Learn how to use container scanning templates to add container scanning to your projects. | {{< icon name="star" >}} |
|
||||
| [Protect your project with secret push protection](../user/application_security/secret_detection/push_protection_tutorial.md) | Enable secret push protection in your project. | {{< icon name="star" >}} |
|
||||
| [Remove a secret from your commits](../user/application_security/secret_detection/remove_secrets_tutorial.md) | Learn how to remove a secret from your commit history. | {{< icon name="star" >}} |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Ci
|
||||
module Builds
|
||||
Timeout = Struct.new(:value, :source)
|
||||
|
||||
class TimeoutCalculator
|
||||
def self.timeout_sources
|
||||
Ci::BuildMetadata.timeout_sources
|
||||
end
|
||||
|
||||
def initialize(build)
|
||||
@build = build
|
||||
end
|
||||
|
||||
def applicable_timeout
|
||||
[job_timeout || project_timeout, runner_timeout].compact.min_by(&:value)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :build
|
||||
|
||||
def job_timeout
|
||||
value = build.options[:job_timeout]
|
||||
return unless value
|
||||
|
||||
Ci::Builds::Timeout.new(value, fetch_source(:job_timeout_source))
|
||||
end
|
||||
|
||||
def project_timeout
|
||||
value = build.project&.build_timeout
|
||||
return unless value
|
||||
|
||||
Ci::Builds::Timeout.new(value, fetch_source(:project_timeout_source))
|
||||
end
|
||||
|
||||
def runner_timeout
|
||||
value = build.runner&.maximum_timeout.to_i
|
||||
return unless value > 0
|
||||
|
||||
Ci::Builds::Timeout.new(value, fetch_source(:runner_timeout_source))
|
||||
end
|
||||
|
||||
def fetch_source(source)
|
||||
self.class.timeout_sources.fetch(source)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,263 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "prawn"
|
||||
require "prawn-svg"
|
||||
|
||||
module Gitlab
|
||||
module PDF
|
||||
module Security
|
||||
class GroupVulnerabilitiesProjectsGrades
|
||||
include Prawn::View
|
||||
|
||||
DEFAULT_COUNTS = '0 projects'
|
||||
GRADES_DISPLAY_INFO = {
|
||||
a: { color: '#16a34a', severities: [] },
|
||||
b: { color: '#f97316', severities: %w[low] },
|
||||
c: { color: '#ea580c', severities: %w[medium] },
|
||||
d: { color: '#b91c1c', severities: %w[high unknown] },
|
||||
f: { color: '#991b1b', severities: %w[critical] }
|
||||
}.freeze
|
||||
|
||||
SVG_STYLES = <<~SVG_STYLES.freeze
|
||||
<defs>
|
||||
<style>
|
||||
.header-text { font-family: sans-serif; font-size: 18px; font-weight: bold; fill: #1f2937; }
|
||||
.subheader { font-family: sans-serif; font-size: 13px; fill: #6b7280; }
|
||||
.grade-letter { font-family: sans-serif; font-size: 16px; font-weight: bold; }
|
||||
.project-count { font-family: sans-serif; font-size: 14px; fill: #1f2937; }
|
||||
.description { font-family: sans-serif; font-size: 12px; fill: #6b7280; }
|
||||
.severity-count { font-family: sans-serif; font-size: 11px; }
|
||||
.grade-f { fill: #{GRADES_DISPLAY_INFO.dig(:f, :color)}; }
|
||||
.grade-d { fill: #{GRADES_DISPLAY_INFO.dig(:d, :color)}; }
|
||||
.grade-c { fill: #{GRADES_DISPLAY_INFO.dig(:c, :color)}; }
|
||||
.grade-b { fill: #{GRADES_DISPLAY_INFO.dig(:b, :color)}; }
|
||||
.grade-a { fill: #{GRADES_DISPLAY_INFO.dig(:a, :color)}; }
|
||||
</style>
|
||||
</defs>
|
||||
SVG_STYLES
|
||||
|
||||
def self.render(pdf, data: {})
|
||||
new(pdf, data).render
|
||||
end
|
||||
|
||||
def initialize(pdf, data)
|
||||
@pdf = pdf
|
||||
@grades = process_raw(data)
|
||||
@expanded_grade = data&.fetch(:expanded_grade, 'F')
|
||||
@gitlab_host_url = Rails.application.routes.url_helpers.root_url.chomp('/')
|
||||
@width = 500
|
||||
@height = 700
|
||||
@y = pdf.cursor
|
||||
end
|
||||
|
||||
def render
|
||||
return :noop if @grades.blank?
|
||||
|
||||
@pdf.bounding_box([0, @y], width: @pdf.bounds.right, height: @height) do
|
||||
@pdf.save_graphics_state
|
||||
@pdf.fill_color "F9F9F9"
|
||||
@pdf.fill_rectangle [0, @pdf.bounds.top], @pdf.bounds.right, @height
|
||||
@pdf.restore_graphics_state
|
||||
|
||||
@pdf.text_box(
|
||||
s_('Project security status'),
|
||||
at: [0, @pdf.bounds.top - 10],
|
||||
width: @pdf.bounds.right,
|
||||
align: :center,
|
||||
style: :bold,
|
||||
size: 16
|
||||
)
|
||||
|
||||
@pdf.text_box(
|
||||
s_('Projects are graded based on the highest severity vulnerability present'),
|
||||
at: [0, @pdf.bounds.top - 40],
|
||||
width: @pdf.bounds.right,
|
||||
align: :center,
|
||||
size: 12
|
||||
)
|
||||
|
||||
svg = build_base_svg
|
||||
@pdf.svg svg, at: [0, @pdf.cursor]
|
||||
|
||||
render_project_names
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_base_svg
|
||||
svg = <<~SVG
|
||||
<svg width="#{@pdf.bounds.width}" height="700" xmlns="http://www.w3.org/2000/svg">
|
||||
#{SVG_STYLES}
|
||||
#{svg_background_layers}
|
||||
#{svg_headers(title_y: 35, description_y: 55)}
|
||||
SVG
|
||||
|
||||
current_svg_y = 80
|
||||
@project_text_positions = [] # Store positions for later text rendering
|
||||
|
||||
@grades.each do |grade|
|
||||
if grade[:letter] == @expanded_grade
|
||||
expanded_drawer_height = (grade[:projects].count * 45) + 80
|
||||
|
||||
if grade[:projects]
|
||||
y_position = current_svg_y + 70
|
||||
grade[:projects].each_with_index do |project, index|
|
||||
@project_text_positions << {
|
||||
project: project,
|
||||
y: y_position + (index * 45),
|
||||
grade: grade[:letter]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
svg += expanded_grade_svg(grade, current_svg_y, expanded_drawer_height)
|
||||
current_svg_y += expanded_drawer_height
|
||||
else
|
||||
svg += collapsed_grade_svg(grade, current_svg_y)
|
||||
current_svg_y += 40 # collapsed row height
|
||||
end
|
||||
end
|
||||
|
||||
svg += '</svg>'
|
||||
end
|
||||
|
||||
def svg_background_layers
|
||||
<<~SVG
|
||||
<rect x="0" y="0" width="#{@pdf.bounds.width}" height="#{@height}" fill="#ffffff"/>
|
||||
<rect x=" 0" y="0" width="#{@pdf.bounds.width}" height="80" fill="#f9fafb" stroke="#e5e7eb" stroke-width="1"/>
|
||||
SVG
|
||||
end
|
||||
|
||||
def svg_headers(title_y:, description_y:)
|
||||
title = s_("Project security status")
|
||||
description = s_("Projects are graded based on the highest severity vulnerability present")
|
||||
|
||||
<<~SVG
|
||||
<text x="20" y="#{title_y}" class="header-text">#{title}</text>
|
||||
<text x="20" y="#{description_y}" class="subheader">#{description}</text>
|
||||
SVG
|
||||
end
|
||||
|
||||
def expanded_grade_svg(grade, current_svg_y, drawer_height)
|
||||
letter_grade = grade[:letter]
|
||||
projects = grade[:projects]
|
||||
|
||||
<<~SVG
|
||||
<g transform="translate(0, #{current_svg_y})">
|
||||
<rect x="0" y="0" width="#{@pdf.bounds.width}" height="#{drawer_height}" fill="#ffffff" stroke="#e5e7eb" stroke-width="1"/>
|
||||
<rect x="0" y="0" width="#{@pdf.bounds.width}" height="40" fill="#f3f4f6"/>
|
||||
<text x="20" y="25" class="grade-letter grade-#{@expanded_grade.downcase}">#{letter_grade}</text>
|
||||
<text x="50" y="25" class="project-count">#{grade[:count]}</text>
|
||||
<text x="20" y="60" class="description">#{grade[:description]}</text>
|
||||
<g transform="translate(20, 80)">
|
||||
#{severity_counts_svg(projects, letter_grade)}
|
||||
</g>
|
||||
</g>
|
||||
SVG
|
||||
end
|
||||
|
||||
def severity_counts_svg(projects, letter_grade)
|
||||
severities_included_in_grade = GRADES_DISPLAY_INFO[letter_grade.downcase.to_sym][:severities]
|
||||
y = 0
|
||||
|
||||
projects.map do |project|
|
||||
y_offset = 15 # Start below where the project name would be
|
||||
svg = ""
|
||||
severities_included_in_grade.each do |severity|
|
||||
count = project['vulnerabilitySeveritiesCount'][severity]
|
||||
next if count == 0
|
||||
|
||||
count_text = "#{count} #{severity}"
|
||||
severity_css = "severity-count grade-#{letter_grade.downcase}"
|
||||
|
||||
svg += "<text x=\"0\" y=\"#{y + y_offset}\" class=\"#{severity_css}\">#{count_text}</text>"
|
||||
y_offset += 15
|
||||
end
|
||||
|
||||
y += 45 # Move to next project position
|
||||
svg
|
||||
end.join
|
||||
end
|
||||
|
||||
def collapsed_grade_svg(grade, current_svg_y)
|
||||
count = grade[:count]
|
||||
letter_grade = grade[:letter]
|
||||
|
||||
<<~SVG
|
||||
<g transform="translate(0, #{current_svg_y})">
|
||||
<rect x="0" y="0" width="#{@pdf.bounds.width}" height="40" fill="#ffffff" stroke="#e5e7eb" stroke-width="1"/>
|
||||
<rect x="0" y="0" width="#{@pdf.bounds.width}" height="40" fill="#f9fafb"/>
|
||||
<text x="20" y="25" class="grade-letter grade-#{letter_grade.downcase}">#{letter_grade}</text>
|
||||
<text x="50" y="25" class="project-count">#{count}</text>
|
||||
</g>
|
||||
SVG
|
||||
end
|
||||
|
||||
def render_project_names
|
||||
@project_text_positions.each do |pos|
|
||||
project_name = pos[:project]['nameWithNamespace']
|
||||
dashboard_link = @gitlab_host_url + pos[:project]['securityDashboardPath']
|
||||
|
||||
@pdf.formatted_text_box(
|
||||
[{ text: project_name, color: '2563eb', link: dashboard_link }],
|
||||
at: [20, @pdf.bounds.top - pos[:y]],
|
||||
width: @pdf.bounds.width - 40,
|
||||
height: 15,
|
||||
overflow: :ellipsis,
|
||||
single_line: true,
|
||||
size: 12
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def process_raw(data)
|
||||
grades = data.present? ? data[:vulnerability_grades] : []
|
||||
return if grades.blank?
|
||||
|
||||
grade_order = %w[F D C B A]
|
||||
grades = grades.sort_by { |g| g[:grade] }.reverse!
|
||||
|
||||
grade_order.map! do |letter_grade|
|
||||
if letter_grade == grades.first&.fetch(:grade)
|
||||
grade = grades.shift
|
||||
|
||||
{
|
||||
letter: grade['grade'],
|
||||
count: "#{grade['count']} projects",
|
||||
projects: sort_projects(grade.dig('projects', 'nodes'))
|
||||
}
|
||||
else
|
||||
{ letter: letter_grade, count: DEFAULT_COUNTS, projects: [] }
|
||||
end
|
||||
end
|
||||
|
||||
grade_order
|
||||
end
|
||||
|
||||
# TODO: Once the below issue is resolved, we can likely delete
|
||||
# this sorting as the projects should arrive to us sorted:
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/545479
|
||||
def sort_projects(projects)
|
||||
return projects if projects.blank?
|
||||
|
||||
projects.sort_by do |project|
|
||||
severities = project["vulnerabilitySeveritiesCount"]
|
||||
[
|
||||
-severities["critical"],
|
||||
-severities["high"],
|
||||
-severities["medium"],
|
||||
-severities["low"],
|
||||
-severities["info"],
|
||||
-severities["unknown"]
|
||||
]
|
||||
end.first(5)
|
||||
end
|
||||
|
||||
def severity_css(letter_grade)
|
||||
"severity-count grade-#{letter_grade.downcase}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -48217,6 +48217,9 @@ msgstr ""
|
|||
msgid "Project ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Project Security Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Project Status"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -68655,6 +68658,9 @@ msgstr ""
|
|||
msgid "Vulnerabilities"
|
||||
msgstr ""
|
||||
|
||||
msgid "Vulnerabilities Over Time"
|
||||
msgstr ""
|
||||
|
||||
msgid "Vulnerabilities over time"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ gem 'rest-client', '~> 2.1.0'
|
|||
gem 'rspec_junit_formatter', '~> 0.6.0'
|
||||
gem 'faker', '~> 3.5', '>= 3.5.2'
|
||||
gem 'knapsack', '~> 4.0'
|
||||
gem 'parallel_tests', '~> 5.1'
|
||||
gem 'parallel_tests', '~> 5.3'
|
||||
gem 'rotp', '~> 6.3.0'
|
||||
gem 'parallel', '~> 1.27'
|
||||
gem 'rainbow', '~> 3.1.1'
|
||||
|
|
@ -32,7 +32,7 @@ gem 'fog-google', '~> 1.25', require: false
|
|||
gem "warning", "~> 1.5"
|
||||
|
||||
# dependencies for jenkins client
|
||||
gem 'nokogiri', '~> 1.18', '>= 1.18.1'
|
||||
gem 'nokogiri', '~> 1.18', '>= 1.18.8'
|
||||
|
||||
gem 'deprecation_toolkit', '~> 2.2.3', require: false
|
||||
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@ GEM
|
|||
net-http (0.4.1)
|
||||
uri
|
||||
netrc (0.11.0)
|
||||
nokogiri (1.18.1)
|
||||
nokogiri (1.18.8)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
octokit (9.2.0)
|
||||
|
|
@ -239,7 +239,7 @@ GEM
|
|||
sawyer (~> 0.9)
|
||||
os (1.1.4)
|
||||
parallel (1.27.0)
|
||||
parallel_tests (5.1.0)
|
||||
parallel_tests (5.3.0)
|
||||
parallel
|
||||
parser (3.3.7.0)
|
||||
ast (~> 2.4.1)
|
||||
|
|
@ -390,10 +390,10 @@ DEPENDENCIES
|
|||
influxdb-client (~> 3.2)
|
||||
junit_merge (~> 0.1.2)
|
||||
knapsack (~> 4.0)
|
||||
nokogiri (~> 1.18, >= 1.18.1)
|
||||
nokogiri (~> 1.18, >= 1.18.8)
|
||||
octokit (~> 9.2.0)
|
||||
parallel (~> 1.27)
|
||||
parallel_tests (~> 5.1)
|
||||
parallel_tests (~> 5.3)
|
||||
pry-byebug (~> 3.11.0)
|
||||
rainbow (~> 3.1.1)
|
||||
rake (~> 13, >= 13.3.0)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe "User creates issue", feature_category: :team_planning do
|
||||
RSpec.describe "User creates issue", :js, feature_category: :team_planning do
|
||||
include DropzoneHelper
|
||||
include ListboxHelpers
|
||||
|
||||
|
|
@ -23,16 +23,12 @@ RSpec.describe "User creates issue", feature_category: :team_planning do
|
|||
sign_out(:user)
|
||||
end
|
||||
|
||||
it "redirects to signin then back to new issue after signin", :js do
|
||||
it "redirects to signin then back to new issue after signin" do
|
||||
create(:issue, project: project)
|
||||
|
||||
visit project_issues_path(project)
|
||||
|
||||
wait_for_all_requests
|
||||
|
||||
page.within ".nav-controls" do
|
||||
click_link "New issue"
|
||||
end
|
||||
click_link 'New item'
|
||||
|
||||
expect(page).to have_current_path new_user_session_path, ignore_query: true
|
||||
|
||||
|
|
@ -42,7 +38,7 @@ RSpec.describe "User creates issue", feature_category: :team_planning do
|
|||
end
|
||||
end
|
||||
|
||||
context "when signed in as guest", :js do
|
||||
context "when signed in as guest" do
|
||||
before do
|
||||
project.add_guest(user)
|
||||
sign_in(user)
|
||||
|
|
@ -114,7 +110,7 @@ RSpec.describe "User creates issue", feature_category: :team_planning do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with due date', :js do
|
||||
context 'with due date' do
|
||||
it 'saves with due date' do
|
||||
visit(new_project_issue_path(project))
|
||||
|
||||
|
|
@ -134,7 +130,7 @@ RSpec.describe "User creates issue", feature_category: :team_planning do
|
|||
end
|
||||
end
|
||||
|
||||
context 'dropzone upload file', :js do
|
||||
context 'dropzone upload file' do
|
||||
before do
|
||||
visit new_project_issue_path(project)
|
||||
end
|
||||
|
|
@ -208,7 +204,7 @@ RSpec.describe "User creates issue", feature_category: :team_planning do
|
|||
end
|
||||
end
|
||||
|
||||
context 'suggestions', :js do
|
||||
context 'suggestions' do
|
||||
it 'displays list of related issues' do
|
||||
visit(new_project_issue_path(project))
|
||||
|
||||
|
|
@ -224,7 +220,7 @@ RSpec.describe "User creates issue", feature_category: :team_planning do
|
|||
end
|
||||
end
|
||||
|
||||
it 'clears local storage after creating a new issue', :js do
|
||||
it 'clears local storage after creating a new issue' do
|
||||
2.times do
|
||||
visit new_project_issue_path(project)
|
||||
|
||||
|
|
@ -238,7 +234,7 @@ RSpec.describe "User creates issue", feature_category: :team_planning do
|
|||
end
|
||||
end
|
||||
|
||||
it 'clears local storage after cancelling a new issue creation', :js do
|
||||
it 'clears local storage after cancelling a new issue creation' do
|
||||
2.times do
|
||||
visit new_project_issue_path(project)
|
||||
|
||||
|
|
@ -254,7 +250,7 @@ RSpec.describe "User creates issue", feature_category: :team_planning do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when signed in as reporter', :js do
|
||||
context 'when signed in as reporter' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
before_all do
|
||||
|
|
@ -279,7 +275,7 @@ RSpec.describe "User creates issue", feature_category: :team_planning do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when signed in as a maintainer', :js do
|
||||
context 'when signed in as a maintainer' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
before_all do
|
||||
|
|
@ -310,7 +306,7 @@ RSpec.describe "User creates issue", feature_category: :team_planning do
|
|||
visit(new_project_issue_path(project))
|
||||
end
|
||||
|
||||
it "will correctly escape user names with an apostrophe when clicking 'Assign to me'", :js do
|
||||
it "will correctly escape user names with an apostrophe when clicking 'Assign to me'" do
|
||||
click_button 'assign yourself'
|
||||
|
||||
expect(page).to have_content(user_special.name)
|
||||
|
|
|
|||
|
|
@ -67,6 +67,23 @@ RSpec.describe 'Sandboxed Mermaid rendering', :js, feature_category: :markdown d
|
|||
end
|
||||
end
|
||||
|
||||
context 'in a project home page' do
|
||||
let!(:wiki) { create(:project_wiki, project: project) }
|
||||
let!(:wiki_page) { create(:wiki_page, wiki: wiki, title: 'home', content: description) }
|
||||
|
||||
before do
|
||||
project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)
|
||||
end
|
||||
|
||||
it 'includes mermaid frame correctly' do
|
||||
visit(project_path(project))
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page.html).to include(expected)
|
||||
end
|
||||
end
|
||||
|
||||
context 'in a group milestone' do
|
||||
let(:group_milestone) { create(:group_milestone, description: description) }
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ exports[`Code navigation popover component renders popover 1`] = `
|
|||
>
|
||||
<div>
|
||||
<pre
|
||||
class="bg-transparent border-0 code highlight m-0 text-wrap"
|
||||
class="bg-transparent border-0 code code-syntax-highlight-theme highlight m-0 text-wrap"
|
||||
>
|
||||
<span
|
||||
class="line"
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import EmptyStateWithoutAnyIssues from '~/issues/list/components/empty_state_wit
|
|||
import IssuesListApp from '~/issues/list/components/issues_list_app.vue';
|
||||
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
|
||||
import NewResourceDropdown from '~/vue_shared/components/new_resource_dropdown/new_resource_dropdown.vue';
|
||||
import CreateWorkItemModal from '~/work_items/components/create_work_item_modal.vue';
|
||||
import WorkItemDrawer from '~/work_items/components/work_item_drawer.vue';
|
||||
import {
|
||||
CREATED_DESC,
|
||||
|
|
@ -171,6 +172,7 @@ describe('CE IssuesListApp component', () => {
|
|||
const mockIssuesQueryResponse = jest.fn().mockResolvedValue(defaultQueryResponse);
|
||||
const mockIssuesCountsQueryResponse = jest.fn().mockResolvedValue(getIssuesCountsQueryResponse);
|
||||
|
||||
const findCreateWorkItemModal = () => wrapper.findComponent(CreateWorkItemModal);
|
||||
const findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons);
|
||||
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
|
||||
const findIssuableByEmail = () => wrapper.findComponent(IssuableByEmail);
|
||||
|
|
@ -178,7 +180,7 @@ describe('CE IssuesListApp component', () => {
|
|||
const findGlButtons = () => wrapper.findAllComponents(GlButton);
|
||||
const findIssuableList = () => wrapper.findComponent(IssuableList);
|
||||
const findListViewTypeBtn = () => wrapper.findByTestId('list-view-type');
|
||||
const findGridtViewTypeBtn = () => wrapper.findByTestId('grid-view-type');
|
||||
const findGridViewTypeBtn = () => wrapper.findByTestId('grid-view-type');
|
||||
const findViewTypeLocalStorageSync = () => wrapper.findAllComponents(LocalStorageSync).at(0);
|
||||
const findNewResourceDropdown = () => wrapper.findComponent(NewResourceDropdown);
|
||||
const findCalendarButton = () => wrapper.findByTestId('subscribe-calendar');
|
||||
|
|
@ -376,6 +378,17 @@ describe('CE IssuesListApp component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('create modal', () => {
|
||||
it.each([true, false])(
|
||||
'renders depending on whether issuesListCreateModal=%s',
|
||||
(issuesListCreateModal) => {
|
||||
wrapper = mountComponent({ provide: { glFeatures: { issuesListCreateModal } } });
|
||||
|
||||
expect(findCreateWorkItemModal().exists()).toBe(issuesListCreateModal);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('new issue button', () => {
|
||||
it('renders when user has permissions', () => {
|
||||
wrapper = mountComponent({ provide: { showNewIssueLink: true }, mountFn: mount });
|
||||
|
|
@ -389,6 +402,12 @@ describe('CE IssuesListApp component', () => {
|
|||
|
||||
expect(findGlButtons().filter((button) => button.text() === 'New issue')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does not render when `issuesListCreateModal` is enabled', () => {
|
||||
wrapper = mountComponent({ provide: { glFeatures: { issuesListCreateModal: true } } });
|
||||
|
||||
expect(findGlButtons().filter((button) => button.text() === 'New issue')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('new issue split dropdown', () => {
|
||||
|
|
@ -403,6 +422,14 @@ describe('CE IssuesListApp component', () => {
|
|||
|
||||
expect(findNewResourceDropdown().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not render when `issuesListCreateModal` is enabled', () => {
|
||||
wrapper = mountComponent({
|
||||
provide: { isProject: false, glFeatures: { issuesListCreateModal: true } },
|
||||
});
|
||||
|
||||
expect(findNewResourceDropdown().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -424,7 +451,7 @@ describe('CE IssuesListApp component', () => {
|
|||
});
|
||||
|
||||
it('switch between list and grid', async () => {
|
||||
findGridtViewTypeBtn().vm.$emit('click');
|
||||
findGridViewTypeBtn().vm.$emit('click');
|
||||
await nextTick();
|
||||
|
||||
expect(findIssuableList().props('isGridView')).toBe(true);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
|
|||
Preview
|
||||
</label>
|
||||
<table
|
||||
class="code"
|
||||
class="code code-syntax-highlight-theme"
|
||||
>
|
||||
<tbody>
|
||||
<tr
|
||||
|
|
|
|||
|
|
@ -3,10 +3,6 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
|
|||
import syntaxHighlight from '~/syntax_highlight';
|
||||
|
||||
describe('Syntax Highlighter', () => {
|
||||
const stubUserColorScheme = (value) => {
|
||||
window.gon.user_color_scheme = value;
|
||||
};
|
||||
|
||||
// We have to bind `document.querySelectorAll` to `document` to not mess up the fn's context
|
||||
describe.each`
|
||||
desc | fn
|
||||
|
|
@ -24,10 +20,9 @@ describe('Syntax Highlighter', () => {
|
|||
});
|
||||
|
||||
it('applies syntax highlighting', () => {
|
||||
stubUserColorScheme('monokai');
|
||||
syntaxHighlight(fn('.js-syntax-highlight'));
|
||||
|
||||
expect(fn('.js-syntax-highlight')).toHaveClass('monokai');
|
||||
expect(fn('.js-syntax-highlight')).toHaveClass('code-syntax-highlight-theme');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -43,13 +38,12 @@ describe('Syntax Highlighter', () => {
|
|||
});
|
||||
|
||||
it('applies highlighting to all applicable children', () => {
|
||||
stubUserColorScheme('monokai');
|
||||
syntaxHighlight(fn('.parent'));
|
||||
|
||||
expect(fn('.parent')).not.toHaveClass('monokai');
|
||||
expect(fn('.foo')).not.toHaveClass('monokai');
|
||||
expect(fn('.parent')).not.toHaveClass('code-syntax-highlight-theme');
|
||||
expect(fn('.foo')).not.toHaveClass('code-syntax-highlight-theme');
|
||||
|
||||
expect(document.querySelectorAll('.monokai').length).toBe(2);
|
||||
expect(document.querySelectorAll('.code-syntax-highlight-theme').length).toBe(2);
|
||||
});
|
||||
|
||||
it('prevents an infinite loop when no matches exist', () => {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
exports[`Blob Simple Viewer component rendering matches the snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="code file-content gl-flex js-syntax-highlight"
|
||||
class="code code-syntax-highlight-theme file-content gl-flex js-syntax-highlight"
|
||||
>
|
||||
<div
|
||||
class="!gl-px-0 line-numbers"
|
||||
|
|
|
|||
|
|
@ -17,58 +17,58 @@ describe('Code Block', () => {
|
|||
createComponent({}, { default: 'DEFAULT SLOT' });
|
||||
|
||||
expect(wrapper.element).toMatchInlineSnapshot(`
|
||||
<pre
|
||||
class="code code-block rounded"
|
||||
>
|
||||
DEFAULT SLOT
|
||||
</pre>
|
||||
`);
|
||||
<pre
|
||||
class="code code-block code-syntax-highlight-theme rounded"
|
||||
>
|
||||
DEFAULT SLOT
|
||||
</pre>
|
||||
`);
|
||||
});
|
||||
|
||||
it('renders with empty code prop', () => {
|
||||
createComponent({});
|
||||
|
||||
expect(wrapper.element).toMatchInlineSnapshot(`
|
||||
<pre
|
||||
class="code code-block rounded"
|
||||
>
|
||||
<code
|
||||
class="gl-block"
|
||||
/>
|
||||
</pre>
|
||||
`);
|
||||
<pre
|
||||
class="code code-block code-syntax-highlight-theme rounded"
|
||||
>
|
||||
<code
|
||||
class="gl-block"
|
||||
/>
|
||||
</pre>
|
||||
`);
|
||||
});
|
||||
|
||||
it('renders code prop when provided', () => {
|
||||
createComponent({ code });
|
||||
|
||||
expect(wrapper.element).toMatchInlineSnapshot(`
|
||||
<pre
|
||||
class="code code-block rounded"
|
||||
>
|
||||
<code
|
||||
class="gl-block"
|
||||
>
|
||||
test-code
|
||||
</code>
|
||||
</pre>
|
||||
`);
|
||||
<pre
|
||||
class="code code-block code-syntax-highlight-theme rounded"
|
||||
>
|
||||
<code
|
||||
class="gl-block"
|
||||
>
|
||||
test-code
|
||||
</code>
|
||||
</pre>
|
||||
`);
|
||||
});
|
||||
|
||||
it('sets maxHeight properly when provided', () => {
|
||||
createComponent({ code, maxHeight: '200px' });
|
||||
|
||||
expect(wrapper.element).toMatchInlineSnapshot(`
|
||||
<pre
|
||||
class="code code-block rounded"
|
||||
style="max-height: 200px; overflow-y: auto;"
|
||||
>
|
||||
<code
|
||||
class="gl-block"
|
||||
>
|
||||
test-code
|
||||
</code>
|
||||
</pre>
|
||||
`);
|
||||
<pre
|
||||
class="code code-block code-syntax-highlight-theme rounded"
|
||||
style="max-height: 200px; overflow-y: auto;"
|
||||
>
|
||||
<code
|
||||
class="gl-block"
|
||||
>
|
||||
test-code
|
||||
</code>
|
||||
</pre>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -78,6 +78,10 @@ describe('CreateWorkItemModal', () => {
|
|||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
gon.current_user_id = 1;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
|
@ -95,6 +99,26 @@ describe('CreateWorkItemModal', () => {
|
|||
expect(findForm().props('preselectedWorkItemType')).toBe(WORK_ITEM_TYPE_NAME_ISSUE);
|
||||
});
|
||||
|
||||
it('renders create-work-item component with preselectedWorkItemType prop set from localStorage draft with related item id', async () => {
|
||||
localStorage.setItem(
|
||||
'autosave/new-full-path-related-22-widgets-draft',
|
||||
JSON.stringify({ TYPE: { name: WORK_ITEM_TYPE_NAME_ISSUE } }),
|
||||
);
|
||||
|
||||
createComponent({
|
||||
relatedItem: {
|
||||
id: 'gid://gitlab/WorkItem/22',
|
||||
type: 'Issue',
|
||||
reference: 'full-path#22',
|
||||
webUrl: '/full-path/-/issues/22',
|
||||
},
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findForm().props('preselectedWorkItemType')).toBe(WORK_ITEM_TYPE_NAME_ISSUE);
|
||||
});
|
||||
|
||||
it('shows toast on workItemCreated', async () => {
|
||||
createComponent();
|
||||
|
||||
|
|
@ -134,6 +158,19 @@ describe('CreateWorkItemModal', () => {
|
|||
expect(mockEvent.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not open modal or prevent link default when user is signed out', async () => {
|
||||
window.gon = { current_user_id: undefined };
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
|
||||
const mockEvent = { preventDefault: jest.fn(), ctrlKey: true };
|
||||
findTrigger().vm.$emit('click', mockEvent);
|
||||
await nextTick();
|
||||
|
||||
expect(findCreateModal().props('visible')).toBe(false);
|
||||
expect(mockEvent.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not render when hideButton=true', () => {
|
||||
createComponent({ hideButton: true });
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import { setNewWorkItemCache } from '~/work_items/graphql/cache_utils';
|
|||
import { updateDraftWorkItemType } from '~/work_items/utils';
|
||||
import namespaceWorkItemTypesQuery from '~/work_items/graphql/namespace_work_item_types.query.graphql';
|
||||
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
|
||||
import updateNewWorkItemMutation from '~/work_items/graphql/update_new_work_item.mutation.graphql';
|
||||
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
|
||||
import { resolvers } from '~/graphql_shared/issuable_client';
|
||||
import setWindowLocation from 'helpers/set_window_location_helper';
|
||||
|
|
@ -73,6 +74,12 @@ describe('Create work item component', () => {
|
|||
const namespaceWorkItemTypes =
|
||||
namespaceWorkItemTypesQueryResponse.data.workspace.workItemTypes.nodes;
|
||||
const { webUrl: namespaceWebUrl } = namespaceWorkItemTypesQueryResponse.data.workspace;
|
||||
const mockRelatedItem = {
|
||||
id: 'gid://gitlab/WorkItem/22',
|
||||
type: 'Issue',
|
||||
reference: 'full-path#22',
|
||||
webUrl: '/full-path/-/issues/22',
|
||||
};
|
||||
|
||||
const findFormTitle = () => wrapper.find('h1');
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
|
|
@ -174,7 +181,11 @@ describe('Create work item component', () => {
|
|||
|
||||
describe('Default', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent();
|
||||
createComponent({
|
||||
props: {
|
||||
relatedItem: mockRelatedItem,
|
||||
},
|
||||
});
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
|
|
@ -183,6 +194,35 @@ describe('Create work item component', () => {
|
|||
expect(findAlert().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('calls `updateNewWorkItemMutation` mutation when any widget emits `updateWidgetDraft` event', () => {
|
||||
jest.spyOn(mockApollo.defaultClient, 'mutate');
|
||||
const mockInput = {
|
||||
workItemType: 'Issue',
|
||||
fullPath: 'full-path',
|
||||
assignees: [
|
||||
{
|
||||
__typename: 'CurrentUser',
|
||||
id: 'gid://gitlab/User/1',
|
||||
name: 'Administrator',
|
||||
username: 'root',
|
||||
webUrl: 'http://127.0.0.1:3000/root',
|
||||
webPath: '/root',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
findAssigneesWidget().vm.$emit('updateWidgetDraft', mockInput);
|
||||
expect(mockApollo.defaultClient.mutate).toHaveBeenCalledWith({
|
||||
mutation: updateNewWorkItemMutation,
|
||||
variables: {
|
||||
input: {
|
||||
...mockInput,
|
||||
relatedItemId: mockRelatedItem.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('emits "confirmCancel" event on Cancel button click if form is filled', async () => {
|
||||
await updateWorkItemTitle();
|
||||
findCancelButton().vm.$emit('click');
|
||||
|
|
@ -233,7 +273,12 @@ describe('Create work item component', () => {
|
|||
const expectedWorkItemTypeData = namespaceWorkItemTypes.find(
|
||||
({ name }) => name === workItemType,
|
||||
);
|
||||
createComponent({ props: { preselectedWorkItemType: workItemType } });
|
||||
createComponent({
|
||||
props: {
|
||||
preselectedWorkItemType: workItemType,
|
||||
relatedItem: mockRelatedItem,
|
||||
},
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
findCancelButton().vm.$emit('click');
|
||||
|
|
@ -245,6 +290,7 @@ describe('Create work item component', () => {
|
|||
workItemType: expectedWorkItemTypeData.name,
|
||||
workItemTypeId: expectedWorkItemTypeData.id,
|
||||
workItemTypeIconName: expectedWorkItemTypeData.iconName,
|
||||
relatedItemId: mockRelatedItem.id,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
@ -367,7 +413,7 @@ describe('Create work item component', () => {
|
|||
});
|
||||
|
||||
it('sets new work item cache and emits changeType on select', async () => {
|
||||
createComponent({ props: { preselectedWorkItemType: null } });
|
||||
createComponent({ props: { preselectedWorkItemType: null, relatedItem: mockRelatedItem } });
|
||||
await waitForPromises();
|
||||
const mockId = 'Issue';
|
||||
|
||||
|
|
@ -380,13 +426,14 @@ describe('Create work item component', () => {
|
|||
workItemType: mockId,
|
||||
workItemTypeId: 'gid://gitlab/WorkItems::Type/1',
|
||||
workItemTypeIconName: 'issue-type-issue',
|
||||
relatedItemId: mockRelatedItem.id,
|
||||
});
|
||||
|
||||
expect(wrapper.emitted('changeType')).toBeDefined();
|
||||
});
|
||||
|
||||
it('sets selected work item type in localStorage draft', async () => {
|
||||
createComponent({ props: { preselectedWorkItemType: null } });
|
||||
createComponent({ props: { preselectedWorkItemType: null, relatedItem: mockRelatedItem } });
|
||||
await waitForPromises();
|
||||
const mockId = 'Issue';
|
||||
|
||||
|
|
@ -395,6 +442,7 @@ describe('Create work item component', () => {
|
|||
|
||||
expect(updateDraftWorkItemType).toHaveBeenCalledWith({
|
||||
fullPath: 'full-path',
|
||||
relatedItemId: mockRelatedItem.id,
|
||||
workItemType: {
|
||||
id: 'gid://gitlab/WorkItems::Type/1',
|
||||
name: mockId,
|
||||
|
|
@ -902,7 +950,11 @@ describe('Create work item component', () => {
|
|||
|
||||
</div>
|
||||
</div>`);
|
||||
createComponent();
|
||||
createComponent({
|
||||
props: {
|
||||
relatedItem: mockRelatedItem,
|
||||
},
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
expect(setNewWorkItemCache).toHaveBeenCalledWith({
|
||||
|
|
@ -917,6 +969,7 @@ describe('Create work item component', () => {
|
|||
a
|
||||
description!`,
|
||||
confidential: false,
|
||||
relatedItemId: mockRelatedItem.id,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -124,67 +124,130 @@ describe('WorkItemBulkEditSidebar component', () => {
|
|||
});
|
||||
|
||||
describe('when not epics list', () => {
|
||||
it('makes POST request to bulk edit', async () => {
|
||||
const issuable_ids = '11,22'; // eslint-disable-line camelcase
|
||||
const add_label_ids = [1, 2, 3]; // eslint-disable-line camelcase
|
||||
const assignee_ids = [5]; // eslint-disable-line camelcase
|
||||
const confidential = 'true';
|
||||
const health_status = 'on_track'; // eslint-disable-line camelcase
|
||||
const remove_label_ids = [4, 5, 6]; // eslint-disable-line camelcase
|
||||
const state_event = 'reopen'; // eslint-disable-line camelcase
|
||||
const subscription_event = 'subscribe'; // eslint-disable-line camelcase
|
||||
axiosMock.onPost().replyOnce(HTTP_STATUS_OK);
|
||||
createComponent({
|
||||
provide: { hasIssuableHealthStatusFeature: true },
|
||||
props: { isEpicsList: false },
|
||||
describe('when work_items_bulk_edit is enabled', () => {
|
||||
it('calls mutation to bulk edit', async () => {
|
||||
const addLabelIds = ['gid://gitlab/Label/1'];
|
||||
const removeLabelIds = ['gid://gitlab/Label/2'];
|
||||
createComponent({
|
||||
provide: {
|
||||
hasIssuableHealthStatusFeature: true,
|
||||
glFeatures: { workItemsBulkEdit: true },
|
||||
},
|
||||
props: { isEpicsList: false },
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
findAssigneeComponent().vm.$emit('input', 'gid://gitlab/User/5');
|
||||
findAddLabelsComponent().vm.$emit('select', addLabelIds);
|
||||
findRemoveLabelsComponent().vm.$emit('select', removeLabelIds);
|
||||
findHealthStatusComponent().vm.$emit('input', 'on_track');
|
||||
findConfidentialityComponent().vm.$emit('input', 'true');
|
||||
findForm().vm.$emit('submit', { preventDefault: () => {} });
|
||||
|
||||
expect(workItemBulkUpdateHandler).toHaveBeenCalledWith({
|
||||
input: {
|
||||
parentId: 'gid://gitlab/Group/1',
|
||||
ids: ['gid://gitlab/WorkItem/11', 'gid://gitlab/WorkItem/22'],
|
||||
labelsWidget: {
|
||||
addLabelIds,
|
||||
removeLabelIds,
|
||||
},
|
||||
assigneesWidget: {
|
||||
assigneeIds: ['gid://gitlab/User/5'],
|
||||
},
|
||||
confidential: true,
|
||||
healthStatusWidget: {
|
||||
healthStatus: 'onTrack',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(findAddLabelsComponent().props('selectedLabelsIds')).toEqual([]);
|
||||
expect(findRemoveLabelsComponent().props('selectedLabelsIds')).toEqual([]);
|
||||
});
|
||||
|
||||
findStateComponent().vm.$emit('input', state_event);
|
||||
findAssigneeComponent().vm.$emit('input', 'gid://gitlab/User/5');
|
||||
findAddLabelsComponent().vm.$emit('select', [
|
||||
'gid://gitlab/Label/1',
|
||||
'gid://gitlab/Label/2',
|
||||
'gid://gitlab/Label/3',
|
||||
]);
|
||||
findRemoveLabelsComponent().vm.$emit('select', [
|
||||
'gid://gitlab/Label/4',
|
||||
'gid://gitlab/Label/5',
|
||||
'gid://gitlab/Label/6',
|
||||
]);
|
||||
findHealthStatusComponent().vm.$emit('input', health_status);
|
||||
findSubscriptionComponent().vm.$emit('input', subscription_event);
|
||||
findConfidentialityComponent().vm.$emit('input', confidential);
|
||||
findForm().vm.$emit('submit', { preventDefault: () => {} });
|
||||
await waitForPromises();
|
||||
it('renders error when there is a mutation error', async () => {
|
||||
createComponent({
|
||||
props: { isEpicsList: true },
|
||||
mutationHandler: jest.fn().mockRejectedValue(new Error('oh no')),
|
||||
});
|
||||
|
||||
expect(axiosMock.history.post[0].url).toBe('/group/project/-/issues/bulk_update');
|
||||
expect(axiosMock.history.post[0].data).toBe(
|
||||
JSON.stringify({
|
||||
update: {
|
||||
add_label_ids,
|
||||
assignee_ids,
|
||||
confidential,
|
||||
health_status,
|
||||
issuable_ids,
|
||||
remove_label_ids,
|
||||
state_event,
|
||||
subscription_event,
|
||||
},
|
||||
}),
|
||||
);
|
||||
findForm().vm.$emit('submit', { preventDefault: () => {} });
|
||||
await waitForPromises();
|
||||
|
||||
expect(createAlert).toHaveBeenCalledWith({
|
||||
captureError: true,
|
||||
error: new Error('oh no'),
|
||||
message: 'Something went wrong while bulk editing.',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders error when there is a response error', async () => {
|
||||
axiosMock.onPost().replyOnce(HTTP_STATUS_BAD_REQUEST);
|
||||
createComponent({ props: { isEpicsList: false } });
|
||||
describe('when work_items_bulk_edit is disabled', () => {
|
||||
it('makes POST request to bulk edit', async () => {
|
||||
const issuable_ids = '11,22'; // eslint-disable-line camelcase
|
||||
const add_label_ids = [1, 2, 3]; // eslint-disable-line camelcase
|
||||
const assignee_ids = [5]; // eslint-disable-line camelcase
|
||||
const confidential = 'true';
|
||||
const health_status = 'on_track'; // eslint-disable-line camelcase
|
||||
const remove_label_ids = [4, 5, 6]; // eslint-disable-line camelcase
|
||||
const state_event = 'reopen'; // eslint-disable-line camelcase
|
||||
const subscription_event = 'subscribe'; // eslint-disable-line camelcase
|
||||
axiosMock.onPost().replyOnce(HTTP_STATUS_OK);
|
||||
createComponent({
|
||||
provide: {
|
||||
hasIssuableHealthStatusFeature: true,
|
||||
glFeatures: { workItemsBulkEdit: false },
|
||||
},
|
||||
props: { isEpicsList: false },
|
||||
});
|
||||
|
||||
findForm().vm.$emit('submit', { preventDefault: () => {} });
|
||||
await waitForPromises();
|
||||
findStateComponent().vm.$emit('input', state_event);
|
||||
findAssigneeComponent().vm.$emit('input', 'gid://gitlab/User/5');
|
||||
findAddLabelsComponent().vm.$emit('select', [
|
||||
'gid://gitlab/Label/1',
|
||||
'gid://gitlab/Label/2',
|
||||
'gid://gitlab/Label/3',
|
||||
]);
|
||||
findRemoveLabelsComponent().vm.$emit('select', [
|
||||
'gid://gitlab/Label/4',
|
||||
'gid://gitlab/Label/5',
|
||||
'gid://gitlab/Label/6',
|
||||
]);
|
||||
findHealthStatusComponent().vm.$emit('input', health_status);
|
||||
findSubscriptionComponent().vm.$emit('input', subscription_event);
|
||||
findConfidentialityComponent().vm.$emit('input', confidential);
|
||||
findForm().vm.$emit('submit', { preventDefault: () => {} });
|
||||
await waitForPromises();
|
||||
|
||||
expect(createAlert).toHaveBeenCalledWith({
|
||||
captureError: true,
|
||||
error: new Error('Request failed with status code 400'),
|
||||
message: 'Something went wrong while bulk editing.',
|
||||
expect(axiosMock.history.post[0].url).toBe('/group/project/-/issues/bulk_update');
|
||||
expect(axiosMock.history.post[0].data).toBe(
|
||||
JSON.stringify({
|
||||
update: {
|
||||
add_label_ids,
|
||||
assignee_ids,
|
||||
confidential,
|
||||
health_status,
|
||||
issuable_ids,
|
||||
remove_label_ids,
|
||||
state_event,
|
||||
subscription_event,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('renders error when there is a response error', async () => {
|
||||
axiosMock.onPost().replyOnce(HTTP_STATUS_BAD_REQUEST);
|
||||
createComponent({ props: { isEpicsList: false } });
|
||||
|
||||
findForm().vm.$emit('submit', { preventDefault: () => {} });
|
||||
await waitForPromises();
|
||||
|
||||
expect(createAlert).toHaveBeenCalledWith({
|
||||
captureError: true,
|
||||
error: new Error('Request failed with status code 400'),
|
||||
message: 'Something went wrong while bulk editing.',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -316,7 +316,7 @@ describe('Create work item page component', () => {
|
|||
expect(visitUrl).toHaveBeenCalledWith('/gitlab-org/gitlab-test/-/issues');
|
||||
});
|
||||
|
||||
it('confirmation modal closes when user clicks "Discard changes" and redirects to list page when on group `work_items/new` route', async () => {
|
||||
it('confirmation modal closes when user clicks "Discard changes" and redirects to issues list page when on group `work_items/new` route', async () => {
|
||||
const historyMock = {
|
||||
base: '/groups/gitlab-org/-',
|
||||
current: {
|
||||
|
|
@ -342,7 +342,7 @@ describe('Create work item page component', () => {
|
|||
await nextTick();
|
||||
|
||||
expect(findCancelConfirmationModal().props('isVisible')).toBe(false);
|
||||
expect(visitUrl).toHaveBeenCalledWith('/groups/gitlab-org/-/work_items');
|
||||
expect(visitUrl).toHaveBeenCalledWith('/groups/gitlab-org/-/issues');
|
||||
});
|
||||
|
||||
it('confirmation modal closes when user clicks "Discard changes" and redirects to list page when on group `epics/new` route', async () => {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import {
|
|||
getNewWorkItemWidgetsAutoSaveKey,
|
||||
getWorkItemWidgets,
|
||||
updateDraftWorkItemType,
|
||||
getDraftWorkItemType,
|
||||
} from '~/work_items/utils';
|
||||
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
|
||||
import { TYPE_EPIC } from '~/issues/constants';
|
||||
|
|
@ -264,6 +265,16 @@ describe('newWorkItemPath', () => {
|
|||
'/foobar/project/-/work_items/new?foo=bar',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns `work_items` path for group issues', () => {
|
||||
expect(
|
||||
newWorkItemPath({
|
||||
fullPath: 'my-group',
|
||||
isGroup: true,
|
||||
workItemType: WORK_ITEM_TYPE_NAME_ISSUE,
|
||||
}),
|
||||
).toBe('/foobar/groups/my-group/-/work_items/new');
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertTypeEnumToName', () => {
|
||||
|
|
@ -451,6 +462,16 @@ describe('getNewWorkItemAutoSaveKey', () => {
|
|||
expect(autosaveKey).toEqual(expectedAutosaveKey);
|
||||
},
|
||||
);
|
||||
|
||||
it('returns autosave key for new related item', () => {
|
||||
const autosaveKey = getNewWorkItemAutoSaveKey({
|
||||
fullPath: 'gitlab-org/gitlab',
|
||||
workItemType: 'issue',
|
||||
relatedItemId: 'gid://gitlab/WorkItem/22',
|
||||
});
|
||||
|
||||
expect(autosaveKey).toEqual('new-gitlab-org/gitlab-issue-related-22-draft');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNewWorkItemWidgetsAutoSaveKey', () => {
|
||||
|
|
@ -460,6 +481,15 @@ describe('getNewWorkItemWidgetsAutoSaveKey', () => {
|
|||
});
|
||||
expect(autosaveKey).toEqual('new-gitlab-org/gitlab-widgets-draft');
|
||||
});
|
||||
|
||||
it('returns autosave key for new related item', () => {
|
||||
const autosaveKey = getNewWorkItemWidgetsAutoSaveKey({
|
||||
fullPath: 'gitlab-org/gitlab',
|
||||
relatedItemId: 'gid://gitlab/WorkItem/22',
|
||||
});
|
||||
|
||||
expect(autosaveKey).toEqual('new-gitlab-org/gitlab-related-22-widgets-draft');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWorkItemWidgets', () => {
|
||||
|
|
@ -520,6 +550,53 @@ describe('updateDraftWorkItemType', () => {
|
|||
JSON.stringify({ TITLE: 'Some work item', TYPE: workItemType }),
|
||||
);
|
||||
});
|
||||
|
||||
it('updates `TYPE` with workItemType to localStorage widgets for related item drafts key when it already exists', () => {
|
||||
const workItemWidgetsKey = 'autosave/new-gitlab-org/gitlab-related-22-widgets-draft';
|
||||
localStorage.setItem(workItemWidgetsKey, JSON.stringify({ TITLE: 'Some work item' }));
|
||||
|
||||
updateDraftWorkItemType({
|
||||
fullPath: 'gitlab-org/gitlab',
|
||||
relatedItemId: 'gid://gitlab/WorkItem/22',
|
||||
workItemType,
|
||||
});
|
||||
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith(
|
||||
workItemWidgetsKey,
|
||||
JSON.stringify({ TITLE: 'Some work item', TYPE: workItemType }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDraftWorkItemType', () => {
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('gets `TYPE` from localStorage widgets draft when it exists', () => {
|
||||
localStorage.setItem(
|
||||
'autosave/new-gitlab-org/gitlab-widgets-draft',
|
||||
JSON.stringify({ TYPE: 'Issue' }),
|
||||
);
|
||||
const workItemType = getDraftWorkItemType({
|
||||
fullPath: 'gitlab-org/gitlab',
|
||||
});
|
||||
|
||||
expect(workItemType).toBe('Issue');
|
||||
});
|
||||
|
||||
it('gets `TYPE` from localStorage widgets for related item draft when it exists', () => {
|
||||
localStorage.setItem(
|
||||
'autosave/new-gitlab-org/gitlab-related-22-widgets-draft',
|
||||
JSON.stringify({ TYPE: 'Issue' }),
|
||||
);
|
||||
const workItemType = getDraftWorkItemType({
|
||||
fullPath: 'gitlab-org/gitlab',
|
||||
relatedItemId: 'gid://gitlab/WorkItem/22',
|
||||
});
|
||||
|
||||
expect(workItemType).toBe('Issue');
|
||||
});
|
||||
});
|
||||
|
||||
describe('`getItems`', () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Ci::Builds::TimeoutCalculator, feature_category: :continuous_integration do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let_it_be(:project) { FactoryBot.build(:project) }
|
||||
let_it_be(:runner) { FactoryBot.build(:ci_runner) }
|
||||
let_it_be(:build) { FactoryBot.build(:ci_build, project: project, runner: runner) }
|
||||
let(:calculator) { described_class.new(build) }
|
||||
|
||||
describe '#applicable_timeout' do
|
||||
where(:job_timeout, :project_timeout, :runner_timeout, :result_value, :result_source) do
|
||||
100 | 200 | 300 | 100 | :job_timeout_source
|
||||
100 | nil | 300 | 100 | :job_timeout_source
|
||||
100 | 50 | 300 | 100 | :job_timeout_source
|
||||
100 | 50 | nil | 100 | :job_timeout_source
|
||||
nil | 200 | 300 | 200 | :project_timeout_source
|
||||
nil | 200 | nil | 200 | :project_timeout_source
|
||||
100 | 200 | 50 | 50 | :runner_timeout_source
|
||||
nil | 200 | 100 | 100 | :runner_timeout_source
|
||||
nil | nil | 100 | 100 | :runner_timeout_source
|
||||
nil | nil | nil | nil | nil
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
allow(build).to receive(:options).and_return({ job_timeout: job_timeout })
|
||||
allow(project).to receive(:build_timeout).and_return(project_timeout)
|
||||
allow(runner).to receive(:maximum_timeout).and_return(runner_timeout)
|
||||
end
|
||||
|
||||
it 'calculates correct timeout' do
|
||||
result = calculator.applicable_timeout
|
||||
|
||||
if result_value.nil? && result_source.nil?
|
||||
expect(result).to be_nil
|
||||
else
|
||||
expect(result).to be_a(Ci::Builds::Timeout)
|
||||
expect(result.value).to eq(result_value)
|
||||
expect(result.source).to eq(described_class.timeout_sources.fetch(result_source))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::PDF::Security::GroupVulnerabilitiesProjectsGrades, feature_category: :vulnerability_management do
|
||||
let(:pdf) { Prawn::Document.new }
|
||||
let(:data) do
|
||||
{
|
||||
vulnerability_grades: [
|
||||
{
|
||||
grade: "F",
|
||||
count: 296,
|
||||
projects: {
|
||||
nodes: [
|
||||
{
|
||||
name: "Oxeye Rulez",
|
||||
nameWithNamespace: "Gitlab Org / Oxeye Rulez",
|
||||
securityDashboardPath: "/gitlab-org/oxeye-rulez/-/security/dashboard",
|
||||
vulnerabilitySeveritiesCount: severities_data(critical: 295, high: 1070)
|
||||
},
|
||||
{
|
||||
name: "Security Reports",
|
||||
nameWithNamespace: "Gitlab Org / Security Reports",
|
||||
securityDashboardPath: "/gitlab-org/security-reports/-/security/dashboard",
|
||||
vulnerabilitySeveritiesCount: severities_data(critical: 1)
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
grade: "D",
|
||||
count: 10,
|
||||
projects: {
|
||||
nodes: [
|
||||
{
|
||||
name: "Cwe 78 Cwe 89 Tests",
|
||||
nameWithNamespace: "Gitlab Org / Cwe 78 Cwe 89 Tests",
|
||||
securityDashboardPath: "/gitlab-org/cwe-78-cwe-89-tests/-/security/dashboard",
|
||||
vulnerabilitySeveritiesCount: severities_data(high: 10)
|
||||
},
|
||||
{ name: "Project 2", nameWithNamespace: "Gitlab Org / Project 2",
|
||||
vulnerabilitySeveritiesCount: severities_data },
|
||||
{ name: "Project 3", nameWithNamespace: "Gitlab Org / Project 3",
|
||||
vulnerabilitySeveritiesCount: severities_data },
|
||||
{ name: "Project 4", nameWithNamespace: "Gitlab Org / Project 4",
|
||||
vulnerabilitySeveritiesCount: severities_data },
|
||||
{ name: "Project 5", nameWithNamespace: "Gitlab Org / Project 5",
|
||||
vulnerabilitySeveritiesCount: severities_data },
|
||||
{ name: "Project 6", nameWithNamespace: "Gitlab Org / Project 6",
|
||||
vulnerabilitySeveritiesCount: severities_data }
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
expanded_grade: "F"
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
def severities_data(severities = {})
|
||||
{ critical: 0, high: 0, info: 0, low: 0, medium: 0, unknown: 0 }
|
||||
.merge(severities)
|
||||
end
|
||||
|
||||
describe '.render' do
|
||||
subject(:render) { described_class.render(pdf, data: data) }
|
||||
|
||||
let(:mock_instance) { instance_double(described_class) }
|
||||
|
||||
before do
|
||||
allow(mock_instance).to receive(:render)
|
||||
allow(described_class).to receive(:new).and_return(mock_instance)
|
||||
end
|
||||
|
||||
it 'creates a new instance and calls render on it' do
|
||||
render
|
||||
|
||||
expect(described_class).to have_received(:new).with(pdf, data).once
|
||||
expect(mock_instance).to have_received(:render).exactly(:once)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#render' do
|
||||
subject(:render_grades) { described_class.render(pdf, data: data) }
|
||||
|
||||
before do
|
||||
allow(pdf).to receive(:text_box).and_call_original
|
||||
allow(pdf).to receive(:svg).and_call_original
|
||||
end
|
||||
|
||||
let(:expected_labels) do
|
||||
[
|
||||
s_('Project security status'),
|
||||
s_('Projects are graded based on the highest severity vulnerability present')
|
||||
]
|
||||
end
|
||||
|
||||
it 'includes expected text elements' do
|
||||
render_grades
|
||||
|
||||
expected_labels.each do |label|
|
||||
expect(pdf).to have_received(:text_box).with(label, any_args).once
|
||||
end
|
||||
end
|
||||
|
||||
it 'renders the SVG table layout' do
|
||||
render_grades
|
||||
|
||||
expect(pdf).to have_received(:svg).with(%r{<svg.*</svg>}m, any_args).at_least(:once)
|
||||
end
|
||||
|
||||
context 'when data is nil' do
|
||||
let(:data) { nil }
|
||||
|
||||
it 'returns :noop without rendering anything' do
|
||||
expect(render_grades).to eq(:noop)
|
||||
expect(pdf).not_to have_received(:svg)
|
||||
expect(pdf).not_to have_received(:text_box)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when data is blank' do
|
||||
let(:data) { {} }
|
||||
|
||||
it 'returns :noop without rendering anything' do
|
||||
expect(render_grades).to eq(:noop)
|
||||
expect(pdf).not_to have_received(:svg)
|
||||
expect(pdf).not_to have_received(:text_box)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -29,120 +29,36 @@ RSpec.describe Ci::BuildMetadata, feature_category: :continuous_integration do
|
|||
describe '#update_timeout_state' do
|
||||
subject { metadata }
|
||||
|
||||
shared_examples 'sets timeout' do |source, timeout|
|
||||
it 'sets project_timeout_source' do
|
||||
expect { subject.update_timeout_state }.to change { subject.reload.timeout_source }.to(source)
|
||||
end
|
||||
let(:calculator) { instance_double(::Ci::Builds::TimeoutCalculator) }
|
||||
|
||||
it 'sets project timeout' do
|
||||
expect { subject.update_timeout_state }.to change { subject.reload.timeout }.to(timeout)
|
||||
end
|
||||
before do
|
||||
allow(::Ci::Builds::TimeoutCalculator).to receive(:new).with(job).and_return(calculator)
|
||||
end
|
||||
|
||||
context 'when job, project and runner timeouts are set' do
|
||||
context 'when job timeout is lower then runner timeout' do
|
||||
before do
|
||||
runner.update!(maximum_timeout: 4000)
|
||||
job.update!(options: { job_timeout: 3000 })
|
||||
end
|
||||
|
||||
it_behaves_like 'sets timeout', 'job_timeout_source', 3000
|
||||
end
|
||||
|
||||
context 'when runner timeout is lower then job timeout' do
|
||||
before do
|
||||
runner.update!(maximum_timeout: 2000)
|
||||
job.update!(options: { job_timeout: 3000 })
|
||||
end
|
||||
|
||||
it_behaves_like 'sets timeout', 'runner_timeout_source', 2000
|
||||
end
|
||||
end
|
||||
|
||||
context 'when job, project timeout values are set and runner is assigned' do
|
||||
context 'when runner has no timeout set' do
|
||||
before do
|
||||
runner.update!(maximum_timeout: nil)
|
||||
job.update!(options: { job_timeout: 3000 })
|
||||
end
|
||||
|
||||
it_behaves_like 'sets timeout', 'job_timeout_source', 3000
|
||||
end
|
||||
end
|
||||
|
||||
context 'when only job and project timeouts are defined' do
|
||||
context 'when job timeout is lower then project timeout' do
|
||||
before do
|
||||
job.update!(options: { job_timeout: 1000 })
|
||||
end
|
||||
|
||||
it_behaves_like 'sets timeout', 'job_timeout_source', 1000
|
||||
end
|
||||
|
||||
context 'when project timeout is lower then job timeout' do
|
||||
before do
|
||||
job.update!(options: { job_timeout: 3000 })
|
||||
end
|
||||
|
||||
it_behaves_like 'sets timeout', 'job_timeout_source', 3000
|
||||
end
|
||||
end
|
||||
|
||||
context 'when only project and runner timeouts are defined' do
|
||||
context 'when no timeouts defined anywhere' do
|
||||
before do
|
||||
runner.update!(maximum_timeout: 1900)
|
||||
allow(calculator).to receive(:applicable_timeout).and_return(nil)
|
||||
end
|
||||
|
||||
context 'when runner timeout is lower then project timeout' do
|
||||
it_behaves_like 'sets timeout', 'runner_timeout_source', 1900
|
||||
end
|
||||
|
||||
context 'when project timeout is lower then runner timeout' do
|
||||
before do
|
||||
runner.update!(maximum_timeout: 2100)
|
||||
end
|
||||
|
||||
it_behaves_like 'sets timeout', 'project_timeout_source', 2000
|
||||
it 'does not change anything' do
|
||||
expect { subject.update_timeout_state }
|
||||
.to not_change { subject.reload.timeout_source }
|
||||
.and not_change { subject.reload.timeout }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when only job and runner timeouts are defined' do
|
||||
context 'when runner timeout is lower them job timeout' do
|
||||
before do
|
||||
job.update!(options: { job_timeout: 2000 })
|
||||
runner.update!(maximum_timeout: 1900)
|
||||
end
|
||||
|
||||
it_behaves_like 'sets timeout', 'runner_timeout_source', 1900
|
||||
end
|
||||
|
||||
context 'when job timeout is lower them runner timeout' do
|
||||
before do
|
||||
job.update!(options: { job_timeout: 1000 })
|
||||
runner.update!(maximum_timeout: 1900)
|
||||
end
|
||||
|
||||
it_behaves_like 'sets timeout', 'job_timeout_source', 1000
|
||||
end
|
||||
end
|
||||
|
||||
context 'when only job timeout is defined and runner is assigned, but has no timeout set' do
|
||||
context 'when at least a timeout is defined' do
|
||||
before do
|
||||
job.update!(options: { job_timeout: 1000 })
|
||||
runner.update!(maximum_timeout: nil)
|
||||
allow(calculator)
|
||||
.to receive(:applicable_timeout)
|
||||
.and_return(
|
||||
::Ci::Builds::Timeout.new(25, ::Ci::BuildMetadata.timeout_sources.fetch(:job_timeout_source)))
|
||||
end
|
||||
|
||||
it_behaves_like 'sets timeout', 'job_timeout_source', 1000
|
||||
end
|
||||
|
||||
context 'when only one timeout value is defined' do
|
||||
context 'when only project timeout value is defined' do
|
||||
before do
|
||||
job.update!(options: { job_timeout: nil })
|
||||
runner.update!(maximum_timeout: nil)
|
||||
end
|
||||
|
||||
it_behaves_like 'sets timeout', 'project_timeout_source', 2000
|
||||
it 'sets the timeout' do
|
||||
expect { subject.update_timeout_state }
|
||||
.to change { subject.reload.timeout_source }.to('job_timeout_source')
|
||||
.and change { subject.reload.timeout }.to(25)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,10 +4,9 @@ RSpec.shared_examples 'code highlight' do
|
|||
include PreferencesHelper
|
||||
|
||||
let_it_be(:current_user) { user }
|
||||
let_it_be(:scheme_class) { user_color_scheme }
|
||||
|
||||
it 'has highlighted code', :js do
|
||||
wait_for_requests
|
||||
expect(subject).to have_selector(".js-syntax-highlight.#{scheme_class}")
|
||||
expect(subject).to have_selector(".js-syntax-highlight")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -19,13 +19,13 @@ RSpec.shared_examples 'rich text editor - code blocks' do
|
|||
end
|
||||
|
||||
it 'applies theme classes to code blocks' do
|
||||
expect(page).not_to have_css('.content-editor-code-block.code.highlight.dark')
|
||||
expect(page).not_to have_css('.content-editor-code-block.code.highlight.code-syntax-highlight-theme')
|
||||
|
||||
type_in_content_editor [:enter, :enter]
|
||||
type_in_content_editor '```js ' # trigger input rule
|
||||
type_in_content_editor 'var a = 0'
|
||||
|
||||
expect(page).to have_css('.content-editor-code-block.code.highlight.dark')
|
||||
expect(page).to have_css('.content-editor-code-block.code.highlight.code-syntax-highlight-theme')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue