Commit 23f894fc authored by Dylan Griffith's avatar Dylan Griffith

Merge branch 'ss/add-scoped-labels-to-issuables-list' into 'master'

Add new label from gitlab-ui to issuables list labels

See merge request gitlab-org/gitlab!34334
parents 69b34501 e5b15216
...@@ -3,8 +3,10 @@ ...@@ -3,8 +3,10 @@
* This is tightly coupled to projects/issues/_issue.html.haml, * This is tightly coupled to projects/issues/_issue.html.haml,
* any changes done to the haml need to be reflected here. * any changes done to the haml need to be reflected here.
*/ */
// TODO: need to move this component to graphql - https://gitlab.com/gitlab-org/gitlab/-/issues/221246
import { escape, isNumber } from 'lodash'; import { escape, isNumber } from 'lodash';
import { GlLink, GlTooltipDirective as GlTooltip } from '@gitlab/ui'; import { GlLink, GlTooltipDirective as GlTooltip, GlLabel } from '@gitlab/ui';
import { import {
dateInWords, dateInWords,
formatDate, formatDate,
...@@ -18,16 +20,21 @@ import initUserPopovers from '~/user_popovers'; ...@@ -18,16 +20,21 @@ import initUserPopovers from '~/user_popovers';
import { mergeUrlParams } from '~/lib/utils/url_utility'; import { mergeUrlParams } from '~/lib/utils/url_utility';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue'; import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
import { isScopedLabel } from '~/lib/utils/common_utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default { export default {
isScopedLabel,
components: { components: {
Icon, Icon,
IssueAssignees, IssueAssignees,
GlLink, GlLink,
GlLabel,
}, },
directives: { directives: {
GlTooltip, GlTooltip,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
issuable: { issuable: {
type: Object, type: Object,
...@@ -57,8 +64,8 @@ export default { ...@@ -57,8 +64,8 @@ export default {
return this.issuableLink({ milestone_title: title }); return this.issuableLink({ milestone_title: title });
}, },
hasLabels() { scopedLabelsAvailable() {
return Boolean(this.issuable.labels && this.issuable.labels.length); return this.glFeatures.scopedLabels;
}, },
hasWeight() { hasWeight() {
return isNumber(this.issuable.weight); return isNumber(this.issuable.weight);
...@@ -163,15 +170,12 @@ export default { ...@@ -163,15 +170,12 @@ export default {
initUserPopovers([this.$refs.openedAgoByContainer.querySelector('a')]); initUserPopovers([this.$refs.openedAgoByContainer.querySelector('a')]);
}, },
methods: { methods: {
labelStyle(label) {
return {
backgroundColor: label.color,
color: label.text_color,
};
},
issuableLink(params) { issuableLink(params) {
return mergeUrlParams(params, this.baseUrl); return mergeUrlParams(params, this.baseUrl);
}, },
isScoped({ name }) {
return isScopedLabel({ title: name }) && this.scopedLabelsAvailable;
},
labelHref({ name }) { labelHref({ name }) {
return this.issuableLink({ 'label_name[]': name }); return this.issuableLink({ 'label_name[]': name });
}, },
...@@ -221,9 +225,9 @@ export default { ...@@ -221,9 +225,9 @@ export default {
></i> ></i>
<gl-link :href="issuable.web_url">{{ issuable.title }}</gl-link> <gl-link :href="issuable.web_url">{{ issuable.title }}</gl-link>
</span> </span>
<span v-if="issuable.has_tasks" class="ml-1 task-status d-none d-sm-inline-block">{{ <span v-if="issuable.has_tasks" class="ml-1 task-status d-none d-sm-inline-block">
issuable.task_status {{ issuable.task_status }}
}}</span> </span>
</div> </div>
<div class="issuable-info"> <div class="issuable-info">
...@@ -256,22 +260,19 @@ export default { ...@@ -256,22 +260,19 @@ export default {
{{ dueDateWords }} {{ dueDateWords }}
</span> </span>
<span v-if="hasLabels" class="js-labels"> <gl-label
<gl-link v-for="label in issuable.labels"
v-for="label in issuable.labels" :key="label.id"
:key="label.id" :target="labelHref(label)"
class="label-link mr-1" :background-color="label.color"
:href="labelHref(label)" :description="label.description"
> :color="label.text_color"
<span :title="label.name"
v-gl-tooltip :scoped="isScoped(label)"
class="badge color-label" size="sm"
:style="labelStyle(label)" class="mr-1"
:title="label.description" >{{ label.name }}</gl-label
>{{ label.name }}</span >
>
</gl-link>
</span>
<span <span
v-if="hasWeight" v-if="hasWeight"
......
...@@ -9,6 +9,10 @@ module EE ...@@ -9,6 +9,10 @@ module EE
alias_method :ee_authorize_admin_group!, :authorize_admin_group! alias_method :ee_authorize_admin_group!, :authorize_admin_group!
before_action :ee_authorize_admin_group!, only: [:restore] before_action :ee_authorize_admin_group!, only: [:restore]
before_action only: :issues do
push_frontend_feature_flag(:scoped_labels, @group)
end
end end
override :render_show_html override :render_show_html
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui'; import { GlLabel } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { trimText } from 'helpers/text_helper'; import { trimText } from 'helpers/text_helper';
import initUserPopovers from '~/user_popovers'; import initUserPopovers from '~/user_popovers';
...@@ -8,6 +8,7 @@ import { mergeUrlParams } from '~/lib/utils/url_utility'; ...@@ -8,6 +8,7 @@ import { mergeUrlParams } from '~/lib/utils/url_utility';
import Issuable from '~/issuables_list/components/issuable.vue'; import Issuable from '~/issuables_list/components/issuable.vue';
import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue'; import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
import { simpleIssue, testAssignees, testLabels } from '../issuable_list_test_data'; import { simpleIssue, testAssignees, testLabels } from '../issuable_list_test_data';
import { isScopedLabel } from '~/lib/utils/common_utils';
jest.mock('~/user_popovers'); jest.mock('~/user_popovers');
...@@ -37,13 +38,18 @@ describe('Issuable component', () => { ...@@ -37,13 +38,18 @@ describe('Issuable component', () => {
let DateOrig; let DateOrig;
let wrapper; let wrapper;
const factory = (props = {}) => { const factory = (props = {}, scopedLabels = false) => {
wrapper = shallowMount(Issuable, { wrapper = shallowMount(Issuable, {
propsData: { propsData: {
issuable: simpleIssue, issuable: simpleIssue,
baseUrl: TEST_BASE_URL, baseUrl: TEST_BASE_URL,
...props, ...props,
}, },
provide: {
glFeatures: {
scopedLabels,
},
},
}); });
}; };
...@@ -53,6 +59,7 @@ describe('Issuable component', () => { ...@@ -53,6 +59,7 @@ describe('Issuable component', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
beforeAll(() => { beforeAll(() => {
...@@ -70,8 +77,7 @@ describe('Issuable component', () => { ...@@ -70,8 +77,7 @@ describe('Issuable component', () => {
const findMilestone = () => wrapper.find('.js-milestone'); const findMilestone = () => wrapper.find('.js-milestone');
const findMilestoneTooltip = () => findMilestone().attributes('title'); const findMilestoneTooltip = () => findMilestone().attributes('title');
const findDueDate = () => wrapper.find('.js-due-date'); const findDueDate = () => wrapper.find('.js-due-date');
const findLabelContainer = () => wrapper.find('.js-labels'); const findLabels = () => wrapper.findAll(GlLabel);
const findLabelLinks = () => findLabelContainer().findAll(GlLink);
const findWeight = () => wrapper.find('.js-weight'); const findWeight = () => wrapper.find('.js-weight');
const findAssignees = () => wrapper.find(IssueAssignees); const findAssignees = () => wrapper.find(IssueAssignees);
const findMergeRequestsCount = () => wrapper.find('.js-merge-requests'); const findMergeRequestsCount = () => wrapper.find('.js-merge-requests');
...@@ -79,6 +85,8 @@ describe('Issuable component', () => { ...@@ -79,6 +85,8 @@ describe('Issuable component', () => {
const findDownvotes = () => wrapper.find('.js-downvotes'); const findDownvotes = () => wrapper.find('.js-downvotes');
const findNotes = () => wrapper.find('.js-notes'); const findNotes = () => wrapper.find('.js-notes');
const findBulkCheckbox = () => wrapper.find('input.selected-issuable'); const findBulkCheckbox = () => wrapper.find('input.selected-issuable');
const findScopedLabels = () => findLabels().filter(w => isScopedLabel({ title: w.text() }));
const findUnscopedLabels = () => findLabels().filter(w => !isScopedLabel({ title: w.text() }));
describe('when mounted', () => { describe('when mounted', () => {
it('initializes user popovers', () => { it('initializes user popovers', () => {
...@@ -90,6 +98,54 @@ describe('Issuable component', () => { ...@@ -90,6 +98,54 @@ describe('Issuable component', () => {
}); });
}); });
describe('when scopedLabels feature is available', () => {
beforeEach(() => {
issuable.labels = [...testLabels];
factory({ issuable }, true);
});
describe('when label is scoped', () => {
it('returns label with correct props', () => {
const scopedLabel = findScopedLabels().at(0);
expect(scopedLabel.props('scoped')).toBe(true);
});
});
describe('when label is not scoped', () => {
it('returns label with correct props', () => {
const notScopedLabel = findUnscopedLabels().at(0);
expect(notScopedLabel.props('scoped')).toBe(false);
});
});
});
describe('when scopedLabels feature is not available', () => {
beforeEach(() => {
issuable.labels = [...testLabels];
factory({ issuable });
});
describe('when label is scoped', () => {
it('label scoped props is false', () => {
const scopedLabel = findScopedLabels().at(0);
expect(scopedLabel.props('scoped')).toBe(false);
});
});
describe('when label is not scoped', () => {
it('label scoped props is false', () => {
const notScopedLabel = findUnscopedLabels().at(0);
expect(notScopedLabel.props('scoped')).toBe(false);
});
});
});
describe('with simple issuable', () => { describe('with simple issuable', () => {
beforeEach(() => { beforeEach(() => {
Object.assign(issuable, { Object.assign(issuable, {
...@@ -113,7 +169,7 @@ describe('Issuable component', () => { ...@@ -113,7 +169,7 @@ describe('Issuable component', () => {
${'task status'} | ${findTaskStatus} ${'task status'} | ${findTaskStatus}
${'milestone'} | ${findMilestone} ${'milestone'} | ${findMilestone}
${'due date'} | ${findDueDate} ${'due date'} | ${findDueDate}
${'labels'} | ${findLabelContainer} ${'labels'} | ${findLabels}
${'weight'} | ${findWeight} ${'weight'} | ${findWeight}
${'merge request count'} | ${findMergeRequestsCount} ${'merge request count'} | ${findMergeRequestsCount}
${'upvotes'} | ${findUpvotes} ${'upvotes'} | ${findUpvotes}
...@@ -239,10 +295,10 @@ describe('Issuable component', () => { ...@@ -239,10 +295,10 @@ describe('Issuable component', () => {
it('renders labels', () => { it('renders labels', () => {
factory({ issuable }); factory({ issuable });
const labels = findLabelLinks().wrappers.map(label => ({ const labels = findLabels().wrappers.map(label => ({
href: label.attributes('href'), href: label.props('target'),
text: label.text(), text: label.text(),
tooltip: label.find('span').attributes('title'), tooltip: label.attributes('description'),
})); }));
const expected = testLabels.map(label => ({ const expected = testLabels.map(label => ({
......
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