From 53de2737fd322332b67cbab1cb2df403ec7272a3 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Mar 2018 18:03:03 +0530 Subject: Allow exposing methods to `gon_helper` --- app/helpers/labels_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index c1c19062c91..b2c641a5dbd 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -1,4 +1,5 @@ module LabelsHelper + extend self include ActionView::Helpers::TagHelper def show_label_issuables_link?(label, issuables_type, current_user: nil, project: nil) -- cgit v1.2.1 From e7789ed91934360f7d7ffda4a15c6cfe6ee0101a Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Mar 2018 18:03:25 +0530 Subject: Add `suggest_colors` from LabelsHelper --- lib/gitlab/gon_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 92f0e0402a8..a7e055ac444 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -20,6 +20,7 @@ module Gitlab gon.sprite_icons = IconsHelper.sprite_icon_path gon.sprite_file_icons = IconsHelper.sprite_file_icons_path gon.test_env = Rails.env.test? + gon.suggested_label_colors = LabelsHelper.suggested_colors if current_user gon.current_user_id = current_user.id -- cgit v1.2.1 From 3005d6046484d6cf42fe02aa7577ad123e4d240d Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Mar 2018 18:04:59 +0530 Subject: ListLabel Shared Model --- app/assets/javascripts/boards/models/label.js | 15 --------------- app/assets/javascripts/vue_shared/models/label.js | 13 +++++++++++++ 2 files changed, 13 insertions(+), 15 deletions(-) delete mode 100644 app/assets/javascripts/boards/models/label.js create mode 100644 app/assets/javascripts/vue_shared/models/label.js diff --git a/app/assets/javascripts/boards/models/label.js b/app/assets/javascripts/boards/models/label.js deleted file mode 100644 index 98c1ec014c4..00000000000 --- a/app/assets/javascripts/boards/models/label.js +++ /dev/null @@ -1,15 +0,0 @@ -/* eslint-disable no-unused-vars, space-before-function-paren */ - -class ListLabel { - constructor (obj) { - this.id = obj.id; - this.title = obj.title; - this.type = obj.type; - this.color = obj.color; - this.textColor = obj.text_color; - this.description = obj.description; - this.priority = (obj.priority !== null) ? obj.priority : Infinity; - } -} - -window.ListLabel = ListLabel; diff --git a/app/assets/javascripts/vue_shared/models/label.js b/app/assets/javascripts/vue_shared/models/label.js new file mode 100644 index 00000000000..70b9efe0c68 --- /dev/null +++ b/app/assets/javascripts/vue_shared/models/label.js @@ -0,0 +1,13 @@ +class ListLabel { + constructor(obj) { + this.id = obj.id; + this.title = obj.title; + this.type = obj.type; + this.color = obj.color; + this.textColor = obj.text_color; + this.description = obj.description; + this.priority = (obj.priority !== null) ? obj.priority : Infinity; + } +} + +window.ListLabel = ListLabel; -- cgit v1.2.1 From 3fe4ea8018fa3f4fa8482f03d228b409be774571 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Mar 2018 18:05:58 +0530 Subject: Update path for ListLabel model --- app/assets/javascripts/boards/index.js | 2 +- spec/javascripts/boards/board_card_spec.js | 2 +- spec/javascripts/boards/boards_store_spec.js | 2 +- spec/javascripts/boards/issue_card_spec.js | 2 +- spec/javascripts/boards/issue_spec.js | 2 +- spec/javascripts/boards/list_spec.js | 2 +- spec/javascripts/boards/modal_store_spec.js | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 8e31f1865f0..8b34fe232c2 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -5,12 +5,12 @@ import Vue from 'vue'; import Flash from '~/flash'; import { __ } from '~/locale'; +import '~/vue_shared/models/label'; import FilteredSearchBoards from './filtered_search_boards'; import eventHub from './eventhub'; import sidebarEventHub from '~/sidebar/event_hub'; // eslint-disable-line import/first import './models/issue'; -import './models/label'; import './models/list'; import './models/milestone'; import './models/assignee'; diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js index 80a598e63bd..13d607a06d2 100644 --- a/spec/javascripts/boards/board_card_spec.js +++ b/spec/javascripts/boards/board_card_spec.js @@ -9,8 +9,8 @@ import axios from '~/lib/utils/axios_utils'; import '~/boards/models/assignee'; import eventHub from '~/boards/eventhub'; +import '~/vue_shared/models/label'; import '~/boards/models/list'; -import '~/boards/models/label'; import '~/boards/stores/boards_store'; import boardCard from '~/boards/components/board_card.vue'; import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data'; diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index 8411f4dd8a6..0cf9e4c9ba1 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -7,8 +7,8 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import Cookies from 'js-cookie'; +import '~/vue_shared/models/label'; import '~/boards/models/issue'; -import '~/boards/models/label'; import '~/boards/models/list'; import '~/boards/models/assignee'; import '~/boards/services/board_service'; diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js index 278155c585e..37088a6421c 100644 --- a/spec/javascripts/boards/issue_card_spec.js +++ b/spec/javascripts/boards/issue_card_spec.js @@ -4,8 +4,8 @@ import Vue from 'vue'; +import '~/vue_shared/models/label'; import '~/boards/models/issue'; -import '~/boards/models/label'; import '~/boards/models/list'; import '~/boards/models/assignee'; import '~/boards/stores/boards_store'; diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js index dbbe14fe3e0..4a11131b55c 100644 --- a/spec/javascripts/boards/issue_spec.js +++ b/spec/javascripts/boards/issue_spec.js @@ -3,8 +3,8 @@ /* global ListIssue */ import Vue from 'vue'; +import '~/vue_shared/models/label'; import '~/boards/models/issue'; -import '~/boards/models/label'; import '~/boards/models/list'; import '~/boards/models/assignee'; import '~/boards/services/board_service'; diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js index 34964b20b05..d9a1d692949 100644 --- a/spec/javascripts/boards/list_spec.js +++ b/spec/javascripts/boards/list_spec.js @@ -6,8 +6,8 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import _ from 'underscore'; +import '~/vue_shared/models/label'; import '~/boards/models/issue'; -import '~/boards/models/label'; import '~/boards/models/list'; import '~/boards/models/assignee'; import '~/boards/services/board_service'; diff --git a/spec/javascripts/boards/modal_store_spec.js b/spec/javascripts/boards/modal_store_spec.js index 7eecb58a4c3..e9d77f035e3 100644 --- a/spec/javascripts/boards/modal_store_spec.js +++ b/spec/javascripts/boards/modal_store_spec.js @@ -1,7 +1,7 @@ /* global ListIssue */ +import '~/vue_shared/models/label'; import '~/boards/models/issue'; -import '~/boards/models/label'; import '~/boards/models/list'; import '~/boards/models/assignee'; import '~/boards/stores/modal_store'; -- cgit v1.2.1 From 8c9094cbaf9fa1f5eda5039448f759f745123186 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Mar 2018 18:07:01 +0530 Subject: LabelsSelectComponent tests mock data --- .../components/sidebar/labels_select/mock_data.js | 49 ++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js new file mode 100644 index 00000000000..e9008c29b22 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js @@ -0,0 +1,49 @@ +export const mockLabels = [ + { + id: 26, + title: 'Foo Label', + description: 'Foobar', + color: '#BADA55', + text_color: '#FFFFFF', + }, +]; + +export const mockSuggestedColors = [ + '#0033CC', + '#428BCA', + '#44AD8E', + '#A8D695', + '#5CB85C', + '#69D100', + '#004E00', + '#34495E', + '#7F8C8D', + '#A295D6', + '#5843AD', + '#8E44AD', + '#FFECDB', + '#AD4363', + '#D10069', + '#CC0033', + '#FF0000', + '#D9534F', + '#D1D100', + '#F0AD4E', + '#AD8D43', +]; + +export const mockConfig = { + showCreate: true, + abilityName: 'issue', + context: { + labels: mockLabels, + }, + namespace: 'gitlab-org', + updatePath: '/gitlab-org/my-project/issue/1', + labelsPath: '/gitlab-org/my-project/labels.json', + labelsWebUrl: '/gitlab-org/my-project/labels', + labelFilterBasePath: '/gitlab-org/my-project/issues', + canEdit: true, + suggestedColors: mockSuggestedColors, + emptyValueText: 'None', +}; -- cgit v1.2.1 From 7d58f0e088431b04639639018c36434fb95c2cb9 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Mar 2018 18:07:33 +0530 Subject: LabelsSelect Base Component --- .../components/sidebar/labels_select/base.vue | 149 +++++++++++++++++++++ .../components/sidebar/labels_select/base_spec.js | 81 +++++++++++ 2 files changed, 230 insertions(+) create mode 100644 app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue create mode 100644 spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue new file mode 100644 index 00000000000..3b17135f0e5 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue @@ -0,0 +1,149 @@ + + + diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js new file mode 100644 index 00000000000..67056793a20 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js @@ -0,0 +1,81 @@ +import Vue from 'vue'; + +import LabelsSelect from '~/labels_select'; +import baseComponent from '~/vue_shared/components/sidebar/labels_select/base.vue'; + +import { mockConfig, mockLabels } from './mock_data'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = (config = mockConfig) => { + const Component = Vue.extend(baseComponent); + + return mountComponent(Component, config); +}; + +describe('BaseComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('hiddenInputName', () => { + it('returns correct string when showCreate prop is `true`', () => { + expect(vm.hiddenInputName).toBe('issue[label_names][]'); + }); + + it('returns correct string when showCreate prop is `false`', () => { + const mockConfigNonEditable = Object.assign({}, mockConfig, { showCreate: false }); + const vmNonEditable = createComponent(mockConfigNonEditable); + expect(vmNonEditable.hiddenInputName).toBe('label_id[]'); + vmNonEditable.$destroy(); + }); + }); + }); + + describe('methods', () => { + describe('handleClick', () => { + it('emits onLabelClick event with label and list of labels as params', () => { + spyOn(vm, '$emit'); + vm.handleClick(mockLabels[0]); + expect(vm.$emit).toHaveBeenCalledWith('onLabelClick', mockLabels[0]); + }); + }); + }); + + describe('mounted', () => { + it('creates LabelsSelect object and assigns it to `labelsDropdon` as prop', () => { + expect(vm.labelsDropdown instanceof LabelsSelect).toBe(true); + }); + }); + + describe('template', () => { + it('renders component container element with classes `block labels`', () => { + expect(vm.$el.classList.contains('block')).toBe(true); + expect(vm.$el.classList.contains('labels')).toBe(true); + }); + + it('renders `.selectbox` element', () => { + expect(vm.$el.querySelector('.selectbox')).not.toBeNull(); + expect(vm.$el.querySelector('.selectbox').getAttribute('style')).toBe('display: none;'); + }); + + it('renders `.dropdown` element', () => { + expect(vm.$el.querySelector('.dropdown')).not.toBeNull(); + }); + + it('renders `.dropdown-menu` element', () => { + const dropdownMenuEl = vm.$el.querySelector('.dropdown-menu'); + expect(dropdownMenuEl).not.toBeNull(); + expect(dropdownMenuEl.querySelector('.dropdown-page-one')).not.toBeNull(); + expect(dropdownMenuEl.querySelector('.dropdown-content')).not.toBeNull(); + expect(dropdownMenuEl.querySelector('.dropdown-loading')).not.toBeNull(); + }); + }); +}); -- cgit v1.2.1 From 5c8854864ae4886c8a642536f6b4297a633144d2 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Mar 2018 18:07:52 +0530 Subject: LabelsSelect DropdownButton Component --- .../sidebar/labels_select/dropdown_button.vue | 78 ++++++++++++++++++++ .../sidebar/labels_select/dropdown_button_spec.js | 82 ++++++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue create mode 100644 spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue new file mode 100644 index 00000000000..47497c1de98 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue @@ -0,0 +1,78 @@ + + + diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js new file mode 100644 index 00000000000..ec63ac306d0 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js @@ -0,0 +1,82 @@ +import Vue from 'vue'; + +import dropdownButtonComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_button.vue'; + +import { mockConfig, mockLabels } from './mock_data'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const componentConfig = Object.assign({}, mockConfig, { + fieldName: 'label_id[]', + labels: mockLabels, + showExtraOptions: false, +}); + +const createComponent = (config = componentConfig) => { + const Component = Vue.extend(dropdownButtonComponent); + + return mountComponent(Component, config); +}; + +describe('DropdownButtonComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('dropdownToggleText', () => { + it('returns text as `Label` when `labels` prop is empty array', () => { + const mockEmptyLabels = Object.assign({}, componentConfig, { labels: [] }); + const vmEmptyLabels = createComponent(mockEmptyLabels); + expect(vmEmptyLabels.dropdownToggleText).toBe('Label'); + vmEmptyLabels.$destroy(); + }); + + it('returns first label name with remaining label count when `labels` prop has more than one item', () => { + const mockMoreLabels = Object.assign({}, componentConfig, { + labels: mockLabels.concat(mockLabels), + }); + const vmMoreLabels = createComponent(mockMoreLabels); + expect(vmMoreLabels.dropdownToggleText).toBe('Foo Label +1 more'); + vmMoreLabels.$destroy(); + }); + + it('returns first label name when `labels` prop has only one item present', () => { + expect(vm.dropdownToggleText).toBe('Foo Label'); + }); + }); + }); + + describe('template', () => { + it('renders component container element of type `button`', () => { + expect(vm.$el.nodeName).toBe('BUTTON'); + }); + + it('renders component container element with required data attributes', () => { + expect(vm.$el.dataset.abilityName).toBe(vm.abilityName); + expect(vm.$el.dataset.fieldName).toBe(vm.fieldName); + expect(vm.$el.dataset.issueUpdate).toBe(vm.updatePath); + expect(vm.$el.dataset.labels).toBe(vm.labelsPath); + expect(vm.$el.dataset.namespacePath).toBe(vm.namespace); + expect(vm.$el.dataset.showAny).not.toBeDefined(); + }); + + it('renders dropdown toggle text element', () => { + const dropdownToggleTextEl = vm.$el.querySelector('.dropdown-toggle-text'); + expect(dropdownToggleTextEl).not.toBeNull(); + expect(dropdownToggleTextEl.innerText.trim()).toBe('Foo Label'); + }); + + it('renders dropdown button icon', () => { + const dropdownIconEl = vm.$el.querySelector('i.fa'); + expect(dropdownIconEl).not.toBeNull(); + expect(dropdownIconEl.classList.contains('fa-chevron-down')).toBe(true); + }); + }); +}); -- cgit v1.2.1 From 523093220beab3c2768a748542829242f6a22c06 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Mar 2018 18:08:13 +0530 Subject: LabelsSelect DropdownCreateLabel Component --- .../labels_select/dropdown_create_label.vue | 84 ++++++++++++++++++++++ .../labels_select/dropdown_create_label_spec.js | 84 ++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue create mode 100644 spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue new file mode 100644 index 00000000000..4200d1e8473 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue @@ -0,0 +1,84 @@ + + + diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js new file mode 100644 index 00000000000..f07aefb2f87 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js @@ -0,0 +1,84 @@ +import Vue from 'vue'; + +import dropdownCreateLabelComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue'; + +import { mockSuggestedColors } from './mock_data'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = () => { + const Component = Vue.extend(dropdownCreateLabelComponent); + + return mountComponent(Component); +}; + +describe('DropdownCreateLabelComponent', () => { + let vm; + + beforeEach(() => { + gon.suggested_label_colors = mockSuggestedColors; + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('created', () => { + it('initializes `suggestedColors` prop on component from `gon.suggested_color_labels` object', () => { + expect(vm.suggestedColors.length).toBe(mockSuggestedColors.length); + }); + }); + + describe('template', () => { + it('renders component container element with classes `dropdown-page-two dropdown-new-label`', () => { + expect(vm.$el.classList.contains('dropdown-page-two', 'dropdown-new-label')).toBe(true); + }); + + it('renders `Go back` button on component header', () => { + const backButtonEl = vm.$el.querySelector('.dropdown-title button.dropdown-title-button.dropdown-menu-back'); + expect(backButtonEl).not.toBe(null); + expect(backButtonEl.querySelector('.fa-arrow-left')).not.toBe(null); + }); + + it('renders component header element', () => { + const headerEl = vm.$el.querySelector('.dropdown-title'); + expect(headerEl.innerText.trim()).toContain('Create new label'); + }); + + it('renders `Close` button on component header', () => { + const closeButtonEl = vm.$el.querySelector('.dropdown-title button.dropdown-title-button.dropdown-menu-close'); + expect(closeButtonEl).not.toBe(null); + expect(closeButtonEl.querySelector('.fa-times.dropdown-menu-close-icon')).not.toBe(null); + }); + + it('renders `Name new label` input element', () => { + expect(vm.$el.querySelector('.dropdown-labels-error.js-label-error')).not.toBe(null); + expect(vm.$el.querySelector('input#new_label_name.default-dropdown-input')).not.toBe(null); + }); + + it('renders suggested colors list elements', () => { + const colorsListContainerEl = vm.$el.querySelector('.suggest-colors.suggest-colors-dropdown'); + expect(colorsListContainerEl).not.toBe(null); + expect(colorsListContainerEl.querySelectorAll('a').length).toBe(mockSuggestedColors.length); + + const colorItemEl = colorsListContainerEl.querySelectorAll('a')[0]; + expect(colorItemEl.dataset.color).toBe(vm.suggestedColors[0]); + expect(colorItemEl.getAttribute('style')).toBe('background-color: rgb(0, 51, 204);'); + }); + + it('renders color input element', () => { + expect(vm.$el.querySelector('.dropdown-label-color-input')).not.toBe(null); + expect(vm.$el.querySelector('.dropdown-label-color-preview.js-dropdown-label-color-preview')).not.toBe(null); + expect(vm.$el.querySelector('input#new_label_color.default-dropdown-input')).not.toBe(null); + }); + + it('renders component action buttons', () => { + const createBtnEl = vm.$el.querySelector('button.js-new-label-btn'); + const cancelBtnEl = vm.$el.querySelector('button.js-cancel-label-btn'); + expect(createBtnEl).not.toBe(null); + expect(createBtnEl.innerText.trim()).toBe('Create'); + expect(cancelBtnEl.innerText.trim()).toBe('Cancel'); + }); + }); +}); -- cgit v1.2.1 From ab1bc5c7f76bcb893004749918a2913c2c965a1b Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Mar 2018 18:08:44 +0530 Subject: LabelsSelect DropdownFooter Component --- .../sidebar/labels_select/dropdown_footer.vue | 34 ++++++++++++++++++ .../sidebar/labels_select/dropdown_footer_spec.js | 42 ++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue create mode 100644 spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue new file mode 100644 index 00000000000..e951a863811 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue @@ -0,0 +1,34 @@ + + + diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js new file mode 100644 index 00000000000..809e0327b1c --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js @@ -0,0 +1,42 @@ +import Vue from 'vue'; + +import dropdownFooterComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_footer.vue'; + +import { mockConfig } from './mock_data'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = (labelsWebUrl = mockConfig.labelsWebUrl) => { + const Component = Vue.extend(dropdownFooterComponent); + + return mountComponent(Component, { + labelsWebUrl, + }); +}; + +describe('DropdownFooterComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('template', () => { + it('renders `Create new label` link element', () => { + const createLabelEl = vm.$el.querySelector('.dropdown-footer-list .dropdown-toggle-page'); + expect(createLabelEl).not.toBeNull(); + expect(createLabelEl.innerText.trim()).toBe('Create new label'); + }); + + it('renders `Manage labels` link element', () => { + const manageLabelsEl = vm.$el.querySelector('.dropdown-footer-list .dropdown-external-link'); + expect(manageLabelsEl).not.toBeNull(); + expect(manageLabelsEl.getAttribute('href')).toBe(vm.labelsWebUrl); + expect(manageLabelsEl.innerText.trim()).toBe('Manage labels'); + }); + }); +}); -- cgit v1.2.1 From 5989aa18f4ec49973e4d42015c1af8c93d3aed1f Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Mar 2018 18:08:56 +0530 Subject: LabelsSelect DropdownHeader Component --- .../sidebar/labels_select/dropdown_header.vue | 21 +++++++++++++ .../sidebar/labels_select/dropdown_header_spec.js | 36 ++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue create mode 100644 spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue new file mode 100644 index 00000000000..7664acdf19c --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue @@ -0,0 +1,21 @@ + + + diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js new file mode 100644 index 00000000000..325fa47c957 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js @@ -0,0 +1,36 @@ +import Vue from 'vue'; + +import dropdownHeaderComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_header.vue'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = () => { + const Component = Vue.extend(dropdownHeaderComponent); + + return mountComponent(Component); +}; + +describe('DropdownHeaderComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('template', () => { + it('renders header text element', () => { + const headerEl = vm.$el.querySelector('.dropdown-title span'); + expect(headerEl.innerText.trim()).toBe('Assign labels'); + }); + + it('renders `Close` button element', () => { + const closeBtnEl = vm.$el.querySelector('.dropdown-title button.dropdown-title-button.dropdown-menu-close'); + expect(closeBtnEl).not.toBeNull(); + expect(closeBtnEl.querySelector('.fa-times.dropdown-menu-close-icon')).not.toBeNull(); + }); + }); +}); -- cgit v1.2.1 From b4bd9777fb519a537c011d87651d29683587893e Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Mar 2018 18:09:12 +0530 Subject: LabelsSelect DropdownHiddenInput Component --- .../labels_select/dropdown_hidden_input.vue | 22 +++++++++++++ .../labels_select/dropdown_hidden_input_spec.js | 37 ++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue create mode 100644 spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue new file mode 100644 index 00000000000..1832c3c1757 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue @@ -0,0 +1,22 @@ + + + diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js new file mode 100644 index 00000000000..703b87498c7 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js @@ -0,0 +1,37 @@ +import Vue from 'vue'; + +import dropdownHiddenInputComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue'; + +import { mockLabels } from './mock_data'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = (name = 'label_id[]', label = mockLabels[0]) => { + const Component = Vue.extend(dropdownHiddenInputComponent); + + return mountComponent(Component, { + name, + label, + }); +}; + +describe('DropdownHiddenInputComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('template', () => { + it('renders input element of type `hidden`', () => { + expect(vm.$el.nodeName).toBe('INPUT'); + expect(vm.$el.getAttribute('type')).toBe('hidden'); + expect(vm.$el.getAttribute('name')).toBe(vm.name); + expect(vm.$el.getAttribute('value')).toBe(`${vm.label.id}`); + }); + }); +}); -- cgit v1.2.1 From 2e5497f0057021daa57d3e8958c840f6b8a82098 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Mar 2018 18:09:27 +0530 Subject: LabelsSelect DropdownSearchInput Component --- .../labels_select/dropdown_search_input.vue | 27 +++++++++++++++ .../labels_select/dropdown_search_input_spec.js | 39 ++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue create mode 100644 spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue new file mode 100644 index 00000000000..ae633460c95 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue @@ -0,0 +1,27 @@ + + + diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js new file mode 100644 index 00000000000..69e11d966c2 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js @@ -0,0 +1,39 @@ +import Vue from 'vue'; + +import dropdownSearchInputComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = () => { + const Component = Vue.extend(dropdownSearchInputComponent); + + return mountComponent(Component); +}; + +describe('DropdownSearchInputComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('template', () => { + it('renders input element with type `search`', () => { + const inputEl = vm.$el.querySelector('input.dropdown-input-field'); + expect(inputEl).not.toBeNull(); + expect(inputEl.getAttribute('type')).toBe('search'); + }); + + it('renders search icon element', () => { + expect(vm.$el.querySelector('.fa-search.dropdown-input-search')).not.toBeNull(); + }); + + it('renders clear search icon element', () => { + expect(vm.$el.querySelector('.fa-times.dropdown-input-clear.js-dropdown-input-clear')).not.toBeNull(); + }); + }); +}); -- cgit v1.2.1 From 8b44ad6e6ab5408dca800b9cdb6a29335704109a Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Mar 2018 18:09:43 +0530 Subject: LabelsSelect DropdownTitle Component --- .../sidebar/labels_select/dropdown_title.vue | 30 ++++++++++++++++ .../sidebar/labels_select/dropdown_title_spec.js | 42 ++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue create mode 100644 spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue new file mode 100644 index 00000000000..7da82e90e29 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue @@ -0,0 +1,30 @@ + + + diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js new file mode 100644 index 00000000000..c3580933072 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js @@ -0,0 +1,42 @@ +import Vue from 'vue'; + +import dropdownTitleComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_title.vue'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = (canEdit = true) => { + const Component = Vue.extend(dropdownTitleComponent); + + return mountComponent(Component, { + canEdit, + }); +}; + +describe('DropdownTitleComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('template', () => { + it('renders title text', () => { + expect(vm.$el.classList.contains('title', 'hide-collapsed')).toBe(true); + expect(vm.$el.innerText.trim()).toContain('Labels'); + }); + + it('renders spinner icon element', () => { + expect(vm.$el.querySelector('.fa-spinner.fa-spin.block-loading')).not.toBeNull(); + }); + + it('renders `Edit` button element', () => { + const editBtnEl = vm.$el.querySelector('button.edit-link.js-sidebar-dropdown-toggle'); + expect(editBtnEl).not.toBeNull(); + expect(editBtnEl.innerText.trim()).toBe('Edit'); + }); + }); +}); -- cgit v1.2.1 From c1ed7f3f961348ca9897beadc75ea9cf2494f344 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Mar 2018 18:09:55 +0530 Subject: LabelsSelect DropdownValue Component --- .../sidebar/labels_select/dropdown_value.vue | 63 +++++++++++++++ .../sidebar/labels_select/dropdown_value_spec.js | 94 ++++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue create mode 100644 spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue new file mode 100644 index 00000000000..ba4c8fba5ec --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue @@ -0,0 +1,63 @@ + + + diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js new file mode 100644 index 00000000000..66e0957b431 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js @@ -0,0 +1,94 @@ +import Vue from 'vue'; + +import dropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue'; + +import { mockConfig, mockLabels } from './mock_data'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = ( + labels = mockLabels, + labelFilterBasePath = mockConfig.labelFilterBasePath, +) => { + const Component = Vue.extend(dropdownValueComponent); + + return mountComponent(Component, { + labels, + labelFilterBasePath, + }); +}; + +describe('DropdownValueComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('isEmpty', () => { + it('returns true if `labels` prop is empty', () => { + const vmEmptyLabels = createComponent([]); + expect(vmEmptyLabels.isEmpty).toBe(true); + vmEmptyLabels.$destroy(); + }); + + it('returns false if `labels` prop is empty', () => { + expect(vm.isEmpty).toBe(false); + }); + }); + }); + + describe('methods', () => { + describe('labelFilterUrl', () => { + it('returns URL string starting with labelFilterBasePath and encoded label.title', () => { + expect(vm.labelFilterUrl({ + title: 'Foo bar', + })).toBe('/gitlab-org/my-project/issues?label_name[]=Foo%20bar'); + }); + }); + + describe('labelStyle', () => { + it('returns object with `color` & `backgroundColor` properties from label.textColor & label.color', () => { + const label = { + textColor: '#FFFFFF', + color: '#BADA55', + }; + const styleObj = vm.labelStyle(label); + + expect(styleObj.color).toBe(label.textColor); + expect(styleObj.backgroundColor).toBe(label.color); + }); + }); + }); + + describe('template', () => { + it('renders component container element with classes `hide-collapsed value issuable-show-labels`', () => { + expect(vm.$el.classList.contains('hide-collapsed', 'value', 'issuable-show-labels')).toBe(true); + }); + + it('render slot content inside component when `labels` prop is empty', () => { + const vmEmptyLabels = createComponent([]); + expect(vmEmptyLabels.$el.querySelector('.text-secondary').innerText.trim()).toBe(mockConfig.emptyValueText); + vmEmptyLabels.$destroy(); + }); + + it('renders label element with filter URL', () => { + expect(vm.$el.querySelector('a').getAttribute('href')).toBe('/gitlab-org/my-project/issues?label_name[]=Foo%20Label'); + }); + + it('renders label element with tooltip and styles based on label details', () => { + const labelEl = vm.$el.querySelector('a span.label.color-label'); + expect(labelEl).not.toBeNull(); + expect(labelEl.dataset.placement).toBe('bottom'); + expect(labelEl.dataset.container).toBe('body'); + expect(labelEl.dataset.originalTitle).toBe(mockLabels[0].description); + expect(labelEl.getAttribute('style')).toBe('background-color: rgb(186, 218, 85);'); + expect(labelEl.innerText.trim()).toBe(mockLabels[0].title); + }); + }); +}); -- cgit v1.2.1 From 00db4cb733e5c1314d63972a280108a1d81c7301 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Mar 2018 18:10:13 +0530 Subject: LabelsSelect DropdownValueCollapsed Component --- .../labels_select/dropdown_value_collapsed.vue | 48 ++++++++++++++ .../labels_select/dropdown_value_collapsed_spec.js | 74 ++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue create mode 100644 spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue new file mode 100644 index 00000000000..5cf728fe050 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue @@ -0,0 +1,48 @@ + + + diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js new file mode 100644 index 00000000000..93b42795bea --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js @@ -0,0 +1,74 @@ +import Vue from 'vue'; + +import dropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue'; + +import { mockLabels } from './mock_data'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = (labels = mockLabels) => { + const Component = Vue.extend(dropdownValueCollapsedComponent); + + return mountComponent(Component, { + labels, + }); +}; + +describe('DropdownValueCollapsedComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('labelsList', () => { + it('returns empty text when `labels` prop is empty array', () => { + const vmEmptyLabels = createComponent([]); + expect(vmEmptyLabels.labelsList).toBe(''); + vmEmptyLabels.$destroy(); + }); + + it('returns labels names separated by coma when `labels` prop has more than one item', () => { + const vmMoreLabels = createComponent(mockLabels.concat(mockLabels)); + expect(vmMoreLabels.labelsList).toBe('Foo Label, Foo Label'); + vmMoreLabels.$destroy(); + }); + + it('returns labels names separated by coma with remaining labels count and `and more` phrase when `labels` prop has more than five items', () => { + const mockMoreLabels = Object.assign([], mockLabels); + for (let i = 0; i < 6; i += 1) { + mockMoreLabels.unshift(mockLabels[0]); + } + + const vmMoreLabels = createComponent(mockMoreLabels); + expect(vmMoreLabels.labelsList).toBe('Foo Label, Foo Label, Foo Label, Foo Label, Foo Label, and 2 more'); + vmMoreLabels.$destroy(); + }); + + it('returns first label name when `labels` prop has only one item present', () => { + expect(vm.labelsList).toBe('Foo Label'); + }); + }); + }); + + describe('template', () => { + it('renders component container element with tooltip`', () => { + expect(vm.$el.dataset.placement).toBe('left'); + expect(vm.$el.dataset.container).toBe('body'); + expect(vm.$el.dataset.originalTitle).toBe(vm.labelsList); + }); + + it('renders tags icon element', () => { + expect(vm.$el.querySelector('.fa-tags')).not.toBeNull(); + }); + + it('renders labels count', () => { + expect(vm.$el.querySelector('span').innerText.trim()).toBe(`${vm.labels.length}`); + }); + }); +}); -- cgit v1.2.1 From 4051d01c9cbd867b2478743ffe1ad50dfb030f6d Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Mon, 5 Mar 2018 18:10:23 +0530 Subject: Add changelog entry --- changelogs/unreleased/kp-label-select-vue.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/kp-label-select-vue.yml diff --git a/changelogs/unreleased/kp-label-select-vue.yml b/changelogs/unreleased/kp-label-select-vue.yml new file mode 100644 index 00000000000..1f5952f2554 --- /dev/null +++ b/changelogs/unreleased/kp-label-select-vue.yml @@ -0,0 +1,5 @@ +--- +title: Port Labels Select dropdown to Vue +merge_request: 17411 +author: +type: other -- cgit v1.2.1