diff options
author | Rajat Jain <rjain@gitlab.com> | 2019-04-15 09:58:30 +0000 |
---|---|---|
committer | Kushal Pandya <kushalspandya@gmail.com> | 2019-04-15 09:58:30 +0000 |
commit | b5ab1d91e377787e0711effebce073af76becc56 (patch) | |
tree | 3282fbee428f5948302eb622466f5527b01c9117 | |
parent | d83eb63beef28a6229b4bf851ee34c51938e29c7 (diff) | |
download | gitlab-ce-b5ab1d91e377787e0711effebce073af76becc56.tar.gz |
Display scoped labels in Issue Boards
This change brings new Scoped labels to Issue board as well.
With the last change, this was missed.
13 files changed, 181 insertions, 24 deletions
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index 3c683e88cf3..915d1676e62 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -16,6 +16,7 @@ import TimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue'; import MilestoneSelect from '~/milestone_select'; import RemoveBtn from './sidebar/remove_issue.vue'; import boardsStore from '../stores/boards_store'; +import { isScopedLabel } from '~/lib/utils/common_utils'; export default Vue.extend({ components: { @@ -140,5 +141,11 @@ export default Vue.extend({ Flash(__('An error occurred while saving assignees')); }); }, + showScopedLabels(label) { + return boardsStore.scopedLabels.enabled && isScopedLabel(label); + }, + helpLink() { + return boardsStore.scopedLabels.helpLink; + }, }, }); diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue index 206573dd444..be0de63e772 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.vue +++ b/app/assets/javascripts/boards/components/issue_card_inner.vue @@ -9,6 +9,8 @@ import eventHub from '../eventhub'; import IssueDueDate from './issue_due_date.vue'; import IssueTimeEstimate from './issue_time_estimate.vue'; import boardsStore from '../stores/boards_store'; +import IssueCardInnerScopedLabel from './issue_card_inner_scoped_label.vue'; +import { isScopedLabel } from '~/lib/utils/common_utils'; export default { components: { @@ -17,6 +19,7 @@ export default { TooltipOnTruncate, IssueDueDate, IssueTimeEstimate, + IssueCardInnerScopedLabel, }, directives: { GlTooltip: GlTooltipDirective, @@ -96,6 +99,9 @@ export default { orderedLabels() { return _.sortBy(this.issue.labels, 'title'); }, + helpLink() { + return boardsStore.scopedLabels.helpLink; + }, }, methods: { isIndexLessThanlimit(index) { @@ -159,6 +165,9 @@ export default { color: label.textColor, }; }, + showScopedLabel(label) { + return boardsStore.scopedLabels.enabled && isScopedLabel(label); + }, }, }; </script> @@ -179,19 +188,29 @@ export default { </h4> </div> <div v-if="showLabelFooter" class="board-card-labels prepend-top-4 d-flex flex-wrap"> - <button - v-for="label in orderedLabels" - v-if="showLabel(label)" - :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)" - > - {{ label.title }} - </button> + <template v-for="label in orderedLabels" v-if="showLabel(label)"> + <issue-card-inner-scoped-label + v-if="showScopedLabel(label)" + :key="label.id" + :label="label" + :label-style="labelStyle(label)" + :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)" + > + {{ label.title }} + </button> + </template> </div> <div class="board-card-footer d-flex justify-content-between align-items-end"> <div diff --git a/app/assets/javascripts/boards/components/issue_card_inner_scoped_label.vue b/app/assets/javascripts/boards/components/issue_card_inner_scoped_label.vue new file mode 100644 index 00000000000..fa4c68964cb --- /dev/null +++ b/app/assets/javascripts/boards/components/issue_card_inner_scoped_label.vue @@ -0,0 +1,45 @@ +<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> diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js index b4d913f5d69..f8ff20cb0cd 100644 --- a/app/assets/javascripts/boards/models/issue.js +++ b/app/assets/javascripts/boards/models/issue.js @@ -5,7 +5,7 @@ import Vue from 'vue'; import '~/vue_shared/models/label'; -import { isEE } from '~/lib/utils/common_utils'; +import { isEE, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import IssueProject from './project'; import boardsStore from '../stores/boards_store'; @@ -141,7 +141,7 @@ class ListIssue { * PATCH the said object. */ if (body) { - this.labels = body.labels; + this.labels = convertObjectPropsToCamelCase(body.labels, { deep: true }); } }); } diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 802796208c2..6b1e37d3f24 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -9,6 +9,10 @@ import { getUrlParamsArray, parseBoolean } from '~/lib/utils/common_utils'; const boardsStore = { disabled: false, + scopedLabels: { + helpLink: '', + enabled: false, + }, filter: { path: '', }, diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 7d21a216443..2c30b4ea587 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -11,7 +11,7 @@ import CreateLabelDropdown from './create_label'; import flash from './flash'; import ModalStore from './boards/stores/modal_store'; import boardsStore from './boards/stores/boards_store'; -import { isEE } from '~/lib/utils/common_utils'; +import { isEE, isScopedLabel } from '~/lib/utils/common_utils'; export default class LabelsSelect { constructor(els, options = {}) { @@ -546,8 +546,6 @@ export default class LabelsSelect { ].join(''), ); - const isScopedLabel = label => label.title.indexOf('::') !== -1; - const tpl = _.template( [ '<% _.each(labels, function(label){ %>', diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 2906604da57..b236daff1e0 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -724,6 +724,18 @@ export const NavigationType = { */ export const isEE = () => window.gon && window.gon.ee; +/** + * Checks if the given Label has a special syntax `::` in + * it's title. + * + * Expected Label to be an Object with `title` as a key: + * { title: 'LabelTitle', ...otherProperties }; + * + * @param {Object} label + * @returns Boolean + */ +export const isScopedLabel = ({ title = '' }) => title.indexOf('::') !== -1; + window.gl = window.gl || {}; window.gl.utils = { ...(window.gl.utils || {}), 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 index ddc488adbcb..4abf7c478ee 100644 --- 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 @@ -1,6 +1,7 @@ <script> import DropdownValueScopedLabel from './dropdown_value_scoped_label.vue'; import DropdownValueRegularLabel from './dropdown_value_regular_label.vue'; +import { isScopedLabel } from '~/lib/utils/common_utils'; export default { components: { @@ -45,8 +46,8 @@ export default { scopedLabelsDescription({ description = '' }) { return `<span class="font-weight-bold scoped-label-tooltip-title">Scoped label</span><br />${description}`; }, - showScopedLabels({ title = '' }) { - return this.enableScopedLabels && title.indexOf('::') !== -1; + showScopedLabels(label) { + return this.enableScopedLabels && isScopedLabel(label); }, }, }; diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index e7fd7fab32b..7084e8715e0 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -424,6 +424,12 @@ margin: 0; line-height: $gl-line-height; } + + &.board-label { + .scoped-label { + top: 1px; + } + } } // Label inside title of Delete Label Modal diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml index 47cc912a9a1..311dc69d213 100644 --- a/app/views/shared/boards/components/sidebar/_labels.html.haml +++ b/app/views/shared/boards/components/sidebar/_labels.html.haml @@ -7,10 +7,17 @@ .value.issuable-show-labels.dont-hide %span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" } = _("None") - %a{ href: "#", - "v-for" => "label in issue.labels" } - .badge.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" } - {{ label.title }} + %span{ "v-for" => "label in issue.labels" } + %span.d-inline-block.position-relative.scoped-label-wrapper{ "v-if" => "showScopedLabels(label)" } + %a{ href: '#' } + %span.badge.color-label.label{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" } + {{ label.title }} + %a.label.scoped-label{ ":href" => "helpLink()" } + %i.fa.fa-question-circle{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" } + %a{ href: "#", "v-else" => true } + .badge.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" } + {{ label.title }} + - if can_admin_issue? .selectbox %input{ type: "hidden", diff --git a/changelogs/unreleased/10921-display-scoped-labels-ce.yml b/changelogs/unreleased/10921-display-scoped-labels-ce.yml new file mode 100644 index 00000000000..7a0e7fec41b --- /dev/null +++ b/changelogs/unreleased/10921-display-scoped-labels-ce.yml @@ -0,0 +1,5 @@ +--- +title: Display scoped labels in Issue Boards +merge_request: 27164 +author: +type: fixed diff --git a/spec/javascripts/boards/components/issue_card_inner_scoped_label_spec.js b/spec/javascripts/boards/components/issue_card_inner_scoped_label_spec.js new file mode 100644 index 00000000000..c62c5b9962d --- /dev/null +++ b/spec/javascripts/boards/components/issue_card_inner_scoped_label_spec.js @@ -0,0 +1,43 @@ +import Vue from 'vue'; +import IssueCardInnerScopedLabel from '~/boards/components/issue_card_inner_scoped_label.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; + +describe('IssueCardInnerScopedLabel Component', () => { + let vm; + const Component = Vue.extend(IssueCardInnerScopedLabel); + const props = { + label: { title: 'Foo::Bar', description: 'Some Random Description' }, + labelStyle: { background: 'white', color: 'black' }, + scopedLabelsDocumentationLink: '/docs-link', + }; + const createComponent = () => mountComponent(Component, { ...props }); + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render label title', () => { + expect(vm.$el.querySelector('.color-label').textContent.trim()).toEqual('Foo::Bar'); + }); + + it('should render question mark symbol', () => { + expect(vm.$el.querySelector('.fa-question-circle')).not.toBeNull(); + }); + + it('should render label style provided', () => { + const node = vm.$el.querySelector('.color-label'); + + expect(node.style.background).toEqual(props.labelStyle.background); + expect(node.style.color).toEqual(props.labelStyle.color); + }); + + it('should render the docs link', () => { + expect(vm.$el.querySelector('a.scoped-label').href).toContain( + props.scopedLabelsDocumentationLink, + ); + }); +}); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index da012e1d5f7..0cd077a6099 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -894,4 +894,14 @@ describe('common_utils', () => { expect(commonUtils.isInViewport(el)).toBe(false); }); }); + + describe('isScopedLabel', () => { + it('returns true when `::` is present in title', () => { + expect(commonUtils.isScopedLabel({ title: 'foo::bar' })).toBe(true); + }); + + it('returns false when `::` is not present', () => { + expect(commonUtils.isScopedLabel({ title: 'foobar' })).toBe(false); + }); + }); }); |