Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-11-17 09:09:23 +00:00
parent d49d44c810
commit 6fa3630aad
39 changed files with 471 additions and 89 deletions

View file

@ -15,9 +15,6 @@ tasks:
cd /workspace/gitlab-development-kit
[[ ! -L /workspace/gitlab-development-kit/gitlab ]] && ln -fs /workspace/gitlab /workspace/gitlab-development-kit/gitlab
mv /workspace/gitlab-development-kit/secrets.yml /workspace/gitlab-development-kit/gitlab/config
# make webpack static, prevents that GitLab tries to connect to localhost webpack from browser outside the workspace
echo "webpack:" >> gdk.yml
echo " static: true" >> gdk.yml
# reconfigure GDK
echo "$(date) Reconfiguring GDK" | tee -a /workspace/startup.log
gdk reconfigure
@ -43,6 +40,7 @@ tasks:
fi
# start GDK
echo "$(date) Starting GDK" | tee -a /workspace/startup.log
export DEV_SERVER_PUBLIC_ADDR=$(gp url 3808)
export RAILS_HOSTS=$(gp url 3000 | sed -e 's+^http[s]*://++')
gdk start
# Run DB migrations

View file

@ -1,11 +1,13 @@
<script>
import { GlButton, GlDropdown, GlDropdownItem, GlIcon, GlLink, GlModal } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import createFlash from '~/flash';
import createFlash, { FLASH_TYPES } from '~/flash';
import { IssuableType } from '~/issuable_show/constants';
import { IssuableStatus, IssueStateEvent } from '~/issue_show/constants';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { visitUrl } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
import promoteToEpicMutation from '../queries/promote_to_epic.mutation.graphql';
import updateIssueMutation from '../queries/update_issue.mutation.graphql';
export default {
@ -24,10 +26,21 @@ export default {
text: __('Yes, close issue'),
attributes: [{ variant: 'warning' }],
},
i18n: {
promoteErrorMessage: __(
'Something went wrong while promoting the issue to an epic. Please try again.',
),
promoteSuccessMessage: __(
'The issue was successfully promoted to an epic. Redirecting to epic...',
),
},
inject: {
canCreateIssue: {
default: false,
},
canPromoteToEpic: {
default: false,
},
canReopenIssue: {
default: false,
},
@ -135,6 +148,37 @@ export default {
this.isUpdatingState = false;
});
},
promoteToEpic() {
this.isUpdatingState = true;
this.$apollo
.mutate({
mutation: promoteToEpicMutation,
variables: {
input: {
iid: this.iid,
projectPath: this.projectPath,
},
},
})
.then(({ data }) => {
if (data.promoteToEpic.errors.length) {
createFlash({ message: data.promoteToEpic.errors.join('; ') });
return;
}
createFlash({
message: this.$options.i18n.promoteSuccessMessage,
type: FLASH_TYPES.SUCCESS,
});
visitUrl(data.promoteToEpic.epic.webPath);
})
.catch(() => createFlash({ message: this.$options.i18n.promoteErrorMessage }))
.finally(() => {
this.isUpdatingState = false;
});
},
},
};
</script>
@ -152,6 +196,9 @@ export default {
<gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath">
{{ newIssueTypeText }}
</gl-dropdown-item>
<gl-dropdown-item v-if="canPromoteToEpic" :disabled="isUpdatingState" @click="promoteToEpic">
{{ __('Promote to epic') }}
</gl-dropdown-item>
<gl-dropdown-item v-if="!isIssueAuthor" :href="reportAbusePath">
{{ __('Report abuse') }}
</gl-dropdown-item>
@ -190,6 +237,14 @@ export default {
<gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath">
{{ newIssueTypeText }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="canPromoteToEpic"
:disabled="isUpdatingState"
data-testid="promote-button"
@click="promoteToEpic"
>
{{ __('Promote to epic') }}
</gl-dropdown-item>
<gl-dropdown-item v-if="!isIssueAuthor" :href="reportAbusePath">
{{ __('Report abuse') }}
</gl-dropdown-item>

View file

@ -45,6 +45,7 @@ export function initIssueHeaderActions(store) {
store,
provide: {
canCreateIssue: parseBoolean(el.dataset.canCreateIssue),
canPromoteToEpic: parseBoolean(el.dataset.canPromoteToEpic),
canReopenIssue: parseBoolean(el.dataset.canReopenIssue),
canReportSpam: parseBoolean(el.dataset.canReportSpam),
canUpdateIssue: parseBoolean(el.dataset.canUpdateIssue),

View file

@ -0,0 +1,8 @@
mutation promoteToEpic($input: PromoteToEpicInput!) {
promoteToEpic(input: $input) {
epic {
webPath
}
errors
}
}

View file

@ -2,8 +2,6 @@ import UsersSelect from '~/users_select';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import initBoards from '~/boards';
document.addEventListener('DOMContentLoaded', () => {
new UsersSelect(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
initBoards();
});
new UsersSelect(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
initBoards();

View file

@ -1,3 +1,3 @@
import initForm from '../../../../shared/milestones/form';
document.addEventListener('DOMContentLoaded', () => initForm(false));
initForm(false);

View file

@ -1,3 +1,3 @@
import initForm from '../../../../shared/milestones/form';
document.addEventListener('DOMContentLoaded', () => initForm(false));
initForm(false);

View file

@ -4,13 +4,11 @@ import Group from '~/group';
import GroupPathValidator from './group_path_validator';
import initFilePickers from '~/file_pickers';
document.addEventListener('DOMContentLoaded', () => {
const parentId = $('#group_parent_id');
if (!parentId.val()) {
new GroupPathValidator(); // eslint-disable-line no-new
}
BindInOut.initAll();
initFilePickers();
const parentId = $('#group_parent_id');
if (!parentId.val()) {
new GroupPathValidator(); // eslint-disable-line no-new
}
BindInOut.initAll();
initFilePickers();
return new Group();
});
new Group(); // eslint-disable-line no-new

View file

@ -1,7 +1,5 @@
import initPackageList from '~/packages/list/packages_list_app_bundle';
document.addEventListener('DOMContentLoaded', () => {
if (document.getElementById('js-vue-packages-list')) {
initPackageList();
}
});
if (document.getElementById('js-vue-packages-list')) {
initPackageList();
}

View file

@ -141,10 +141,9 @@ export default {
addMultipleToDiscussionWarning() {
return sprintf(
__(
'%{icon}You are about to add %{usersTag} people to the discussion. They will all receive a notification.',
'You are about to add %{usersTag} people to the discussion. They will all receive a notification.',
),
{
icon: '<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>',
usersTag: `<strong><span class="js-referenced-users-count">${this.referencedUsers.length}</span></strong>`,
},
false,
@ -293,6 +292,7 @@ export default {
<template v-if="previewMarkdown && !markdownPreviewLoading">
<div v-if="referencedCommands" class="referenced-commands" v-html="referencedCommands"></div>
<div v-if="shouldShowReferencedUsers" class="referenced-users">
<gl-icon name="warning-solid" />
<span v-html="addMultipleToDiscussionWarning"></span>
</div>
</template>

View file

@ -201,6 +201,15 @@ $line-removed-dark: $red-200;
// Misc component overrides that should live elsewhere
.gl-label {
filter: brightness(0.9) contrast(1.1);
// This applies to the gl-label markups
// rendered and cached in the backend (labels_helper.rb)
&.gl-label-scoped {
.gl-label-text-scoped,
.gl-label-close {
color: $gray-900;
}
}
}
// white-ish text for light labels
@ -210,6 +219,15 @@ $line-removed-dark: $red-200;
color: $gray-900;
}
// This applies to "gl-labels" from "gitlab-ui"
.gl-label.gl-label-scoped.gl-label-text-dark,
.gl-label.gl-label-scoped.gl-label-text-light {
.gl-label-text-scoped,
.gl-label-close {
color: $gray-900;
}
}
// duplicated class as the original .atwho-view style is added later
.atwho-view.atwho-view {
background-color: $white;

View file

@ -14,7 +14,7 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:dag_pipeline_tab, project, default_enabled: true)
push_frontend_feature_flag(:pipelines_security_report_summary, project)
push_frontend_feature_flag(:new_pipeline_form, project)
push_frontend_feature_flag(:new_pipeline_form, project, default_enabled: true)
push_frontend_feature_flag(:graphql_pipeline_header, project, type: :development, default_enabled: false)
push_frontend_feature_flag(:graphql_pipeline_details, project, type: :development, default_enabled: false)
push_frontend_feature_flag(:new_pipeline_form_prefilled_vars, project, type: :development)

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
module Types
class ContainerRepositoryCleanupStatusEnum < BaseEnum
graphql_name 'ContainerRepositoryCleanupStatus'
description 'Status of the tags cleanup of a container repository'
value 'UNSCHEDULED', value: 'cleanup_unscheduled', description: 'The tags cleanup is not scheduled. This is the default state.'
value 'SCHEDULED', value: 'cleanup_scheduled', description: 'The tags cleanup is scheduled and is going to be executed shortly.'
value 'UNFINISHED', value: 'cleanup_unfinished', description: 'The tags cleanup has been partially executed. There are still remaining tags to delete.'
value 'ONGOING', value: 'cleanup_ongoing', description: 'The tags cleanup is ongoing.'
end
end

View file

@ -15,6 +15,7 @@ module Types
field :created_at, Types::TimeType, null: false, description: 'Timestamp when the container repository was created.'
field :updated_at, Types::TimeType, null: false, description: 'Timestamp when the container repository was updated.'
field :expiration_policy_started_at, Types::TimeType, null: true, description: 'Timestamp when the cleanup done by the expiration policy was started on the container repository.'
field :expiration_policy_cleanup_status, Types::ContainerRepositoryCleanupStatusEnum, null: true, description: 'The tags cleanup status for the container repository.'
field :status, Types::ContainerRepositoryStatusEnum, null: true, description: 'Status of the container repository.'
field :tags_count, GraphQL::INT_TYPE, null: false, description: 'Number of tags associated with this image.'
field :can_delete, GraphQL::BOOLEAN_TYPE, null: false, description: 'Can the current user delete the container repository.'

View file

@ -22,7 +22,7 @@
.dropdown.inline.js-ca-dropdown
%button.dropdown-menu-toggle{ "data-toggle" => "dropdown", :type => "button" }
%span.dropdown-label {{ n__('Last %d day', 'Last %d days', 30) }}
%i.fa.fa-chevron-down
= sprite_icon("chevron-down", css_class: "dropdown-menu-toggle-icon gl-top-3")
%ul.dropdown-menu.dropdown-menu-right
%li
%a{ "href" => "#", "data-value" => "7" }

View file

@ -6,7 +6,7 @@
= s_('Pipeline|Run Pipeline')
%hr
- if Feature.enabled?(:new_pipeline_form, @project)
- if Feature.enabled?(:new_pipeline_form, @project, default_enabled: true)
#js-new-pipeline{ data: { project_id: @project.id,
pipelines_path: project_pipelines_path(@project),
config_variables_path: config_variables_namespace_project_pipelines_path(@project.namespace, @project),

View file

@ -0,0 +1,5 @@
---
title: Add cleanup status field to graphQL ContainerRepositoryType
merge_request: 47544
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Replace fa-chevron-down in project level VSA
merge_request: 47885
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Replace fa-exclamation-triangle in markdown field MERGE_REQUEST_ID
merge_request: 47786
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Default enable new_pipeline_form
merge_request: 46915
author:
type: added

View file

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/229632
milestone: '13.2'
type: development
group: group::continuous integration
default_enabled: false
default_enabled: true

View file

@ -18,6 +18,7 @@ const IS_DEV_SERVER = process.env.WEBPACK_DEV_SERVER === 'true';
const IS_EE = require('./helpers/is_ee_env');
const DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost';
const DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
const DEV_SERVER_PUBLIC_ADDR = process.env.DEV_SERVER_PUBLIC_ADDR;
const DEV_SERVER_HTTPS = process.env.DEV_SERVER_HTTPS && process.env.DEV_SERVER_HTTPS !== 'false';
const DEV_SERVER_LIVERELOAD = IS_DEV_SERVER && process.env.DEV_SERVER_LIVERELOAD !== 'false';
const WEBPACK_REPORT = process.env.WEBPACK_REPORT && process.env.WEBPACK_REPORT !== 'false';
@ -554,6 +555,7 @@ module.exports = {
devServer: {
host: DEV_SERVER_HOST,
port: DEV_SERVER_PORT,
public: DEV_SERVER_PUBLIC_ADDR,
https: DEV_SERVER_HTTPS,
headers: {
'Access-Control-Allow-Origin': '*',

View file

@ -3313,6 +3313,11 @@ type ContainerRepository {
"""
createdAt: Time!
"""
The tags cleanup status for the container repository.
"""
expirationPolicyCleanupStatus: ContainerRepositoryCleanupStatus
"""
Timestamp when the cleanup done by the expiration policy was started on the container repository.
"""
@ -3354,6 +3359,31 @@ type ContainerRepository {
updatedAt: Time!
}
"""
Status of the tags cleanup of a container repository
"""
enum ContainerRepositoryCleanupStatus {
"""
The tags cleanup is ongoing.
"""
ONGOING
"""
The tags cleanup is scheduled and is going to be executed shortly.
"""
SCHEDULED
"""
The tags cleanup has been partially executed. There are still remaining tags to delete.
"""
UNFINISHED
"""
The tags cleanup is not scheduled. This is the default state.
"""
UNSCHEDULED
}
"""
The connection type for ContainerRepository.
"""
@ -3388,6 +3418,11 @@ type ContainerRepositoryDetails {
"""
createdAt: Time!
"""
The tags cleanup status for the container repository.
"""
expirationPolicyCleanupStatus: ContainerRepositoryCleanupStatus
"""
Timestamp when the cleanup done by the expiration policy was started on the container repository.
"""

View file

@ -8947,6 +8947,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "expirationPolicyCleanupStatus",
"description": "The tags cleanup status for the container repository.",
"args": [
],
"type": {
"kind": "ENUM",
"name": "ContainerRepositoryCleanupStatus",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "expirationPolicyStartedAt",
"description": "Timestamp when the cleanup done by the expiration policy was started on the container repository.",
@ -9091,6 +9105,41 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "ContainerRepositoryCleanupStatus",
"description": "Status of the tags cleanup of a container repository",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "UNSCHEDULED",
"description": "The tags cleanup is not scheduled. This is the default state.",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "SCHEDULED",
"description": "The tags cleanup is scheduled and is going to be executed shortly.",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "UNFINISHED",
"description": "The tags cleanup has been partially executed. There are still remaining tags to delete.",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "ONGOING",
"description": "The tags cleanup is ongoing.",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ContainerRepositoryConnection",
@ -9199,6 +9248,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "expirationPolicyCleanupStatus",
"description": "The tags cleanup status for the container repository.",
"args": [
],
"type": {
"kind": "ENUM",
"name": "ContainerRepositoryCleanupStatus",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "expirationPolicyStartedAt",
"description": "Timestamp when the cleanup done by the expiration policy was started on the container repository.",

View file

@ -535,6 +535,7 @@ A container repository.
| ----- | ---- | ----------- |
| `canDelete` | Boolean! | Can the current user delete the container repository. |
| `createdAt` | Time! | Timestamp when the container repository was created. |
| `expirationPolicyCleanupStatus` | ContainerRepositoryCleanupStatus | The tags cleanup status for the container repository. |
| `expirationPolicyStartedAt` | Time | Timestamp when the cleanup done by the expiration policy was started on the container repository. |
| `id` | ID! | ID of the container repository. |
| `location` | String! | URL of the container repository. |
@ -552,6 +553,7 @@ Details of a container repository.
| ----- | ---- | ----------- |
| `canDelete` | Boolean! | Can the current user delete the container repository. |
| `createdAt` | Time! | Timestamp when the container repository was created. |
| `expirationPolicyCleanupStatus` | ContainerRepositoryCleanupStatus | The tags cleanup status for the container repository. |
| `expirationPolicyStartedAt` | Time | Timestamp when the cleanup done by the expiration policy was started on the container repository. |
| `id` | ID! | ID of the container repository. |
| `location` | String! | URL of the container repository. |
@ -3812,6 +3814,17 @@ Mode of a commit action.
| `SEVEN_DAYS` | 7 days until tags are automatically removed |
| `THIRTY_DAYS` | 30 days until tags are automatically removed |
### ContainerRepositoryCleanupStatus
Status of the tags cleanup of a container repository.
| Value | Description |
| ----- | ----------- |
| `ONGOING` | The tags cleanup is ongoing. |
| `SCHEDULED` | The tags cleanup is scheduled and is going to be executed shortly. |
| `UNFINISHED` | The tags cleanup has been partially executed. There are still remaining tags to delete. |
| `UNSCHEDULED` | The tags cleanup is not scheduled. This is the default state. |
### ContainerRepositoryStatus
Status of a container repository.

View file

@ -397,7 +397,7 @@ field :foo, GraphQL::STRING_TYPE,
'if `my_feature_flag` feature flag is disabled'
def foo
object.foo unless Feature.enabled?(:my_feature_flag, object)
object.foo if Feature.enabled?(:my_feature_flag, object)
end
```

View file

@ -63,6 +63,8 @@ job finishes but the DAST job fails, the security dashboard doesn't show SAST re
the analyzer outputs an
[exit code](../../../development/integrations/secure.md#exit-code).
You can filter the vulnerabilities list by selecting from the **Severity** and **Scanner** dropdowns.
## Project Security Dashboard
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/235558) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.6.
@ -105,6 +107,11 @@ You can filter the vulnerabilities by one or more of the following:
| Severity | Critical, High, Medium, Low, Info, Unknown |
| Scanner | [Available Scanners](../index.md#security-scanning-tools) |
You can filter the vulnerabilities list by selecting from the **Status**, **Severity**, and
**Scanner** dropdowns. In the **Scanner** dropdown, select individual scanners or scanner groups to
toggle those scanners. The **Scanner** dropdown includes both GitLab scanners, and in GitLab 13.6
and later, custom scanners.
You can also dismiss vulnerabilities in the table:
1. Select the checkbox for each vulnerability you want to dismiss.
@ -260,6 +267,11 @@ You can filter which vulnerabilities the vulnerability report displays by:
| Scanner | [Available Scanners](../index.md#security-scanning-tools) |
| Project | Projects configured in the Security Center settings |
You can filter the vulnerabilities list by selecting from the **Status**, **Severity**, and
**Scanner**, and **Project** dropdowns. In the **Scanner** dropdown, select individual scanners or
scanner groups to toggle those scanners. The **Scanner** dropdown includes both GitLab scanners, and
in GitLab 13.6 and later, custom scanners.
Clicking any vulnerability in the table takes you to its
[Vulnerability Details](../vulnerabilities) page to see more information on that vulnerability.
To create an issue associated with the vulnerability, click the **Create Issue** button.

View file

@ -95,12 +95,13 @@ While you can view and manage the full details of an issue on the [issue page](#
you can also work with multiple issues at a time using the [Issues List](#issues-list),
[Issue Boards](#issue-boards), Issue references, and [Epics](#epics)**(PREMIUM)**.
Key actions for Issues include:
Key actions for issues include:
- [Creating issues](managing_issues.md#create-a-new-issue)
- [Moving issues](managing_issues.md#moving-issues)
- [Closing issues](managing_issues.md#closing-issues)
- [Deleting issues](managing_issues.md#deleting-issues)
- [Promoting issues](managing_issues.md#promote-an-issue-to-an-epic) **(PREMIUM)**
### Issue page

View file

@ -7,9 +7,15 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Managing issues
[GitLab Issues](index.md) are the fundamental medium for collaborating on ideas and
planning work in GitLab. [Creating](#create-a-new-issue), [moving](#moving-issues),
[closing](#closing-issues), and [deleting](#deleting-issues) are key actions that
you can do with issues.
planning work in GitLab.
Key actions for issues include:
- [Creating issues](#create-a-new-issue)
- [Moving issues](#moving-issues)
- [Closing issues](#closing-issues)
- [Deleting issues](#deleting-issues)
- [Promoting issues](#promote-an-issue-to-an-epic) **(PREMIUM)**
## Create a new issue
@ -280,6 +286,23 @@ editing it and clicking on the delete button.
![delete issue - button](img/delete_issue.png)
## Promote an issue to an epic **(PREMIUM)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3777) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.6.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/37081) to [GitLab Premium](https://about.gitlab.com/pricing/) in 12.8.
> - Promoting issues to epics via the UI [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/233974) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.6.
You can promote an issue to an epic in the immediate parent group.
To promote an issue to an epic:
1. In an issue, select the vertical ellipsis (**{ellipsis_v}**) button.
1. Select **Promote to epic**.
Alternatively, you can use the `/promote` [quick action](../quick_actions.md#quick-actions-for-issues-merge-requests-and-epics).
Read more about promoting an issue to an epic on the [Manage epics page](../../group/epics/manage_epics.md#promote-an-issue-to-an-epic).
## Add an issue to an iteration **(STARTER)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216158) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.2.

View file

@ -528,9 +528,6 @@ msgstr ""
msgid "%{host} sign-in from new location"
msgstr ""
msgid "%{icon}You are about to add %{usersTag} people to the discussion. They will all receive a notification."
msgstr ""
msgid "%{integrations_link_start}Integrations%{link_end} enable you to make third-party applications part of your GitLab workflow. If the available integrations don't meet your needs, consider using a %{webhooks_link_start}webhook%{link_end}."
msgstr ""
@ -7707,6 +7704,9 @@ msgstr ""
msgid "Could not restore the group"
msgstr ""
msgid "Could not retrieve custom scanners for scanner filter. Please try again later."
msgstr ""
msgid "Could not revoke impersonation token %{token_name}."
msgstr ""
@ -21810,6 +21810,9 @@ msgstr ""
msgid "Promote issue to an epic"
msgstr ""
msgid "Promote to epic"
msgstr ""
msgid "Promote to group label"
msgstr ""
@ -25341,6 +25344,9 @@ msgstr ""
msgid "Something went wrong while performing the action."
msgstr ""
msgid "Something went wrong while promoting the issue to an epic. Please try again."
msgstr ""
msgid "Something went wrong while reopening a requirement."
msgstr ""
@ -26956,6 +26962,9 @@ msgstr ""
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr ""
msgid "The issue was successfully promoted to an epic. Redirecting to epic..."
msgstr ""
msgid "The license for Deploy Board is required to use this feature."
msgstr ""
@ -30764,6 +30773,9 @@ msgstr ""
msgid "You already have pending todo for this alert"
msgstr ""
msgid "You are about to add %{usersTag} people to the discussion. They will all receive a notification."
msgstr ""
msgid "You are about to delete %{domain} from your instance. This domain will no longer be available to any Knative application."
msgstr ""

View file

@ -0,0 +1,12 @@
{
"type": "array",
"items": {
"type": "object",
"required": ["node"],
"properties": {
"node": {
"$ref": "./container_repository.json"
}
}
}
}

View file

@ -1,6 +1,6 @@
{
"type": "object",
"required": ["id", "name", "path", "location", "createdAt", "updatedAt", "tagsCount", "canDelete"],
"required": ["id", "name", "path", "location", "createdAt", "updatedAt", "tagsCount", "canDelete", "expirationPolicyCleanupStatus"],
"properties": {
"id": {
"type": "string"
@ -31,6 +31,10 @@
},
"canDelete": {
"type": "boolean"
},
"expirationPolicyCleanupStatus": {
"type": "string",
"enum": ["UNSCHEDULED", "SCHEDULED", "UNFINISHED", "ONGOING"]
}
}
}

View file

@ -1,37 +1,8 @@
{
"type": "object",
"required": ["id", "name", "path", "location", "createdAt", "updatedAt", "tagsCount", "canDelete", "tags"],
"required": ["tags"],
"allOf": [{ "$ref": "./container_repository.json" }],
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"location": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"updatedAt": {
"type": "string"
},
"expirationPolicyStartedAt": {
"type": ["string", "null"]
},
"status": {
"type": ["string", "null"]
},
"tagsCount": {
"type": "integer"
},
"canDelete": {
"type": "boolean"
},
"tags": {
"type": "object",
"required": ["nodes"],

View file

@ -1,14 +1,21 @@
import { GlButton, GlDropdown, GlDropdownItem, GlLink, GlModal } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import createFlash, { FLASH_TYPES } from '~/flash';
import { IssuableType } from '~/issuable_show/constants';
import HeaderActions from '~/issue_show/components/header_actions.vue';
import { IssuableStatus, IssueStateEvent } from '~/issue_show/constants';
import promoteToEpicMutation from '~/issue_show/queries/promote_to_epic.mutation.graphql';
import * as urlUtility from '~/lib/utils/url_utility';
import createStore from '~/notes/stores';
jest.mock('~/flash');
describe('HeaderActions component', () => {
let dispatchEventSpy;
let mutateMock;
let wrapper;
let visitUrlSpy;
const localVue = createLocalVue();
localVue.use(Vuex);
@ -16,6 +23,7 @@ describe('HeaderActions component', () => {
const defaultProps = {
canCreateIssue: true,
canPromoteToEpic: true,
canReopenIssue: true,
canReportSpam: true,
canUpdateIssue: true,
@ -29,7 +37,27 @@ describe('HeaderActions component', () => {
submitAsSpamPath: 'gitlab-org/gitlab-test/-/issues/32/submit_as_spam',
};
const mutate = jest.fn().mockResolvedValue({ data: { updateIssue: { errors: [] } } });
const updateIssueMutationResponse = { data: { updateIssue: { errors: [] } } };
const promoteToEpicMutationResponse = {
data: {
promoteToEpic: {
errors: [],
epic: {
webPath: '/groups/gitlab-org/-/epics/1',
},
},
},
};
const promoteToEpicMutationErrorResponse = {
data: {
promoteToEpic: {
errors: ['The issue has already been promoted to an epic.'],
epic: {},
},
},
};
const findToggleIssueStateButton = () => wrapper.find(GlButton);
@ -50,7 +78,10 @@ describe('HeaderActions component', () => {
props = {},
issueState = IssuableStatus.Open,
blockedByIssues = [],
mutateResponse = {},
} = {}) => {
mutateMock = jest.fn().mockResolvedValue(mutateResponse);
store.getters.getNoteableData.state = issueState;
store.getters.getNoteableData.blocked_by_issues = blockedByIssues;
@ -63,7 +94,7 @@ describe('HeaderActions component', () => {
},
mocks: {
$apollo: {
mutate,
mutate: mutateMock,
},
},
});
@ -73,6 +104,9 @@ describe('HeaderActions component', () => {
if (dispatchEventSpy) {
dispatchEventSpy.mockRestore();
}
if (visitUrlSpy) {
visitUrlSpy.mockRestore();
}
wrapper.destroy();
});
@ -90,7 +124,11 @@ describe('HeaderActions component', () => {
beforeEach(() => {
dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
wrapper = mountComponent({ props: { issueType }, issueState });
wrapper = mountComponent({
props: { issueType },
issueState,
mutateResponse: updateIssueMutationResponse,
});
});
it(`has text "${buttonText}"`, () => {
@ -100,11 +138,11 @@ describe('HeaderActions component', () => {
it('calls apollo mutation', () => {
findToggleIssueStateButton().vm.$emit('click');
expect(mutate).toHaveBeenCalledWith(
expect(mutateMock).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
input: {
iid: defaultProps.iid.toString(),
iid: defaultProps.iid,
projectPath: defaultProps.projectPath,
stateEvent: newIssueState,
},
@ -129,15 +167,17 @@ describe('HeaderActions component', () => {
${'desktop dropdown'} | ${false} | ${findDesktopDropdownItems}
`('$description', ({ isCloseIssueItemVisible, findDropdownItems }) => {
describe.each`
description | itemText | isItemVisible | canUpdateIssue | canCreateIssue | isIssueAuthor | canReportSpam
${`when user can update ${issueType}`} | ${`Close ${issueType}`} | ${isCloseIssueItemVisible} | ${true} | ${true} | ${true} | ${true}
${`when user cannot update ${issueType}`} | ${`Close ${issueType}`} | ${false} | ${false} | ${true} | ${true} | ${true}
${`when user can create ${issueType}`} | ${`New ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user cannot create ${issueType}`} | ${`New ${issueType}`} | ${false} | ${true} | ${false} | ${true} | ${true}
${'when user can report abuse'} | ${'Report abuse'} | ${true} | ${true} | ${true} | ${false} | ${true}
${'when user cannot report abuse'} | ${'Report abuse'} | ${false} | ${true} | ${true} | ${true} | ${true}
${'when user can submit as spam'} | ${'Submit as spam'} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user cannot submit as spam'} | ${'Submit as spam'} | ${false} | ${true} | ${true} | ${true} | ${false}
description | itemText | isItemVisible | canUpdateIssue | canCreateIssue | isIssueAuthor | canReportSpam | canPromoteToEpic
${`when user can update ${issueType}`} | ${`Close ${issueType}`} | ${isCloseIssueItemVisible} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user cannot update ${issueType}`} | ${`Close ${issueType}`} | ${false} | ${false} | ${true} | ${true} | ${true} | ${true}
${`when user can create ${issueType}`} | ${`New ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user cannot create ${issueType}`} | ${`New ${issueType}`} | ${false} | ${true} | ${false} | ${true} | ${true} | ${true}
${'when user can promote to epic'} | ${'Promote to epic'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user cannot promote to epic'} | ${'Promote to epic'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${false}
${'when user can report abuse'} | ${'Report abuse'} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true}
${'when user cannot report abuse'} | ${'Report abuse'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user can submit as spam'} | ${'Submit as spam'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user cannot submit as spam'} | ${'Submit as spam'} | ${false} | ${true} | ${true} | ${true} | ${false} | ${true}
`(
'$description',
({
@ -147,6 +187,7 @@ describe('HeaderActions component', () => {
canCreateIssue,
isIssueAuthor,
canReportSpam,
canPromoteToEpic,
}) => {
beforeEach(() => {
wrapper = mountComponent({
@ -156,6 +197,7 @@ describe('HeaderActions component', () => {
isIssueAuthor,
issueType,
canReportSpam,
canPromoteToEpic,
},
});
});
@ -172,6 +214,65 @@ describe('HeaderActions component', () => {
});
});
describe('when "Promote to epic" button is clicked', () => {
describe('when response is successful', () => {
beforeEach(() => {
visitUrlSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({});
wrapper = mountComponent({
mutateResponse: promoteToEpicMutationResponse,
});
wrapper.find('[data-testid="promote-button"]').vm.$emit('click');
});
it('invokes GraphQL mutation when clicked', () => {
expect(mutateMock).toHaveBeenCalledWith(
expect.objectContaining({
mutation: promoteToEpicMutation,
variables: {
input: {
iid: defaultProps.iid,
projectPath: defaultProps.projectPath,
},
},
}),
);
});
it('shows a success message and tells the user they are being redirected', () => {
expect(createFlash).toHaveBeenCalledWith({
message: 'The issue was successfully promoted to an epic. Redirecting to epic...',
type: FLASH_TYPES.SUCCESS,
});
});
it('redirects to newly created epic path', () => {
expect(visitUrlSpy).toHaveBeenCalledWith(
promoteToEpicMutationResponse.data.promoteToEpic.epic.webPath,
);
});
});
describe('when response contains errors', () => {
beforeEach(() => {
visitUrlSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({});
wrapper = mountComponent({
mutateResponse: promoteToEpicMutationErrorResponse,
});
wrapper.find('[data-testid="promote-button"]').vm.$emit('click');
});
it('shows an error message', () => {
expect(createFlash).toHaveBeenCalledWith({
message: promoteToEpicMutationErrorResponse.data.promoteToEpic.errors.join('; '),
});
});
});
});
describe('modal', () => {
const blockedByIssues = [
{ iid: 13, web_url: 'gitlab-org/gitlab-test/-/issues/13' },
@ -197,7 +298,7 @@ describe('HeaderActions component', () => {
it('calls apollo mutation when primary button is clicked', () => {
findModal().vm.$emit('primary');
expect(mutate).toHaveBeenCalledWith(
expect(mutateMock).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
input: {

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['ContainerRepositoryCleanupStatus'] do
it 'exposes all statuses' do
expected_keys = ContainerRepository.expiration_policy_cleanup_statuses
.keys
.map { |k| k.gsub('cleanup_', '') }
.map(&:upcase)
expect(described_class.values.keys).to contain_exactly(*expected_keys)
end
end

View file

@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['ContainerRepositoryDetails'] do
fields = %i[id name path location created_at updated_at expiration_policy_started_at status tags_count can_delete tags]
fields = %i[id name path location created_at updated_at expiration_policy_started_at status tags_count can_delete expiration_policy_cleanup_status tags]
it { expect(described_class.graphql_name).to eq('ContainerRepositoryDetails') }

View file

@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['ContainerRepository'] do
fields = %i[id name path location created_at updated_at expiration_policy_started_at status tags_count can_delete]
fields = %i[id name path location created_at updated_at expiration_policy_started_at status tags_count can_delete expiration_policy_cleanup_status]
it { expect(described_class.graphql_name).to eq('ContainerRepository') }
@ -20,4 +20,12 @@ RSpec.describe GitlabSchema.types['ContainerRepository'] do
is_expected.to have_graphql_type(Types::ContainerRepositoryStatusEnum)
end
end
describe 'expiration_policy_cleanup_status field' do
subject { described_class.fields['expirationPolicyCleanupStatus'] }
it 'returns cleanup status enum' do
is_expected.to have_graphql_type(Types::ContainerRepositoryCleanupStatusEnum)
end
end
end

View file

@ -34,7 +34,7 @@ RSpec.describe 'container repository details' do
subject
end
it 'matches the expected schema' do
it 'matches the JSON schema' do
expect(container_repository_details_response).to match_schema('graphql/container_repository_details')
end
end

View file

@ -47,6 +47,10 @@ RSpec.describe 'getting container repositories in a project' do
before do
subject
end
it 'matches the JSON schema' do
expect(container_repositories_response).to match_schema('graphql/container_repositories')
end
end
context 'with different permissions' do