diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-15 18:15:01 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-15 18:15:01 +0300 |
commit | b616fd825faac3e7f194e1f942ef30730021e463 (patch) | |
tree | 8e187d885200ee5dd7958d7ef32383918ba8e99f | |
parent | 1a129420d6bd3e5223e8ba4a5b7749764118a885 (diff) |
Add latest changes from gitlab-org/gitlab@master
84 files changed, 1220 insertions, 443 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 296a103cfe8..bf5145be7ee 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -76dabc8174f7978025f48adcfab0a19c85416531 +1250b121b00ef5b3d637463cd4b9e5d93076f9b0 diff --git a/app/assets/javascripts/header_search/init.js b/app/assets/javascripts/header_search/init.js new file mode 100644 index 00000000000..2b9fac0b395 --- /dev/null +++ b/app/assets/javascripts/header_search/init.js @@ -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 }; diff --git a/app/assets/javascripts/issues/new/components/type_popover.vue b/app/assets/javascripts/issues/new/components/type_popover.vue index a70e79b70f9..9c43e527f8b 100644 --- a/app/assets/javascripts/issues/new/components/type_popover.vue +++ b/app/assets/javascripts/issues/new/components/type_popover.vue @@ -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" diff --git a/app/assets/javascripts/linked_resources/components/add_issuable_resource_link_form.vue b/app/assets/javascripts/linked_resources/components/add_issuable_resource_link_form.vue new file mode 100644 index 00000000000..6a0deb41fd1 --- /dev/null +++ b/app/assets/javascripts/linked_resources/components/add_issuable_resource_link_form.vue @@ -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> diff --git a/app/assets/javascripts/linked_resources/components/resource_links_block.vue b/app/assets/javascripts/linked_resources/components/resource_links_block.vue index 3bfee61df15..46c4fc7f632 100644 --- a/app/assets/javascripts/linked_resources/components/resource_links_block.vue +++ b/app/assets/javascripts/linked_resources/components/resource_links_block.vue @@ -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; + }, + }, + methods: { + async toggleResourceLinkForm() { + this.isFormVisible = !this.isFormVisible; }, - resourceLinkHeaderText() { - return LINKED_RESOURCES_HEADER_TEXT; + 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> diff --git a/app/assets/javascripts/linked_resources/constants.js b/app/assets/javascripts/linked_resources/constants.js index 358de326830..1b11cfc5f88 100644 --- a/app/assets/javascripts/linked_resources/constants.js +++ b/app/assets/javascripts/linked_resources/constants.js @@ -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'), +}); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 21d5decb15b..349a28ace52 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -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); diff --git a/app/assets/javascripts/pages/projects/init_blob.js b/app/assets/javascripts/pages/projects/init_blob.js index f37a2987685..7db34816cfe 100644 --- a/app/assets/javascripts/pages/projects/init_blob.js +++ b/app/assets/javascripts/pages/projects/init_blob.js @@ -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'), ); diff --git a/app/assets/javascripts/repository/queries/blob_info.query.graphql b/app/assets/javascripts/repository/queries/blob_info.query.graphql index 45a7793e559..8baee80e5d6 100644 --- a/app/assets/javascripts/repository/queries/blob_info.query.graphql +++ b/app/assets/javascripts/repository/queries/blob_info.query.graphql @@ -27,7 +27,6 @@ query getBlobInfo( fileType language path - blamePath editBlobPath gitpodBlobUrl ideEditPath diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue index 9683288f937..6babbca58c3 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue +++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue @@ -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"> diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue index b6854ee0375..7b62f0cdb7d 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue +++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue @@ -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> diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue index ccc8b44942a..1bdae40332f 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue @@ -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> diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index 40467205d0d..7314b0afc54 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -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" diff --git a/app/assets/javascripts/work_items/components/work_item_information.vue b/app/assets/javascripts/work_items/components/work_item_information.vue new file mode 100644 index 00000000000..2ff7ba169ea --- /dev/null +++ b/app/assets/javascripts/work_items/components/work_item_information.vue @@ -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> diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js index c9ccbd48ba1..2140b418e6d 100644 --- a/app/assets/javascripts/work_items/constants.js +++ b/app/assets/javascripts/work_items/constants.js @@ -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'; diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index d59ca14ee84..f322c6c8929 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -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; diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss index f1f43e55921..1c43212f501 100644 --- a/app/assets/stylesheets/framework/highlight.scss +++ b/app/assets/stylesheets/framework/highlight.scss @@ -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; - } -} diff --git a/app/assets/stylesheets/highlight/common.scss b/app/assets/stylesheets/highlight/common.scss index 8e4f4600d5c..fcbd05141b9 100644 --- a/app/assets/stylesheets/highlight/common.scss +++ b/app/assets/stylesheets/highlight/common.scss @@ -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); - } - } -} - -@mixin line-hover-bg($color: $white-normal) { - &:hover, - &:focus-within { - background-color: darken($color, 10); + &:hover::before { + @include gl-display-inline-block; } -} -@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; } } diff --git a/app/assets/stylesheets/highlight/themes/dark.scss b/app/assets/stylesheets/highlight/themes/dark.scss index f7f3c8964bf..709e7f5ae18 100644 --- a/app/assets/stylesheets/highlight/themes/dark.scss +++ b/app/assets/stylesheets/highlight/themes/dark.scss @@ -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, diff --git a/app/assets/stylesheets/highlight/themes/monokai.scss b/app/assets/stylesheets/highlight/themes/monokai.scss index 9dfe48f5609..0ed9c209417 100644 --- a/app/assets/stylesheets/highlight/themes/monokai.scss +++ b/app/assets/stylesheets/highlight/themes/monokai.scss @@ -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, diff --git a/app/assets/stylesheets/highlight/themes/none.scss b/app/assets/stylesheets/highlight/themes/none.scss index 7fd81353363..868e466b1f8 100644 --- a/app/assets/stylesheets/highlight/themes/none.scss +++ b/app/assets/stylesheets/highlight/themes/none.scss @@ -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, diff --git a/app/assets/stylesheets/highlight/themes/solarized-dark.scss b/app/assets/stylesheets/highlight/themes/solarized-dark.scss index 95d3c8feb54..6260339a48d 100644 --- a/app/assets/stylesheets/highlight/themes/solarized-dark.scss +++ b/app/assets/stylesheets/highlight/themes/solarized-dark.scss @@ -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, diff --git a/app/assets/stylesheets/highlight/themes/solarized-light.scss b/app/assets/stylesheets/highlight/themes/solarized-light.scss index 47f5e0bd3da..e6f098f4cdf 100644 --- a/app/assets/stylesheets/highlight/themes/solarized-light.scss +++ b/app/assets/stylesheets/highlight/themes/solarized-light.scss @@ -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, diff --git a/app/assets/stylesheets/highlight/white_base.scss b/app/assets/stylesheets/highlight/white_base.scss index 336c2b739f1..770a90bbc57 100644 --- a/app/assets/stylesheets/highlight/white_base.scss +++ b/app/assets/stylesheets/highlight/white_base.scss @@ -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 { diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb index 802cb09b36a..42b55f47f92 100644 --- a/app/graphql/types/ci/job_type.rb +++ b/app/graphql/types/ci/job_type.rb @@ -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 diff --git a/app/graphql/types/ci/variable_type.rb b/app/graphql/types/ci/variable_type.rb index 5d2acfb9c9f..63f89b6d207 100644 --- a/app/graphql/types/ci/variable_type.rb +++ b/app/graphql/types/ci/variable_type.rb @@ -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 diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 795def55306..1c01a81ee41 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -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) } diff --git a/app/policies/work_item_policy.rb b/app/policies/work_item_policy.rb index ea7559592e1..2f3561f1135 100644 --- a/app/policies/work_item_policy.rb +++ b/app/policies/work_item_policy.rb @@ -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 diff --git a/app/services/work_items/create_from_task_service.rb b/app/services/work_items/create_from_task_service.rb index d5fa5fca772..ef1d47c560d 100644 --- a/app/services/work_items/create_from_task_service.rb +++ b/app/services/work_items/create_from_task_service.rb @@ -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], diff --git a/app/services/work_items/parent_links/create_service.rb b/app/services/work_items/parent_links/create_service.rb index 87995cc6550..78013f081c8 100644 --- a/app/services/work_items/parent_links/create_service.rb +++ b/app/services/work_items/parent_links/create_service.rb @@ -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 diff --git a/app/services/work_items/parent_links/destroy_service.rb b/app/services/work_items/parent_links/destroy_service.rb index deca24159d3..55870d44db9 100644 --- a/app/services/work_items/parent_links/destroy_service.rb +++ b/app/services/work_items/parent_links/destroy_service.rb @@ -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 diff --git a/app/services/work_items/task_list_reference_removal_service.rb b/app/services/work_items/task_list_reference_removal_service.rb index e7ec73a96e0..9152580bef0 100644 --- a/app/services/work_items/task_list_reference_removal_service.rb +++ b/app/services/work_items/task_list_reference_removal_service.rb @@ -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 diff --git a/app/services/work_items/task_list_reference_replacement_service.rb b/app/services/work_items/task_list_reference_replacement_service.rb index 1044a4feb88..b098d67561b 100644 --- a/app/services/work_items/task_list_reference_replacement_service.rb +++ b/app/services/work_items/task_list_reference_replacement_service.rb @@ -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 diff --git a/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml b/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml index ba0935fff7d..e257117a32e 100644 --- a/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml +++ b/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml @@ -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 diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml index 96564e44cf2..64f45ec89d1 100644 --- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml +++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml @@ -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| diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml index 453c6438edf..f8ac3832a77 100644 --- a/app/views/shared/_file_highlight.html.haml +++ b/app/views/shared/_file_highlight.html.haml @@ -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 diff --git a/app/views/shared/issuable/form/_type_selector.html.haml b/app/views/shared/issuable/form/_type_selector.html.haml index d5c696b1698..a94ef70b2d5 100644 --- a/app/views/shared/issuable/form/_type_selector.html.haml +++ b/app/views/shared/issuable/form/_type_selector.html.haml @@ -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 diff --git a/config/feature_flags/development/ci_increase_includes_to_250.yml b/config/feature_flags/development/ci_increase_includes_to_250.yml new file mode 100644 index 00000000000..b6291ab0cd3 --- /dev/null +++ b/config/feature_flags/development/ci_increase_includes_to_250.yml @@ -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 diff --git a/db/migrate/20220714105122_update_default_project_import_level_on_namespace_settings.rb b/db/migrate/20220714105122_update_default_project_import_level_on_namespace_settings.rb new file mode 100644 index 00000000000..30357ded9ce --- /dev/null +++ b/db/migrate/20220714105122_update_default_project_import_level_on_namespace_settings.rb @@ -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 diff --git a/db/schema_migrations/20220714105122 b/db/schema_migrations/20220714105122 new file mode 100644 index 00000000000..f3ec5c17af7 --- /dev/null +++ b/db/schema_migrations/20220714105122 @@ -0,0 +1 @@ +c452f7dc9a76b6daa7ced88f2ed93332a84bfcb94a7e94f31149e43b888e210f
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index bb70833b62d..299a022de9a 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -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)) ); diff --git a/doc/administration/auth/ldap/index.md b/doc/administration/auth/ldap/index.md index 3bf2fff898d..05eee338e64 100644 --- a/doc/administration/auth/ldap/index.md +++ b/doc/administration/auth/ldap/index.md @@ -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: diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index e83c9eb1523..fbf6bc116f4 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -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. | diff --git a/doc/ci/runners/saas/macos_saas_runner.md b/doc/ci/runners/saas/macos_saas_runner.md index 09a0cc975f2..5a2d84b6996 100644 --- a/doc/ci/runners/saas/macos_saas_runner.md +++ b/doc/ci/runners/saas/macos_saas_runner.md @@ -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). diff --git a/doc/development/documentation/testing.md b/doc/development/documentation/testing.md index feb10845aea..d55cbe28d9b 100644 --- a/doc/development/documentation/testing.md +++ b/doc/development/documentation/testing.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) diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md index ceabdc9722a..d0a91ab664e 100644 --- a/doc/user/application_security/dependency_scanning/index.md +++ b/doc/user/application_security/dependency_scanning/index.md @@ -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 diff --git a/lib/gitlab/ci/config/external/context.rb b/lib/gitlab/ci/config/external/context.rb index 2def565bc19..ec628399785 100644 --- a/lib/gitlab/ci/config/external/context.rb +++ b/lib/gitlab/ci/config/external/context.rb @@ -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 diff --git a/lib/gitlab/ci/config/external/mapper.rb b/lib/gitlab/ci/config/external/mapper.rb index c1250c82750..2a1060a6059 100644 --- a/lib/gitlab/ci/config/external/mapper.rb +++ b/lib/gitlab/ci/config/external/mapper.rb @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 5031f9f2e25..8254899c809 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/package.json b/package.json index af8f6ca3b8d..163667ec7c6 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/spec/frontend/header_search/init_spec.js b/spec/frontend/header_search/init_spec.js new file mode 100644 index 00000000000..2feef79e8d1 --- /dev/null +++ b/spec/frontend/header_search/init_spec.js @@ -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(); + }); +}); diff --git a/spec/frontend/issuable/linked_resources/components/__snapshots__/resource_links_block_spec.js.snap b/spec/frontend/issuable/linked_resources/components/__snapshots__/resource_links_block_spec.js.snap index 24586744ad6..2ccfe4f91e7 100644 --- a/spec/frontend/issuable/linked_resources/components/__snapshots__/resource_links_block_spec.js.snap +++ b/spec/frontend/issuable/linked_resources/components/__snapshots__/resource_links_block_spec.js.snap @@ -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> `; diff --git a/spec/frontend/issuable/linked_resources/components/add_issuable_resource_link_form_spec.js b/spec/frontend/issuable/linked_resources/components/add_issuable_resource_link_form_spec.js new file mode 100644 index 00000000000..b3707569848 --- /dev/null +++ b/spec/frontend/issuable/linked_resources/components/add_issuable_resource_link_form_spec.js @@ -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); + }); + }); +}); diff --git a/spec/frontend/issuable/linked_resources/components/resource_links_block_spec.js b/spec/frontend/issuable/linked_resources/components/resource_links_block_spec.js index c17ca1a3287..bca63a34b2e 100644 --- a/spec/frontend/issuable/linked_resources/components/resource_links_block_spec.js +++ b/spec/frontend/issuable/linked_resources/components/resource_links_block_spec.js @@ -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'; + const mountComponent = () => { + wrapper = mountExtended(ResourceLinksBlock, { + propsData: { + helpPath, + canAddResourceLinks: true, + }, + data() { + return { + isFormVisible: false, + }; + }, + }); + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + }); + }; + describe('with defaults', () => { - it('renders correct component', () => { - wrapper = shallowMount(ResourceLinksBlock, { - propsData: { - helpPath, - canAddResourceLinks: true, - }, - }); + 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); }); }); }); diff --git a/spec/frontend/issues/new/components/__snapshots__/type_popover_spec.js.snap b/spec/frontend/issues/new/components/__snapshots__/type_popover_spec.js.snap index 881dcda126f..1a199ed2ee9 100644 --- a/spec/frontend/issues/new/components/__snapshots__/type_popover_spec.js.snap +++ b/spec/frontend/issues/new/components/__snapshots__/type_popover_spec.js.snap @@ -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" diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js index 4db295fe0b7..0a5766a25f9 100644 --- a/spec/frontend/repository/mock_data.js +++ b/spec/frontend/repository/mock_data.js @@ -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', diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js index 525f8983971..eb2eec92534 100644 --- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js @@ -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', () => { diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js index 8dc3348acfa..42c4f2eacb8 100644 --- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js @@ -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, }); }); }); diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js index be52c144efb..2c03b7aa7d3 100644 --- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js @@ -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 = {}) => { diff --git a/spec/frontend/work_items/components/work_item_information_spec.js b/spec/frontend/work_items/components/work_item_information_spec.js new file mode 100644 index 00000000000..d5f6921c2bc --- /dev/null +++ b/spec/frontend/work_items/components/work_item_information_spec.js @@ -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); + }); +}); diff --git a/spec/frontend/work_items/pages/work_item_detail_spec.js b/spec/frontend/work_items/pages/work_item_detail_spec.js index 42821493f50..43869468ad0 100644 --- a/spec/frontend/work_items/pages/work_item_detail_spec.js +++ b/spec/frontend/work_items/pages/work_item_detail_spec.js @@ -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); + }); + }); }); diff --git a/spec/graphql/types/ci/job_type_spec.rb b/spec/graphql/types/ci/job_type_spec.rb index 959f0ebefd8..bc9e64282bc 100644 --- a/spec/graphql/types/ci/job_type_spec.rb +++ b/spec/graphql/types/ci/job_type_spec.rb @@ -23,6 +23,7 @@ RSpec.describe Types::Ci::JobType do id kind manual_job + manual_variables name needs pipeline diff --git a/spec/graphql/types/ci/variables_type_spec.rb b/spec/graphql/types/ci/variable_type_spec.rb index 0a97a0f72f3..a81e6adbab6 100644 --- a/spec/graphql/types/ci/variables_type_spec.rb +++ b/spec/graphql/types/ci/variable_type_spec.rb @@ -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 diff --git a/spec/lib/gitlab/ci/config/external/context_spec.rb b/spec/lib/gitlab/ci/config/external/context_spec.rb index 800c563cd0b..40702e75404 100644 --- a/spec/lib/gitlab/ci/config/external/context_spec.rb +++ b/spec/lib/gitlab/ci/config/external/context_spec.rb @@ -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 diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb index 7e1b31fea6a..e74fdc2071b 100644 --- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb @@ -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 diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb index 15a0ff40aa4..841a46e197d 100644 --- a/spec/lib/gitlab/ci/config/external/processor_spec.rb +++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb @@ -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 diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb index c794e3ca9ae..566bdbacf4a 100644 --- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb @@ -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 diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 12053d56467..5a50ce0911f 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -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) } diff --git a/spec/policies/work_item_policy_spec.rb b/spec/policies/work_item_policy_spec.rb index 9cfc4455979..f8ec7d9f9bc 100644 --- a/spec/policies/work_item_policy_spec.rb +++ b/spec/policies/work_item_policy_spec.rb @@ -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 diff --git a/spec/requests/api/graphql/ci/group_variables_spec.rb b/spec/requests/api/graphql/ci/group_variables_spec.rb index f0a571f1fef..5ea6646ec2c 100644 --- a/spec/requests/api/graphql/ci/group_variables_spec.rb +++ b/spec/requests/api/graphql/ci/group_variables_spec.rb @@ -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 diff --git a/spec/requests/api/graphql/ci/instance_variables_spec.rb b/spec/requests/api/graphql/ci/instance_variables_spec.rb index 1faa4289029..7acf73a4e7a 100644 --- a/spec/requests/api/graphql/ci/instance_variables_spec.rb +++ b/spec/requests/api/graphql/ci/instance_variables_spec.rb @@ -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 diff --git a/spec/requests/api/graphql/ci/manual_variables_spec.rb b/spec/requests/api/graphql/ci/manual_variables_spec.rb new file mode 100644 index 00000000000..b7aa76511a3 --- /dev/null +++ b/spec/requests/api/graphql/ci/manual_variables_spec.rb @@ -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 diff --git a/spec/requests/api/graphql/ci/project_variables_spec.rb b/spec/requests/api/graphql/ci/project_variables_spec.rb index a4c1ef9c650..e61f146b24c 100644 --- a/spec/requests/api/graphql/ci/project_variables_spec.rb +++ b/spec/requests/api/graphql/ci/project_variables_spec.rb @@ -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 diff --git a/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb b/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb index 05d3587d342..e576d0ee7ef 100644 --- a/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb @@ -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) diff --git a/spec/services/ci/create_pipeline_service/include_spec.rb b/spec/services/ci/create_pipeline_service/include_spec.rb index 3116801d50c..849eb5885f6 100644 --- a/spec/services/ci/create_pipeline_service/include_spec.rb +++ b/spec/services/ci/create_pipeline_service/include_spec.rb @@ -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 diff --git a/spec/services/ci/unlock_artifacts_service_spec.rb b/spec/services/ci/unlock_artifacts_service_spec.rb index 8ee07fc44c8..94d39fc9f14 100644 --- a/spec/services/ci/unlock_artifacts_service_spec.rb +++ b/spec/services/ci/unlock_artifacts_service_spec.rb @@ -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" diff --git a/spec/services/work_items/create_service_spec.rb b/spec/services/work_items/create_service_spec.rb index 7b44601bfdd..1b6af1fef5a 100644 --- a/spec/services/work_items/create_service_spec.rb +++ b/spec/services/work_items/create_service_spec.rb @@ -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,33 +124,43 @@ 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 } } } - let(:opts) do - { - title: 'Awesome work_item', - description: 'please fix', - work_item_type: create(:work_item_type, :task) - } + 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)) + 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) + 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 '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./) + 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 @@ -157,14 +169,27 @@ RSpec.describe WorkItems::CreateService 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') + 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 + { + title: 'Awesome work_item', + description: 'please fix', + work_item_type: create(:work_item_type, :task) + } + 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 end end diff --git a/spec/services/work_items/delete_task_service_spec.rb b/spec/services/work_items/delete_task_service_spec.rb index 04944645c9b..07a0d8d6c1a 100644 --- a/spec/services/work_items/delete_task_service_spec.rb +++ b/spec/services/work_items/delete_task_service_spec.rb @@ -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 diff --git a/spec/services/work_items/parent_links/create_service_spec.rb b/spec/services/work_items/parent_links/create_service_spec.rb index 7aa90d01a0b..a2f695900ae 100644 --- a/spec/services/work_items/parent_links/create_service_spec.rb +++ b/spec/services/work_items/parent_links/create_service_spec.rb @@ -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 diff --git a/spec/services/work_items/parent_links/destroy_service_spec.rb b/spec/services/work_items/parent_links/destroy_service_spec.rb index 4c155909ac4..574b70af397 100644 --- a/spec/services/work_items/parent_links/destroy_service_spec.rb +++ b/spec/services/work_items/parent_links/destroy_service_spec.rb @@ -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 diff --git a/spec/services/work_items/task_list_reference_removal_service_spec.rb b/spec/services/work_items/task_list_reference_removal_service_spec.rb index bca72da0efa..88fabe5fe40 100644 --- a/spec/services/work_items/task_list_reference_removal_service_spec.rb +++ b/spec/services/work_items/task_list_reference_removal_service_spec.rb @@ -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 diff --git a/spec/services/work_items/task_list_reference_replacement_service_spec.rb b/spec/services/work_items/task_list_reference_replacement_service_spec.rb index e7914eb4a92..965c5f1d554 100644 --- a/spec/services/work_items/task_list_reference_replacement_service_spec.rb +++ b/spec/services/work_items/task_list_reference_replacement_service_spec.rb @@ -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 diff --git a/spec/views/projects/blob/_viewer.html.haml_spec.rb b/spec/views/projects/blob/_viewer.html.haml_spec.rb index 2761d10f9ad..893cfec1491 100644 --- a/spec/views/projects/blob/_viewer.html.haml_spec.rb +++ b/spec/views/projects/blob/_viewer.html.haml_spec.rb @@ -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' diff --git a/yarn.lock b/yarn.lock index bdaf4f889cf..ec6cbe098a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" |