Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-07-15 18:15:01 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-07-15 18:15:01 +0300
commitb616fd825faac3e7f194e1f942ef30730021e463 (patch)
tree8e187d885200ee5dd7958d7ef32383918ba8e99f
parent1a129420d6bd3e5223e8ba4a5b7749764118a885 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/header_search/init.js48
-rw-r--r--app/assets/javascripts/issues/new/components/type_popover.vue5
-rw-r--r--app/assets/javascripts/linked_resources/components/add_issuable_resource_link_form.vue75
-rw-r--r--app/assets/javascripts/linked_resources/components/resource_links_block.vue54
-rw-r--r--app/assets/javascripts/linked_resources/constants.js17
-rw-r--r--app/assets/javascripts/main.js30
-rw-r--r--app/assets/javascripts/pages/projects/init_blob.js2
-rw-r--r--app/assets/javascripts/repository/queries/blob_info.query.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue31
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue27
-rw-r--r--app/assets/javascripts/work_items/components/work_item_information.vue57
-rw-r--r--app/assets/javascripts/work_items/constants.js1
-rw-r--r--app/assets/stylesheets/framework/files.scss4
-rw-r--r--app/assets/stylesheets/framework/highlight.scss44
-rw-r--r--app/assets/stylesheets/highlight/common.scss48
-rw-r--r--app/assets/stylesheets/highlight/themes/dark.scss14
-rw-r--r--app/assets/stylesheets/highlight/themes/monokai.scss14
-rw-r--r--app/assets/stylesheets/highlight/themes/none.scss14
-rw-r--r--app/assets/stylesheets/highlight/themes/solarized-dark.scss14
-rw-r--r--app/assets/stylesheets/highlight/themes/solarized-light.scss14
-rw-r--r--app/assets/stylesheets/highlight/white_base.scss14
-rw-r--r--app/graphql/types/ci/job_type.rb10
-rw-r--r--app/graphql/types/ci/variable_type.rb9
-rw-r--r--app/models/ci/pipeline.rb4
-rw-r--r--app/policies/work_item_policy.rb4
-rw-r--r--app/services/work_items/create_from_task_service.rb1
-rw-r--r--app/services/work_items/parent_links/create_service.rb8
-rw-r--r--app/services/work_items/parent_links/destroy_service.rb2
-rw-r--r--app/services/work_items/task_list_reference_removal_service.rb15
-rw-r--r--app/services/work_items/task_list_reference_replacement_service.rb9
-rw-r--r--app/views/projects/protected_tags/shared/_create_protected_tag.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/_autodevops_form.html.haml2
-rw-r--r--app/views/shared/_file_highlight.html.haml9
-rw-r--r--app/views/shared/issuable/form/_type_selector.html.haml60
-rw-r--r--config/feature_flags/development/ci_increase_includes_to_250.yml8
-rw-r--r--db/migrate/20220714105122_update_default_project_import_level_on_namespace_settings.rb13
-rw-r--r--db/schema_migrations/202207141051221
-rw-r--r--db/structure.sql2
-rw-r--r--doc/administration/auth/ldap/index.md2
-rw-r--r--doc/api/graphql/reference/index.md5
-rw-r--r--doc/ci/runners/saas/macos_saas_runner.md2
-rw-r--r--doc/development/documentation/testing.md1
-rw-r--r--doc/user/application_security/dependency_scanning/index.md2
-rw-r--r--lib/gitlab/ci/config/external/context.rb15
-rw-r--r--lib/gitlab/ci/config/external/mapper.rb6
-rw-r--r--locale/gitlab.pot51
-rw-r--r--package.json2
-rw-r--r--spec/frontend/header_search/init_spec.js73
-rw-r--r--spec/frontend/issuable/linked_resources/components/__snapshots__/resource_links_block_spec.js.snap187
-rw-r--r--spec/frontend/issuable/linked_resources/components/add_issuable_resource_link_form_spec.js61
-rw-r--r--spec/frontend/issuable/linked_resources/components/resource_links_block_spec.js69
-rw-r--r--spec/frontend/issues/new/components/__snapshots__/type_popover_spec.js.snap3
-rw-r--r--spec/frontend/repository/mock_data.js1
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js15
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js3
-rw-r--r--spec/frontend/work_items/components/work_item_information_spec.js48
-rw-r--r--spec/frontend/work_items/pages/work_item_detail_spec.js24
-rw-r--r--spec/graphql/types/ci/job_type_spec.rb1
-rw-r--r--spec/graphql/types/ci/variable_type_spec.rb (renamed from spec/graphql/types/ci/variables_type_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/config/external/context_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/config/external/processor_spec.rb6
-rw-r--r--spec/lib/gitlab/gitaly_client/ref_service_spec.rb17
-rw-r--r--spec/models/ci/pipeline_spec.rb22
-rw-r--r--spec/policies/work_item_policy_spec.rb29
-rw-r--r--spec/requests/api/graphql/ci/group_variables_spec.rb6
-rw-r--r--spec/requests/api/graphql/ci/instance_variables_spec.rb4
-rw-r--r--spec/requests/api/graphql/ci/manual_variables_spec.rb95
-rw-r--r--spec/requests/api/graphql/ci/project_variables_spec.rb6
-rw-r--r--spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/include_spec.rb46
-rw-r--r--spec/services/ci/unlock_artifacts_service_spec.rb2
-rw-r--r--spec/services/work_items/create_service_spec.rb71
-rw-r--r--spec/services/work_items/delete_task_service_spec.rb2
-rw-r--r--spec/services/work_items/parent_links/create_service_spec.rb26
-rw-r--r--spec/services/work_items/parent_links/destroy_service_spec.rb14
-rw-r--r--spec/services/work_items/task_list_reference_removal_service_spec.rb5
-rw-r--r--spec/services/work_items/task_list_reference_replacement_service_spec.rb10
-rw-r--r--spec/views/projects/blob/_viewer.html.haml_spec.rb1
-rw-r--r--yarn.lock8
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"