Skip to content
代码片段 群组 项目
提交 dcbccbe3 编辑于 作者: Olena Horal-Koretska's avatar Olena Horal-Koretska
浏览文件

Merge branch '378679-overview-tab-personal-projects-add-topics' into 'master'

No related branches found
No related tags found
无相关合并请求
...@@ -11,6 +11,7 @@ export default { ...@@ -11,6 +11,7 @@ export default {
* id: number | string; * id: number | string;
* name: string; * name: string;
* webUrl: string; * webUrl: string;
* topics: string[];
* forksCount?: number; * forksCount?: number;
* avatarUrl: string | null; * avatarUrl: string | null;
* starCount: number; * starCount: number;
......
<script> <script>
import { GlAvatarLabeled, GlIcon, GlLink, GlBadge, GlTooltipDirective } from '@gitlab/ui'; import {
GlAvatarLabeled,
GlIcon,
GlLink,
GlBadge,
GlTooltipDirective,
GlPopover,
GlSprintf,
} from '@gitlab/ui';
import uniqueId from 'lodash/uniqueId';
import { VISIBILITY_TYPE_ICON, PROJECT_VISIBILITY_TYPE } from '~/visibility_level/constants'; import { VISIBILITY_TYPE_ICON, PROJECT_VISIBILITY_TYPE } from '~/visibility_level/constants';
import { ACCESS_LEVEL_LABELS } from '~/access_level/constants'; import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
...@@ -7,6 +16,10 @@ import { FEATURABLE_ENABLED } from '~/featurable/constants'; ...@@ -7,6 +16,10 @@ import { FEATURABLE_ENABLED } from '~/featurable/constants';
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue'; import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { numberToMetricPrefix } from '~/lib/utils/number_utils'; import { numberToMetricPrefix } from '~/lib/utils/number_utils';
import { truncate } from '~/lib/utils/text_utility';
const MAX_TOPICS_TO_SHOW = 3;
const MAX_TOPIC_TITLE_LENGTH = 15;
export default { export default {
i18n: { i18n: {
...@@ -14,6 +27,9 @@ export default { ...@@ -14,6 +27,9 @@ export default {
forks: __('Forks'), forks: __('Forks'),
issues: __('Issues'), issues: __('Issues'),
archived: __('Archived'), archived: __('Archived'),
topics: __('Topics'),
topicsPopoverTargetText: __('+ %{count} more'),
moreTopics: __('More topics'),
}, },
components: { components: {
GlAvatarLabeled, GlAvatarLabeled,
...@@ -21,6 +37,8 @@ export default { ...@@ -21,6 +37,8 @@ export default {
UserAccessRoleBadge, UserAccessRoleBadge,
GlLink, GlLink,
GlBadge, GlBadge,
GlPopover,
GlSprintf,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -33,6 +51,7 @@ export default { ...@@ -33,6 +51,7 @@ export default {
* id: number | string; * id: number | string;
* name: string; * name: string;
* webUrl: string; * webUrl: string;
* topics: string[];
* forksCount?: number; * forksCount?: number;
* avatarUrl: string | null; * avatarUrl: string | null;
* starCount: number; * starCount: number;
...@@ -49,6 +68,11 @@ export default { ...@@ -49,6 +68,11 @@ export default {
required: true, required: true,
}, },
}, },
data() {
return {
topicsPopoverTarget: uniqueId('project-topics-popover-'),
};
},
computed: { computed: {
visibilityIcon() { visibilityIcon() {
return VISIBILITY_TYPE_ICON[this.project.visibility]; return VISIBILITY_TYPE_ICON[this.project.visibility];
...@@ -83,9 +107,32 @@ export default { ...@@ -83,9 +107,32 @@ export default {
isIssuesEnabled() { isIssuesEnabled() {
return this.project.issuesAccessLevel === FEATURABLE_ENABLED; return this.project.issuesAccessLevel === FEATURABLE_ENABLED;
}, },
hasTopics() {
return this.project.topics.length;
},
visibleTopics() {
return this.project.topics.slice(0, MAX_TOPICS_TO_SHOW);
},
popoverTopics() {
return this.project.topics.slice(MAX_TOPICS_TO_SHOW);
},
}, },
methods: { methods: {
numberToMetricPrefix, numberToMetricPrefix,
topicPath(topic) {
return `/explore/projects/topics/${encodeURIComponent(topic)}`;
},
topicTitle(topic) {
return truncate(topic, MAX_TOPIC_TITLE_LENGTH);
},
topicTooltipTitle(topic) {
// Matches conditional in app/assets/javascripts/lib/utils/text_utility.js#L88
if (topic.length - 1 > MAX_TOPIC_TITLE_LENGTH) {
return topic;
}
return null;
},
}, },
}; };
</script> </script>
...@@ -111,6 +158,43 @@ export default { ...@@ -111,6 +158,43 @@ export default {
accessLevelLabel accessLevelLabel
}}</user-access-role-badge> }}</user-access-role-badge>
</template> </template>
<div v-if="hasTopics" class="gl-mt-3" data-testid="project-topics">
<div
class="gl-w-full gl-display-inline-flex gl-flex-wrap gl-font-base gl-font-weight-normal gl-align-items-center gl-mx-n2 gl-my-n2"
>
<span class="gl-p-2 gl-text-secondary">{{ $options.i18n.topics }}:</span>
<div v-for="topic in visibleTopics" :key="topic" class="gl-p-2">
<gl-badge v-gl-tooltip="topicTooltipTitle(topic)" :href="topicPath(topic)">
{{ topicTitle(topic) }}
</gl-badge>
</div>
<template v-if="popoverTopics.length">
<div
:id="topicsPopoverTarget"
class="gl-p-2 gl-text-secondary"
role="button"
tabindex="0"
>
<gl-sprintf :message="$options.i18n.topicsPopoverTargetText">
<template #count>{{ popoverTopics.length }}</template>
</gl-sprintf>
</div>
<gl-popover :target="topicsPopoverTarget" :title="$options.i18n.moreTopics">
<div class="gl-font-base gl-font-weight-normal gl-mx-n2 gl-my-n2">
<div
v-for="topic in popoverTopics"
:key="topic"
class="gl-p-2 gl-display-inline-block"
>
<gl-badge v-gl-tooltip="topicTooltipTitle(topic)" :href="topicPath(topic)">
{{ topicTitle(topic) }}
</gl-badge>
</div>
</div>
</gl-popover>
</template>
</div>
</div>
</gl-avatar-labeled> </gl-avatar-labeled>
<div <div
class="gl-md-display-flex gl-flex-direction-column gl-align-items-flex-end gl-flex-shrink-0 gl-mt-3 gl-md-mt-0" class="gl-md-display-flex gl-flex-direction-column gl-align-items-flex-end gl-flex-shrink-0 gl-mt-3 gl-md-mt-0"
......
import { GlAvatarLabeled, GlBadge, GlIcon } from '@gitlab/ui'; import { GlAvatarLabeled, GlBadge, GlIcon, GlPopover } from '@gitlab/ui';
import projects from 'test_fixtures/api/users/projects/get.json'; import projects from 'test_fixtures/api/users/projects/get.json';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import ProjectsListItem from '~/vue_shared/components/projects_list/projects_list_item.vue'; import ProjectsListItem from '~/vue_shared/components/projects_list/projects_list_item.vue';
...@@ -13,6 +13,8 @@ import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge. ...@@ -13,6 +13,8 @@ import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.
import { ACCESS_LEVEL_LABELS } from '~/access_level/constants'; import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
import { FEATURABLE_DISABLED, FEATURABLE_ENABLED } from '~/featurable/constants'; import { FEATURABLE_DISABLED, FEATURABLE_ENABLED } from '~/featurable/constants';
jest.mock('lodash/uniqueId', () => (prefix) => `${prefix}1`);
describe('ProjectsListItem', () => { describe('ProjectsListItem', () => {
let wrapper; let wrapper;
...@@ -32,6 +34,8 @@ describe('ProjectsListItem', () => { ...@@ -32,6 +34,8 @@ describe('ProjectsListItem', () => {
const findAvatarLabeled = () => wrapper.findComponent(GlAvatarLabeled); const findAvatarLabeled = () => wrapper.findComponent(GlAvatarLabeled);
const findIssuesLink = () => wrapper.findByRole('link', { name: ProjectsListItem.i18n.issues }); const findIssuesLink = () => wrapper.findByRole('link', { name: ProjectsListItem.i18n.issues });
const findForksLink = () => wrapper.findByRole('link', { name: ProjectsListItem.i18n.forks }); const findForksLink = () => wrapper.findByRole('link', { name: ProjectsListItem.i18n.forks });
const findProjectTopics = () => wrapper.findByTestId('project-topics');
const findPopover = () => findProjectTopics().findComponent(GlPopover);
it('renders project avatar', () => { it('renders project avatar', () => {
createComponent(); createComponent();
...@@ -166,4 +170,64 @@ describe('ProjectsListItem', () => { ...@@ -166,4 +170,64 @@ describe('ProjectsListItem', () => {
expect(findForksLink().exists()).toBe(false); expect(findForksLink().exists()).toBe(false);
}); });
}); });
describe('if project has topics', () => {
it('renders first three topics', () => {
createComponent();
const firstThreeTopics = project.topics.slice(0, 3);
const firstThreeBadges = findProjectTopics().findAllComponents(GlBadge).wrappers.slice(0, 3);
const firstThreeBadgesText = firstThreeBadges.map((badge) => badge.text());
const firstThreeBadgesHref = firstThreeBadges.map((badge) => badge.attributes('href'));
expect(firstThreeTopics).toEqual(firstThreeBadgesText);
expect(firstThreeBadgesHref).toEqual(
firstThreeTopics.map((topic) => `/explore/projects/topics/${encodeURIComponent(topic)}`),
);
});
it('renders the rest of the topics in a popover', () => {
createComponent();
const topics = project.topics.slice(3);
const badges = findPopover().findAllComponents(GlBadge).wrappers;
const badgesText = badges.map((badge) => badge.text());
const badgesHref = badges.map((badge) => badge.attributes('href'));
expect(topics).toEqual(badgesText);
expect(badgesHref).toEqual(
topics.map((topic) => `/explore/projects/topics/${encodeURIComponent(topic)}`),
);
});
it('renders button to open popover', () => {
createComponent();
const expectedButtonId = 'project-topics-popover-1';
expect(wrapper.findByText('+ 2 more').attributes('id')).toBe(expectedButtonId);
expect(findPopover().props('target')).toBe(expectedButtonId);
});
describe('when topic has a name longer than 15 characters', () => {
it('truncates name and shows tooltip with full name', () => {
const topicWithLongName = 'topic with very very very long name';
createComponent({
propsData: {
project: {
...project,
topics: [topicWithLongName, ...project.topics],
},
},
});
const firstTopicBadge = findProjectTopics().findComponent(GlBadge);
const tooltip = getBinding(firstTopicBadge.element, 'gl-tooltip');
expect(firstTopicBadge.text()).toBe('topic with ver…');
expect(tooltip.value).toBe(topicWithLongName);
});
});
});
}); });
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册