diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue index 3d4eebb9524da48d4ea60a6d71c6367f0d77c3ed..53e976d698b12da309d986d25d3892b9f894f2c5 100644 --- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue +++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue @@ -9,14 +9,16 @@ import { } from '@gitlab/ui'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { STATUS_OPEN } from '~/issues/constants'; +import { issuableStatusText, STATUS_OPEN } from '~/issues/constants'; import { isExternal } from '~/lib/utils/url_utility'; import { n__, sprintf } from '~/locale'; +import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue'; export default { components: { + ConfidentialityBadge, GlIcon, GlBadge, GlButton, @@ -77,8 +79,16 @@ export default { required: false, default: false, }, + workspaceType: { + type: String, + required: false, + default: '', + }, }, computed: { + badgeText() { + return issuableStatusText[this.issuableState]; + }, badgeVariant() { return this.issuableState === STATUS_OPEN ? 'success' : 'info'; }, @@ -109,6 +119,7 @@ export default { }, methods: { handleRightSidebarToggleClick() { + this.$emit('toggle'); if (this.toggleSidebarButtonEl) { this.toggleSidebarButtonEl.dispatchEvent(new Event('click')); } @@ -118,21 +129,23 @@ export default { </script> <template> - <div class="detail-page-header"> + <div class="detail-page-header gl-flex-direction-column gl-sm-flex-direction-row"> <div class="detail-page-header-body"> <gl-badge class="issuable-status-badge gl-mr-3" :variant="badgeVariant"> <gl-icon v-if="statusIcon" :name="statusIcon" :class="statusIconClass" /> - <span class="gl-display-none gl-sm-display-block"><slot name="status-badge"></slot></span> + <span class="gl-display-none gl-sm-display-block gl-ml-2"> + <slot name="status-badge">{{ badgeText }}</slot> + </span> </gl-badge> - <div class="issuable-meta gl-display-flex! gl-align-items-center"> - <div v-if="blocked || confidential" class="gl-display-inline-block"> - <div v-if="blocked" data-testid="blocked" class="issuable-warning-icon inline"> - <gl-icon name="lock" :aria-label="__('Blocked')" /> - </div> - <div v-if="confidential" data-testid="confidential" class="issuable-warning-icon inline"> - <gl-icon name="eye-slash" :aria-label="__('Confidential')" /> - </div> + <div class="issuable-meta gl-display-flex! gl-align-items-center gl-flex-wrap"> + <div v-if="blocked" data-testid="blocked" class="issuable-warning-icon inline"> + <gl-icon name="lock" :aria-label="__('Blocked')" /> </div> + <confidentiality-badge + v-if="confidential" + :issuable-type="issuableType" + :workspace-type="workspaceType" + /> <span> <template v-if="showWorkItemTypeIcon"> <work-item-type-icon :work-item-type="issuableType" show-text /> @@ -182,10 +195,7 @@ export default { @click="handleRightSidebarToggleClick" /> </div> - <div - data-testid="header-actions" - class="detail-page-header-actions gl-display-flex gl-md-display-block" - > + <div data-testid="header-actions" class="detail-page-header-actions gl-display-flex"> <slot name="header-actions"></slot> </div> </div> diff --git a/ee/app/assets/javascripts/epic/components/epic_header.vue b/ee/app/assets/javascripts/epic/components/epic_header.vue index 2399016cb0f4daf1656feead3cff87fed243493f..46dba944b61ca17d81d61d5b071ef6a213209524 100644 --- a/ee/app/assets/javascripts/epic/components/epic_header.vue +++ b/ee/app/assets/javascripts/epic/components/epic_header.vue @@ -1,12 +1,8 @@ <script> -import { GlButton, GlBadge, GlIcon } from '@gitlab/ui'; import { mapState, mapGetters, mapActions } from 'vuex'; import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants'; -import { __ } from '~/locale'; -import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; -import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; -import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue'; import { STATUS_CLOSED, STATUS_OPEN, TYPE_EPIC, WORKSPACE_GROUP } from '~/issues/constants'; +import IssuableHeader from '~/vue_shared/issuable/show/components/issuable_header.vue'; import epicUtils from '../utils/epic_utils'; import EpicHeaderActions from './epic_header_actions.vue'; @@ -15,22 +11,23 @@ export default { WORKSPACE_GROUP, components: { EpicHeaderActions, - GlIcon, - GlBadge, - GlButton, - UserAvatarLink, - TimeagoTooltip, - ConfidentialityBadge, + IssuableHeader, }, computed: { - ...mapState(['sidebarCollapsed', 'author', 'created', 'confidential']), + ...mapState(['sidebarCollapsed', 'author', 'created', 'confidential', 'state']), ...mapGetters(['isEpicOpen']), + formattedAuthor() { + const { src, url, username } = this.author; + return { + ...this.author, + avatarUrl: src, + username: username.startsWith('@') ? username.substring(1) : username, + webUrl: url, + }; + }, statusIcon() { return this.isEpicOpen ? 'epic' : 'epic-closed'; }, - statusText() { - return this.isEpicOpen ? __('Open') : __('Closed'); - }, }, mounted() { /** @@ -56,44 +53,18 @@ export default { </script> <template> - <div class="detail-page-header gl-flex-wrap"> - <div class="detail-page-header-body"> - <gl-badge class="issuable-status-badge gl-mr-3" :variant="isEpicOpen ? 'success' : 'info'"> - <gl-icon :name="statusIcon" /> - <span class="gl-display-none gl-sm-display-block gl-ml-2">{{ statusText }}</span> - </gl-badge> - <div class="issuable-meta"> - <confidentiality-badge - v-if="confidential" - :workspace-type="$options.WORKSPACE_GROUP" - :issuable-type="$options.TYPE_EPIC" - /> - {{ __('Created') }} - <timeago-tooltip :time="created" /> - {{ __('by') }} - <strong class="text-nowrap"> - <user-avatar-link - :link-href="author.url" - :img-src="author.src" - :img-size="24" - :tooltip-text="author.username" - :username="author.name" - img-css-classes="avatar-inline" - /> - </strong> - </div> - </div> - <gl-button - :aria-label="__('Toggle sidebar')" - type="button" - class="float-right gl-display-block gl-sm-display-none! gl-align-self-center gutter-toggle issuable-gutter-toggle" - icon="chevron-double-lg-left" - @click="toggleSidebar({ sidebarCollapsed })" - /> - <div - class="detail-page-header-actions gl-display-flex gl-flex-wrap gl-align-items-center gl-w-full gl-sm-w-auto" - > + <issuable-header + :author="formattedAuthor" + :confidential="confidential" + :created-at="created" + :issuable-state="state" + :issuable-type="$options.TYPE_EPIC" + :status-icon="statusIcon" + :workspace-type="$options.WORKSPACE_GROUP" + @toggle="toggleSidebar({ sidebarCollapsed })" + > + <template #header-actions> <epic-header-actions /> - </div> - </div> + </template> + </issuable-header> </template> diff --git a/ee/app/assets/javascripts/integrations/jira/issues_show/components/jira_issues_show_root.vue b/ee/app/assets/javascripts/integrations/jira/issues_show/components/jira_issues_show_root.vue index 3761bb8f58d2ba3df262cf0d034e0cdd06549d78..2454a82a7f71ba8ee8d1245110f8a24269944b0d 100644 --- a/ee/app/assets/javascripts/integrations/jira/issues_show/components/jira_issues_show_root.vue +++ b/ee/app/assets/javascripts/integrations/jira/issues_show/components/jira_issues_show_root.vue @@ -6,7 +6,7 @@ import ExternalIssueAlert from 'ee/external_issues_show/components/external_issu import { fetchIssue } from 'ee/integrations/jira/issues_show/api'; import JiraIssueSidebar from 'ee/integrations/jira/issues_show/components/sidebar/jira_issues_sidebar_root.vue'; -import { issuableStatusText, STATUS_OPEN } from '~/issues/constants'; +import { STATUS_OPEN } from '~/issues/constants'; import IssuableShow from '~/vue_shared/issuable/show/components/issuable_show_root.vue'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { s__ } from '~/locale'; @@ -41,9 +41,6 @@ export default { isIssueOpen() { return this.issue.state === STATUS_OPEN; }, - statusBadgeText() { - return issuableStatusText[this.issue.state]; - }, statusIcon() { return this.isIssueOpen ? 'issue-open-m' : 'mobile-issue-close'; }, @@ -89,8 +86,6 @@ export default { :status-icon="statusIcon" status-icon-class="gl-sm-display-none" > - <template #status-badge>{{ statusBadgeText }}</template> - <template #right-sidebar-items="{ sidebarExpanded, toggleSidebar }"> <jira-issue-sidebar :sidebar-expanded="sidebarExpanded" diff --git a/ee/app/assets/javascripts/integrations/zentao/issues_show/components/zentao_issues_show_root.vue b/ee/app/assets/javascripts/integrations/zentao/issues_show/components/zentao_issues_show_root.vue index f99cd763f366c74f742b2a43c7abf6f9535ae192..e86e6a8572907474444166fec63d9666c3b1d069 100644 --- a/ee/app/assets/javascripts/integrations/zentao/issues_show/components/zentao_issues_show_root.vue +++ b/ee/app/assets/javascripts/integrations/zentao/issues_show/components/zentao_issues_show_root.vue @@ -5,7 +5,7 @@ import Note from 'ee/external_issues_show/components/note.vue'; import ExternalIssueAlert from 'ee/external_issues_show/components/external_issue_alert.vue'; import { fetchIssue } from 'ee/integrations/zentao/issues_show/api'; import ZentaoIssueSidebar from 'ee/integrations/zentao/issues_show/components/sidebar/zentao_issues_sidebar_root.vue'; -import { issuableStatusText, STATUS_OPEN } from '~/issues/constants'; +import { STATUS_OPEN } from '~/issues/constants'; import IssuableShow from '~/vue_shared/issuable/show/components/issuable_show_root.vue'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; @@ -41,9 +41,6 @@ export default { isIssueOpen() { return this.issue.state === STATUS_OPEN; }, - statusBadgeText() { - return issuableStatusText[this.issue?.state]; - }, statusIcon() { return this.isIssueOpen ? 'issue-open-m' : 'mobile-issue-close'; }, @@ -89,8 +86,6 @@ export default { <external-issue-alert issue-tracker-name="ZenTao" :issue-url="issue.webUrl" /> <issuable-show :issuable="issue" :enable-edit="false" :status-icon="statusIcon"> - <template v-if="statusBadgeText" #status-badge>{{ statusBadgeText }}</template> - <template #right-sidebar-items> <zentao-issue-sidebar :issue="issue" /> </template> diff --git a/ee/spec/features/epics/epic_show_spec.rb b/ee/spec/features/epics/epic_show_spec.rb index 464df416e2da24a5ad110a2aa7c76fb27977cd54..eeba159ab5a7bb766105520ec06a669282627834 100644 --- a/ee/spec/features/epics/epic_show_spec.rb +++ b/ee/spec/features/epics/epic_show_spec.rb @@ -186,10 +186,10 @@ def open_colors_dropdown it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nos commodius agimus. Ex rebus enim timiditas, non ex vocabulis nascitur. Ita prorsus, inquam; Duo...' it 'shows epic status, date and author in header' do - page.within('.epic-page-container .detail-page-header-body') do - expect(find('.issuable-status-badge > span')).to have_content('Open') - expect(find('.issuable-meta')).to have_content('Created') - expect(find('.issuable-meta [data-testid="user-avatar-link-username"]')).to have_content('Rick Sanchez') + within('.detail-page-header-body') do + expect(page).to have_css('.issuable-status-badge', text: 'Open') + expect(page).to have_css('.issuable-meta', text: 'Created') + expect(page).to have_link('Rick Sanchez') end end diff --git a/ee/spec/frontend/epic/components/epic_header_spec.js b/ee/spec/frontend/epic/components/epic_header_spec.js index c3d0ad3316353e930c94b25fc94e5bbbc78c91fe..acea7d9c08be0623bd90ae07fe14554b8cbb93f1 100644 --- a/ee/spec/frontend/epic/components/epic_header_spec.js +++ b/ee/spec/frontend/epic/components/epic_header_spec.js @@ -1,93 +1,38 @@ -import { GlBadge, GlButton, GlIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import EpicHeader from 'ee/epic/components/epic_header.vue'; import EpicHeaderActions from 'ee/epic/components/epic_header_actions.vue'; import createStore from 'ee/epic/store'; -import { STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants'; -import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue'; -import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; -import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; -import { mockEpicMeta, mockEpicData } from '../mock_data'; +import IssuableHeader from '~/vue_shared/issuable/show/components/issuable_header.vue'; +import { mockEpicMeta } from '../mock_data'; describe('EpicHeader component', () => { let wrapper; - const createComponent = (state = {}) => { + const createComponent = () => { const store = createStore(); store.dispatch('setEpicMeta', mockEpicMeta); - store.dispatch('setEpicData', { ...mockEpicData, ...state }); - wrapper = shallowMount(EpicHeader, { store }); }; - const findConfidentialityBadge = () => wrapper.findComponent(ConfidentialityBadge); const findEpicHeaderActions = () => wrapper.findComponent(EpicHeaderActions); - const findStatusBadge = () => wrapper.findComponent(GlBadge); - const findStatusBadgeIcon = () => wrapper.findComponent(GlIcon); - const findTimeagoTooltip = () => wrapper.findComponent(TimeagoTooltip); - const findToggleSidebarButton = () => wrapper.findComponent(GlButton); - const findUserAvatarLink = () => wrapper.findComponent(UserAvatarLink); - - describe('status badge', () => { - describe('when epic is open', () => { - beforeEach(() => { - createComponent({ state: STATUS_OPEN }); - }); - - it('renders `Open` text', () => { - expect(findStatusBadge().text()).toBe('Open'); - }); - - it('renders correct icon', () => { - expect(findStatusBadgeIcon().props('name')).toBe('epic'); - }); - }); - - describe('when epic is closed', () => { - beforeEach(() => { - createComponent({ state: STATUS_CLOSED }); - }); - - it('renders `Closed` text', () => { - expect(findStatusBadge().text()).toBe('Closed'); - }); + const findIssuableHeader = () => wrapper.findComponent(IssuableHeader); - it('renders correct icon', () => { - expect(findStatusBadgeIcon().props('name')).toBe('epic-closed'); - }); - }); + beforeEach(() => { + createComponent(); }); - it('renders correct badge when epic is confidential', () => { - createComponent({ confidential: true }); - - expect(findConfidentialityBadge().props()).toMatchObject({ - workspaceType: 'group', + it('renders IssuableHeader component', () => { + expect(findIssuableHeader().props()).toMatchObject({ + confidential: false, + createdAt: '2015-07-03T10:00:00.000Z', + issuableState: 'opened', issuableType: 'epic', + statusIcon: 'epic', + workspaceType: 'group', }); }); - it('renders timeago tooltip', () => { - createComponent(); - - expect(findTimeagoTooltip().exists()).toBe(true); - }); - - it('renders user avatar link', () => { - createComponent(); - - expect(findUserAvatarLink().exists()).toBe(true); - }); - - it('renders toggle sidebar button', () => { - createComponent(); - - expect(findToggleSidebarButton().attributes('aria-label')).toBe('Toggle sidebar'); - }); - it('renders actions dropdown', () => { - createComponent(); - expect(findEpicHeaderActions().exists()).toBe(true); }); }); diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js index fa38ab8d44d3116390095a11bd16d0e8b7b83b9e..d2b7b2e89c87c6c4f4a78e37feb2fc05e6547f47 100644 --- a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js @@ -1,13 +1,16 @@ import { GlButton, GlBadge, GlIcon, GlAvatarLabeled, GlAvatarLink } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import { TYPE_ISSUE, WORKSPACE_PROJECT } from '~/issues/constants'; +import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue'; import IssuableHeader from '~/vue_shared/issuable/show/components/issuable_header.vue'; - import { mockIssuableShowProps, mockIssuable } from '../mock_data'; const issuableHeaderProps = { ...mockIssuable, ...mockIssuableShowProps, + issuableType: TYPE_ISSUE, + workspaceType: WORKSPACE_PROJECT, }; describe('IssuableHeader', () => { @@ -53,6 +56,14 @@ describe('IssuableHeader', () => { setHTMLFixture('<button class="js-toggle-right-sidebar-button">Collapse sidebar</button>'); }); + it('emits a "toggle" event', () => { + createComponent(); + + findButton().vm.$emit('click'); + + expect(wrapper.emitted('toggle')).toEqual([[]]); + }); + it('dispatches `click` event on sidebar toggle button', () => { createComponent(); const toggleSidebarButtonEl = document.querySelector('.js-toggle-right-sidebar-button'); @@ -94,14 +105,12 @@ describe('IssuableHeader', () => { }); it('renders confidential icon when issuable is confidential', () => { - createComponent({ - confidential: true, - }); + createComponent({ confidential: true }); - const confidentialEl = wrapper.findByTestId('confidential'); - - expect(confidentialEl.exists()).toBe(true); - expect(confidentialEl.findComponent(GlIcon).props('name')).toBe('eye-slash'); + expect(wrapper.findComponent(ConfidentialityBadge).props()).toEqual({ + issuableType: 'issue', + workspaceType: 'project', + }); }); it('renders issuable author avatar', () => {