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