Commit bcf7fb3c authored by Phil Hughes's avatar Phil Hughes

Merge branch '9425-gitlab-npm-registry-to-support-npm-tags' into 'master'

Display NPM tags on package details page

See merge request gitlab-org/gitlab!23061
parents cb10f5b0 5fd7b605
---
title: NPM dist tags will now be displayed on the package details page.
merge_request: 23061
author:
type: added
<script> <script>
import { import {
GlButton, GlButton,
GlIcon,
GlModal, GlModal,
GlModalDirective, GlModalDirective,
GlTooltipDirective, GlTooltipDirective,
...@@ -14,7 +15,7 @@ import PackageInformation from './information.vue'; ...@@ -14,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 Icon from '~/vue_shared/components/icon.vue'; import PackageTags from './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';
...@@ -30,8 +31,9 @@ export default { ...@@ -30,8 +31,9 @@ export default {
GlLink, GlLink,
GlModal, GlModal,
GlTable, GlTable,
Icon, GlIcon,
PackageInformation, PackageInformation,
PackageTags,
NpmInstallation, NpmInstallation,
MavenInstallation, MavenInstallation,
ConanInstallation, ConanInstallation,
...@@ -104,6 +106,9 @@ export default { ...@@ -104,6 +106,9 @@ export default {
isValidPackage() { isValidPackage() {
return Boolean(this.packageEntity.name); return Boolean(this.packageEntity.name);
}, },
hasTagsToDisplay() {
return Boolean(this.packageEntity.tags && this.packageEntity.tags.length);
},
canDeletePackage() { canDeletePackage() {
return this.canDelete && this.destroyPath; return this.canDelete && this.destroyPath;
}, },
...@@ -208,7 +213,11 @@ export default { ...@@ -208,7 +213,11 @@ export default {
<div v-else class="packages-app"> <div v-else class="packages-app">
<div class="detail-page-header d-flex justify-content-between"> <div class="detail-page-header d-flex justify-content-between">
<strong class="js-version-title">{{ packageEntity.version }}</strong> <div class="d-flex align-items-center">
<gl-icon name="fork" class="append-right-8" />
<strong class="append-right-default js-version-title">{{ packageEntity.version }}</strong>
<package-tags v-if="hasTagsToDisplay" :tags="packageEntity.tags" />
</div>
<gl-button <gl-button
v-if="canDeletePackage" v-if="canDeletePackage"
v-gl-modal="'delete-modal'" v-gl-modal="'delete-modal'"
...@@ -260,7 +269,7 @@ export default { ...@@ -260,7 +269,7 @@ export default {
tbody-tr-class="js-file-row" tbody-tr-class="js-file-row"
> >
<template #name="items"> <template #name="items">
<icon name="doc-code" class="space-right" /> <gl-icon name="doc-code" class="space-right" />
<gl-link <gl-link
:href="items.item.downloadPath" :href="items.item.downloadPath"
class="js-file-download" class="js-file-download"
......
<script>
import { GlBadge, GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
export default {
name: 'PackageTags',
components: {
GlBadge,
GlIcon,
GlSprintf,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
tagDisplayLimit: {
type: Number,
required: false,
default: 2,
},
tags: {
type: Array,
required: true,
default: () => [],
},
},
computed: {
tagCount() {
return this.tags.length;
},
tagsToRender() {
return this.tags.slice(0, this.tagDisplayLimit);
},
moreTagsDisplay() {
return Math.max(0, this.tags.length - this.tagDisplayLimit);
},
moreTagsTooltip() {
if (this.moreTagsDisplay) {
return this.tags
.slice(this.tagDisplayLimit)
.map(x => x.name)
.join(', ');
}
return '';
},
},
};
</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>
<gl-badge
v-for="(tag, index) in tagsToRender"
:key="index"
ref="tagBadge"
class="append-right-4"
variant="info"
>{{ tag.name }}</gl-badge
>
<gl-badge
v-if="moreTagsDisplay"
ref="moreBadge"
v-gl-tooltip
variant="light"
:title="moreTagsTooltip"
><gl-sprintf message="+%{tags} more">
<template #tags>
{{ moreTagsDisplay }}
</template>
</gl-sprintf></gl-badge
>
</div>
</template>
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
.row .row
.col-12 .col-12
#js-vue-packages-detail{ data: { package: @package.to_json(include: [:conan_metadatum, :maven_metadatum, :package_files]), #js-vue-packages-detail{ data: { package: @package.to_json(include: [:conan_metadatum, :maven_metadatum, :package_files, :tags]),
package_files: @package_files.to_json(methods: :download_path), package_files: @package_files.to_json(methods: :download_path),
can_delete: can?(current_user, :destroy_package, @project).to_s, can_delete: can?(current_user, :destroy_package, @project).to_s,
destroy_path: project_package_path(@project, @package), destroy_path: project_package_path(@project, @package),
......
...@@ -5,6 +5,7 @@ import PackagesApp from 'ee/packages/details/components/app.vue'; ...@@ -5,6 +5,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 * 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';
...@@ -50,6 +51,7 @@ describe('PackagesApp', () => { ...@@ -50,6 +51,7 @@ describe('PackagesApp', () => {
const deleteButton = () => wrapper.find('.js-delete-button'); const deleteButton = () => wrapper.find('.js-delete-button');
const deleteModal = () => wrapper.find(GlModal); const deleteModal = () => wrapper.find(GlModal);
const modalDeleteButton = () => wrapper.find({ ref: 'modal-delete-button' }); const modalDeleteButton = () => wrapper.find({ ref: 'modal-delete-button' });
const packageTags = () => wrapper.find(PackageTags);
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -143,6 +145,25 @@ describe('PackagesApp', () => { ...@@ -143,6 +145,25 @@ describe('PackagesApp', () => {
}); });
}); });
describe('package tags', () => {
it('displays the package-tags component when the package has tags', () => {
createComponent({
packageEntity: {
...npmPackage,
tags: [{ name: 'foo' }],
},
});
expect(packageTags().exists()).toBe(true);
});
it('does not display the package-tags component when there are no tags', () => {
createComponent();
expect(packageTags().exists()).toBe(false);
});
});
describe('tracking', () => { describe('tracking', () => {
let eventSpy; let eventSpy;
let utilSpy; let utilSpy;
......
import { mount } from '@vue/test-utils';
import PackageTags from 'ee/packages/details/components/package_tags.vue';
import { mockTags } from '../../mock_data';
describe('PackageTags', () => {
let wrapper;
function createComponent(tags = [], props = {}) {
const propsData = {
tags,
...props,
};
wrapper = mount(PackageTags, {
propsData,
});
}
const tagBadges = () => wrapper.findAll({ ref: 'tagBadge' });
const moreBadge = () => wrapper.find({ ref: 'moreBadge' });
afterEach(() => {
if (wrapper) wrapper.destroy();
});
it('renders the correct number of tags', () => {
createComponent(mockTags.slice(0, 2));
expect(tagBadges().length).toBe(2);
expect(moreBadge().exists()).toBe(false);
});
it('does not render more than the configured tagDisplayLimit', () => {
createComponent(mockTags);
expect(tagBadges().length).toBe(2);
});
it('renders the more tags badge if there are more than the configured limit', () => {
createComponent(mockTags);
expect(tagBadges().length).toBe(2);
expect(moreBadge().exists()).toBe(true);
expect(moreBadge().text()).toContain('2');
});
it('renders the configured tagDisplayLimit when set in props', () => {
createComponent(mockTags, { tagDisplayLimit: 1 });
expect(tagBadges().length).toBe(1);
expect(moreBadge().exists()).toBe(true);
expect(moreBadge().text()).toContain('3');
});
});
...@@ -75,3 +75,18 @@ export const conanPackage = { ...@@ -75,3 +75,18 @@ export const conanPackage = {
}; };
export const packageList = [mavenPackage, npmPackage, conanPackage]; export const packageList = [mavenPackage, npmPackage, conanPackage];
export const mockTags = [
{
name: 'foo-1',
},
{
name: 'foo-2',
},
{
name: 'foo-3',
},
{
name: 'foo-4',
},
];
...@@ -173,6 +173,11 @@ msgid_plural "%d staged changes" ...@@ -173,6 +173,11 @@ msgid_plural "%d staged changes"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "%d tag"
msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
msgid "%d unstaged change" msgid "%d unstaged change"
msgid_plural "%d unstaged changes" msgid_plural "%d unstaged changes"
msgstr[0] "" msgstr[0] ""
......
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