Commit f5c1aa66 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera Committed by Mark Florian

Add support for dynamic slots

- source
- tests
parent 9fc33b59
<script> <script>
/* eslint-disable vue/v-slot-style */
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import { GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui'; import { GlIcon, GlSprintf, GlTooltipDirective, GlBadge } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import PackageTags from '../../shared/components/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';
...@@ -16,11 +18,20 @@ export default { ...@@ -16,11 +18,20 @@ export default {
GlSprintf, GlSprintf,
PackageTags, PackageTags,
MetadataItem, MetadataItem,
GlBadge,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
mixins: [timeagoMixin], mixins: [timeagoMixin],
i18n: {
packageInfo: __('v%{version} published %{timeAgo}'),
},
data() {
return {
isDesktop: true,
};
},
computed: { computed: {
...mapState(['packageEntity', 'packageFiles']), ...mapState(['packageEntity', 'packageFiles']),
...mapGetters(['packageTypeDisplay', 'packagePipeline', 'packageIcon']), ...mapGetters(['packageTypeDisplay', 'packagePipeline', 'packageIcon']),
...@@ -31,8 +42,13 @@ export default { ...@@ -31,8 +42,13 @@ export default {
return numberToHumanSize(this.packageFiles.reduce((acc, p) => acc + p.size, 0)); return numberToHumanSize(this.packageFiles.reduce((acc, p) => acc + p.size, 0));
}, },
}, },
i18n: { mounted() {
packageInfo: __('v%{version} published %{timeAgo}'), this.isDesktop = GlBreakpointInstance.isDesktop();
},
methods: {
dynamicSlotName(index) {
return `metadata-tag${index}`;
},
}, },
}; };
</script> </script>
...@@ -75,10 +91,21 @@ export default { ...@@ -75,10 +91,21 @@ export default {
<metadata-item data-testid="package-ref" icon="branch" :text="packagePipeline.ref" /> <metadata-item data-testid="package-ref" icon="branch" :text="packagePipeline.ref" />
</template> </template>
<template v-if="hasTagsToDisplay" #metadata-tags> <template v-if="isDesktop && hasTagsToDisplay" #metadata-tags>
<package-tags :tag-display-limit="2" :tags="packageEntity.tags" hide-label /> <package-tags :tag-display-limit="2" :tags="packageEntity.tags" hide-label />
</template> </template>
<!-- we need to duplicate the package tags on mobile to ensure proper styling inside the flex wrap -->
<template
v-for="(tag, index) in packageEntity.tags"
v-else-if="hasTagsToDisplay"
v-slot:[dynamicSlotName(index)]
>
<gl-badge :key="index" class="gl-my-1" data-testid="tag-badge" variant="info" size="sm">
{{ tag.name }}
</gl-badge>
</template>
<template #right-actions> <template #right-actions>
<slot name="delete-button"></slot> <slot name="delete-button"></slot>
</template> </template>
......
...@@ -30,8 +30,13 @@ export default { ...@@ -30,8 +30,13 @@ export default {
metadataSlots: [], metadataSlots: [],
}; };
}, },
mounted() { async mounted() {
this.metadataSlots = Object.keys(this.$slots).filter(k => k.startsWith('metadata-')); const METADATA_PREFIX = 'metadata-';
this.metadataSlots = Object.keys(this.$slots).filter(k => k.startsWith(METADATA_PREFIX));
// we need to wait for next tick to ensure that dynamic names slots are picked up
await this.$nextTick();
this.metadataSlots = Object.keys(this.$slots).filter(k => k.startsWith(METADATA_PREFIX));
}, },
}; };
</script> </script>
......
---
title: 'Package details: on mobile show all the tags'
merge_request: 46679
author:
type: changed
import Vuex from 'vuex'; import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import PackageTitle from '~/packages/details/components/package_title.vue'; import PackageTitle from '~/packages/details/components/package_title.vue';
import PackageTags from '~/packages/shared/components/package_tags.vue'; import PackageTags from '~/packages/shared/components/package_tags.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue'; import TitleArea from '~/vue_shared/components/registry/title_area.vue';
...@@ -53,6 +54,7 @@ describe('PackageTitle', () => { ...@@ -53,6 +54,7 @@ describe('PackageTitle', () => {
const pipelineProject = () => wrapper.find('[data-testid="pipeline-project"]'); const pipelineProject = () => wrapper.find('[data-testid="pipeline-project"]');
const packageRef = () => wrapper.find('[data-testid="package-ref"]'); const packageRef = () => wrapper.find('[data-testid="package-ref"]');
const packageTags = () => wrapper.find(PackageTags); const packageTags = () => wrapper.find(PackageTags);
const packageBadges = () => wrapper.findAll('[data-testid="tag-badge"]');
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -70,6 +72,14 @@ describe('PackageTitle', () => { ...@@ -70,6 +72,14 @@ describe('PackageTitle', () => {
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
}); });
it('with tags on mobile', async () => {
jest.spyOn(GlBreakpointInstance, 'isDesktop').mockReturnValue(false);
await createComponent({ packageEntity: { ...mavenPackage, tags: mockTags } });
await wrapper.vm.$nextTick();
expect(packageBadges()).toHaveLength(mockTags.length);
});
}); });
describe('package title', () => { describe('package title', () => {
......
...@@ -5,12 +5,16 @@ import component from '~/vue_shared/components/registry/title_area.vue'; ...@@ -5,12 +5,16 @@ import component from '~/vue_shared/components/registry/title_area.vue';
describe('title area', () => { describe('title area', () => {
let wrapper; let wrapper;
const DYNAMIC_SLOT = 'metadata-dynamic-slot';
const findSubHeaderSlot = () => wrapper.find('[data-testid="sub-header"]'); const findSubHeaderSlot = () => wrapper.find('[data-testid="sub-header"]');
const findRightActionsSlot = () => wrapper.find('[data-testid="right-actions"]'); const findRightActionsSlot = () => wrapper.find('[data-testid="right-actions"]');
const findMetadataSlot = name => wrapper.find(`[data-testid="${name}"]`); const findMetadataSlot = name => wrapper.find(`[data-testid="${name}"]`);
const findTitle = () => wrapper.find('[data-testid="title"]'); const findTitle = () => wrapper.find('[data-testid="title"]');
const findAvatar = () => wrapper.find(GlAvatar); const findAvatar = () => wrapper.find(GlAvatar);
const findInfoMessages = () => wrapper.findAll('[data-testid="info-message"]'); const findInfoMessages = () => wrapper.findAll('[data-testid="info-message"]');
const findDynamicSlot = () => wrapper.find(`[data-testid="${DYNAMIC_SLOT}`);
const findSlotOrderElements = () => wrapper.findAll('[slot-test]');
const mountComponent = ({ propsData = { title: 'foo' }, slots } = {}) => { const mountComponent = ({ propsData = { title: 'foo' }, slots } = {}) => {
wrapper = shallowMount(component, { wrapper = shallowMount(component, {
...@@ -98,6 +102,59 @@ describe('title area', () => { ...@@ -98,6 +102,59 @@ describe('title area', () => {
}); });
}); });
describe('dynamic slots', () => {
const createDynamicSlot = () => {
return wrapper.vm.$createElement('div', {
attrs: {
'data-testid': DYNAMIC_SLOT,
'slot-test': true,
},
});
};
it('shows dynamic slots', async () => {
mountComponent();
// we manually add a new slot to simulate dynamic slots being evaluated after the initial mount
wrapper.vm.$slots[DYNAMIC_SLOT] = createDynamicSlot();
await wrapper.vm.$nextTick();
expect(findDynamicSlot().exists()).toBe(false);
await wrapper.vm.$nextTick();
expect(findDynamicSlot().exists()).toBe(true);
});
it('preserve the order of the slots', async () => {
mountComponent({
slots: {
'metadata-foo': '<div slot-test data-testid="metadata-foo"></div>',
},
});
// rewrite slot putting dynamic slot as first
wrapper.vm.$slots = {
'metadata-dynamic-slot': createDynamicSlot(),
'metadata-foo': wrapper.vm.$slots['metadata-foo'],
};
await wrapper.vm.$nextTick();
expect(findDynamicSlot().exists()).toBe(false);
expect(findMetadataSlot('metadata-foo').exists()).toBe(true);
await wrapper.vm.$nextTick();
expect(
findSlotOrderElements()
.at(0)
.attributes('data-testid'),
).toBe(DYNAMIC_SLOT);
expect(
findSlotOrderElements()
.at(1)
.attributes('data-testid'),
).toBe('metadata-foo');
});
});
describe('info-messages', () => { describe('info-messages', () => {
it('shows a message when the props contains one', () => { it('shows a message when the props contains one', () => {
mountComponent({ propsData: { infoMessages: [{ text: 'foo foo bar bar' }] } }); mountComponent({ propsData: { infoMessages: [{ text: 'foo foo bar bar' }] } });
......
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