Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-02-15 18:09:09 +00:00
parent 51858218a3
commit fb994e98ec
53 changed files with 517 additions and 102 deletions

View File

@ -1 +1 @@
c886f7f37533e8ed19e245a83f363086daf590e9
29dec5fdae0846da19f803058441581b43fda91d

View File

@ -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

View File

@ -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" />

View File

@ -34,6 +34,8 @@ export const LIST = 'list';
export const NOT_FILTER = 'not[';
export const flashAnimationDuration = 2000;
export default {
BoardType,
ListType,

View File

@ -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 || [];

View File

@ -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));

View File

@ -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));

View File

@ -28,6 +28,9 @@ export default {
},
getListByLabelId: (state) => (labelId) => {
if (!labelId) {
return null;
}
return find(state.boardLists, (l) => l.label?.id === labelId);
},

View File

@ -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';

View File

@ -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);
},
};

View File

@ -15,6 +15,7 @@ export default () => ({
filterParams: {},
boardConfig: {},
labels: [],
highlightedLists: [],
selectedBoardItems: [],
groupProjects: [],
groupProjectsFlags: {

View File

@ -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);
}

View File

@ -14,6 +14,7 @@ query getState($projectPath: ID!, $iid: String!) {
pipelines(first: 1) {
nodes {
status
warnings
}
}
shouldBeRebased

View File

@ -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;

View File

@ -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;

View File

@ -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;
}

View File

@ -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

View File

@ -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.'

View File

@ -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)

View File

@ -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')

View File

@ -1,5 +1,5 @@
---
title: Improve highlighting for merge diffs
merge_request: 52499
merge_request: 53980
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Add user setting for opting into marketing emails
merge_request: 53921
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Accept deeply nested arrays for CI script keyword
merge_request: 53737
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Added warnings field to the pipelines GraphQL type
merge_request: 54089
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Highlight board lists when they are added
merge_request: 53779
author:
type: changed

View File

@ -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

View File

@ -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

View File

@ -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 {
@ -20284,6 +20289,11 @@ type Project {
"""
iids: [ID!]
"""
The state of latest requirement test report.
"""
lastTestReportState: TestReportState
"""
Search query for requirement title.
"""
@ -20344,6 +20354,11 @@ type Project {
"""
last: Int
"""
The state of latest requirement test report.
"""
lastTestReportState: TestReportState
"""
Search query for requirement title.
"""

View File

@ -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": {
@ -58909,6 +58937,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.",

View File

@ -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

View File

@ -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.

View File

@ -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).

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -268,18 +268,17 @@ module Gitlab
end
end
class StringOrNestedArrayOfStringsValidator < NestedArrayOfStringsValidator
class StringOrNestedArrayOfStringsValidator < ActiveModel::EachValidator
include LegacyValidationHelpers
include NestedArrayHelpers
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')
max_level = options.fetch(:max_level, 1)
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
private
def validate_string_or_nested_array_of_strings(values)
validate_string(values) || validate_nested_array_of_strings(values)
end
end
class TypeValidator < ActiveModel::EachValidator

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 ""

View File

@ -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')

View File

@ -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();
});
});
});

View File

@ -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();
});
});
});

View File

@ -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 });
testAction(
actions.createList,
{ backlog: true },
state,
[],
[{ type: 'addList', payload: backlogList }],
done,
);
expect(dispatch).toHaveBeenCalledWith('addList', backlogList);
});
it('should commit CREATE_LIST_FAILURE mutation when API returns an error', (done) => {
it('dispatches highlightList after addList has succeeded', async () => {
const list = {
id: 'gid://gitlab/List/1',
listType: 'label',
title: 'Open',
labelId: '4',
};
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', 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();
});
});

View File

@ -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?

View File

@ -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)

View File

@ -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

View File

@ -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