Commit ae5ecfad authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch '3727-fe-labels-for-epics' into 'master'

Add labels support to Epics

Closes #3727 and #4032

See merge request gitlab-org/gitlab-ee!4773
parents c0c34cda 9a058ba9
...@@ -7,6 +7,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager { ...@@ -7,6 +7,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) { constructor(store, updateUrl = false, cantEdit = []) {
super({ super({
page: 'boards', page: 'boards',
isGroup: true,
filteredSearchTokenKeys: FilteredSearchTokenKeysIssues, filteredSearchTokenKeys: FilteredSearchTokenKeysIssues,
stateFiltersSelector: '.issues-state-filters', stateFiltersSelector: '.issues-state-filters',
}); });
......
...@@ -125,6 +125,16 @@ export default class FilteredSearchDropdownManager { ...@@ -125,6 +125,16 @@ export default class FilteredSearchDropdownManager {
endpoint = `${endpoint}?only_group_labels=true`; endpoint = `${endpoint}?only_group_labels=true`;
} }
// EE-only
if (this.groupAncestor) {
endpoint = `${endpoint}&include_ancestor_groups=true`;
}
// EE-only
if (this.isGroupDecendent) {
endpoint = `${endpoint}&include_descendant_groups=true`;
}
return endpoint; return endpoint;
} }
......
...@@ -109,6 +109,7 @@ export default class FilteredSearchManager { ...@@ -109,6 +109,7 @@ export default class FilteredSearchManager {
page: this.page, page: this.page,
isGroup: this.isGroup, isGroup: this.isGroup,
isGroupAncestor: this.isGroupAncestor, isGroupAncestor: this.isGroupAncestor,
isGroupDecendent: this.isGroupDecendent,
filteredSearchTokenKeys: this.filteredSearchTokenKeys, filteredSearchTokenKeys: this.filteredSearchTokenKeys,
}); });
......
...@@ -88,7 +88,7 @@ export default { ...@@ -88,7 +88,7 @@ export default {
</script> </script>
<template> <template>
<div class="block labels"> <div class="block labels js-labels-block">
<dropdown-value-collapsed <dropdown-value-collapsed
v-if="showCreate" v-if="showCreate"
:labels="context.labels" :labels="context.labels"
...@@ -104,7 +104,7 @@ export default { ...@@ -104,7 +104,7 @@ export default {
</dropdown-value> </dropdown-value>
<div <div
v-if="canEdit" v-if="canEdit"
class="selectbox" class="selectbox js-selectbox"
style="display: none;" style="display: none;"
> >
<dropdown-hidden-input <dropdown-hidden-input
......
...@@ -35,7 +35,7 @@ export default { ...@@ -35,7 +35,7 @@ export default {
</script> </script>
<template> <template>
<div class="hide-collapsed value issuable-show-labels"> <div class="hide-collapsed value issuable-show-labels js-value">
<span <span
v-if="isEmpty" v-if="isEmpty"
class="text-secondary" class="text-secondary"
......
- page_title 'Labels' - page_title 'Labels'
- issuables = ['issues', 'merge requests'] + (@group&.feature_available?(:epics) ? ['epics'] : [])
.top-area.adjust .top-area.adjust
.nav-text .nav-text
Labels can be applied to issues and merge requests. Group labels are available for any project within the group. = _("Labels can be applied to %{features}. Group labels are available for any project within the group.") % { features: issuables.to_sentence }
.nav-controls .nav-controls
- if can?(current_user, :admin_label, @group) - if can?(current_user, :admin_label, @group)
...@@ -16,4 +18,4 @@ ...@@ -16,4 +18,4 @@
= paginate @labels, theme: 'gitlab' = paginate @labels, theme: 'gitlab'
- else - else
.nothing-here-block .nothing-here-block
No labels created yet. = _("No labels created yet.")
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
- status = label_subscription_status(label, @project).inquiry if current_user - status = label_subscription_status(label, @project).inquiry if current_user
- subject = local_assigns[:subject] - subject = local_assigns[:subject]
- toggle_subscription_path = toggle_subscription_label_path(label, @project) if current_user - toggle_subscription_path = toggle_subscription_label_path(label, @project) if current_user
- show_label_epics_link = @group&.feature_available?(:epics)
- show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project) - show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project)
- show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project) - show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project)
...@@ -14,6 +15,10 @@ ...@@ -14,6 +15,10 @@
= icon('caret-down') = icon('caret-down')
.dropdown-menu.dropdown-menu-align-right .dropdown-menu.dropdown-menu-align-right
%ul %ul
- if show_label_epics_link
%li
= link_to group_epics_path(@group, label_name:[label.name]) do
View epics
- if show_label_merge_requests_link - if show_label_merge_requests_link
%li %li
= link_to_label(label, subject: subject, type: :merge_request) do = link_to_label(label, subject: subject, type: :merge_request) do
......
- subject = local_assigns[:subject] - subject = local_assigns[:subject]
- show_label_epics_link = @group&.feature_available?(:epics)
- show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project) - show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project)
- show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project) - show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project)
...@@ -23,6 +24,9 @@ ...@@ -23,6 +24,9 @@
.description-text .description-text
= markdown_field(label, :description) = markdown_field(label, :description)
.hidden-xs.hidden-sm .hidden-xs.hidden-sm
- if show_label_epics_link
= link_to 'Epics', group_epics_path(@group, label_name:[label.name])
&middot;
- if show_label_issues_link - if show_label_issues_link
= link_to_label(label, subject: subject) { 'Issues' } = link_to_label(label, subject: subject) { 'Issues' }
- if show_label_merge_requests_link - if show_label_merge_requests_link
......
...@@ -210,4 +210,3 @@ DELETE /groups/:id/-/epics/:epic_iid ...@@ -210,4 +210,3 @@ DELETE /groups/:id/-/epics/:epic_iid
```bash ```bash
curl --header DELETE "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/1/-/epics/5?title=New%20Title curl --header DELETE "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/1/-/epics/5?title=New%20Title
``` ```
...@@ -3,8 +3,9 @@ ...@@ -3,8 +3,9 @@
import issuableApp from '~/issue_show/components/app.vue'; import issuableApp from '~/issue_show/components/app.vue';
import relatedIssuesRoot from 'ee/related_issues/components/related_issues_root.vue'; import relatedIssuesRoot from 'ee/related_issues/components/related_issues_root.vue';
import issuableAppEventHub from '~/issue_show/event_hub'; import issuableAppEventHub from '~/issue_show/event_hub';
import epicHeader from './epic_header.vue';
import epicSidebar from '../../sidebar/components/sidebar_app.vue'; import epicSidebar from '../../sidebar/components/sidebar_app.vue';
import SidebarContext from '../sidebar_context';
import epicHeader from './epic_header.vue';
export default { export default {
name: 'EpicShowApp', name: 'EpicShowApp',
...@@ -85,6 +86,27 @@ ...@@ -85,6 +86,27 @@
type: String, type: String,
required: false, required: false,
}, },
labels: {
type: Array,
required: true,
},
namespace: {
type: String,
required: false,
default: '#',
},
labelsPath: {
type: String,
required: true,
},
labelsWebUrl: {
type: String,
required: true,
},
epicsWebUrl: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -94,6 +116,9 @@ ...@@ -94,6 +116,9 @@
projectNamespace: '', projectNamespace: '',
}; };
}, },
mounted() {
this.sidebarContext = new SidebarContext();
},
methods: { methods: {
deleteEpic() { deleteEpic() {
issuableAppEventHub.$emit('delete.issuable'); issuableAppEventHub.$emit('delete.issuable');
...@@ -137,6 +162,12 @@ ...@@ -137,6 +162,12 @@
:editable="canUpdate" :editable="canUpdate"
:initial-start-date="startDate" :initial-start-date="startDate"
:initial-end-date="endDate" :initial-end-date="endDate"
:initial-labels="labels"
:namespace="namespace"
:update-path="updateEndpoint"
:labels-path="labelsPath"
:labels-web-url="labelsWebUrl"
:epics-web-url="epicsWebUrl"
/> />
<related-issues-root <related-issues-root
:endpoint="issueLinksEndpoint" :endpoint="issueLinksEndpoint"
......
import Vue from 'vue'; import Vue from 'vue';
import '~/vue_shared/models/label';
import EpicShowApp from './components/epic_show_app.vue'; import EpicShowApp from './components/epic_show_app.vue';
export default () => { export default () => {
...@@ -6,7 +7,7 @@ export default () => { ...@@ -6,7 +7,7 @@ export default () => {
const metaData = JSON.parse(el.dataset.meta); const metaData = JSON.parse(el.dataset.meta);
const initialData = JSON.parse(el.dataset.initial); const initialData = JSON.parse(el.dataset.initial);
const props = Object.assign({}, initialData, metaData); const props = Object.assign({}, initialData, metaData, el.dataset);
// Convert backend casing to match frontend style guide // Convert backend casing to match frontend style guide
props.startDate = props.start_date; props.startDate = props.start_date;
......
import Mousetrap from 'mousetrap';
export default class SidebarContext {
constructor() {
const $issuableSidebar = $('.js-issuable-update');
Mousetrap.bind('l', () => SidebarContext.openSidebarDropdown($issuableSidebar.find('.js-labels-block')));
$issuableSidebar
.off('click', '.js-sidebar-dropdown-toggle')
.on('click', '.js-sidebar-dropdown-toggle', function onClickEdit(e) {
e.preventDefault();
const $block = $(this).parents('.js-labels-block');
const $selectbox = $block.find('.js-selectbox');
// We use `:visible` to detect element visibility
// since labels dropdown itself is handled by
// labels_select.js which internally uses
// $.hide() & $.show() to toggle elements
// which requires us to use `display: none;`
// in `labels_select/base.vue` as well.
// see: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4773#note_61844731
if ($selectbox.is(':visible')) {
$selectbox.hide();
$block.find('.js-value').show();
} else {
$selectbox.show();
$block.find('.js-value').hide();
}
if ($selectbox.is(':visible')) {
setTimeout(() => $block.find('.js-label-select').trigger('click'), 0);
}
});
}
static openSidebarDropdown($block) {
$block.find('.js-sidebar-dropdown-toggle').trigger('click');
}
}
<script> <script>
/* global ListLabel */
/* eslint-disable vue/require-default-prop */ /* eslint-disable vue/require-default-prop */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import Flash from '~/flash'; import Flash from '~/flash';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import sidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue'; import SidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue';
import sidebarCollapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue'; import SidebarCollapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
import SidebarLabelsSelect from '~/vue_shared/components/sidebar/labels_select/base.vue';
import SidebarService from '../services/sidebar_service'; import SidebarService from '../services/sidebar_service';
import Store from '../stores/sidebar_store'; import Store from '../stores/sidebar_store';
export default { export default {
name: 'EpicSidebar', name: 'EpicSidebar',
components: { components: {
sidebarDatePicker, SidebarDatePicker,
sidebarCollapsedGroupedDatePicker, SidebarCollapsedGroupedDatePicker,
SidebarLabelsSelect,
}, },
props: { props: {
endpoint: { endpoint: {
...@@ -32,6 +35,31 @@ ...@@ -32,6 +35,31 @@
type: String, type: String,
required: false, required: false,
}, },
initialLabels: {
type: Array,
required: true,
},
namespace: {
type: String,
required: false,
default: '#',
},
updatePath: {
type: String,
required: true,
},
labelsPath: {
type: String,
required: true,
},
labelsWebUrl: {
type: String,
required: true,
},
epicsWebUrl: {
type: String,
required: true,
},
}, },
data() { data() {
const store = new Store({ const store = new Store({
...@@ -46,13 +74,16 @@ ...@@ -46,13 +74,16 @@
savingStartDate: false, savingStartDate: false,
savingEndDate: false, savingEndDate: false,
service: new SidebarService(this.endpoint), service: new SidebarService(this.endpoint),
epicContext: {
labels: this.initialLabels,
},
}; };
}, },
methods: { methods: {
toggleSidebar() { toggleSidebar() {
this.collapsed = !this.collapsed; this.collapsed = !this.collapsed;
const contentContainer = this.$el.closest('.page-with-sidebar'); const contentContainer = this.$el.closest('.page-with-contextual-sidebar');
contentContainer.classList.toggle('right-sidebar-expanded'); contentContainer.classList.toggle('right-sidebar-expanded');
contentContainer.classList.toggle('right-sidebar-collapsed'); contentContainer.classList.toggle('right-sidebar-collapsed');
...@@ -82,6 +113,24 @@ ...@@ -82,6 +113,24 @@
saveEndDate(date) { saveEndDate(date) {
return this.saveDate('end', date); return this.saveDate('end', date);
}, },
handleLabelClick(label) {
if (label.isAny) {
this.epicContext.labels = [];
} else {
const labelIndex = this.epicContext.labels.findIndex(l => l.id === label.id);
if (labelIndex === -1) {
this.epicContext.labels.push(new ListLabel({
id: label.id,
title: label.title,
color: label.color[0],
textColor: label.text_color,
}));
} else {
this.epicContext.labels.splice(labelIndex, 1);
}
}
},
}, },
}; };
</script> </script>
...@@ -91,9 +140,10 @@ ...@@ -91,9 +140,10 @@
class="right-sidebar" class="right-sidebar"
:class="{ 'right-sidebar-expanded' : !collapsed, 'right-sidebar-collapsed': collapsed }" :class="{ 'right-sidebar-expanded' : !collapsed, 'right-sidebar-collapsed': collapsed }"
> >
<div class="issuable-sidebar"> <div class="issuable-sidebar js-issuable-update">
<sidebar-date-picker <sidebar-date-picker
v-if="!collapsed" v-if="!collapsed"
block-class="start-date"
:collapsed="collapsed" :collapsed="collapsed"
:is-loading="savingStartDate" :is-loading="savingStartDate"
:editable="editable" :editable="editable"
...@@ -106,6 +156,7 @@ ...@@ -106,6 +156,7 @@
/> />
<sidebar-date-picker <sidebar-date-picker
v-if="!collapsed" v-if="!collapsed"
block-class="end-date"
:collapsed="collapsed" :collapsed="collapsed"
:is-loading="savingEndDate" :is-loading="savingEndDate"
:editable="editable" :editable="editable"
...@@ -123,6 +174,20 @@ ...@@ -123,6 +174,20 @@
:show-toggle-sidebar="true" :show-toggle-sidebar="true"
@toggleCollapse="toggleSidebar" @toggleCollapse="toggleSidebar"
/> />
<sidebar-labels-select
ability-name="epic"
:context="epicContext"
:namespace="namespace"
:update-path="updatePath"
:labels-path="labelsPath"
:labels-web-url="labelsWebUrl"
:label-filter-base-path="epicsWebUrl"
:can-edit="editable"
:show-create="true"
@onLabelClick="handleLabelClick"
>
{{ __('None') }}
</sidebar-labels-select>
</div> </div>
</aside> </aside>
</template> </template>
...@@ -5,6 +5,13 @@ const tokenKeys = [{ ...@@ -5,6 +5,13 @@ const tokenKeys = [{
symbol: '@', symbol: '@',
icon: 'pencil', icon: 'pencil',
tag: '@author', tag: '@author',
}, {
key: 'label',
type: 'array',
param: 'name[]',
symbol: '~',
icon: 'tag',
tag: '~label',
}]; }];
const alternativeTokenKeys = [{ const alternativeTokenKeys = [{
......
...@@ -5,6 +5,9 @@ import initNewEpic from 'ee/epics/new_epic/new_epic_bundle'; ...@@ -5,6 +5,9 @@ import initNewEpic from 'ee/epics/new_epic/new_epic_bundle';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({ initFilteredSearch({
page: 'epics', page: 'epics',
isGroup: true,
isGroupAncestor: true,
isGroupDecendent: true,
filteredSearchTokenKeys: FilteredSearchTokenKeysEpics, filteredSearchTokenKeys: FilteredSearchTokenKeysEpics,
stateFiltersSelector: '.epics-state-filters', stateFiltersSelector: '.epics-state-filters',
}); });
......
...@@ -17,7 +17,13 @@ class EpicsFinder < IssuableFinder ...@@ -17,7 +17,13 @@ class EpicsFinder < IssuableFinder
end end
def row_count def row_count
execute.count count = execute.count
# When filtering by multiple labels, count returns a hash of
# records grouped by id - so we just have to get length of the Hash.
# Once we have state for epics, we can use default issuables row_count
# method.
count.is_a?(Hash) ? count.length : count
end end
# we don't have states for epics for now this method (#4017) # we don't have states for epics for now this method (#4017)
......
module EpicsHelper module EpicsHelper
def epic_meta_data def epic_show_app_data(epic, opts)
author = @epic.author author = epic.author
group = epic.group
data = { epic_meta = {
created: @epic.created_at, created: epic.created_at,
author: { author: {
name: author.name, name: author.name,
url: user_path(author), url: user_path(author),
username: "@#{author.username}", username: "@#{author.username}",
src: avatar_icon_for_user(@epic.author) src: opts[:author_icon]
}, },
start_date: @epic.start_date, start_date: epic.start_date,
end_date: @epic.end_date end_date: epic.end_date
} }
data.to_json {
initial: opts[:initial].merge(labels: epic.labels).to_json,
meta: epic_meta.to_json,
namespace: group.path,
labels_path: group_labels_path(group, format: :json, only_group_labels: true, include_ancestor_groups: true),
labels_web_url: group_labels_path(group),
epics_web_url: group_epics_path(group)
}
end
def epic_endpoint_query_params(opts)
opts[:data] ||= {}
opts[:data][:endpoint_query_params] = {
only_group_labels: true,
include_ancestor_groups: true,
include_descendant_groups: true
}.to_json
opts
end end
end end
...@@ -12,3 +12,7 @@ ...@@ -12,3 +12,7 @@
&middot; &middot;
opened #{time_ago_with_tooltip(epic.created_at, placement: 'bottom')} opened #{time_ago_with_tooltip(epic.created_at, placement: 'bottom')}
by #{link_to_member(@group, epic.author, avatar: false)} by #{link_to_member(@group, epic.author, avatar: false)}
- if epic.labels.any?
&nbsp;
- epic.labels.each do |label|
= link_to render_colored_label(label, tooltip: true), group_epics_path(@group, label_name:[label.name]), class: 'label-link'
...@@ -15,4 +15,4 @@ ...@@ -15,4 +15,4 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'common_vue'
#epic-show-app{ data: { initial: issuable_initial_data(@epic).to_json, meta: epic_meta_data } } #epic-show-app{ data: epic_show_app_data(@epic, author_icon: avatar_icon_for_user(@epic.author), initial: issuable_initial_data(@epic)) }
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
.scroll-container .scroll-container
%ul.tokens-container.list-unstyled %ul.tokens-container.list-unstyled
%li.input-token %li.input-token
%input.form-control.filtered-search{ search_filter_input_options(type) } %input.form-control.filtered-search{ epic_endpoint_query_params(search_filter_input_options(type)) }
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } } %ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { action: 'submit' } } %li.filter-dropdown-item{ data: { action: 'submit' } }
...@@ -46,6 +46,18 @@ ...@@ -46,6 +46,18 @@
= render 'shared/issuable/user_dropdown_item', = render 'shared/issuable/user_dropdown_item',
user: User.new(username: '{{username}}', name: '{{name}}'), user: User.new(username: '{{username}}', name: '{{name}}'),
avatar: { lazy: true, url: '{{avatar_url}}' } avatar: { lazy: true, url: '{{avatar_url}}' }
#js-dropdown-label.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link{ type: 'button' }
= _("No Label")
%li.divider.droplab-item-ignore
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item{ type: 'button' }
%button.btn.btn-link
%span.dropdown-label-box{ style: 'background: {{color}}' }
%span.label-title.js-data-value
{{ title }}
%button.clear-search.hidden{ type: 'button' } %button.clear-search.hidden{ type: 'button' }
= icon('times') = icon('times')
......
---
title: Allow to add or remove labels from epics and filter epics by labels
merge_request: 4773
author:
type: added
---
title: Allow adding or removing labels from epics and filter epics by labels
merge_request:
author:
type: added
...@@ -130,4 +130,22 @@ describe EpicsFinder do ...@@ -130,4 +130,22 @@ describe EpicsFinder do
end end
end end
end end
describe '#row_count' do
let(:label) { create(:label) }
let(:label2) { create(:label) }
let!(:labeled_epic) { create(:labeled_epic, group: group, labels: [label]) }
let!(:labeled_epic2) { create(:labeled_epic, group: group, labels: [label, label2]) }
before do
group.add_developer(search_user)
stub_licensed_features(epics: true)
end
it 'returns number of rows when epics are grouped' do
params = { group_id: group.id, label_name: [label.title, label2.title] }
expect(described_class.new(search_user, params).row_count).to eq(1)
end
end
end end
...@@ -3,18 +3,32 @@ require 'spec_helper' ...@@ -3,18 +3,32 @@ require 'spec_helper'
describe EpicsHelper do describe EpicsHelper do
include ApplicationHelper include ApplicationHelper
describe '#epic_meta_data' do describe '#epic_show_app_data' do
it 'returns the correct json' do it 'returns the correct json' do
user = create(:user) user = create(:user)
@epic = create(:epic, author: user) @epic = create(:epic, author: user)
expect(JSON.parse(epic_meta_data).keys).to match_array(%w[created author start_date end_date]) data = epic_show_app_data(@epic, initial: {}, author_icon: 'icon_path')
expect(JSON.parse(epic_meta_data)['author']).to eq({ meta_data = JSON.parse(data[:meta])
expected_keys = %i(initial meta namespace labels_path labels_web_url epics_web_url)
expect(data.keys).to match_array(expected_keys)
expect(meta_data.keys).to match_array(%w[created author start_date end_date])
expect(meta_data['author']).to eq({
'name' => user.name, 'name' => user.name,
'url' => "/#{user.username}", 'url' => "/#{user.username}",
'username' => "@#{user.username}", 'username' => "@#{user.username}",
'src' => "#{avatar_icon_for_user(user)}" 'src' => 'icon_path'
}) })
end end
end end
describe '#epic_endpoint_query_params' do
it 'it includes epic specific options in JSON format' do
opts = epic_endpoint_query_params({})
expected = "{\"only_group_labels\":true,\"include_ancestor_groups\":true,\"include_descendant_groups\":true}"
expect(opts[:data][:endpoint_query_params]).to eq(expected)
end
end
end end
...@@ -36,6 +36,11 @@ describe('EpicShowApp', () => { ...@@ -36,6 +36,11 @@ describe('EpicShowApp', () => {
markdownDocsPath, markdownDocsPath,
author, author,
created, created,
namespace,
labelsPath,
labelsWebUrl,
epicsWebUrl,
labels,
} = props; } = props;
const EpicShowApp = Vue.extend(epicShowApp); const EpicShowApp = Vue.extend(epicShowApp);
...@@ -72,6 +77,12 @@ describe('EpicShowApp', () => { ...@@ -72,6 +77,12 @@ describe('EpicShowApp', () => {
editable: canUpdate, editable: canUpdate,
initialStartDate: startDate, initialStartDate: startDate,
initialEndDate: endDate, initialEndDate: endDate,
initialLabels: labels,
updatePath: updateEndpoint,
labelsPath,
labelsWebUrl,
epicsWebUrl,
namespace,
}); });
setTimeout(done); setTimeout(done);
......
export const mockLabels = [
{
id: 26,
title: 'Foo Label',
description: 'Foobar',
color: '#BADA55',
text_color: '#FFFFFF',
},
];
export const contentProps = { export const contentProps = {
endpoint: '', endpoint: '',
updateEndpoint: gl.TEST_HOST, updateEndpoint: gl.TEST_HOST,
...@@ -8,10 +18,15 @@ export const contentProps = { ...@@ -8,10 +18,15 @@ export const contentProps = {
markdownDocsPath: '', markdownDocsPath: '',
issueLinksEndpoint: '/', issueLinksEndpoint: '/',
groupPath: '', groupPath: '',
namespace: 'gitlab-org',
labelsPath: '',
labelsWebUrl: '',
epicsWebUrl: '',
initialTitleHtml: '', initialTitleHtml: '',
initialTitleText: '', initialTitleText: '',
startDate: '2017-01-01', startDate: '2017-01-01',
endDate: '2017-10-10', endDate: '2017-10-10',
labels: mockLabels,
}; };
export const headerProps = { export const headerProps = {
......
...@@ -3,15 +3,23 @@ import _ from 'underscore'; ...@@ -3,15 +3,23 @@ import _ from 'underscore';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import epicSidebar from 'ee/epics/sidebar/components/sidebar_app.vue'; import epicSidebar from 'ee/epics/sidebar/components/sidebar_app.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { props } from '../../epic_show/mock_data';
describe('epicSidebar', () => { describe('epicSidebar', () => {
let vm; let vm;
let originalCookieState; let originalCookieState;
let EpicSidebar; let EpicSidebar;
const {
updateEndpoint,
labelsPath,
labelsWebUrl,
epicsWebUrl,
labels,
} = props;
beforeEach(() => { beforeEach(() => {
setFixtures(` setFixtures(`
<div class="page-with-sidebar right-sidebar-expanded"> <div class="page-with-contextual-sidebar right-sidebar-expanded">
<div id="epic-sidebar"></div> <div id="epic-sidebar"></div>
</div> </div>
`); `);
...@@ -21,6 +29,11 @@ describe('epicSidebar', () => { ...@@ -21,6 +29,11 @@ describe('epicSidebar', () => {
EpicSidebar = Vue.extend(epicSidebar); EpicSidebar = Vue.extend(epicSidebar);
vm = mountComponent(EpicSidebar, { vm = mountComponent(EpicSidebar, {
endpoint: gl.TEST_HOST, endpoint: gl.TEST_HOST,
initialLabels: labels,
updatePath: updateEndpoint,
labelsPath,
labelsWebUrl,
epicsWebUrl,
}, '#epic-sidebar'); }, '#epic-sidebar');
}); });
...@@ -36,6 +49,11 @@ describe('epicSidebar', () => { ...@@ -36,6 +49,11 @@ describe('epicSidebar', () => {
vm = mountComponent(EpicSidebar, { vm = mountComponent(EpicSidebar, {
endpoint: gl.TEST_HOST, endpoint: gl.TEST_HOST,
initialStartDate: '2017-01-01', initialStartDate: '2017-01-01',
initialLabels: labels,
updatePath: updateEndpoint,
labelsPath,
labelsWebUrl,
epicsWebUrl,
}); });
expect(vm.$el.querySelector('.value-content strong').innerText.trim()).toEqual('Jan 1, 2017'); expect(vm.$el.querySelector('.value-content strong').innerText.trim()).toEqual('Jan 1, 2017');
...@@ -45,6 +63,11 @@ describe('epicSidebar', () => { ...@@ -45,6 +63,11 @@ describe('epicSidebar', () => {
vm = mountComponent(EpicSidebar, { vm = mountComponent(EpicSidebar, {
endpoint: gl.TEST_HOST, endpoint: gl.TEST_HOST,
initialEndDate: '2018-01-01', initialEndDate: '2018-01-01',
initialLabels: labels,
updatePath: updateEndpoint,
labelsPath,
labelsWebUrl,
epicsWebUrl,
}); });
expect(vm.$el.querySelector('.value-content strong').innerText.trim()).toEqual('Jan 1, 2018'); expect(vm.$el.querySelector('.value-content strong').innerText.trim()).toEqual('Jan 1, 2018');
...@@ -55,6 +78,11 @@ describe('epicSidebar', () => { ...@@ -55,6 +78,11 @@ describe('epicSidebar', () => {
endpoint: gl.TEST_HOST, endpoint: gl.TEST_HOST,
initialStartDate: '2017-01-01', initialStartDate: '2017-01-01',
initialEndDate: '2018-01-01', initialEndDate: '2018-01-01',
initialLabels: labels,
updatePath: updateEndpoint,
labelsPath,
labelsWebUrl,
epicsWebUrl,
}); });
const datePickers = vm.$el.querySelectorAll('.block'); const datePickers = vm.$el.querySelectorAll('.block');
...@@ -68,6 +96,11 @@ describe('epicSidebar', () => { ...@@ -68,6 +96,11 @@ describe('epicSidebar', () => {
vm = mountComponent(EpicSidebar, { vm = mountComponent(EpicSidebar, {
endpoint: gl.TEST_HOST, endpoint: gl.TEST_HOST,
initialStartDate: '2017-01-01', initialStartDate: '2017-01-01',
initialLabels: labels,
updatePath: updateEndpoint,
labelsPath,
labelsWebUrl,
epicsWebUrl,
}); });
}); });
...@@ -89,7 +122,7 @@ describe('epicSidebar', () => { ...@@ -89,7 +122,7 @@ describe('epicSidebar', () => {
}); });
it('should toggle contentContainer css class', () => { it('should toggle contentContainer css class', () => {
const contentContainer = document.querySelector('.page-with-sidebar'); const contentContainer = document.querySelector('.page-with-contextual-sidebar');
expect(contentContainer.classList.contains('right-sidebar-expanded')).toEqual(true); expect(contentContainer.classList.contains('right-sidebar-expanded')).toEqual(true);
expect(contentContainer.classList.contains('right-sidebar-collapsed')).toEqual(false); expect(contentContainer.classList.contains('right-sidebar-collapsed')).toEqual(false);
...@@ -113,6 +146,11 @@ describe('epicSidebar', () => { ...@@ -113,6 +146,11 @@ describe('epicSidebar', () => {
component = new EpicSidebar({ component = new EpicSidebar({
propsData: { propsData: {
endpoint: gl.TEST_HOST, endpoint: gl.TEST_HOST,
initialLabels: labels,
updatePath: updateEndpoint,
labelsPath,
labelsWebUrl,
epicsWebUrl,
}, },
}); });
}); });
...@@ -146,6 +184,38 @@ describe('epicSidebar', () => { ...@@ -146,6 +184,38 @@ describe('epicSidebar', () => {
it('should handle errors gracefully', () => {}); it('should handle errors gracefully', () => {});
}); });
describe('handleLabelClick', () => {
const label = {
id: 1,
title: 'Foo',
color: ['#BADA55'],
text_color: '#FFFFFF',
};
it('initializes `epicContext.labels` as empty array when `label.isAny` is `true`', () => {
const labelIsAny = { isAny: true };
vm.handleLabelClick(labelIsAny);
expect(Array.isArray(vm.epicContext.labels)).toBe(true);
expect(vm.epicContext.labels.length).toBe(0);
});
it('adds provided `label` to epicContext.labels', () => {
vm.handleLabelClick(label);
// epicContext.labels gets initialized with initialLabels, hence
// newly insert label will be at second position (index `1`)
expect(vm.epicContext.labels.length).toBe(2);
expect(vm.epicContext.labels[1].id).toBe(label.id);
vm.handleLabelClick(label);
});
it('filters epicContext.labels to exclude provided `label` if it is already present in `epicContext.labels`', () => {
vm.handleLabelClick(label); // Select
vm.handleLabelClick(label); // Un-select
expect(vm.epicContext.labels.length).toBe(1);
expect(vm.epicContext.labels[0].id).toBe(labels[0].id);
});
});
describe('saveDate error', () => { describe('saveDate error', () => {
let interceptor; let interceptor;
let component; let component;
...@@ -160,6 +230,11 @@ describe('epicSidebar', () => { ...@@ -160,6 +230,11 @@ describe('epicSidebar', () => {
component = new EpicSidebar({ component = new EpicSidebar({
propsData: { propsData: {
endpoint: gl.TEST_HOST, endpoint: gl.TEST_HOST,
initialLabels: labels,
updatePath: updateEndpoint,
labelsPath,
labelsWebUrl,
epicsWebUrl,
}, },
}); });
}); });
......
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