diff options
author | Kushal Pandya <kushalspandya@gmail.com> | 2019-04-15 09:58:31 +0000 |
---|---|---|
committer | Kushal Pandya <kushalspandya@gmail.com> | 2019-04-15 09:58:31 +0000 |
commit | a5f13e591f617931434d66263418a2f26abe3abe (patch) | |
tree | 3282fbee428f5948302eb622466f5527b01c9117 | |
parent | d83eb63beef28a6229b4bf851ee34c51938e29c7 (diff) | |
parent | b5ab1d91e377787e0711effebce073af76becc56 (diff) | |
download | gitlab-ce-a5f13e591f617931434d66263418a2f26abe3abe.tar.gz |
Merge branch '10921-display-scoped-labels-ce' into 'master'
Display scoped labels in Issue Boards
See merge request gitlab-org/gitlab-ce!27164
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); + }); + }); }); |