Commit 386e7775 authored by Florie Guibert's avatar Florie Guibert Committed by Kushal Pandya

Update labels in Vue with GlLabel component

- Epic sidebar
- Issue boards
- Group Issues list
parent f61daee3
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import { GlTooltipDirective } from '@gitlab/ui'; import { GlLabel, GlTooltipDirective } from '@gitlab/ui';
import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner'; import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner';
import { sprintf, __ } from '~/locale'; import { sprintf, __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
...@@ -10,18 +10,17 @@ import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_ ...@@ -10,18 +10,17 @@ import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_
import IssueDueDate from './issue_due_date.vue'; import IssueDueDate from './issue_due_date.vue';
import IssueTimeEstimate from './issue_time_estimate.vue'; import IssueTimeEstimate from './issue_time_estimate.vue';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
import IssueCardInnerScopedLabel from './issue_card_inner_scoped_label.vue';
import { isScopedLabel } from '~/lib/utils/common_utils'; import { isScopedLabel } from '~/lib/utils/common_utils';
export default { export default {
components: { components: {
GlLabel,
Icon, Icon,
UserAvatarLink, UserAvatarLink,
TooltipOnTruncate, TooltipOnTruncate,
IssueDueDate, IssueDueDate,
IssueTimeEstimate, IssueTimeEstimate,
IssueCardWeight: () => import('ee_component/boards/components/issue_card_weight.vue'), IssueCardWeight: () => import('ee_component/boards/components/issue_card_weight.vue'),
IssueCardInnerScopedLabel,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -145,12 +144,6 @@ export default { ...@@ -145,12 +144,6 @@ export default {
boardsStore.toggleFilter(filter); boardsStore.toggleFilter(filter);
}, },
labelStyle(label) {
return {
backgroundColor: label.color,
color: label.textColor,
};
},
showScopedLabel(label) { showScopedLabel(label) {
return boardsStore.scopedLabels.enabled && isScopedLabel(label); return boardsStore.scopedLabels.enabled && isScopedLabel(label);
}, },
...@@ -184,27 +177,16 @@ export default { ...@@ -184,27 +177,16 @@ export default {
</div> </div>
<div v-if="showLabelFooter" class="board-card-labels prepend-top-4 d-flex flex-wrap"> <div v-if="showLabelFooter" class="board-card-labels prepend-top-4 d-flex flex-wrap">
<template v-for="label in orderedLabels"> <template v-for="label in orderedLabels">
<issue-card-inner-scoped-label <gl-label
v-if="showScopedLabel(label)"
:key="label.id" :key="label.id"
:label="label" :background-color="label.color"
:label-style="labelStyle(label)" :title="label.title"
:description="label.description"
size="sm"
:scoped="showScopedLabel(label)"
:scoped-labels-documentation-link="helpLink" :scoped-labels-documentation-link="helpLink"
@scoped-label-click="filterByLabel($event)"
/>
<button
v-else
:key="label.id"
v-gl-tooltip
:style="labelStyle(label)"
:title="label.description"
class="badge color-label append-right-4 prepend-top-4"
type="button"
@click="filterByLabel(label)" @click="filterByLabel(label)"
> />
{{ label.title }}
</button>
</template> </template>
</div> </div>
<div class="board-card-footer d-flex justify-content-between align-items-end"> <div class="board-card-footer d-flex justify-content-between align-items-end">
......
<script>
import { GlLink, GlTooltip } from '@gitlab/ui';
export default {
components: {
GlTooltip,
GlLink,
},
props: {
label: {
type: Object,
required: true,
},
labelStyle: {
type: Object,
required: true,
},
scopedLabelsDocumentationLink: {
type: String,
required: true,
},
},
};
</script>
<template>
<span
class="d-inline-block position-relative scoped-label-wrapper append-right-4 prepend-top-4 board-label"
>
<a @click="$emit('scoped-label-click', label)">
<span :ref="'labelTitleRef'" :style="labelStyle" class="badge label color-label">
{{ label.title }}
</span>
<gl-tooltip :target="() => $refs.labelTitleRef" placement="top" boundary="viewport">
<span class="font-weight-bold scoped-label-tooltip-title">{{ __('Scoped label') }}</span
><br />
{{ label.description }}
</gl-tooltip>
</a>
<gl-link :href="scopedLabelsDocumentationLink" target="_blank" class="label scoped-label"
><i class="fa fa-question-circle" :style="labelStyle"></i
></gl-link>
</span>
</template>
<script> <script>
import DropdownValueScopedLabel from './dropdown_value_scoped_label.vue'; import { GlLabel } from '@gitlab/ui';
import DropdownValueRegularLabel from './dropdown_value_regular_label.vue';
import { isScopedLabel } from '~/lib/utils/common_utils'; import { isScopedLabel } from '~/lib/utils/common_utils';
export default { export default {
components: { components: {
DropdownValueScopedLabel, GlLabel,
DropdownValueRegularLabel,
}, },
props: { props: {
labels: { labels: {
...@@ -37,12 +35,6 @@ export default { ...@@ -37,12 +35,6 @@ export default {
labelFilterUrl(label) { labelFilterUrl(label) {
return `${this.labelFilterBasePath}?label_name[]=${encodeURIComponent(label.title)}`; return `${this.labelFilterBasePath}?label_name[]=${encodeURIComponent(label.title)}`;
}, },
labelStyle(label) {
return {
color: label.textColor,
backgroundColor: label.color,
};
},
scopedLabelsDescription({ description = '' }) { scopedLabelsDescription({ description = '' }) {
return `<span class="font-weight-bold scoped-label-tooltip-title">Scoped label</span><br />${description}`; return `<span class="font-weight-bold scoped-label-tooltip-title">Scoped label</span><br />${description}`;
}, },
...@@ -65,22 +57,15 @@ export default { ...@@ -65,22 +57,15 @@ export default {
</span> </span>
<template v-for="label in labels" v-else> <template v-for="label in labels" v-else>
<dropdown-value-scoped-label <gl-label
v-if="showScopedLabels(label)"
:key="label.id" :key="label.id"
:label="label" :target="labelFilterUrl(label)"
:label-filter-url="labelFilterUrl(label)" :background-color="label.color"
:label-style="labelStyle(label)" :title="label.title"
:description="label.description"
:scoped="showScopedLabels(label)"
:scoped-labels-documentation-link="scopedLabelsDocumentationLink" :scoped-labels-documentation-link="scopedLabelsDocumentationLink"
/> />
<dropdown-value-regular-label
v-else
:key="label.id"
:label="label"
:label-filter-url="labelFilterUrl(label)"
:label-style="labelStyle(label)"
/>
</template> </template>
</div> </div>
</template> </template>
<script>
import { GlTooltip } from '@gitlab/ui';
export default {
components: {
GlTooltip,
},
props: {
label: {
type: Object,
required: true,
},
labelStyle: {
type: Object,
required: true,
},
labelFilterUrl: {
type: String,
required: true,
},
},
};
</script>
<template>
<a ref="regularLabelRef" :href="labelFilterUrl">
<span :style="labelStyle" class="badge color-label">
{{ label.title }}
</span>
<gl-tooltip
v-if="label.description"
:target="() => $refs.regularLabelRef"
placement="top"
boundary="viewport"
>
{{ label.description }}
</gl-tooltip>
</a>
</template>
<script>
import { GlLink, GlTooltip } from '@gitlab/ui';
export default {
components: {
GlTooltip,
GlLink,
},
props: {
label: {
type: Object,
required: true,
},
labelStyle: {
type: Object,
required: true,
},
scopedLabelsDocumentationLink: {
type: String,
required: true,
},
labelFilterUrl: {
type: String,
required: true,
},
},
};
</script>
<template>
<span class="d-inline-block position-relative scoped-label-wrapper">
<a :href="labelFilterUrl">
<span :ref="`labelTitleRef`" :style="labelStyle" class="badge color-label label">
{{ label.title }}
</span>
<gl-tooltip
v-if="label.description"
:target="() => $refs.labelTitleRef"
placement="top"
boundary="viewport"
>
<span class="font-weight-bold scoped-label-tooltip-title">{{ __('Scoped label') }}</span
><br />
{{ label.description }}
</gl-tooltip>
</a>
<gl-link :href="scopedLabelsDocumentationLink" target="_blank" class="label scoped-label"
><i class="fa fa-question-circle" :style="labelStyle"></i
></gl-link>
</span>
</template>
...@@ -266,20 +266,9 @@ ...@@ -266,20 +266,9 @@
background-color: $blue-50; background-color: $blue-50;
} }
.badge { .gl-label {
border: 0; margin-top: 4px;
outline: 0; margin-right: 4px;
&:hover {
text-decoration: underline;
}
@include media-breakpoint-down(lg) {
font-size: $gl-font-size-xs;
padding-left: $gl-padding-4;
padding-right: $gl-padding-4;
font-weight: $gl-font-weight-bold;
}
} }
.confidential-icon { .confidential-icon {
......
...@@ -158,6 +158,10 @@ ...@@ -158,6 +158,10 @@
a:not(.btn) { a:not(.btn) {
color: inherit; color: inherit;
.gl-label-text:hover {
color: inherit;
}
&:hover { &:hover {
color: $blue-800; color: $blue-800;
......
---
title: Update labels in Vue with GlLabel component
merge_request: 21465
author:
type: changed
...@@ -13,6 +13,7 @@ import { __, n__ } from '~/locale'; ...@@ -13,6 +13,7 @@ import { __, n__ } from '~/locale';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow'; import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import boardsStoreEE from '../stores/boards_store_ee'; import boardsStoreEE from '../stores/boards_store_ee';
import flash from '~/flash'; import flash from '~/flash';
import { isScopedLabel } from '~/lib/utils/common_utils';
// NOTE: need to revisit how we handle headerHeight, because we have so many different header and footer options. // NOTE: need to revisit how we handle headerHeight, because we have so many different header and footer options.
export default { export default {
...@@ -157,6 +158,12 @@ export default { ...@@ -157,6 +158,12 @@ export default {
onEnter() { onEnter() {
this.offFocus(); this.offFocus();
}, },
showScopedLabels(label) {
return boardsStoreEE.store.scopedLabels.enabled && isScopedLabel(label);
},
helpLink() {
return boardsStoreEE.store.scopedLabels.helpLink;
},
}, },
}; };
</script> </script>
...@@ -176,7 +183,7 @@ export default { ...@@ -176,7 +183,7 @@ export default {
<gl-label <gl-label
:title="activeListLabel.title" :title="activeListLabel.title"
:background-color="activeListLabel.color" :background-color="activeListLabel.color"
color="light" :scoped="showScopedLabels(activeListLabel)"
/> />
</template> </template>
<template v-else-if="boardListType === $options.assignee"> <template v-else-if="boardListType === $options.assignee">
......
...@@ -90,7 +90,7 @@ export default { ...@@ -90,7 +90,7 @@ export default {
new ListLabel({ new ListLabel({
id: label.id, id: label.id,
title: label.title, title: label.title,
color: label.color[0], color: label.color,
textColor: label.text_color, textColor: label.text_color,
}), }),
); );
......
...@@ -14,6 +14,8 @@ describe 'Issue Boards', :js do ...@@ -14,6 +14,8 @@ describe 'Issue Boards', :js do
let!(:stretch) { create(:label, project: project, name: 'Stretch') } let!(:stretch) { create(:label, project: project, name: 'Stretch') }
let!(:issue1) { create(:labeled_issue, project: project, assignees: [user], milestone: milestone, labels: [development], weight: 3, relative_position: 2) } let!(:issue1) { create(:labeled_issue, project: project, assignees: [user], milestone: milestone, labels: [development], weight: 3, relative_position: 2) }
let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) } let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) }
let!(:scoped_label_1) { create(:label, project: project, name: 'Scoped1::Label1') }
let!(:scoped_label_2) { create(:label, project: project, name: 'Scoped2::Label2') }
let(:board) { create(:board, project: project) } let(:board) { create(:board, project: project) }
let!(:list) { create(:list, board: board, label: development, position: 0) } let!(:list) { create(:list, board: board, label: development, position: 0) }
let(:card1) { find('.board:nth-child(2)').find('.board-card:nth-child(2)') } let(:card1) { find('.board:nth-child(2)').find('.board-card:nth-child(2)') }
...@@ -252,9 +254,6 @@ describe 'Issue Boards', :js do ...@@ -252,9 +254,6 @@ describe 'Issue Boards', :js do
end end
context 'scoped labels' do context 'scoped labels' do
let!(:scoped_label_1) { create(:label, project: project, name: 'Scoped::Label1') }
let!(:scoped_label_2) { create(:label, project: project, name: 'Scoped::Label2') }
before do before do
stub_licensed_features(scoped_labels: true) stub_licensed_features(scoped_labels: true)
...@@ -262,7 +261,7 @@ describe 'Issue Boards', :js do ...@@ -262,7 +261,7 @@ describe 'Issue Boards', :js do
wait_for_requests wait_for_requests
end end
it 'removes existing scoped label' do it 'adds multiple scoped labels' do
click_card(card1) click_card(card1)
page.within('.labels') do page.within('.labels') do
...@@ -280,6 +279,42 @@ describe 'Issue Boards', :js do ...@@ -280,6 +279,42 @@ describe 'Issue Boards', :js do
find('.dropdown-menu-close-icon').click find('.dropdown-menu-close-icon').click
page.within('.value') do
expect(page).to have_selector('.gl-label-scoped', count: 2)
expect(page).to have_content(scoped_label_1.scoped_label_key)
expect(page).to have_content(scoped_label_1.scoped_label_value)
expect(page).to have_content(scoped_label_2.scoped_label_key)
expect(page).to have_content(scoped_label_2.scoped_label_value)
end
end
end
context 'with scoped label assigned' do
let!(:issue3) { create(:labeled_issue, project: project, labels: [development, scoped_label_1, scoped_label_2], relative_position: 3) }
let(:board) { create(:board, project: project) }
let(:card3) { find('.board:nth-child(2)').find('.board-card:nth-child(1)') }
before do
stub_licensed_features(scoped_labels: true)
visit project_board_path(project, board)
wait_for_requests
end
it 'removes existing scoped label' do
click_card(card3)
page.within('.labels') do
click_link 'Edit'
wait_for_requests
click_link scoped_label_2.title
wait_for_requests
find('.dropdown-menu-close-icon').click
page.within('.value') do page.within('.value') do
expect(page).to have_selector('.gl-label-scoped', count: 1) expect(page).to have_selector('.gl-label-scoped', count: 1)
expect(page).not_to have_content(scoped_label_1.scoped_label_value) expect(page).not_to have_content(scoped_label_1.scoped_label_value)
...@@ -288,9 +323,12 @@ describe 'Issue Boards', :js do ...@@ -288,9 +323,12 @@ describe 'Issue Boards', :js do
end end
end end
expect(card1).to have_selector('.scoped-label-wrapper', count: 1) expect(card3).to have_selector('.gl-label-scoped', count: 1)
expect(card1).not_to have_content(scoped_label_1.title) expect(card3).not_to have_content(scoped_label_1.scoped_label_key)
expect(card1).to have_content(scoped_label_2.title) expect(card3).not_to have_content(scoped_label_1.scoped_label_value)
expect(card3).to have_content(scoped_label_2.scoped_label_key)
expect(card3).to have_content(scoped_label_2.scoped_label_value)
end
end end
end end
end end
...@@ -51,7 +51,7 @@ describe 'Labels Hierarchy', :js do ...@@ -51,7 +51,7 @@ describe 'Labels Hierarchy', :js do
expect(page).to have_selector('a', text: issue.title) expect(page).to have_selector('a', text: issue.title)
end end
expect(page).to have_selector('.badge', text: label.title) expect(page).to have_selector('.gl-label', text: label.title)
end end
end end
end end
......
...@@ -64,6 +64,10 @@ describe('BoardSettingsSideBar', () => { ...@@ -64,6 +64,10 @@ describe('BoardSettingsSideBar', () => {
findList: bs.findList, findList: bs.findList,
addList: bs.addList, addList: bs.addList,
removeList: bs.removeList, removeList: bs.removeList,
scopedLabels: {
enabled: false,
scopedLabelsDocumentationLink: '',
},
}; };
boardsStore.initEESpecific(storeMock); boardsStore.initEESpecific(storeMock);
......
import { mount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import IssueCardWeight from 'ee/boards/components/issue_card_weight.vue'; import IssueCardWeight from 'ee/boards/components/issue_card_weight.vue';
import ListIssueEE from 'ee/boards/models/issue'; import ListIssueEE from 'ee/boards/models/issue';
import ListLabel from '~/boards/models/label'; import ListLabel from '~/boards/models/label';
import IssueCardInner from '~/boards/components/issue_card_inner.vue'; import IssueCardInner from '~/boards/components/issue_card_inner.vue';
import defaultStore from '~/boards/stores'; import defaultStore from '~/boards/stores';
import { GlLabel } from '@gitlab/ui';
describe('Issue card component', () => { describe('Issue card component', () => {
let wrapper; let wrapper;
...@@ -11,7 +12,7 @@ describe('Issue card component', () => { ...@@ -11,7 +12,7 @@ describe('Issue card component', () => {
let list; let list;
const createComponent = (props = {}, store = defaultStore) => { const createComponent = (props = {}, store = defaultStore) => {
wrapper = mount(IssueCardInner, { wrapper = shallowMount(IssueCardInner, {
store, store,
propsData: { propsData: {
list, list,
...@@ -33,7 +34,7 @@ describe('Issue card component', () => { ...@@ -33,7 +34,7 @@ describe('Issue card component', () => {
label: { label: {
id: 5000, id: 5000,
title: 'Testing', title: 'Testing',
color: 'red', color: '#ff0000',
description: 'testing;', description: 'testing;',
textColor: 'white', textColor: 'white',
}, },
...@@ -62,7 +63,7 @@ describe('Issue card component', () => { ...@@ -62,7 +63,7 @@ describe('Issue card component', () => {
const label1 = new ListLabel({ const label1 = new ListLabel({
id: 3, id: 3,
title: 'testing 123', title: 'testing 123',
color: 'blue', color: '#000cff',
text_color: 'white', text_color: 'white',
description: 'test', description: 'test',
}); });
...@@ -80,13 +81,14 @@ describe('Issue card component', () => { ...@@ -80,13 +81,14 @@ describe('Issue card component', () => {
id: 9001, id: 9001,
type, type,
title, title,
color: '#000000',
}), }),
); );
createComponent({ groupId: 1 }); createComponent({ groupId: 1 });
expect(wrapper.findAll('.badge').length).toBe(3); expect(wrapper.findAll(GlLabel).length).toBe(3);
expect(wrapper.text()).toContain(title); expect(wrapper.find(GlLabel).props('title')).toContain(title);
}); });
it('shows no labels when the isShowingLabels state is false', () => { it('shows no labels when the isShowingLabels state is false', () => {
......
import Vue from 'vue'; import Vue from 'vue';
import { mount } from '@vue/test-utils';
import SidebarLabels from 'ee/epic/components/sidebar_items/sidebar_labels.vue'; import SidebarLabels from 'ee/epic/components/sidebar_items/sidebar_labels.vue';
import createStore from 'ee/epic/store'; import createStore from 'ee/epic/store';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import { mockEpicMeta, mockEpicData, mockLabels } from '../../mock_data'; import { mockEpicMeta, mockEpicData, mockLabels } from '../../mock_data';
describe('SidebarLabelsComponent', () => { describe('SidebarLabelsComponent', () => {
let vm; let wrapper;
let store; let store;
beforeEach(() => { beforeEach(() => {
...@@ -16,26 +16,30 @@ describe('SidebarLabelsComponent', () => { ...@@ -16,26 +16,30 @@ describe('SidebarLabelsComponent', () => {
store.dispatch('setEpicMeta', mockEpicMeta); store.dispatch('setEpicMeta', mockEpicMeta);
store.dispatch('setEpicData', mockEpicData); store.dispatch('setEpicData', mockEpicData);
vm = mountComponentWithStore(Component, { wrapper = mount(Component, {
propsData: { canUpdate: false, sidebarCollapsed: false },
store, store,
props: { canUpdate: false, sidebarCollapsed: false }, stubs: {
GlLabel: true,
},
}); });
}); });
afterEach(() => { afterEach(() => {
vm.$destroy(); wrapper.destroy();
wrapper = null;
}); });
describe('data', () => { describe('data', () => {
it('returns default data props', () => { it('returns default data props', () => {
expect(vm.sidebarExpandedOnClick).toBe(false); expect(wrapper.vm.sidebarExpandedOnClick).toBe(false);
}); });
}); });
describe('computed', () => { describe('computed', () => {
describe('epicContext', () => { describe('epicContext', () => {
it('returns object containing `this.labels` as a child prop', () => { it('returns object containing `this.labels` as a child prop', () => {
expect(vm.epicContext.labels).toBe(vm.labels); expect(wrapper.vm.epicContext.labels).toBe(wrapper.vm.labels);
}); });
}); });
}); });
...@@ -43,11 +47,11 @@ describe('SidebarLabelsComponent', () => { ...@@ -43,11 +47,11 @@ describe('SidebarLabelsComponent', () => {
describe('methods', () => { describe('methods', () => {
describe('toggleSidebarRevealLabelsDropdown', () => { describe('toggleSidebarRevealLabelsDropdown', () => {
it('calls `toggleSidebar` action with param `sidebarCollapse`', () => { it('calls `toggleSidebar` action with param `sidebarCollapse`', () => {
jest.spyOn(vm, 'toggleSidebar'); jest.spyOn(wrapper.vm, 'toggleSidebar');
vm.toggleSidebarRevealLabelsDropdown(); wrapper.vm.toggleSidebarRevealLabelsDropdown();
expect(vm.toggleSidebar).toHaveBeenCalledWith( expect(wrapper.vm.toggleSidebar).toHaveBeenCalledWith(
jasmine.objectContaining({ jasmine.objectContaining({
sidebarCollapsed: false, sidebarCollapsed: false,
}), }),
...@@ -57,14 +61,14 @@ describe('SidebarLabelsComponent', () => { ...@@ -57,14 +61,14 @@ describe('SidebarLabelsComponent', () => {
describe('handleDropdownClose', () => { describe('handleDropdownClose', () => {
it('calls `toggleSidebar` action only when `sidebarExpandedOnClick` prop is true', () => { it('calls `toggleSidebar` action only when `sidebarExpandedOnClick` prop is true', () => {
jest.spyOn(vm, 'toggleSidebar'); jest.spyOn(wrapper.vm, 'toggleSidebar');
vm.sidebarExpandedOnClick = true; wrapper.vm.sidebarExpandedOnClick = true;
vm.handleDropdownClose(); wrapper.vm.handleDropdownClose();
expect(vm.sidebarExpandedOnClick).toBe(false); expect(wrapper.vm.sidebarExpandedOnClick).toBe(false);
expect(vm.toggleSidebar).toHaveBeenCalledWith( expect(wrapper.vm.toggleSidebar).toHaveBeenCalledWith(
jasmine.objectContaining({ jasmine.objectContaining({
sidebarCollapsed: false, sidebarCollapsed: false,
}), }),
...@@ -86,34 +90,34 @@ describe('SidebarLabelsComponent', () => { ...@@ -86,34 +90,34 @@ describe('SidebarLabelsComponent', () => {
it('initializes `epicContext.labels` as empty array when `label.isAny` is `true`', () => { it('initializes `epicContext.labels` as empty array when `label.isAny` is `true`', () => {
const labelIsAny = { isAny: true }; const labelIsAny = { isAny: true };
vm.handleLabelClick(labelIsAny); wrapper.vm.handleLabelClick(labelIsAny);
expect(Array.isArray(vm.epicContext.labels)).toBe(true); expect(Array.isArray(wrapper.vm.epicContext.labels)).toBe(true);
expect(vm.epicContext.labels.length).toBe(0); expect(wrapper.vm.epicContext.labels.length).toBe(0);
}); });
it('adds provided `label` to epicContext.labels', () => { it('adds provided `label` to epicContext.labels', () => {
vm.handleLabelClick(label); wrapper.vm.handleLabelClick(label);
// epicContext.labels gets initialized with initialLabels, hence // epicContext.labels gets initialized with initialLabels, hence
// newly insert label will be at second position (index `1`) // newly insert label will be at second position (index `1`)
expect(vm.epicContext.labels.length).toBe(2); expect(wrapper.vm.epicContext.labels.length).toBe(2);
expect(vm.epicContext.labels[1].id).toBe(label.id); expect(wrapper.vm.epicContext.labels[1].id).toBe(label.id);
vm.handleLabelClick(label); wrapper.vm.handleLabelClick(label);
}); });
it('filters epicContext.labels to exclude provided `label` if it is already present in `epicContext.labels`', () => { it('filters epicContext.labels to exclude provided `label` if it is already present in `epicContext.labels`', () => {
vm.handleLabelClick(label); // Select wrapper.vm.handleLabelClick(label); // Select
vm.handleLabelClick(label); // Un-select wrapper.vm.handleLabelClick(label); // Un-select
expect(vm.epicContext.labels.length).toBe(1); expect(wrapper.vm.epicContext.labels.length).toBe(1);
expect(vm.epicContext.labels[0].id).toBe(mockLabels[0].id); expect(wrapper.vm.epicContext.labels[0].id).toBe(mockLabels[0].id);
}); });
}); });
}); });
describe('template', () => { describe('template', () => {
it('renders labels select element container', () => { it('renders labels select element container', () => {
expect(vm.$el.classList.contains('js-labels-block')).toBe(true); expect(wrapper.vm.$el.classList.contains('js-labels-block')).toBe(true);
}); });
}); });
}); });
...@@ -16983,9 +16983,6 @@ msgstr "" ...@@ -16983,9 +16983,6 @@ msgstr ""
msgid "Scoped issue boards" msgid "Scoped issue boards"
msgstr "" msgstr ""
msgid "Scoped label"
msgstr ""
msgid "Scopes" msgid "Scopes"
msgstr "" msgstr ""
......
...@@ -519,7 +519,7 @@ describe 'Issue Boards', :js do ...@@ -519,7 +519,7 @@ describe 'Issue Boards', :js do
page.within(find('.board:nth-child(2)')) do page.within(find('.board:nth-child(2)')) do
expect(page).to have_selector('.board-card', count: 8) expect(page).to have_selector('.board-card', count: 8)
expect(find('.board-card', match: :first)).to have_content(bug.title) expect(find('.board-card', match: :first)).to have_content(bug.title)
click_button(bug.title) click_link(bug.title)
wait_for_requests wait_for_requests
end end
...@@ -536,7 +536,7 @@ describe 'Issue Boards', :js do ...@@ -536,7 +536,7 @@ describe 'Issue Boards', :js do
it 'removes label filter by clicking label button on issue' do it 'removes label filter by clicking label button on issue' do
page.within(find('.board:nth-child(2)')) do page.within(find('.board:nth-child(2)')) do
page.within(find('.board-card', match: :first)) do page.within(find('.board-card', match: :first)) do
click_button(bug.title) click_link(bug.title)
end end
wait_for_requests wait_for_requests
......
...@@ -305,7 +305,7 @@ describe 'Issue Boards', :js do ...@@ -305,7 +305,7 @@ describe 'Issue Boards', :js do
end end
# 'Development' label does not show since the card is in a 'Development' list label # 'Development' label does not show since the card is in a 'Development' list label
expect(card).to have_selector('.badge', count: 2) expect(card).to have_selector('.gl-label', count: 2)
expect(card).to have_content(bug.title) expect(card).to have_content(bug.title)
end end
...@@ -335,7 +335,7 @@ describe 'Issue Boards', :js do ...@@ -335,7 +335,7 @@ describe 'Issue Boards', :js do
end end
# 'Development' label does not show since the card is in a 'Development' list label # 'Development' label does not show since the card is in a 'Development' list label
expect(card).to have_selector('.badge', count: 3) expect(card).to have_selector('.gl-label', count: 3)
expect(card).to have_content(bug.title) expect(card).to have_content(bug.title)
expect(card).to have_content(regression.title) expect(card).to have_content(regression.title)
end end
......
import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import IssueCardInnerScopedLabel from '~/boards/components/issue_card_inner_scoped_label.vue';
describe('IssueCardInnerScopedLabel Component', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(IssueCardInnerScopedLabel, {
propsData: {
label: { title: 'Foo::Bar', description: 'Some Random Description' },
labelStyle: { background: 'white', color: 'black' },
scopedLabelsDocumentationLink: '/docs-link',
},
});
});
afterEach(() => {
wrapper.destroy();
});
it('should render label title', () => {
expect(wrapper.find('.color-label').text()).toBe('Foo::Bar');
});
it('should render question mark symbol', () => {
expect(wrapper.find('.fa-question-circle').exists()).toBe(true);
});
it('should render label style provided', () => {
const label = wrapper.find('.color-label');
expect(label.attributes('style')).toContain('background: white;');
expect(label.attributes('style')).toContain('color: black;');
});
it('should render the docs link', () => {
expect(wrapper.find(GlLink).attributes('href')).toBe('/docs-link');
});
});
...@@ -8,6 +8,7 @@ import '~/boards/models/list'; ...@@ -8,6 +8,7 @@ import '~/boards/models/list';
import IssueCardInner from '~/boards/components/issue_card_inner.vue'; import IssueCardInner from '~/boards/components/issue_card_inner.vue';
import { listObj } from '../../javascripts/boards/mock_data'; import { listObj } from '../../javascripts/boards/mock_data';
import store from '~/boards/stores'; import store from '~/boards/stores';
import { GlLabel } from '@gitlab/ui';
describe('Issue card component', () => { describe('Issue card component', () => {
const user = new ListAssignee({ const user = new ListAssignee({
...@@ -20,7 +21,7 @@ describe('Issue card component', () => { ...@@ -20,7 +21,7 @@ describe('Issue card component', () => {
const label1 = new ListLabel({ const label1 = new ListLabel({
id: 3, id: 3,
title: 'testing 123', title: 'testing 123',
color: 'blue', color: '#000CFF',
text_color: 'white', text_color: 'white',
description: 'test', description: 'test',
}); });
...@@ -50,6 +51,9 @@ describe('Issue card component', () => { ...@@ -50,6 +51,9 @@ describe('Issue card component', () => {
rootPath: '/', rootPath: '/',
}, },
store, store,
stubs: {
GlLabel: true,
},
}); });
}); });
...@@ -290,25 +294,11 @@ describe('Issue card component', () => { ...@@ -290,25 +294,11 @@ describe('Issue card component', () => {
}); });
it('does not render list label but renders all other labels', () => { it('does not render list label but renders all other labels', () => {
expect(wrapper.findAll('.badge').length).toBe(1); expect(wrapper.findAll(GlLabel).length).toBe(1);
}); const label = wrapper.find(GlLabel);
expect(label.props('title')).toEqual(label1.title);
it('renders label', () => { expect(label.props('description')).toEqual(label1.description);
const nodes = wrapper.findAll('.badge').wrappers.map(label => label.attributes('title')); expect(label.props('backgroundColor')).toEqual(label1.color);
expect(nodes.includes(label1.description)).toBe(true);
});
it('sets label description as title', () => {
expect(wrapper.find('.badge').attributes('title')).toContain(label1.description);
});
it('sets background color of button', () => {
const nodes = wrapper
.findAll('.badge')
.wrappers.map(label => label.element.style.backgroundColor);
expect(nodes.includes(label1.color)).toBe(true);
}); });
it('does not render label if label does not have an ID', done => { it('does not render label if label does not have an ID', done => {
...@@ -321,7 +311,7 @@ describe('Issue card component', () => { ...@@ -321,7 +311,7 @@ describe('Issue card component', () => {
wrapper.vm wrapper.vm
.$nextTick() .$nextTick()
.then(() => { .then(() => {
expect(wrapper.findAll('.badge').length).toBe(1); expect(wrapper.findAll(GlLabel).length).toBe(1);
expect(wrapper.text()).not.toContain('closed'); expect(wrapper.text()).not.toContain('closed');
done(); done();
}) })
......
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { hexToRgb } from '~/lib/utils/color_utils';
import DropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue'; import DropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue';
import DropdownValueScopedLabel from '~/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue'; import { GlLabel } from '@gitlab/ui';
import { import {
mockConfig, mockConfig,
mockLabels, mockLabels,
} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data'; } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
const labelStyles = {
textColor: '#FFFFFF',
color: '#BADA55',
};
const createComponent = ( const createComponent = (
labels = mockLabels, labels = mockLabels,
labelFilterBasePath = mockConfig.labelFilterBasePath, labelFilterBasePath = mockConfig.labelFilterBasePath,
) => { ) =>
labels.forEach(label => Object.assign(label, labelStyles)); mount(DropdownValueComponent, {
return mount(DropdownValueComponent, {
propsData: { propsData: {
labels, labels,
labelFilterBasePath, labelFilterBasePath,
enableScopedLabels: true, enableScopedLabels: true,
}, },
stubs: {
GlLabel: true,
},
}); });
};
describe('DropdownValueComponent', () => { describe('DropdownValueComponent', () => {
let vm; let vm;
...@@ -56,24 +51,17 @@ describe('DropdownValueComponent', () => { ...@@ -56,24 +51,17 @@ describe('DropdownValueComponent', () => {
describe('methods', () => { describe('methods', () => {
describe('labelFilterUrl', () => { describe('labelFilterUrl', () => {
it('returns URL string starting with labelFilterBasePath and encoded label.title', () => { it('returns URL string starting with labelFilterBasePath and encoded label.title', () => {
expect(vm.find(DropdownValueScopedLabel).props('labelFilterUrl')).toBe( expect(vm.find(GlLabel).props('target')).toBe(
'/gitlab-org/my-project/issues?label_name[]=Foo%3A%3ABar', '/gitlab-org/my-project/issues?label_name[]=Foo%20Label',
); );
}); });
}); });
describe('labelStyle', () => {
it('returns object with `color` & `backgroundColor` properties from label.textColor & label.color', () => {
expect(vm.find(DropdownValueScopedLabel).props('labelStyle')).toEqual({
color: labelStyles.textColor,
backgroundColor: labelStyles.color,
});
});
});
describe('showScopedLabels', () => { describe('showScopedLabels', () => {
it('returns true if the label is scoped label', () => { it('returns true if the label is scoped label', () => {
expect(vm.findAll(DropdownValueScopedLabel).length).toEqual(1); const labels = vm.findAll(GlLabel);
expect(labels.length).toEqual(2);
expect(labels.at(1).props('scoped')).toBe(true);
}); });
}); });
}); });
...@@ -95,33 +83,10 @@ describe('DropdownValueComponent', () => { ...@@ -95,33 +83,10 @@ describe('DropdownValueComponent', () => {
vmEmptyLabels.destroy(); vmEmptyLabels.destroy();
}); });
it('renders label element with filter URL', () => { it('renders DropdownValueComponent element', () => {
expect(vm.find('a').attributes('href')).toBe( const labelEl = vm.find(GlLabel);
'/gitlab-org/my-project/issues?label_name[]=Foo%20Label',
);
});
it('renders label element and styles based on label details', () => {
const labelEl = vm.find('a span.badge.color-label');
expect(labelEl.exists()).toBe(true); expect(labelEl.exists()).toBe(true);
expect(labelEl.attributes('style')).toContain(
`background-color: rgb(${hexToRgb(labelStyles.color).join(', ')});`,
);
expect(labelEl.text().trim()).toBe(mockLabels[0].title);
});
describe('label is of scoped-label type', () => {
it('renders a scoped-label-wrapper span to incorporate 2 anchors', () => {
expect(vm.find('span.scoped-label-wrapper').exists()).toBe(true);
});
it('renders anchor tag containing question icon', () => {
const anchor = vm.find('.scoped-label-wrapper a.scoped-label');
expect(anchor.exists()).toBe(true);
expect(anchor.find('i.fa-question-circle').exists()).toBe(true);
});
}); });
}); });
}); });
...@@ -32,7 +32,7 @@ describe('Board card', () => { ...@@ -32,7 +32,7 @@ describe('Board card', () => {
const label1 = new ListLabel({ const label1 = new ListLabel({
id: 3, id: 3,
title: 'testing 123', title: 'testing 123',
color: 'blue', color: '#000cff',
text_color: 'white', text_color: 'white',
description: 'test', description: 'test',
}); });
...@@ -155,12 +155,6 @@ describe('Board card', () => { ...@@ -155,12 +155,6 @@ describe('Board card', () => {
expect(boardsStore.detail.issue).toEqual({}); expect(boardsStore.detail.issue).toEqual({});
}); });
it('does not set detail issue if button is clicked', () => {
triggerEvent('mouseup', vm.$el.querySelector('button'));
expect(boardsStore.detail.issue).toEqual({});
});
it('does not set detail issue if img is clicked', done => { it('does not set detail issue if img is clicked', done => {
vm.issue.assignees = [ vm.issue.assignees = [
new ListAssignee({ new ListAssignee({
......
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