diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-15 21:09:09 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-15 21:09:09 +0300 |
commit | fb994e98ecce2a1f1dfaa87c9c3de8535815813b (patch) | |
tree | 0a0915d6a0dcd22aab31d2f89c72c85597aa5b86 | |
parent | 51858218a3961c6d872703eabde0635bc0a1368f (diff) |
Add latest changes from gitlab-org/gitlab@master
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 |