Commit 6ef00388 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '197926-initial-package-detail-title' into 'master'

New package details title base implementation

See merge request gitlab-org/gitlab!24055
parents fe2e6239 b6f10a72
---
title: Updated package details page header to begin updating the page design.
merge_request: 24055
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 '../../shared/components/package_tags.vue';
import PackageTitle from './package_title.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { generatePackageInfo } from '../utils';
......@@ -34,10 +34,10 @@ export default {
GlTable,
GlIcon,
PackageInformation,
PackageTags,
NpmInstallation,
MavenInstallation,
ConanInstallation,
PackageTitle,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -99,9 +99,6 @@ export default {
isValidPackage() {
return Boolean(this.packageEntity.name);
},
hasTagsToDisplay() {
return Boolean(this.packageEntity.tags && this.packageEntity.tags.length);
},
canDeletePackage() {
return this.canDelete && this.destroyPath;
},
......@@ -205,12 +202,10 @@ export default {
/>
<div v-else class="packages-app">
<div class="detail-page-header d-flex justify-content-between">
<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>
<div class="detail-page-header d-flex justify-content-between flex-column flex-sm-row">
<package-title />
<div class="mt-sm-2">
<gl-button
v-if="canDeletePackage"
v-gl-modal="'delete-modal'"
......@@ -220,6 +215,7 @@ export default {
>{{ __('Delete') }}</gl-button
>
</div>
</div>
<div class="row prepend-top-default" data-qa-selector="package_information_content">
<div class="col-sm-6">
......
<script>
import { mapState, mapGetters } from 'vuex';
import { GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import PackageTags from '../../shared/components/package_tags.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
name: 'PackageTitle',
components: {
GlIcon,
GlSprintf,
PackageTags,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [timeagoMixin],
computed: {
...mapState(['packageEntity', 'packageFiles']),
...mapGetters(['packageTypeDisplay']),
hasTagsToDisplay() {
return Boolean(this.packageEntity.tags && this.packageEntity.tags.length);
},
totalSize() {
return numberToHumanSize(this.packageFiles.reduce((acc, p) => acc + p.size, 0));
},
},
};
</script>
<template>
<div class="flex-column">
<h1 class="gl-font-size-20 prepend-top-8 append-bottom-4">{{ packageEntity.name }}</h1>
<div class="d-flex align-items-center text-secondary">
<gl-icon name="eye" class="append-right-8" />
<gl-sprintf message="v%{version} published %{timeAgo}">
<template #version>
{{ packageEntity.version }}
</template>
<template #timeAgo>
<span v-gl-tooltip :title="tooltipTitle(packageEntity.created_at)">
&nbsp;{{ timeFormatted(packageEntity.created_at) }}
</span>
</template>
</gl-sprintf>
</div>
<div class="d-flex flex-wrap align-items-center append-bottom-8">
<div v-if="packageTypeDisplay" class="d-flex align-items-center append-right-default">
<gl-icon name="package" class="text-secondary append-right-8" />
<span ref="package-type" class="font-weight-bold">{{ packageTypeDisplay }}</span>
</div>
<div v-if="hasTagsToDisplay" class="d-flex align-items-center append-right-default">
<package-tags :tag-display-limit="1" :tags="packageEntity.tags" />
</div>
<div class="d-flex align-items-center append-right-default">
<gl-icon name="disk" class="text-secondary append-right-8" />
<span ref="package-size" class="font-weight-bold">{{ totalSize }}</span>
</div>
</div>
</div>
</template>
export default ({ packageEntity }) => {
import { s__ } from '~/locale';
export const packageHasPipeline = ({ packageEntity }) => {
if (packageEntity?.build_info?.pipeline_id) {
return true;
}
return false;
};
export const packageTypeDisplay = ({ packageEntity }) => {
switch (packageEntity.package_type) {
case 'conan':
return s__('PackageType|Conan');
case 'maven':
return s__('PackageType|Maven');
case 'npm':
return s__('PackageType|NPM');
case 'nuget':
return s__('PackageType|NuGet');
default:
return null;
}
};
......@@ -2,7 +2,7 @@ import Vue from 'vue';
import Vuex from 'vuex';
import state from './state';
import * as actions from './actions';
import packageHasPipeline from './getters';
import * as getters from './getters';
import mutations from './mutations';
Vue.use(Vuex);
......@@ -10,9 +10,7 @@ Vue.use(Vuex);
export default (initialState = {}) =>
new Vuex.Store({
actions,
getters: {
packageHasPipeline,
},
getters,
mutations,
state: {
...state(),
......
......@@ -60,6 +60,7 @@ export default {
'd-block': this.tagCount === 1,
'd-md-block': this.tagCount > 1,
'append-right-4': index !== this.tagsToRender.length - 1,
'prepend-left-8': !this.hideLabel && index === 0,
};
},
},
......@@ -70,7 +71,7 @@ export default {
<div class="d-flex align-items-center">
<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>
<strong class="js-tags-count">{{ tagsDisplay }}</strong>
</div>
<gl-badge
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PackageTitle renders with tags 1`] = `
<div
class="flex-column"
>
<h1
class="gl-font-size-20 prepend-top-8 append-bottom-4"
>
Test package
</h1>
<div
class="d-flex align-items-center text-secondary"
>
<gl-icon-stub
class="append-right-8"
name="eye"
size="16"
/>
<gl-sprintf-stub
message="v%{version} published %{timeAgo}"
/>
</div>
<div
class="d-flex flex-wrap align-items-center append-bottom-8"
>
<div
class="d-flex align-items-center append-right-default"
>
<gl-icon-stub
class="text-secondary append-right-8"
name="package"
size="16"
/>
<span
class="font-weight-bold"
>
maven
</span>
</div>
<div
class="d-flex align-items-center append-right-default"
>
<package-tags-stub
tagdisplaylimit="1"
tags="[object Object],[object Object],[object Object],[object Object]"
/>
</div>
<div
class="d-flex align-items-center append-right-default"
>
<gl-icon-stub
class="text-secondary append-right-8"
name="disk"
size="16"
/>
<span
class="font-weight-bold"
>
300 bytes
</span>
</div>
</div>
</div>
`;
exports[`PackageTitle renders without tags 1`] = `
<div
class="flex-column"
>
<h1
class="gl-font-size-20 prepend-top-8 append-bottom-4"
>
Test package
</h1>
<div
class="d-flex align-items-center text-secondary"
>
<gl-icon-stub
class="append-right-8"
name="eye"
size="16"
/>
<gl-sprintf-stub
message="v%{version} published %{timeAgo}"
/>
</div>
<div
class="d-flex flex-wrap align-items-center append-bottom-8"
>
<div
class="d-flex align-items-center append-right-default"
>
<gl-icon-stub
class="text-secondary append-right-8"
name="package"
size="16"
/>
<span
class="font-weight-bold"
>
maven
</span>
</div>
<!---->
<div
class="d-flex align-items-center append-right-default"
>
<gl-icon-stub
class="text-secondary append-right-8"
name="disk"
size="16"
/>
<span
class="font-weight-bold"
>
300 bytes
</span>
</div>
</div>
</div>
`;
......@@ -3,10 +3,10 @@ import { mount, createLocalVue } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
import Tracking from '~/tracking';
import PackagesApp from 'ee/packages/details/components/app.vue';
import PackageTitle from 'ee/packages/details/components/package_title.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/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';
......@@ -46,6 +46,7 @@ describe('PackagesApp', () => {
},
getters: {
packageHasPipeline: () => packageEntity.build_info && packageEntity.build_info.pipeline_id,
packageTypeDisplay: () => {},
},
});
......@@ -56,7 +57,7 @@ describe('PackagesApp', () => {
});
}
const versionTitle = () => wrapper.find('.js-version-title');
const packageTitle = () => wrapper.find(PackageTitle);
const emptyState = () => wrapper.find('.js-package-empty-state');
const allPackageInformation = () => wrapper.findAll(PackageInformation);
const packageInformation = index => allPackageInformation().at(index);
......@@ -68,17 +69,15 @@ describe('PackagesApp', () => {
const deleteButton = () => wrapper.find('.js-delete-button');
const deleteModal = () => wrapper.find(GlModal);
const modalDeleteButton = () => wrapper.find({ ref: 'modal-delete-button' });
const packageTags = () => wrapper.find(PackageTags);
afterEach(() => {
wrapper.destroy();
});
it('renders the app and displays the package version as the title', () => {
it('renders the app and displays the package title', () => {
createComponent();
expect(versionTitle()).toExist();
expect(versionTitle().text()).toBe(mavenPackage.version);
expect(packageTitle()).toExist();
});
it('renders an empty state component when no an invalid package is passed as a prop', () => {
......@@ -153,23 +152,6 @@ describe('PackagesApp', () => {
});
});
describe('package tags', () => {
it('displays the package-tags component when the package has tags', () => {
createComponent({
...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', () => {
let eventSpy;
let utilSpy;
......
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import PackageTitle from 'ee/packages/details/components/package_title.vue';
import PackageTags from 'ee/packages/shared/components/package_tags.vue';
import {
conanPackage,
mavenFiles,
mavenPackage,
mockTags,
npmFiles,
npmPackage,
nugetPackage,
} from '../../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('PackageTitle', () => {
let wrapper;
let store;
function createComponent(packageEntity = mavenPackage, packageFiles = mavenFiles) {
store = new Vuex.Store({
state: {
packageEntity,
packageFiles,
},
getters: {
packageTypeDisplay: ({ packageEntity: { package_type: type } }) => type,
},
});
wrapper = shallowMount(PackageTitle, {
localVue,
store,
});
}
const packageType = () => wrapper.find({ ref: 'package-type' });
const packageSize = () => wrapper.find({ ref: 'package-size' });
const packageTags = () => wrapper.find(PackageTags);
afterEach(() => {
wrapper.destroy();
});
describe('renders', () => {
it('without tags', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
});
it('with tags', () => {
createComponent({ ...mavenPackage, tags: mockTags });
expect(wrapper.element).toMatchSnapshot();
});
});
describe.each`
packageEntity | expectedResult
${conanPackage} | ${'conan'}
${mavenPackage} | ${'maven'}
${npmPackage} | ${'npm'}
${nugetPackage} | ${'nuget'}
`(`package type`, ({ packageEntity, expectedResult }) => {
beforeEach(() => createComponent(packageEntity));
it(`${packageEntity.package_type} should render from Vuex getters ${expectedResult}`, () => {
expect(packageType().text()).toBe(expectedResult);
});
});
describe('calculates the package size', () => {
it('correctly calulates when there is only 1 file', () => {
createComponent(npmPackage, npmFiles);
expect(packageSize().text()).toBe('200 bytes');
});
it('correctly calulates when there are multiple files', () => {
createComponent();
expect(packageSize().text()).toBe('300 bytes');
});
});
describe('package tags', () => {
it('displays the package-tags component when the package has tags', () => {
createComponent({
...npmPackage,
tags: mockTags,
});
expect(packageTags().exists()).toBe(true);
});
it('does not display the package-tags component when there are no tags', () => {
createComponent();
expect(packageTags().exists()).toBe(false);
});
});
});
import packageHasPipeline from 'ee/packages/details/store/getters';
import { packageHasPipeline, packageTypeDisplay } from 'ee/packages/details/store/getters';
import {
conanPackage,
npmPackage,
mavenPackage as packageWithoutBuildInfo,
nugetPackage,
mockPipelineInfo,
mavenPackage as packageWithoutBuildInfo,
} from '../../mock_data';
describe('Getters PackageDetails Store', () => {
......@@ -35,4 +37,20 @@ describe('Getters PackageDetails Store', () => {
expect(packageHasPipeline(state)).toEqual(false);
});
});
describe('packageTypeDisplay', () => {
describe.each`
packageEntity | expectedResult
${conanPackage} | ${'Conan'}
${packageWithoutBuildInfo} | ${'Maven'}
${npmPackage} | ${'NPM'}
${nugetPackage} | ${'NuGet'}
`(`package type`, ({ packageEntity, expectedResult }) => {
beforeEach(() => setupState({ packageEntity }));
it(`${packageEntity.package_type} should show as ${expectedResult}`, () => {
expect(packageTypeDisplay(state)).toBe(expectedResult);
});
});
});
});
......@@ -77,6 +77,18 @@ export const conanPackage = {
_links,
};
export const nugetPackage = {
created_at: '2015-12-10',
id: 4,
name: 'NugetPackage1',
package_files: [],
package_type: 'nuget',
project_id: 1,
tags: [],
updated_at: '2015-12-10',
version: '1.0.0',
};
export const mockTags = [
{
name: 'foo-1',
......
......@@ -68,54 +68,46 @@ describe('PackageTags', () => {
});
describe('tagBadgeStyle', () => {
const defaultStyle = {
'd-none': true,
'd-block': false,
'd-md-block': false,
'append-right-4': false,
};
const defaultStyle = ['badge', 'badge-info', 'd-none'];
it('shows tag badge when there is only one', () => {
createComponent([mockTags[0]]);
const expectedStyle = {
...defaultStyle,
'd-block': true,
};
const expectedStyle = [...defaultStyle, 'd-block', 'prepend-left-8'];
expect(wrapper.vm.tagBadgeClass(0)).toEqual(expectedStyle);
expect(
tagBadges()
.at(0)
.classes(),
).toEqual(expectedStyle);
});
it('shows tag badge for medium or heigher resolutions', () => {
createComponent(mockTags);
const expectedStyle = {
...defaultStyle,
'd-md-block': true,
};
const expectedStyle = [...defaultStyle, 'd-md-block'];
expect(wrapper.vm.tagBadgeClass(1)).toEqual(expectedStyle);
expect(
tagBadges()
.at(1)
.classes(),
).toEqual(expectedStyle);
});
it('correctly appends right when there is more than one tag', () => {
it('correctly prepends left and appends right when there is more than one tag', () => {
createComponent(mockTags, {
tagDisplayLimit: 4,
});
const expectedStyleWithoutAppend = {
...defaultStyle,
'd-md-block': true,
};
const expectedStyleWithoutAppend = [...defaultStyle, 'd-md-block'];
const expectedStyleWithAppend = [...expectedStyleWithoutAppend, 'append-right-4'];
const expectedStyleWithAppend = {
...expectedStyleWithoutAppend,
'append-right-4': true,
};
const allBadges = tagBadges();
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);
expect(allBadges.at(0).classes()).toEqual([...expectedStyleWithAppend, 'prepend-left-8']);
expect(allBadges.at(1).classes()).toEqual(expectedStyleWithAppend);
expect(allBadges.at(2).classes()).toEqual(expectedStyleWithAppend);
expect(allBadges.at(3).classes()).toEqual(expectedStyleWithoutAppend);
});
});
});
......@@ -13266,6 +13266,18 @@ msgstr ""
msgid "PackageRegistry|yarn"
msgstr ""
msgid "PackageType|Conan"
msgstr ""
msgid "PackageType|Maven"
msgstr ""
msgid "PackageType|NPM"
msgstr ""
msgid "PackageType|NuGet"
msgstr ""
msgid "Packages"
msgstr ""
......
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