Commit 9de4b5fb authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '197932-add-package-tags-display-to-package-list-page' into 'master'

Resolve "Add package tags display to package list page"

Closes #197932

See merge request gitlab-org/gitlab!23675
parents 047bab74 d01c9d24
---
title: Displays package tags next to the name on the new package list page
merge_request: 23675
author:
type: added
...@@ -15,7 +15,7 @@ import PackageInformation from './information.vue'; ...@@ -15,7 +15,7 @@ import PackageInformation from './information.vue';
import NpmInstallation from './npm_installation.vue'; import NpmInstallation from './npm_installation.vue';
import MavenInstallation from './maven_installation.vue'; import MavenInstallation from './maven_installation.vue';
import ConanInstallation from './conan_installation.vue'; import ConanInstallation from './conan_installation.vue';
import PackageTags from './package_tags.vue'; import PackageTags from '../../shared/components/package_tags.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils'; import { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import { generatePackageInfo } from '../utils'; import { generatePackageInfo } from '../utils';
......
...@@ -21,6 +21,7 @@ import { ...@@ -21,6 +21,7 @@ import {
} from '../constants'; } from '../constants';
import { TrackingActions } from '../../shared/constants'; import { TrackingActions } from '../../shared/constants';
import { packageTypeToTrackCategory } from '../../shared/utils'; import { packageTypeToTrackCategory } from '../../shared/utils';
import PackageTags from '../../shared/components/package_tags.vue';
export default { export default {
components: { components: {
...@@ -32,6 +33,7 @@ export default { ...@@ -32,6 +33,7 @@ export default {
TimeAgoTooltip, TimeAgoTooltip,
GlModal, GlModal,
Icon, Icon,
PackageTags,
}, },
mixins: [Tracking.mixin()], mixins: [Tracking.mixin()],
data() { data() {
...@@ -191,14 +193,19 @@ export default { ...@@ -191,14 +193,19 @@ export default {
stacked="md" stacked="md"
> >
<template #cell(name)="{value, item}"> <template #cell(name)="{value, item}">
<div ref="col-name" class="flex-truncate-parent"> <div
<a class="flex-truncate-parent d-flex align-items-center justify-content-end justify-content-md-start"
:href="item._links.web_path" >
class="flex-truncate-child" <a :href="item._links.web_path" data-qa-selector="package_link">
data-qa-selector="package_link"
>
{{ value }} {{ value }}
</a> </a>
<package-tags
v-if="item.tags && item.tags.length"
class="prepend-left-8"
:tags="item.tags"
hide-label
:tag-display-limit="1"
/>
</div> </div>
</template> </template>
......
<script> <script>
import { GlBadge, GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui'; import { GlBadge, GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { n__ } from '~/locale';
export default { export default {
name: 'PackageTags', name: 'PackageTags',
...@@ -22,6 +23,11 @@ export default { ...@@ -22,6 +23,11 @@ export default {
required: true, required: true,
default: () => [], default: () => [],
}, },
hideLabel: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
tagCount() { tagCount() {
...@@ -43,19 +49,35 @@ export default { ...@@ -43,19 +49,35 @@ export default {
return ''; return '';
}, },
tagsDisplay() {
return n__('%d tag', '%d tags', this.tagCount);
},
},
methods: {
tagBadgeClass(index) {
return {
'd-none': true,
'd-block': this.tagCount === 1,
'd-md-block': this.tagCount > 1,
'append-right-4': index !== this.tagsToRender.length - 1,
};
},
}, },
}; };
</script> </script>
<template> <template>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<gl-icon name="labels" class="append-right-8" /> <div v-if="!hideLabel" ref="tagLabel" class="d-flex align-items-center">
<strong class="append-right-8 js-tags-count">{{ n__('%d tag', '%d tags', tagCount) }}</strong> <gl-icon name="labels" class="append-right-8" />
<strong class="append-right-8 js-tags-count">{{ tagsDisplay }}</strong>
</div>
<gl-badge <gl-badge
v-for="(tag, index) in tagsToRender" v-for="(tag, index) in tagsToRender"
:key="index" :key="index"
ref="tagBadge" ref="tagBadge"
class="append-right-4" :class="tagBadgeClass(index)"
variant="info" variant="info"
>{{ tag.name }}</gl-badge >{{ tag.name }}</gl-badge
> >
...@@ -66,11 +88,20 @@ export default { ...@@ -66,11 +88,20 @@ export default {
v-gl-tooltip v-gl-tooltip
variant="light" variant="light"
:title="moreTagsTooltip" :title="moreTagsTooltip"
class="d-none d-md-block prepend-left-4"
><gl-sprintf message="+%{tags} more"> ><gl-sprintf message="+%{tags} more">
<template #tags> <template #tags>
{{ moreTagsDisplay }} {{ moreTagsDisplay }}
</template> </template>
</gl-sprintf></gl-badge </gl-sprintf></gl-badge
> >
<gl-badge
v-if="moreTagsDisplay && hideLabel"
ref="moreBadge"
variant="light"
class="d-md-none prepend-left-4"
>{{ tagsDisplay }}</gl-badge
>
</div> </div>
</template> </template>
...@@ -916,6 +916,7 @@ module EE ...@@ -916,6 +916,7 @@ module EE
expose :project_id, if: ->(_, opts) { opts[:group] } expose :project_id, if: ->(_, opts) { opts[:group] }
expose :project_path, if: ->(obj, opts) { opts[:group] && Ability.allowed?(opts[:user], :read_project, obj.project) } expose :project_path, if: ->(obj, opts) { opts[:group] && Ability.allowed?(opts[:user], :read_project, obj.project) }
expose :build_info, using: BuildInfo expose :build_info, using: BuildInfo
expose :tags
private private
......
...@@ -6,7 +6,7 @@ import PackagesApp from 'ee/packages/details/components/app.vue'; ...@@ -6,7 +6,7 @@ import PackagesApp from 'ee/packages/details/components/app.vue';
import PackageInformation from 'ee/packages/details/components/information.vue'; import PackageInformation from 'ee/packages/details/components/information.vue';
import NpmInstallation from 'ee/packages/details/components/npm_installation.vue'; import NpmInstallation from 'ee/packages/details/components/npm_installation.vue';
import MavenInstallation from 'ee/packages/details/components/maven_installation.vue'; import MavenInstallation from 'ee/packages/details/components/maven_installation.vue';
import PackageTags from 'ee/packages/details/components/package_tags.vue'; import PackageTags from 'ee/packages/shared/components/package_tags.vue';
import * as SharedUtils from 'ee/packages/shared/utils'; import * as SharedUtils from 'ee/packages/shared/utils';
import { TrackingActions } from 'ee/packages/shared/constants'; import { TrackingActions } from 'ee/packages/shared/constants';
import ConanInstallation from 'ee/packages/details/components/conan_installation.vue'; import ConanInstallation from 'ee/packages/details/components/conan_installation.vue';
......
...@@ -3,6 +3,7 @@ import _ from 'underscore'; ...@@ -3,6 +3,7 @@ import _ from 'underscore';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import PackagesList from 'ee/packages/list/components/packages_list.vue'; import PackagesList from 'ee/packages/list/components/packages_list.vue';
import PackageTags from 'ee/packages/shared/components/package_tags.vue';
import * as SharedUtils from 'ee/packages/shared/utils'; import * as SharedUtils from 'ee/packages/shared/utils';
import { TrackingActions } from 'ee/packages/shared/constants'; import { TrackingActions } from 'ee/packages/shared/constants';
import stubChildren from 'helpers/stub_children'; import stubChildren from 'helpers/stub_children';
...@@ -18,6 +19,7 @@ describe('packages_list', () => { ...@@ -18,6 +19,7 @@ describe('packages_list', () => {
const findPackageListDeleteModal = () => wrapper.find({ ref: 'packageListDeleteModal' }); const findPackageListDeleteModal = () => wrapper.find({ ref: 'packageListDeleteModal' });
const findSortingItems = () => wrapper.findAll({ name: 'sorting-item-stub' }); const findSortingItems = () => wrapper.findAll({ name: 'sorting-item-stub' });
const findFirstProjectColumn = () => wrapper.find({ ref: 'col-project' }); const findFirstProjectColumn = () => wrapper.find({ ref: 'col-project' });
const findPackageTags = () => wrapper.findAll(PackageTags);
const mountOptions = { const mountOptions = {
stubs: { stubs: {
...@@ -83,11 +85,16 @@ describe('packages_list', () => { ...@@ -83,11 +85,16 @@ describe('packages_list', () => {
const sorting = findPackageListPagination(); const sorting = findPackageListPagination();
expect(sorting.exists()).toBe(true); expect(sorting.exists()).toBe(true);
}); });
it('contains a modal component', () => { it('contains a modal component', () => {
const sorting = findPackageListDeleteModal(); const sorting = findPackageListDeleteModal();
expect(sorting.exists()).toBe(true); expect(sorting.exists()).toBe(true);
}); });
it('renders package tags when a package has tags', () => {
expect(findPackageTags()).toHaveLength(1);
});
describe('when the user can destroy the package', () => { describe('when the user can destroy the package', () => {
it('show the action column', () => { it('show the action column', () => {
const action = findFirstActionColumn(); const action = findFirstActionColumn();
......
...@@ -77,8 +77,6 @@ export const conanPackage = { ...@@ -77,8 +77,6 @@ export const conanPackage = {
_links, _links,
}; };
export const packageList = [mavenPackage, npmPackage, conanPackage];
export const mockTags = [ export const mockTags = [
{ {
name: 'foo-1', name: 'foo-1',
...@@ -94,6 +92,8 @@ export const mockTags = [ ...@@ -94,6 +92,8 @@ export const mockTags = [
}, },
]; ];
export const packageList = [mavenPackage, { ...npmPackage, tags: mockTags }, conanPackage];
export const mockPipelineInfo = { export const mockPipelineInfo = {
id: 1, id: 1,
ref: 'branch-name', ref: 'branch-name',
......
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import PackageTags from 'ee/packages/details/components/package_tags.vue'; import PackageTags from 'ee/packages/shared/components/package_tags.vue';
import { mockTags } from '../../mock_data'; import { mockTags } from '../../mock_data';
describe('PackageTags', () => { describe('PackageTags', () => {
...@@ -16,6 +16,7 @@ describe('PackageTags', () => { ...@@ -16,6 +16,7 @@ describe('PackageTags', () => {
}); });
} }
const tagLabel = () => wrapper.find({ ref: 'tagLabel' });
const tagBadges = () => wrapper.findAll({ ref: 'tagBadge' }); const tagBadges = () => wrapper.findAll({ ref: 'tagBadge' });
const moreBadge = () => wrapper.find({ ref: 'moreBadge' }); const moreBadge = () => wrapper.find({ ref: 'moreBadge' });
...@@ -23,23 +24,37 @@ describe('PackageTags', () => { ...@@ -23,23 +24,37 @@ describe('PackageTags', () => {
if (wrapper) wrapper.destroy(); if (wrapper) wrapper.destroy();
}); });
describe('tag label', () => {
it('shows the tag label by default', () => {
createComponent();
expect(tagLabel().exists()).toBe(true);
});
it('hides when hideLabel prop is set to true', () => {
createComponent(mockTags, { hideLabel: true });
expect(tagLabel().exists()).toBe(false);
});
});
it('renders the correct number of tags', () => { it('renders the correct number of tags', () => {
createComponent(mockTags.slice(0, 2)); createComponent(mockTags.slice(0, 2));
expect(tagBadges().length).toBe(2); expect(tagBadges()).toHaveLength(2);
expect(moreBadge().exists()).toBe(false); expect(moreBadge().exists()).toBe(false);
}); });
it('does not render more than the configured tagDisplayLimit', () => { it('does not render more than the configured tagDisplayLimit', () => {
createComponent(mockTags); createComponent(mockTags);
expect(tagBadges().length).toBe(2); expect(tagBadges()).toHaveLength(2);
}); });
it('renders the more tags badge if there are more than the configured limit', () => { it('renders the more tags badge if there are more than the configured limit', () => {
createComponent(mockTags); createComponent(mockTags);
expect(tagBadges().length).toBe(2); expect(tagBadges()).toHaveLength(2);
expect(moreBadge().exists()).toBe(true); expect(moreBadge().exists()).toBe(true);
expect(moreBadge().text()).toContain('2'); expect(moreBadge().text()).toContain('2');
}); });
...@@ -47,8 +62,60 @@ describe('PackageTags', () => { ...@@ -47,8 +62,60 @@ describe('PackageTags', () => {
it('renders the configured tagDisplayLimit when set in props', () => { it('renders the configured tagDisplayLimit when set in props', () => {
createComponent(mockTags, { tagDisplayLimit: 1 }); createComponent(mockTags, { tagDisplayLimit: 1 });
expect(tagBadges().length).toBe(1); expect(tagBadges()).toHaveLength(1);
expect(moreBadge().exists()).toBe(true); expect(moreBadge().exists()).toBe(true);
expect(moreBadge().text()).toContain('3'); expect(moreBadge().text()).toContain('3');
}); });
describe('tagBadgeStyle', () => {
const defaultStyle = {
'd-none': true,
'd-block': false,
'd-md-block': false,
'append-right-4': false,
};
it('shows tag badge when there is only one', () => {
createComponent([mockTags[0]]);
const expectedStyle = {
...defaultStyle,
'd-block': true,
};
expect(wrapper.vm.tagBadgeClass(0)).toEqual(expectedStyle);
});
it('shows tag badge for medium or heigher resolutions', () => {
createComponent(mockTags);
const expectedStyle = {
...defaultStyle,
'd-md-block': true,
};
expect(wrapper.vm.tagBadgeClass(1)).toEqual(expectedStyle);
});
it('correctly appends right when there is more than one tag', () => {
createComponent(mockTags, {
tagDisplayLimit: 4,
});
const expectedStyleWithoutAppend = {
...defaultStyle,
'd-md-block': true,
};
const expectedStyleWithAppend = {
...expectedStyleWithoutAppend,
'append-right-4': true,
};
expect(wrapper.vm.tagBadgeClass(0)).toEqual(expectedStyleWithAppend);
expect(wrapper.vm.tagBadgeClass(1)).toEqual(expectedStyleWithAppend);
expect(wrapper.vm.tagBadgeClass(2)).toEqual(expectedStyleWithAppend);
expect(wrapper.vm.tagBadgeClass(3)).toEqual(expectedStyleWithoutAppend);
});
});
}); });
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