summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKushal Pandya <kushalspandya@gmail.com>2019-04-15 09:58:31 +0000
committerKushal Pandya <kushalspandya@gmail.com>2019-04-15 09:58:31 +0000
commita5f13e591f617931434d66263418a2f26abe3abe (patch)
tree3282fbee428f5948302eb622466f5527b01c9117
parentd83eb63beef28a6229b4bf851ee34c51938e29c7 (diff)
parentb5ab1d91e377787e0711effebce073af76becc56 (diff)
downloadgitlab-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
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js7
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue45
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner_scoped_label.vue45
-rw-r--r--app/assets/javascripts/boards/models/issue.js4
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js4
-rw-r--r--app/assets/javascripts/labels_select.js4
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js12
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue5
-rw-r--r--app/assets/stylesheets/pages/labels.scss6
-rw-r--r--app/views/shared/boards/components/sidebar/_labels.html.haml15
-rw-r--r--changelogs/unreleased/10921-display-scoped-labels-ce.yml5
-rw-r--r--spec/javascripts/boards/components/issue_card_inner_scoped_label_spec.js43
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js10
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);
+ });
+ });
});