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'; ...@@ -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 '../../shared/components/package_tags.vue'; import PackageTitle from './package_title.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';
...@@ -34,10 +34,10 @@ export default { ...@@ -34,10 +34,10 @@ export default {
GlTable, GlTable,
GlIcon, GlIcon,
PackageInformation, PackageInformation,
PackageTags,
NpmInstallation, NpmInstallation,
MavenInstallation, MavenInstallation,
ConanInstallation, ConanInstallation,
PackageTitle,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -99,9 +99,6 @@ export default { ...@@ -99,9 +99,6 @@ 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;
}, },
...@@ -205,12 +202,10 @@ export default { ...@@ -205,12 +202,10 @@ 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 flex-column flex-sm-row">
<div class="d-flex align-items-center"> <package-title />
<gl-icon name="fork" class="append-right-8" />
<strong class="append-right-default js-version-title">{{ packageEntity.version }}</strong> <div class="mt-sm-2">
<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'"
...@@ -220,6 +215,7 @@ export default { ...@@ -220,6 +215,7 @@ export default {
>{{ __('Delete') }}</gl-button >{{ __('Delete') }}</gl-button
> >
</div> </div>
</div>
<div class="row prepend-top-default" data-qa-selector="package_information_content"> <div class="row prepend-top-default" data-qa-selector="package_information_content">
<div class="col-sm-6"> <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) { if (packageEntity?.build_info?.pipeline_id) {
return true; return true;
} }
return false; 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'; ...@@ -2,7 +2,7 @@ import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import state from './state'; import state from './state';
import * as actions from './actions'; import * as actions from './actions';
import packageHasPipeline from './getters'; import * as getters from './getters';
import mutations from './mutations'; import mutations from './mutations';
Vue.use(Vuex); Vue.use(Vuex);
...@@ -10,9 +10,7 @@ Vue.use(Vuex); ...@@ -10,9 +10,7 @@ Vue.use(Vuex);
export default (initialState = {}) => export default (initialState = {}) =>
new Vuex.Store({ new Vuex.Store({
actions, actions,
getters: { getters,
packageHasPipeline,
},
mutations, mutations,
state: { state: {
...state(), ...state(),
......
...@@ -60,6 +60,7 @@ export default { ...@@ -60,6 +60,7 @@ export default {
'd-block': this.tagCount === 1, 'd-block': this.tagCount === 1,
'd-md-block': this.tagCount > 1, 'd-md-block': this.tagCount > 1,
'append-right-4': index !== this.tagsToRender.length - 1, 'append-right-4': index !== this.tagsToRender.length - 1,
'prepend-left-8': !this.hideLabel && index === 0,
}; };
}, },
}, },
...@@ -70,7 +71,7 @@ export default { ...@@ -70,7 +71,7 @@ export default {
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div v-if="!hideLabel" ref="tagLabel" 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" /> <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> </div>
<gl-badge <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'; ...@@ -3,10 +3,10 @@ import { mount, createLocalVue } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui'; import { GlModal } from '@gitlab/ui';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import PackagesApp from 'ee/packages/details/components/app.vue'; 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 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/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';
...@@ -46,6 +46,7 @@ describe('PackagesApp', () => { ...@@ -46,6 +46,7 @@ describe('PackagesApp', () => {
}, },
getters: { getters: {
packageHasPipeline: () => packageEntity.build_info && packageEntity.build_info.pipeline_id, packageHasPipeline: () => packageEntity.build_info && packageEntity.build_info.pipeline_id,
packageTypeDisplay: () => {},
}, },
}); });
...@@ -56,7 +57,7 @@ describe('PackagesApp', () => { ...@@ -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 emptyState = () => wrapper.find('.js-package-empty-state');
const allPackageInformation = () => wrapper.findAll(PackageInformation); const allPackageInformation = () => wrapper.findAll(PackageInformation);
const packageInformation = index => allPackageInformation().at(index); const packageInformation = index => allPackageInformation().at(index);
...@@ -68,17 +69,15 @@ describe('PackagesApp', () => { ...@@ -68,17 +69,15 @@ 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();
}); });
it('renders the app and displays the package version as the title', () => { it('renders the app and displays the package title', () => {
createComponent(); createComponent();
expect(versionTitle()).toExist(); expect(packageTitle()).toExist();
expect(versionTitle().text()).toBe(mavenPackage.version);
}); });
it('renders an empty state component when no an invalid package is passed as a prop', () => { it('renders an empty state component when no an invalid package is passed as a prop', () => {
...@@ -153,23 +152,6 @@ describe('PackagesApp', () => { ...@@ -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', () => { describe('tracking', () => {
let eventSpy; let eventSpy;
let utilSpy; 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 { import {
conanPackage,
npmPackage, npmPackage,
mavenPackage as packageWithoutBuildInfo, nugetPackage,
mockPipelineInfo, mockPipelineInfo,
mavenPackage as packageWithoutBuildInfo,
} from '../../mock_data'; } from '../../mock_data';
describe('Getters PackageDetails Store', () => { describe('Getters PackageDetails Store', () => {
...@@ -35,4 +37,20 @@ describe('Getters PackageDetails Store', () => { ...@@ -35,4 +37,20 @@ describe('Getters PackageDetails Store', () => {
expect(packageHasPipeline(state)).toEqual(false); 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 = { ...@@ -77,6 +77,18 @@ export const conanPackage = {
_links, _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 = [ export const mockTags = [
{ {
name: 'foo-1', name: 'foo-1',
......
...@@ -68,54 +68,46 @@ describe('PackageTags', () => { ...@@ -68,54 +68,46 @@ describe('PackageTags', () => {
}); });
describe('tagBadgeStyle', () => { describe('tagBadgeStyle', () => {
const defaultStyle = { const defaultStyle = ['badge', 'badge-info', 'd-none'];
'd-none': true,
'd-block': false,
'd-md-block': false,
'append-right-4': false,
};
it('shows tag badge when there is only one', () => { it('shows tag badge when there is only one', () => {
createComponent([mockTags[0]]); createComponent([mockTags[0]]);
const expectedStyle = { const expectedStyle = [...defaultStyle, 'd-block', 'prepend-left-8'];
...defaultStyle,
'd-block': true,
};
expect(wrapper.vm.tagBadgeClass(0)).toEqual(expectedStyle); expect(
tagBadges()
.at(0)
.classes(),
).toEqual(expectedStyle);
}); });
it('shows tag badge for medium or heigher resolutions', () => { it('shows tag badge for medium or heigher resolutions', () => {
createComponent(mockTags); createComponent(mockTags);
const expectedStyle = { const expectedStyle = [...defaultStyle, 'd-md-block'];
...defaultStyle,
'd-md-block': true,
};
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, { createComponent(mockTags, {
tagDisplayLimit: 4, tagDisplayLimit: 4,
}); });
const expectedStyleWithoutAppend = { const expectedStyleWithoutAppend = [...defaultStyle, 'd-md-block'];
...defaultStyle, const expectedStyleWithAppend = [...expectedStyleWithoutAppend, 'append-right-4'];
'd-md-block': true,
};
const expectedStyleWithAppend = { const allBadges = tagBadges();
...expectedStyleWithoutAppend,
'append-right-4': true,
};
expect(wrapper.vm.tagBadgeClass(0)).toEqual(expectedStyleWithAppend); expect(allBadges.at(0).classes()).toEqual([...expectedStyleWithAppend, 'prepend-left-8']);
expect(wrapper.vm.tagBadgeClass(1)).toEqual(expectedStyleWithAppend); expect(allBadges.at(1).classes()).toEqual(expectedStyleWithAppend);
expect(wrapper.vm.tagBadgeClass(2)).toEqual(expectedStyleWithAppend); expect(allBadges.at(2).classes()).toEqual(expectedStyleWithAppend);
expect(wrapper.vm.tagBadgeClass(3)).toEqual(expectedStyleWithoutAppend); expect(allBadges.at(3).classes()).toEqual(expectedStyleWithoutAppend);
}); });
}); });
}); });
...@@ -13266,6 +13266,18 @@ msgstr "" ...@@ -13266,6 +13266,18 @@ msgstr ""
msgid "PackageRegistry|yarn" msgid "PackageRegistry|yarn"
msgstr "" msgstr ""
msgid "PackageType|Conan"
msgstr ""
msgid "PackageType|Maven"
msgstr ""
msgid "PackageType|NPM"
msgstr ""
msgid "PackageType|NuGet"
msgstr ""
msgid "Packages" msgid "Packages"
msgstr "" 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