Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-08-12 12:11:32 +00:00
parent 94a5041917
commit ee772e0c77
59 changed files with 526 additions and 286 deletions

View File

@ -12,6 +12,21 @@
## Proposal
## Additional details
<!--
_NOTE: If the issue has addressed all of these questions, this separate section can be removed._
-->
Some relevant technical details, if applicable, such as:
- Does this need a ~"feature flag"?
- Is there an example response showing the data structure that should be returned (new endpoints only)?
- What permissions should be used?
- Is this EE or CE?
- [ ] EE
- [ ] CE
- Additional comments:
## Implementation Table
<!--

View File

@ -258,16 +258,6 @@ Layout/HashAlignment:
- 'lib/tasks/gitlab/import_export/export.rake'
- 'lib/tasks/gitlab/import_export/import.rake'
- 'lib/tasks/tanuki_emoji.rake'
- 'qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/13_secure/change_vulnerability_status_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/13_secure/security_reports_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/1_manage/group/group_saml_enforced_sso_git_access_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/1_manage/group/group_saml_non_enforced_sso_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/3_create/repository/project_templates_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/3_create/repository/push_rules_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/5_package/dependency_proxy_sso_spec.rb'
- 'qa/qa/support/loglinking.rb'
- 'qa/spec/support/loglinking_spec.rb'
- 'spec/controllers/concerns/product_analytics_tracking_spec.rb'
- 'spec/controllers/concerns/redis_tracking_spec.rb'
- 'spec/controllers/import/bitbucket_controller_spec.rb'

View File

@ -1,5 +1,13 @@
<script>
import { GlAlert, GlButton, GlDrawer, GlFormGroup, GlFormInput, GlFormSelect } from '@gitlab/ui';
import {
GlAlert,
GlButton,
GlDrawer,
GlFormCheckbox,
GlFormGroup,
GlFormInput,
GlFormSelect,
} from '@gitlab/ui';
import { get as getPropValueByPath, isEmpty } from 'lodash';
import { produce } from 'immer';
import { MountingPortal } from 'portal-vue';
@ -26,6 +34,7 @@ export default {
GlAlert,
GlButton,
GlDrawer,
GlFormCheckbox,
GlFormGroup,
GlFormInput,
GlFormSelect,
@ -113,7 +122,9 @@ export default {
const { fields, model } = this;
return fields.some((field) => {
return field.required && isEmpty(model[field.name]);
return (
field.required && isEmpty(model[field.name]) && typeof model[field.name] !== 'boolean'
);
});
},
variables() {
@ -216,6 +227,8 @@ export default {
});
},
getFieldLabel(field) {
if (field.bool) return null;
const optionalSuffix = field.required ? '' : ` ${MSG_OPTIONAL}`;
return field.label + optionalSuffix;
},
@ -273,6 +286,9 @@ export default {
v-model="model[field.name]"
:options="field.values"
/>
<gl-form-checkbox v-else-if="field.bool" :id="field.name" v-model="model[field.name]"
><span class="gl-font-weight-bold">{{ field.label }}</span></gl-form-checkbox
>
<gl-form-input v-else :id="field.name" v-bind="field.input" v-model="model[field.name]" />
</gl-form-group>
<span class="gl-float-right">

View File

@ -74,7 +74,7 @@ export default {
return { groupId: this.groupGraphQLId };
},
fields() {
return [
const fields = [
{ name: 'firstName', label: __('First name'), required: true },
{ name: 'lastName', label: __('Last name'), required: true },
{ name: 'email', label: __('Email'), required: true },
@ -86,6 +86,11 @@ export default {
},
{ name: 'description', label: __('Description') },
];
if (this.isEditMode)
fields.push({ name: 'active', label: s__('Crm|Active'), required: true, bool: true });
return fields;
},
organizationSelectValues() {
const values = this.organizations.map((o) => {

View File

@ -6,6 +6,7 @@ fragment ContactFragment on CustomerRelationsContact {
email
phone
description
active
organization {
__typename
id

View File

@ -4,4 +4,5 @@ fragment OrganizationFragment on CustomerRelationsOrganization {
name
defaultRate
description
active
}

View File

@ -52,16 +52,23 @@ export default {
additionalCreateParams() {
return { groupId: this.groupGraphQLId };
},
},
fields: [
{ name: 'name', label: __('Name'), required: true },
{
name: 'defaultRate',
label: s__('Crm|Default rate'),
input: { type: 'number', step: '0.01' },
fields() {
const fields = [
{ name: 'name', label: __('Name'), required: true },
{
name: 'defaultRate',
label: s__('Crm|Default rate'),
input: { type: 'number', step: '0.01' },
},
{ name: 'description', label: __('Description') },
];
if (this.isEditMode)
fields.push({ name: 'active', label: s__('Crm|Active'), required: true, bool: true });
return fields;
},
{ name: 'description', label: __('Description') },
],
},
};
</script>
@ -73,7 +80,7 @@ export default {
:mutation="mutation"
:additional-create-params="additionalCreateParams"
:existing-id="organizationGraphQLId"
:fields="$options.fields"
:fields="fields"
:title="title"
:success-message="successMessage"
/>

View File

@ -104,12 +104,9 @@ export default {
class="d-inline-flex mb-2"
/>
<gl-button-group class="gl-ml-4 gl-mb-4" data-testid="commit-sha-group">
<gl-button
label
class="gl-font-monospace"
data-testid="commit-sha-short-id"
v-text="commit.short_id"
/>
<gl-button label class="gl-font-monospace" data-testid="commit-sha-short-id">{{
commit.short_id
}}</gl-button>
<modal-copy-button
:text="commit.id"
:title="__('Copy commit SHA')"

View File

@ -18,6 +18,7 @@ export default {
<div class="context-header ide-context-header">
<a :href="project.web_url" :title="s__('IDE|Go to project')" data-testid="go-to-project-link">
<project-avatar
:project-id="project.id"
:project-name="project.name"
:project-avatar-url="project.avatar_url"
:size="48"

View File

@ -125,6 +125,7 @@ export default {
>
<project-avatar
class="gl-mr-3"
:project-id="item.id"
:project-avatar-url="item.avatar_url"
:project-name="item.name"
aria-hidden="true"

View File

@ -189,25 +189,23 @@ export default {
v-if="hasEnvironment"
:href="environmentLink.link"
data-testid="job-environment-link"
v-text="environmentLink.name"
/>
>{{ environmentLink.name }}</gl-link
>
</template>
<template #clusterNameOrLink>
<gl-link
v-if="clusterNameOrLink.path"
:href="clusterNameOrLink.path"
data-testid="job-cluster-link"
v-text="clusterNameOrLink.name"
/>
>{{ clusterNameOrLink.name }}</gl-link
>
<template v-else>{{ clusterNameOrLink.name }}</template>
</template>
<template #kubernetesNamespace>{{ kubernetesNamespace }}</template>
<template #deploymentLink>
<gl-link
:href="deploymentLink.path"
data-testid="job-deployment-link"
v-text="deploymentLink.name"
/>
<gl-link :href="deploymentLink.path" data-testid="job-deployment-link">{{
deploymentLink.name
}}</gl-link>
</template>
</gl-sprintf>
</p>

View File

@ -350,7 +350,7 @@ function linesFromSelection(textArea) {
* @param {Object} textArea - the targeted text area
* @param {Number} selectionStart - start position of original selection
* @param {Number} selectionEnd - end position of original selection
* @param {Number} startPos - start pos of first line
* @param {Number} lineStart - start pos of first line
* @param {Number} firstLineChange - number of characters changed on first line
* @param {Number} totalChanged - total number of characters changed
*/
@ -358,23 +358,20 @@ function setNewSelectionRange(
textArea,
selectionStart,
selectionEnd,
startPos,
lineStart,
firstLineChange,
totalChanged,
) {
let newStart = Math.max(lineStart, selectionStart + firstLineChange);
let newEnd = Math.max(lineStart, selectionEnd + totalChanged);
if (selectionStart === selectionEnd) {
textArea.setSelectionRange(
Math.max(0, selectionStart + firstLineChange),
Math.max(0, selectionEnd + firstLineChange),
);
} else if (selectionStart === startPos) {
textArea.setSelectionRange(selectionStart, Math.max(0, selectionEnd + totalChanged));
} else {
textArea.setSelectionRange(
Math.max(0, selectionStart + firstLineChange),
Math.max(0, selectionEnd + totalChanged),
);
newEnd = newStart;
} else if (selectionStart === lineStart) {
newStart = lineStart;
}
textArea.setSelectionRange(newStart, newEnd);
}
/**
@ -390,10 +387,8 @@ function indentLines(textArea) {
textArea.setSelectionRange(startPos, endPos);
lines.forEach((line) => {
if (line.length > 0) {
line = INDENT_CHAR.repeat(INDENT_LENGTH) + line;
totalAdded += INDENT_LENGTH;
}
line = INDENT_CHAR.repeat(INDENT_LENGTH) + line;
totalAdded += INDENT_LENGTH;
shiftedLines.push(line);
});
@ -439,7 +434,8 @@ function outdentLines(textArea) {
const textToInsert = shiftedLines.join('\n');
insertText(textArea, textToInsert);
if (totalRemoved > 0) insertText(textArea, textToInsert);
setNewSelectionRange(
textArea,
selectionStart,

View File

@ -166,11 +166,9 @@ export default {
<div class="card card-slim gl-overflow-hidden">
<div
:class="{ 'panel-empty-heading border-bottom-0': !hasBody, 'gl-border-b-0': !isOpen }"
class="card-header gl-display-flex gl-justify-content-space-between gl-align-items-center gl-bg-gray-10"
class="gl-display-flex gl-justify-content-space-between gl-line-height-24 gl-py-3 gl-px-5 gl-bg-gray-10 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
>
<h3
class="card-title h5 position-relative gl-my-0 gl-display-flex gl-align-items-center gl-h-7"
>
<h3 class="card-title h5 gl-my-0 gl-display-flex gl-align-items-center gl-flex-grow-1">
<gl-link
id="user-content-related-issues"
class="anchor position-absolute gl-text-decoration-none"
@ -189,28 +187,30 @@ export default {
<gl-icon name="question" :size="12" />
</gl-link>
<div class="gl-display-inline-flex">
<div class="js-related-issues-header-issue-count gl-display-inline-flex gl-mx-5">
<span class="gl-display-inline-flex gl-align-items-center">
<gl-icon :name="issuableTypeIcon" class="gl-mr-2 gl-text-gray-500" />
{{ badgeLabel }}
</span>
</div>
<gl-button
v-if="canAdmin"
data-qa-selector="related_issues_plus_button"
data-testid="add-button"
icon="plus"
:aria-label="addIssuableButtonText"
:class="qaClass"
@click="$emit('toggleAddRelatedIssuesForm', $event)"
/>
<div class="js-related-issues-header-issue-count gl-display-inline-flex gl-mx-3">
<span class="gl-display-inline-flex gl-align-items-center">
<gl-icon :name="issuableTypeIcon" class="gl-mr-2 gl-text-gray-500" />
{{ badgeLabel }}
</span>
</div>
</h3>
<slot name="header-actions"></slot>
<div class="gl-pl-4 gl-ml-3">
<gl-button
v-if="canAdmin"
size="small"
data-qa-selector="related_issues_plus_button"
data-testid="related-issues-plus-button"
:aria-label="addIssuableButtonText"
:class="qaClass"
class="gl-ml-3"
@click="$emit('toggleAddRelatedIssuesForm', $event)"
>
<slot name="add-button-text">{{ __('Add') }}</slot>
</gl-button>
<div class="gl-pl-3 gl-ml-3 gl-border-l-1 gl-border-l-solid gl-border-l-gray-100">
<gl-button
category="tertiary"
size="small"
:icon="toggleIcon"
:aria-label="toggleLabel"
data-testid="toggle-links"

View File

@ -196,12 +196,9 @@ export default {
</gl-link>
</div>
<gl-button-group class="gl-ml-4 js-commit-sha-group">
<gl-button
label
class="gl-font-monospace"
data-testid="last-commit-id-label"
v-text="showCommitId"
/>
<gl-button label class="gl-font-monospace" data-testid="last-commit-id-label">{{
showCommitId
}}</gl-button>
<clipboard-button
:text="commit.sha"
:title="__('Copy commit SHA')"

View File

@ -1,5 +1,6 @@
<script>
import { GlAvatar } from '@gitlab/ui';
import { getIdFromGraphQLId, isGid } from '~/graphql_shared/utils';
import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
export default {
@ -8,9 +9,12 @@ export default {
},
props: {
projectId: {
type: Number,
type: [Number, String],
default: 0,
required: false,
validator(value) {
return typeof value === 'string' ? isGid(value) : true;
},
},
projectName: {
type: String,
@ -36,6 +40,9 @@ export default {
avatarAlt() {
return this.alt ?? this.projectName;
},
entityId() {
return isGid(this.projectId) ? getIdFromGraphQLId(this.projectId) : this.projectId;
},
},
AVATAR_SHAPE_OPTION_RECT,
};
@ -44,7 +51,7 @@ export default {
<template>
<gl-avatar
:shape="$options.AVATAR_SHAPE_OPTION_RECT"
:entity-id="projectId"
:entity-id="entityId"
:entity-name="projectName"
:src="projectAvatarUrl"
:alt="avatarAlt"

View File

@ -53,6 +53,7 @@ export default {
>
<gl-icon v-if="selected" class="js-selected-icon" name="mobile-issue-close" />
<project-avatar
:project-id="project.id"
:project-avatar-url="projectAvatarUrl"
:project-name="projectNameWithNamespace"
class="gl-mr-3"

View File

@ -244,7 +244,7 @@ export default {
>
{{ $options.i18n.addChildButtonLabel }}
</gl-button>
<div class="gl-border-l-1 gl-border-l-solid gl-border-l-gray-50 gl-pl-3 gl-ml-3">
<div class="gl-border-l-1 gl-border-l-solid gl-border-l-gray-100 gl-pl-3 gl-ml-3">
<gl-button
category="tertiary"
size="small"

View File

@ -11,7 +11,6 @@ class Admin::ApplicationsController < Admin::ApplicationController
def index
applications = ApplicationsFinder.new.execute
@applications = Kaminari.paginate_array(applications).page(params[:page])
@application_counts = OauthAccessToken.distinct_resource_owner_counts(@applications)
end
def show

View File

@ -4,6 +4,7 @@ module Projects
module Pipelines
class StagesController < Projects::Pipelines::ApplicationController
before_action :authorize_update_pipeline!
before_action :stage, only: [:play_manual]
urgency :low, [
:play_manual
@ -26,7 +27,7 @@ module Projects
private
def stage
@pipeline_stage ||= pipeline.find_stage_by_name!(params[:stage_name])
@stage ||= pipeline.stage(params[:stage_name]).presence || render_404
end
end
end

View File

@ -1224,10 +1224,6 @@ module Ci
stages.find_by(name: name)
end
def find_stage_by_name!(name)
stages.find_by!(name: name)
end
def full_error_messages
errors ? errors.full_messages.to_sentence : ""
end

View File

@ -6,7 +6,6 @@ class OauthAccessToken < Doorkeeper::AccessToken
alias_attribute :user, :resource_owner
scope :distinct_resource_owner_counts, ->(applications) { where(application: applications).distinct.group(:application_id).count(:resource_owner_id) }
scope :latest_per_application, -> { select('distinct on(application_id) *').order(application_id: :desc, created_at: :desc) }
scope :preload_application, -> { preload(:application) }

View File

@ -4,6 +4,7 @@ module Integrations
class ProjectEntity < Grape::Entity
include RequestAwareEntity
expose :id
expose :avatar_url
expose :full_name
expose :name

View File

@ -28,8 +28,6 @@
= _('Name')
%th
= _('Callback URL')
%th
= _('Clients')
%th
= _('Trusted')
%th
@ -41,7 +39,6 @@
%tr{ id: "application_#{application.id}" }
%td= link_to application.name, admin_application_path(application)
%td= application.redirect_uri
%td= @application_counts[application.id].to_i
%td= application.trusted? ? _('Yes'): _('No')
%td= application.confidential? ? _('Yes'): _('No')
%td= link_to 'Edit', edit_admin_application_path(application), class: 'gl-button btn btn-link'

View File

@ -230,6 +230,96 @@ In the upstream pipeline:
artifacts: true
```
#### Pass artifacts to a downstream pipeline
You can pass artifacts to a downstream pipeline by using [`needs:project`](../yaml/index.md#needsproject).
1. In a job in the upstream pipeline, save the artifacts using the [`artifacts`](../yaml/index.md#artifacts) keyword.
1. Trigger the downstream pipeline with a trigger job:
```yaml
build_artifacts:
stage: build
script:
- echo "This is a test artifact!" >> artifact.txt
artifacts:
paths:
- artifact.txt
deploy:
stage: deploy
trigger: my/downstream_project
```
1. In a job in the downstream pipeline, fetch the artifacts from the upstream pipeline
by using `needs:project`. Set `job` to the job in the upstream pipeline to fetch artifacts from,
`ref` to the branch, and `artifacts: true`.
```yaml
test:
stage: test
script:
- cat artifact.txt
needs:
- project: my/upstream_project
job: build_artifacts
ref: main
artifacts: true
```
#### Pass artifacts to a downstream pipeline from a Merge Request pipeline
When you use `needs:project` to [pass artifacts to a downstream pipeline](#pass-artifacts-to-a-downstream-pipeline),
the `ref` value is usually a branch name, like `main` or `development`.
For merge request pipelines, the `ref` value is in the form of `refs/merge-requests/<id>/head`,
where `id` is the merge request ID. You can retrieve this ref with the [`CI_MERGE_REQUEST_REF_PATH`](../variables/predefined_variables.md#predefined-variables-for-merge-request-pipelines)
CI/CD variable. Do not use a branch name as the `ref` with merge request pipelines,
because the downstream pipeline attempts to fetch artifacts from the latest branch pipeline.
To fetch the artifacts from the upstream `merge request` pipeline instead of the `branch` pipeline,
pass this variable to the downstream pipeline using variable inheritance:
1. In a job in the upstream pipeline, save the artifacts using the [`artifacts`](../yaml/index.md#artifacts) keyword.
1. In the job that triggers the downstream pipeline, pass the `$CI_MERGE_REQUEST_REF_PATH` variable by using
[variable inheritance](#pass-cicd-variables-to-a-downstream-pipeline-by-using-the-variables-keyword):
```yaml
build_artifacts:
stage: build
script:
- echo "This is a test artifact!" >> artifact.txt
artifacts:
paths:
- artifact.txt
upstream_job:
variables:
UPSTREAM_REF: $CI_MERGE_REQUEST_REF_PATH
trigger:
project: my/downstream_project
branch: my-branch
```
1. In a job in the downstream pipeline, fetch the artifacts from the upstream pipeline
by using `needs:project`. Set the `ref` to the `UPSTREAM_REF` variable, and `job`
to the job in the upstream pipeline to fetch artifacts from:
```yaml
test:
stage: test
script:
- cat artifact.txt
needs:
- project: my/upstream_project
job: build_artifacts
ref: UPSTREAM_REF
artifacts: true
```
This method works for fetching artifacts from a regular merge request parent pipeline,
but fetching artifacts from [merge results](merged_results_pipelines.md) pipelines is not supported.
#### Use `rules` or `only`/`except` with multi-project pipelines
You can use CI/CD variables or the [`rules`](../yaml/index.md#rulesif) keyword to

View File

@ -289,10 +289,10 @@ smoke-test-job:
In `include` sections in your `.gitlab-ci.yml` file, you can use:
- [Project variables](../variables/index.md#add-a-cicd-variable-to-a-project)
- [Group variables](../variables/index.md#add-a-cicd-variable-to-a-group)
- [Instance variables](../variables/index.md#add-a-cicd-variable-to-an-instance)
- Project [predefined variables](../variables/predefined_variables.md)
- [Project variables](../variables/index.md#add-a-cicd-variable-to-a-project).
- [Group variables](../variables/index.md#add-a-cicd-variable-to-a-group).
- [Instance variables](../variables/index.md#add-a-cicd-variable-to-an-instance).
- Project [predefined variables](../variables/predefined_variables.md).
- In GitLab 14.2 and later, the `$CI_COMMIT_REF_NAME` [predefined variable](../variables/predefined_variables.md).
When used in `include`, the `CI_COMMIT_REF_NAME` variable returns the full
@ -322,7 +322,11 @@ include:
file: '.compliance-gitlab-ci.yml'
```
For an example of how you can include these predefined variables, and the variables' impact on CI/CD jobs,
You cannot use variables defined in jobs, or in a global [`variables`](../yaml/index.md#variables)
section which defines the default variables for all jobs. Includes are evaluated before jobs,
so these variables cannot be used with `include`.
For an example of how you can include predefined variables, and the variables' impact on CI/CD jobs,
see this [CI/CD variable demo](https://youtu.be/4XR8gw3Pkos).
## Use `rules` with `include`

View File

@ -171,11 +171,11 @@ Use `include:local` instead of symbolic links.
**Possible inputs**:
- A full path relative to the root directory (`/`).
A full path relative to the root directory (`/`):
- The YAML file must have the extension `.yml` or `.yaml`.
- You can [use `*` and `**` wildcards in the file path](includes.md#use-includelocal-with-wildcard-file-paths).
CI/CD variables [are supported](../variables/where_variables_can_be_used.md#gitlab-ciyml-file).
- You can use [certain CI/CD variables](includes.md#use-variables-with-include).
**Example of `include:local`**:
@ -208,10 +208,10 @@ use `include:file`. You can use `include:file` in combination with `include:proj
**Possible inputs**:
- A full path, relative to the root directory (`/`). The YAML file must have the
extension `.yml` or `.yaml`.
A full path, relative to the root directory (`/`):
CI/CD variables [are supported](../variables/where_variables_can_be_used.md#gitlab-ciyml-file).
- The YAML file must have the extension `.yml` or `.yaml`.
- You can use [certain CI/CD variables](includes.md#use-variables-with-include).
**Example of `include:file`**:
@ -268,10 +268,11 @@ Use `include:remote` with a full URL to include a file from a different location
**Possible inputs**:
- A public URL accessible by an HTTP/HTTPS `GET` request. Authentication with the
remote URL is not supported. The YAML file must have the extension `.yml` or `.yaml`.
A public URL accessible by an HTTP/HTTPS `GET` request:
CI/CD variables [are supported](../variables/where_variables_can_be_used.md#gitlab-ciyml-file).
- Authentication with the remote URL is not supported.
- The YAML file must have the extension `.yml` or `.yaml`.
- You can use [certain CI/CD variables](includes.md#use-variables-with-include).
**Example of `include:remote`**:
@ -296,9 +297,12 @@ Use `include:template` to include [`.gitlab-ci.yml` templates](https://gitlab.co
**Possible inputs**:
- [`.gitlab-ci.yml` templates](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates).
A [CI/CD template](../examples/index.md#cicd-templates):
CI/CD variables [are supported](../variables/where_variables_can_be_used.md#gitlab-ciyml-file).
- Templates are stored in [`lib/gitlab/ci/templates`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates).
Not all templates are designed to be used with `include:template`, so check template
comments before using one.
- You can use [certain CI/CD variables](includes.md#use-variables-with-include).
**Example of `include:template`**:

View File

@ -147,7 +147,7 @@ Best practices for [client-side logging](logging.md) for GitLab frontend develop
## [Internationalization (i18n) and Translations](../i18n/externalization.md)
Frontend internationalization support is described in [this document](../i18n/).
Frontend internationalization support is described in [this document](../i18n/index.md).
The [externalization part of the guide](../i18n/externalization.md) explains the helpers/methods available.
## [Troubleshooting](troubleshooting.md)

View File

@ -426,6 +426,21 @@ Feature.enabled?(:a_feature, project) && Feature.disabled?(:a_feature_override,
/chatops run feature set --project=gitlab-org/gitlab a_feature_override true
```
#### Percentage-based actor selection
When using the percentage rollout of actors on multiple feature flags, the actors for each feature flag are selected separately.
For example, the following feature flags are enabled for a certain percentage of actors:
```plaintext
/chatops run chatops feature set feature-set-1 25 --actors
/chatops run chatops feature set feature-set-2 25 --actors
```
If a project A has `:feature-set-1` enabled, there is no guarantee that project A also has `:feature-set-2` enabled.
For more detail, see [This is how percentages work in Flipper](https://www.hackwithpassion.com/this-is-how-percentages-work-in-flipper).
#### Use actors for verifying in production
WARNING:

View File

@ -14,8 +14,8 @@ When implementing new features, please refer to these existing features to avoid
- [Merge request Templates](../user/project/description_templates.md#create-a-merge-request-template): `.gitlab/merge_request_templates/`.
- [GitLab agent](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/doc/configuration_repository.md#layout): `.gitlab/agents/`.
- [CODEOWNERS](../user/project/code_owners.md#set-up-code-owners): `.gitlab/CODEOWNERS`.
- [Route Maps](../ci/review_apps/#route-maps): `.gitlab/route-map.yml`.
- [Route Maps](../ci/review_apps/index.md#route-maps): `.gitlab/route-map.yml`.
- [Customize Auto DevOps Helm Values](../topics/autodevops/customize.md#customize-values-for-helm-chart): `.gitlab/auto-deploy-values.yaml`.
- [Insights](../user/project/insights/index.md#configure-your-insights): `.gitlab/insights.yml`.
- [Service Desk Templates](../user/project/service_desk.md#using-customized-email-templates): `.gitlab/service_desk_templates/`.
- [Web IDE](../user/project/web_ide/#web-ide-configuration-file): `.gitlab/.gitlab-webide.yml`.
- [Web IDE](../user/project/web_ide/index.md#web-ide-configuration-file): `.gitlab/.gitlab-webide.yml`.

View File

@ -314,7 +314,7 @@ This documentation gives an overview of the report JSON format,
as well as recommendations and examples to help integrators set its fields.
The format is extensively described in the documentation of
[SAST](../../user/application_security/sast/index.md#reports-json-format),
[DAST](../../user/application_security/dast/#reports),
[DAST](../../user/application_security/dast/index.md#reports),
[Dependency Scanning](../../user/application_security/dependency_scanning/index.md#reports-json-format),
and [Container Scanning](../../user/application_security/container_scanning/index.md#reports-json-format)

View File

@ -169,7 +169,7 @@ MultiStore uses two feature flags to control the actual migration:
- `use_primary_and_secondary_stores_for_[store_name]`
- `use_primary_store_as_default_for_[store_name]`
For example, if our new Redis instance is called `Gitlab::Redis::Foo`, we can [create](../../../ee/development/feature_flags/#create-a-new-feature-flag) two feature flags by executing:
For example, if our new Redis instance is called `Gitlab::Redis::Foo`, we can [create](../feature_flags/index.md#create-a-new-feature-flag) two feature flags by executing:
```shell
bin/feature-flag use_primary_and_secondary_stores_for_foo

View File

@ -28,7 +28,7 @@ others - particularly [latency-sensitive jobs](worker_attributes.md#latency-sens
this will result in a poor user experience.
This only applies to new worker classes when they are first introduced.
As we recommend [using feature flags](../feature_flags/) as a general
As we recommend [using feature flags](../feature_flags/index.md) as a general
development process, it's best to control the entire change (including
scheduling of the new Sidekiq worker) with a feature flag.

View File

@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Writing consumer tests
This tutorial guides you through writing a consumer test from scratch. To start, the consumer tests are written using [`jest-pact`](https://github.com/pact-foundation/jest-pact) that builds on top of [`pact-js`](https://github.com/pact-foundation/pact-js). This tutorial shows you how to write a consumer test for the `/discussions.json` endpoint, which is actually `/:namespace_name/:project_name/-/merge_requests/:id/discussions.json`.
This tutorial guides you through writing a consumer test from scratch. To start, the consumer tests are written using [`jest-pact`](https://github.com/pact-foundation/jest-pact) that builds on top of [`pact-js`](https://github.com/pact-foundation/pact-js). This tutorial shows you how to write a consumer test for the `/discussions.json` REST API endpoint, which is actually `/:namespace_name/:project_name/-/merge_requests/:id/discussions.json`. For an example of a GraphQL consumer test, see [`spec/contracts/consumer/specs/project/pipeline/show.spec.js`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/spec/contracts/consumer/specs/project/pipeline/show.spec.js).
## Create the skeleton
@ -24,7 +24,7 @@ To learn more about how the contract test directory is structured, see the contr
The Pact consumer test is defined through the `pactWith` function that takes `PactOptions` and the `PactFn`.
```javascript
const { pactWith } = require('jest-pact');
import { pactWith } from 'jest-pact';
pactWith(PactOptions, PactFn);
```
@ -34,7 +34,7 @@ pactWith(PactOptions, PactFn);
`PactOptions` with `jest-pact` introduces [additional options](https://github.com/pact-foundation/jest-pact/blob/dce370c1ab4b7cb5dff12c4b62246dc229c53d0e/README.md#defaults) that build on top of the ones [provided in `pact-js`](https://github.com/pact-foundation/pact-js#constructor). In most cases, you define the `consumer`, `provider`, `log`, and `dir` options for these tests.
```javascript
const { pactWith } = require('jest-pact');
import { pactWith } from 'jest-pact';
pactWith(
{
@ -54,7 +54,7 @@ To learn more about how to name the consumers and providers, see contract testin
The `PactFn` is where your tests are defined. This is where you set up the mock provider and where you can use the standard Jest methods like [`Jest.describe`](https://jestjs.io/docs/api#describename-fn), [`Jest.beforeEach`](https://jestjs.io/docs/api#beforeeachfn-timeout), and [`Jest.it`](https://jestjs.io/docs/api#testname-fn-timeout). For more information, see [https://jestjs.io/docs/api](https://jestjs.io/docs/api).
```javascript
const { pactWith } = require('jest-pact');
import { pactWith } from 'jest-pact';
pactWith(
{
@ -70,7 +70,7 @@ pactWith(
});
it('return a successful body', () => {
it('return a successful body', async () => {
});
});
@ -92,8 +92,8 @@ For this tutorial, define four attributes for the `Interaction`:
After you define the `Interaction`, add that interaction to the mock provider by calling `addInteraction`.
```javascript
const { pactWith } = require('jest-pact');
const { Matchers } = require('@pact-foundation/pact');
import { pactWith } from 'jest-pact';
import { Matchers } from '@pact-foundation/pact';
pactWith(
{
@ -132,7 +132,7 @@ pactWith(
provider.addInteraction(interaction);
});
it('return a successful body', () => {
it('return a successful body', async () => {
});
});
@ -142,38 +142,36 @@ pactWith(
### Response body `Matchers`
Notice how we use `Matchers` in the `body` of the expected response. This allows us to be flexible enough to accept different values but still be strict enough to distinguish between valid and invalid values. We must ensure that we have a tight definition that is neither too strict nor too lax. Read more about the [different types of `Matchers`](https://github.com/pact-foundation/pact-js#using-the-v3-matching-rules).
Notice how we use `Matchers` in the `body` of the expected response. This allows us to be flexible enough to accept different values but still be strict enough to distinguish between valid and invalid values. We must ensure that we have a tight definition that is neither too strict nor too lax. Read more about the [different types of `Matchers`](https://github.com/pact-foundation/pact-js/blob/master/docs/matching.md). We are currently using the V2 matching rules.
## Write the test
After the mock provider is set up, you can write the test. For this test, you make a request and expect a particular response.
First, set up the client that makes the API request. To do that, create `spec/contracts/consumer/endpoints/project/merge_requests.js` and add the following API request.
First, set up the client that makes the API request. To do that, create `spec/contracts/consumer/resources/api/project/merge_requests.js` and add the following API request. If the endpoint is a GraphQL, then we create it under `spec/contracts/consumer/resources/graphql` instead.
```javascript
const axios = require('axios');
import axios from 'axios';
exports.getDiscussions = (endpoint) => {
const url = endpoint.url;
export async function getDiscussions(endpoint) {
const { url } = endpoint;
return axios
.request({
method: 'GET',
baseURL: url,
url: '/gitlab-org/gitlab-qa/-/merge_requests/1/discussions.json',
headers: { Accept: '*/*' },
})
.then((response) => response.data);
};
return axios({
method: 'GET',
baseURL: url,
url: '/gitlab-org/gitlab-qa/-/merge_requests/1/discussions.json',
headers: { Accept: '*/*' },
})
}
```
After that's set up, import it to the test file and call it to make the request. Then, you can make the request and define your expectations.
```javascript
const { pactWith } = require('jest-pact');
const { Matchers } = require('@pact-foundation/pact');
import { pactWith } from 'jest-pact';
import { Matchers } from '@pact-foundation/pact';
const { getDiscussions } = require('../endpoints/project/merge_requests');
import { getDiscussions } from '../../../resources/api/project/merge_requests';
pactWith(
{
@ -211,17 +209,17 @@ pactWith(
};
});
it('return a successful body', () => {
return getDiscussions({
it('return a successful body', async () => {
const discussions = await getDiscussions({
url: provider.mockService.baseUrl,
}).then((discussions) => {
expect(discussions).toEqual(Matchers.eachLike({
id: 'fd73763cbcbf7b29eb8765d969a38f7d735e222a',
project_id: 6954442,
...
resolved: true
}));
});
expect(discussions).toEqual(Matchers.eachLike({
id: 'fd73763cbcbf7b29eb8765d969a38f7d735e222a',
project_id: 6954442,
...
resolved: true
}));
});
});
},
@ -237,7 +235,7 @@ As you may have noticed, the request and response definitions can get large. Thi
Create a file under `spec/contracts/consumer/fixtures/project/merge_request` called `discussions.fixture.js` where you will place the `request` and `response` definitions.
```javascript
const { Matchers } = require('@pact-foundation/pact');
import { Matchers } from '@pact-foundation/pact';
const body = Matchers.eachLike({
id: Matchers.string('fd73763cbcbf7b29eb8765d969a38f7d735e222a'),
@ -254,11 +252,15 @@ const Discussions = {
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
body: body,
body,
},
scenario: {
state: 'a merge request with discussions exists',
uponReceiving: 'a request for discussions',
},
request: {
uponReceiving: 'a request for discussions',
withRequest: {
method: 'GET',
path: '/gitlab-org/gitlab-qa/-/merge_requests/1/discussions.json',
@ -275,36 +277,41 @@ exports.Discussions = Discussions;
With all of that moved to the `fixture`, you can simplify the test to the following:
```javascript
const { pactWith } = require('jest-pact');
import { pactWith } from 'jest-pact';
const { Discussions } = require('../fixtures/discussions.fixture');
const { getDiscussions } = require('../endpoints/project/merge_requests');
import { Discussions } from '../../../fixtures/project/merge_request/discussions.fixture';
import { getDiscussions } from '../../../resources/api/project/merge_requests';
const CONSUMER_NAME = 'MergeRequest#show';
const PROVIDER_NAME = 'Merge Request Discussions Endpoint';
const CONSUMER_LOG = '../logs/consumer.log';
const CONTRACT_DIR = '../contracts/project/merge_request/show';
pactWith(
{
consumer: 'MergeRequest#show',
provider: 'Merge Request Discussions Endpoint',
log: '../logs/consumer.log',
dir: '../contracts/project/merge_request/show',
consumer: CONSUMER_NAME,
provider: PROVIDER_NAME,
log: CONSUMER_LOG,
dir: CONTRACT_DIR,
},
(provider) => {
describe('Merge Request Discussions Endpoint', () => {
describe(PROVIDER_NAME, () => {
beforeEach(() => {
const interaction = {
state: 'a merge request with discussions exists',
...Discussions.scenario,
...Discussions.request,
willRespondWith: Discussions.success,
};
return provider.addInteraction(interaction);
provider.addInteraction(interaction);
});
it('return a successful body', () => {
return getDiscussions({
it('return a successful body', async () => {
const discussions = await getDiscussions({
url: provider.mockService.baseUrl,
}).then((discussions) => {
expect(discussions).toEqual(Discussions.body);
});
expect(discussions).toEqual(Discussions.body);
});
});
},

View File

@ -50,11 +50,11 @@ Having an organized and sensible folder structure for the test suite makes it ea
The consumer tests are grouped according to the different pages in the application. Each file contains various types of requests found in a page. As such, the consumer test files are named using the Rails standards of how pages are referenced. For example, the project pipelines page would be the `Project::Pipeline#index` page so the equivalent consumer test would be located in `consumer/specs/project/pipelines/index.spec.js`.
When defining the location to output the contract generated by the test, we want to follow the same file structure which would be `contracts/project/pipelines/` for this example. This is the structure in `consumer/endpoints` and `consumer/fixtures` as well.
When defining the location to output the contract generated by the test, we want to follow the same file structure which would be `contracts/project/pipelines/` for this example. This is the structure in `consumer/resources` and `consumer/fixtures` as well.
#### Provider tests
The provider tests are grouped similarly to our controllers. Each of these tests contains various tests for an API endpoint. For example, the API endpoint to get a list of pipelines for a project would be located in `provider/pact_helpers/project/pipelines/get_list_project_pipelines_helper.rb`. The provider states are structured the same way.
The provider tests are grouped similarly to our controllers. Each of these tests contains various tests for an API endpoint. For example, the API endpoint to get a list of pipelines for a project would be located in `provider/pact_helpers/project/pipelines/get_list_project_pipelines_helper.rb`. The provider states are grouped according to the different pages in the application similar to the consumer tests.
### Naming conventions

View File

@ -391,7 +391,7 @@ git checkout <default-branch>
git merge <feature-branch>
```
In GitLab, you typically use a [merge request](../user/project/merge_requests/) to merge your changes, instead of using the command line.
In GitLab, you typically use a [merge request](../user/project/merge_requests/index.md) to merge your changes, instead of using the command line.
To create a merge request from a fork to an upstream repository, see the
[forking workflow](../user/project/repository/forking_workflow.md).

View File

@ -19,6 +19,6 @@ sudo gitlab-rake gitlab:spdx:import
bundle exec rake gitlab:spdx:import RAILS_ENV=production
```
To perform this task in the [offline environment](../user/application_security/offline_deployments/#defining-offline-environments),
To perform this task in the [offline environment](../user/application_security/offline_deployments/index.md#defining-offline-environments),
an outbound connection to [`licenses.json`](https://spdx.org/licenses/licenses.json) should be
allowed.

View File

@ -30,6 +30,6 @@ type: index
## Securing your GitLab installation
Consider access control features like [Sign up restrictions](../user/admin_area/settings/sign_up_restrictions.md) and [Authentication options](../topics/authentication/) to harden your GitLab instance and minimize the risk of unwanted user account creation.
Consider access control features like [Sign up restrictions](../user/admin_area/settings/sign_up_restrictions.md) and [Authentication options](../topics/authentication/index.md) to harden your GitLab instance and minimize the risk of unwanted user account creation.
Self-hosting GitLab customers and administrators are responsible for the security of their underlying hosts, and for keeping GitLab itself up to date. It is important to [regularly patch GitLab](../policy/maintenance.md), patch your operating system and its software, and harden your hosts in accordance with vendor guidance.

View File

@ -2247,6 +2247,9 @@ msgstr ""
msgid "Add existing confidential %{issuableType}"
msgstr ""
msgid "Add existing issue"
msgstr ""
msgid "Add header and footer to emails. Please note that color settings will only be applied within the application interface"
msgstr ""
@ -11220,6 +11223,9 @@ msgstr ""
msgid "Critical vulnerabilities present"
msgstr ""
msgid "Crm|Active"
msgstr ""
msgid "Crm|Contact"
msgstr ""

View File

@ -198,7 +198,7 @@
"devDependencies": {
"@gitlab/eslint-plugin": "15.0.0",
"@gitlab/stylelint-config": "4.1.0",
"@graphql-eslint/eslint-plugin": "3.10.6",
"@graphql-eslint/eslint-plugin": "3.10.7",
"@testing-library/dom": "^7.16.2",
"@vue/test-utils": "1.3.0",
"@vue/vue2-jest": "^27.0.0",
@ -211,7 +211,7 @@
"cheerio": "^1.0.0-rc.9",
"commander": "^2.20.3",
"custom-jquery-matchers": "^2.1.0",
"eslint": "8.19.0",
"eslint": "8.21.0",
"eslint-import-resolver-jest": "3.0.2",
"eslint-import-resolver-webpack": "0.13.2",
"eslint-plugin-no-jquery": "2.7.0",

View File

@ -33,7 +33,7 @@ module QA
# wait_until required due to feature_caching. Remove along with feature flag removal.
Page::File::Edit.perform do |file|
Support::Waiter.wait_until(sleep_interval: 2, max_duration: 60, reload_page: page,
retry_on_exception: true) do
retry_on_exception: true) do
expect(file).to have_element(:editor_toolbar_button)
end
file.remove_content

View File

@ -8,18 +8,18 @@ module QA
PRODUCTION_ADDRESS = 'https://gitlab.com'
PRE_PROD_ADDRESS = 'https://pre.gitlab.com'
SENTRY_ENVIRONMENTS = {
staging: 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg',
staging: 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg',
staging_canary: 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg-cny',
staging_ref: 'https://sentry.gitlab.net/gitlab/staging-ref/?environment=gstg-ref',
pre: 'https://sentry.gitlab.net/gitlab/pregitlabcom/?environment=pre',
canary: 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd',
production: 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd-cny'
staging_ref: 'https://sentry.gitlab.net/gitlab/staging-ref/?environment=gstg-ref',
pre: 'https://sentry.gitlab.net/gitlab/pregitlabcom/?environment=pre',
canary: 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd',
production: 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd-cny'
}.freeze
KIBANA_ENVIRONMENTS = {
staging: 'https://nonprod-log.gitlab.net/',
staging: 'https://nonprod-log.gitlab.net/',
staging_canary: 'https://nonprod-log.gitlab.net/',
canary: 'https://log.gprd.gitlab.net/',
production: 'https://log.gprd.gitlab.net/'
canary: 'https://log.gprd.gitlab.net/',
production: 'https://log.gprd.gitlab.net/'
}.freeze
def self.failure_metadata(correlation_id)

View File

@ -37,14 +37,14 @@ RSpec.describe QA::Support::Loglinking do
describe '.sentry_url' do
let(:url_hash) do
{
:staging => 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg',
:staging_canary => 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg-cny',
:staging_ref => 'https://sentry.gitlab.net/gitlab/staging-ref/?environment=gstg-ref',
:pre => 'https://sentry.gitlab.net/gitlab/pregitlabcom/?environment=pre',
:canary => 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd',
:production => 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd-cny',
:foo => nil,
nil => nil
:staging => 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg',
:staging_canary => 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg-cny',
:staging_ref => 'https://sentry.gitlab.net/gitlab/staging-ref/?environment=gstg-ref',
:pre => 'https://sentry.gitlab.net/gitlab/pregitlabcom/?environment=pre',
:canary => 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd',
:production => 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd-cny',
:foo => nil,
nil => nil
}
end
@ -60,14 +60,14 @@ RSpec.describe QA::Support::Loglinking do
describe '.kibana_url' do
let(:url_hash) do
{
:staging => 'https://nonprod-log.gitlab.net/',
:staging_canary => 'https://nonprod-log.gitlab.net/',
:staging_ref => nil,
:pre => nil,
:canary => 'https://log.gprd.gitlab.net/',
:production => 'https://log.gprd.gitlab.net/',
:foo => nil,
nil => nil
:staging => 'https://nonprod-log.gitlab.net/',
:staging_canary => 'https://nonprod-log.gitlab.net/',
:staging_ref => nil,
:pre => nil,
:canary => 'https://log.gprd.gitlab.net/',
:production => 'https://log.gprd.gitlab.net/',
:foo => nil,
nil => nil
}
end

View File

@ -232,7 +232,9 @@ RSpec.describe 'Related issues', :js do
it 'add related issue' do
click_button 'Add a related issue'
fill_in 'Paste issue link', with: "#{issue_b.to_reference(project)} "
click_button 'Add'
page.within('.linked-issues-card-body') do
click_button 'Add'
end
wait_for_requests
@ -249,7 +251,9 @@ RSpec.describe 'Related issues', :js do
it 'add cross-project related issue' do
click_button 'Add a related issue'
fill_in 'Paste issue link', with: "#{issue_project_b_a.to_reference(project)} "
click_button 'Add'
page.within('.linked-issues-card-body') do
click_button 'Add'
end
wait_for_requests
@ -359,7 +363,9 @@ RSpec.describe 'Related issues', :js do
it 'add related issue' do
click_button 'Add a related issue'
fill_in 'Paste issue link', with: "##{issue_d.iid} "
click_button 'Add'
page.within('.linked-issues-card-body') do
click_button 'Add'
end
wait_for_requests
@ -375,7 +381,9 @@ RSpec.describe 'Related issues', :js do
it 'add invalid related issue' do
click_button 'Add a related issue'
fill_in 'Paste issue link', with: '#9999999 '
click_button 'Add'
page.within('.linked-issues-card-body') do
click_button 'Add'
end
wait_for_requests
@ -390,7 +398,9 @@ RSpec.describe 'Related issues', :js do
it 'add unauthorized related issue' do
click_button 'Add a related issue'
fill_in 'Paste issue link', with: "#{issue_project_unauthorized_a.to_reference(project)} "
click_button 'Add'
page.within('.linked-issues-card-body') do
click_button 'Add'
end
wait_for_requests

View File

@ -56,8 +56,9 @@ describe('Customer relations contact form wrapper', () => {
${'edit'} | ${'Edit contact'} | ${'Contact has been updated.'} | ${updateContactMutation} | ${contacts[0].id}
${'create'} | ${'New contact'} | ${'Contact has been added.'} | ${createContactMutation} | ${null}
`('in $mode mode', ({ mode, title, successMessage, mutation, existingId }) => {
const isEditMode = mode === 'edit';
beforeEach(() => {
const isEditMode = mode === 'edit';
mountComponent({ isEditMode });
return waitForPromises();
@ -82,7 +83,7 @@ describe('Customer relations contact form wrapper', () => {
});
it('renders correct fields prop', () => {
expect(findContactForm().props('fields')).toEqual([
const fields = [
{ name: 'firstName', label: 'First name', required: true },
{ name: 'lastName', label: 'Last name', required: true },
{ name: 'email', label: 'Email', required: true },
@ -98,7 +99,9 @@ describe('Customer relations contact form wrapper', () => {
],
},
{ name: 'description', label: 'Description' },
]);
];
if (isEditMode) fields.push({ name: 'active', label: 'Active', required: true, bool: true });
expect(findContactForm().props('fields')).toEqual(fields);
});
it('renders correct title prop', () => {

View File

@ -1,4 +1,4 @@
import { GlAlert, GlFormInput, GlFormSelect, GlFormGroup } from '@gitlab/ui';
import { GlAlert, GlFormCheckbox, GlFormInput, GlFormSelect, GlFormGroup } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
@ -78,6 +78,7 @@ describe('Reusable form component', () => {
const findSaveButton = () => wrapper.findByTestId('save-button');
const findForm = () => wrapper.find('form');
const findError = () => wrapper.findComponent(GlAlert);
const findFormGroup = (at) => wrapper.findAllComponents(GlFormGroup).at(at);
const mountComponent = (propsData) => {
wrapper = shallowMountExtended(Form, {
@ -92,7 +93,7 @@ describe('Reusable form component', () => {
});
};
const mountContact = ({ propsData } = {}) => {
const mountContact = ({ propsData, extraFields = [] } = {}) => {
mountComponent({
fields: [
{ name: 'firstName', label: 'First name', required: true },
@ -108,6 +109,7 @@ describe('Reusable form component', () => {
{ key: 'gid://gitlab/CustomerRelations::Organization/2', value: 'ABC Corp' },
],
},
...extraFields,
],
getQuery: {
query: getGroupContactsQuery,
@ -136,7 +138,8 @@ describe('Reusable form component', () => {
mutation: updateContactMutation,
existingId: 'gid://gitlab/CustomerRelations::Contact/12',
};
mountContact({ propsData });
const extraFields = [{ name: 'active', label: 'Active', required: true, bool: true }];
mountContact({ propsData, extraFields });
};
const mountOrganization = ({ propsData } = {}) => {
@ -285,18 +288,16 @@ describe('Reusable form component', () => {
});
it.each`
index | id | componentName | value
${0} | ${'firstName'} | ${'GlFormInput'} | ${'Marty'}
${1} | ${'lastName'} | ${'GlFormInput'} | ${'McFly'}
${2} | ${'email'} | ${'GlFormInput'} | ${'example@gitlab.com'}
${4} | ${'description'} | ${'GlFormInput'} | ${undefined}
${3} | ${'phone'} | ${'GlFormInput'} | ${undefined}
${5} | ${'organizationId'} | ${'GlFormSelect'} | ${'gid://gitlab/CustomerRelations::Organization/2'}
index | id | component | value
${0} | ${'firstName'} | ${GlFormInput} | ${'Marty'}
${1} | ${'lastName'} | ${GlFormInput} | ${'McFly'}
${2} | ${'email'} | ${GlFormInput} | ${'example@gitlab.com'}
${4} | ${'description'} | ${GlFormInput} | ${undefined}
${3} | ${'phone'} | ${GlFormInput} | ${undefined}
${5} | ${'organizationId'} | ${GlFormSelect} | ${'gid://gitlab/CustomerRelations::Organization/2'}
`(
'should render a $componentName for #$id with the value "$value"',
({ index, id, componentName, value }) => {
const component = componentName === 'GlFormInput' ? GlFormInput : GlFormSelect;
const findFormGroup = (at) => wrapper.findAllComponents(GlFormGroup).at(at);
'should render the correct component for #$id with the value "$value"',
({ index, id, component, value }) => {
const findFormElement = () => findFormGroup(index).find(component);
expect(findFormElement().attributes('id')).toBe(id);
@ -304,6 +305,14 @@ describe('Reusable form component', () => {
},
);
it('should render a checked GlFormCheckbox for #active', () => {
const activeCheckboxIndex = 6;
const findFormElement = () => findFormGroup(activeCheckboxIndex).find(GlFormCheckbox);
expect(findFormElement().attributes('id')).toBe('active');
expect(findFormElement().attributes('checked')).toBe('true');
});
it('should include updated values in update mutation', () => {
wrapper.find('#firstName').vm.$emit('input', 'Michael');
wrapper
@ -314,6 +323,7 @@ describe('Reusable form component', () => {
expect(handler).toHaveBeenCalledWith('updateContact', {
input: {
active: true,
description: null,
email: 'example@gitlab.com',
firstName: 'Michael',

View File

@ -13,6 +13,7 @@ export const getGroupContactsQueryResponse = {
email: 'example@gitlab.com',
phone: null,
description: null,
active: true,
organization: {
__typename: 'CustomerRelationsOrganization',
id: 'gid://gitlab/CustomerRelations::Organization/2',
@ -27,6 +28,7 @@ export const getGroupContactsQueryResponse = {
email: null,
phone: null,
description: null,
active: true,
organization: null,
},
{
@ -37,6 +39,7 @@ export const getGroupContactsQueryResponse = {
email: 'jd@gitlab.com',
phone: '+44 44 4444 4444',
description: 'Vice President',
active: true,
organization: null,
},
],
@ -58,6 +61,7 @@ export const getGroupOrganizationsQueryResponse = {
name: 'Test Inc',
defaultRate: 100,
description: null,
active: true,
},
{
__typename: 'CustomerRelationsOrganization',
@ -65,6 +69,7 @@ export const getGroupOrganizationsQueryResponse = {
name: 'ABC Company',
defaultRate: 110,
description: 'VIP',
active: true,
},
{
__typename: 'CustomerRelationsOrganization',
@ -72,6 +77,7 @@ export const getGroupOrganizationsQueryResponse = {
name: 'GitLab',
defaultRate: 120,
description: null,
active: true,
},
],
},
@ -91,6 +97,7 @@ export const createContactMutationResponse = {
phone: null,
description: null,
organization: null,
active: true,
},
errors: [],
},
@ -119,6 +126,7 @@ export const updateContactMutationResponse = {
phone: null,
description: null,
organization: null,
active: true,
},
errors: [],
},
@ -143,6 +151,7 @@ export const createOrganizationMutationResponse = {
name: 'A',
defaultRate: null,
description: null,
active: true,
},
errors: [],
},
@ -168,6 +177,7 @@ export const updateOrganizationMutationResponse = {
name: 'A',
defaultRate: null,
description: null,
active: true,
},
errors: [],
},

View File

@ -49,7 +49,7 @@ describe('Customer relations organization form wrapper', () => {
mountComponent({ isEditMode: true });
const organizationForm = findOrganizationForm();
expect(organizationForm.props('fields')).toHaveLength(3);
expect(organizationForm.props('fields')).toHaveLength(4);
expect(organizationForm.props('title')).toBe('Edit organization');
expect(organizationForm.props('successMessage')).toBe('Organization has been updated.');
expect(organizationForm.props('mutation')).toBe(updateOrganizationMutation);

View File

@ -3,6 +3,7 @@ import IDEProjectHeader from '~/ide/components/ide_project_header.vue';
import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
const mockProject = {
id: 1,
name: 'test proj',
avatar_url: 'https://gitlab.com',
path_with_namespace: 'path/with-namespace',
@ -30,6 +31,7 @@ describe('IDE project header', () => {
it('renders ProjectAvatar with correct props', () => {
expect(findProjectAvatar().props()).toMatchObject({
projectId: mockProject.id,
projectName: mockProject.name,
projectAvatarUrl: mockProject.avatar_url,
});

View File

@ -15,6 +15,7 @@ import UrlSync from '~/vue_shared/components/url_sync.vue';
const mockOverrides = Array(DEFAULT_PER_PAGE * 3)
.fill(1)
.map((_, index) => ({
id: index,
name: `test-proj-${index}`,
avatar_url: `avatar-${index}`,
full_path: `test-proj-${index}`,
@ -59,6 +60,7 @@ describe('IntegrationOverrides', () => {
const avatar = link.findComponent(ProjectAvatar);
return {
id: avatar.props('projectId'),
href: link.attributes('href'),
avatarUrl: avatar.props('projectAvatarUrl'),
avatarName: avatar.props('projectName'),
@ -109,6 +111,7 @@ describe('IntegrationOverrides', () => {
it('renders overrides as rows in table', () => {
expect(findRowsAsModel()).toEqual(
mockOverrides.map((x) => ({
id: x.id,
href: x.full_path,
avatarUrl: x.avatar_url,
avatarName: x.name,

View File

@ -18,9 +18,10 @@ import {
describe('RelatedIssuesBlock', () => {
let wrapper;
const findIssueCountBadgeAddButton = () => wrapper.findByTestId('add-button');
const findToggleButton = () => wrapper.findByTestId('toggle-links');
const findRelatedIssuesBody = () => wrapper.findByTestId('related-issues-body');
const findIssueCountBadgeAddButton = () =>
wrapper.find('[data-testid="related-issues-plus-button"]');
afterEach(() => {
if (wrapper) {

View File

@ -346,6 +346,17 @@ describe('init markdown', () => {
},
);
it('indents a blank line two spaces to the right', () => {
textArea.value = '012\n\n89';
textArea.setSelectionRange(4, 4);
textArea.dispatchEvent(indentEvent);
expect(textArea.value).toEqual('012\n \n89');
expect(textArea.selectionStart).toEqual(6);
expect(textArea.selectionEnd).toEqual(6);
});
it.each`
selectionStart | selectionEnd | expected | expectedSelectionStart | expectedSelectionEnd
${0} | ${0} | ${'234\n 789\n 34'} | ${0} | ${0}
@ -356,6 +367,7 @@ describe('init markdown', () => {
${14} | ${15} | ${' 234\n 789\n34'} | ${12} | ${13}
${0} | ${15} | ${'234\n789\n34'} | ${0} | ${10}
${3} | ${13} | ${'234\n789\n34'} | ${1} | ${8}
${6} | ${6} | ${' 234\n789\n 34'} | ${6} | ${6}
`(
'outdents the selected lines two spaces to the left',
({
@ -377,6 +389,17 @@ describe('init markdown', () => {
},
);
it('outdent a blank line has no effect', () => {
textArea.value = '012\n\n89';
textArea.setSelectionRange(4, 4);
textArea.dispatchEvent(outdentEvent);
expect(textArea.value).toEqual('012\n\n89');
expect(textArea.selectionStart).toEqual(4);
expect(textArea.selectionEnd).toEqual(4);
});
it('does not indent if meta is not set', () => {
const indentNoMetaEvent = new KeyboardEvent('keydown', { key: ']' });
const text = '012\n456\n89';

View File

@ -128,7 +128,7 @@ describe('packages_list_row', () => {
findDeleteButton().vm.$emit('click');
await nextTick();
expect(wrapper.emitted('packageToDelete')).toBeTruthy();
expect(wrapper.emitted('packageToDelete')).toHaveLength(1);
expect(wrapper.emitted('packageToDelete')[0]).toEqual([packageWithoutTags]);
});
});

View File

@ -43,13 +43,39 @@ describe('ProjectAvatar', () => {
});
describe('with `projectId` prop', () => {
it('renders GlAvatar with specified `entityId` prop', () => {
const validatorFunc = ProjectAvatar.props.projectId.validator;
it('prop validators return true for valid types', () => {
expect(validatorFunc(1)).toBe(true);
expect(validatorFunc('gid://gitlab/Project/1')).toBe(true);
});
it('prop validators return false for invalid types', () => {
expect(validatorFunc('1')).toBe(false);
});
it('renders GlAvatar with `entityId` 0 when `projectId` is not informed', () => {
createComponent({ props: { projectId: undefined } });
const avatar = findGlAvatar();
expect(avatar.props('entityId')).toBe(0);
});
it('renders GlAvatar with specified `entityId` when `projectId` is a Number', () => {
const mockProjectId = 1;
createComponent({ props: { projectId: mockProjectId } });
const avatar = findGlAvatar();
expect(avatar.props('entityId')).toBe(mockProjectId);
});
it('renders GlAvatar with specified `entityId` when `projectId` is a gid String', () => {
const mockProjectId = 'gid://gitlab/Project/1';
createComponent({ props: { projectId: mockProjectId } });
const avatar = findGlAvatar();
expect(avatar.props('entityId')).toBe(1);
});
});
describe('with `projectAvatarUrl` prop', () => {

View File

@ -56,6 +56,7 @@ describe('ProjectListItem component', () => {
expect(avatar.exists()).toBe(true);
expect(avatar.props()).toMatchObject({
projectId: project.id,
projectAvatarUrl: '',
projectName: project.name_with_namespace,
});

View File

@ -4589,24 +4589,6 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
end
describe '#find_stage_by_name' do
subject { pipeline.find_stage_by_name!(stage_name) }
context 'when stage exists' do
it { is_expected.to eq(stage) }
end
context 'when stage does not exist' do
let(:stage_name) { 'build' }
it 'raises an ActiveRecord exception' do
expect do
subject
end.to raise_exception(ActiveRecord::RecordNotFound)
end
end
end
end
describe '#full_error_messages' do

View File

@ -10,27 +10,6 @@ RSpec.describe OauthAccessToken do
let(:token) { create(:oauth_access_token, application_id: app_one.id) }
describe 'scopes' do
describe '.distinct_resource_owner_counts' do
let(:tokens) { described_class.all }
before do
token
create_list(:oauth_access_token, 2, resource_owner: user, application_id: app_two.id)
end
it 'returns unique owners' do
expect(tokens.count).to eq(3)
expect(tokens.distinct_resource_owner_counts([app_one])).to eq({ app_one.id => 1 })
expect(tokens.distinct_resource_owner_counts([app_two])).to eq({ app_two.id => 1 })
expect(tokens.distinct_resource_owner_counts([app_three])).to eq({})
expect(tokens.distinct_resource_owner_counts([app_one, app_two]))
.to eq({
app_one.id => 1,
app_two.id => 1
})
end
end
describe '.latest_per_application' do
let!(:app_two_token1) { create(:oauth_access_token, application: app_two) }
let!(:app_two_token2) { create(:oauth_access_token, application: app_two) }

View File

@ -38,6 +38,7 @@ RSpec.describe Admin::IntegrationsController, :enable_admin_mode do
expect(response).to include_pagination_headers
expect(json_response).to contain_exactly(
{
'id' => project.id,
'avatar_url' => project.avatar_url,
'full_name' => project.full_name,
'name' => project.name,

View File

@ -16,6 +16,7 @@ RSpec.describe Integrations::ProjectEntity do
it 'contains needed attributes' do
expect(subject).to include(
id: project.id,
avatar_url: include('uploads'),
name: project.name,
full_path: project_path(project),

View File

@ -1075,10 +1075,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.7.3.tgz#9ea641146436da388ffbad25d7f2abe0df52c235"
integrity sha512-NMV++7Ew1FSBDN1xiZaauU9tfeSfgDHcOLpn+8bGpP+O5orUPm2Eu66R5eC5gkjBPaXosNAxNWtriee+aFk4+g==
"@graphql-eslint/eslint-plugin@3.10.6":
version "3.10.6"
resolved "https://registry.yarnpkg.com/@graphql-eslint/eslint-plugin/-/eslint-plugin-3.10.6.tgz#4d5748fade6c11d74aeff9a99d6e38d2ed8f6310"
integrity sha512-rxGSrKVsDHCuZRvP81ElgtCs0sikdhcHqQySiyhir4G+VhiNlPZ7SQJWrXm9JJEAeB0wQ50kabvse5NRk0hqog==
"@graphql-eslint/eslint-plugin@3.10.7":
version "3.10.7"
resolved "https://registry.yarnpkg.com/@graphql-eslint/eslint-plugin/-/eslint-plugin-3.10.7.tgz#9a203e2084371eca933d88b73ce7a6bebbbb9872"
integrity sha512-Vp32LMsHTgRNc2q+OrXRNR1i2nlAbVfN0tMTlFHgnzJfnEJDV332cpjiUF9F82IKjNFSde/pF3cuYccu+UUR/g==
dependencies:
"@babel/code-frame" "^7.16.7"
"@graphql-tools/code-file-loader" "^7.2.14"
@ -1248,15 +1248,20 @@
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.0.tgz#0eee6373e11418bfe0b5638f654df7a4ca6a3950"
integrity sha512-wYn6r8zVZyQJ6rQaALBEln5B1pzxb9shV5Ef97kTvn6yVGrqyXVnDqnU24MXnFubR+rZjBY9NWuxX3FB2sTsjg==
"@humanwhocodes/config-array@^0.9.2":
version "0.9.5"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.5.tgz#2cbaf9a89460da24b5ca6531b8bbfc23e1df50c7"
integrity sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==
"@humanwhocodes/config-array@^0.10.4":
version "0.10.4"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.10.4.tgz#01e7366e57d2ad104feea63e72248f22015c520c"
integrity sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==
dependencies:
"@humanwhocodes/object-schema" "^1.2.1"
debug "^4.1.1"
minimatch "^3.0.4"
"@humanwhocodes/gitignore-to-minimatch@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz#316b0a63b91c10e53f242efb4ace5c3b34e8728d"
integrity sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==
"@humanwhocodes/object-schema@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
@ -2485,7 +2490,7 @@ acorn@^7.1.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
acorn@^8.0.4, acorn@^8.2.4, acorn@^8.7.1:
acorn@^8.0.4, acorn@^8.2.4, acorn@^8.8.0:
version "8.8.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
@ -5262,13 +5267,14 @@ eslint-visitor-keys@^3.1.0, eslint-visitor-keys@^3.3.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826"
integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
eslint@8.19.0:
version "8.19.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.19.0.tgz#7342a3cbc4fbc5c106a1eefe0fd0b50b6b1a7d28"
integrity sha512-SXOPj3x9VKvPe81TjjUJCYlV4oJjQw68Uek+AM0X4p+33dj2HY5bpTZOgnQHcG2eAm1mtCU9uNMnJi7exU/kYw==
eslint@8.21.0:
version "8.21.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.21.0.tgz#1940a68d7e0573cef6f50037addee295ff9be9ef"
integrity sha512-/XJ1+Qurf1T9G2M5IHrsjp+xrGT73RZf23xA1z5wB1ZzzEAWSZKvRwhWxTFp1rvkvCfwcvAUNAP31bhKTTGfDA==
dependencies:
"@eslint/eslintrc" "^1.3.0"
"@humanwhocodes/config-array" "^0.9.2"
"@humanwhocodes/config-array" "^0.10.4"
"@humanwhocodes/gitignore-to-minimatch" "^1.0.2"
ajv "^6.10.0"
chalk "^4.0.0"
cross-spawn "^7.0.2"
@ -5278,14 +5284,17 @@ eslint@8.19.0:
eslint-scope "^7.1.1"
eslint-utils "^3.0.0"
eslint-visitor-keys "^3.3.0"
espree "^9.3.2"
espree "^9.3.3"
esquery "^1.4.0"
esutils "^2.0.2"
fast-deep-equal "^3.1.3"
file-entry-cache "^6.0.1"
find-up "^5.0.0"
functional-red-black-tree "^1.0.1"
glob-parent "^6.0.1"
globals "^13.15.0"
globby "^11.1.0"
grapheme-splitter "^1.0.4"
ignore "^5.2.0"
import-fresh "^3.0.0"
imurmurhash "^0.1.4"
@ -5303,12 +5312,12 @@ eslint@8.19.0:
text-table "^0.2.0"
v8-compile-cache "^2.0.3"
espree@^9.0.0, espree@^9.3.2:
version "9.3.2"
resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.2.tgz#f58f77bd334731182801ced3380a8cc859091596"
integrity sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==
espree@^9.0.0, espree@^9.3.2, espree@^9.3.3:
version "9.3.3"
resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.3.tgz#2dd37c4162bb05f433ad3c1a52ddf8a49dc08e9d"
integrity sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng==
dependencies:
acorn "^8.7.1"
acorn "^8.8.0"
acorn-jsx "^5.3.2"
eslint-visitor-keys "^3.3.0"
@ -5678,6 +5687,14 @@ find-up@^4.0.0, find-up@^4.1.0:
locate-path "^5.0.0"
path-exists "^4.0.0"
find-up@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
dependencies:
locate-path "^6.0.0"
path-exists "^4.0.0"
find-yarn-workspace-root@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd"
@ -6007,6 +6024,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
grapheme-splitter@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
graphlib@^2.1.8:
version "2.1.8"
resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.8.tgz#5761d414737870084c92ec7b5dbcb0592c9d35da"
@ -7711,6 +7733,13 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
locate-path@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
dependencies:
p-locate "^5.0.0"
lodash.assign@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
@ -9241,6 +9270,13 @@ p-locate@^4.1.0:
dependencies:
p-limit "^2.2.0"
p-locate@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
dependencies:
p-limit "^3.0.2"
p-map@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b"