diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 1bc300eff79d6df061ef598dfe533fdba018b3f5..d67ef01242003dc4d43437c59b8bc80ccab12458 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -4,7 +4,6 @@ import { GlLoadingIcon, GlBadge, GlButton, - GlIcon, GlLabel, GlPopover, GlLink, @@ -13,15 +12,11 @@ import { import SafeHtml from '~/vue_shared/directives/safe_html'; import { visitUrl } from '~/lib/utils/url_utility'; import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue'; +import VisibilityIcon from '~/vue_shared/components/visibility_icon.vue'; import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants'; import { helpPagePath } from '~/helpers/help_page_helper'; import { __ } from '~/locale'; -import { - VISIBILITY_LEVELS_STRING_TO_INTEGER, - VISIBILITY_TYPE_ICON, - GROUP_VISIBILITY_TYPE, - PROJECT_VISIBILITY_TYPE, -} from '~/visibility_level/constants'; +import { VISIBILITY_LEVELS_STRING_TO_INTEGER } from '~/visibility_level/constants'; import { ITEM_TYPE, ACTIVE_TAB_SHARED } from '../constants'; import eventHub from '../event_hub'; @@ -40,7 +35,6 @@ export default { GlBadge, GlButton, GlLoadingIcon, - GlIcon, GlLabel, GlPopover, GlLink, @@ -48,6 +42,7 @@ export default { ItemTypeIcon, ItemActions, ItemStats, + VisibilityIcon, }, inject: { currentGroupVisibility: { @@ -97,13 +92,6 @@ export default { isGroup() { return this.group.type === ITEM_TYPE.GROUP; }, - visibilityIcon() { - return VISIBILITY_TYPE_ICON[this.group.visibility]; - }, - visibilityTooltip() { - if (this.isGroup) return GROUP_VISIBILITY_TYPE[this.group.visibility]; - return PROJECT_VISIBILITY_TYPE[this.group.visibility]; - }, microdata() { return this.group.microdata || {}; }, @@ -226,12 +214,12 @@ export default { <!-- link hover text-decoration from over-extending --> {{ group.name }} </a> - <gl-icon - v-gl-tooltip.bottom - class="gl-display-inline-flex gl-align-items-center gl-mr-3 gl-text-gray-500" - :name="visibilityIcon" - :title="visibilityTooltip" + <visibility-icon + :is-group="isGroup" + :visibility-level="group.visibility" + class="gl-mr-3" data-testid="group-visibility-icon" + tooltip-placement="bottom" /> <template v-if="shouldShowVisibilityWarning"> <gl-button diff --git a/app/assets/javascripts/vue_shared/components/visibility_icon.vue b/app/assets/javascripts/vue_shared/components/visibility_icon.vue new file mode 100644 index 0000000000000000000000000000000000000000..a05651be1ca7dbc6729eef07954fabbbbab653a4 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/visibility_icon.vue @@ -0,0 +1,55 @@ +<script> +import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; +import { + GROUP_VISIBILITY_TYPE, + PROJECT_VISIBILITY_TYPE, + VISIBILITY_TYPE_ICON, +} from '~/visibility_level/constants'; + +export default { + components: { + GlIcon, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + isGroup: { + default: false, + required: false, + type: Boolean, + }, + tooltipPlacement: { + default: 'top', + required: false, + type: String, + }, + visibilityLevel: { + required: true, + type: String, + }, + }, + computed: { + visibilityIcon() { + return VISIBILITY_TYPE_ICON[this.visibilityLevel]; + }, + visibilityTooltip() { + if (this.isGroup) { + return GROUP_VISIBILITY_TYPE[this.visibilityLevel]; + } + + return PROJECT_VISIBILITY_TYPE[this.visibilityLevel]; + }, + }, +}; +</script> + +<template> + <gl-icon + v-gl-tooltip="{ placement: tooltipPlacement }" + :aria-label="visibilityTooltip" + :name="visibilityIcon" + :title="visibilityTooltip" + class="gl-display-inline-flex gl-text-secondary" + /> +</template> diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js index 454f691f5b94718caa591feb0db91906b09184c6..9a7b740f2efe2f40a63e51631dfc11dd746ef289 100644 --- a/spec/frontend/groups/components/group_item_spec.js +++ b/spec/frontend/groups/components/group_item_spec.js @@ -3,6 +3,7 @@ import waitForPromises from 'helpers/wait_for_promises'; import GroupFolder from '~/groups/components/group_folder.vue'; import GroupItem from 'jh_else_ce/groups/components/group_item.vue'; import ItemActions from '~/groups/components/item_actions.vue'; +import VisibilityIcon from '~/vue_shared/components/visibility_icon.vue'; import eventHub from '~/groups/event_hub'; import { getGroupItemMicrodata } from '~/groups/store/utils'; import * as urlUtilities from '~/lib/utils/url_utility'; @@ -11,8 +12,6 @@ import { VISIBILITY_LEVEL_PRIVATE_STRING, VISIBILITY_LEVEL_INTERNAL_STRING, VISIBILITY_LEVEL_PUBLIC_STRING, - GROUP_VISIBILITY_TYPE, - PROJECT_VISIBILITY_TYPE, } from '~/visibility_level/constants'; import { helpPagePath } from '~/helpers/help_page_helper'; import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper'; @@ -117,49 +116,39 @@ describe('GroupItemComponent', () => { wrapper.destroy(); }); }); + }); - describe('visibilityTooltip', () => { - describe('if item represents group', () => { - it.each` - visibilityLevel | visibilityTooltip - ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${GROUP_VISIBILITY_TYPE[VISIBILITY_LEVEL_PUBLIC_STRING]} - ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${GROUP_VISIBILITY_TYPE[VISIBILITY_LEVEL_INTERNAL_STRING]} - ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${GROUP_VISIBILITY_TYPE[VISIBILITY_LEVEL_PRIVATE_STRING]} - `( - 'should return corresponding text when visibility level is $visibilityLevel', - ({ visibilityLevel, visibilityTooltip }) => { - const group = { ...mockParentGroupItem }; - - group.type = 'group'; - group.visibility = visibilityLevel; - wrapper = createComponent({ group }); - - expect(wrapper.vm.visibilityTooltip).toBe(visibilityTooltip); - wrapper.destroy(); - }, - ); + describe('visibilityIcon', () => { + describe('if item represents a group', () => { + it('should display the visibility icon with appropriate props', () => { + const group = { ...mockParentGroupItem }; + + group.type = 'group'; + group.visibility = VISIBILITY_LEVEL_PRIVATE_STRING; + wrapper = createComponent({ group }); + + expect(wrapper.findComponent(VisibilityIcon).props()).toEqual({ + isGroup: true, + tooltipPlacement: 'bottom', + visibilityLevel: VISIBILITY_LEVEL_PRIVATE_STRING, + }); }); + }); - describe('if item represents project', () => { - it.each` - visibilityLevel | visibilityTooltip - ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${PROJECT_VISIBILITY_TYPE[VISIBILITY_LEVEL_PUBLIC_STRING]} - ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${PROJECT_VISIBILITY_TYPE[VISIBILITY_LEVEL_INTERNAL_STRING]} - ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${PROJECT_VISIBILITY_TYPE[VISIBILITY_LEVEL_PRIVATE_STRING]} - `( - 'should return corresponding text when visibility level is $visibilityLevel', - ({ visibilityLevel, visibilityTooltip }) => { - const group = { ...mockParentGroupItem }; - - group.type = 'project'; - group.lastActivityAt = '2017-04-09T18:40:39.101Z'; - group.visibility = visibilityLevel; - wrapper = createComponent({ group }); - - expect(wrapper.vm.visibilityTooltip).toBe(visibilityTooltip); - wrapper.destroy(); - }, - ); + describe('if item represents a project', () => { + it('should display the visibility icon with appropriate props', () => { + const group = { ...mockParentGroupItem }; + + group.type = 'project'; + group.lastActivityAt = '2017-04-09T18:40:39.101Z'; + group.visibility = VISIBILITY_LEVEL_PRIVATE_STRING; + wrapper = createComponent({ group }); + + expect(wrapper.findComponent(VisibilityIcon).props()).toEqual({ + isGroup: false, + tooltipPlacement: 'bottom', + visibilityLevel: VISIBILITY_LEVEL_PRIVATE_STRING, + }); }); }); }); @@ -279,7 +268,6 @@ describe('GroupItemComponent', () => { expect(vm.$el.querySelector('.title a.no-expand')).toBeDefined(); expect(visibilityIconEl).not.toBe(null); - expect(visibilityIconEl.getAttribute('title')).toBe(vm.visibilityTooltip); expect(vm.$el.querySelector('.access-type')).toBeDefined(); expect(vm.$el.querySelector('.description')).toBeDefined(); diff --git a/spec/frontend/vue_shared/components/visibility_icon_spec.js b/spec/frontend/vue_shared/components/visibility_icon_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..deff7556f601421fd252eca37eb121917264fc92 --- /dev/null +++ b/spec/frontend/vue_shared/components/visibility_icon_spec.js @@ -0,0 +1,81 @@ +import { GlIcon } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import VisibilityIcon from '~/vue_shared/components/visibility_icon.vue'; +import { + GROUP_VISIBILITY_TYPE, + PROJECT_VISIBILITY_TYPE, + VISIBILITY_LEVEL_INTERNAL_STRING, + VISIBILITY_LEVEL_PRIVATE_STRING, + VISIBILITY_LEVEL_PUBLIC_STRING, + VISIBILITY_TYPE_ICON, +} from '~/visibility_level/constants'; + +describe('Visibility icon', () => { + let glTooltipDirectiveMock; + let wrapper; + + const createComponent = (props = {}) => { + glTooltipDirectiveMock = jest.fn(); + + wrapper = shallowMountExtended(VisibilityIcon, { + directives: { + GlTooltip: glTooltipDirectiveMock, + }, + propsData: { + ...props, + }, + }); + }; + + const findIcon = () => wrapper.findComponent(GlIcon); + + describe('visibilityTooltip', () => { + describe('if item represents group', () => { + it.each` + visibilityLevel | visibilityTooltip | visibilityIcon | tooltipPlacement + ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${GROUP_VISIBILITY_TYPE[VISIBILITY_LEVEL_PUBLIC_STRING]} | ${VISIBILITY_TYPE_ICON[VISIBILITY_LEVEL_PUBLIC_STRING]} | ${'top'} + ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${GROUP_VISIBILITY_TYPE[VISIBILITY_LEVEL_INTERNAL_STRING]} | ${VISIBILITY_TYPE_ICON[VISIBILITY_LEVEL_INTERNAL_STRING]} | ${'bottom'} + ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${GROUP_VISIBILITY_TYPE[VISIBILITY_LEVEL_PRIVATE_STRING]} | ${VISIBILITY_TYPE_ICON[VISIBILITY_LEVEL_PRIVATE_STRING]} | ${'right'} + `( + 'should return corresponding text when visibility level is $visibilityLevel', + ({ visibilityLevel, visibilityTooltip, visibilityIcon, tooltipPlacement }) => { + createComponent({ isGroup: true, visibilityLevel, tooltipPlacement }); + + expect(findIcon().attributes()).toMatchObject({ + arialabel: visibilityTooltip, + name: visibilityIcon, + title: visibilityTooltip, + }); + + expect(glTooltipDirectiveMock.mock.calls[0][1].value).toEqual({ + placement: tooltipPlacement, + }); + }, + ); + }); + + describe('if item represents project', () => { + it.each` + visibilityLevel | visibilityTooltip | visibilityIcon | tooltipPlacement + ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${PROJECT_VISIBILITY_TYPE[VISIBILITY_LEVEL_PUBLIC_STRING]} | ${VISIBILITY_TYPE_ICON[VISIBILITY_LEVEL_PUBLIC_STRING]} | ${'top'} + ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${PROJECT_VISIBILITY_TYPE[VISIBILITY_LEVEL_INTERNAL_STRING]} | ${VISIBILITY_TYPE_ICON[VISIBILITY_LEVEL_INTERNAL_STRING]} | ${'bottom'} + ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${PROJECT_VISIBILITY_TYPE[VISIBILITY_LEVEL_PRIVATE_STRING]} | ${VISIBILITY_TYPE_ICON[VISIBILITY_LEVEL_PRIVATE_STRING]} | ${'left'} + `( + 'should return corresponding text when visibility level is $visibilityLevel', + ({ visibilityLevel, visibilityTooltip, visibilityIcon, tooltipPlacement }) => { + createComponent({ visibilityLevel, tooltipPlacement }); + + expect(findIcon().attributes()).toMatchObject({ + arialabel: visibilityTooltip, + name: visibilityIcon, + title: visibilityTooltip, + }); + + expect(glTooltipDirectiveMock.mock.calls[0][1].value).toEqual({ + placement: tooltipPlacement, + }); + }, + ); + }); + }); +});