Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
f675c7d41d
commit
444f662b8d
|
@ -105,8 +105,8 @@ export default {
|
|||
atVersion: this.designsVersion,
|
||||
};
|
||||
},
|
||||
isDiscussionHighlighted() {
|
||||
return this.discussion.notes[0].id === this.activeDiscussion.id;
|
||||
isDiscussionActive() {
|
||||
return this.discussion.notes.some(({ id }) => id === this.activeDiscussion.id);
|
||||
},
|
||||
resolveCheckboxText() {
|
||||
return this.discussion.resolved
|
||||
|
@ -134,18 +134,6 @@ export default {
|
|||
isFormVisible() {
|
||||
return this.isFormRendered && this.discussionWithOpenForm === this.discussion.id;
|
||||
},
|
||||
shouldScrollToDiscussion(activeDiscussion) {
|
||||
const ALLOWED_ACTIVE_DISCUSSION_SOURCES = [
|
||||
ACTIVE_DISCUSSION_SOURCE_TYPES.pin,
|
||||
ACTIVE_DISCUSSION_SOURCE_TYPES.url,
|
||||
];
|
||||
const { id: activeDiscussionId, source: activeDiscussionSource } = activeDiscussion;
|
||||
|
||||
return (
|
||||
ALLOWED_ACTIVE_DISCUSSION_SOURCES.includes(activeDiscussionSource) &&
|
||||
activeDiscussionId === this.discussion.notes[0].id
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addDiscussionComment(
|
||||
|
@ -199,6 +187,14 @@ export default {
|
|||
this.isResolving = false;
|
||||
});
|
||||
},
|
||||
shouldScrollToDiscussion(activeDiscussion) {
|
||||
const ALLOWED_ACTIVE_DISCUSSION_SOURCES = [
|
||||
ACTIVE_DISCUSSION_SOURCE_TYPES.pin,
|
||||
ACTIVE_DISCUSSION_SOURCE_TYPES.url,
|
||||
];
|
||||
const { source } = activeDiscussion;
|
||||
return ALLOWED_ACTIVE_DISCUSSION_SOURCES.includes(source) && this.isDiscussionActive;
|
||||
},
|
||||
},
|
||||
createNoteMutation,
|
||||
};
|
||||
|
@ -221,7 +217,7 @@ export default {
|
|||
:note="firstNote"
|
||||
:markdown-preview-path="markdownPreviewPath"
|
||||
:is-resolving="isResolving"
|
||||
:class="{ 'gl-bg-blue-50': isDiscussionHighlighted }"
|
||||
:class="{ 'gl-bg-blue-50': isDiscussionActive }"
|
||||
@error="$emit('updateNoteError', $event)"
|
||||
>
|
||||
<template v-if="discussion.resolvable" #resolveDiscussion>
|
||||
|
@ -265,7 +261,7 @@ export default {
|
|||
:note="note"
|
||||
:markdown-preview-path="markdownPreviewPath"
|
||||
:is-resolving="isResolving"
|
||||
:class="{ 'gl-bg-blue-50': isDiscussionHighlighted }"
|
||||
:class="{ 'gl-bg-blue-50': isDiscussionActive }"
|
||||
@error="$emit('updateNoteError', $event)"
|
||||
/>
|
||||
<li v-show="isReplyPlaceholderVisible" class="reply-wrapper">
|
||||
|
|
|
@ -7,7 +7,7 @@ import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link
|
|||
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import DesignReplyForm from './design_reply_form.vue';
|
||||
import { findNoteId } from '../../utils/design_management_utils';
|
||||
import { findNoteId, extractDesignNoteId } from '../../utils/design_management_utils';
|
||||
import { hasErrors } from '../../utils/cache_update';
|
||||
|
||||
export default {
|
||||
|
@ -47,7 +47,7 @@ export default {
|
|||
return findNoteId(this.note.id);
|
||||
},
|
||||
isNoteLinked() {
|
||||
return this.$route.hash === `#note_${this.noteAnchorId}`;
|
||||
return extractDesignNoteId(this.$route.hash) === this.noteAnchorId;
|
||||
},
|
||||
mutationPayload() {
|
||||
return {
|
||||
|
@ -59,13 +59,6 @@ export default {
|
|||
return !this.isEditing && this.note.userPermissions.adminNote;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
if (this.isNoteLinked) {
|
||||
this.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' });
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
hideForm() {
|
||||
this.isEditing = false;
|
||||
|
|
|
@ -237,7 +237,12 @@ export default {
|
|||
});
|
||||
},
|
||||
isNoteInactive(note) {
|
||||
return this.activeDiscussion.id && this.activeDiscussion.id !== note.id;
|
||||
const discussionNotes = note.discussion.notes.nodes || [];
|
||||
|
||||
return (
|
||||
this.activeDiscussion.id &&
|
||||
!discussionNotes.some(({ id }) => id === this.activeDiscussion.id)
|
||||
);
|
||||
},
|
||||
designPinClass(note) {
|
||||
return { inactive: this.isNoteInactive(note), resolved: note.resolved };
|
||||
|
|
|
@ -25,5 +25,10 @@ fragment DesignNote on Note {
|
|||
}
|
||||
discussion {
|
||||
id
|
||||
notes {
|
||||
nodes {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,11 +11,11 @@
|
|||
- if auth_active?(provider)
|
||||
- if unlink_allowed
|
||||
= link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do
|
||||
= s_('Profiles|Disconnect')
|
||||
= s_('Profiles|Disconnect %{provider}') % { provider: label_for_provider(provider) }
|
||||
- else
|
||||
%a.provider-btn
|
||||
= s_('Profiles|Active')
|
||||
= s_('Profiles|%{provider} Active') % { provider: label_for_provider(provider) }
|
||||
- elsif link_allowed
|
||||
= link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn gl-text-blue-500' do
|
||||
= s_('Profiles|Connect')
|
||||
= s_('Profiles|Connect %{provider}') % { provider: label_for_provider(provider) }
|
||||
= render_if_exists 'profiles/accounts/group_saml_unlink_buttons', group_saml_identities: group_saml_identities
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add job token authentication for the GitLab PyPI package repository
|
||||
merge_request: 40888
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Drop code_owner column from approval_merge_request_rules
|
||||
merge_request: 40322
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Display provider name for profile social sign-in connectors
|
||||
merge_request: 41198
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Highlight design discussion if any comment in discussion is linked
|
||||
merge_request: 41062
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DropCodeOwnerColumnFromApprovalMergeRequestRule < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
remove_column :approval_merge_request_rules, :code_owner
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
unless column_exists?(:approval_merge_request_rules, :code_owner)
|
||||
with_lock_retries do
|
||||
add_column :approval_merge_request_rules, :code_owner, :boolean, default: false, null: false
|
||||
end
|
||||
end
|
||||
|
||||
add_concurrent_index(
|
||||
:approval_merge_request_rules,
|
||||
[:merge_request_id, :code_owner, :name],
|
||||
unique: true,
|
||||
where: "code_owner = true AND section IS NULL",
|
||||
name: "approval_rule_name_index_for_code_owners"
|
||||
)
|
||||
|
||||
add_concurrent_index(
|
||||
:approval_merge_request_rules,
|
||||
[:merge_request_id, :code_owner],
|
||||
name: "index_approval_merge_request_rules_1"
|
||||
)
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
6fb93002ffd5c1d1bfff5bea8a99cbbfc7cefefbc450a9d067ee0cfab8d11e9e
|
|
@ -9304,7 +9304,6 @@ CREATE TABLE public.approval_merge_request_rules (
|
|||
updated_at timestamp with time zone NOT NULL,
|
||||
merge_request_id integer NOT NULL,
|
||||
approvals_required smallint DEFAULT 0 NOT NULL,
|
||||
code_owner boolean DEFAULT false NOT NULL,
|
||||
name character varying NOT NULL,
|
||||
rule_type smallint DEFAULT 1 NOT NULL,
|
||||
report_type smallint,
|
||||
|
@ -19048,8 +19047,6 @@ CREATE UNIQUE INDEX any_approver_project_rule_type_unique_index ON public.approv
|
|||
|
||||
CREATE INDEX approval_mr_rule_index_merge_request_id ON public.approval_merge_request_rules USING btree (merge_request_id);
|
||||
|
||||
CREATE UNIQUE INDEX approval_rule_name_index_for_code_owners ON public.approval_merge_request_rules USING btree (merge_request_id, code_owner, name) WHERE ((code_owner = true) AND (section IS NULL));
|
||||
|
||||
CREATE UNIQUE INDEX backup_labels_group_id_project_id_title_idx ON public.backup_labels USING btree (group_id, project_id, title);
|
||||
|
||||
CREATE INDEX backup_labels_group_id_title_idx ON public.backup_labels USING btree (group_id, title) WHERE (project_id = NULL::integer);
|
||||
|
@ -19222,8 +19219,6 @@ CREATE UNIQUE INDEX index_approval_merge_request_rule_sources_1 ON public.approv
|
|||
|
||||
CREATE INDEX index_approval_merge_request_rule_sources_2 ON public.approval_merge_request_rule_sources USING btree (approval_project_rule_id);
|
||||
|
||||
CREATE INDEX index_approval_merge_request_rules_1 ON public.approval_merge_request_rules USING btree (merge_request_id, code_owner);
|
||||
|
||||
CREATE UNIQUE INDEX index_approval_merge_request_rules_approved_approvers_1 ON public.approval_merge_request_rules_approved_approvers USING btree (approval_merge_request_rule_id, user_id);
|
||||
|
||||
CREATE INDEX index_approval_merge_request_rules_approved_approvers_2 ON public.approval_merge_request_rules_approved_approvers USING btree (user_id);
|
||||
|
|
|
@ -51,12 +51,17 @@ Using the consolidated object storage configuration has a number of advantages:
|
|||
- It enables the use of [encrypted S3 buckets](#encrypted-s3-buckets).
|
||||
- It [uploads files to S3 with proper `Content-MD5` headers](https://gitlab.com/gitlab-org/gitlab-workhorse/-/issues/222).
|
||||
|
||||
NOTE: **Note:**
|
||||
Only AWS S3-compatible providers and Google are
|
||||
supported at the moment since [direct upload
|
||||
mode](../development/uploads.md#direct-upload) must be used. Background
|
||||
upload is not supported in this mode. We recommend direct upload mode because
|
||||
it does not require a shared folder, and [this setting may become the default](https://gitlab.com/gitlab-org/gitlab/-/issues/27331).
|
||||
Because [direct upload mode](../development/uploads.md#direct-upload)
|
||||
must be enabled, only the following providers can be used:
|
||||
|
||||
- [Amazon S3-compatible providers](#s3-compatible-connection-settings)
|
||||
- [Google Cloud Storage](#google-cloud-storage-gcs)
|
||||
- [Azure Blob storage](#azure-blob-storage)
|
||||
|
||||
Background upload is not supported with the consolidated object storage
|
||||
configuration. We recommend enabling direct upload mode because it does
|
||||
not require a shared folder, and [this setting may become the
|
||||
default](https://gitlab.com/gitlab-org/gitlab/-/issues/27331).
|
||||
|
||||
NOTE: **Note:**
|
||||
Consolidated object storage configuration cannot be used for
|
||||
|
|
|
@ -266,7 +266,7 @@ POST /groups/:id/epics
|
|||
| `title` | string | yes | The title of the epic |
|
||||
| `labels` | string | no | The comma separated list of labels |
|
||||
| `description` | string | no | The description of the epic. Limited to 1,048,576 characters. |
|
||||
| `confidential` | boolean | no | Whether the epic should be confidential. Will be ignored if `confidential_epics` feature flag is disabled. |
|
||||
| `confidential` | boolean | no | Whether the epic should be confidential |
|
||||
| `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) |
|
||||
| `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) |
|
||||
| `due_date_is_fixed` | boolean | no | Whether due date should be sourced from `due_date_fixed` or from milestones (since 11.3) |
|
||||
|
@ -347,7 +347,7 @@ PUT /groups/:id/epics/:epic_iid
|
|||
| `epic_iid` | integer/string | yes | The internal ID of the epic |
|
||||
| `title` | string | no | The title of an epic |
|
||||
| `description` | string | no | The description of an epic. Limited to 1,048,576 characters. |
|
||||
| `confidential` | boolean | no | Whether the epic should be confidential. Will be ignored if `confidential_epics` feature flag is disabled. |
|
||||
| `confidential` | boolean | no | Whether the epic should be confidential |
|
||||
| `labels` | string | no | The comma separated list of labels |
|
||||
| `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) |
|
||||
| `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) |
|
||||
|
|
|
@ -2614,7 +2614,7 @@ input CreateEpicInput {
|
|||
clientMutationId: String
|
||||
|
||||
"""
|
||||
Indicates if the epic is confidential. Will be ignored if `confidential_epics` feature flag is disabled
|
||||
Indicates if the epic is confidential
|
||||
"""
|
||||
confidential: Boolean
|
||||
|
||||
|
@ -16809,7 +16809,7 @@ input UpdateEpicInput {
|
|||
clientMutationId: String
|
||||
|
||||
"""
|
||||
Indicates if the epic is confidential. Will be ignored if `confidential_epics` feature flag is disabled
|
||||
Indicates if the epic is confidential
|
||||
"""
|
||||
confidential: Boolean
|
||||
|
||||
|
|
|
@ -7089,7 +7089,7 @@
|
|||
},
|
||||
{
|
||||
"name": "confidential",
|
||||
"description": "Indicates if the epic is confidential. Will be ignored if `confidential_epics` feature flag is disabled",
|
||||
"description": "Indicates if the epic is confidential",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
|
@ -49522,7 +49522,7 @@
|
|||
},
|
||||
{
|
||||
"name": "confidential",
|
||||
"description": "Indicates if the epic is confidential. Will be ignored if `confidential_epics` feature flag is disabled",
|
||||
"description": "Indicates if the epic is confidential",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
|
|
|
@ -164,18 +164,6 @@ To make an epic confidential:
|
|||
- **In an existing epic:** in the epic's sidebar, select **Edit** next to **Confidentiality** then
|
||||
select **Turn on**.
|
||||
|
||||
### Disable confidential epics **(PREMIUM ONLY)**
|
||||
|
||||
The confidential epics feature is deployed behind a feature flag that is **enabled by default**.
|
||||
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
|
||||
can disable it for your self-managed instance.
|
||||
|
||||
To disable it:
|
||||
|
||||
```ruby
|
||||
Feature.disable(:confidential_epics)
|
||||
```
|
||||
|
||||
## Manage issues assigned to an epic
|
||||
|
||||
### Add a new issue to an epic
|
||||
|
|
|
@ -26,19 +26,19 @@ For information on how to create and upload a package, view the GitLab documenta
|
|||
## Use GitLab CI/CD to build packages
|
||||
|
||||
You can use [GitLab CI/CD](../../../ci/README.md) to build packages.
|
||||
For Maven, NuGet and NPM packages, and Composer dependencies, you can
|
||||
For Maven, NuGet, NPM, Conan, and PyPI packages, and Composer dependencies, you can
|
||||
authenticate with GitLab by using the `CI_JOB_TOKEN`.
|
||||
|
||||
CI/CD templates, which you can use to get started, are in [this repo](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates).
|
||||
|
||||
Learn more about [using CI/CD to build Maven packages](../maven_repository/index.md#creating-maven-packages-with-gitlab-cicd), [NPM packages](../npm_registry/index.md#publishing-a-package-with-cicd), [Composer packages](../composer_repository/index.md#publishing-the-package-with-cicd), and [NuGet packages](../nuget_repository/index.md#publishing-a-nuget-package-with-cicd).
|
||||
Learn more about [using CI/CD to build Maven packages](../maven_repository/index.md#creating-maven-packages-with-gitlab-cicd), [NPM packages](../npm_registry/index.md#publishing-a-package-with-cicd), [Composer packages](../composer_repository/index.md#publishing-the-package-with-cicd), [NuGet Packages](../nuget_repository/index.md#publishing-a-nuget-package-with-cicd), [Conan Packages](../conan_repository/index.md#using-gitlab-ci-with-conan-packages), and [PyPI packages](../pypi_repository/index.md#using-gitlab-ci-with-pypi-packages).
|
||||
|
||||
If you use CI/CD to build a package, extended activity
|
||||
information is displayed when you view the package details:
|
||||
|
||||
![Package CI/CD activity](img/package_activity_v12_10.png)
|
||||
|
||||
You can view which pipeline published the package, as well as the commit and
|
||||
When using Maven and NPM, you can view which pipeline published the package, as well as the commit and
|
||||
user who triggered it.
|
||||
|
||||
## Download a package
|
||||
|
|
|
@ -302,20 +302,10 @@ Successfully installed mypypipackage-0.0.1
|
|||
|
||||
## Using GitLab CI with PyPI packages
|
||||
|
||||
NOTE: **Note:**
|
||||
`CI_JOB_TOKEN`s are not yet supported for use with PyPI.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11678) in GitLab 13.4.
|
||||
|
||||
To work with PyPI commands within [GitLab CI/CD](./../../../ci/README.md), you can use
|
||||
[environment variables](./../../../ci/variables/README.md#custom-environment-variables)
|
||||
to access your authentication tokens in your commands.
|
||||
|
||||
Set up environment variables for `TWINE_PASSWORD` and `TWINE_USERNAME` using either:
|
||||
|
||||
- A [personal access token](../../../user/profile/personal_access_tokens.md) and your GitLab username.
|
||||
- A [deploy token](./../../project/deploy_tokens/index.md) and its associated deploy token username.
|
||||
|
||||
You can now access your `TWINE_USERNAME` and `TWINE_PASSWORD` using any `twine` command in your
|
||||
`.gitlab-ci.yml` file.
|
||||
`CI_JOB_TOKEN` in place of the personal access token or deploy token in your commands.
|
||||
|
||||
For example:
|
||||
|
||||
|
@ -326,5 +316,18 @@ run:
|
|||
script:
|
||||
- pip install twine
|
||||
- python setup.py sdist bdist_wheel
|
||||
- TWINE_PASSWORD=${TWINE_PASSWORD} TWINE_USERNAME=${TWINE_USERNAME} python -m twine upload --repository-url https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/pypi dist/*
|
||||
- TWINE_PASSWORD=${CI_JOB_TOKEN} TWINE_USERNAME=gitlab-ci-token python -m twine upload --repository-url https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/pypi dist/*
|
||||
```
|
||||
|
||||
You can also use `CI_JOB_TOKEN` in a `~/.pypirc` file that you check into GitLab:
|
||||
|
||||
```ini
|
||||
[distutils]
|
||||
index-servers =
|
||||
gitlab
|
||||
|
||||
[gitlab]
|
||||
repository = https://gitlab.com/api/v4/projects/${env.CI_PROJECT_ID}/packages/pypi
|
||||
username = gitlab-ci-token
|
||||
password = ${env.CI_JOB_TOKEN}
|
||||
```
|
||||
|
|
|
@ -64,7 +64,7 @@ module API
|
|||
requires :sha256, type: String, desc: 'The PyPi package sha256 check sum'
|
||||
end
|
||||
|
||||
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true
|
||||
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
||||
get 'files/:sha256/*file_identifier' do
|
||||
project = unauthorized_user_project!
|
||||
|
||||
|
@ -87,7 +87,7 @@ module API
|
|||
|
||||
# An Api entry point but returns an HTML file instead of JSON.
|
||||
# PyPi simple API returns the package descriptor as a simple HTML file.
|
||||
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true
|
||||
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
||||
get 'simple/*package_name', format: :txt do
|
||||
authorize_read_package!(authorized_user_project)
|
||||
|
||||
|
@ -117,7 +117,7 @@ module API
|
|||
optional :sha256_digest, type: String
|
||||
end
|
||||
|
||||
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true
|
||||
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
||||
post do
|
||||
authorize_upload!(authorized_user_project)
|
||||
bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?(:pypi_max_file_size, params[:content].size)
|
||||
|
@ -135,7 +135,7 @@ module API
|
|||
forbidden!
|
||||
end
|
||||
|
||||
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true
|
||||
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
||||
post 'authorize' do
|
||||
authorize_workhorse!(
|
||||
subject: authorized_user_project,
|
||||
|
|
|
@ -18783,6 +18783,9 @@ msgstr ""
|
|||
msgid "Profiles| You are going to change the username %{currentUsernameBold} to %{newUsernameBold}. Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group. Please update your Git repository remotes as soon as possible."
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|%{provider} Active"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|@username"
|
||||
msgstr ""
|
||||
|
||||
|
@ -18834,7 +18837,7 @@ msgstr ""
|
|||
msgid "Profiles|Commit email"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|Connect"
|
||||
msgid "Profiles|Connect %{provider}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|Connected Accounts"
|
||||
|
@ -18858,6 +18861,9 @@ msgstr ""
|
|||
msgid "Profiles|Disconnect"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|Disconnect %{provider}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|Do not show on profile"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -22,7 +22,12 @@ RSpec.describe 'Merge request > User assigns themselves' do
|
|||
end
|
||||
|
||||
it 'updates updated_by', :js do
|
||||
expect { click_button 'assign yourself' }.to change { merge_request.reload.updated_at }
|
||||
expect do
|
||||
click_button 'assign yourself'
|
||||
|
||||
expect(find('.assignee')).to have_content(user.name)
|
||||
wait_for_all_requests
|
||||
end.to change { merge_request.reload.updated_at }
|
||||
end
|
||||
|
||||
it 'returns user to the merge request', :js do
|
||||
|
|
|
@ -9,6 +9,39 @@ RSpec.describe 'Profile > Account', :js do
|
|||
sign_in(user)
|
||||
end
|
||||
|
||||
describe 'Social sign-in' do
|
||||
context 'when an identity does not exist' do
|
||||
before do
|
||||
allow(Devise).to receive_messages(omniauth_configs: { google_oauth2: {} })
|
||||
end
|
||||
|
||||
it 'allows the user to connect' do
|
||||
visit profile_account_path
|
||||
|
||||
expect(page).to have_link('Connect Google', href: '/users/auth/google_oauth2')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an identity already exists' do
|
||||
before do
|
||||
allow(Devise).to receive_messages(omniauth_configs: { twitter: {}, saml: {} })
|
||||
|
||||
create(:identity, user: user, provider: :twitter)
|
||||
create(:identity, user: user, provider: :saml)
|
||||
|
||||
visit profile_account_path
|
||||
end
|
||||
|
||||
it 'allows the user to disconnect when there is an existing identity' do
|
||||
expect(page).to have_link('Disconnect Twitter', href: '/profile/account/unlink?provider=twitter')
|
||||
end
|
||||
|
||||
it 'shows active for a provider that is not allowed to unlink' do
|
||||
expect(page).to have_content('Saml Active')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Change username' do
|
||||
let(:new_username) { 'bar' }
|
||||
let(:new_user_path) { "/#{new_username}" }
|
||||
|
|
|
@ -8,8 +8,9 @@ import createNoteMutation from '~/design_management/graphql/mutations/create_not
|
|||
import toggleResolveDiscussionMutation from '~/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql';
|
||||
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
|
||||
import ToggleRepliesWidget from '~/design_management/components/design_notes/toggle_replies_widget.vue';
|
||||
import mockDiscussion from '../../mock_data/discussion';
|
||||
|
||||
const discussion = {
|
||||
const defaultMockDiscussion = {
|
||||
id: '0',
|
||||
resolved: false,
|
||||
resolvable: true,
|
||||
|
@ -49,7 +50,7 @@ describe('Design discussions component', () => {
|
|||
wrapper = mount(DesignDiscussion, {
|
||||
propsData: {
|
||||
resolvedDiscussionsExpanded: true,
|
||||
discussion,
|
||||
discussion: defaultMockDiscussion,
|
||||
noteableId: 'noteable-id',
|
||||
designId: 'design-id',
|
||||
discussionIndex: 1,
|
||||
|
@ -82,7 +83,7 @@ describe('Design discussions component', () => {
|
|||
beforeEach(() => {
|
||||
createComponent({
|
||||
discussion: {
|
||||
...discussion,
|
||||
...defaultMockDiscussion,
|
||||
resolvable: false,
|
||||
},
|
||||
});
|
||||
|
@ -125,7 +126,7 @@ describe('Design discussions component', () => {
|
|||
|
||||
it('renders a checkbox with Resolve thread text in reply form', () => {
|
||||
findReplyPlaceholder().vm.$emit('onClick');
|
||||
wrapper.setProps({ discussionWithOpenForm: discussion.id });
|
||||
wrapper.setProps({ discussionWithOpenForm: defaultMockDiscussion.id });
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findResolveCheckbox().text()).toBe('Resolve thread');
|
||||
|
@ -141,7 +142,7 @@ describe('Design discussions component', () => {
|
|||
beforeEach(() => {
|
||||
createComponent({
|
||||
discussion: {
|
||||
...discussion,
|
||||
...defaultMockDiscussion,
|
||||
resolved: true,
|
||||
resolvedBy: notes[0].author,
|
||||
resolvedAt: '2020-05-08T07:10:45Z',
|
||||
|
@ -206,7 +207,7 @@ describe('Design discussions component', () => {
|
|||
|
||||
it('renders a checkbox with Unresolve thread text in reply form', () => {
|
||||
findReplyPlaceholder().vm.$emit('onClick');
|
||||
wrapper.setProps({ discussionWithOpenForm: discussion.id });
|
||||
wrapper.setProps({ discussionWithOpenForm: defaultMockDiscussion.id });
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findResolveCheckbox().text()).toBe('Unresolve thread');
|
||||
|
@ -218,7 +219,7 @@ describe('Design discussions component', () => {
|
|||
it('hides reply placeholder and opens form on placeholder click', () => {
|
||||
createComponent();
|
||||
findReplyPlaceholder().vm.$emit('onClick');
|
||||
wrapper.setProps({ discussionWithOpenForm: discussion.id });
|
||||
wrapper.setProps({ discussionWithOpenForm: defaultMockDiscussion.id });
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findReplyPlaceholder().exists()).toBe(false);
|
||||
|
@ -228,7 +229,7 @@ describe('Design discussions component', () => {
|
|||
|
||||
it('calls mutation on submitting form and closes the form', () => {
|
||||
createComponent(
|
||||
{ discussionWithOpenForm: discussion.id },
|
||||
{ discussionWithOpenForm: defaultMockDiscussion.id },
|
||||
{ discussionComment: 'test', isFormRendered: true },
|
||||
);
|
||||
|
||||
|
@ -246,7 +247,7 @@ describe('Design discussions component', () => {
|
|||
|
||||
it('clears the discussion comment on closing comment form', () => {
|
||||
createComponent(
|
||||
{ discussionWithOpenForm: discussion.id },
|
||||
{ discussionWithOpenForm: defaultMockDiscussion.id },
|
||||
{ discussionComment: 'test', isFormRendered: true },
|
||||
);
|
||||
|
||||
|
@ -263,19 +264,26 @@ describe('Design discussions component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('applies correct class to design notes when discussion is highlighted', () => {
|
||||
createComponent(
|
||||
{},
|
||||
{
|
||||
activeDiscussion: {
|
||||
id: notes[0].id,
|
||||
source: 'pin',
|
||||
},
|
||||
},
|
||||
);
|
||||
describe('when any note from a discussion is active', () => {
|
||||
it.each([notes[0], notes[0].discussion.notes.nodes[1]])(
|
||||
'applies correct class to all notes in the active discussion',
|
||||
note => {
|
||||
createComponent(
|
||||
{ discussion: mockDiscussion },
|
||||
{
|
||||
activeDiscussion: {
|
||||
id: note.id,
|
||||
source: 'pin',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(wrapper.findAll(DesignNote).wrappers.every(note => note.classes('gl-bg-blue-50'))).toBe(
|
||||
true,
|
||||
expect(
|
||||
wrapper
|
||||
.findAll(DesignNote)
|
||||
.wrappers.every(designNote => designNote.classes('gl-bg-blue-50')),
|
||||
).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -285,7 +293,7 @@ describe('Design discussions component', () => {
|
|||
expect(mutate).toHaveBeenCalledWith({
|
||||
mutation: toggleResolveDiscussionMutation,
|
||||
variables: {
|
||||
id: discussion.id,
|
||||
id: defaultMockDiscussion.id,
|
||||
resolve: true,
|
||||
},
|
||||
});
|
||||
|
@ -296,7 +304,7 @@ describe('Design discussions component', () => {
|
|||
|
||||
it('calls toggleResolveDiscussion mutation after adding a note if checkbox was checked', () => {
|
||||
createComponent(
|
||||
{ discussionWithOpenForm: discussion.id },
|
||||
{ discussionWithOpenForm: defaultMockDiscussion.id },
|
||||
{ discussionComment: 'test', isFormRendered: true },
|
||||
);
|
||||
findResolveButton().trigger('click');
|
||||
|
@ -306,7 +314,7 @@ describe('Design discussions component', () => {
|
|||
expect(mutate).toHaveBeenCalledWith({
|
||||
mutation: toggleResolveDiscussionMutation,
|
||||
variables: {
|
||||
id: discussion.id,
|
||||
id: defaultMockDiscussion.id,
|
||||
resolve: true,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -86,16 +86,6 @@ describe('Design note component', () => {
|
|||
expect(wrapper.find(TimeAgoTooltip).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should trigger a scrollIntoView method', () => {
|
||||
createComponent({
|
||||
note,
|
||||
});
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(scrollIntoViewMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render edit icon when user does not have a permission', () => {
|
||||
createComponent({
|
||||
note,
|
||||
|
|
|
@ -13,8 +13,9 @@ describe('Design overlay component', () => {
|
|||
|
||||
const findAllNotes = () => wrapper.findAll('.js-image-badge');
|
||||
const findCommentBadge = () => wrapper.find('.comment-indicator');
|
||||
const findFirstBadge = () => findAllNotes().at(0);
|
||||
const findSecondBadge = () => findAllNotes().at(1);
|
||||
const findBadgeAtIndex = noteIndex => findAllNotes().at(noteIndex);
|
||||
const findFirstBadge = () => findBadgeAtIndex(0);
|
||||
const findSecondBadge = () => findBadgeAtIndex(1);
|
||||
|
||||
const clickAndDragBadge = (elem, fromPoint, toPoint) => {
|
||||
elem.trigger('mousedown', { clientX: fromPoint.x, clientY: fromPoint.y });
|
||||
|
@ -104,16 +105,43 @@ describe('Design overlay component', () => {
|
|||
expect(findSecondBadge().classes()).toContain('resolved');
|
||||
});
|
||||
|
||||
it('when there is an active discussion, should apply inactive class to all pins besides the active one', () => {
|
||||
wrapper.setData({
|
||||
activeDiscussion: {
|
||||
id: notes[0].id,
|
||||
source: 'discussion',
|
||||
},
|
||||
describe('when no discussion is active', () => {
|
||||
it('should not apply inactive class to any pins', () => {
|
||||
expect(
|
||||
findAllNotes(0).wrappers.every(designNote => designNote.classes('gl-bg-blue-50')),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findSecondBadge().classes()).toContain('inactive');
|
||||
describe('when a discussion is active', () => {
|
||||
it.each([notes[0].discussion.notes.nodes[1], notes[0].discussion.notes.nodes[0]])(
|
||||
'should not apply inactive class to the pin for the active discussion',
|
||||
note => {
|
||||
wrapper.setData({
|
||||
activeDiscussion: {
|
||||
id: note.id,
|
||||
source: 'discussion',
|
||||
},
|
||||
});
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findBadgeAtIndex(0).classes()).not.toContain('inactive');
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('should apply inactive class to all pins besides the active one', () => {
|
||||
wrapper.setData({
|
||||
activeDiscussion: {
|
||||
id: notes[0].id,
|
||||
source: 'discussion',
|
||||
},
|
||||
});
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findSecondBadge().classes()).toContain('inactive');
|
||||
expect(findFirstBadge().classes()).not.toContain('inactive');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
export default {
|
||||
id: 'discussion-id-1',
|
||||
resolved: false,
|
||||
resolvable: true,
|
||||
notes: [
|
||||
{
|
||||
id: 'note-id-1',
|
||||
index: 1,
|
||||
position: {
|
||||
height: 100,
|
||||
width: 100,
|
||||
x: 10,
|
||||
y: 15,
|
||||
},
|
||||
author: {
|
||||
name: 'John',
|
||||
webUrl: 'link-to-john-profile',
|
||||
},
|
||||
createdAt: '2020-05-08T07:10:45Z',
|
||||
userPermissions: {
|
||||
adminNote: true,
|
||||
},
|
||||
resolved: false,
|
||||
},
|
||||
{
|
||||
id: 'note-id-3',
|
||||
index: 3,
|
||||
position: {
|
||||
height: 50,
|
||||
width: 50,
|
||||
x: 25,
|
||||
y: 25,
|
||||
},
|
||||
author: {
|
||||
name: 'Mary',
|
||||
webUrl: 'link-to-mary-profile',
|
||||
},
|
||||
createdAt: '2020-05-08T07:10:45Z',
|
||||
userPermissions: {
|
||||
adminNote: true,
|
||||
},
|
||||
resolved: false,
|
||||
},
|
||||
],
|
||||
};
|
|
@ -1,46 +1,44 @@
|
|||
import DISCUSSION_1 from './discussion';
|
||||
|
||||
const DISCUSSION_2 = {
|
||||
id: 'discussion-id-2',
|
||||
notes: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'note-id-2',
|
||||
index: 2,
|
||||
position: {
|
||||
height: 50,
|
||||
width: 50,
|
||||
x: 25,
|
||||
y: 25,
|
||||
},
|
||||
author: {
|
||||
name: 'Mary',
|
||||
webUrl: 'link-to-mary-profile',
|
||||
},
|
||||
createdAt: '2020-05-08T07:10:45Z',
|
||||
userPermissions: {
|
||||
adminNote: true,
|
||||
},
|
||||
resolved: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default [
|
||||
{
|
||||
id: 'note-id-1',
|
||||
index: 1,
|
||||
position: {
|
||||
height: 100,
|
||||
width: 100,
|
||||
x: 10,
|
||||
y: 15,
|
||||
},
|
||||
author: {
|
||||
name: 'John',
|
||||
webUrl: 'link-to-john-profile',
|
||||
},
|
||||
createdAt: '2020-05-08T07:10:45Z',
|
||||
userPermissions: {
|
||||
adminNote: true,
|
||||
},
|
||||
...DISCUSSION_1.notes[0],
|
||||
discussion: {
|
||||
id: 'discussion-id-1',
|
||||
id: DISCUSSION_1.id,
|
||||
notes: {
|
||||
nodes: DISCUSSION_1.notes,
|
||||
},
|
||||
},
|
||||
resolved: false,
|
||||
},
|
||||
{
|
||||
id: 'note-id-2',
|
||||
index: 2,
|
||||
position: {
|
||||
height: 50,
|
||||
width: 50,
|
||||
x: 25,
|
||||
y: 25,
|
||||
},
|
||||
author: {
|
||||
name: 'Mary',
|
||||
webUrl: 'link-to-mary-profile',
|
||||
},
|
||||
createdAt: '2020-05-08T07:10:45Z',
|
||||
userPermissions: {
|
||||
adminNote: true,
|
||||
},
|
||||
discussion: {
|
||||
id: 'discussion-id-2',
|
||||
},
|
||||
resolved: true,
|
||||
...DISCUSSION_2.notes.nodes[0],
|
||||
discussion: DISCUSSION_2,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -1,141 +0,0 @@
|
|||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import VueRouter from 'vue-router';
|
||||
import VueDraggable from 'vuedraggable';
|
||||
import Design from '~/design_management/components/list/item.vue';
|
||||
import createRouter from '~/design_management/router';
|
||||
import getDesignListQuery from '~/design_management/graphql/queries/get_design_list.query.graphql';
|
||||
import permissionsQuery from '~/design_management/graphql/queries/design_permissions.query.graphql';
|
||||
import moveDesignMutation from '~/design_management/graphql/mutations/move_design.mutation.graphql';
|
||||
import { deprecatedCreateFlash as createFlash } from '~/flash';
|
||||
import createMockApollo from '../../helpers/mock_apollo_helper';
|
||||
import Index from '~/design_management/pages/index.vue';
|
||||
import {
|
||||
designListQueryResponse,
|
||||
permissionsQueryResponse,
|
||||
moveDesignMutationResponse,
|
||||
reorderedDesigns,
|
||||
moveDesignMutationResponseWithErrors,
|
||||
} from '../mock_data/apollo_mock';
|
||||
|
||||
jest.mock('~/flash');
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueApollo);
|
||||
|
||||
const router = createRouter();
|
||||
localVue.use(VueRouter);
|
||||
|
||||
const designToMove = {
|
||||
__typename: 'Design',
|
||||
id: '2',
|
||||
event: 'NONE',
|
||||
filename: 'fox_2.jpg',
|
||||
notesCount: 2,
|
||||
image: 'image-2',
|
||||
imageV432x230: 'image-2',
|
||||
};
|
||||
|
||||
describe('Design management index page with Apollo mock', () => {
|
||||
let wrapper;
|
||||
let fakeApollo;
|
||||
let moveDesignHandler;
|
||||
|
||||
async function moveDesigns(localWrapper) {
|
||||
await jest.runOnlyPendingTimers();
|
||||
await localWrapper.vm.$nextTick();
|
||||
|
||||
localWrapper.find(VueDraggable).vm.$emit('input', reorderedDesigns);
|
||||
localWrapper.find(VueDraggable).vm.$emit('change', {
|
||||
moved: {
|
||||
newIndex: 0,
|
||||
element: designToMove,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const findDesigns = () => wrapper.findAll(Design);
|
||||
|
||||
function createComponent({
|
||||
moveHandler = jest.fn().mockResolvedValue(moveDesignMutationResponse),
|
||||
}) {
|
||||
moveDesignHandler = moveHandler;
|
||||
|
||||
const requestHandlers = [
|
||||
[getDesignListQuery, jest.fn().mockResolvedValue(designListQueryResponse)],
|
||||
[permissionsQuery, jest.fn().mockResolvedValue(permissionsQueryResponse)],
|
||||
[moveDesignMutation, moveDesignHandler],
|
||||
];
|
||||
|
||||
fakeApollo = createMockApollo(requestHandlers);
|
||||
wrapper = shallowMount(Index, {
|
||||
localVue,
|
||||
apolloProvider: fakeApollo,
|
||||
router,
|
||||
stubs: { VueDraggable },
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
it('has a design with id 1 as a first one', async () => {
|
||||
createComponent({});
|
||||
|
||||
await jest.runOnlyPendingTimers();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findDesigns()).toHaveLength(3);
|
||||
expect(
|
||||
findDesigns()
|
||||
.at(0)
|
||||
.props('id'),
|
||||
).toBe('1');
|
||||
});
|
||||
|
||||
it('calls a mutation with correct parameters and reorders designs', async () => {
|
||||
createComponent({});
|
||||
|
||||
await moveDesigns(wrapper);
|
||||
|
||||
expect(moveDesignHandler).toHaveBeenCalled();
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(
|
||||
findDesigns()
|
||||
.at(0)
|
||||
.props('id'),
|
||||
).toBe('2');
|
||||
});
|
||||
|
||||
it('displays flash if mutation had a recoverable error', async () => {
|
||||
createComponent({
|
||||
moveHandler: jest.fn().mockResolvedValue(moveDesignMutationResponseWithErrors),
|
||||
});
|
||||
|
||||
await moveDesigns(wrapper);
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(createFlash).toHaveBeenCalledWith('Houston, we have a problem');
|
||||
});
|
||||
|
||||
it('displays flash if mutation had a non-recoverable error', async () => {
|
||||
createComponent({
|
||||
moveHandler: jest.fn().mockRejectedValue('Error'),
|
||||
});
|
||||
|
||||
await moveDesigns(wrapper);
|
||||
|
||||
await wrapper.vm.$nextTick(); // kick off the DOM update
|
||||
await jest.runOnlyPendingTimers(); // kick off the mocked GQL stuff (promises)
|
||||
await wrapper.vm.$nextTick(); // kick off the DOM update for flash
|
||||
|
||||
expect(createFlash).toHaveBeenCalledWith(
|
||||
'Something went wrong when reordering designs. Please try again',
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,13 +1,15 @@
|
|||
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||
import { ApolloMutation } from 'vue-apollo';
|
||||
import VueApollo, { ApolloMutation } from 'vue-apollo';
|
||||
import VueDraggable from 'vuedraggable';
|
||||
import VueRouter from 'vue-router';
|
||||
import { GlEmptyState } from '@gitlab/ui';
|
||||
import createMockApollo from 'jest/helpers/mock_apollo_helper';
|
||||
import Index from '~/design_management/pages/index.vue';
|
||||
import uploadDesignQuery from '~/design_management/graphql/mutations/upload_design.mutation.graphql';
|
||||
import DesignDestroyer from '~/design_management/components/design_destroyer.vue';
|
||||
import DesignDropzone from '~/design_management/components/upload/design_dropzone.vue';
|
||||
import DeleteButton from '~/design_management/components/delete_button.vue';
|
||||
import Design from '~/design_management/components/list/item.vue';
|
||||
import { DESIGNS_ROUTE_NAME } from '~/design_management/router/constants';
|
||||
import {
|
||||
EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE,
|
||||
|
@ -17,6 +19,16 @@ import { deprecatedCreateFlash as createFlash } from '~/flash';
|
|||
import createRouter from '~/design_management/router';
|
||||
import * as utils from '~/design_management/utils/design_management_utils';
|
||||
import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management/constants';
|
||||
import {
|
||||
designListQueryResponse,
|
||||
permissionsQueryResponse,
|
||||
moveDesignMutationResponse,
|
||||
reorderedDesigns,
|
||||
moveDesignMutationResponseWithErrors,
|
||||
} from '../mock_data/apollo_mock';
|
||||
import getDesignListQuery from '~/design_management/graphql/queries/get_design_list.query.graphql';
|
||||
import permissionsQuery from '~/design_management/graphql/queries/design_permissions.query.graphql';
|
||||
import moveDesignMutation from '~/design_management/graphql/mutations/move_design.mutation.graphql';
|
||||
|
||||
jest.mock('~/flash.js');
|
||||
const mockPageEl = {
|
||||
|
@ -61,9 +73,21 @@ const mockVersion = {
|
|||
id: 'gid://gitlab/DesignManagement::Version/1',
|
||||
};
|
||||
|
||||
const designToMove = {
|
||||
__typename: 'Design',
|
||||
id: '2',
|
||||
event: 'NONE',
|
||||
filename: 'fox_2.jpg',
|
||||
notesCount: 2,
|
||||
image: 'image-2',
|
||||
imageV432x230: 'image-2',
|
||||
};
|
||||
|
||||
describe('Design management index page', () => {
|
||||
let mutate;
|
||||
let wrapper;
|
||||
let fakeApollo;
|
||||
let moveDesignHandler;
|
||||
|
||||
const findDesignCheckboxes = () => wrapper.findAll('.design-checkbox');
|
||||
const findSelectAllButton = () => wrapper.find('.js-select-all');
|
||||
|
@ -74,6 +98,20 @@ describe('Design management index page', () => {
|
|||
const findDropzoneWrapper = () => wrapper.find('[data-testid="design-dropzone-wrapper"]');
|
||||
const findFirstDropzoneWithDesign = () => wrapper.findAll(DesignDropzone).at(1);
|
||||
const findDesignsWrapper = () => wrapper.find('[data-testid="designs-root"]');
|
||||
const findDesigns = () => wrapper.findAll(Design);
|
||||
|
||||
async function moveDesigns(localWrapper) {
|
||||
await jest.runOnlyPendingTimers();
|
||||
await localWrapper.vm.$nextTick();
|
||||
|
||||
localWrapper.find(VueDraggable).vm.$emit('input', reorderedDesigns);
|
||||
localWrapper.find(VueDraggable).vm.$emit('change', {
|
||||
moved: {
|
||||
newIndex: 0,
|
||||
element: designToMove,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function createComponent({
|
||||
loading = false,
|
||||
|
@ -118,8 +156,30 @@ describe('Design management index page', () => {
|
|||
});
|
||||
}
|
||||
|
||||
function createComponentWithApollo({
|
||||
moveHandler = jest.fn().mockResolvedValue(moveDesignMutationResponse),
|
||||
}) {
|
||||
localVue.use(VueApollo);
|
||||
moveDesignHandler = moveHandler;
|
||||
|
||||
const requestHandlers = [
|
||||
[getDesignListQuery, jest.fn().mockResolvedValue(designListQueryResponse)],
|
||||
[permissionsQuery, jest.fn().mockResolvedValue(permissionsQueryResponse)],
|
||||
[moveDesignMutation, moveDesignHandler],
|
||||
];
|
||||
|
||||
fakeApollo = createMockApollo(requestHandlers);
|
||||
wrapper = shallowMount(Index, {
|
||||
localVue,
|
||||
apolloProvider: fakeApollo,
|
||||
router,
|
||||
stubs: { VueDraggable },
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
describe('designs', () => {
|
||||
|
@ -584,4 +644,64 @@ describe('Design management index page', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with mocked Apollo client', () => {
|
||||
it('has a design with id 1 as a first one', async () => {
|
||||
createComponentWithApollo({});
|
||||
|
||||
await jest.runOnlyPendingTimers();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findDesigns()).toHaveLength(3);
|
||||
expect(
|
||||
findDesigns()
|
||||
.at(0)
|
||||
.props('id'),
|
||||
).toBe('1');
|
||||
});
|
||||
|
||||
it('calls a mutation with correct parameters and reorders designs', async () => {
|
||||
createComponentWithApollo({});
|
||||
|
||||
await moveDesigns(wrapper);
|
||||
|
||||
expect(moveDesignHandler).toHaveBeenCalled();
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(
|
||||
findDesigns()
|
||||
.at(0)
|
||||
.props('id'),
|
||||
).toBe('2');
|
||||
});
|
||||
|
||||
it('displays flash if mutation had a recoverable error', async () => {
|
||||
createComponentWithApollo({
|
||||
moveHandler: jest.fn().mockResolvedValue(moveDesignMutationResponseWithErrors),
|
||||
});
|
||||
|
||||
await moveDesigns(wrapper);
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(createFlash).toHaveBeenCalledWith('Houston, we have a problem');
|
||||
});
|
||||
|
||||
it('displays flash if mutation had a non-recoverable error', async () => {
|
||||
createComponentWithApollo({
|
||||
moveHandler: jest.fn().mockRejectedValue('Error'),
|
||||
});
|
||||
|
||||
await moveDesigns(wrapper);
|
||||
|
||||
await wrapper.vm.$nextTick(); // kick off the DOM update
|
||||
await jest.runOnlyPendingTimers(); // kick off the mocked GQL stuff (promises)
|
||||
await wrapper.vm.$nextTick(); // kick off the DOM update for flash
|
||||
|
||||
expect(createFlash).toHaveBeenCalledWith(
|
||||
'Something went wrong when reordering designs. Please try again',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -45,7 +45,7 @@ RSpec.describe Gitlab::Auth::OAuth::Provider do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#config_for' do
|
||||
describe '.config_for' do
|
||||
context 'for an LDAP provider' do
|
||||
context 'when the provider exists' do
|
||||
it 'returns the config' do
|
||||
|
@ -91,4 +91,46 @@ RSpec.describe Gitlab::Auth::OAuth::Provider do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.label_for' do
|
||||
subject { described_class.label_for(name) }
|
||||
|
||||
context 'when configuration specifies a custom label' do
|
||||
let(:name) { 'google_oauth2' }
|
||||
let(:label) { 'Custom Google Provider' }
|
||||
let(:provider) { OpenStruct.new({ 'name' => name, 'label' => label }) }
|
||||
|
||||
before do
|
||||
stub_omniauth_setting(providers: [provider])
|
||||
end
|
||||
|
||||
it 'returns the custom label name' do
|
||||
expect(subject).to eq(label)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when configuration does not specify a custom label' do
|
||||
let(:provider) { OpenStruct.new({ 'name' => name } ) }
|
||||
|
||||
before do
|
||||
stub_omniauth_setting(providers: [provider])
|
||||
end
|
||||
|
||||
context 'when the name does not correspond to a label mapping' do
|
||||
let(:name) { 'twitter' }
|
||||
|
||||
it 'returns the titleized name' do
|
||||
expect(subject).to eq(name.titleize)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the name corresponds to a label mapping' do
|
||||
let(:name) { 'gitlab' }
|
||||
|
||||
it 'returns the mapped name' do
|
||||
expect(subject).to eq('GitLab.com')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,6 +11,7 @@ RSpec.describe API::PypiPackages do
|
|||
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
|
||||
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
|
||||
let_it_be(:job) { create(:ci_build, :running, user: user) }
|
||||
|
||||
describe 'GET /api/v4/projects/:id/packages/pypi/simple/:package_name' do
|
||||
let_it_be(:package) { create(:pypi_package, project: project) }
|
||||
|
@ -58,6 +59,8 @@ RSpec.describe API::PypiPackages do
|
|||
|
||||
it_behaves_like 'deploy token for package GET requests'
|
||||
|
||||
it_behaves_like 'job token for package GET requests'
|
||||
|
||||
it_behaves_like 'rejects PyPI access with unknown project id'
|
||||
end
|
||||
|
||||
|
@ -108,6 +111,8 @@ RSpec.describe API::PypiPackages do
|
|||
|
||||
it_behaves_like 'deploy token for package uploads'
|
||||
|
||||
it_behaves_like 'job token for package uploads'
|
||||
|
||||
it_behaves_like 'rejects PyPI access with unknown project id'
|
||||
end
|
||||
|
||||
|
@ -198,6 +203,8 @@ RSpec.describe API::PypiPackages do
|
|||
|
||||
it_behaves_like 'deploy token for package uploads'
|
||||
|
||||
it_behaves_like 'job token for package uploads'
|
||||
|
||||
it_behaves_like 'rejects PyPI access with unknown project id'
|
||||
|
||||
context 'file size above maximum limit' do
|
||||
|
@ -273,6 +280,26 @@ RSpec.describe API::PypiPackages do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with job token headers' do
|
||||
let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token) }
|
||||
|
||||
context 'valid token' do
|
||||
it_behaves_like 'returning response status', :success
|
||||
end
|
||||
|
||||
context 'invalid token' do
|
||||
let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar') }
|
||||
|
||||
it_behaves_like 'returning response status', :success
|
||||
end
|
||||
|
||||
context 'invalid user' do
|
||||
let(:headers) { basic_auth_header('foo', job.token) }
|
||||
|
||||
it_behaves_like 'returning response status', :success
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'rejects PyPI access with unknown project id'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -70,3 +70,59 @@ RSpec.shared_examples 'does not cause n^2 queries' do
|
|||
end.not_to exceed_query_limit(control)
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'job token for package GET requests' do
|
||||
context 'with job token headers' do
|
||||
let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token) }
|
||||
|
||||
subject { get api(url), headers: headers }
|
||||
|
||||
before do
|
||||
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
context 'valid token' do
|
||||
it_behaves_like 'returning response status', :success
|
||||
end
|
||||
|
||||
context 'invalid token' do
|
||||
let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar') }
|
||||
|
||||
it_behaves_like 'returning response status', :unauthorized
|
||||
end
|
||||
|
||||
context 'invalid user' do
|
||||
let(:headers) { basic_auth_header('foo', job.token) }
|
||||
|
||||
it_behaves_like 'returning response status', :unauthorized
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'job token for package uploads' do
|
||||
context 'with job token headers' do
|
||||
let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token).merge(workhorse_header) }
|
||||
|
||||
before do
|
||||
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
context 'valid token' do
|
||||
it_behaves_like 'returning response status', :success
|
||||
end
|
||||
|
||||
context 'invalid token' do
|
||||
let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar').merge(workhorse_header) }
|
||||
|
||||
it_behaves_like 'returning response status', :unauthorized
|
||||
end
|
||||
|
||||
context 'invalid user' do
|
||||
let(:headers) { basic_auth_header('foo', job.token).merge(workhorse_header) }
|
||||
|
||||
it_behaves_like 'returning response status', :unauthorized
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue