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: { computed: {
...mapState(['filterParams']), ...mapState(['filterParams', 'highlightedLists']),
...mapGetters(['getIssuesByList']), ...mapGetters(['getIssuesByList']),
highlighted() {
return this.highlightedLists.includes(this.list.id);
},
listIssues() { listIssues() {
return this.getIssuesByList(this.list.id); return this.getIssuesByList(this.list.id);
}, },
@ -48,6 +51,16 @@ export default {
deep: true, deep: true,
immediate: true, immediate: true,
}, },
highlighted: {
handler(highlighted) {
if (highlighted) {
this.$nextTick(() => {
this.$el.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
}
},
immediate: true,
},
}, },
methods: { methods: {
...mapActions(['fetchIssuesForList']), ...mapActions(['fetchIssuesForList']),
@ -68,6 +81,7 @@ export default {
> >
<div <div
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base" 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-header :can-admin-list="canAdminList" :list="list" :disabled="disabled" />
<board-list <board-list

View File

@ -54,6 +54,16 @@ export default {
}, },
deep: true, deep: true,
}, },
'list.highlighted': {
handler(highlighted) {
if (highlighted) {
this.$nextTick(() => {
this.$el.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
}
},
immediate: true,
},
}, },
mounted() { mounted() {
const instance = this; const instance = this;
@ -98,6 +108,7 @@ export default {
> >
<div <div
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base" 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-header :can-admin-list="canAdminList" :list="list" :disabled="disabled" />
<board-list ref="board-list" :disabled="disabled" :issues="listIssues" :list="list" /> <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 NOT_FILTER = 'not[';
export const flashAnimationDuration = 2000;
export default { export default {
BoardType, BoardType,
ListType, ListType,

View File

@ -44,6 +44,7 @@ class List {
this.isExpandable = Boolean(typeInfo.isExpandable); this.isExpandable = Boolean(typeInfo.isExpandable);
this.isExpanded = !obj.collapsed; this.isExpanded = !obj.collapsed;
this.page = 1; this.page = 1;
this.highlighted = obj.highlighted;
this.loading = true; this.loading = true;
this.loadingMore = false; this.loadingMore = false;
this.issues = obj.issues || []; this.issues = obj.issues || [];

View File

@ -1,7 +1,7 @@
import { pick } from 'lodash'; import { pick } from 'lodash';
import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql'; 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 createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql'; import createGqClient, { fetchPolicies } from '~/lib/graphql';
@ -110,9 +110,31 @@ export default {
.catch(() => commit(types.RECEIVE_BOARD_LISTS_FAILURE)); .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 { boardId } = state;
const existingList = getters.getListByLabelId(labelId);
if (existingList) {
dispatch('highlightList', existingList.id);
return;
}
gqlClient gqlClient
.mutate({ .mutate({
mutation: createBoardListMutation, mutation: createBoardListMutation,
@ -130,6 +152,7 @@ export default {
} else { } else {
const list = data.boardListCreate?.list; const list = data.boardListCreate?.list;
dispatch('addList', list); dispatch('addList', list);
dispatch('highlightList', list.id);
} }
}) })
.catch(() => commit(types.CREATE_LIST_FAILURE)); .catch(() => commit(types.CREATE_LIST_FAILURE));

View File

@ -14,7 +14,7 @@ import {
convertObjectPropsToCamelCase, convertObjectPropsToCamelCase,
} from '~/lib/utils/common_utils'; } from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility'; import { mergeUrlParams } from '~/lib/utils/url_utility';
import { ListType } from '../constants'; import { ListType, flashAnimationDuration } from '../constants';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import ListAssignee from '../models/assignee'; import ListAssignee from '../models/assignee';
import ListLabel from '../models/label'; import ListLabel from '../models/label';
@ -106,6 +106,11 @@ const boardsStore = {
list list
.save() .save()
.then(() => { .then(() => {
list.highlighted = true;
setTimeout(() => {
list.highlighted = false;
}, flashAnimationDuration);
// Remove any new issues from the backlog // Remove any new issues from the backlog
// as they will be visible in the new list // as they will be visible in the new list
list.issues.forEach(backlogList.removeIssue.bind(backlogList)); list.issues.forEach(backlogList.removeIssue.bind(backlogList));

View File

@ -28,6 +28,9 @@ export default {
}, },
getListByLabelId: (state) => (labelId) => { getListByLabelId: (state) => (labelId) => {
if (!labelId) {
return null;
}
return find(state.boardLists, (l) => l.label?.id === labelId); 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 ADD_BOARD_ITEM_TO_SELECTION = 'ADD_BOARD_ITEM_TO_SELECTION';
export const REMOVE_BOARD_ITEM_FROM_SELECTION = 'REMOVE_BOARD_ITEM_FROM_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 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) => { [mutationTypes.SET_ADD_COLUMN_FORM_VISIBLE]: (state, visible) => {
state.addColumnFormVisible = 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: {}, filterParams: {},
boardConfig: {}, boardConfig: {},
labels: [], labels: [],
highlightedLists: [],
selectedBoardItems: [], selectedBoardItems: [],
groupProjects: [], groupProjects: [],
groupProjectsFlags: { groupProjectsFlags: {

View File

@ -42,6 +42,7 @@ export default class Profile {
$('#user_notification_email').on('select2-selecting', (event) => { $('#user_notification_email').on('select2-selecting', (event) => {
setTimeout(this.submitForm.bind(event.currentTarget)); setTimeout(this.submitForm.bind(event.currentTarget));
}); });
$('#user_email_opted_in').on('change', this.submitForm);
$('#user_notified_of_own_activity').on('change', this.submitForm); $('#user_notified_of_own_activity').on('change', this.submitForm);
this.form.on('submit', this.onSubmitForm); this.form.on('submit', this.onSubmitForm);
} }

View File

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

View File

@ -172,6 +172,11 @@ export default class MergeRequestStore {
this.canBeMerged = mergeRequest.mergeStatus === 'can_be_merged'; this.canBeMerged = mergeRequest.mergeStatus === 'can_be_merged';
this.canMerge = mergeRequest.userPermissions.canMerge; this.canMerge = mergeRequest.userPermissions.canMerge;
this.ciStatus = pipeline?.status.toLowerCase(); this.ciStatus = pipeline?.status.toLowerCase();
if (pipeline?.warnings && this.ciStatus === 'success') {
this.ciStatus = `${this.ciStatus}-with-warnings`;
}
this.commitsCount = mergeRequest.commitCount || 10; this.commitsCount = mergeRequest.commitCount || 10;
this.branchMissing = !mergeRequest.sourceBranchExists || !mergeRequest.targetBranchExists; this.branchMissing = !mergeRequest.sourceBranchExists || !mergeRequest.targetBranchExists;
this.hasConflicts = mergeRequest.conflicts; this.hasConflicts = mergeRequest.conflicts;

View File

@ -138,6 +138,47 @@
border: 1px solid var(--gray-100, $gray-100); 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 { .board-header {
&.has-border::before { &.has-border::before {
border-top: 3px solid; 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 { .commit.flex-list {
display: flex; display: flex;
} }

View File

@ -29,7 +29,7 @@ class Profiles::NotificationsController < Profiles::ApplicationController
end end
def user_params 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 end
private private

View File

@ -27,6 +27,9 @@ module Types
field :status, PipelineStatusEnum, null: false, field :status, PipelineStatusEnum, null: false,
description: "Status of the pipeline (#{::Ci::Pipeline.all_state_names.compact.join(', ').upcase})" 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, field :detailed_status, Types::Ci::DetailedStatusType, null: false,
description: 'Detailed status of the pipeline.' description: 'Detailed status of the pipeline.'

View File

@ -14,7 +14,6 @@ module Analytics
end end
def track_visit(target_id) def track_visit(target_id)
return unless Feature.enabled?(:track_unique_visits, default_enabled: true)
return unless visitor_id return unless visitor_id
Gitlab::Analytics::UniqueVisits.new.track_visit(visitor_id, target_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) = form.select :notification_email, @user.public_verified_emails, { include_blank: false }, class: "select2", disabled: local_assigns.fetch(:email_change_disabled, nil)
.help-block .help-block
= local_assigns.fetch(:help_text, nil) = 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 title: Improve highlighting for merge diffs
merge_request: 52499 merge_request: 53980
author: author:
type: added 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 # Show full error reports and disable caching
config.active_record.verbose_query_logs = true config.active_record.verbose_query_logs = true
config.consider_all_requests_local = 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 # Show a warning when a large data set is loaded into memory
config.active_record.warn_on_records_fetched_greater_than = 1000 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' milestone: '13.9'
type: development type: development
group: group::source code 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 Permissions for the current user on the resource
""" """
userPermissions: PipelinePermissions! userPermissions: PipelinePermissions!
"""
Indicates if a pipeline has warnings.
"""
warnings: Boolean!
} }
type PipelineAnalytics { type PipelineAnalytics {
@ -20284,6 +20289,11 @@ type Project {
""" """
iids: [ID!] iids: [ID!]
"""
The state of latest requirement test report.
"""
lastTestReportState: TestReportState
""" """
Search query for requirement title. Search query for requirement title.
""" """
@ -20344,6 +20354,11 @@ type Project {
""" """
last: Int last: Int
"""
The state of latest requirement test report.
"""
lastTestReportState: TestReportState
""" """
Search query for requirement title. Search query for requirement title.
""" """

View File

@ -54399,6 +54399,24 @@
}, },
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "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, "inputFields": null,
@ -58805,6 +58823,16 @@
} }
}, },
"defaultValue": null "defaultValue": null
},
{
"name": "lastTestReportState",
"description": "The state of latest requirement test report.",
"type": {
"kind": "ENUM",
"name": "TestReportState",
"ofType": null
},
"defaultValue": null
} }
], ],
"type": { "type": {
@ -58909,6 +58937,16 @@
}, },
"defaultValue": null "defaultValue": null
}, },
{
"name": "lastTestReportState",
"description": "The state of latest requirement test report.",
"type": {
"kind": "ENUM",
"name": "TestReportState",
"ofType": null
},
"defaultValue": null
},
{ {
"name": "after", "name": "after",
"description": "Returns the elements in the list that come after the specified cursor.", "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. | | `upstream` | Pipeline | Pipeline that triggered the pipeline. |
| `user` | User | Pipeline user. | | `user` | User | Pipeline user. |
| `userPermissions` | PipelinePermissions! | Permissions for the current user on the resource | | `userPermissions` | PipelinePermissions! | Permissions for the current user on the resource |
| `warnings` | Boolean! | Indicates if a pipeline has warnings. |
### PipelineAnalytics ### 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. 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. Navigate to **Project > Settings > Access Tokens**.
1. Give the token a name, and select the `api` scope. 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: WARNING:
Never commit the `auth.json` file to your repository. To install packages from a CI/CD job, 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 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). [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: 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 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`. 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 - 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 ```yaml
build: 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 `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 `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` 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: 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 Runners log in to the Dependency Proxy automatically. To pull through
the Dependency Proxy, use the `CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX` the Dependency Proxy, use the `CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX`
environment variable: [predefined CI/CD variable](../../../ci/variables/predefined_variables.md):
```yaml ```yaml
# .gitlab-ci.yml # .gitlab-ci.yml
image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/node:latest 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_USER`: A CI/CD 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_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_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. - `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 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 ### 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. 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. or you can hard code your server's hostname and project's ID.
```xml ```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: user's home location. In this example:
- The user is `root`, because the job runs in a Docker container. - 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 ### 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 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**. under your project's **Settings > CI/CD > Variables**.
## Package naming convention ## 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}` ### `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 ```shell
NPM_TOKEN=<your_token> npm install 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. - This is the email address your notifications are sent to.
- Global notification level - Global notification level
- This is the default [notification level](#notification-levels) which applies to all your notifications. - 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. - Receive notifications about your own activity.
- Check this checkbox if you want to receive notification about your own activity. Default: Not checked. - 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 class Commands < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable include ::Gitlab::Config::Entry::Validatable
MAX_NESTING_LEVEL = 10
validations do 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 end
def value def value
Array(@config).flatten(1) Array(@config).flatten(MAX_NESTING_LEVEL)
end end
end end
end end

View File

@ -268,18 +268,17 @@ module Gitlab
end end
end end
class StringOrNestedArrayOfStringsValidator < NestedArrayOfStringsValidator class StringOrNestedArrayOfStringsValidator < ActiveModel::EachValidator
include LegacyValidationHelpers
include NestedArrayHelpers
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
unless validate_string_or_nested_array_of_strings(value) max_level = options.fetch(:max_level, 1)
record.errors.add(attribute, 'should be a string or an array containing strings and arrays of strings')
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 end
private
def validate_string_or_nested_array_of_strings(values)
validate_string(values) || validate_nested_array_of_strings(values)
end
end end
class TypeValidator < ActiveModel::EachValidator 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 private
def version 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 NEXT_VERSION
else else
VERSION VERSION

View File

@ -31,7 +31,7 @@ module Gitlab
# Skip inline diff if empty line was replaced with content # Skip inline diff if empty line was replaced with content
return if old_line == "" 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) CharDiff.new(old_line, new_line).changed_ranges(offset: offset)
else else
deprecated_diff deprecated_diff

View File

@ -13,5 +13,18 @@ module Gitlab
MIN_GAP = 2 MIN_GAP = 2
NoSpaceLeft = Class.new(StandardError) 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
end end

View File

@ -2,8 +2,6 @@
module Gitlab module Gitlab
module RelativePositioning module RelativePositioning
IllegalRange = Class.new(ArgumentError)
class Range class Range
attr_reader :lhs, :rhs attr_reader :lhs, :rhs
@ -34,18 +32,6 @@ module Gitlab
end end
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 class ClosedRange < RelativePositioning::Range
def initialize(lhs, rhs) def initialize(lhs, rhs)
@lhs, @rhs = lhs, rhs @lhs, @rhs = lhs, rhs

View File

@ -7525,9 +7525,21 @@ msgstr ""
msgid "ComplianceFrameworks|All" msgid "ComplianceFrameworks|All"
msgstr "" 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" msgid "ComplianceFrameworks|Error fetching compliance frameworks data. Please refresh the page"
msgstr "" 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." msgid "ComplianceFrameworks|Once you have created a compliance framework it will appear here."
msgstr "" msgstr ""
@ -7543,6 +7555,9 @@ msgstr ""
msgid "ComplianceFrameworks|Use %{codeStart}::%{codeEnd} to create a %{linkStart}scoped set%{linkEnd} (eg. %{codeStart}SOX::AWS%{codeEnd})" msgid "ComplianceFrameworks|Use %{codeStart}::%{codeEnd} to create a %{linkStart}scoped set%{linkEnd} (eg. %{codeStart}SOX::AWS%{codeEnd})"
msgstr "" msgstr ""
msgid "ComplianceFrameworks|e.g. include-gitlab.ci.yml@group-name/project-name"
msgstr ""
msgid "ComplianceFramework|GDPR" msgid "ComplianceFramework|GDPR"
msgstr "" msgstr ""
@ -24237,6 +24252,9 @@ msgstr ""
msgid "Receive notifications about your own activity" msgid "Receive notifications about your own activity"
msgstr "" msgstr ""
msgid "Receive product marketing emails"
msgstr ""
msgid "Recent" msgid "Recent"
msgstr "" msgstr ""

View File

@ -119,10 +119,11 @@ RSpec.describe Profiles::NotificationsController do
it 'updates only permitted attributes' do it 'updates only permitted attributes' do
sign_in(user) 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 user.reload
expect(user.notification_email).to eq('new@example.com') 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.notified_of_own_activity).to eq(true)
expect(user.admin).to eq(false) expect(user.admin).to eq(false)
expect(controller).to set_flash[:notice].to('Notification settings saved') expect(controller).to set_flash[:notice].to('Notification settings saved')

View File

@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter'; import AxiosMockAdapter from 'axios-mock-adapter';
import Vue from 'vue'; import Vue, { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { listObj } from 'jest/boards/mock_data'; import { listObj } from 'jest/boards/mock_data';
@ -30,6 +30,7 @@ describe('Board Column Component', () => {
const createComponent = ({ const createComponent = ({
listType = ListType.backlog, listType = ListType.backlog,
collapsed = false, collapsed = false,
highlighted = false,
withLocalStorage = true, withLocalStorage = true,
} = {}) => { } = {}) => {
const boardId = '1'; const boardId = '1';
@ -37,6 +38,7 @@ describe('Board Column Component', () => {
const listMock = { const listMock = {
...listObj, ...listObj,
list_type: listType, list_type: listType,
highlighted,
collapsed, collapsed,
}; };
@ -91,4 +93,14 @@ describe('Board Column Component', () => {
expect(isCollapsed()).toBe(true); 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 { shallowMount } from '@vue/test-utils';
import { listObj } from 'jest/boards/mock_data'; import { listObj } from 'jest/boards/mock_data';
@ -66,4 +67,16 @@ describe('Board Column Component', () => {
expect(isCollapsed()).toBe(true); 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', () => { 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 = { const backlogList = {
id: 'gid://gitlab/List/1', id: 'gid://gitlab/List/1',
listType: 'backlog', listType: 'backlog',
@ -205,25 +225,35 @@ describe('createList', () => {
}), }),
); );
const state = { await actions.createList({ getters, state, commit, dispatch }, { backlog: true });
fullPath: 'gitlab-org',
boardId: '1',
boardType: 'group',
disabled: false,
boardLists: [{ type: 'closed' }],
};
testAction( expect(dispatch).toHaveBeenCalledWith('addList', backlogList);
actions.createList,
{ backlog: true },
state,
[],
[{ type: 'addList', payload: backlogList }],
done,
);
}); });
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( jest.spyOn(gqlClient, 'mutate').mockReturnValue(
Promise.resolve({ Promise.resolve({
data: { data: {
@ -235,22 +265,28 @@ describe('createList', () => {
}), }),
); );
const state = { await actions.createList({ getters, state, commit, dispatch }, { backlog: true });
fullPath: 'gitlab-org',
boardId: '1', expect(commit).toHaveBeenCalledWith(types.CREATE_LIST_FAILURE);
boardType: 'group', });
disabled: false,
boardLists: [{ type: 'closed' }], 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( getters = {
actions.createList, getListByLabelId: jest.fn().mockReturnValue(existingList),
{ backlog: true }, };
state,
[{ type: types.CREATE_LIST_FAILURE }], await actions.createList({ getters, state, commit, dispatch }, { backlog: true });
[],
done, 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 id iid sha before_sha status detailed_status config_source duration
coverage created_at updated_at started_at finished_at committed_at coverage created_at updated_at started_at finished_at committed_at
stages user retryable cancelable jobs source_job downstream stages user retryable cancelable jobs source_job downstream
upstream path project active user_permissions upstream path project active user_permissions warnings
] ]
if Gitlab.ee? if Gitlab.ee?

View File

@ -9,19 +9,6 @@ RSpec.describe Analytics::UniqueVisitsHelper do
let(:target_id) { 'p_analytics_valuestream' } let(:target_id) { 'p_analytics_valuestream' }
let(:current_user) { create(:user) } 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 it 'does not track visit if user is not logged in' do
expect_any_instance_of(Gitlab::Analytics::UniqueVisits).not_to receive(:track_visit) 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 describe '#errors' do
it 'saves errors' do it 'saves errors' do
expect(entry.errors) 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 end
end end
context 'when entry value is multi-level nested array' do 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 describe '#errors' do
it 'saves errors' do it 'saves errors' do
expect(entry.errors) 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 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