Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1a129420d6
commit
b616fd825f
|
|
@ -1 +1 @@
|
|||
76dabc8174f7978025f48adcfab0a19c85416531
|
||||
1250b121b00ef5b3d637463cd4b9e5d93076f9b0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
import * as Sentry from '@sentry/browser';
|
||||
|
||||
async function eventHandler(callback = () => {}) {
|
||||
if (this.newHeaderSearchFeatureFlag) {
|
||||
const { initHeaderSearchApp } = await import(
|
||||
/* webpackChunkName: 'globalSearch' */ '~/header_search'
|
||||
).catch((error) => Sentry.captureException(error));
|
||||
|
||||
// In case the user started searching before we bootstrapped,
|
||||
// let's pass the search along.
|
||||
const initialSearchValue = this.searchInputBox.value;
|
||||
initHeaderSearchApp(initialSearchValue);
|
||||
|
||||
// this is new #search input element. We need to re-find it.
|
||||
// And re-focus in it.
|
||||
document.querySelector('#search').focus();
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const { default: initSearchAutocomplete } = await import(
|
||||
/* webpackChunkName: 'globalSearch' */ '../search_autocomplete'
|
||||
).catch((error) => Sentry.captureException(error));
|
||||
|
||||
const searchDropdown = initSearchAutocomplete();
|
||||
searchDropdown.onSearchInputFocus();
|
||||
callback();
|
||||
}
|
||||
|
||||
function cleanEventListeners() {
|
||||
document.querySelector('#search').removeEventListener('focus', eventHandler);
|
||||
}
|
||||
|
||||
function initHeaderSearch() {
|
||||
const searchInputBox = document.querySelector('#search');
|
||||
|
||||
searchInputBox?.addEventListener(
|
||||
'focus',
|
||||
eventHandler.bind(
|
||||
{ searchInputBox, newHeaderSearchFeatureFlag: gon?.features?.newHeaderSearch },
|
||||
cleanEventListeners,
|
||||
),
|
||||
{ once: true },
|
||||
);
|
||||
}
|
||||
|
||||
export default initHeaderSearch;
|
||||
export { eventHandler };
|
||||
|
|
@ -18,8 +18,9 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<span id="popovercontainer">
|
||||
<gl-icon id="issue-type-info" name="question-o" class="gl-ml-5 gl-text-gray-500" />
|
||||
<span id="popovercontainer" class="gl-ml-2">
|
||||
<gl-icon id="issue-type-info" name="question-o" class="gl-text-blue-600" />
|
||||
|
||||
<gl-popover
|
||||
target="issue-type-info"
|
||||
container="popovercontainer"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
<script>
|
||||
import { GlFormGroup, GlButton, GlFormInput } from '@gitlab/ui';
|
||||
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
|
||||
import { resourceLinksFormI18n } from '../constants';
|
||||
|
||||
export default {
|
||||
name: 'AddIssuableResourceLinkForm',
|
||||
components: {
|
||||
GlFormGroup,
|
||||
GlButton,
|
||||
GlFormInput,
|
||||
},
|
||||
i18n: resourceLinksFormI18n,
|
||||
directives: {
|
||||
autofocusonshow,
|
||||
},
|
||||
props: {
|
||||
isSubmitting: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
linkTextValue: '',
|
||||
linkValue: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isSubmitButtonDisabled() {
|
||||
return this.linkValue.length === 0 || this.isSubmitting;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onFormCancel() {
|
||||
this.linkValue = '';
|
||||
this.linkTextValue = '';
|
||||
this.$emit('add-issuable-resource-link-form-cancel');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent>
|
||||
<gl-form-group :label="$options.i18n.linkTextLabel">
|
||||
<gl-form-input
|
||||
v-model="linkTextValue"
|
||||
v-autofocusonshow
|
||||
data-testid="link-text-input"
|
||||
type="text"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-form-group :label="$options.i18n.linkValueLabel">
|
||||
<gl-form-input v-model="linkValue" data-testid="link-value-input" type="text" />
|
||||
</gl-form-group>
|
||||
<div class="gl-mt-5 gl-clearfix">
|
||||
<gl-button
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
data-testid="add-button"
|
||||
:disabled="isSubmitButtonDisabled"
|
||||
:loading="isSubmitting"
|
||||
type="submit"
|
||||
class="gl-float-left"
|
||||
>
|
||||
{{ $options.i18n.submitButtonText }}
|
||||
</gl-button>
|
||||
<gl-button class="gl-float-right" @click="onFormCancel">
|
||||
{{ $options.i18n.cancelButtonText }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
|
@ -1,10 +1,7 @@
|
|||
<script>
|
||||
import { GlLink, GlIcon, GlButton } from '@gitlab/ui';
|
||||
import {
|
||||
LINKED_RESOURCES_HEADER_TEXT,
|
||||
LINKED_RESOURCES_HELP_TEXT,
|
||||
LINKED_RESOURCES_ADD_BUTTON_TEXT,
|
||||
} from '../constants';
|
||||
import { resourceLinksI18n } from '../constants';
|
||||
import AddIssuableResourceLinkForm from './add_issuable_resource_link_form.vue';
|
||||
|
||||
export default {
|
||||
name: 'ResourceLinksBlock',
|
||||
|
|
@ -12,7 +9,9 @@ export default {
|
|||
GlLink,
|
||||
GlButton,
|
||||
GlIcon,
|
||||
AddIssuableResourceLinkForm,
|
||||
},
|
||||
i18n: resourceLinksI18n,
|
||||
props: {
|
||||
helpPath: {
|
||||
type: String,
|
||||
|
|
@ -25,18 +24,26 @@ export default {
|
|||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isFormVisible: false,
|
||||
isSubmitting: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
helpLinkText() {
|
||||
return LINKED_RESOURCES_HELP_TEXT;
|
||||
},
|
||||
badgeLabel() {
|
||||
return 0;
|
||||
},
|
||||
resourceLinkAddButtonText() {
|
||||
return LINKED_RESOURCES_ADD_BUTTON_TEXT;
|
||||
hasBody() {
|
||||
return this.isFormVisible;
|
||||
},
|
||||
resourceLinkHeaderText() {
|
||||
return LINKED_RESOURCES_HEADER_TEXT;
|
||||
},
|
||||
methods: {
|
||||
async toggleResourceLinkForm() {
|
||||
this.isFormVisible = !this.isFormVisible;
|
||||
},
|
||||
hideResourceLinkForm() {
|
||||
this.isFormVisible = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -46,7 +53,7 @@ export default {
|
|||
<div id="resource-links" class="gl-mt-5">
|
||||
<div class="card card-slim gl-overflow-hidden">
|
||||
<div
|
||||
:class="{ 'panel-empty-heading border-bottom-0': true }"
|
||||
:class="{ 'panel-empty-heading border-bottom-0': !hasBody }"
|
||||
class="card-header gl-display-flex gl-justify-content-space-between"
|
||||
>
|
||||
<h3
|
||||
|
|
@ -58,13 +65,13 @@ export default {
|
|||
href="#resource-links"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<slot name="header-text">{{ resourceLinkHeaderText }}</slot>
|
||||
<slot name="header-text">{{ $options.i18n.headerText }}</slot>
|
||||
<gl-link
|
||||
:href="helpPath"
|
||||
target="_blank"
|
||||
class="gl-display-flex gl-align-items-center gl-ml-2 gl-text-gray-500"
|
||||
data-testid="help-link"
|
||||
:aria-label="helpLinkText"
|
||||
:aria-label="$options.i18n.helpText"
|
||||
>
|
||||
<gl-icon name="question" :size="12" />
|
||||
</gl-link>
|
||||
|
|
@ -79,11 +86,26 @@ export default {
|
|||
<gl-button
|
||||
v-if="canAddResourceLinks"
|
||||
icon="plus"
|
||||
:aria-label="resourceLinkAddButtonText"
|
||||
:aria-label="$options.i18n.addButtonText"
|
||||
@click="toggleResourceLinkForm"
|
||||
/>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="linked-issues-card-body bg-gray-light"
|
||||
:class="{
|
||||
'gl-p-5': isFormVisible,
|
||||
}"
|
||||
>
|
||||
<div v-show="isFormVisible" class="card-body bordered-box gl-bg-white">
|
||||
<add-issuable-resource-link-form
|
||||
ref="resourceLinkForm"
|
||||
:is-submitting="isSubmitting"
|
||||
@add-issuable-resource-link-form-cancel="hideResourceLinkForm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
import { __ } from '~/locale';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
export const LINKED_RESOURCES_HEADER_TEXT = __('Linked resources');
|
||||
export const LINKED_RESOURCES_HELP_TEXT = __('Read more about linked resources');
|
||||
export const LINKED_RESOURCES_ADD_BUTTON_TEXT = __('Add a resource link');
|
||||
export const resourceLinksI18n = Object.freeze({
|
||||
headerText: s__('LinkedResources|Linked resources'),
|
||||
helpText: s__('LinkedResources|Read more about linked resources'),
|
||||
addButtonText: s__('LinkedResources|Add a resource link'),
|
||||
});
|
||||
|
||||
export const resourceLinksFormI18n = Object.freeze({
|
||||
linkTextLabel: s__('LinkedResources|Text (Optional)'),
|
||||
linkValueLabel: s__('LinkedResources|Link'),
|
||||
submitButtonText: s__('LinkedResources|Add'),
|
||||
cancelButtonText: s__('LinkedResources|Cancel'),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import initUserPopovers from './user_popovers';
|
|||
import initBroadcastNotifications from './broadcast_notification';
|
||||
import { initTopNav } from './nav';
|
||||
import { initCopyCodeButton } from './behaviors/copy_code';
|
||||
import initHeaderSearch from './header_search/init';
|
||||
|
||||
import 'ee_else_ce/main_ee';
|
||||
import 'jh_else_ce/main_jh';
|
||||
|
|
@ -141,35 +142,10 @@ function deferredInitialisation() {
|
|||
}
|
||||
}
|
||||
|
||||
// header search vue component bootstrap
|
||||
// loading this inside requestIdleCallback is causing issues
|
||||
// see https://gitlab.com/gitlab-org/gitlab/-/issues/365746
|
||||
const searchInputBox = document.querySelector('#search');
|
||||
if (searchInputBox) {
|
||||
searchInputBox.addEventListener(
|
||||
'focus',
|
||||
() => {
|
||||
if (gon.features?.newHeaderSearch) {
|
||||
import(/* webpackChunkName: 'globalSearch' */ '~/header_search')
|
||||
.then(async ({ initHeaderSearchApp }) => {
|
||||
// In case the user started searching before we bootstrapped, let's pass the search along.
|
||||
const initialSearchValue = searchInputBox.value;
|
||||
await initHeaderSearchApp(initialSearchValue);
|
||||
// this is new #search input element. We need to re-find it.
|
||||
document.querySelector('#search').focus();
|
||||
})
|
||||
.catch(() => {});
|
||||
} else {
|
||||
import(/* webpackChunkName: 'globalSearch' */ './search_autocomplete')
|
||||
.then(({ default: initSearchAutocomplete }) => {
|
||||
const searchDropdown = initSearchAutocomplete();
|
||||
searchDropdown.onSearchInputFocus();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
}
|
||||
initHeaderSearch();
|
||||
|
||||
const $body = $('body');
|
||||
const $document = $(document);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export default () => {
|
|||
// eslint-disable-next-line no-new
|
||||
new BlobLinePermalinkUpdater(
|
||||
document.querySelector('#blob-content-holder'),
|
||||
'.file-line-num[data-line-number], .file-line-num[data-line-number] *',
|
||||
'.diff-line-num[data-line-number], .diff-line-num[data-line-number] *',
|
||||
document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ query getBlobInfo(
|
|||
fileType
|
||||
language
|
||||
path
|
||||
blamePath
|
||||
editBlobPath
|
||||
gitpodBlobUrl
|
||||
ideEditPath
|
||||
|
|
|
|||
|
|
@ -51,10 +51,6 @@ export default {
|
|||
required: false,
|
||||
default: null,
|
||||
},
|
||||
blamePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
lines() {
|
||||
|
|
@ -80,7 +76,6 @@ export default {
|
|||
:number="startingFrom + index + 1"
|
||||
:content="line"
|
||||
:language="language"
|
||||
:blame-path="blamePath"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="gl-display-flex">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlLink, GlSafeHtmlDirective, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
|
||||
import { setAttributes } from '~/lib/utils/dom_utils';
|
||||
import { BIDI_CHARS, BIDI_CHARS_CLASS_LIST, BIDI_CHAR_TOOLTIP } from '../constants';
|
||||
|
||||
|
|
@ -9,7 +9,6 @@ export default {
|
|||
},
|
||||
directives: {
|
||||
SafeHtml: GlSafeHtmlDirective,
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
number: {
|
||||
|
|
@ -24,10 +23,6 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
blamePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
formattedContent() {
|
||||
|
|
@ -63,35 +58,21 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="gl-display-flex line-links-wrapper">
|
||||
<div
|
||||
class="gl-p-0! gl-absolute gl-z-index-3 diff-line-num gl-border-r gl-display-flex line-links line-numbers"
|
||||
:class="firstLineClass"
|
||||
>
|
||||
<gl-link
|
||||
v-gl-tooltip="__('View blame')"
|
||||
class="gl-user-select-none gl-ml-3 gl-shadow-none! file-line-blame"
|
||||
:href="`${blamePath}#L${number}`"
|
||||
data-track-action="click_link"
|
||||
data-track-label="file_line_action"
|
||||
data-track-property="blame"
|
||||
/>
|
||||
|
||||
<div class="gl-display-flex">
|
||||
<div class="gl-p-0! gl-absolute gl-z-index-3 gl-border-r diff-line-num line-numbers">
|
||||
<gl-link
|
||||
:id="`L${number}`"
|
||||
class="gl-user-select-none gl-flex-grow-1 gl-justify-content-end gl-pr-3 gl-shadow-none! file-line-num"
|
||||
class="gl-user-select-none gl-ml-5 gl-pr-3 gl-shadow-none! file-line-num diff-line-num"
|
||||
:class="firstLineClass"
|
||||
:to="`#L${number}`"
|
||||
:data-line-number="number"
|
||||
data-track-action="click_link"
|
||||
data-track-label="file_line_action"
|
||||
data-track-property="link"
|
||||
>
|
||||
{{ number }}
|
||||
</gl-link>
|
||||
</div>
|
||||
|
||||
<pre
|
||||
class="gl-p-0! gl-w-full gl-overflow-visible! gl-border-none! code highlight gl-line-height-normal"
|
||||
class="gl-p-0! gl-w-full gl-overflow-visible! gl-ml-11! gl-border-none! code highlight gl-line-height-normal"
|
||||
:class="firstLineClass"
|
||||
><code><span :id="`LC${number}`" v-safe-html="formattedContent" :lang="language" class="line" data-testid="content"></span></code></pre>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -199,7 +199,6 @@ export default {
|
|||
:starting-from="firstChunk.startingFrom"
|
||||
:is-highlighted="firstChunk.isHighlighted"
|
||||
:language="firstChunk.language"
|
||||
:blame-path="blob.blamePath"
|
||||
/>
|
||||
|
||||
<gl-loading-icon v-if="isLoading" size="sm" class="gl-my-5" />
|
||||
|
|
@ -214,7 +213,6 @@ export default {
|
|||
:is-highlighted="chunk.isHighlighted"
|
||||
:chunk-index="index"
|
||||
:language="chunk.language"
|
||||
:blame-path="blob.blamePath"
|
||||
@appear="highlightChunk"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { GlAlert, GlSkeletonLoader, GlIcon, GlButton } from '@gitlab/ui';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
|
||||
import {
|
||||
i18n,
|
||||
WIDGET_TYPE_ASSIGNEES,
|
||||
|
|
@ -8,6 +9,7 @@ import {
|
|||
WIDGET_TYPE_DESCRIPTION,
|
||||
WIDGET_TYPE_WEIGHT,
|
||||
WIDGET_TYPE_HIERARCHY,
|
||||
WORK_ITEM_VIEWED_STORAGE_KEY,
|
||||
} from '../constants';
|
||||
import workItemQuery from '../graphql/work_item.query.graphql';
|
||||
import workItemTitleSubscription from '../graphql/work_item_title.subscription.graphql';
|
||||
|
|
@ -18,6 +20,7 @@ import WorkItemDescription from './work_item_description.vue';
|
|||
import WorkItemAssignees from './work_item_assignees.vue';
|
||||
import WorkItemLabels from './work_item_labels.vue';
|
||||
import WorkItemWeight from './work_item_weight.vue';
|
||||
import WorkItemInformation from './work_item_information.vue';
|
||||
|
||||
export default {
|
||||
i18n,
|
||||
|
|
@ -33,6 +36,8 @@ export default {
|
|||
WorkItemTitle,
|
||||
WorkItemState,
|
||||
WorkItemWeight,
|
||||
WorkItemInformation,
|
||||
LocalStorageSync,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
props: {
|
||||
|
|
@ -56,6 +61,7 @@ export default {
|
|||
return {
|
||||
error: undefined,
|
||||
workItem: {},
|
||||
showInfoBanner: true,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
|
|
@ -120,6 +126,17 @@ export default {
|
|||
return `../../issues/${this.parentWorkItem?.iid}`;
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
/** make sure that if the user has not even dismissed the alert ,
|
||||
* should no be able to see the information next time and update the local storage * */
|
||||
this.dismissBanner();
|
||||
},
|
||||
methods: {
|
||||
dismissBanner() {
|
||||
this.showInfoBanner = false;
|
||||
},
|
||||
},
|
||||
WORK_ITEM_VIEWED_STORAGE_KEY,
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -174,6 +191,16 @@ export default {
|
|||
@click="$emit('close')"
|
||||
/>
|
||||
</div>
|
||||
<local-storage-sync
|
||||
v-model="showInfoBanner"
|
||||
:storage-key="$options.WORK_ITEM_VIEWED_STORAGE_KEY"
|
||||
>
|
||||
<work-item-information
|
||||
v-if="showInfoBanner"
|
||||
:show-info-banner="showInfoBanner"
|
||||
@work-item-banner-dismissed="dismissBanner"
|
||||
/>
|
||||
</local-storage-sync>
|
||||
<work-item-title
|
||||
:work-item-id="workItem.id"
|
||||
:work-item-title="workItem.title"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
<script>
|
||||
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
learnTasksButtonText: s__('WorkItem|Learn about tasks'),
|
||||
workItemsText: s__('WorkItem|work items'),
|
||||
tasksInformationTitle: s__('WorkItem|Introducing tasks'),
|
||||
tasksInformationBody: s__(
|
||||
'WorkItem|A task provides the ability to break down your work into smaller pieces tied to an issue. Tasks are the first items using our new %{workItemsLink} objects. Additional work item types will be coming soon.',
|
||||
),
|
||||
},
|
||||
helpPageLinks: {
|
||||
tasksDocLinkPath: helpPagePath('user/tasks'),
|
||||
workItemsLinkPath: helpPagePath(`development/work_items`),
|
||||
},
|
||||
components: {
|
||||
GlAlert,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
},
|
||||
props: {
|
||||
showInfoBanner: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ['work-item-banner-dismissed'],
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="gl-display-block gl-mb-2">
|
||||
<gl-alert
|
||||
v-if="showInfoBanner"
|
||||
variant="tip"
|
||||
:title="$options.i18n.tasksInformationTitle"
|
||||
:primary-button-link="$options.helpPageLinks.tasksDocLinkPath"
|
||||
:primary-button-text="$options.i18n.learnTasksButtonText"
|
||||
data-testid="work-item-information"
|
||||
class="gl-mt-3"
|
||||
@dismiss="$emit('work-item-banner-dismissed')"
|
||||
>
|
||||
<gl-sprintf :message="$options.i18n.tasksInformationBody">
|
||||
<template #workItemsLink>
|
||||
<gl-link :href="$options.helpPageLinks.workItemsLinkPath">{{
|
||||
$options.i18n.workItemsText
|
||||
}}</gl-link>
|
||||
</template>
|
||||
></gl-sprintf
|
||||
>
|
||||
</gl-alert>
|
||||
</section>
|
||||
</template>
|
||||
|
|
@ -20,6 +20,7 @@ export const WIDGET_TYPE_DESCRIPTION = 'DESCRIPTION';
|
|||
export const WIDGET_TYPE_LABELS = 'LABELS';
|
||||
export const WIDGET_TYPE_WEIGHT = 'WEIGHT';
|
||||
export const WIDGET_TYPE_HIERARCHY = 'HIERARCHY';
|
||||
export const WORK_ITEM_VIEWED_STORAGE_KEY = 'gl-show-work-item-banner';
|
||||
|
||||
export const WIDGET_TYPE_TASK_ICON = 'task-done';
|
||||
|
||||
|
|
|
|||
|
|
@ -202,10 +202,6 @@
|
|||
float: none;
|
||||
border-left: 1px solid $gray-100;
|
||||
|
||||
.file-line-num {
|
||||
@include gl-min-w-9;
|
||||
}
|
||||
|
||||
i {
|
||||
float: none;
|
||||
margin-right: 0;
|
||||
|
|
|
|||
|
|
@ -48,9 +48,8 @@
|
|||
|
||||
a {
|
||||
font-family: $monospace-font;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
@include gl-display-flex;
|
||||
@include gl-justify-content-end;
|
||||
|
||||
i,
|
||||
svg {
|
||||
|
|
@ -91,44 +90,3 @@ td.line-numbers {
|
|||
cursor: pointer;
|
||||
text-decoration: underline wavy $red-500;
|
||||
}
|
||||
|
||||
.blob-viewer {
|
||||
.line-numbers {
|
||||
// for server-side-rendering
|
||||
.line-links {
|
||||
min-width: 6.5rem;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// for client
|
||||
&.line-links {
|
||||
min-width: 6.5rem;
|
||||
border-bottom-left-radius: 0;
|
||||
|
||||
+ pre {
|
||||
margin-left: 6.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.line-links {
|
||||
&:hover .file-line-blame::before,
|
||||
&:hover .file-line-num::before,
|
||||
&:focus-within .file-line-blame::before,
|
||||
&:focus-within .file-line-num::before {
|
||||
@include gl-visibility-visible;
|
||||
}
|
||||
}
|
||||
|
||||
.file-line-num,
|
||||
.file-line-blame {
|
||||
@include gl-align-items-center;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,50 +98,32 @@
|
|||
}
|
||||
}
|
||||
|
||||
@mixin line-number-link($color) {
|
||||
min-width: $gl-spacing-scale-9;
|
||||
|
||||
@mixin line-link($color, $icon) {
|
||||
&::before {
|
||||
@include gl-visibility-hidden;
|
||||
@include gl-display-none;
|
||||
@include gl-align-self-center;
|
||||
@include gl-mr-1;
|
||||
@include gl-w-5;
|
||||
@include gl-h-5;
|
||||
background-color: rgba($color, 0.3);
|
||||
mask-image: asset_url('icons-stacked.svg##{$icon}');
|
||||
@include gl-mt-2;
|
||||
@include gl-mr-2;
|
||||
@include gl-w-4;
|
||||
@include gl-h-4;
|
||||
@include gl-absolute;
|
||||
@include gl-left-3;
|
||||
background-color: $color;
|
||||
mask-image: asset_url('icons-stacked.svg#link');
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: cover;
|
||||
mask-position: center;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::before {
|
||||
background-color: rgba($color, 0.6);
|
||||
}
|
||||
&:hover::before {
|
||||
@include gl-display-inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin line-hover-bg($color: $white-normal) {
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
background-color: darken($color, 10);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin first-line-top-space($bg-color: $gray-light, $border-color: $white-normal) {
|
||||
&:first-child {
|
||||
.line-links {
|
||||
&::before {
|
||||
@include gl-absolute;
|
||||
@include gl-h-3;
|
||||
content: '';
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
width: 6.5rem;
|
||||
background-color: $bg-color;
|
||||
border-right: 1px solid $border-color;
|
||||
}
|
||||
}
|
||||
&:focus::before {
|
||||
@include gl-display-inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -127,19 +127,7 @@ $dark-il: #de935f;
|
|||
.code.dark {
|
||||
// Line numbers
|
||||
.file-line-num {
|
||||
@include line-link($white, 'link');
|
||||
}
|
||||
|
||||
.file-line-blame {
|
||||
@include line-link($white, 'git');
|
||||
}
|
||||
|
||||
.line-links {
|
||||
@include line-hover-bg($dark-main-bg);
|
||||
}
|
||||
|
||||
.line-links-wrapper {
|
||||
@include first-line-top-space($dark-main-bg, $dark-code-border);
|
||||
@include line-number-link($dark-line-num-color);
|
||||
}
|
||||
|
||||
.line-numbers,
|
||||
|
|
|
|||
|
|
@ -120,19 +120,7 @@ $monokai-gh: #75715e;
|
|||
|
||||
// Line numbers
|
||||
.file-line-num {
|
||||
@include line-link($white, 'link');
|
||||
}
|
||||
|
||||
.file-line-blame {
|
||||
@include line-link($white, 'git');
|
||||
}
|
||||
|
||||
.line-links {
|
||||
@include line-hover-bg($monokai-bg);
|
||||
}
|
||||
|
||||
.line-links-wrapper {
|
||||
@include first-line-top-space($monokai-bg, $monokai-border);
|
||||
@include line-number-link($monokai-line-num-color);
|
||||
}
|
||||
|
||||
.line-numbers,
|
||||
|
|
|
|||
|
|
@ -25,19 +25,7 @@
|
|||
|
||||
// Line numbers
|
||||
.file-line-num {
|
||||
@include line-link($black, 'link');
|
||||
}
|
||||
|
||||
.file-line-blame {
|
||||
@include line-link($black, 'git');
|
||||
}
|
||||
|
||||
.line-links {
|
||||
@include line-hover-bg;
|
||||
}
|
||||
|
||||
.line-links-wrapper {
|
||||
@include first-line-top-space;
|
||||
@include line-number-link($black-transparent);
|
||||
}
|
||||
|
||||
.line-numbers,
|
||||
|
|
|
|||
|
|
@ -123,19 +123,7 @@ $solarized-dark-il: #2aa198;
|
|||
|
||||
// Line numbers
|
||||
.file-line-num {
|
||||
@include line-link($white, 'link');
|
||||
}
|
||||
|
||||
.file-line-blame {
|
||||
@include line-link($white, 'git');
|
||||
}
|
||||
|
||||
.line-links {
|
||||
@include line-hover-bg($solarized-dark-pre-bg);
|
||||
}
|
||||
|
||||
.line-links-wrapper {
|
||||
@include first-line-top-space($solarized-dark-pre-bg, $solarized-dark-pre-border);
|
||||
@include line-number-link($solarized-dark-line-color);
|
||||
}
|
||||
|
||||
.line-numbers,
|
||||
|
|
|
|||
|
|
@ -109,19 +109,7 @@ $solarized-light-il: #2aa198;
|
|||
@include hljs-override('title.class_.inherited__', $solarized-light-no);
|
||||
// Line numbers
|
||||
.file-line-num {
|
||||
@include line-link($black, 'link');
|
||||
}
|
||||
|
||||
.file-line-blame {
|
||||
@include line-link($black, 'git');
|
||||
}
|
||||
|
||||
.line-links {
|
||||
@include line-hover-bg($solarized-light-pre-bg);
|
||||
}
|
||||
|
||||
.line-links-wrapper {
|
||||
@include first-line-top-space($solarized-light-pre-bg, $solarized-light-border);
|
||||
@include line-number-link($solarized-light-line-color);
|
||||
}
|
||||
|
||||
.line-numbers,
|
||||
|
|
|
|||
|
|
@ -95,15 +95,7 @@ $white-gc-bg: #eaf2f5;
|
|||
|
||||
// Line numbers
|
||||
.file-line-num {
|
||||
@include line-link($black, 'link');
|
||||
}
|
||||
|
||||
.file-line-blame {
|
||||
@include line-link($black, 'git');
|
||||
}
|
||||
|
||||
.line-links {
|
||||
@include line-hover-bg;
|
||||
@include line-number-link($black-transparent);
|
||||
}
|
||||
|
||||
.line-numbers,
|
||||
|
|
@ -134,10 +126,6 @@ pre.code,
|
|||
border-color: $white-normal;
|
||||
}
|
||||
|
||||
.line-links-wrapper {
|
||||
@include first-line-top-space;
|
||||
}
|
||||
|
||||
&,
|
||||
pre.code,
|
||||
.line_holder .line_content {
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ module Types
|
|||
description: 'Downstream pipeline for a bridge.'
|
||||
field :manual_job, GraphQL::Types::Boolean, null: true,
|
||||
description: 'Whether the job has a manual action.'
|
||||
field :manual_variables, VariableType.connection_type, null: true,
|
||||
description: 'Variables added to a manual job when the job is triggered.'
|
||||
field :playable, GraphQL::Types::Boolean, null: false, method: :playable?,
|
||||
description: 'Indicates the job can be played.'
|
||||
field :previous_stage_jobs_or_needs, Types::Ci::JobNeedUnion.connection_type, null: true,
|
||||
|
|
@ -190,6 +192,14 @@ module Types
|
|||
def triggered
|
||||
object.try(:trigger_request)
|
||||
end
|
||||
|
||||
def manual_variables
|
||||
if object.manual? && object.respond_to?(:job_variables)
|
||||
object.job_variables
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -26,6 +26,15 @@ module Types
|
|||
|
||||
field :raw, GraphQL::Types::Boolean, null: true,
|
||||
description: 'Indicates whether the variable is raw.'
|
||||
|
||||
field :environment_scope, GraphQL::Types::String, null: true,
|
||||
description: 'Scope defining the environments in which the variable can be used.'
|
||||
|
||||
def environment_scope
|
||||
if object.respond_to?(:environment_scope)
|
||||
object.environment_scope
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -338,8 +338,8 @@ module Ci
|
|||
scope :for_id, -> (id) { where(id: id) }
|
||||
scope :for_iid, -> (iid) { where(iid: iid) }
|
||||
scope :for_project, -> (project_id) { where(project_id: project_id) }
|
||||
scope :created_after, -> (time) { where('ci_pipelines.created_at > ?', time) }
|
||||
scope :created_before_id, -> (id) { where('ci_pipelines.id < ?', id) }
|
||||
scope :created_after, -> (time) { where(arel_table[:created_at].gt(time)) }
|
||||
scope :created_before_id, -> (id) { where(arel_table[:id].lt(id)) }
|
||||
scope :before_pipeline, -> (pipeline) { created_before_id(pipeline.id).outside_pipeline_family(pipeline) }
|
||||
scope :with_pipeline_source, -> (source) { where(source: source) }
|
||||
|
||||
|
|
|
|||
|
|
@ -13,4 +13,8 @@ class WorkItemPolicy < IssuePolicy
|
|||
# need to make sure we also prevent this rule if read_issue
|
||||
# is prevented
|
||||
rule { ~can?(:read_issue) }.prevent :read_work_item
|
||||
|
||||
rule { can?(:reporter_access) }.policy do
|
||||
enable :admin_parent_link
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ module WorkItems
|
|||
|
||||
replacement_result = TaskListReferenceReplacementService.new(
|
||||
work_item: @work_item,
|
||||
current_user: @current_user,
|
||||
work_item_reference: create_and_link_result[:work_item].to_reference,
|
||||
line_number_start: @work_item_params[:line_number_start],
|
||||
line_number_end: @work_item_params[:line_number_end],
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ module WorkItems
|
|||
|
||||
def linkable_issuables(work_items)
|
||||
@linkable_issuables ||= begin
|
||||
return [] unless can?(current_user, :read_work_item, issuable.project)
|
||||
return [] unless can?(current_user, :admin_parent_link, issuable)
|
||||
|
||||
work_items.select do |work_item|
|
||||
linkable?(work_item)
|
||||
|
|
@ -29,7 +29,7 @@ module WorkItems
|
|||
end
|
||||
|
||||
def linkable?(work_item)
|
||||
can?(current_user, :update_work_item, work_item) &&
|
||||
can?(current_user, :admin_parent_link, work_item) &&
|
||||
!previous_related_issuables.include?(work_item)
|
||||
end
|
||||
|
||||
|
|
@ -42,8 +42,8 @@ module WorkItems
|
|||
::WorkItem.find(id)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
@errors << _("Task with ID: %{id} could not be found.") % { id: id }
|
||||
nil
|
||||
end
|
||||
next
|
||||
end.compact
|
||||
end
|
||||
|
||||
# TODO: Create system notes when work item's parent or children are updated
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ module WorkItems
|
|||
end
|
||||
|
||||
def permission_to_remove_relation?
|
||||
can?(current_user, :update_work_item, child) && can?(current_user, :update_work_item, parent)
|
||||
can?(current_user, :admin_parent_link, child) && can?(current_user, :admin_parent_link, parent)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ module WorkItems
|
|||
@line_number_end = line_number_end
|
||||
@lock_version = lock_version
|
||||
@current_user = current_user
|
||||
@task_reference = /#{Regexp.escape(@task.to_reference)}(?!\d)\+/
|
||||
end
|
||||
|
||||
def execute
|
||||
|
|
@ -26,7 +27,9 @@ module WorkItems
|
|||
line_matches_reference = (@line_number_start..@line_number_end).any? do |line_number|
|
||||
markdown_line = source_lines[line_number - 1]
|
||||
|
||||
/#{Regexp.escape(@task.to_reference)}(?!\d)/.match?(markdown_line)
|
||||
if @task_reference.match?(markdown_line)
|
||||
markdown_line.sub!(@task_reference, @task.title)
|
||||
end
|
||||
end
|
||||
|
||||
unless line_matches_reference
|
||||
|
|
@ -35,8 +38,6 @@ module WorkItems
|
|||
)
|
||||
end
|
||||
|
||||
remove_task_lines!(source_lines)
|
||||
|
||||
::WorkItems::UpdateService.new(
|
||||
project: @work_item.project,
|
||||
current_user: @current_user,
|
||||
|
|
@ -51,13 +52,5 @@ module WorkItems
|
|||
rescue ActiveRecord::StaleObjectError
|
||||
::ServiceResponse.error(message: STALE_OBJECT_MESSAGE)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def remove_task_lines!(source_lines)
|
||||
source_lines.delete_if.each_with_index do |_line, index|
|
||||
index >= @line_number_start - 1 && index < @line_number_end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ module WorkItems
|
|||
class TaskListReferenceReplacementService
|
||||
STALE_OBJECT_MESSAGE = 'Stale work item. Check lock version'
|
||||
|
||||
def initialize(work_item:, work_item_reference:, line_number_start:, line_number_end:, title:, lock_version:)
|
||||
def initialize(work_item:, current_user:, work_item_reference:, line_number_start:, line_number_end:, title:, lock_version:)
|
||||
@work_item = work_item
|
||||
@current_user = current_user
|
||||
@work_item_reference = work_item_reference
|
||||
@line_number_start = line_number_start
|
||||
@line_number_end = line_number_end
|
||||
|
|
@ -32,7 +33,11 @@ module WorkItems
|
|||
source_lines[@line_number_start - 1] = markdown_task_first_line
|
||||
remove_additional_lines!(source_lines)
|
||||
|
||||
@work_item.update!(description: source_lines.join("\n"))
|
||||
::WorkItems::UpdateService.new(
|
||||
project: @work_item.project,
|
||||
current_user: @current_user,
|
||||
params: { description: source_lines.join("\n"), lock_version: @lock_version }
|
||||
).execute(@work_item)
|
||||
|
||||
::ServiceResponse.success
|
||||
rescue ActiveRecord::StaleObjectError
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
.card-header
|
||||
= _('Protect a tag')
|
||||
.card-body
|
||||
= form_errors(@protected_tag)
|
||||
= form_errors(@protected_tag, pajamas_alert: true)
|
||||
.form-group.row
|
||||
= f.label :name, _('Tag:'), class: 'col-md-2 text-left text-md-right'
|
||||
.col-md-10.protected-tags-dropdown
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
.row
|
||||
.col-lg-12
|
||||
= gitlab_ui_form_for @project, url: project_settings_ci_cd_path(@project, anchor: 'autodevops-settings') do |f|
|
||||
= form_errors(@project)
|
||||
= form_errors(@project, pajamas_alert: true)
|
||||
%fieldset.builds-feature.js-auto-devops-settings
|
||||
.form-group
|
||||
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
|
||||
|
|
|
|||
|
|
@ -1,16 +1,13 @@
|
|||
#blob-content.file-content.code.js-syntax-highlight
|
||||
- offset = defined?(first_line_number) ? first_line_number : 1
|
||||
.line-numbers{ class: "gl-p-0\!" }
|
||||
.line-numbers
|
||||
- if blob.data.present?
|
||||
- link = blob_link if defined?(blob_link)
|
||||
- blame_link = project_blame_path(@project, tree_join(@ref, blob.path))
|
||||
- blob.data.each_line.each_with_index do |_, index|
|
||||
- i = index + offset
|
||||
-# We're not using `link_to` because it is too slow once we get to thousands of lines.
|
||||
.gl-display-flex.line-links.diff-line-num
|
||||
%a.file-line-blame.gl-display-flex.has-tooltip.gl-ml-3{ href: "#{blame_link}#L#{i}", title: _('View blame'), data: { track_action: "click_link", track_label: "file_line_action", track_property: "blame" } }
|
||||
%a.file-line-num.gl-display-flex.gl-justify-content-end.flex-grow-1.gl-pr-3{ href: "#{link}#L#{i}", id: "L#{i}", 'data-line-number' => i, data: { track_action: "click_link", track_label: "file_line_action", track_property: "link" } }
|
||||
= i
|
||||
%a.file-line-num.diff-line-num{ href: "#{link}#L#{i}", id: "L#{i}", 'data-line-number' => i }
|
||||
= i
|
||||
- highlight = defined?(highlight_line) && highlight_line ? highlight_line - offset : nil
|
||||
.blob-content{ data: { blob_id: blob.id, path: blob.path, highlight_line: highlight, qa_selector: 'file_content' } }
|
||||
%pre.code.highlight
|
||||
|
|
|
|||
|
|
@ -1,35 +1,35 @@
|
|||
- return unless issuable.supports_issue_type? && can?(current_user, :create_issue, @project)
|
||||
|
||||
.form-group
|
||||
= form.label :type, _('Type')
|
||||
.gl-display-flex.gl-align-items-center
|
||||
.issuable-form-select-holder.selectbox.form-group.gl-mb-0
|
||||
.dropdown.js-issuable-type-filter-dropdown-wrap
|
||||
%button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
|
||||
%span.dropdown-toggle-text.is-default
|
||||
= issuable.issue_type.capitalize || _("Select type")
|
||||
= sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3")
|
||||
.dropdown-menu.dropdown-menu-selectable.dropdown-select
|
||||
.dropdown-title.gl-display-flex
|
||||
%span.gl-ml-auto
|
||||
= _("Select type")
|
||||
%button.dropdown-title-button.dropdown-menu-close.gl-ml-auto{ type: 'button', "aria-label" => _('Close') }
|
||||
= sprite_icon('close', size: 16, css_class: 'dropdown-menu-close-icon')
|
||||
.dropdown-content{ data: { testid: 'issue-type-select-dropdown' } }
|
||||
%ul
|
||||
- if create_issue_type_allowed?(@project, :issue)
|
||||
%li.js-filter-issuable-type
|
||||
= link_to new_project_issue_path(@project), class: ("is-active" if issuable.issue?) do
|
||||
#{sprite_icon(work_item_type_icon(:issue), css_class: 'gl-icon')} #{_('Issue')}
|
||||
- if create_issue_type_allowed?(@project, :incident)
|
||||
%li.js-filter-issuable-type{ data: { track: { action: "select_issue_type_incident", label: "select_issue_type_incident_dropdown_option" } } }
|
||||
= link_to new_project_issue_path(@project, { issuable_template: 'incident', issue: { issue_type: 'incident' } }), class: ("is-active" if issuable.incident?) do
|
||||
#{sprite_icon(work_item_type_icon(:incident), css_class: 'gl-icon')} #{_('Incident')}
|
||||
|
||||
= form.label :type do
|
||||
= _('Type')
|
||||
#js-type-popover
|
||||
|
||||
- if issuable.incident?
|
||||
%p.form-text.text-muted
|
||||
- incident_docs_url = help_page_path('operations/incident_management/incidents.md')
|
||||
- incident_docs_start = format('<a href="%{url}" target="_blank" rel="noopener noreferrer">', url: incident_docs_url)
|
||||
= format(_('A %{incident_docs_start}modified issue%{incident_docs_end} to guide the resolution of incidents.'), incident_docs_start: incident_docs_start, incident_docs_end: '</a>').html_safe
|
||||
.issuable-form-select-holder.selectbox.form-group.gl-mb-0.gl-display-block
|
||||
.dropdown.js-issuable-type-filter-dropdown-wrap
|
||||
%button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
|
||||
%span.dropdown-toggle-text.is-default
|
||||
= issuable.issue_type.capitalize || _("Select type")
|
||||
= sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3")
|
||||
.dropdown-menu.dropdown-menu-selectable.dropdown-select
|
||||
.dropdown-title.gl-display-flex
|
||||
%span.gl-ml-auto
|
||||
= _("Select type")
|
||||
%button.dropdown-title-button.dropdown-menu-close.gl-ml-auto{ type: 'button', "aria-label" => _('Close') }
|
||||
= sprite_icon('close', size: 16, css_class: 'dropdown-menu-close-icon')
|
||||
.dropdown-content{ data: { testid: 'issue-type-select-dropdown' } }
|
||||
%ul
|
||||
- if create_issue_type_allowed?(@project, :issue)
|
||||
%li.js-filter-issuable-type
|
||||
= link_to new_project_issue_path(@project), class: ("is-active" if issuable.issue?) do
|
||||
#{sprite_icon(work_item_type_icon(:issue), css_class: 'gl-icon')} #{_('Issue')}
|
||||
- if create_issue_type_allowed?(@project, :incident)
|
||||
%li.js-filter-issuable-type{ data: { track: { action: "select_issue_type_incident", label: "select_issue_type_incident_dropdown_option" } } }
|
||||
= link_to new_project_issue_path(@project, { issuable_template: 'incident', issue: { issue_type: 'incident' } }), class: ("is-active" if issuable.incident?) do
|
||||
#{sprite_icon(work_item_type_icon(:incident), css_class: 'gl-icon')} #{_('Incident')}
|
||||
|
||||
- if issuable.incident?
|
||||
%p.form-text.text-muted
|
||||
- incident_docs_url = help_page_path('operations/incident_management/incidents.md')
|
||||
- incident_docs_start = format('<a href="%{url}" target="_blank" rel="noopener noreferrer">', url: incident_docs_url)
|
||||
= format(_('A %{incident_docs_start}modified issue%{incident_docs_end} to guide the resolution of incidents.'), incident_docs_start: incident_docs_start, incident_docs_end: '</a>').html_safe
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: ci_increase_includes_to_250
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64934
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/344449
|
||||
milestone: '15.2'
|
||||
type: development
|
||||
group: group::pipeline authoring
|
||||
default_enabled: false
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UpdateDefaultProjectImportLevelOnNamespaceSettings < Gitlab::Database::Migration[2.0]
|
||||
enable_lock_retries!
|
||||
|
||||
def up
|
||||
change_column :namespace_settings, :project_import_level, :smallint, default: 50, null: false
|
||||
end
|
||||
|
||||
def down
|
||||
change_column :namespace_settings, :project_import_level, :smallint, default: 0, null: false
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
c452f7dc9a76b6daa7ced88f2ed93332a84bfcb94a7e94f31149e43b888e210f
|
||||
|
|
@ -17549,7 +17549,7 @@ CREATE TABLE namespace_settings (
|
|||
enabled_git_access_protocol smallint DEFAULT 0 NOT NULL,
|
||||
unique_project_download_limit smallint DEFAULT 0 NOT NULL,
|
||||
unique_project_download_limit_interval_in_seconds integer DEFAULT 0 NOT NULL,
|
||||
project_import_level smallint DEFAULT 0 NOT NULL,
|
||||
project_import_level smallint DEFAULT 50 NOT NULL,
|
||||
include_for_free_user_cap_preview boolean DEFAULT false NOT NULL,
|
||||
CONSTRAINT check_0ba93c78c7 CHECK ((char_length(default_branch_name) <= 255))
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Integrate LDAP with GitLab **(FREE SELF)**
|
||||
|
||||
GitLab integrates with [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol)
|
||||
GitLab integrates with [LDAP - Lightweight Directory Access Protocol](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol)
|
||||
to support user authentication.
|
||||
|
||||
This integration works with most LDAP-compliant directory servers, including:
|
||||
|
|
|
|||
|
|
@ -1435,6 +1435,7 @@ Input type: `CreateEpicInput`
|
|||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationcreateepicaddlabelids"></a>`addLabelIds` | [`[ID!]`](#id) | IDs of labels to be added to the epic. |
|
||||
| <a id="mutationcreateepicaddlabels"></a>`addLabels` | [`[String!]`](#string) | Array of labels to be added to the epic. |
|
||||
| <a id="mutationcreateepicclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationcreateepiccolor"></a>`color` | [`Color`](#color) | Color of the epic. Available only when feature flag `epic_color_highlight` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. |
|
||||
| <a id="mutationcreateepicconfidential"></a>`confidential` | [`Boolean`](#boolean) | Indicates if the epic is confidential. |
|
||||
|
|
@ -5117,6 +5118,7 @@ Input type: `UpdateEpicInput`
|
|||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationupdateepicaddlabelids"></a>`addLabelIds` | [`[ID!]`](#id) | IDs of labels to be added to the epic. |
|
||||
| <a id="mutationupdateepicaddlabels"></a>`addLabels` | [`[String!]`](#string) | Array of labels to be added to the epic. |
|
||||
| <a id="mutationupdateepicclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationupdateepiccolor"></a>`color` | [`Color`](#color) | Color of the epic. Available only when feature flag `epic_color_highlight` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. |
|
||||
| <a id="mutationupdateepicconfidential"></a>`confidential` | [`Boolean`](#boolean) | Indicates if the epic is confidential. |
|
||||
|
|
@ -5126,6 +5128,7 @@ Input type: `UpdateEpicInput`
|
|||
| <a id="mutationupdateepicgrouppath"></a>`groupPath` | [`ID!`](#id) | Group the epic to mutate is in. |
|
||||
| <a id="mutationupdateepiciid"></a>`iid` | [`ID!`](#id) | IID of the epic to mutate. |
|
||||
| <a id="mutationupdateepicremovelabelids"></a>`removeLabelIds` | [`[ID!]`](#id) | IDs of labels to be removed from the epic. |
|
||||
| <a id="mutationupdateepicremovelabels"></a>`removeLabels` | [`[String!]`](#string) | Array of labels to be removed from the epic. |
|
||||
| <a id="mutationupdateepicstartdatefixed"></a>`startDateFixed` | [`String`](#string) | Start date of the epic. |
|
||||
| <a id="mutationupdateepicstartdateisfixed"></a>`startDateIsFixed` | [`Boolean`](#boolean) | Indicates start date should be sourced from start_date_fixed field not the issue milestones. |
|
||||
| <a id="mutationupdateepicstateevent"></a>`stateEvent` | [`EpicStateEvent`](#epicstateevent) | State event for the epic. |
|
||||
|
|
@ -9934,6 +9937,7 @@ Represents the total number of issues and their weights for a particular day.
|
|||
| <a id="cijobid"></a>`id` | [`JobID`](#jobid) | ID of the job. |
|
||||
| <a id="cijobkind"></a>`kind` | [`CiJobKind!`](#cijobkind) | Indicates the type of job. |
|
||||
| <a id="cijobmanualjob"></a>`manualJob` | [`Boolean`](#boolean) | Whether the job has a manual action. |
|
||||
| <a id="cijobmanualvariables"></a>`manualVariables` | [`CiVariableConnection`](#civariableconnection) | Variables added to a manual job when the job is triggered. (see [Connections](#connections)) |
|
||||
| <a id="cijobname"></a>`name` | [`String`](#string) | Name of the job. |
|
||||
| <a id="cijobneeds"></a>`needs` | [`CiBuildNeedConnection`](#cibuildneedconnection) | References to builds that must complete before the jobs run. (see [Connections](#connections)) |
|
||||
| <a id="cijobpipeline"></a>`pipeline` | [`Pipeline`](#pipeline) | Pipeline the job belongs to. |
|
||||
|
|
@ -10096,6 +10100,7 @@ GitLab CI/CD configuration template.
|
|||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="civariableenvironmentscope"></a>`environmentScope` | [`String`](#string) | Scope defining the environments in which the variable can be used. |
|
||||
| <a id="civariableid"></a>`id` | [`ID!`](#id) | ID of the variable. |
|
||||
| <a id="civariablekey"></a>`key` | [`String`](#string) | Name of the variable. |
|
||||
| <a id="civariablemasked"></a>`masked` | [`Boolean`](#boolean) | Indicates whether the variable is masked. |
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# SaaS runners on macOS (Beta) **(PREMIUM SAAS)**
|
||||
|
||||
SaaS runners on macOS are in [Beta]](../../../policy/alpha-beta-support.md#beta-features) for approved open source programs and customers in Premium and Ultimate plans.
|
||||
SaaS runners on macOS are in [Beta](../../../policy/alpha-beta-support.md#beta-features) for approved open source programs and customers in Premium and Ultimate plans.
|
||||
|
||||
SaaS runners on macOS provide an on-demand macOS build environment integrated with
|
||||
GitLab SaaS [CI/CD](../../../ci/index.md).
|
||||
|
|
|
|||
|
|
@ -361,6 +361,7 @@ To configure Vale in your editor, install one of the following as appropriate:
|
|||
- Sublime Text [`SublimeLinter-contrib-vale` package](https://packagecontrol.io/packages/SublimeLinter-contrib-vale).
|
||||
- Visual Studio Code [`errata-ai.vale-server` extension](https://marketplace.visualstudio.com/items?itemName=errata-ai.vale-server).
|
||||
You can configure the plugin to [display only a subset of alerts](#show-subset-of-vale-alerts).
|
||||
- Atom [`atomic-vale` package](https://atom.io/packages/atomic-vale).
|
||||
- Vim [ALE plugin](https://github.com/dense-analysis/ale).
|
||||
- JetBrains IDEs - No plugin exists, but
|
||||
[this issue comment](https://github.com/errata-ai/vale-server/issues/39#issuecomment-751714451)
|
||||
|
|
|
|||
|
|
@ -627,7 +627,7 @@ The following variables are used for configuring specific analyzers (used for a
|
|||
| `PIP_REQUIREMENTS_FILE` | `gemnasium-python` | | Pip requirements file to be scanned. |
|
||||
| `DS_PIP_VERSION` | `gemnasium-python` | | Force the install of a specific pip version (example: `"19.3"`), otherwise the pip installed in the Docker image is used. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12811) in GitLab 12.7) |
|
||||
| `DS_PIP_DEPENDENCY_PATH` | `gemnasium-python` | | Path to load Python pip dependencies from. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12412) in GitLab 12.2) |
|
||||
| `DS_INCLUDE_DEV_DEPENDENCIES` | `gemnasium` | `"true"` | When set to `"false"`, development dependencies and their vulnerabilities are not reported. Only NPM projects are supported. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/227861) in GitLab 15.1. |
|
||||
| `DS_INCLUDE_DEV_DEPENDENCIES` | `gemnasium` | `"true"` | When set to `"false"`, development dependencies and their vulnerabilities are not reported. Only NPM and Poetry projects are supported. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/227861) in GitLab 15.1. |
|
||||
|
||||
#### Other variables
|
||||
|
||||
|
|
|
|||
|
|
@ -9,14 +9,20 @@ module Gitlab
|
|||
|
||||
TimeoutError = Class.new(StandardError)
|
||||
|
||||
MAX_INCLUDES = 100
|
||||
TRIAL_MAX_INCLUDES = 250
|
||||
|
||||
include ::Gitlab::Utils::StrongMemoize
|
||||
|
||||
attr_reader :project, :sha, :user, :parent_pipeline, :variables
|
||||
attr_reader :expandset, :execution_deadline, :logger
|
||||
attr_reader :expandset, :execution_deadline, :logger, :max_includes
|
||||
|
||||
delegate :instrument, to: :logger
|
||||
|
||||
def initialize(project: nil, sha: nil, user: nil, parent_pipeline: nil, variables: nil, logger: nil)
|
||||
def initialize(
|
||||
project: nil, sha: nil, user: nil, parent_pipeline: nil, variables: nil,
|
||||
logger: nil
|
||||
)
|
||||
@project = project
|
||||
@sha = sha
|
||||
@user = user
|
||||
|
|
@ -25,7 +31,7 @@ module Gitlab
|
|||
@expandset = Set.new
|
||||
@execution_deadline = 0
|
||||
@logger = logger || Gitlab::Ci::Pipeline::Logger.new(project: project)
|
||||
|
||||
@max_includes = Feature.enabled?(:ci_increase_includes_to_250, project) ? TRIAL_MAX_INCLUDES : MAX_INCLUDES
|
||||
yield self if block_given?
|
||||
end
|
||||
|
||||
|
|
@ -52,6 +58,7 @@ module Gitlab
|
|||
ctx.expandset = expandset
|
||||
ctx.execution_deadline = execution_deadline
|
||||
ctx.logger = logger
|
||||
ctx.max_includes = max_includes
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -86,7 +93,7 @@ module Gitlab
|
|||
|
||||
protected
|
||||
|
||||
attr_writer :expandset, :execution_deadline, :logger
|
||||
attr_writer :expandset, :execution_deadline, :logger, :max_includes
|
||||
|
||||
private
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ module Gitlab
|
|||
class Mapper
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
MAX_INCLUDES = 100
|
||||
|
||||
FILE_CLASSES = [
|
||||
External::File::Remote,
|
||||
External::File::Template,
|
||||
|
|
@ -134,8 +132,8 @@ module Gitlab
|
|||
end
|
||||
|
||||
def verify_max_includes!
|
||||
if expandset.count >= MAX_INCLUDES
|
||||
raise TooManyIncludesError, "Maximum of #{MAX_INCLUDES} nested includes are allowed!"
|
||||
if expandset.count >= context.max_includes
|
||||
raise TooManyIncludesError, "Maximum of #{context.max_includes} nested includes are allowed!"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -460,6 +460,9 @@ msgstr ""
|
|||
msgid "%{address} is an invalid IP address range"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{attribute} must be between %{min} and %{max}"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{author_link} cloned %{original_issue} to %{new_issue}."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -2147,9 +2150,6 @@ msgstr ""
|
|||
msgid "Add a related issue"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add a resource link"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add a suffix to Service Desk email address. %{linkStart}Learn more.%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -23456,9 +23456,6 @@ msgstr ""
|
|||
msgid "Linked issues"
|
||||
msgstr ""
|
||||
|
||||
msgid "Linked resources"
|
||||
msgstr ""
|
||||
|
||||
msgid "LinkedIn"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -23468,6 +23465,27 @@ msgstr ""
|
|||
msgid "LinkedPipelines|%{counterLabel} more downstream pipelines"
|
||||
msgstr ""
|
||||
|
||||
msgid "LinkedResources|Add"
|
||||
msgstr ""
|
||||
|
||||
msgid "LinkedResources|Add a resource link"
|
||||
msgstr ""
|
||||
|
||||
msgid "LinkedResources|Cancel"
|
||||
msgstr ""
|
||||
|
||||
msgid "LinkedResources|Link"
|
||||
msgstr ""
|
||||
|
||||
msgid "LinkedResources|Linked resources"
|
||||
msgstr ""
|
||||
|
||||
msgid "LinkedResources|Read more about linked resources"
|
||||
msgstr ""
|
||||
|
||||
msgid "LinkedResources|Text (Optional)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Links"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -31759,9 +31777,6 @@ msgstr ""
|
|||
msgid "Read more about GitLab at %{link_to_promo}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Read more about linked resources"
|
||||
msgstr ""
|
||||
|
||||
msgid "Read more about project permissions %{help_link_open}here%{help_link_close}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -34745,6 +34760,9 @@ msgstr ""
|
|||
msgid "SecurityOrchestration|There was a problem creating the new security policy"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|This %{namespaceType} does not contain any security policies."
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|This group"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -34763,9 +34781,6 @@ msgstr ""
|
|||
msgid "SecurityOrchestration|This project"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|This project does not contain any security policies."
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|This view only shows scan results for the agent %{agent}. You can view scan results for all agents in the %{linkStart}Operational Vulnerabilities tab of the vulnerability report%{linkEnd}."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -43837,6 +43852,9 @@ msgstr ""
|
|||
msgid "Work in progress Limit"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|A task provides the ability to break down your work into smaller pieces tied to an issue. Tasks are the first items using our new %{workItemsLink} objects. Additional work item types will be coming soon."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Add a task"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -43884,6 +43902,12 @@ msgstr ""
|
|||
msgid "WorkItem|Expand child items"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Introducing tasks"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Learn about tasks"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|No child items are currently assigned. Use child items to prioritize tasks that your team should complete in order to accomplish your goals!"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -43920,6 +43944,9 @@ msgstr ""
|
|||
msgid "WorkItem|Work item deleted"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|work items"
|
||||
msgstr ""
|
||||
|
||||
msgid "Would you like to create a new branch?"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@
|
|||
"devDependencies": {
|
||||
"@gitlab/eslint-plugin": "13.1.0",
|
||||
"@gitlab/stylelint-config": "4.1.0",
|
||||
"@graphql-eslint/eslint-plugin": "3.10.5",
|
||||
"@graphql-eslint/eslint-plugin": "3.10.6",
|
||||
"@testing-library/dom": "^7.16.2",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@vue/test-utils": "1.3.0",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
|
||||
|
||||
import initHeaderSearch, { eventHandler } from '~/header_search/init';
|
||||
|
||||
describe('Header Search EventListener', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
jest.restoreAllMocks();
|
||||
setHTMLFixture(`
|
||||
<div class="js-header-content">
|
||||
<div class="header-search" id="js-header-search" data-autocomplete-path="/search/autocomplete" data-issues-path="/dashboard/issues" data-mr-path="/dashboard/merge_requests" data-search-context="{}" data-search-path="/search">
|
||||
<input autocomplete="off" class="form-control gl-form-input gl-search-box-by-type-input" data-qa-selector="search_box" id="search" name="search" placeholder="Search GitLab" type="text">
|
||||
</div>
|
||||
</div>`);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetHTMLFixture();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('attached event listener', () => {
|
||||
const searchInputBox = document?.querySelector('#search');
|
||||
const addEventListener = jest.spyOn(searchInputBox, 'addEventListener');
|
||||
initHeaderSearch();
|
||||
|
||||
expect(addEventListener).toBeCalled();
|
||||
});
|
||||
|
||||
it('removes event listener ', async () => {
|
||||
const removeEventListener = jest.fn();
|
||||
jest.mock('~/header_search', () => ({ initHeaderSearchApp: jest.fn() }));
|
||||
await eventHandler.apply(
|
||||
{
|
||||
newHeaderSearchFeatureFlag: true,
|
||||
searchInputBox: document.querySelector('#search'),
|
||||
},
|
||||
[removeEventListener],
|
||||
);
|
||||
|
||||
expect(removeEventListener).toBeCalled();
|
||||
});
|
||||
|
||||
it('attaches new vue dropdown when feature flag is enabled', async () => {
|
||||
const mockVueApp = jest.fn();
|
||||
jest.mock('~/header_search', () => ({ initHeaderSearchApp: mockVueApp }));
|
||||
await eventHandler.apply(
|
||||
{
|
||||
newHeaderSearchFeatureFlag: true,
|
||||
searchInputBox: document.querySelector('#search'),
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
expect(mockVueApp).toBeCalled();
|
||||
});
|
||||
|
||||
it('attaches old vue dropdown when feature flag is disabled', async () => {
|
||||
const mockLegacyApp = jest.fn(() => ({
|
||||
onSearchInputFocus: jest.fn(),
|
||||
}));
|
||||
jest.mock('~/search_autocomplete', () => mockLegacyApp);
|
||||
await eventHandler.apply(
|
||||
{
|
||||
newHeaderSearchFeatureFlag: false,
|
||||
searchInputBox: document.querySelector('#search'),
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
expect(mockLegacyApp).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -14,25 +14,32 @@ exports[`ResourceLinksBlock with defaults renders correct component 1`] = `
|
|||
<h3
|
||||
class="card-title h5 position-relative gl-my-0 gl-display-flex gl-align-items-center gl-h-7"
|
||||
>
|
||||
<gl-link-stub
|
||||
<a
|
||||
aria-hidden="true"
|
||||
class="anchor position-absolute gl-text-decoration-none"
|
||||
class="gl-link anchor position-absolute gl-text-decoration-none"
|
||||
href="#resource-links"
|
||||
id="user-content-resource-links"
|
||||
/>
|
||||
Linked resources
|
||||
<gl-link-stub
|
||||
<a
|
||||
aria-label="Read more about linked resources"
|
||||
class="gl-display-flex gl-align-items-center gl-ml-2 gl-text-gray-500"
|
||||
class="gl-link gl-display-flex gl-align-items-center gl-ml-2 gl-text-gray-500"
|
||||
data-testid="help-link"
|
||||
href="/help/user/project/issues/linked_resources"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<gl-icon-stub
|
||||
name="question"
|
||||
size="12"
|
||||
/>
|
||||
</gl-link-stub>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="gl-icon s12"
|
||||
data-testid="question-icon"
|
||||
role="img"
|
||||
>
|
||||
<use
|
||||
href="#question"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<div
|
||||
class="gl-display-inline-flex"
|
||||
|
|
@ -43,28 +50,166 @@ exports[`ResourceLinksBlock with defaults renders correct component 1`] = `
|
|||
<span
|
||||
class="gl-display-inline-flex gl-align-items-center"
|
||||
>
|
||||
<gl-icon-stub
|
||||
class="gl-mr-2 gl-text-gray-500"
|
||||
name="link"
|
||||
size="16"
|
||||
/>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="gl-mr-2 gl-text-gray-500 gl-icon s16"
|
||||
data-testid="link-icon"
|
||||
role="img"
|
||||
>
|
||||
<use
|
||||
href="#link"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
0
|
||||
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<gl-button-stub
|
||||
<button
|
||||
aria-label="Add a resource link"
|
||||
buttontextclasses=""
|
||||
category="primary"
|
||||
icon="plus"
|
||||
size="medium"
|
||||
variant="default"
|
||||
/>
|
||||
class="btn btn-default btn-md gl-button btn-icon"
|
||||
type="button"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="gl-button-icon gl-icon s16"
|
||||
data-testid="plus-icon"
|
||||
role="img"
|
||||
>
|
||||
<use
|
||||
href="#plus"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!---->
|
||||
</button>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="linked-issues-card-body bg-gray-light"
|
||||
>
|
||||
<div
|
||||
class="card-body bordered-box gl-bg-white"
|
||||
style="display: none;"
|
||||
>
|
||||
<form>
|
||||
<fieldset
|
||||
aria-describedby=""
|
||||
class="form-group gl-form-group"
|
||||
id="__BVID__14"
|
||||
>
|
||||
<legend
|
||||
class="bv-no-focus-ring col-form-label pt-0 col-form-label"
|
||||
id="__BVID__14__BV_label_"
|
||||
tabindex="-1"
|
||||
>
|
||||
|
||||
Text (Optional)
|
||||
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
</legend>
|
||||
<div
|
||||
aria-labelledby="__BVID__14__BV_label_"
|
||||
class="bv-no-focus-ring"
|
||||
role="group"
|
||||
tabindex="-1"
|
||||
>
|
||||
<input
|
||||
class="gl-form-input form-control"
|
||||
data-testid="link-text-input"
|
||||
id="__BVID__16"
|
||||
type="text"
|
||||
/>
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset
|
||||
aria-describedby=""
|
||||
class="form-group gl-form-group"
|
||||
id="__BVID__18"
|
||||
>
|
||||
<legend
|
||||
class="bv-no-focus-ring col-form-label pt-0 col-form-label"
|
||||
id="__BVID__18__BV_label_"
|
||||
tabindex="-1"
|
||||
>
|
||||
|
||||
Link
|
||||
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
</legend>
|
||||
<div
|
||||
aria-labelledby="__BVID__18__BV_label_"
|
||||
class="bv-no-focus-ring"
|
||||
role="group"
|
||||
tabindex="-1"
|
||||
>
|
||||
<input
|
||||
class="gl-form-input form-control"
|
||||
data-testid="link-value-input"
|
||||
id="__BVID__20"
|
||||
type="text"
|
||||
/>
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div
|
||||
class="gl-mt-5 gl-clearfix"
|
||||
>
|
||||
<button
|
||||
class="btn gl-float-left btn-confirm btn-md disabled gl-button"
|
||||
data-testid="add-button"
|
||||
disabled="disabled"
|
||||
type="submit"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
<span
|
||||
class="gl-button-text"
|
||||
>
|
||||
|
||||
Add
|
||||
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn gl-float-right btn-default btn-md gl-button"
|
||||
type="button"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
<span
|
||||
class="gl-button-text"
|
||||
>
|
||||
|
||||
Cancel
|
||||
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
import { nextTick } from 'vue';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import AddIssuableResourceLinkForm from '~/linked_resources/components/add_issuable_resource_link_form.vue';
|
||||
|
||||
describe('AddIssuableResourceLinkForm', () => {
|
||||
let wrapper;
|
||||
|
||||
const mountComponent = () => {
|
||||
wrapper = mountExtended(AddIssuableResourceLinkForm);
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
const findAddButton = () => wrapper.findByTestId('add-button');
|
||||
const findCancelButton = () => wrapper.findByText('Cancel');
|
||||
const findLinkTextInput = () => wrapper.findByTestId('link-text-input');
|
||||
const findLinkValueInput = () => wrapper.findByTestId('link-value-input');
|
||||
|
||||
const cancelForm = async () => {
|
||||
await findCancelButton().trigger('click');
|
||||
};
|
||||
|
||||
describe('cancel form button', () => {
|
||||
const closeFormEvent = { 'add-issuable-resource-link-form-cancel': [[]] };
|
||||
|
||||
beforeEach(() => {
|
||||
mountComponent();
|
||||
});
|
||||
|
||||
it('should close the form on cancel', async () => {
|
||||
await cancelForm();
|
||||
|
||||
expect(wrapper.emitted()).toEqual(closeFormEvent);
|
||||
});
|
||||
|
||||
it('keeps the button disabled without input', () => {
|
||||
expect(findAddButton().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps the button disabled with only text input', async () => {
|
||||
findLinkTextInput().setValue('link text');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findAddButton().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('enables add button when link input is provided', async () => {
|
||||
findLinkTextInput().setValue('link text');
|
||||
findLinkValueInput().setValue('https://foo.example.com');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findAddButton().props('disabled')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,24 +1,59 @@
|
|||
import { GlButton } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import ResourceLinksBlock from '~/linked_resources/components/resource_links_block.vue';
|
||||
import AddIssuableResourceLinkForm from '~/linked_resources/components/add_issuable_resource_link_form.vue';
|
||||
|
||||
describe('ResourceLinksBlock', () => {
|
||||
let wrapper;
|
||||
|
||||
const findResourceLinkAddButton = () => wrapper.find(GlButton);
|
||||
const resourceLinkForm = () => wrapper.findComponent(AddIssuableResourceLinkForm);
|
||||
const helpPath = '/help/user/project/issues/linked_resources';
|
||||
|
||||
describe('with defaults', () => {
|
||||
it('renders correct component', () => {
|
||||
wrapper = shallowMount(ResourceLinksBlock, {
|
||||
propsData: {
|
||||
helpPath,
|
||||
canAddResourceLinks: true,
|
||||
},
|
||||
});
|
||||
const mountComponent = () => {
|
||||
wrapper = mountExtended(ResourceLinksBlock, {
|
||||
propsData: {
|
||||
helpPath,
|
||||
canAddResourceLinks: true,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isFormVisible: false,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.destroy();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
describe('with defaults', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent();
|
||||
});
|
||||
|
||||
it('renders correct component', () => {
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should show the form when add button is clicked', async () => {
|
||||
await findResourceLinkAddButton().trigger('click');
|
||||
|
||||
expect(resourceLinkForm().isVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it('should hide the form when the hide event is emitted', async () => {
|
||||
// open the form
|
||||
await findResourceLinkAddButton().trigger('click');
|
||||
|
||||
await resourceLinkForm().vm.$emit('add-issuable-resource-link-form-cancel');
|
||||
|
||||
expect(resourceLinkForm().isVisible()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with canAddResourceLinks=false', () => {
|
||||
|
|
@ -30,6 +65,26 @@ describe('ResourceLinksBlock', () => {
|
|||
});
|
||||
|
||||
expect(findResourceLinkAddButton().exists()).toBe(false);
|
||||
expect(resourceLinkForm().isVisible()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with isFormVisible=true', () => {
|
||||
it('renders the form with correct props', () => {
|
||||
wrapper = shallowMount(ResourceLinksBlock, {
|
||||
propsData: {
|
||||
canAddResourceLinks: true,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isFormVisible: true,
|
||||
isSubmitting: false,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
expect(resourceLinkForm().exists()).toBe(true);
|
||||
expect(resourceLinkForm().props('isSubmitting')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
|
||||
exports[`Issue type info popover renders 1`] = `
|
||||
<span
|
||||
class="gl-ml-2"
|
||||
id="popovercontainer"
|
||||
>
|
||||
<gl-icon-stub
|
||||
class="gl-ml-5 gl-text-gray-500"
|
||||
class="gl-text-blue-600"
|
||||
id="issue-type-info"
|
||||
name="question-o"
|
||||
size="16"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ export const simpleViewerMock = {
|
|||
language: 'javascript',
|
||||
path: 'some_file.js',
|
||||
webPath: 'some_file.js',
|
||||
blamePath: 'blame/file.js',
|
||||
editBlobPath: 'some_file.js/edit',
|
||||
gitpodBlobUrl: 'https://gitpod.io#path/to/blob.js',
|
||||
ideEditPath: 'some_file.js/ide/edit',
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ const DEFAULT_PROPS = {
|
|||
number: 2,
|
||||
content: '// Line content',
|
||||
language: 'javascript',
|
||||
blamePath: 'blame/file.js',
|
||||
};
|
||||
|
||||
describe('Chunk Line component', () => {
|
||||
|
|
@ -21,7 +20,7 @@ describe('Chunk Line component', () => {
|
|||
wrapper = shallowMountExtended(ChunkLine, { propsData: { ...DEFAULT_PROPS, ...props } });
|
||||
};
|
||||
|
||||
const findLinks = () => wrapper.findAllComponents(GlLink);
|
||||
const findLink = () => wrapper.findComponent(GlLink);
|
||||
const findContent = () => wrapper.findByTestId('content');
|
||||
const findWrappedBidiChars = () => wrapper.findAllByTestId('bidi-wrapper');
|
||||
|
||||
|
|
@ -48,22 +47,14 @@ describe('Chunk Line component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('renders a blame link', () => {
|
||||
expect(findLinks().at(0).attributes()).toMatchObject({
|
||||
href: `${DEFAULT_PROPS.blamePath}#L${DEFAULT_PROPS.number}`,
|
||||
});
|
||||
|
||||
expect(findLinks().at(0).text()).toBe('');
|
||||
});
|
||||
|
||||
it('renders a line number', () => {
|
||||
expect(findLinks().at(1).attributes()).toMatchObject({
|
||||
expect(findLink().attributes()).toMatchObject({
|
||||
'data-line-number': `${DEFAULT_PROPS.number}`,
|
||||
to: `#L${DEFAULT_PROPS.number}`,
|
||||
id: `L${DEFAULT_PROPS.number}`,
|
||||
});
|
||||
|
||||
expect(findLinks().at(1).text()).toBe(DEFAULT_PROPS.number.toString());
|
||||
expect(findLink().text()).toBe(DEFAULT_PROPS.number.toString());
|
||||
});
|
||||
|
||||
it('renders content', () => {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ const DEFAULT_PROPS = {
|
|||
startingFrom: 140,
|
||||
totalLines: 50,
|
||||
language: 'javascript',
|
||||
blamePath: 'blame/file.js',
|
||||
};
|
||||
|
||||
describe('Chunk component', () => {
|
||||
|
|
@ -77,7 +76,6 @@ describe('Chunk component', () => {
|
|||
number: DEFAULT_PROPS.startingFrom + 1,
|
||||
content: splitContent[0],
|
||||
language: DEFAULT_PROPS.language,
|
||||
blamePath: DEFAULT_PROPS.blamePath,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -41,8 +41,7 @@ describe('Source Viewer component', () => {
|
|||
const content = chunk1 + chunk2;
|
||||
const path = 'some/path.js';
|
||||
const fileType = 'javascript';
|
||||
const blamePath = 'some/blame/path.js';
|
||||
const DEFAULT_BLOB_DATA = { language, rawTextBlob: content, path, blamePath, fileType };
|
||||
const DEFAULT_BLOB_DATA = { language, rawTextBlob: content, path, fileType };
|
||||
const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`;
|
||||
|
||||
const createComponent = async (blob = {}) => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import { GlAlert, GlLink } from '@gitlab/ui';
|
||||
import WorkItemInformation from '~/work_items/components/work_item_information.vue';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
|
||||
const createComponent = () => mount(WorkItemInformation);
|
||||
|
||||
describe('Work item information alert', () => {
|
||||
let wrapper;
|
||||
const tasksHelpPath = helpPagePath('user/tasks');
|
||||
const workItemsHelpPath = helpPagePath('development/work_items');
|
||||
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
const findHelpLink = () => wrapper.findComponent(GlLink);
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('should be visible', () => {
|
||||
expect(findAlert().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should emit `work-item-banner-dismissed` event when cross icon is clicked', () => {
|
||||
findAlert().vm.$emit('dismiss');
|
||||
expect(wrapper.emitted('work-item-banner-dismissed').length).toBe(1);
|
||||
});
|
||||
|
||||
it('the alert variant should be tip', () => {
|
||||
expect(findAlert().props('variant')).toBe('tip');
|
||||
});
|
||||
|
||||
it('should have the correct text for primary button and link', () => {
|
||||
expect(findAlert().props('title')).toBe(WorkItemInformation.i18n.tasksInformationTitle);
|
||||
expect(findAlert().props('primaryButtonText')).toBe(
|
||||
WorkItemInformation.i18n.learnTasksButtonText,
|
||||
);
|
||||
expect(findAlert().props('primaryButtonLink')).toBe(tasksHelpPath);
|
||||
});
|
||||
|
||||
it('should have the correct link to work item link', () => {
|
||||
expect(findHelpLink().exists()).toBe(true);
|
||||
expect(findHelpLink().attributes('href')).toBe(workItemsHelpPath);
|
||||
});
|
||||
});
|
||||
|
|
@ -4,6 +4,7 @@ import Vue from 'vue';
|
|||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
|
||||
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
|
||||
import WorkItemDescription from '~/work_items/components/work_item_description.vue';
|
||||
import WorkItemState from '~/work_items/components/work_item_state.vue';
|
||||
|
|
@ -11,10 +12,12 @@ import WorkItemTitle from '~/work_items/components/work_item_title.vue';
|
|||
import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
|
||||
import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
|
||||
import WorkItemWeight from '~/work_items/components/work_item_weight.vue';
|
||||
import WorkItemInformation from '~/work_items/components/work_item_information.vue';
|
||||
import { i18n } from '~/work_items/constants';
|
||||
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
|
||||
import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
|
||||
import { temporaryConfig } from '~/work_items/graphql/provider';
|
||||
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
|
||||
import {
|
||||
workItemTitleSubscriptionResponse,
|
||||
workItemResponseFactory,
|
||||
|
|
@ -23,6 +26,7 @@ import {
|
|||
|
||||
describe('WorkItemDetail component', () => {
|
||||
let wrapper;
|
||||
useLocalStorageSpy();
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
|
|
@ -42,6 +46,8 @@ describe('WorkItemDetail component', () => {
|
|||
const findParentButton = () => findParent().findComponent(GlButton);
|
||||
const findCloseButton = () => wrapper.find('[data-testid="work-item-close"]');
|
||||
const findWorkItemType = () => wrapper.find('[data-testid="work-item-type"]');
|
||||
const findWorkItemInformationAlert = () => wrapper.findComponent(WorkItemInformation);
|
||||
const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
|
||||
|
||||
const createComponent = ({
|
||||
isModal = false,
|
||||
|
|
@ -300,4 +306,22 @@ describe('WorkItemDetail component', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('work item information', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('is visible when viewed for the first time and sets localStorage value', async () => {
|
||||
localStorage.clear();
|
||||
expect(findWorkItemInformationAlert().exists()).toBe(true);
|
||||
expect(findLocalStorageSync().props('value')).toBe(true);
|
||||
});
|
||||
|
||||
it('is not visible after reading local storage input', async () => {
|
||||
await findLocalStorageSync().vm.$emit('input', false);
|
||||
expect(findWorkItemInformationAlert().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ RSpec.describe Types::Ci::JobType do
|
|||
id
|
||||
kind
|
||||
manual_job
|
||||
manual_variables
|
||||
name
|
||||
needs
|
||||
pipeline
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ require 'spec_helper'
|
|||
RSpec.describe GitlabSchema.types['CiVariable'] do
|
||||
it 'contains attributes related to CI variables' do
|
||||
expect(described_class).to have_graphql_fields(
|
||||
:id, :key, :value, :variable_type, :protected, :masked, :raw
|
||||
:id, :key, :value, :variable_type, :protected, :masked, :raw, :environment_scope
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'fast_spec_helper'
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Ci::Config::External::Context do
|
||||
let(:project) { double('Project') }
|
||||
let(:project) { build(:project) }
|
||||
let(:user) { double('User') }
|
||||
let(:sha) { '12345' }
|
||||
let(:variables) { Gitlab::Ci::Variables::Collection.new([{ 'key' => 'a', 'value' => 'b' }]) }
|
||||
|
|
@ -126,7 +126,7 @@ RSpec.describe Gitlab::Ci::Config::External::Context do
|
|||
end
|
||||
|
||||
context 'with attributes' do
|
||||
let(:new_attributes) { { project: double, user: double, sha: '56789' } }
|
||||
let(:new_attributes) { { project: build(:project), user: double, sha: '56789' } }
|
||||
|
||||
it_behaves_like 'a mutated context'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -232,11 +232,9 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do
|
|||
image: 'image:1.0' }
|
||||
end
|
||||
|
||||
before do
|
||||
stub_const("#{described_class}::MAX_INCLUDES", 2)
|
||||
end
|
||||
|
||||
it 'does not raise an exception' do
|
||||
allow(context).to receive(:max_includes).and_return(2)
|
||||
|
||||
expect { subject }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
|
@ -250,11 +248,9 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do
|
|||
image: 'image:1.0' }
|
||||
end
|
||||
|
||||
before do
|
||||
stub_const("#{described_class}::MAX_INCLUDES", 1)
|
||||
end
|
||||
|
||||
it 'raises an exception' do
|
||||
allow(context).to receive(:max_includes).and_return(1)
|
||||
|
||||
expect { subject }.to raise_error(described_class::TooManyIncludesError)
|
||||
end
|
||||
|
||||
|
|
@ -264,6 +260,8 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do
|
|||
end
|
||||
|
||||
it 'raises an exception' do
|
||||
allow(context).to receive(:max_includes).and_return(1)
|
||||
|
||||
expect { subject }.to raise_error(described_class::TooManyIncludesError)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -323,11 +323,9 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do
|
|||
end
|
||||
|
||||
context 'when too many includes is included' do
|
||||
before do
|
||||
stub_const('Gitlab::Ci::Config::External::Mapper::MAX_INCLUDES', 1)
|
||||
end
|
||||
|
||||
it 'raises an error' do
|
||||
allow(context).to receive(:max_includes).and_return(1)
|
||||
|
||||
expect { subject }.to raise_error(Gitlab::Ci::Config::External::Processor::IncludeError, /Maximum of 1 nested/)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -241,17 +241,16 @@ RSpec.describe Gitlab::GitalyClient::RefService do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#ref_exists?', :seed_helper do
|
||||
it 'finds the master branch ref' do
|
||||
expect(client.ref_exists?('refs/heads/master')).to eq(true)
|
||||
end
|
||||
describe '#ref_exists?' do
|
||||
let(:ref) { 'refs/heads/master' }
|
||||
|
||||
it 'returns false for an illegal tag name ref' do
|
||||
expect(client.ref_exists?('refs/tags/.this-tag-name-is-illegal')).to eq(false)
|
||||
end
|
||||
it 'sends a ref_exists message' do
|
||||
expect_any_instance_of(Gitaly::RefService::Stub)
|
||||
.to receive(:ref_exists)
|
||||
.with(gitaly_request_with_params(ref: ref), kind_of(Hash))
|
||||
.and_return(double('ref_exists_response', value: true))
|
||||
|
||||
it 'raises an argument error if the ref name parameter does not start with refs/' do
|
||||
expect { client.ref_exists?('reXXXXX') }.to raise_error(ArgumentError)
|
||||
expect(client.ref_exists?(ref)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -211,6 +211,28 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.created_after' do
|
||||
let_it_be(:old_pipeline) { create(:ci_pipeline, created_at: 1.week.ago) }
|
||||
let_it_be(:pipeline) { create(:ci_pipeline) }
|
||||
|
||||
subject { described_class.created_after(1.day.ago) }
|
||||
|
||||
it 'returns the pipeline' do
|
||||
is_expected.to contain_exactly(pipeline)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.created_before_id' do
|
||||
let_it_be(:pipeline) { create(:ci_pipeline) }
|
||||
let_it_be(:new_pipeline) { create(:ci_pipeline) }
|
||||
|
||||
subject { described_class.created_before_id(new_pipeline.id) }
|
||||
|
||||
it 'returns the pipeline' do
|
||||
is_expected.to contain_exactly(pipeline)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.for_sha' do
|
||||
subject { described_class.for_sha(sha) }
|
||||
|
||||
|
|
|
|||
|
|
@ -131,4 +131,33 @@ RSpec.describe WorkItemPolicy do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'admin_parent_link' do
|
||||
context 'when user is reporter' do
|
||||
let(:current_user) { reporter }
|
||||
|
||||
it { is_expected.to be_allowed(:admin_parent_link) }
|
||||
end
|
||||
|
||||
context 'when user is guest' do
|
||||
let(:current_user) { guest }
|
||||
|
||||
it { is_expected.to be_disallowed(:admin_parent_link) }
|
||||
|
||||
context 'when guest authored the work item' do
|
||||
let(:work_item_subject) { authored_work_item }
|
||||
let(:current_user) { guest_author }
|
||||
|
||||
it { is_expected.to be_disallowed(:admin_parent_link) }
|
||||
end
|
||||
|
||||
context 'when guest is assigned to the work item' do
|
||||
before do
|
||||
work_item.assignees = [guest]
|
||||
end
|
||||
|
||||
it { is_expected.to be_disallowed(:admin_parent_link) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ RSpec.describe 'Query.group(fullPath).ciVariables' do
|
|||
protected
|
||||
masked
|
||||
raw
|
||||
environmentScope
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -35,7 +36,7 @@ RSpec.describe 'Query.group(fullPath).ciVariables' do
|
|||
|
||||
it "returns the group's CI variables" do
|
||||
variable = create(:ci_group_variable, group: group, key: 'TEST_VAR', value: 'test',
|
||||
masked: false, protected: true, raw: true)
|
||||
masked: false, protected: true, raw: true, environment_scope: 'staging')
|
||||
|
||||
post_graphql(query, current_user: user)
|
||||
|
||||
|
|
@ -46,7 +47,8 @@ RSpec.describe 'Query.group(fullPath).ciVariables' do
|
|||
'variableType' => 'ENV_VAR',
|
||||
'masked' => false,
|
||||
'protected' => true,
|
||||
'raw' => true
|
||||
'raw' => true,
|
||||
'environmentScope' => 'staging'
|
||||
})
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ RSpec.describe 'Query.ciVariables' do
|
|||
protected
|
||||
masked
|
||||
raw
|
||||
environmentScope
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -39,7 +40,8 @@ RSpec.describe 'Query.ciVariables' do
|
|||
'variableType' => 'ENV_VAR',
|
||||
'masked' => false,
|
||||
'protected' => true,
|
||||
'raw' => true
|
||||
'raw' => true,
|
||||
'environmentScope' => nil
|
||||
})
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Query.project(fullPath).pipelines.jobs.manualVariables' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:query) do
|
||||
%(
|
||||
query {
|
||||
project(fullPath: "#{project.full_path}") {
|
||||
pipelines {
|
||||
nodes {
|
||||
jobs {
|
||||
nodes {
|
||||
manualVariables {
|
||||
nodes {
|
||||
key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
it 'returns the manual variables for the jobs' do
|
||||
job = create(:ci_build, :manual, pipeline: pipeline)
|
||||
create(:ci_job_variable, key: 'MANUAL_TEST_VAR', job: job)
|
||||
|
||||
post_graphql(query, current_user: user)
|
||||
|
||||
variables_data = graphql_data.dig('project', 'pipelines', 'nodes').first
|
||||
.dig('jobs', 'nodes').flat_map { |job| job.dig('manualVariables', 'nodes') }
|
||||
expect(variables_data.map { |var| var['key'] }).to match_array(['MANUAL_TEST_VAR'])
|
||||
end
|
||||
|
||||
it 'does not fetch job variables for jobs that are not manual' do
|
||||
job = create(:ci_build, pipeline: pipeline)
|
||||
create(:ci_job_variable, key: 'THIS_VAR_WOULD_SHOULD_NEVER_EXIST', job: job)
|
||||
|
||||
post_graphql(query, current_user: user)
|
||||
|
||||
variables_data = graphql_data.dig('project', 'pipelines', 'nodes').first
|
||||
.dig('jobs', 'nodes').flat_map { |job| job.dig('manualVariables', 'nodes') }
|
||||
expect(variables_data).to be_empty
|
||||
end
|
||||
|
||||
it 'does not fetch job variables for bridges' do
|
||||
create(:ci_bridge, :manual, pipeline: pipeline)
|
||||
|
||||
post_graphql(query, current_user: user)
|
||||
|
||||
variables_data = graphql_data.dig('project', 'pipelines', 'nodes').first
|
||||
.dig('jobs', 'nodes').flat_map { |job| job.dig('manualVariables', 'nodes') }
|
||||
expect(variables_data).to be_empty
|
||||
end
|
||||
|
||||
it 'does not produce N+1 queries', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/367991' do
|
||||
second_user = create(:user)
|
||||
project.add_maintainer(second_user)
|
||||
job = create(:ci_build, :manual, pipeline: pipeline)
|
||||
create(:ci_job_variable, key: 'MANUAL_TEST_VAR_1', job: job)
|
||||
|
||||
control_count = ActiveRecord::QueryRecorder.new do
|
||||
post_graphql(query, current_user: user)
|
||||
end
|
||||
|
||||
variables_data = graphql_data.dig('project', 'pipelines', 'nodes').first
|
||||
.dig('jobs', 'nodes').flat_map { |job| job.dig('manualVariables', 'nodes') }
|
||||
expect(variables_data.map { |var| var['key'] }).to match_array(['MANUAL_TEST_VAR_1'])
|
||||
|
||||
job = create(:ci_build, :manual, pipeline: pipeline)
|
||||
create(:ci_job_variable, key: 'MANUAL_TEST_VAR_2', job: job)
|
||||
|
||||
expect do
|
||||
post_graphql(query, current_user: second_user)
|
||||
end.not_to exceed_query_limit(control_count)
|
||||
|
||||
variables_data = graphql_data.dig('project', 'pipelines', 'nodes').first
|
||||
.dig('jobs', 'nodes').flat_map { |job| job.dig('manualVariables', 'nodes') }
|
||||
expect(variables_data.map { |var| var['key'] }).to match_array(%w(MANUAL_TEST_VAR_1 MANUAL_TEST_VAR_2))
|
||||
end
|
||||
end
|
||||
|
|
@ -21,6 +21,7 @@ RSpec.describe 'Query.project(fullPath).ciVariables' do
|
|||
protected
|
||||
masked
|
||||
raw
|
||||
environmentScope
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -35,7 +36,7 @@ RSpec.describe 'Query.project(fullPath).ciVariables' do
|
|||
|
||||
it "returns the project's CI variables" do
|
||||
variable = create(:ci_variable, project: project, key: 'TEST_VAR', value: 'test',
|
||||
masked: false, protected: true, raw: true)
|
||||
masked: false, protected: true, raw: true, environment_scope: 'production')
|
||||
|
||||
post_graphql(query, current_user: user)
|
||||
|
||||
|
|
@ -46,7 +47,8 @@ RSpec.describe 'Query.project(fullPath).ciVariables' do
|
|||
'variableType' => 'ENV_VAR',
|
||||
'masked' => false,
|
||||
'protected' => true,
|
||||
'raw' => true
|
||||
'raw' => true,
|
||||
'environmentScope' => 'production'
|
||||
})
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ RSpec.describe "Delete a task in a work item's description" do
|
|||
end.to change(WorkItem, :count).by(-1).and(
|
||||
change(IssueLink, :count).by(-1)
|
||||
).and(
|
||||
change(work_item, :description).from("- [ ] #{task.to_reference}+").to('')
|
||||
change(work_item, :description).from("- [ ] #{task.to_reference}+").to("- [ ] #{task.title}")
|
||||
)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
|
|
|
|||
|
|
@ -126,5 +126,51 @@ RSpec.describe Ci::CreatePipelineService do
|
|||
it_behaves_like 'not including the file'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with ci_increase_includes_to_250 enabled on root project' do
|
||||
let_it_be(:included_project) do
|
||||
create(:project, :repository).tap { |p| p.add_developer(user) }
|
||||
end
|
||||
|
||||
before do
|
||||
stub_const('::Gitlab::Ci::Config::External::Context::MAX_INCLUDES', 0)
|
||||
stub_const('::Gitlab::Ci::Config::External::Context::TRIAL_MAX_INCLUDES', 3)
|
||||
|
||||
stub_feature_flags(ci_increase_includes_to_250: false)
|
||||
stub_feature_flags(ci_increase_includes_to_250: project)
|
||||
|
||||
allow(Project)
|
||||
.to receive(:find_by_full_path)
|
||||
.with(included_project.full_path)
|
||||
.and_return(included_project)
|
||||
|
||||
allow(included_project.repository)
|
||||
.to receive(:blob_data_at).with(included_project.commit.id, '.gitlab-ci.yml')
|
||||
.and_return(local_config)
|
||||
|
||||
allow(included_project.repository)
|
||||
.to receive(:blob_data_at).with(included_project.commit.id, file_location)
|
||||
.and_return(File.read(Rails.root.join(file_location)))
|
||||
end
|
||||
|
||||
let(:config) do
|
||||
<<~EOY
|
||||
include:
|
||||
- project: #{included_project.full_path}
|
||||
file: .gitlab-ci.yml
|
||||
EOY
|
||||
end
|
||||
|
||||
let(:local_config) do
|
||||
<<~EOY
|
||||
include: #{file_location}
|
||||
|
||||
job:
|
||||
script: exit 0
|
||||
EOY
|
||||
end
|
||||
|
||||
it_behaves_like 'including the file'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ RSpec.describe Ci::UnlockArtifactsService do
|
|||
WHERE
|
||||
"ci_pipelines"."ci_ref_id" = #{ci_ref.id}
|
||||
AND "ci_pipelines"."locked" = 1
|
||||
AND (ci_pipelines.id < #{before_pipeline.id})
|
||||
AND "ci_pipelines"."id" < #{before_pipeline.id}
|
||||
AND "ci_pipelines"."id" NOT IN
|
||||
(WITH RECURSIVE
|
||||
"base_and_descendants"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ RSpec.describe WorkItems::CreateService do
|
|||
let_it_be_with_reload(:project) { create(:project) }
|
||||
let_it_be(:parent) { create(:work_item, project: project) }
|
||||
let_it_be(:guest) { create(:user) }
|
||||
let_it_be(:reporter) { create(:user) }
|
||||
let_it_be(:user_with_no_access) { create(:user) }
|
||||
|
||||
let(:widget_params) { {} }
|
||||
|
|
@ -22,6 +23,7 @@ RSpec.describe WorkItems::CreateService do
|
|||
|
||||
before_all do
|
||||
project.add_guest(guest)
|
||||
project.add_reporter(reporter)
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
|
|
@ -122,8 +124,59 @@ RSpec.describe WorkItems::CreateService do
|
|||
end
|
||||
|
||||
describe 'hierarchy widget' do
|
||||
context 'when parent is valid work item' do
|
||||
let(:widget_params) { { hierarchy_widget: { parent: parent } } }
|
||||
let(:widget_params) { { hierarchy_widget: { parent: parent } } }
|
||||
|
||||
shared_examples 'fails creating work item and returns errors' do
|
||||
it 'does not create new work item if parent can not be set' do
|
||||
expect { service_result }.not_to change(WorkItem, :count)
|
||||
|
||||
expect(service_result[:status]).to be(:error)
|
||||
expect(service_result[:message]).to match(error_message)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user can admin parent link' do
|
||||
let(:current_user) { reporter }
|
||||
|
||||
context 'when parent is valid work item' do
|
||||
let(:opts) do
|
||||
{
|
||||
title: 'Awesome work_item',
|
||||
description: 'please fix',
|
||||
work_item_type: create(:work_item_type, :task)
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates new work item and sets parent reference' do
|
||||
expect { service_result }.to change(
|
||||
WorkItem, :count).by(1).and(change(
|
||||
WorkItems::ParentLink, :count).by(1))
|
||||
|
||||
expect(service_result[:status]).to be(:success)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when parent type is invalid' do
|
||||
let_it_be(:parent) { create(:work_item, :task, project: project) }
|
||||
|
||||
it_behaves_like 'fails creating work item and returns errors' do
|
||||
let(:error_message) { 'only Issue and Incident can be parent of Task.'}
|
||||
end
|
||||
end
|
||||
|
||||
context 'when hierarchy feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(work_items_hierarchy: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'fails creating work item and returns errors' do
|
||||
let(:error_message) { '`work_items_hierarchy` feature flag disabled for this project' }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user cannot admin parent link' do
|
||||
let(:current_user) { guest }
|
||||
|
||||
let(:opts) do
|
||||
{
|
||||
|
|
@ -133,36 +186,8 @@ RSpec.describe WorkItems::CreateService do
|
|||
}
|
||||
end
|
||||
|
||||
it 'creates new work item and sets parent reference' do
|
||||
expect { service_result }.to change(
|
||||
WorkItem, :count).by(1).and(change(
|
||||
WorkItems::ParentLink, :count).by(1))
|
||||
|
||||
expect(service_result[:status]).to be(:success)
|
||||
end
|
||||
|
||||
context 'when parent type is invalid' do
|
||||
let_it_be(:parent) { create(:work_item, :task, project: project) }
|
||||
|
||||
it 'does not create new work item if parent can not be set' do
|
||||
expect { service_result }.not_to change(WorkItem, :count)
|
||||
|
||||
expect(service_result[:status]).to be(:error)
|
||||
expect(service_result[:message]).to match(/only Issue and Incident can be parent of Task./)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when hierarchy feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(work_items_hierarchy: false)
|
||||
end
|
||||
|
||||
it 'does not create new work item if parent can not be set' do
|
||||
expect { service_result }.not_to change(WorkItem, :count)
|
||||
|
||||
expect(service_result[:status]).to be(:error)
|
||||
expect(service_result[:message]).to eq('`work_items_hierarchy` feature flag disabled for this project')
|
||||
end
|
||||
it_behaves_like 'fails creating work item and returns errors' do
|
||||
let(:error_message) { 'No matching task found. Make sure that you are adding a valid task ID.'}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ RSpec.describe WorkItems::DeleteTaskService do
|
|||
it 'removes the task list item with the work item reference' do
|
||||
expect do
|
||||
service_result
|
||||
end.to change(list_work_item, :description).from(list_work_item.description).to('')
|
||||
end.to change(list_work_item, :description).from(list_work_item.description).to("- [ ] #{task.title}")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ require 'spec_helper'
|
|||
RSpec.describe WorkItems::ParentLinks::CreateService do
|
||||
describe '#execute' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:guest) { create(:user) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:work_item) { create(:work_item, project: project) }
|
||||
let_it_be(:task) { create(:work_item, :task, project: project) }
|
||||
|
|
@ -13,7 +14,7 @@ RSpec.describe WorkItems::ParentLinks::CreateService do
|
|||
let_it_be(:guest_task) { create(:work_item, :task) }
|
||||
let_it_be(:invalid_task) { build_stubbed(:work_item, :task, id: non_existing_record_id)}
|
||||
let_it_be(:another_project) { (create :project) }
|
||||
let_it_be(:other_project_task) { create(:work_item, :task, project: another_project) }
|
||||
let_it_be(:other_project_task) { create(:work_item, :task, iid: 100, project: another_project) }
|
||||
let_it_be(:existing_parent_link) { create(:parent_link, work_item: task, work_item_parent: work_item)}
|
||||
|
||||
let(:parent_link_class) { WorkItems::ParentLink }
|
||||
|
|
@ -21,9 +22,10 @@ RSpec.describe WorkItems::ParentLinks::CreateService do
|
|||
let(:params) { {} }
|
||||
|
||||
before do
|
||||
project.add_developer(user)
|
||||
project.add_reporter(user)
|
||||
project.add_guest(guest)
|
||||
guest_task.project.add_guest(user)
|
||||
another_project.add_developer(user)
|
||||
another_project.add_reporter(user)
|
||||
end
|
||||
|
||||
shared_examples 'returns not found error' do
|
||||
|
|
@ -52,7 +54,7 @@ RSpec.describe WorkItems::ParentLinks::CreateService do
|
|||
it_behaves_like 'returns not found error'
|
||||
end
|
||||
|
||||
context 'when user has no permission to link work item' do
|
||||
context 'when user has no permission to link work items' do
|
||||
let(:params) { { issuable_references: [guest_task.id] } }
|
||||
|
||||
it_behaves_like 'returns not found error'
|
||||
|
|
@ -148,6 +150,22 @@ RSpec.describe WorkItems::ParentLinks::CreateService do
|
|||
expect(subject).to eq(service_error(message, http_status: 422))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is a guest' do
|
||||
let(:user) { guest }
|
||||
|
||||
it_behaves_like 'returns not found error'
|
||||
end
|
||||
|
||||
context 'when user is a guest assigned to the work item' do
|
||||
let(:user) { guest }
|
||||
|
||||
before do
|
||||
work_item.assignees = [guest]
|
||||
end
|
||||
|
||||
it_behaves_like 'returns not found error'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe WorkItems::ParentLinks::DestroyService do
|
||||
describe '#execute' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:reporter) { create(:user) }
|
||||
let_it_be(:guest) { create(:user) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:work_item) { create(:work_item, project: project) }
|
||||
let_it_be(:task) { create(:work_item, :task, project: project) }
|
||||
|
|
@ -14,10 +15,13 @@ RSpec.describe WorkItems::ParentLinks::DestroyService do
|
|||
|
||||
subject { described_class.new(parent_link, user).execute }
|
||||
|
||||
before do
|
||||
project.add_reporter(reporter)
|
||||
project.add_guest(guest)
|
||||
end
|
||||
|
||||
context 'when user has permissions to update work items' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
end
|
||||
let(:user) { reporter }
|
||||
|
||||
it 'removes relation' do
|
||||
expect { subject }.to change(parent_link_class, :count).by(-1)
|
||||
|
|
@ -29,6 +33,8 @@ RSpec.describe WorkItems::ParentLinks::DestroyService do
|
|||
end
|
||||
|
||||
context 'when user has insufficient permissions' do
|
||||
let(:user) { guest }
|
||||
|
||||
it 'does not remove relation' do
|
||||
expect { subject }.not_to change(parent_link_class, :count).from(1)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ RSpec.describe WorkItems::TaskListReferenceRemovalService do
|
|||
let(:line_number_end) { 1 }
|
||||
let(:work_item) { single_line_work_item }
|
||||
|
||||
it_behaves_like 'successful work item task reference removal service', ''
|
||||
it_behaves_like 'successful work item task reference removal service', '- [ ] My title 1 single line'
|
||||
|
||||
context 'when description does not contain a task' do
|
||||
let_it_be(:no_matching_work_item) { create(:work_item, project: project, description: 'no matching task') }
|
||||
|
|
@ -102,7 +102,8 @@ RSpec.describe WorkItems::TaskListReferenceRemovalService do
|
|||
end
|
||||
|
||||
context 'when task mardown spans multiple lines' do
|
||||
it_behaves_like 'successful work item task reference removal service', "Any text\n\n* [x] task\n\nMore text"
|
||||
it_behaves_like 'successful work item task reference removal service',
|
||||
"Any text\n\n* [ ] Item to be converted\n My title 1 second line\n third line\n* [x] task\n\nMore text"
|
||||
end
|
||||
|
||||
context 'when updating the work item fails' do
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe WorkItems::TaskListReferenceReplacementService do
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:developer) { create(:user) }
|
||||
let_it_be(:project) { create(:project, :repository).tap { |project| project.add_developer(developer) } }
|
||||
let_it_be(:single_line_work_item, refind: true) { create(:work_item, project: project, description: '- [ ] single line', lock_version: 3) }
|
||||
let_it_be(:multiple_line_work_item, refind: true) { create(:work_item, project: project, description: "Any text\n\n* [ ] Item to be converted\n second line\n third line", lock_version: 3) }
|
||||
|
||||
|
|
@ -37,6 +38,7 @@ RSpec.describe WorkItems::TaskListReferenceReplacementService do
|
|||
subject(:result) do
|
||||
described_class.new(
|
||||
work_item: work_item,
|
||||
current_user: developer,
|
||||
work_item_reference: reference,
|
||||
line_number_start: line_number_start,
|
||||
line_number_end: line_number_end,
|
||||
|
|
@ -52,6 +54,12 @@ RSpec.describe WorkItems::TaskListReferenceReplacementService do
|
|||
let(:task_prefix) { '- [ ]' }
|
||||
|
||||
it_behaves_like 'successful work item task reference replacement service'
|
||||
|
||||
it 'creates description version note' do
|
||||
expect { result }.to change(Note, :count).by(1)
|
||||
expect(work_item.notes.last.note).to eq('changed the description')
|
||||
expect(work_item.saved_description_version.id).to eq(work_item.notes.last.system_note_metadata.description_version_id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when task mardown spans multiple lines' do
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ RSpec.describe 'projects/blob/_viewer.html.haml' do
|
|||
before do
|
||||
assign(:project, project)
|
||||
assign(:blob, blob)
|
||||
assign(:ref, 'master')
|
||||
assign(:id, File.join('master', blob.path))
|
||||
|
||||
controller.params[:controller] = 'projects/blob'
|
||||
|
|
|
|||
|
|
@ -1072,10 +1072,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.7.3.tgz#9ea641146436da388ffbad25d7f2abe0df52c235"
|
||||
integrity sha512-NMV++7Ew1FSBDN1xiZaauU9tfeSfgDHcOLpn+8bGpP+O5orUPm2Eu66R5eC5gkjBPaXosNAxNWtriee+aFk4+g==
|
||||
|
||||
"@graphql-eslint/eslint-plugin@3.10.5":
|
||||
version "3.10.5"
|
||||
resolved "https://registry.yarnpkg.com/@graphql-eslint/eslint-plugin/-/eslint-plugin-3.10.5.tgz#a5d26fe95b52d5fbd02a4c122ca0bc1b2d62fcb2"
|
||||
integrity sha512-rMsuoXA9ldD5IU+3sv9BqDb9SmP+BJFtzF8Y4bV1Pj5O3SROkVDHk/dbN4pv5uFu+Az/AM1BkwVbSzz9CvP5Sw==
|
||||
"@graphql-eslint/eslint-plugin@3.10.6":
|
||||
version "3.10.6"
|
||||
resolved "https://registry.yarnpkg.com/@graphql-eslint/eslint-plugin/-/eslint-plugin-3.10.6.tgz#4d5748fade6c11d74aeff9a99d6e38d2ed8f6310"
|
||||
integrity sha512-rxGSrKVsDHCuZRvP81ElgtCs0sikdhcHqQySiyhir4G+VhiNlPZ7SQJWrXm9JJEAeB0wQ50kabvse5NRk0hqog==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.16.7"
|
||||
"@graphql-tools/code-file-loader" "^7.2.14"
|
||||
|
|
|
|||
Loading…
Reference in New Issue