Commit c4e5e2f1 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 20308a92 b06f1cdd
<script> <script>
/* eslint-disable @gitlab/vue-require-i18n-strings */ /* eslint-disable @gitlab/vue-require-i18n-strings */
import { GlTabs, GlTab, GlBadge } from '@gitlab/ui';
import ModalStore from '../../stores/modal_store'; import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins'; import modalMixin from '../../mixins/modal_mixins';
export default { export default {
components: {
GlTabs,
GlTab,
GlBadge,
},
mixins: [modalMixin], mixins: [modalMixin],
data() { data() {
return ModalStore.store; return ModalStore.store;
...@@ -19,18 +25,18 @@ export default { ...@@ -19,18 +25,18 @@ export default {
}; };
</script> </script>
<template> <template>
<div class="top-area gl-mt-3 gl-mb-3"> <gl-tabs class="gl-mt-3">
<ul class="nav-links issues-state-filters"> <gl-tab @click.prevent="changeTab('all')">
<li :class="{ active: activeTab == 'all' }"> <template slot="title">
<a href="#" role="button" @click.prevent="changeTab('all')"> <span>Open issues</span>
Open issues <span class="badge badge-pill"> {{ issuesCount }} </span> <gl-badge size="sm" class="gl-tab-counter-badge">{{ issuesCount }}</gl-badge>
</a> </template>
</li> </gl-tab>
<li :class="{ active: activeTab == 'selected' }"> <gl-tab @click.prevent="changeTab('selected')">
<a href="#" role="button" @click.prevent="changeTab('selected')"> <template slot="title">
Selected issues <span class="badge badge-pill"> {{ selectedCount }} </span> <span>Selected issues</span>
</a> <gl-badge size="sm" class="gl-tab-counter-badge">{{ selectedCount }}</gl-badge>
</li> </template>
</ul> </gl-tab>
</div> </gl-tabs>
</template> </template>
<script> <script>
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { GlIcon } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui';
import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility'; import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility';
import { import {
DISCUSSION_FILTERS_DEFAULT_VALUE, DISCUSSION_FILTERS_DEFAULT_VALUE,
...@@ -14,7 +13,9 @@ import notesEventHub from '../event_hub'; ...@@ -14,7 +13,9 @@ import notesEventHub from '../event_hub';
export default { export default {
components: { components: {
GlIcon, GlDropdown,
GlDropdownItem,
GlDropdownDivider,
}, },
props: { props: {
filters: { filters: {
...@@ -66,9 +67,6 @@ export default { ...@@ -66,9 +67,6 @@ export default {
selectFilter(value, persistFilter = true) { selectFilter(value, persistFilter = true) {
const filter = parseInt(value, 10); const filter = parseInt(value, 10);
// close dropdown
this.toggleDropdown();
if (filter === this.currentValue) return; if (filter === this.currentValue) return;
this.currentValue = filter; this.currentValue = filter;
this.filterDiscussion({ this.filterDiscussion({
...@@ -78,9 +76,6 @@ export default { ...@@ -78,9 +76,6 @@ export default {
}); });
this.toggleCommentsForm(); this.toggleCommentsForm();
}, },
toggleDropdown() {
$(this.$refs.dropdownToggle).dropdown('toggle');
},
toggleCommentsForm() { toggleCommentsForm() {
this.setCommentsDisabled(this.currentValue === HISTORY_ONLY_FILTER_VALUE); this.setCommentsDisabled(this.currentValue === HISTORY_ONLY_FILTER_VALUE);
}, },
...@@ -92,7 +87,6 @@ export default { ...@@ -92,7 +87,6 @@ export default {
if (/^note_/.test(hash) && this.currentValue !== DISCUSSION_FILTERS_DEFAULT_VALUE) { if (/^note_/.test(hash) && this.currentValue !== DISCUSSION_FILTERS_DEFAULT_VALUE) {
this.selectFilter(this.defaultValue, false); this.selectFilter(this.defaultValue, false);
this.toggleDropdown(); // close dropdown
this.setTargetNoteHash(hash); this.setTargetNoteHash(hash);
} }
}, },
...@@ -109,43 +103,24 @@ export default { ...@@ -109,43 +103,24 @@ export default {
</script> </script>
<template> <template>
<div <gl-dropdown
v-if="displayFilters" v-if="displayFilters"
class="discussion-filter-container js-discussion-filter-container d-inline-block align-bottom full-width-mobile"
>
<button
id="discussion-filter-dropdown" id="discussion-filter-dropdown"
ref="dropdownToggle" class="gl-mr-3 full-width-mobile discussion-filter-container js-discussion-filter-container qa-discussion-filter"
class="btn btn-sm qa-discussion-filter" :text="currentFilter.title"
data-toggle="dropdown"
aria-expanded="false"
> >
{{ currentFilter.title }} <gl-icon name="chevron-down" /> <div v-for="filter in filters" :key="filter.value" class="dropdown-item-wrapper">
</button> <gl-dropdown-item
<div :is-check-item="true"
ref="dropdownMenu" :is-checked="filter.value === currentValue"
class="dropdown-menu dropdown-menu-selectable dropdown-menu-right"
aria-labelledby="discussion-filter-dropdown"
>
<div class="dropdown-content">
<ul>
<li
v-for="filter in filters"
:key="filter.value"
:data-filter-type="filterType(filter.value)"
>
<button
:class="{ 'is-active': filter.value === currentValue }" :class="{ 'is-active': filter.value === currentValue }"
:data-filter-type="filterType(filter.value)"
class="qa-filter-options" class="qa-filter-options"
type="button" @click.prevent="selectFilter(filter.value)"
@click="selectFilter(filter.value)"
> >
{{ filter.title }} {{ filter.title }}
</button> </gl-dropdown-item>
<div v-if="filter.value === defaultValue" class="dropdown-divider"></div> <gl-dropdown-divider v-if="filter.value === defaultValue" />
</li>
</ul>
</div>
</div>
</div> </div>
</gl-dropdown>
</template> </template>
gs
<script> <script>
import { GlIcon } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import { __ } from '~/locale'; import { __ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
...@@ -15,7 +14,8 @@ const SORT_OPTIONS = [ ...@@ -15,7 +14,8 @@ const SORT_OPTIONS = [
export default { export default {
SORT_OPTIONS, SORT_OPTIONS,
components: { components: {
GlIcon, GlDropdown,
GlDropdownItem,
LocalStorageSync, LocalStorageSync,
}, },
mixins: [Tracking.mixin()], mixins: [Tracking.mixin()],
...@@ -49,33 +49,27 @@ export default { ...@@ -49,33 +49,27 @@ export default {
</script> </script>
<template> <template>
<div <div class="gl-mr-3 gl-display-inline-block gl-vertical-align-bottom full-width-mobile">
data-testid="sort-discussion-filter"
class="gl-mr-2 gl-display-inline-block gl-vertical-align-bottom full-width-mobile"
>
<local-storage-sync <local-storage-sync
:value="sortDirection" :value="sortDirection"
:storage-key="storageKey" :storage-key="storageKey"
@input="setDiscussionSortDirection" @input="setDiscussionSortDirection"
/> />
<button class="btn btn-sm js-dropdown-text" data-toggle="dropdown" aria-expanded="false"> <gl-dropdown
{{ dropdownText }} :text="dropdownText"
<gl-icon name="chevron-down" /> data-testid="sort-discussion-filter"
</button> class="js-dropdown-text full-width-mobile"
<div ref="dropdownMenu" class="dropdown-menu dropdown-menu-selectable dropdown-menu-right"> >
<div class="dropdown-content"> <gl-dropdown-item
<ul> v-for="{ text, key, cls } in $options.SORT_OPTIONS"
<li v-for="{ text, key, cls } in $options.SORT_OPTIONS" :key="key"> :key="key"
<button :class="cls"
:class="[cls, { 'is-active': isDropdownItemActive(key) }]" :is-check-item="true"
type="button" :is-checked="isDropdownItemActive(key)"
@click="fetchSortedDiscussions(key)" @click="fetchSortedDiscussions(key)"
> >
{{ text }} {{ text }}
</button> </gl-dropdown-item>
</li> </gl-dropdown>
</ul>
</div>
</div>
</div> </div>
</template> </template>
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import { GlDeprecatedButton, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import Clipboard from 'clipboard'; import Clipboard from 'clipboard';
import { __ } from '~/locale'; import { __ } from '~/locale';
export default { export default {
components: { components: {
GlDeprecatedButton, GlButton,
GlIcon,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
props: { props: {
text: { text: {
type: String, type: String,
...@@ -55,15 +52,12 @@ export default { ...@@ -55,15 +52,12 @@ export default {
default: null, default: null,
}, },
}, },
copySuccessText: __('Copied'), copySuccessText: __('Copied'),
computed: { computed: {
modalDomId() { modalDomId() {
return this.modalId ? `#${this.modalId}` : ''; return this.modalId ? `#${this.modalId}` : '';
}, },
}, },
mounted() { mounted() {
this.$nextTick(() => { this.$nextTick(() => {
this.clipboard = new Clipboard(this.$el, { this.clipboard = new Clipboard(this.$el, {
...@@ -83,13 +77,11 @@ export default { ...@@ -83,13 +77,11 @@ export default {
.on('error', e => this.$emit('error', e)); .on('error', e => this.$emit('error', e));
}); });
}, },
destroyed() { destroyed() {
if (this.clipboard) { if (this.clipboard) {
this.clipboard.destroy(); this.clipboard.destroy();
} }
}, },
methods: { methods: {
updateTooltip(target) { updateTooltip(target) {
const $target = $(target); const $target = $(target);
...@@ -112,15 +104,12 @@ export default { ...@@ -112,15 +104,12 @@ export default {
}; };
</script> </script>
<template> <template>
<gl-deprecated-button <gl-button
v-gl-tooltip="{ placement: tooltipPlacement, container: tooltipContainer }" v-gl-tooltip="{ placement: tooltipPlacement, container: tooltipContainer }"
:class="cssClasses" :class="cssClasses"
:data-clipboard-target="target" :data-clipboard-target="target"
:data-clipboard-text="text" :data-clipboard-text="text"
:title="title" :title="title"
> icon="copy-to-clipboard"
<slot> />
<gl-icon name="copy-to-clipboard" />
</slot>
</gl-deprecated-button>
</template> </template>
# frozen_string_literal: true
# Verifies features availability based on issue type.
# This can be used, for example, for hiding UI elements or blocking specific
# quick actions for particular issue types;
module IssueAvailableFeatures
extend ActiveSupport::Concern
# EE only features are listed on EE::IssueAvailableFeatures
def available_features_for_issue_types
{}.with_indifferent_access
end
def issue_type_supports?(feature)
unless available_features_for_issue_types.has_key?(feature)
raise ArgumentError, 'invalid feature'
end
available_features_for_issue_types[feature].include?(issue_type)
end
end
IssueAvailableFeatures.prepend_if_ee('EE::IssueAvailableFeatures')
...@@ -20,6 +20,7 @@ class Issue < ApplicationRecord ...@@ -20,6 +20,7 @@ class Issue < ApplicationRecord
include StateEventable include StateEventable
include IdInOrdered include IdInOrdered
include Presentable include Presentable
include IssueAvailableFeatures
DueDateStruct = Struct.new(:title, :name).freeze DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
......
---
title: Update issue boards modal to gl-tabs
merge_request: 43740
author:
type: changed
---
title: Update button in modal_copy_button.vue to use GlButton from GitLab UI
merge_request: 43714
author:
type: other
...@@ -310,7 +310,7 @@ The following table gives an overview of how the API functions generally behave. ...@@ -310,7 +310,7 @@ The following table gives an overview of how the API functions generally behave.
The following table shows the possible return codes for API requests. The following table shows the possible return codes for API requests.
| Return values | Description | | Return values | Description |
| ------------- | ----------- | | ------------------------ | ----------- |
| `200 OK` | The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON. | | `200 OK` | The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON. |
| `204 No Content` | The server has successfully fulfilled the request and that there is no additional content to send in the response payload body. | | `204 No Content` | The server has successfully fulfilled the request and that there is no additional content to send in the response payload body. |
| `201 Created` | The `POST` request was successful and the resource is returned as JSON. | | `201 Created` | The `POST` request was successful and the resource is returned as JSON. |
...@@ -323,7 +323,8 @@ The following table shows the possible return codes for API requests. ...@@ -323,7 +323,8 @@ The following table shows the possible return codes for API requests.
| `409 Conflict` | A conflicting resource already exists, e.g., creating a project with a name that already exists. | | `409 Conflict` | A conflicting resource already exists, e.g., creating a project with a name that already exists. |
| `412` | Indicates the request was denied. May happen if the `If-Unmodified-Since` header is provided when trying to delete a resource, which was modified in between. | | `412` | Indicates the request was denied. May happen if the `If-Unmodified-Since` header is provided when trying to delete a resource, which was modified in between. |
| `422 Unprocessable` | The entity could not be processed. | | `422 Unprocessable` | The entity could not be processed. |
| `500 Server Error` | While handling the request something went wrong server-side. | | `429 Too Many Requests` | The user exceeded the [application rate limits](../administration/instance_limits.md#rate-limits). |
| `500 Server Error` | While handling the request, something went wrong server-side. |
## Pagination ## Pagination
......
...@@ -434,5 +434,8 @@ npm dist-tag rm @scope/package@version my-tag # Delete a tag from the package ...@@ -434,5 +434,8 @@ npm dist-tag rm @scope/package@version my-tag # Delete a tag from the package
npm install @scope/package@my-tag # Install a specific tag npm install @scope/package@my-tag # Install a specific tag
``` ```
NOTE: **Note:**
You cannot use your `CI_JOB_TOKEN` or deploy token with the `npm dist-tag` commands. View [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/258835) for details.
CAUTION: **Warning:** CAUTION: **Warning:**
Due to a bug in NPM 6.9.0, deleting dist tags fails. Make sure your NPM version is greater than 6.9.1. Due to a bug in NPM 6.9.0, deleting dist tags fails. Make sure your NPM version is greater than 6.9.1.
...@@ -79,6 +79,9 @@ This will enable the `Bug` dropdown option when creating or editing issues. When ...@@ -79,6 +79,9 @@ This will enable the `Bug` dropdown option when creating or editing issues. When
to the issue description field. The 'Reset template' button will discard any to the issue description field. The 'Reset template' button will discard any
changes you made after picking the template and return it to its initial status. changes you made after picking the template and return it to its initial status.
TIP: **Tip:**
You can create short-cut links to create an issue using a designated template. For example: `https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Feature%20proposal`.
![Description templates](img/description_templates.png) ![Description templates](img/description_templates.png)
## Setting a default template for merge requests and issues **(STARTER)** ## Setting a default template for merge requests and issues **(STARTER)**
......
...@@ -8,8 +8,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w ...@@ -8,8 +8,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5554) in [GitLab 8.11](https://about.gitlab.com/releases/2016/08/22/gitlab-8-11-released/#issue-board). > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5554) in [GitLab 8.11](https://about.gitlab.com/releases/2016/08/22/gitlab-8-11-released/#issue-board).
## Overview
The GitLab Issue Board is a software project management tool used to plan, The GitLab Issue Board is a software project management tool used to plan,
organize, and visualize a workflow for a feature or product release. organize, and visualize a workflow for a feature or product release.
It can be used as a [Kanban](https://en.wikipedia.org/wiki/Kanban_(development)) or a It can be used as a [Kanban](https://en.wikipedia.org/wiki/Kanban_(development)) or a
......
...@@ -8,8 +8,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w ...@@ -8,8 +8,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1904) in [GitLab Starter 9.2](https://about.gitlab.com/releases/2017/05/22/gitlab-9-2-released/#multiple-assignees-for-issues). > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1904) in [GitLab Starter 9.2](https://about.gitlab.com/releases/2017/05/22/gitlab-9-2-released/#multiple-assignees-for-issues).
## Overview
In large teams, where there is shared ownership of an issue, it can be difficult In large teams, where there is shared ownership of an issue, it can be difficult
to track who is working on it, who already completed their contributions, who to track who is working on it, who already completed their contributions, who
didn't even start yet. didn't even start yet.
......
...@@ -6,8 +6,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w ...@@ -6,8 +6,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Labels # Labels
## Overview
As your count of issues, merge requests, and epics grows in GitLab, it's more and more challenging As your count of issues, merge requests, and epics grows in GitLab, it's more and more challenging
to keep track of those items. Especially as your organization grows from just a few people to to keep track of those items. Especially as your organization grows from just a few people to
hundreds or thousands. This is where labels come in. They help you organize and tag your work hundreds or thousands. This is where labels come in. They help you organize and tag your work
......
...@@ -14,8 +14,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w ...@@ -14,8 +14,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> value, so the burndown chart considers them as closed on the milestone > value, so the burndown chart considers them as closed on the milestone
> `start_date`. In that case, a warning will be displayed. > `start_date`. In that case, a warning will be displayed.
## Overview
Burndown Charts are visual representations of the progress of completing a milestone. Burndown Charts are visual representations of the progress of completing a milestone.
![burndown chart](img/burndown_chart.png) ![burndown chart](img/burndown_chart.png)
......
...@@ -7,8 +7,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w ...@@ -7,8 +7,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Milestones # Milestones
## Overview
Milestones in GitLab are a way to track issues and merge requests created to achieve a broader goal in a certain period of time. Milestones in GitLab are a way to track issues and merge requests created to achieve a broader goal in a certain period of time.
Milestones allow you to organize issues and merge requests into a cohesive group, with an optional start date and an optional due date. Milestones allow you to organize issues and merge requests into a cohesive group, with an optional start date and an optional due date.
......
...@@ -10,8 +10,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w ...@@ -10,8 +10,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/214839) to [GitLab Starter](https://about.gitlab.com/pricing/) in 13.0. > - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/214839) to [GitLab Starter](https://about.gitlab.com/pricing/) in 13.0.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/215364) to [GitLab Core](https://about.gitlab.com/pricing/) in 13.2. > - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/215364) to [GitLab Core](https://about.gitlab.com/pricing/) in 13.2.
## Overview
Service Desk is a module that allows your team to connect directly Service Desk is a module that allows your team to connect directly
with any external party through email right inside of GitLab; no external tools required. with any external party through email right inside of GitLab; no external tools required.
An ongoing conversation right where your software is built ensures that user feedback ends An ongoing conversation right where your software is built ensures that user feedback ends
......
...@@ -6,7 +6,7 @@ module EE ...@@ -6,7 +6,7 @@ module EE
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
def supports_epic? def supports_epic?
is_a?(Issue) && !incident? && project.group.present? is_a?(Issue) && issue_type_supports?(:epics) && project.group.present?
end end
def supports_health_status? def supports_health_status?
......
# frozen_string_literal: true
module EE
module IssueAvailableFeatures
include ::Gitlab::Utils::StrongMemoize
extend ::Gitlab::Utils::Override
override :available_features_for_issue_types
def available_features_for_issue_types
strong_memoize(:available_features_for_issue_types) do
super.merge(epics: %w(issue))
end
end
end
end
...@@ -133,7 +133,7 @@ RSpec.describe 'Epic show', :js do ...@@ -133,7 +133,7 @@ RSpec.describe 'Epic show', :js do
it 'shows epic thread filter dropdown' do it 'shows epic thread filter dropdown' do
page.within('.js-noteable-awards') do page.within('.js-noteable-awards') do
expect(find('.js-discussion-filter-container #discussion-filter-dropdown')).to have_content('Show all activity') expect(find('#discussion-filter-dropdown')).to have_content('Show all activity')
end end
end end
...@@ -142,9 +142,7 @@ RSpec.describe 'Epic show', :js do ...@@ -142,9 +142,7 @@ RSpec.describe 'Epic show', :js do
context 'when sorted by `Oldest first`' do context 'when sorted by `Oldest first`' do
it 'shows comments in the correct order' do it 'shows comments in the correct order' do
page.within('[data-testid="sort-discussion-filter"]') do
expect(find('.js-dropdown-text')).to have_content('Oldest first') expect(find('.js-dropdown-text')).to have_content('Oldest first')
end
items = all('.timeline-entry .timeline-discussion-body .note-text') items = all('.timeline-entry .timeline-discussion-body .note-text')
expect(items[0]).to have_content(notes[0].note) expect(items[0]).to have_content(notes[0].note)
...@@ -155,7 +153,7 @@ RSpec.describe 'Epic show', :js do ...@@ -155,7 +153,7 @@ RSpec.describe 'Epic show', :js do
context 'when sorted by `Newest first`' do context 'when sorted by `Newest first`' do
before do before do
page.within('[data-testid="sort-discussion-filter"]') do page.within('[data-testid="sort-discussion-filter"]') do
find('button').click find('.js-dropdown-text').click
find('.js-newest-first').click find('.js-newest-first').click
wait_for_requests wait_for_requests
end end
...@@ -163,7 +161,7 @@ RSpec.describe 'Epic show', :js do ...@@ -163,7 +161,7 @@ RSpec.describe 'Epic show', :js do
it 'shows comments in the correct order', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/225637' do it 'shows comments in the correct order', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/225637' do
page.within('[data-testid="sort-discussion-filter"]') do page.within('[data-testid="sort-discussion-filter"]') do
expect(find('.js-dropdown-text')).to have_content('Newest first') expect(find('.js-newest-first')).to have_content('Newest first')
end end
items = all('.timeline-entry .timeline-discussion-body .note-text') items = all('.timeline-entry .timeline-discussion-body .note-text')
......
...@@ -758,4 +758,16 @@ RSpec.describe Issue do ...@@ -758,4 +758,16 @@ RSpec.describe Issue do
it { is_expected.to eq(supports_iterations) } it { is_expected.to eq(supports_iterations) }
end end
end end
describe '#issue_type_supports?' do
let_it_be(:issue) { create(:issue) }
let_it_be(:test_case) { create(:quality_test_case) }
let_it_be(:incident) { create(:incident) }
it do
expect(issue.issue_type_supports?(:epics)).to be(true)
expect(test_case.issue_type_supports?(:epics)).to be(false)
expect(incident.issue_type_supports?(:epics)).to be(false)
end
end
end end
...@@ -121,6 +121,16 @@ RSpec.describe Notes::QuickActionsService do ...@@ -121,6 +121,16 @@ RSpec.describe Notes::QuickActionsService do
end end
end end
context 'on a test case' do
before do
issue.update!(issue_type: :test_case)
end
it 'leaves the note empty' do
expect(execute(note)).to be_empty
end
end
context 'on a merge request' do context 'on a merge request' do
let(:note_mr) { create(:note_on_merge_request, project: project, note: note_text) } let(:note_mr) { create(:note_on_merge_request, project: project, note: note_text) }
......
...@@ -168,14 +168,30 @@ RSpec.describe Epics::IssuePromoteService do ...@@ -168,14 +168,30 @@ RSpec.describe Epics::IssuePromoteService do
end end
end end
context 'on an incident' do context 'on other issue types' do
it 'raises error' do shared_examples_for 'raising error' do
issue.update!(issue_type: :incident) before do
issue.update(issue_type: issue_type)
end
it 'raises error' do
expect { subject.execute(issue) } expect { subject.execute(issue) }
.to raise_error(Epics::IssuePromoteService::PromoteError, /is not supported/) .to raise_error(Epics::IssuePromoteService::PromoteError, /is not supported/)
end end
end end
context 'on an incident' do
let(:issue_type) { :incident }
it_behaves_like 'raising error'
end
context 'on a test case' do
let(:issue_type) { :test_case }
it_behaves_like 'raising error'
end
end
end end
end end
end end
......
...@@ -76,6 +76,15 @@ module QA ...@@ -76,6 +76,15 @@ module QA
parse_body(response)[:title].include?(title) parse_body(response)[:title].include?(title)
end end
end end
private
def api_get
with_paginated_response_body(Runtime::API::Request.new(api_client, '/user/keys', per_page: '100').url) do |page|
key = page.find { |key| key[:title] == title }
break process_api_response(key) if key
end
end
end end
end end
end end
...@@ -36,6 +36,10 @@ module QA ...@@ -36,6 +36,10 @@ module QA
Flow::Login.sign_in Flow::Login.sign_in
end end
after do
ssh_key.remove_via_api!
end
it 'clones, pushes, and pulls a snippet over HTTP, edits via UI', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/826' do it 'clones, pushes, and pulls a snippet over HTTP, edits via UI', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/826' do
Resource::Repository::Push.fabricate! do |push| Resource::Repository::Push.fabricate! do |push|
push.repository_http_uri = repository_uri_http push.repository_http_uri = repository_uri_http
......
...@@ -36,6 +36,10 @@ module QA ...@@ -36,6 +36,10 @@ module QA
Flow::Login.sign_in Flow::Login.sign_in
end end
after do
ssh_key.remove_via_api!
end
it 'clones, pushes, and pulls a project snippet over HTTP, edits via UI', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/833' do it 'clones, pushes, and pulls a project snippet over HTTP, edits via UI', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/833' do
Resource::Repository::Push.fabricate! do |push| Resource::Repository::Push.fabricate! do |push|
push.repository_http_uri = repository_uri_http push.repository_http_uri = repository_uri_http
......
...@@ -83,6 +83,10 @@ module QA ...@@ -83,6 +83,10 @@ module QA
end end
end end
after do
key.remove_via_api!
end
it 'denies access', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/860' do it 'denies access', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/860' do
expect { push_a_project_with_ssh_key(key) }.to raise_error(QA::Git::Repository::RepositoryCommandError, /fatal: Could not read from remote repository/) expect { push_a_project_with_ssh_key(key) }.to raise_error(QA::Git::Repository::RepositoryCommandError, /fatal: Could not read from remote repository/)
end end
...@@ -126,6 +130,10 @@ module QA ...@@ -126,6 +130,10 @@ module QA
end end
end end
after do
key.remove_via_api!
end
it 'allows access', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/863' do it 'allows access', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/863' do
expect { push_a_project_with_ssh_key(key) }.not_to raise_error expect { push_a_project_with_ssh_key(key) }.not_to raise_error
end end
......
...@@ -45,13 +45,19 @@ module QA ...@@ -45,13 +45,19 @@ module QA
end end
context 'Add SSH key', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/738' do context 'Add SSH key', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/738' do
key = nil
before do before do
sign_in sign_in
Resource::SSHKey.fabricate_via_browser_ui! do |resource| key = Resource::SSHKey.fabricate_via_browser_ui! do |resource|
resource.title = "key for audit event test #{Time.now.to_f}" resource.title = "key for audit event test #{Time.now.to_f}"
end end
end end
after do
key&.reload!&.remove_via_api!
end
it_behaves_like 'audit event', ["Added SSH key"] it_behaves_like 'audit event', ["Added SSH key"]
end end
......
# frozen_string_literal: true # frozen_string_literal: true
module QA module QA
RSpec.describe 'Plan' do RSpec.describe 'Plan', :reliable do
describe 'Epics Management' do describe 'Epics Management' do
before do before do
Flow::Login.sign_in Flow::Login.sign_in
end end
it 'creates an epic', :reliable, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/522' do it 'creates an epic', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/522' do
epic_title = 'Epic created via GUI' epic_title = 'Epic created via GUI'
EE::Resource::Epic.fabricate_via_browser_ui! do |epic| EE::Resource::Epic.fabricate_via_browser_ui! do |epic|
epic.title = epic_title epic.title = epic_title
...@@ -36,7 +36,7 @@ module QA ...@@ -36,7 +36,7 @@ module QA
epic.visit! epic.visit!
end end
it 'adds/removes issue to/from epic', :reliable, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/526' do it 'adds/removes issue to/from epic', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/526' do
EE::Page::Group::Epic::Show.perform do |show| EE::Page::Group::Epic::Show.perform do |show|
show.add_issue_to_epic(issue.web_url) show.add_issue_to_epic(issue.web_url)
...@@ -48,7 +48,7 @@ module QA ...@@ -48,7 +48,7 @@ module QA
end end
end end
it 'comments on epic', :reliable, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/525' do it 'comments on epic', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/525' do
comment = 'My Epic Comment' comment = 'My Epic Comment'
EE::Page::Group::Epic::Show.perform do |show| EE::Page::Group::Epic::Show.perform do |show|
show.add_comment_to_epic(comment) show.add_comment_to_epic(comment)
......
...@@ -5,12 +5,17 @@ module QA ...@@ -5,12 +5,17 @@ module QA
describe 'GitLab SSH push' do describe 'GitLab SSH push' do
let(:file_name) { 'README.md' } let(:file_name) { 'README.md' }
key = nil
after do
key&.remove_via_api!
end
context 'regular git commit' do context 'regular git commit' do
it "is replicated to the secondary", testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/686' do it "is replicated to the secondary", testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/686' do
key_title = "Geo SSH #{Time.now.to_f}" key_title = "Geo SSH #{Time.now.to_f}"
file_content = 'This is a Geo project! Commit from primary.' file_content = 'This is a Geo project! Commit from primary.'
project = nil project = nil
key = nil
QA::Flow::Login.while_signed_in(address: :geo_primary) do QA::Flow::Login.while_signed_in(address: :geo_primary) do
# Create a new SSH key for the user # Create a new SSH key for the user
...@@ -73,7 +78,6 @@ module QA ...@@ -73,7 +78,6 @@ module QA
key_title = "Geo SSH LFS #{Time.now.to_f}" key_title = "Geo SSH LFS #{Time.now.to_f}"
file_content = 'The rendered file could not be displayed because it is stored in LFS.' file_content = 'The rendered file could not be displayed because it is stored in LFS.'
project = nil project = nil
key = nil
QA::Flow::Login.while_signed_in(address: :geo_primary) do QA::Flow::Login.while_signed_in(address: :geo_primary) do
# Create a new SSH key for the user # Create a new SSH key for the user
......
...@@ -6,12 +6,17 @@ module QA ...@@ -6,12 +6,17 @@ module QA
let(:file_content_primary) { 'This is a Geo project! Commit from primary.' } let(:file_content_primary) { 'This is a Geo project! Commit from primary.' }
let(:file_content_secondary) { 'This is a Geo project! Commit from secondary.' } let(:file_content_secondary) { 'This is a Geo project! Commit from secondary.' }
key = nil
after do
key&.remove_via_api!
end
context 'regular git commit' do context 'regular git commit' do
it 'is proxied to the primary and ultimately replicated to the secondary', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/698' do it 'is proxied to the primary and ultimately replicated to the secondary', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/698' do
file_name = 'README.md' file_name = 'README.md'
key_title = "Geo SSH to 2nd #{Time.now.to_f}" key_title = "Geo SSH to 2nd #{Time.now.to_f}"
project = nil project = nil
key = nil
QA::Flow::Login.while_signed_in(address: :geo_primary) do QA::Flow::Login.while_signed_in(address: :geo_primary) do
# Create a new SSH key for the user # Create a new SSH key for the user
...@@ -95,7 +100,6 @@ module QA ...@@ -95,7 +100,6 @@ module QA
file_name_primary = 'README.md' file_name_primary = 'README.md'
file_name_secondary = 'README_MORE.md' file_name_secondary = 'README_MORE.md'
project = nil project = nil
key = nil
QA::Flow::Login.while_signed_in(address: :geo_primary) do QA::Flow::Login.while_signed_in(address: :geo_primary) do
# Create a new SSH key for the user # Create a new SSH key for the user
......
...@@ -3,12 +3,17 @@ ...@@ -3,12 +3,17 @@
module QA module QA
RSpec.describe 'Geo', :orchestrated, :geo do RSpec.describe 'Geo', :orchestrated, :geo do
describe 'GitLab wiki SSH push' do describe 'GitLab wiki SSH push' do
key = nil
after do
key&.remove_via_api!
end
context 'wiki commit' do context 'wiki commit' do
it 'is replicated to the secondary', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/688' do it 'is replicated to the secondary', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/688' do
wiki_content = 'This tests replication of wikis via SSH' wiki_content = 'This tests replication of wikis via SSH'
push_content = 'This is from the Geo wiki push via SSH!' push_content = 'This is from the Geo wiki push via SSH!'
project = nil project = nil
key = nil
QA::Flow::Login.while_signed_in(address: :geo_primary) do QA::Flow::Login.while_signed_in(address: :geo_primary) do
# Create a new SSH key # Create a new SSH key
......
...@@ -34,6 +34,10 @@ module QA ...@@ -34,6 +34,10 @@ module QA
end end
end end
after do
key.remove_via_api!
end
it 'proxies wiki commit to primary node and ultmately replicates to secondary node', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/694' do it 'proxies wiki commit to primary node and ultmately replicates to secondary node', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/694' do
QA::Runtime::Logger.debug('*****Visiting the secondary geo node*****') QA::Runtime::Logger.debug('*****Visiting the secondary geo node*****')
......
...@@ -79,7 +79,7 @@ RSpec.describe 'Issue Boards add issue modal', :js do ...@@ -79,7 +79,7 @@ RSpec.describe 'Issue Boards add issue modal', :js do
it 'loads issues' do it 'loads issues' do
page.within('.add-issues-modal') do page.within('.add-issues-modal') do
page.within('.nav-links') do page.within('.gl-tabs') do
expect(page).to have_content('2') expect(page).to have_content('2')
end end
...@@ -146,7 +146,7 @@ RSpec.describe 'Issue Boards add issue modal', :js do ...@@ -146,7 +146,7 @@ RSpec.describe 'Issue Boards add issue modal', :js do
page.within('.add-issues-modal') do page.within('.add-issues-modal') do
first('.board-card .board-card-number').click first('.board-card .board-card-number').click
page.within('.nav-links') do page.within('.gl-tabs') do
expect(page).to have_content('Selected issues 1') expect(page).to have_content('Selected issues 1')
end end
end end
......
...@@ -74,13 +74,15 @@ describe('DiscussionFilter component', () => { ...@@ -74,13 +74,15 @@ describe('DiscussionFilter component', () => {
}); });
it('renders the all filters', () => { it('renders the all filters', () => {
expect(wrapper.findAll('.dropdown-menu li').length).toBe(discussionFiltersMock.length); expect(wrapper.findAll('.discussion-filter-container .dropdown-item').length).toBe(
discussionFiltersMock.length,
);
}); });
it('renders the default selected item', () => { it('renders the default selected item', () => {
expect( expect(
wrapper wrapper
.find('#discussion-filter-dropdown') .find('#discussion-filter-dropdown .dropdown-item')
.text() .text()
.trim(), .trim(),
).toBe(discussionFiltersMock[0].title); ).toBe(discussionFiltersMock[0].title);
...@@ -88,7 +90,7 @@ describe('DiscussionFilter component', () => { ...@@ -88,7 +90,7 @@ describe('DiscussionFilter component', () => {
it('updates to the selected item', () => { it('updates to the selected item', () => {
const filterItem = wrapper.find( const filterItem = wrapper.find(
`.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"] button`, `.discussion-filter-container .dropdown-item[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"]`,
); );
filterItem.trigger('click'); filterItem.trigger('click');
...@@ -98,7 +100,9 @@ describe('DiscussionFilter component', () => { ...@@ -98,7 +100,9 @@ describe('DiscussionFilter component', () => {
it('only updates when selected filter changes', () => { it('only updates when selected filter changes', () => {
wrapper wrapper
.find(`.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`) .find(
`.discussion-filter-container .dropdown-item[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"]`,
)
.trigger('click'); .trigger('click');
expect(filterDiscussion).not.toHaveBeenCalled(); expect(filterDiscussion).not.toHaveBeenCalled();
...@@ -106,7 +110,7 @@ describe('DiscussionFilter component', () => { ...@@ -106,7 +110,7 @@ describe('DiscussionFilter component', () => {
it('disables commenting when "Show history only" filter is applied', () => { it('disables commenting when "Show history only" filter is applied', () => {
const filterItem = wrapper.find( const filterItem = wrapper.find(
`.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"] button`, `.discussion-filter-container .dropdown-item[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"]`,
); );
filterItem.trigger('click'); filterItem.trigger('click');
...@@ -115,7 +119,7 @@ describe('DiscussionFilter component', () => { ...@@ -115,7 +119,7 @@ describe('DiscussionFilter component', () => {
it('enables commenting when "Show history only" filter is not applied', () => { it('enables commenting when "Show history only" filter is not applied', () => {
const filterItem = wrapper.find( const filterItem = wrapper.find(
`.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`, `.discussion-filter-container .dropdown-item[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"]`,
); );
filterItem.trigger('click'); filterItem.trigger('click');
...@@ -124,10 +128,10 @@ describe('DiscussionFilter component', () => { ...@@ -124,10 +128,10 @@ describe('DiscussionFilter component', () => {
it('renders a dropdown divider for the default filter', () => { it('renders a dropdown divider for the default filter', () => {
const defaultFilter = wrapper.findAll( const defaultFilter = wrapper.findAll(
`.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] > *`, `.discussion-filter-container .dropdown-item-wrapper > *`,
); );
expect(defaultFilter.at(defaultFilter.length - 1).classes('dropdown-divider')).toBe(true); expect(defaultFilter.at(1).classes('gl-new-dropdown-divider')).toBe(true);
}); });
describe('Merge request tabs', () => { describe('Merge request tabs', () => {
......
...@@ -55,7 +55,7 @@ describe('Sort Discussion component', () => { ...@@ -55,7 +55,7 @@ describe('Sort Discussion component', () => {
it('calls the right actions', () => { it('calls the right actions', () => {
createComponent(); createComponent();
wrapper.find('.js-newest-first').trigger('click'); wrapper.find('.js-newest-first').vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', DESC); expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', DESC);
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'change_discussion_sort_direction', { expect(Tracking.event).toHaveBeenCalledWith(undefined, 'change_discussion_sort_direction', {
...@@ -67,7 +67,7 @@ describe('Sort Discussion component', () => { ...@@ -67,7 +67,7 @@ describe('Sort Discussion component', () => {
it('shows the "Oldest First" as the dropdown', () => { it('shows the "Oldest First" as the dropdown', () => {
createComponent(); createComponent();
expect(wrapper.find('.js-dropdown-text').text()).toBe('Oldest first'); expect(wrapper.find('.js-dropdown-text').props('text')).toBe('Oldest first');
}); });
}); });
...@@ -79,7 +79,7 @@ describe('Sort Discussion component', () => { ...@@ -79,7 +79,7 @@ describe('Sort Discussion component', () => {
describe('when the dropdown item is clicked', () => { describe('when the dropdown item is clicked', () => {
it('calls the right actions', () => { it('calls the right actions', () => {
wrapper.find('.js-oldest-first').trigger('click'); wrapper.find('.js-oldest-first').vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', ASC); expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', ASC);
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'change_discussion_sort_direction', { expect(Tracking.event).toHaveBeenCalledWith(undefined, 'change_discussion_sort_direction', {
...@@ -87,13 +87,13 @@ describe('Sort Discussion component', () => { ...@@ -87,13 +87,13 @@ describe('Sort Discussion component', () => {
}); });
}); });
it('applies the active class to the correct button in the dropdown', () => { it('sets is-checked to true on the active button in the dropdown', () => {
expect(wrapper.find('.js-newest-first').classes()).toContain('is-active'); expect(wrapper.find('.js-newest-first').props('isChecked')).toBe(true);
}); });
}); });
it('shows the "Newest First" as the dropdown', () => { it('shows the "Newest First" as the dropdown', () => {
expect(wrapper.find('.js-dropdown-text').text()).toBe('Newest first'); expect(wrapper.find('.js-dropdown-text').props('text')).toBe('Newest first');
}); });
}); });
}); });
...@@ -1238,4 +1238,12 @@ RSpec.describe Issue do ...@@ -1238,4 +1238,12 @@ RSpec.describe Issue do
expect(issue.allows_reviewers?).to be(false) expect(issue.allows_reviewers?).to be(false)
end end
end end
describe '#issue_type_supports?' do
let_it_be(:issue) { create(:issue) }
it 'raises error when feature is invalid' do
expect { issue.issue_type_supports?(:unkown_feature) }.to raise_error(ArgumentError)
end
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment