diff --git a/ee/app/assets/javascripts/linked_resources/components/resource_links_block.vue b/ee/app/assets/javascripts/linked_resources/components/resource_links_block.vue index 92145addf707061379b5f32eaa1061bd799f6a4b..185b5395444e384af34eb11d1392752bb36378de 100644 --- a/ee/app/assets/javascripts/linked_resources/components/resource_links_block.vue +++ b/ee/app/assets/javascripts/linked_resources/components/resource_links_block.vue @@ -4,7 +4,7 @@ import { produce } from 'immer'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import { TYPE_ISSUE } from '~/graphql_shared/constants'; import { createAlert } from '~/flash'; -import { sprintf } from '~/locale'; +import { __, sprintf } from '~/locale'; import { resourceLinksI18n } from '../constants'; import { displayAndLogError, identifyLinkType } from './utils'; import getIssuableResourceLinks from './graphql/queries/get_issuable_resource_links.query.graphql'; @@ -45,6 +45,7 @@ export default { }, data() { return { + isOpen: true, isFormVisible: false, isSubmitting: false, resourceLinks: [], @@ -70,19 +71,29 @@ export default { badgeLabel() { return this.isFetching && this.resourceLinks.length === 0 ? '...' : this.resourceLinks.length; }, - hasBody() { - return this.isFormVisible; - }, hasResourceLinks() { return Boolean(this.resourceLinks.length); }, isFetching() { return this.$apollo.queries.resourceLinks.loading; }, + toggleIcon() { + return this.isOpen ? 'chevron-lg-up' : 'chevron-lg-down'; + }, + toggleLabel() { + return this.isOpen ? __('Collapse') : __('Expand'); + }, }, methods: { + handleToggle() { + this.isOpen = !this.isOpen; + if (!this.isOpen) { + this.isFormVisible = false; + } + }, async toggleResourceLinkForm() { this.isFormVisible = !this.isFormVisible; + this.isOpen = true; }, hideResourceLinkForm() { this.isFormVisible = false; @@ -205,12 +216,10 @@ export default { <div id="resource-links" class="gl-mt-5"> <div class="card card-slim gl-overflow-hidden"> <div - :class="{ 'panel-empty-heading border-bottom-0': !hasBody }" - class="card-header gl-display-flex gl-justify-content-space-between gl-bg-gray-10" + :class="{ 'panel-empty-heading border-bottom-0': !isFormVisible, 'gl-border-b-1': !isOpen }" + class="card-header gl-display-flex gl-justify-content-space-between gl-bg-gray-10 gl-align-items-center gl-line-height-24 gl-py-3" > - <h3 - class="card-title h5 position-relative gl-my-0 gl-display-flex gl-align-items-center gl-h-7" - > + <h3 class="card-title h5 position-relative gl-my-0 gl-display-flex gl-align-items-center"> <gl-link id="user-content-resource-links" class="anchor position-absolute gl-text-decoration-none" @@ -235,17 +244,34 @@ export default { {{ badgeLabel }} </span> </div> - <gl-button - v-if="canAddResourceLinks" - icon="plus" - :aria-label="$options.i18n.addButtonText" - @click="toggleResourceLinkForm" - /> </div> </h3> + <slot name="header-actions"></slot> + <gl-button + v-if="canAddResourceLinks" + size="small" + :aria-label="$options.i18n.addButtonText" + class="gl-ml-auto" + data-testid="add-resource-links" + @click="toggleResourceLinkForm" + > + <slot name="add-button-text">{{ __('Add') }}</slot> + </gl-button> + <div class="gl-pl-3 gl-ml-3 gl-border-l-1 gl-border-l-solid gl-border-l-gray-100"> + <gl-button + category="tertiary" + size="small" + :icon="toggleIcon" + :aria-label="toggleLabel" + :disabled="!hasResourceLinks" + data-testid="toggle-links" + @click="handleToggle" + /> + </div> </div> <div - class="bg-gray-light" + v-if="isOpen" + class="gl-bg-gray-10" :class="{ 'linked-issues-card-body gl-p-5': isFormVisible, }" diff --git a/ee/spec/frontend/issuable/linked_resources/components/resource_links_block_spec.js b/ee/spec/frontend/issuable/linked_resources/components/resource_links_block_spec.js index f022ad95459bc764a98267cf47587f30b3d28ef4..ab6c211001ab1aa9763d73de97c6063550ed5861 100644 --- a/ee/spec/frontend/issuable/linked_resources/components/resource_links_block_spec.js +++ b/ee/spec/frontend/issuable/linked_resources/components/resource_links_block_spec.js @@ -1,9 +1,9 @@ -import { GlButton, GlLoadingIcon } from '@gitlab/ui'; +import { GlLoadingIcon } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; import Vue, { nextTick } from 'vue'; import { shallowMount } from '@vue/test-utils'; import waitForPromises from 'helpers/wait_for_promises'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; import ResourceLinksBlock from 'ee/linked_resources/components/resource_links_block.vue'; import ResourceLinksList from 'ee/linked_resources/components/resource_links_list.vue'; import ResourceLinkItem from 'ee/linked_resources/components/resource_links_list_item.vue'; @@ -60,7 +60,7 @@ function createMockApolloCreateProvider() { describe('ResourceLinksBlock', () => { let wrapper; - const findResourceLinkAddButton = () => wrapper.findComponent(GlButton); + const findResourceLinkAddButton = () => wrapper.findByTestId('add-resource-links'); const resourceLinkForm = () => wrapper.findComponent(AddIssuableResourceLinkForm); const helpPath = '/help/user/project/issues/linked_resources'; const issuableId = 1; @@ -141,7 +141,7 @@ describe('ResourceLinksBlock', () => { describe('with canAddResourceLinks=false', () => { it('does not show the add button', () => { - wrapper = shallowMount(ResourceLinksBlock, { + wrapper = shallowMountExtended(ResourceLinksBlock, { propsData: { issuableId, canAddResourceLinks: false,