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>2021-02-15 21:09:09 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-02-15 21:09:09 +0300
commitfb994e98ecce2a1f1dfaa87c9c3de8535815813b (patch)
tree0a0915d6a0dcd22aab31d2f89c72c85597aa5b86
parent51858218a3961c6d872703eabde0635bc0a1368f (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue16
-rw-r--r--app/assets/javascripts/boards/components/board_column_deprecated.vue11
-rw-r--r--app/assets/javascripts/boards/constants.js2
-rw-r--r--app/assets/javascripts/boards/models/list.js1
-rw-r--r--app/assets/javascripts/boards/stores/actions.js27
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js7
-rw-r--r--app/assets/javascripts/boards/stores/getters.js3
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js2
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js8
-rw-r--r--app/assets/javascripts/boards/stores/state.js1
-rw-r--r--app/assets/javascripts/profile/profile.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js5
-rw-r--r--app/assets/stylesheets/page_bundles/boards.scss41
-rw-r--r--app/assets/stylesheets/pages/commits.scss24
-rw-r--r--app/controllers/profiles/notifications_controller.rb2
-rw-r--r--app/graphql/types/ci/pipeline_type.rb3
-rw-r--r--app/helpers/analytics/unique_visits_helper.rb1
-rw-r--r--app/views/profiles/notifications/_email_settings.html.haml4
-rw-r--r--changelogs/unreleased/285464_improve_highlighting_for_diffs.yml (renamed from changelogs/unreleased/16950_improve_highlighting_for_diffs.yml)2
-rw-r--r--changelogs/unreleased/add-user-setting-for-opting-into-marketing-emails.yml5
-rw-r--r--changelogs/unreleased/ci-allow-deep-nesting-for-scripts.yml5
-rw-r--r--changelogs/unreleased/ph-ph-fixWidgetGraphqlPipelineWarnings.yml5
-rw-r--r--changelogs/unreleased/psi-board-list-highlight.yml5
-rw-r--r--config/environments/development.rb8
-rw-r--r--config/feature_flags/development/improved_merge_diff_highlighting.yml2
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql15
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json38
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--doc/ci/examples/semantic-release.md4
-rw-r--r--doc/user/packages/composer_repository/index.md2
-rw-r--r--doc/user/packages/container_registry/index.md6
-rw-r--r--doc/user/packages/dependency_proxy/index.md10
-rw-r--r--doc/user/packages/maven_repository/index.md4
-rw-r--r--doc/user/packages/npm_registry/index.md4
-rw-r--r--doc/user/profile/notifications.md2
-rw-r--r--lib/gitlab/ci/config/entry/commands.rb6
-rw-r--r--lib/gitlab/config/entry/validators.rb17
-rw-r--r--lib/gitlab/config/entry/validators/nested_array_helpers.rb46
-rw-r--r--lib/gitlab/diff/highlight_cache.rb2
-rw-r--r--lib/gitlab/diff/inline_diff.rb2
-rw-r--r--lib/gitlab/relative_positioning.rb13
-rw-r--r--lib/gitlab/relative_positioning/range.rb14
-rw-r--r--locale/gitlab.pot18
-rw-r--r--spec/controllers/profiles/notifications_controller_spec.rb3
-rw-r--r--spec/frontend/boards/components/board_column_deprecated_spec.js14
-rw-r--r--spec/frontend/boards/components/board_column_spec.js13
-rw-r--r--spec/frontend/boards/stores/actions_spec.js96
-rw-r--r--spec/graphql/types/ci/pipeline_type_spec.rb2
-rw-r--r--spec/helpers/analytics/unique_visits_helper_spec.rb13
-rw-r--r--spec/lib/gitlab/ci/config/entry/commands_spec.rb8
-rw-r--r--spec/lib/gitlab/config/entry/validators/nested_array_helpers_spec.rb70
53 files changed, 516 insertions, 101 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 624ecebd74c..b0fd27e1f7b 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-c886f7f37533e8ed19e245a83f363086daf590e9
+29dec5fdae0846da19f803058441581b43fda91d
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue
index 2586351208c..41b9ee795eb 100644
--- a/app/assets/javascripts/boards/components/board_column.vue
+++ b/app/assets/javascripts/boards/components/board_column.vue
@@ -31,8 +31,11 @@ export default {
},
},
computed: {
- ...mapState(['filterParams']),
+ ...mapState(['filterParams', 'highlightedLists']),
...mapGetters(['getIssuesByList']),
+ highlighted() {
+ return this.highlightedLists.includes(this.list.id);
+ },
listIssues() {
return this.getIssuesByList(this.list.id);
},
@@ -48,6 +51,16 @@ export default {
deep: true,
immediate: true,
},
+ highlighted: {
+ handler(highlighted) {
+ if (highlighted) {
+ this.$nextTick(() => {
+ this.$el.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ });
+ }
+ },
+ immediate: true,
+ },
},
methods: {
...mapActions(['fetchIssuesForList']),
@@ -68,6 +81,7 @@ export default {
>
<div
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base"
+ :class="{ 'board-column-highlighted': highlighted }"
>
<board-list-header :can-admin-list="canAdminList" :list="list" :disabled="disabled" />
<board-list
diff --git a/app/assets/javascripts/boards/components/board_column_deprecated.vue b/app/assets/javascripts/boards/components/board_column_deprecated.vue
index b7931adf067..3dc77654e28 100644
--- a/app/assets/javascripts/boards/components/board_column_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_column_deprecated.vue
@@ -54,6 +54,16 @@ export default {
},
deep: true,
},
+ 'list.highlighted': {
+ handler(highlighted) {
+ if (highlighted) {
+ this.$nextTick(() => {
+ this.$el.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ });
+ }
+ },
+ immediate: true,
+ },
},
mounted() {
const instance = this;
@@ -98,6 +108,7 @@ export default {
>
<div
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base"
+ :class="{ 'board-column-highlighted': list.highlighted }"
>
<board-list-header :can-admin-list="canAdminList" :list="list" :disabled="disabled" />
<board-list ref="board-list" :disabled="disabled" :issues="listIssues" :list="list" />
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index f45c5d8fbcd..3ab89b2c9da 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -34,6 +34,8 @@ export const LIST = 'list';
export const NOT_FILTER = 'not[';
+export const flashAnimationDuration = 2000;
+
export default {
BoardType,
ListType,
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index f6681193dcb..6c6e2522d92 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -44,6 +44,7 @@ class List {
this.isExpandable = Boolean(typeInfo.isExpandable);
this.isExpanded = !obj.collapsed;
this.page = 1;
+ this.highlighted = obj.highlighted;
this.loading = true;
this.loadingMore = false;
this.issues = obj.issues || [];
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 5ef3147a0a9..e2d07f9128c 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -1,7 +1,7 @@
import { pick } from 'lodash';
import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
-import { BoardType, ListType, inactiveId } from '~/boards/constants';
+import { BoardType, ListType, inactiveId, flashAnimationDuration } from '~/boards/constants';
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
@@ -110,9 +110,31 @@ export default {
.catch(() => commit(types.RECEIVE_BOARD_LISTS_FAILURE));
},
- createList: ({ state, commit, dispatch }, { backlog, labelId, milestoneId, assigneeId }) => {
+ highlightList: ({ commit, state }, listId) => {
+ if ([ListType.backlog, ListType.closed].includes(state.boardLists[listId].listType)) {
+ return;
+ }
+
+ commit(types.ADD_LIST_TO_HIGHLIGHTED_LISTS, listId);
+
+ setTimeout(() => {
+ commit(types.REMOVE_LIST_FROM_HIGHLIGHTED_LISTS, listId);
+ }, flashAnimationDuration);
+ },
+
+ createList: (
+ { state, commit, dispatch, getters },
+ { backlog, labelId, milestoneId, assigneeId },
+ ) => {
const { boardId } = state;
+ const existingList = getters.getListByLabelId(labelId);
+
+ if (existingList) {
+ dispatch('highlightList', existingList.id);
+ return;
+ }
+
gqlClient
.mutate({
mutation: createBoardListMutation,
@@ -130,6 +152,7 @@ export default {
} else {
const list = data.boardListCreate?.list;
dispatch('addList', list);
+ dispatch('highlightList', list.id);
}
})
.catch(() => commit(types.CREATE_LIST_FAILURE));
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index de6d03669f7..39d95552084 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -14,7 +14,7 @@ import {
convertObjectPropsToCamelCase,
} from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
-import { ListType } from '../constants';
+import { ListType, flashAnimationDuration } from '../constants';
import eventHub from '../eventhub';
import ListAssignee from '../models/assignee';
import ListLabel from '../models/label';
@@ -106,6 +106,11 @@ const boardsStore = {
list
.save()
.then(() => {
+ list.highlighted = true;
+ setTimeout(() => {
+ list.highlighted = false;
+ }, flashAnimationDuration);
+
// Remove any new issues from the backlog
// as they will be visible in the new list
list.issues.forEach(backlogList.removeIssue.bind(backlogList));
diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js
index 827c1e377cf..cab97088bc6 100644
--- a/app/assets/javascripts/boards/stores/getters.js
+++ b/app/assets/javascripts/boards/stores/getters.js
@@ -28,6 +28,9 @@ export default {
},
getListByLabelId: (state) => (labelId) => {
+ if (!labelId) {
+ return null;
+ }
return find(state.boardLists, (l) => l.label?.id === labelId);
},
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index 5ec0ee158df..a89e961ae2d 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -43,3 +43,5 @@ export const SET_SELECTED_PROJECT = 'SET_SELECTED_PROJECT';
export const ADD_BOARD_ITEM_TO_SELECTION = 'ADD_BOARD_ITEM_TO_SELECTION';
export const REMOVE_BOARD_ITEM_FROM_SELECTION = 'REMOVE_BOARD_ITEM_FROM_SELECTION';
export const SET_ADD_COLUMN_FORM_VISIBLE = 'SET_ADD_COLUMN_FORM_VISIBLE';
+export const ADD_LIST_TO_HIGHLIGHTED_LISTS = 'ADD_LIST_TO_HIGHLIGHTED_LISTS';
+export const REMOVE_LIST_FROM_HIGHLIGHTED_LISTS = 'REMOVE_LIST_FROM_HIGHLIGHTED_LISTS';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index 09f7f267ee3..79c98c3d90c 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -274,4 +274,12 @@ export default {
[mutationTypes.SET_ADD_COLUMN_FORM_VISIBLE]: (state, visible) => {
state.addColumnFormVisible = visible;
},
+
+ [mutationTypes.ADD_LIST_TO_HIGHLIGHTED_LISTS]: (state, listId) => {
+ state.highlightedLists.push(listId);
+ },
+
+ [mutationTypes.REMOVE_LIST_FROM_HIGHLIGHTED_LISTS]: (state, listId) => {
+ state.highlightedLists = state.highlightedLists.filter((id) => id !== listId);
+ },
};
diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js
index badbd2d80e5..91544d6c9c5 100644
--- a/app/assets/javascripts/boards/stores/state.js
+++ b/app/assets/javascripts/boards/stores/state.js
@@ -15,6 +15,7 @@ export default () => ({
filterParams: {},
boardConfig: {},
labels: [],
+ highlightedLists: [],
selectedBoardItems: [],
groupProjects: [],
groupProjectsFlags: {
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index 300dd5ecc51..a7332b81b9f 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -42,6 +42,7 @@ export default class Profile {
$('#user_notification_email').on('select2-selecting', (event) => {
setTimeout(this.submitForm.bind(event.currentTarget));
});
+ $('#user_email_opted_in').on('change', this.submitForm);
$('#user_notified_of_own_activity').on('change', this.submitForm);
this.form.on('submit', this.onSubmitForm);
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
index b284bb23969..13ea07884b1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
@@ -14,6 +14,7 @@ query getState($projectPath: ID!, $iid: String!) {
pipelines(first: 1) {
nodes {
status
+ warnings
}
}
shouldBeRebased
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index 78a17493d31..a0f14f558d2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -172,6 +172,11 @@ export default class MergeRequestStore {
this.canBeMerged = mergeRequest.mergeStatus === 'can_be_merged';
this.canMerge = mergeRequest.userPermissions.canMerge;
this.ciStatus = pipeline?.status.toLowerCase();
+
+ if (pipeline?.warnings && this.ciStatus === 'success') {
+ this.ciStatus = `${this.ciStatus}-with-warnings`;
+ }
+
this.commitsCount = mergeRequest.commitCount || 10;
this.branchMissing = !mergeRequest.sourceBranchExists || !mergeRequest.targetBranchExists;
this.hasConflicts = mergeRequest.conflicts;
diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss
index 3d1ae3519a9..620b37914d8 100644
--- a/app/assets/stylesheets/page_bundles/boards.scss
+++ b/app/assets/stylesheets/page_bundles/boards.scss
@@ -138,6 +138,47 @@
border: 1px solid var(--gray-100, $gray-100);
}
+// to highlight columns we have animated pulse of box-shadow
+// we don't want to actually animate the box-shadow property
+// because that causes costly repaints. Instead we can add a
+// pseudo-element that is the same size as our element, then
+// animate opacity/transform to give a soothing single pulse
+.board-column-highlighted::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ pointer-events: none;
+ opacity: 0;
+ z-index: -1;
+ box-shadow: 0 0 6px 3px $blue-200;
+ animation-name: board-column-flash-border;
+ animation-duration: 1.2s;
+ animation-fill-mode: forwards;
+ animation-timing-function: ease-in-out;
+}
+
+@keyframes board-column-flash-border {
+ 0%,
+ 100% {
+ opacity: 0;
+ transform: scale(0.98);
+ }
+
+ 25%,
+ 75% {
+ opacity: 1;
+ transform: scale(0.99);
+ }
+
+ 50% {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
.board-header {
&.has-border::before {
border-top: 3px solid;
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 226c7867eca..7111d3d4107 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -108,6 +108,30 @@
}
}
+.text-expander {
+ display: inline-flex;
+ background: $white;
+ color: $gl-text-color-secondary;
+ padding: 1px $gl-padding-4;
+ cursor: pointer;
+ border: 1px solid $border-white-normal;
+ border-radius: $border-radius-default;
+ margin-left: 5px;
+ font-size: 12px;
+ line-height: $gl-font-size;
+ outline: none;
+
+ &.open {
+ background-color: darken($gray-light, 10%);
+ box-shadow: inset 0 0 2px rgba($black, 0.2);
+ }
+
+ &:hover {
+ background-color: darken($gray-light, 10%);
+ text-decoration: none;
+ }
+}
+
.commit.flex-list {
display: flex;
}
diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb
index a3e7638cdbc..0a73239709a 100644
--- a/app/controllers/profiles/notifications_controller.rb
+++ b/app/controllers/profiles/notifications_controller.rb
@@ -29,7 +29,7 @@ class Profiles::NotificationsController < Profiles::ApplicationController
end
def user_params
- params.require(:user).permit(:notification_email, :notified_of_own_activity)
+ params.require(:user).permit(:notification_email, :email_opted_in, :notified_of_own_activity)
end
private
diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb
index af7e0fa224f..2c386c9b564 100644
--- a/app/graphql/types/ci/pipeline_type.rb
+++ b/app/graphql/types/ci/pipeline_type.rb
@@ -27,6 +27,9 @@ module Types
field :status, PipelineStatusEnum, null: false,
description: "Status of the pipeline (#{::Ci::Pipeline.all_state_names.compact.join(', ').upcase})"
+ field :warnings, GraphQL::BOOLEAN_TYPE, null: false, method: :has_warnings?,
+ description: "Indicates if a pipeline has warnings."
+
field :detailed_status, Types::Ci::DetailedStatusType, null: false,
description: 'Detailed status of the pipeline.'
diff --git a/app/helpers/analytics/unique_visits_helper.rb b/app/helpers/analytics/unique_visits_helper.rb
index 4c709b2ed23..337a5dc9536 100644
--- a/app/helpers/analytics/unique_visits_helper.rb
+++ b/app/helpers/analytics/unique_visits_helper.rb
@@ -14,7 +14,6 @@ module Analytics
end
def track_visit(target_id)
- return unless Feature.enabled?(:track_unique_visits, default_enabled: true)
return unless visitor_id
Gitlab::Analytics::UniqueVisits.new.track_visit(visitor_id, target_id)
diff --git a/app/views/profiles/notifications/_email_settings.html.haml b/app/views/profiles/notifications/_email_settings.html.haml
index 7ac3ef9b141..f452a5b2eb5 100644
--- a/app/views/profiles/notifications/_email_settings.html.haml
+++ b/app/views/profiles/notifications/_email_settings.html.haml
@@ -4,3 +4,7 @@
= form.select :notification_email, @user.public_verified_emails, { include_blank: false }, class: "select2", disabled: local_assigns.fetch(:email_change_disabled, nil)
.help-block
= local_assigns.fetch(:help_text, nil)
+.form-group
+ %label{ for: 'user_email_opted_in' }
+ = form.check_box :email_opted_in
+ %span= _('Receive product marketing emails')
diff --git a/changelogs/unreleased/16950_improve_highlighting_for_diffs.yml b/changelogs/unreleased/285464_improve_highlighting_for_diffs.yml
index da487e91980..1e6bf23b379 100644
--- a/changelogs/unreleased/16950_improve_highlighting_for_diffs.yml
+++ b/changelogs/unreleased/285464_improve_highlighting_for_diffs.yml
@@ -1,5 +1,5 @@
---
title: Improve highlighting for merge diffs
-merge_request: 52499
+merge_request: 53980
author:
type: added
diff --git a/changelogs/unreleased/add-user-setting-for-opting-into-marketing-emails.yml b/changelogs/unreleased/add-user-setting-for-opting-into-marketing-emails.yml
new file mode 100644
index 00000000000..d169e43d047
--- /dev/null
+++ b/changelogs/unreleased/add-user-setting-for-opting-into-marketing-emails.yml
@@ -0,0 +1,5 @@
+---
+title: Add user setting for opting into marketing emails
+merge_request: 53921
+author:
+type: added
diff --git a/changelogs/unreleased/ci-allow-deep-nesting-for-scripts.yml b/changelogs/unreleased/ci-allow-deep-nesting-for-scripts.yml
new file mode 100644
index 00000000000..643957d7338
--- /dev/null
+++ b/changelogs/unreleased/ci-allow-deep-nesting-for-scripts.yml
@@ -0,0 +1,5 @@
+---
+title: Accept deeply nested arrays for CI script keyword
+merge_request: 53737
+author:
+type: changed
diff --git a/changelogs/unreleased/ph-ph-fixWidgetGraphqlPipelineWarnings.yml b/changelogs/unreleased/ph-ph-fixWidgetGraphqlPipelineWarnings.yml
new file mode 100644
index 00000000000..d0cb9490a9e
--- /dev/null
+++ b/changelogs/unreleased/ph-ph-fixWidgetGraphqlPipelineWarnings.yml
@@ -0,0 +1,5 @@
+---
+title: Added warnings field to the pipelines GraphQL type
+merge_request: 54089
+author:
+type: added
diff --git a/changelogs/unreleased/psi-board-list-highlight.yml b/changelogs/unreleased/psi-board-list-highlight.yml
new file mode 100644
index 00000000000..e97f6696049
--- /dev/null
+++ b/changelogs/unreleased/psi-board-list-highlight.yml
@@ -0,0 +1,5 @@
+---
+title: Highlight board lists when they are added
+merge_request: 53779
+author:
+type: changed
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 146cdd4f5a7..50d394859bc 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -11,7 +11,13 @@ Rails.application.configure do
# Show full error reports and disable caching
config.active_record.verbose_query_logs = true
config.consider_all_requests_local = true
- config.action_controller.perform_caching = false
+
+ if Rails.root.join('tmp', 'caching-dev.txt').exist?
+ config.action_controller.perform_caching = true
+ config.action_controller.enable_fragment_cache_logging = true
+ else
+ config.action_controller.perform_caching = false
+ end
# Show a warning when a large data set is loaded into memory
config.active_record.warn_on_records_fetched_greater_than = 1000
diff --git a/config/feature_flags/development/improved_merge_diff_highlighting.yml b/config/feature_flags/development/improved_merge_diff_highlighting.yml
index 61978ff48da..b397405a12c 100644
--- a/config/feature_flags/development/improved_merge_diff_highlighting.yml
+++ b/config/feature_flags/development/improved_merge_diff_highlighting.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/299884
milestone: '13.9'
type: development
group: group::source code
-default_enabled: false
+default_enabled: true
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 8e8e91ec6e8..a08e9284234 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -18566,6 +18566,11 @@ type Pipeline {
Permissions for the current user on the resource
"""
userPermissions: PipelinePermissions!
+
+ """
+ Indicates if a pipeline has warnings.
+ """
+ warnings: Boolean!
}
type PipelineAnalytics {
@@ -20285,6 +20290,11 @@ type Project {
iids: [ID!]
"""
+ The state of latest requirement test report.
+ """
+ lastTestReportState: TestReportState
+
+ """
Search query for requirement title.
"""
search: String
@@ -20345,6 +20355,11 @@ type Project {
last: Int
"""
+ The state of latest requirement test report.
+ """
+ lastTestReportState: TestReportState
+
+ """
Search query for requirement title.
"""
search: String
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index d161a4a5e17..e380ea3d0d5 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -54399,6 +54399,24 @@
},
"isDeprecated": false,
"deprecationReason": null
+ },
+ {
+ "name": "warnings",
+ "description": "Indicates if a pipeline has warnings.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
}
],
"inputFields": null,
@@ -58805,6 +58823,16 @@
}
},
"defaultValue": null
+ },
+ {
+ "name": "lastTestReportState",
+ "description": "The state of latest requirement test report.",
+ "type": {
+ "kind": "ENUM",
+ "name": "TestReportState",
+ "ofType": null
+ },
+ "defaultValue": null
}
],
"type": {
@@ -58910,6 +58938,16 @@
"defaultValue": null
},
{
+ "name": "lastTestReportState",
+ "description": "The state of latest requirement test report.",
+ "type": {
+ "kind": "ENUM",
+ "name": "TestReportState",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 030ae25c22d..df31b6f1dd0 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -2807,6 +2807,7 @@ Information about pagination in a connection..
| `upstream` | Pipeline | Pipeline that triggered the pipeline. |
| `user` | User | Pipeline user. |
| `userPermissions` | PipelinePermissions! | Permissions for the current user on the resource |
+| `warnings` | Boolean! | Indicates if a pipeline has warnings. |
### PipelineAnalytics
diff --git a/doc/ci/examples/semantic-release.md b/doc/ci/examples/semantic-release.md
index 83ab72b3478..c0fc93fe1b3 100644
--- a/doc/ci/examples/semantic-release.md
+++ b/doc/ci/examples/semantic-release.md
@@ -84,9 +84,9 @@ This example configures the pipeline with a single job, `publish`, which runs `s
The default `before_script` generates a temporary `.npmrc` that is used to authenticate to the Package Registry during the `publish` job.
-## Set up environment variables
+## Set up CI/CD variables
-As part of publishing a package, semantic-release increases the version number in `package.json`. For semantic-release to commit this change and push it back to GitLab, the pipeline requires a custom environment variable named `GITLAB_TOKEN`. To create this variable:
+As part of publishing a package, semantic-release increases the version number in `package.json`. For semantic-release to commit this change and push it back to GitLab, the pipeline requires a custom CI/CD variable named `GITLAB_TOKEN`. To create this variable:
1. Navigate to **Project > Settings > Access Tokens**.
1. Give the token a name, and select the `api` scope.
diff --git a/doc/user/packages/composer_repository/index.md b/doc/user/packages/composer_repository/index.md
index c7245a0fd19..734dee3b4c5 100644
--- a/doc/user/packages/composer_repository/index.md
+++ b/doc/user/packages/composer_repository/index.md
@@ -273,5 +273,5 @@ Output indicates that the package has been successfully installed.
WARNING:
Never commit the `auth.json` file to your repository. To install packages from a CI/CD job,
consider using the [`composer config`](https://getcomposer.org/doc/articles/handling-private-packages.md#satis) tool with your personal access token
-stored in a [GitLab CI/CD environment variable](../../../ci/variables/README.md) or in
+stored in a [GitLab CI/CD variable](../../../ci/variables/README.md) or in
[HashiCorp Vault](../../../ci/secrets/index.md).
diff --git a/doc/user/packages/container_registry/index.md b/doc/user/packages/container_registry/index.md
index 74218fc8c13..e3a469c4b6c 100644
--- a/doc/user/packages/container_registry/index.md
+++ b/doc/user/packages/container_registry/index.md
@@ -144,7 +144,7 @@ Before you can build and push images by using GitLab CI/CD, you must authenticat
To use CI/CD to authenticate, you can use:
-- The `CI_REGISTRY_USER` variable.
+- The `CI_REGISTRY_USER` CI/CD variable.
This variable has read-write access to the Container Registry and is valid for
one job only. Its password is also automatically created and assigned to `CI_REGISTRY_PASSWORD`.
@@ -209,7 +209,7 @@ build:
- docker push $CI_REGISTRY/group/project/image:latest
```
-You can also make use of [other variables](../../../ci/variables/README.md) to avoid hard-coding:
+You can also make use of [other CI/CD variables](../../../ci/variables/README.md) to avoid hard-coding:
```yaml
build:
@@ -382,7 +382,7 @@ The following example defines two stages: `build`, and `clean`. The
`build_image` job builds the Docker image for the branch, and the
`delete_image` job deletes it. The `reg` executable is downloaded and used to
remove the image matching the `$CI_PROJECT_PATH:$CI_COMMIT_REF_SLUG`
-[environment variable](../../../ci/variables/predefined_variables.md).
+[predefined CI/CD variable](../../../ci/variables/predefined_variables.md).
To use this example, change the `IMAGE_TAG` variable to match your needs:
diff --git a/doc/user/packages/dependency_proxy/index.md b/doc/user/packages/dependency_proxy/index.md
index 1883c551e18..627ca5c0c88 100644
--- a/doc/user/packages/dependency_proxy/index.md
+++ b/doc/user/packages/dependency_proxy/index.md
@@ -96,17 +96,17 @@ You can authenticate using:
Runners log in to the Dependency Proxy automatically. To pull through
the Dependency Proxy, use the `CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX`
-environment variable:
+[predefined CI/CD variable](../../../ci/variables/predefined_variables.md):
```yaml
# .gitlab-ci.yml
image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/node:latest
```
-There are other additional predefined environment variables you can also use:
+There are other additional predefined CI/CD variables you can also use:
-- `CI_DEPENDENCY_PROXY_USER`: A CI user for logging in to the Dependency Proxy.
-- `CI_DEPENDENCY_PROXY_PASSWORD`: A CI password for logging in to the Dependency Proxy.
+- `CI_DEPENDENCY_PROXY_USER`: A CI/CD user for logging in to the Dependency Proxy.
+- `CI_DEPENDENCY_PROXY_PASSWORD`: A CI/CD password for logging in to the Dependency Proxy.
- `CI_DEPENDENCY_PROXY_SERVER`: The server for logging in to the Dependency Proxy.
- `CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX`: The image prefix for pulling images through the Dependency Proxy.
@@ -119,7 +119,7 @@ Proxy manually without including the port:
docker pull gitlab.example.com:443/my-group/dependency_proxy/containers/alpine:latest
```
-You can also use [custom environment variables](../../../ci/variables/README.md#custom-cicd-variables) to store and access your personal access token or other valid credentials.
+You can also use [custom CI/CD variables](../../../ci/variables/README.md#custom-cicd-variables) to store and access your personal access token or other valid credentials.
### Store a Docker image in Dependency Proxy cache
diff --git a/doc/user/packages/maven_repository/index.md b/doc/user/packages/maven_repository/index.md
index 3888a44bc06..c43fc2664d3 100644
--- a/doc/user/packages/maven_repository/index.md
+++ b/doc/user/packages/maven_repository/index.md
@@ -732,7 +732,7 @@ You can create a new package each time the `master` branch is updated.
```
1. Make sure your `pom.xml` file includes the following.
- You can either let Maven use the CI environment variables, as shown in this example,
+ You can either let Maven use the [predefined CI/CD variables](../../../ci/variables/predefined_variables.md), as shown in this example,
or you can hard code your server's hostname and project's ID.
```xml
@@ -771,7 +771,7 @@ The next time the `deploy` job runs, it copies `ci_settings.xml` to the
user's home location. In this example:
- The user is `root`, because the job runs in a Docker container.
-- Maven uses the configured CI [environment variables](../../../ci/variables/README.md#predefined-cicd-variables).
+- Maven uses the configured CI/CD variables.
### Create Maven packages with GitLab CI/CD by using Gradle
diff --git a/doc/user/packages/npm_registry/index.md b/doc/user/packages/npm_registry/index.md
index bd2b571e43e..93c480593ef 100644
--- a/doc/user/packages/npm_registry/index.md
+++ b/doc/user/packages/npm_registry/index.md
@@ -199,7 +199,7 @@ Then, you can run `npm publish` either locally or by using GitLab CI/CD.
NPM_TOKEN=<your_token> npm publish
```
-- **GitLab CI/CD:** Set an `NPM_TOKEN` [variable](../../../ci/variables/README.md)
+- **GitLab CI/CD:** Set an `NPM_TOKEN` [CI/CD variable](../../../ci/variables/README.md)
under your project's **Settings > CI/CD > Variables**.
## Package naming convention
@@ -450,7 +450,7 @@ And the `.npmrc` file should look like:
### `npm install` returns `Error: Failed to replace env in config: ${npm_TOKEN}`
-You do not need a token to run `npm install` unless your project is private. The token is only required to publish. If the `.npmrc` file was checked in with a reference to `$npm_TOKEN`, you can remove it. If you prefer to leave the reference in, you must set a value prior to running `npm install` or set the value by using [GitLab environment variables](../../../ci/variables/README.md):
+You do not need a token to run `npm install` unless your project is private. The token is only required to publish. If the `.npmrc` file was checked in with a reference to `$npm_TOKEN`, you can remove it. If you prefer to leave the reference in, you must set a value prior to running `npm install` or set the value by using [GitLab CI/CD variables](../../../ci/variables/README.md):
```shell
NPM_TOKEN=<your_token> npm install
diff --git a/doc/user/profile/notifications.md b/doc/user/profile/notifications.md
index 0a48b0a2b23..703154945db 100644
--- a/doc/user/profile/notifications.md
+++ b/doc/user/profile/notifications.md
@@ -56,6 +56,8 @@ Your **Global notification settings** are the default settings unless you select
- This is the email address your notifications are sent to.
- Global notification level
- This is the default [notification level](#notification-levels) which applies to all your notifications.
+- Receive product marketing emails
+ - Check this checkbox if you want to receive periodic emails related to GitLab features.
- Receive notifications about your own activity.
- Check this checkbox if you want to receive notification about your own activity. Default: Not checked.
diff --git a/lib/gitlab/ci/config/entry/commands.rb b/lib/gitlab/ci/config/entry/commands.rb
index 7a86fca3056..341f87b44ab 100644
--- a/lib/gitlab/ci/config/entry/commands.rb
+++ b/lib/gitlab/ci/config/entry/commands.rb
@@ -10,12 +10,14 @@ module Gitlab
class Commands < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
+ MAX_NESTING_LEVEL = 10
+
validations do
- validates :config, string_or_nested_array_of_strings: true
+ validates :config, string_or_nested_array_of_strings: { max_level: MAX_NESTING_LEVEL }
end
def value
- Array(@config).flatten(1)
+ Array(@config).flatten(MAX_NESTING_LEVEL)
end
end
end
diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb
index 88786ed82ff..8120f2c1243 100644
--- a/lib/gitlab/config/entry/validators.rb
+++ b/lib/gitlab/config/entry/validators.rb
@@ -268,17 +268,16 @@ module Gitlab
end
end
- class StringOrNestedArrayOfStringsValidator < NestedArrayOfStringsValidator
- def validate_each(record, attribute, value)
- unless validate_string_or_nested_array_of_strings(value)
- record.errors.add(attribute, 'should be a string or an array containing strings and arrays of strings')
- end
- end
+ class StringOrNestedArrayOfStringsValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+ include NestedArrayHelpers
- private
+ def validate_each(record, attribute, value)
+ max_level = options.fetch(:max_level, 1)
- def validate_string_or_nested_array_of_strings(values)
- validate_string(values) || validate_nested_array_of_strings(values)
+ unless validate_string(value) || validate_nested_array(value, max_level, &method(:validate_string))
+ record.errors.add(attribute, "should be a string or a nested array of strings up to #{max_level} levels deep")
+ end
end
end
diff --git a/lib/gitlab/config/entry/validators/nested_array_helpers.rb b/lib/gitlab/config/entry/validators/nested_array_helpers.rb
new file mode 100644
index 00000000000..9f5d17d74b0
--- /dev/null
+++ b/lib/gitlab/config/entry/validators/nested_array_helpers.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ module Validators
+ # Include this module to validate deeply nested array of values
+ #
+ # class MyNestedValidator < ActiveModel::EachValidator
+ # include NestedArrayHelpers
+ #
+ # def validate_each(record, attribute, value)
+ # max_depth = options.fetch(:max_depth, 1)
+ #
+ # unless validate_nested_array(value, max_depth) { |v| v.is_a?(Integer) }
+ # record.errors.add(attribute, "is invalid")
+ # end
+ # end
+ # end
+ #
+ module NestedArrayHelpers
+ def validate_nested_array(value, max_depth = 1, &validator_proc)
+ return false unless value.is_a?(Array)
+
+ validate_nested_array_recursively(value, max_depth, &validator_proc)
+ end
+
+ private
+
+ # rubocop: disable Performance/RedundantBlockCall
+ # Disables Rubocop rule for easier readability reasons.
+ def validate_nested_array_recursively(value, nesting_level, &validator_proc)
+ return true if validator_proc.call(value)
+ return false if nesting_level <= 0
+ return false unless value.is_a?(Array)
+
+ value.all? do |element|
+ validate_nested_array_recursively(element, nesting_level - 1, &validator_proc)
+ end
+ end
+ # rubocop: enable Performance/RedundantBlockCall
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/highlight_cache.rb b/lib/gitlab/diff/highlight_cache.rb
index 5141b5170f0..7932cd2a837 100644
--- a/lib/gitlab/diff/highlight_cache.rb
+++ b/lib/gitlab/diff/highlight_cache.rb
@@ -77,7 +77,7 @@ module Gitlab
private
def version
- if Feature.enabled?(:improved_merge_diff_highlighting, diffable.project)
+ if Feature.enabled?(:improved_merge_diff_highlighting, diffable.project, default_enabled: :yaml)
NEXT_VERSION
else
VERSION
diff --git a/lib/gitlab/diff/inline_diff.rb b/lib/gitlab/diff/inline_diff.rb
index 9b3fe1e3a43..cf769262958 100644
--- a/lib/gitlab/diff/inline_diff.rb
+++ b/lib/gitlab/diff/inline_diff.rb
@@ -31,7 +31,7 @@ module Gitlab
# Skip inline diff if empty line was replaced with content
return if old_line == ""
- if Feature.enabled?(:improved_merge_diff_highlighting, project)
+ if Feature.enabled?(:improved_merge_diff_highlighting, project, default_enabled: :yaml)
CharDiff.new(old_line, new_line).changed_ranges(offset: offset)
else
deprecated_diff
diff --git a/lib/gitlab/relative_positioning.rb b/lib/gitlab/relative_positioning.rb
index b5a923f0824..e2cbe4b2de0 100644
--- a/lib/gitlab/relative_positioning.rb
+++ b/lib/gitlab/relative_positioning.rb
@@ -13,5 +13,18 @@ module Gitlab
MIN_GAP = 2
NoSpaceLeft = Class.new(StandardError)
+ IllegalRange = Class.new(ArgumentError)
+
+ def self.range(lhs, rhs)
+ if lhs && rhs
+ ClosedRange.new(lhs, rhs)
+ elsif lhs
+ StartingFrom.new(lhs)
+ elsif rhs
+ EndingAt.new(rhs)
+ else
+ raise IllegalRange, 'One of rhs or lhs must be provided' unless lhs && rhs
+ end
+ end
end
end
diff --git a/lib/gitlab/relative_positioning/range.rb b/lib/gitlab/relative_positioning/range.rb
index 174d5ef4b35..0b0ccdf5be4 100644
--- a/lib/gitlab/relative_positioning/range.rb
+++ b/lib/gitlab/relative_positioning/range.rb
@@ -2,8 +2,6 @@
module Gitlab
module RelativePositioning
- IllegalRange = Class.new(ArgumentError)
-
class Range
attr_reader :lhs, :rhs
@@ -34,18 +32,6 @@ module Gitlab
end
end
- def self.range(lhs, rhs)
- if lhs && rhs
- ClosedRange.new(lhs, rhs)
- elsif lhs
- StartingFrom.new(lhs)
- elsif rhs
- EndingAt.new(rhs)
- else
- raise IllegalRange, 'One of rhs or lhs must be provided' unless lhs && rhs
- end
- end
-
class ClosedRange < RelativePositioning::Range
def initialize(lhs, rhs)
@lhs, @rhs = lhs, rhs
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 06a1b72ffd8..329ec718cf0 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -7525,9 +7525,21 @@ msgstr ""
msgid "ComplianceFrameworks|All"
msgstr ""
+msgid "ComplianceFrameworks|Combines with the CI configuration at runtime."
+msgstr ""
+
+msgid "ComplianceFrameworks|Compliance pipeline configuration location (optional)"
+msgstr ""
+
+msgid "ComplianceFrameworks|Could not find this configuration location, please try a different location"
+msgstr ""
+
msgid "ComplianceFrameworks|Error fetching compliance frameworks data. Please refresh the page"
msgstr ""
+msgid "ComplianceFrameworks|Invalid format: it should follow the format [PATH].y(a)ml@[GROUP]/[PROJECT]"
+msgstr ""
+
msgid "ComplianceFrameworks|Once you have created a compliance framework it will appear here."
msgstr ""
@@ -7543,6 +7555,9 @@ msgstr ""
msgid "ComplianceFrameworks|Use %{codeStart}::%{codeEnd} to create a %{linkStart}scoped set%{linkEnd} (eg. %{codeStart}SOX::AWS%{codeEnd})"
msgstr ""
+msgid "ComplianceFrameworks|e.g. include-gitlab.ci.yml@group-name/project-name"
+msgstr ""
+
msgid "ComplianceFramework|GDPR"
msgstr ""
@@ -24237,6 +24252,9 @@ msgstr ""
msgid "Receive notifications about your own activity"
msgstr ""
+msgid "Receive product marketing emails"
+msgstr ""
+
msgid "Recent"
msgstr ""
diff --git a/spec/controllers/profiles/notifications_controller_spec.rb b/spec/controllers/profiles/notifications_controller_spec.rb
index 90df7cc0991..03749366703 100644
--- a/spec/controllers/profiles/notifications_controller_spec.rb
+++ b/spec/controllers/profiles/notifications_controller_spec.rb
@@ -119,10 +119,11 @@ RSpec.describe Profiles::NotificationsController do
it 'updates only permitted attributes' do
sign_in(user)
- put :update, params: { user: { notification_email: 'new@example.com', notified_of_own_activity: true, admin: true } }
+ put :update, params: { user: { notification_email: 'new@example.com', email_opted_in: true, notified_of_own_activity: true, admin: true } }
user.reload
expect(user.notification_email).to eq('new@example.com')
+ expect(user.email_opted_in).to eq(true)
expect(user.notified_of_own_activity).to eq(true)
expect(user.admin).to eq(false)
expect(controller).to set_flash[:notice].to('Notification settings saved')
diff --git a/spec/frontend/boards/components/board_column_deprecated_spec.js b/spec/frontend/boards/components/board_column_deprecated_spec.js
index 4466e966302..e6d65e48c3f 100644
--- a/spec/frontend/boards/components/board_column_deprecated_spec.js
+++ b/spec/frontend/boards/components/board_column_deprecated_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import { listObj } from 'jest/boards/mock_data';
@@ -30,6 +30,7 @@ describe('Board Column Component', () => {
const createComponent = ({
listType = ListType.backlog,
collapsed = false,
+ highlighted = false,
withLocalStorage = true,
} = {}) => {
const boardId = '1';
@@ -37,6 +38,7 @@ describe('Board Column Component', () => {
const listMock = {
...listObj,
list_type: listType,
+ highlighted,
collapsed,
};
@@ -91,4 +93,14 @@ describe('Board Column Component', () => {
expect(isCollapsed()).toBe(true);
});
});
+
+ describe('highlighting', () => {
+ it('scrolls to column when highlighted', async () => {
+ createComponent({ highlighted: true });
+
+ await nextTick();
+
+ expect(wrapper.element.scrollIntoView).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/boards/components/board_column_spec.js b/spec/frontend/boards/components/board_column_spec.js
index 1dcdad2b492..79248d53f53 100644
--- a/spec/frontend/boards/components/board_column_spec.js
+++ b/spec/frontend/boards/components/board_column_spec.js
@@ -1,3 +1,4 @@
+import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import { listObj } from 'jest/boards/mock_data';
@@ -66,4 +67,16 @@ describe('Board Column Component', () => {
expect(isCollapsed()).toBe(true);
});
});
+
+ describe('highlighting', () => {
+ it('scrolls to column when highlighted', async () => {
+ createComponent();
+
+ store.state.highlightedLists.push(listObj.id);
+
+ await nextTick();
+
+ expect(wrapper.element.scrollIntoView).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index 21c2356d5cb..1649bab3baf 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -186,7 +186,27 @@ describe('fetchLists', () => {
});
describe('createList', () => {
- it('should dispatch addList action when creating backlog list', (done) => {
+ let commit;
+ let dispatch;
+ let getters;
+ let state;
+
+ beforeEach(() => {
+ state = {
+ fullPath: 'gitlab-org',
+ boardId: '1',
+ boardType: 'group',
+ disabled: false,
+ boardLists: [{ type: 'closed' }],
+ };
+ commit = jest.fn();
+ dispatch = jest.fn();
+ getters = {
+ getListByLabelId: jest.fn(),
+ };
+ });
+
+ it('should dispatch addList action when creating backlog list', async () => {
const backlogList = {
id: 'gid://gitlab/List/1',
listType: 'backlog',
@@ -205,25 +225,35 @@ describe('createList', () => {
}),
);
- const state = {
- fullPath: 'gitlab-org',
- boardId: '1',
- boardType: 'group',
- disabled: false,
- boardLists: [{ type: 'closed' }],
+ await actions.createList({ getters, state, commit, dispatch }, { backlog: true });
+
+ expect(dispatch).toHaveBeenCalledWith('addList', backlogList);
+ });
+
+ it('dispatches highlightList after addList has succeeded', async () => {
+ const list = {
+ id: 'gid://gitlab/List/1',
+ listType: 'label',
+ title: 'Open',
+ labelId: '4',
};
- testAction(
- actions.createList,
- { backlog: true },
- state,
- [],
- [{ type: 'addList', payload: backlogList }],
- done,
- );
+ jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
+ data: {
+ boardListCreate: {
+ list,
+ errors: [],
+ },
+ },
+ });
+
+ await actions.createList({ getters, state, commit, dispatch }, { labelId: '4' });
+
+ expect(dispatch).toHaveBeenCalledWith('addList', list);
+ expect(dispatch).toHaveBeenCalledWith('highlightList', list.id);
});
- it('should commit CREATE_LIST_FAILURE mutation when API returns an error', (done) => {
+ it('should commit CREATE_LIST_FAILURE mutation when API returns an error', async () => {
jest.spyOn(gqlClient, 'mutate').mockReturnValue(
Promise.resolve({
data: {
@@ -235,22 +265,28 @@ describe('createList', () => {
}),
);
- const state = {
- fullPath: 'gitlab-org',
- boardId: '1',
- boardType: 'group',
- disabled: false,
- boardLists: [{ type: 'closed' }],
+ await actions.createList({ getters, state, commit, dispatch }, { backlog: true });
+
+ expect(commit).toHaveBeenCalledWith(types.CREATE_LIST_FAILURE);
+ });
+
+ it('highlights list and does not re-query if it already exists', async () => {
+ const existingList = {
+ id: 'gid://gitlab/List/1',
+ listType: 'label',
+ title: 'Some label',
+ position: 1,
};
- testAction(
- actions.createList,
- { backlog: true },
- state,
- [{ type: types.CREATE_LIST_FAILURE }],
- [],
- done,
- );
+ getters = {
+ getListByLabelId: jest.fn().mockReturnValue(existingList),
+ };
+
+ await actions.createList({ getters, state, commit, dispatch }, { backlog: true });
+
+ expect(dispatch).toHaveBeenCalledWith('highlightList', existingList.id);
+ expect(dispatch).toHaveBeenCalledTimes(1);
+ expect(commit).not.toHaveBeenCalled();
});
});
diff --git a/spec/graphql/types/ci/pipeline_type_spec.rb b/spec/graphql/types/ci/pipeline_type_spec.rb
index d435e337ad7..2a1e030480d 100644
--- a/spec/graphql/types/ci/pipeline_type_spec.rb
+++ b/spec/graphql/types/ci/pipeline_type_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Types::Ci::PipelineType do
id iid sha before_sha status detailed_status config_source duration
coverage created_at updated_at started_at finished_at committed_at
stages user retryable cancelable jobs source_job downstream
- upstream path project active user_permissions
+ upstream path project active user_permissions warnings
]
if Gitlab.ee?
diff --git a/spec/helpers/analytics/unique_visits_helper_spec.rb b/spec/helpers/analytics/unique_visits_helper_spec.rb
index ff363e81ac7..b4b370c169d 100644
--- a/spec/helpers/analytics/unique_visits_helper_spec.rb
+++ b/spec/helpers/analytics/unique_visits_helper_spec.rb
@@ -9,19 +9,6 @@ RSpec.describe Analytics::UniqueVisitsHelper do
let(:target_id) { 'p_analytics_valuestream' }
let(:current_user) { create(:user) }
- before do
- stub_feature_flags(track_unique_visits: true)
- end
-
- it 'does not track visits if feature flag disabled' do
- stub_feature_flags(track_unique_visits: false)
- sign_in(current_user)
-
- expect_any_instance_of(Gitlab::Analytics::UniqueVisits).not_to receive(:track_visit)
-
- helper.track_visit(target_id)
- end
-
it 'does not track visit if user is not logged in' do
expect_any_instance_of(Gitlab::Analytics::UniqueVisits).not_to receive(:track_visit)
diff --git a/spec/lib/gitlab/ci/config/entry/commands_spec.rb b/spec/lib/gitlab/ci/config/entry/commands_spec.rb
index 439799fe973..1b8dfae692a 100644
--- a/spec/lib/gitlab/ci/config/entry/commands_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/commands_spec.rb
@@ -87,18 +87,20 @@ RSpec.describe Gitlab::Ci::Config::Entry::Commands do
describe '#errors' do
it 'saves errors' do
expect(entry.errors)
- .to include 'commands config should be a string or an array containing strings and arrays of strings'
+ .to include 'commands config should be a string or a nested array of strings up to 10 levels deep'
end
end
end
context 'when entry value is multi-level nested array' do
- let(:config) { [['ls', ['echo 1']], 'pwd'] }
+ let(:config) do
+ ['ls 0', ['ls 1', ['ls 2', ['ls 3', ['ls 4', ['ls 5', ['ls 6', ['ls 7', ['ls 8', ['ls 9', ['ls 10']]]]]]]]]]]
+ end
describe '#errors' do
it 'saves errors' do
expect(entry.errors)
- .to include 'commands config should be a string or an array containing strings and arrays of strings'
+ .to include 'commands config should be a string or a nested array of strings up to 10 levels deep'
end
end
diff --git a/spec/lib/gitlab/config/entry/validators/nested_array_helpers_spec.rb b/spec/lib/gitlab/config/entry/validators/nested_array_helpers_spec.rb
new file mode 100644
index 00000000000..cd68307e71f
--- /dev/null
+++ b/spec/lib/gitlab/config/entry/validators/nested_array_helpers_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Config::Entry::Validators::NestedArrayHelpers do
+ let(:config_struct) do
+ Struct.new(:value, keyword_init: true) do
+ include ActiveModel::Validations
+ extend Gitlab::Config::Entry::Validators::NestedArrayHelpers
+
+ validates_each :value do |record, attr, value|
+ unless validate_nested_array(value, 2) { |v| v.is_a?(Integer) }
+ record.errors.add(attr, "is invalid")
+ end
+ end
+ end
+ end
+
+ describe '#validate_nested_array' do
+ let(:config) { config_struct.new(value: value) }
+
+ subject(:errors) { config.errors }
+
+ before do
+ config.valid?
+ end
+
+ context 'with valid values' do
+ context 'with arrays of integers' do
+ let(:value) { [10, 11] }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'with nested arrays of integers' do
+ let(:value) { [10, [11, 12]] }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
+ context 'with invalid values' do
+ subject(:error_messages) { errors.messages }
+
+ context 'with single integers' do
+ let(:value) { 10 }
+
+ it { is_expected.to eq({ value: ['is invalid'] }) }
+ end
+
+ context 'when it is nested over the limit' do
+ let(:value) { [10, [11, [12]]] }
+
+ it { is_expected.to eq({ value: ['is invalid'] }) }
+ end
+
+ context 'when a value in the array is not valid' do
+ let(:value) { [10, 11.5] }
+
+ it { is_expected.to eq({ value: ['is invalid'] }) }
+ end
+
+ context 'when a value in the nested array is not valid' do
+ let(:value) { [10, [11, 12.5]] }
+
+ it { is_expected.to eq({ value: ['is invalid'] }) }
+ end
+ end
+ end
+end