diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index 4222ffe42fe739fd3b7b5d637e13512b11fe0a6d..842c110e896c23b5941f724e2746e677b5b426a6 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -6,6 +6,7 @@ import workItemTitleSubscription from '../graphql/work_item_title.subscription.g import WorkItemActions from './work_item_actions.vue'; import WorkItemState from './work_item_state.vue'; import WorkItemTitle from './work_item_title.vue'; +import WorkItemLinks from './work_item_links/work_item_links.vue'; export default { i18n, @@ -15,6 +16,7 @@ export default { WorkItemActions, WorkItemTitle, WorkItemState, + WorkItemLinks, }, props: { workItemId: { @@ -105,6 +107,7 @@ export default { @error="error = $event" @updated="$emit('workItemUpdated')" /> + <work-item-links :work-item-id="workItem.id" /> </template> </section> </template> diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue new file mode 100644 index 0000000000000000000000000000000000000000..6124e669cb32af6bb1e0a7f1eca47ed0dbc6c767 --- /dev/null +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue @@ -0,0 +1,86 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import WorkItemLinksForm from './work_item_links_form.vue'; + +export default { + components: { + GlButton, + WorkItemLinksForm, + }, + props: { + workItemId: { + type: String, + required: false, + default: null, + }, + }, + data() { + return { + isShownAddForm: false, + isOpen: false, + children: [], + }; + }, + computed: { + // Only used for children for now but should be extended later to support parents and siblings + isChildrenEmpty() { + return this.children.length === 0; + }, + toggleIcon() { + return this.isOpen ? 'angle-up' : 'angle-down'; + }, + }, + methods: { + toggle() { + this.isOpen = !this.isOpen; + }, + toggleAddForm() { + this.isShownAddForm = !this.isShownAddForm; + }, + }, + i18n: { + title: s__('WorkItem|Child items'), + emptyStateMessage: s__( + 'WorkItem|No child items are currently assigned. Use child items to prioritize tasks that your team should complete in order to accomplish your goals!', + ), + addChildButtonLabel: s__('WorkItem|Add a child'), + }, +}; +</script> + +<template> + <div class="gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100"> + <div + class="gl-p-4 gl-display-flex gl-justify-content-space-between" + :class="{ 'gl-border-b-1 gl-border-b-solid gl-border-b-gray-100': isOpen }" + > + <h5 class="gl-m-0 gl-line-height-32">{{ $options.i18n.title }}</h5> + <div class="gl-border-l-1 gl-border-l-solid gl-border-l-gray-50 gl-pl-4"> + <gl-button + category="tertiary" + :icon="toggleIcon" + data-testid="toggle-links" + @click="toggle" + /> + </div> + </div> + <div v-if="isOpen" class="gl-bg-gray-10 gl-p-4" data-testid="links-body"> + <div v-if="isChildrenEmpty" class="gl-px-8" data-testid="links-empty"> + <p> + {{ $options.i18n.emptyStateMessage }} + </p> + <gl-button + v-if="!isShownAddForm" + category="secondary" + variant="confirm" + data-testid="toggle-add-form" + @click="toggleAddForm" + > + {{ $options.i18n.addChildButtonLabel }} + </gl-button> + <work-item-links-form v-else data-testid="add-links-form" @cancel="toggleAddForm" /> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue new file mode 100644 index 0000000000000000000000000000000000000000..22728f580261e2bfa4c6b8e4cbafdcee141e7004 --- /dev/null +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue @@ -0,0 +1,28 @@ +<script> +import { GlForm, GlFormInput, GlButton } from '@gitlab/ui'; + +export default { + components: { + GlForm, + GlFormInput, + GlButton, + }, + data() { + return { + relatedWorkItem: '', + }; + }, +}; +</script> + +<template> + <gl-form @submit.prevent> + <gl-form-input v-model="relatedWorkItem" class="gl-mb-4" /> + <gl-button type="submit" category="secondary" variant="confirm"> + {{ s__('WorkItem|Add') }} + </gl-button> + <gl-button category="tertiary" class="gl-float-right" @click="$emit('cancel')"> + {{ s__('WorkItem|Cancel') }} + </gl-button> + </gl-form> +</template> diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 796c77a601ab8b0081dcdd5eba3d5c8a5154d2d7..f3c24b16f1e30a32a6dc1a206cfa1829df7b9586 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -43092,9 +43092,21 @@ msgstr "" msgid "Work in progress Limit" msgstr "" +msgid "WorkItem|Add" +msgstr "" + +msgid "WorkItem|Add a child" +msgstr "" + msgid "WorkItem|Are you sure you want to delete the work item? This action cannot be reversed." msgstr "" +msgid "WorkItem|Cancel" +msgstr "" + +msgid "WorkItem|Child items" +msgstr "" + msgid "WorkItem|Convert to work item" msgstr "" @@ -43107,6 +43119,9 @@ msgstr "" msgid "WorkItem|New Task" msgstr "" +msgid "WorkItem|No child items are currently assigned. Use child items to prioritize tasks that your team should complete in order to accomplish your goals!" +msgstr "" + msgid "WorkItem|Select type" msgstr "" diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..ee5937ab7e714ed9d5b8a5b79cd4d6aa2e57dfd9 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js @@ -0,0 +1,65 @@ +import { nextTick } from 'vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue'; + +describe('WorkItemLinks', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMountExtended(WorkItemLinks, { propsData: { workItemId: '123' } }); + }; + + const findToggleButton = () => wrapper.findByTestId('toggle-links'); + const findLinksBody = () => wrapper.findByTestId('links-body'); + const findEmptyState = () => wrapper.findByTestId('links-empty'); + const findToggleAddFormButton = () => wrapper.findByTestId('toggle-add-form'); + const findAddLinksForm = () => wrapper.findByTestId('add-links-form'); + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('is collapsed by default', () => { + expect(findToggleButton().props('icon')).toBe('angle-down'); + expect(findLinksBody().exists()).toBe(false); + }); + + it('expands on click toggle button', async () => { + findToggleButton().vm.$emit('click'); + await nextTick(); + + expect(findToggleButton().props('icon')).toBe('angle-up'); + expect(findLinksBody().exists()).toBe(true); + }); + + it('displays empty state if there are no links', async () => { + findToggleButton().vm.$emit('click'); + await nextTick(); + + expect(findEmptyState().exists()).toBe(true); + expect(findToggleAddFormButton().exists()).toBe(true); + }); + + describe('add link form', () => { + it('displays form on click add button and hides form on cancel', async () => { + findToggleButton().vm.$emit('click'); + await nextTick(); + + expect(findEmptyState().exists()).toBe(true); + + findToggleAddFormButton().vm.$emit('click'); + await nextTick(); + + expect(findAddLinksForm().exists()).toBe(true); + + findAddLinksForm().vm.$emit('cancel'); + await nextTick(); + + expect(findAddLinksForm().exists()).toBe(false); + }); + }); +});