Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitpod.yml4
-rw-r--r--app/assets/javascripts/issue_show/components/header_actions.vue57
-rw-r--r--app/assets/javascripts/issue_show/issue.js1
-rw-r--r--app/assets/javascripts/issue_show/queries/promote_to_epic.mutation.graphql8
-rw-r--r--app/assets/javascripts/pages/groups/boards/index.js8
-rw-r--r--app/assets/javascripts/pages/groups/milestones/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/milestones/new/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/new/index.js16
-rw-r--r--app/assets/javascripts/pages/groups/packages/index/index.js8
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue4
-rw-r--r--app/assets/stylesheets/themes/_dark.scss18
-rw-r--r--app/controllers/projects/pipelines_controller.rb2
-rw-r--r--app/graphql/types/container_repository_cleanup_status_enum.rb13
-rw-r--r--app/graphql/types/container_repository_type.rb1
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml2
-rw-r--r--app/views/projects/pipelines/new.html.haml2
-rw-r--r--changelogs/unreleased/10io-add-cleanup-status-to-container-repository-details-graphql.yml5
-rw-r--r--changelogs/unreleased/mw-replace-fa-chevron-duown-in-project-level-vsa.yml5
-rw-r--r--changelogs/unreleased/mw-replace-fa-exclamation-triangle-markdown.yml5
-rw-r--r--changelogs/unreleased/new-pipeline-form-default-true.yml5
-rw-r--r--config/feature_flags/development/new_pipeline_form.yml2
-rw-r--r--config/webpack.config.js2
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql35
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json63
-rw-r--r--doc/api/graphql/reference/index.md13
-rw-r--r--doc/development/api_graphql_styleguide.md2
-rw-r--r--doc/user/application_security/security_dashboard/index.md12
-rw-r--r--doc/user/project/issues/index.md3
-rw-r--r--doc/user/project/issues/managing_issues.md29
-rw-r--r--locale/gitlab.pot18
-rw-r--r--spec/fixtures/api/schemas/graphql/container_repositories.json12
-rw-r--r--spec/fixtures/api/schemas/graphql/container_repository.json6
-rw-r--r--spec/fixtures/api/schemas/graphql/container_repository_details.json33
-rw-r--r--spec/frontend/issue_show/components/header_actions_spec.js131
-rw-r--r--spec/graphql/types/container_repository_cleanup_status_enum_spec.rb13
-rw-r--r--spec/graphql/types/container_repository_details_type_spec.rb2
-rw-r--r--spec/graphql/types/container_repository_type_spec.rb10
-rw-r--r--spec/requests/api/graphql/container_repository/container_repository_details_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/container_repositories_spec.rb4
39 files changed, 471 insertions, 89 deletions
diff --git a/.gitpod.yml b/.gitpod.yml
index 6a771e77769..dc1dbc1472d 100644
--- a/.gitpod.yml
+++ b/.gitpod.yml
@@ -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
diff --git a/app/assets/javascripts/issue_show/components/header_actions.vue b/app/assets/javascripts/issue_show/components/header_actions.vue
index 165fa745485..ae0aca0fd27 100644
--- a/app/assets/javascripts/issue_show/components/header_actions.vue
+++ b/app/assets/javascripts/issue_show/components/header_actions.vue
@@ -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>
diff --git a/app/assets/javascripts/issue_show/issue.js b/app/assets/javascripts/issue_show/issue.js
index 3d0d8d882be..8260460828b 100644
--- a/app/assets/javascripts/issue_show/issue.js
+++ b/app/assets/javascripts/issue_show/issue.js
@@ -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),
diff --git a/app/assets/javascripts/issue_show/queries/promote_to_epic.mutation.graphql b/app/assets/javascripts/issue_show/queries/promote_to_epic.mutation.graphql
new file mode 100644
index 00000000000..12d05af0f5e
--- /dev/null
+++ b/app/assets/javascripts/issue_show/queries/promote_to_epic.mutation.graphql
@@ -0,0 +1,8 @@
+mutation promoteToEpic($input: PromoteToEpicInput!) {
+ promoteToEpic(input: $input) {
+ epic {
+ webPath
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/pages/groups/boards/index.js b/app/assets/javascripts/pages/groups/boards/index.js
index 79c3be771d0..922f39627c9 100644
--- a/app/assets/javascripts/pages/groups/boards/index.js
+++ b/app/assets/javascripts/pages/groups/boards/index.js
@@ -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();
diff --git a/app/assets/javascripts/pages/groups/milestones/edit/index.js b/app/assets/javascripts/pages/groups/milestones/edit/index.js
index ddd10fe5062..af0264c7992 100644
--- a/app/assets/javascripts/pages/groups/milestones/edit/index.js
+++ b/app/assets/javascripts/pages/groups/milestones/edit/index.js
@@ -1,3 +1,3 @@
import initForm from '../../../../shared/milestones/form';
-document.addEventListener('DOMContentLoaded', () => initForm(false));
+initForm(false);
diff --git a/app/assets/javascripts/pages/groups/milestones/new/index.js b/app/assets/javascripts/pages/groups/milestones/new/index.js
index ddd10fe5062..af0264c7992 100644
--- a/app/assets/javascripts/pages/groups/milestones/new/index.js
+++ b/app/assets/javascripts/pages/groups/milestones/new/index.js
@@ -1,3 +1,3 @@
import initForm from '../../../../shared/milestones/form';
-document.addEventListener('DOMContentLoaded', () => initForm(false));
+initForm(false);
diff --git a/app/assets/javascripts/pages/groups/new/index.js b/app/assets/javascripts/pages/groups/new/index.js
index 640e64b5d3e..7021473b380 100644
--- a/app/assets/javascripts/pages/groups/new/index.js
+++ b/app/assets/javascripts/pages/groups/new/index.js
@@ -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
diff --git a/app/assets/javascripts/pages/groups/packages/index/index.js b/app/assets/javascripts/pages/groups/packages/index/index.js
index 4836900aa28..1c4a10fd653 100644
--- a/app/assets/javascripts/pages/groups/packages/index/index.js
+++ b/app/assets/javascripts/pages/groups/packages/index/index.js
@@ -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();
+}
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 1808e4d6ad4..893c0a0a5b9 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -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>
diff --git a/app/assets/stylesheets/themes/_dark.scss b/app/assets/stylesheets/themes/_dark.scss
index 66cc4452858..6ab02bd5e27 100644
--- a/app/assets/stylesheets/themes/_dark.scss
+++ b/app/assets/stylesheets/themes/_dark.scss
@@ -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;
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index c576b700ac1..f71a92ee874 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -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)
diff --git a/app/graphql/types/container_repository_cleanup_status_enum.rb b/app/graphql/types/container_repository_cleanup_status_enum.rb
new file mode 100644
index 00000000000..6e654e65360
--- /dev/null
+++ b/app/graphql/types/container_repository_cleanup_status_enum.rb
@@ -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
diff --git a/app/graphql/types/container_repository_type.rb b/app/graphql/types/container_repository_type.rb
index 2c8382ea058..45d19fdbc50 100644
--- a/app/graphql/types/container_repository_type.rb
+++ b/app/graphql/types/container_repository_type.rb
@@ -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.'
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index d99579c25c0..b98ab9757fa 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -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" }
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index bddd542325b..bc8e6a6d9cc 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -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),
diff --git a/changelogs/unreleased/10io-add-cleanup-status-to-container-repository-details-graphql.yml b/changelogs/unreleased/10io-add-cleanup-status-to-container-repository-details-graphql.yml
new file mode 100644
index 00000000000..ebe84b20539
--- /dev/null
+++ b/changelogs/unreleased/10io-add-cleanup-status-to-container-repository-details-graphql.yml
@@ -0,0 +1,5 @@
+---
+title: Add cleanup status field to graphQL ContainerRepositoryType
+merge_request: 47544
+author:
+type: added
diff --git a/changelogs/unreleased/mw-replace-fa-chevron-duown-in-project-level-vsa.yml b/changelogs/unreleased/mw-replace-fa-chevron-duown-in-project-level-vsa.yml
new file mode 100644
index 00000000000..9055a181995
--- /dev/null
+++ b/changelogs/unreleased/mw-replace-fa-chevron-duown-in-project-level-vsa.yml
@@ -0,0 +1,5 @@
+---
+title: Replace fa-chevron-down in project level VSA
+merge_request: 47885
+author:
+type: changed
diff --git a/changelogs/unreleased/mw-replace-fa-exclamation-triangle-markdown.yml b/changelogs/unreleased/mw-replace-fa-exclamation-triangle-markdown.yml
new file mode 100644
index 00000000000..f1ba024beb7
--- /dev/null
+++ b/changelogs/unreleased/mw-replace-fa-exclamation-triangle-markdown.yml
@@ -0,0 +1,5 @@
+---
+title: Replace fa-exclamation-triangle in markdown field MERGE_REQUEST_ID
+merge_request: 47786
+author:
+type: changed
diff --git a/changelogs/unreleased/new-pipeline-form-default-true.yml b/changelogs/unreleased/new-pipeline-form-default-true.yml
new file mode 100644
index 00000000000..dd12ef458ec
--- /dev/null
+++ b/changelogs/unreleased/new-pipeline-form-default-true.yml
@@ -0,0 +1,5 @@
+---
+title: Default enable new_pipeline_form
+merge_request: 46915
+author:
+type: added
diff --git a/config/feature_flags/development/new_pipeline_form.yml b/config/feature_flags/development/new_pipeline_form.yml
index 4c0ba5af350..3a4b30f4bd9 100644
--- a/config/feature_flags/development/new_pipeline_form.yml
+++ b/config/feature_flags/development/new_pipeline_form.yml
@@ -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
diff --git a/config/webpack.config.js b/config/webpack.config.js
index fe256e17e73..c72583e4b24 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -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': '*',
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index c4232de2b98..7335b35f432 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -3314,6 +3314,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.
"""
expirationPolicyStartedAt: Time
@@ -3355,6 +3360,31 @@ type ContainerRepository {
}
"""
+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.
"""
type ContainerRepositoryConnection {
@@ -3389,6 +3419,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.
"""
expirationPolicyStartedAt: Time
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 305d7dbf08f..4539c6806e1 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -8948,6 +8948,20 @@
"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.",
"args": [
@@ -9092,6 +9106,41 @@
"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",
"description": "The connection type for ContainerRepository.",
@@ -9200,6 +9249,20 @@
"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.",
"args": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 64334895135..f0e0d56d47d 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -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.
diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md
index 231b3bcfd88..0e81a332d6c 100644
--- a/doc/development/api_graphql_styleguide.md
+++ b/doc/development/api_graphql_styleguide.md
@@ -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
```
diff --git a/doc/user/application_security/security_dashboard/index.md b/doc/user/application_security/security_dashboard/index.md
index 9f402cea9dc..1b038ef76a0 100644
--- a/doc/user/application_security/security_dashboard/index.md
+++ b/doc/user/application_security/security_dashboard/index.md
@@ -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.
diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md
index 6be71f540ad..716377f2e45 100644
--- a/doc/user/project/issues/index.md
+++ b/doc/user/project/issues/index.md
@@ -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
diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md
index 8b5d2fcb2e8..62b388ec137 100644
--- a/doc/user/project/issues/managing_issues.md
+++ b/doc/user/project/issues/managing_issues.md
@@ -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.
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index cb068850413..77a877ebde2 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/spec/fixtures/api/schemas/graphql/container_repositories.json b/spec/fixtures/api/schemas/graphql/container_repositories.json
new file mode 100644
index 00000000000..8e8982ff8c7
--- /dev/null
+++ b/spec/fixtures/api/schemas/graphql/container_repositories.json
@@ -0,0 +1,12 @@
+{
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": ["node"],
+ "properties": {
+ "node": {
+ "$ref": "./container_repository.json"
+ }
+ }
+ }
+}
diff --git a/spec/fixtures/api/schemas/graphql/container_repository.json b/spec/fixtures/api/schemas/graphql/container_repository.json
index 0737e71dd17..e252bedab82 100644
--- a/spec/fixtures/api/schemas/graphql/container_repository.json
+++ b/spec/fixtures/api/schemas/graphql/container_repository.json
@@ -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"]
}
}
}
diff --git a/spec/fixtures/api/schemas/graphql/container_repository_details.json b/spec/fixtures/api/schemas/graphql/container_repository_details.json
index b076711dcea..3db91796fc6 100644
--- a/spec/fixtures/api/schemas/graphql/container_repository_details.json
+++ b/spec/fixtures/api/schemas/graphql/container_repository_details.json
@@ -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"],
diff --git a/spec/frontend/issue_show/components/header_actions_spec.js b/spec/frontend/issue_show/components/header_actions_spec.js
index ec9f8ea1dc8..67b8665a889 100644
--- a/spec/frontend/issue_show/components/header_actions_spec.js
+++ b/spec/frontend/issue_show/components/header_actions_spec.js
@@ -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: {
diff --git a/spec/graphql/types/container_repository_cleanup_status_enum_spec.rb b/spec/graphql/types/container_repository_cleanup_status_enum_spec.rb
new file mode 100644
index 00000000000..36cfc789ee9
--- /dev/null
+++ b/spec/graphql/types/container_repository_cleanup_status_enum_spec.rb
@@ -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
diff --git a/spec/graphql/types/container_repository_details_type_spec.rb b/spec/graphql/types/container_repository_details_type_spec.rb
index 13563dbb5aa..b5ff460fcf7 100644
--- a/spec/graphql/types/container_repository_details_type_spec.rb
+++ b/spec/graphql/types/container_repository_details_type_spec.rb
@@ -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') }
diff --git a/spec/graphql/types/container_repository_type_spec.rb b/spec/graphql/types/container_repository_type_spec.rb
index ec5bb14a483..3d3445ba5c3 100644
--- a/spec/graphql/types/container_repository_type_spec.rb
+++ b/spec/graphql/types/container_repository_type_spec.rb
@@ -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
diff --git a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
index a63adb8efc4..3c1c63c1670 100644
--- a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
+++ b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
@@ -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
diff --git a/spec/requests/api/graphql/project/container_repositories_spec.rb b/spec/requests/api/graphql/project/container_repositories_spec.rb
index 428424802a2..7e32f54bf1d 100644
--- a/spec/requests/api/graphql/project/container_repositories_spec.rb
+++ b/spec/requests/api/graphql/project/container_repositories_spec.rb
@@ -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