296 lines
8.4 KiB
Vue
296 lines
8.4 KiB
Vue
<script>
|
|
import { GlLink, GlIcon, GlLoadingIcon, GlButton, GlCard } from '@gitlab/ui';
|
|
import { __, sprintf } from '~/locale';
|
|
import {
|
|
issuableIconMap,
|
|
linkedIssueTypesMap,
|
|
linkedIssueTypesTextMap,
|
|
issuablesBlockHeaderTextMap,
|
|
issuablesBlockHelpTextMap,
|
|
issuablesBlockAddButtonTextMap,
|
|
} from '../constants';
|
|
import AddIssuableForm from './add_issuable_form.vue';
|
|
import RelatedIssuesList from './related_issues_list.vue';
|
|
|
|
export default {
|
|
name: 'RelatedIssuesBlock',
|
|
components: {
|
|
GlLink,
|
|
GlIcon,
|
|
GlLoadingIcon,
|
|
GlButton,
|
|
GlCard,
|
|
AddIssuableForm,
|
|
RelatedIssuesList,
|
|
},
|
|
props: {
|
|
isFetching: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false,
|
|
},
|
|
isSubmitting: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false,
|
|
},
|
|
relatedIssues: {
|
|
type: Array,
|
|
required: false,
|
|
default: () => [],
|
|
},
|
|
canAdmin: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false,
|
|
},
|
|
canReorder: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false,
|
|
},
|
|
isFormVisible: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false,
|
|
},
|
|
pendingReferences: {
|
|
type: Array,
|
|
required: false,
|
|
default: () => [],
|
|
},
|
|
inputValue: {
|
|
type: String,
|
|
required: false,
|
|
default: '',
|
|
},
|
|
pathIdSeparator: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
helpPath: {
|
|
type: String,
|
|
required: false,
|
|
default: '',
|
|
},
|
|
autoCompleteSources: {
|
|
type: Object,
|
|
required: false,
|
|
default: () => ({}),
|
|
},
|
|
issuableType: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
showCategorizedIssues: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: true,
|
|
},
|
|
autoCompleteEpics: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: true,
|
|
},
|
|
autoCompleteIssues: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: true,
|
|
},
|
|
hasError: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false,
|
|
},
|
|
itemAddFailureMessage: {
|
|
type: String,
|
|
required: false,
|
|
default: '',
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
isOpen: true,
|
|
};
|
|
},
|
|
computed: {
|
|
hasRelatedIssues() {
|
|
return this.relatedIssues.length > 0;
|
|
},
|
|
categorisedIssues() {
|
|
if (this.showCategorizedIssues) {
|
|
return Object.values(linkedIssueTypesMap)
|
|
.map((linkType) => ({
|
|
linkType,
|
|
issues: this.relatedIssues.filter((issue) => issue.linkType === linkType),
|
|
}))
|
|
.filter((obj) => obj.issues.length > 0);
|
|
}
|
|
|
|
return [{ issues: this.relatedIssues }];
|
|
},
|
|
shouldShowTokenBody() {
|
|
return this.hasRelatedIssues || this.isFetching;
|
|
},
|
|
headerText() {
|
|
return issuablesBlockHeaderTextMap[this.issuableType];
|
|
},
|
|
helpLinkText() {
|
|
return issuablesBlockHelpTextMap[this.issuableType];
|
|
},
|
|
addIssuableButtonText() {
|
|
return issuablesBlockAddButtonTextMap[this.issuableType];
|
|
},
|
|
badgeLabel() {
|
|
return this.isFetching && this.relatedIssues.length === 0 ? '...' : this.relatedIssues.length;
|
|
},
|
|
hasHelpPath() {
|
|
return this.helpPath.length > 0;
|
|
},
|
|
issuableTypeIcon() {
|
|
return issuableIconMap[this.issuableType];
|
|
},
|
|
toggleIcon() {
|
|
return this.isOpen ? 'chevron-lg-up' : 'chevron-lg-down';
|
|
},
|
|
toggleLabel() {
|
|
return this.isOpen ? __('Collapse') : __('Expand');
|
|
},
|
|
emptyStateMessage() {
|
|
return this.showCategorizedIssues
|
|
? sprintf(this.$options.i18n.emptyItemsPremium, { issuableType: this.issuableType })
|
|
: sprintf(this.$options.i18n.emptyItemsFree, { issuableType: this.issuableType });
|
|
},
|
|
},
|
|
methods: {
|
|
handleToggle() {
|
|
this.isOpen = !this.isOpen;
|
|
},
|
|
addButtonClick(event) {
|
|
this.isOpen = true;
|
|
this.$emit('toggleAddRelatedIssuesForm', event);
|
|
},
|
|
},
|
|
linkedIssueTypesTextMap,
|
|
i18n: {
|
|
emptyItemsFree: __("Link %{issuableType}s together to show that they're related."),
|
|
emptyItemsPremium: __(
|
|
"Link %{issuableType}s together to show that they're related or that one is blocking others.",
|
|
),
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div id="related-issues" class="related-issues-block">
|
|
<gl-card
|
|
class="gl-new-card"
|
|
header-class="gl-new-card-header"
|
|
body-class="gl-new-card-body"
|
|
:aria-expanded="isOpen.toString()"
|
|
>
|
|
<template #header>
|
|
<div class="gl-new-card-title-wrapper">
|
|
<h3 class="gl-new-card-title" data-testid="card-title">
|
|
<gl-link
|
|
id="user-content-related-issues"
|
|
class="anchor position-absolute gl-text-decoration-none"
|
|
href="#related-issues"
|
|
aria-hidden="true"
|
|
/>
|
|
<slot name="header-text">{{ headerText }}</slot>
|
|
</h3>
|
|
<div class="gl-new-card-count js-related-issues-header-issue-count">
|
|
<gl-icon :name="issuableTypeIcon" class="gl-mr-2" />
|
|
{{ badgeLabel }}
|
|
</div>
|
|
</div>
|
|
<slot name="header-actions"></slot>
|
|
<gl-button
|
|
v-if="canAdmin"
|
|
size="small"
|
|
data-testid="related-issues-plus-button"
|
|
:aria-label="addIssuableButtonText"
|
|
class="gl-ml-3"
|
|
@click="addButtonClick"
|
|
>
|
|
<slot name="add-button-text">{{ __('Add') }}</slot>
|
|
</gl-button>
|
|
<div class="gl-new-card-toggle">
|
|
<gl-button
|
|
category="tertiary"
|
|
size="small"
|
|
:icon="toggleIcon"
|
|
:aria-label="toggleLabel"
|
|
data-testid="toggle-links"
|
|
@click="handleToggle"
|
|
/>
|
|
</div>
|
|
</template>
|
|
<div
|
|
v-if="isOpen"
|
|
class="linked-issues-card-body gl-new-card-content"
|
|
data-testid="related-issues-body"
|
|
>
|
|
<div
|
|
v-if="isFormVisible"
|
|
class="js-add-related-issues-form-area gl-new-card-add-form"
|
|
:class="{ 'gl-mb-5': shouldShowTokenBody, 'gl-show-field-errors': hasError }"
|
|
data-testid="add-item-form"
|
|
>
|
|
<add-issuable-form
|
|
:show-categorized-issues="showCategorizedIssues"
|
|
:is-submitting="isSubmitting"
|
|
:issuable-type="issuableType"
|
|
:input-value="inputValue"
|
|
:pending-references="pendingReferences"
|
|
:auto-complete-sources="autoCompleteSources"
|
|
:auto-complete-epics="autoCompleteEpics"
|
|
:auto-complete-issues="autoCompleteIssues"
|
|
:path-id-separator="pathIdSeparator"
|
|
:has-error="hasError"
|
|
:item-add-failure-message="itemAddFailureMessage"
|
|
@pendingIssuableRemoveRequest="$emit('pendingIssuableRemoveRequest', $event)"
|
|
@addIssuableFormInput="$emit('addIssuableFormInput', $event)"
|
|
@addIssuableFormBlur="$emit('addIssuableFormBlur', $event)"
|
|
@addIssuableFormSubmit="$emit('addIssuableFormSubmit', $event)"
|
|
@addIssuableFormCancel="$emit('addIssuableFormCancel', $event)"
|
|
/>
|
|
</div>
|
|
<template v-if="shouldShowTokenBody">
|
|
<gl-loading-icon v-if="isFetching" size="sm" class="gl-py-2" />
|
|
<related-issues-list
|
|
v-for="(category, index) in categorisedIssues"
|
|
:key="category.linkType"
|
|
:list-link-type="category.linkType"
|
|
:heading="$options.linkedIssueTypesTextMap[category.linkType]"
|
|
:can-admin="canAdmin"
|
|
:can-reorder="canReorder"
|
|
:is-fetching="isFetching"
|
|
:issuable-type="issuableType"
|
|
:path-id-separator="pathIdSeparator"
|
|
:related-issues="category.issues"
|
|
:class="{
|
|
'gl-pb-3 gl-mb-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100':
|
|
index !== categorisedIssues.length - 1,
|
|
}"
|
|
@relatedIssueRemoveRequest="$emit('relatedIssueRemoveRequest', $event)"
|
|
@saveReorder="$emit('saveReorder', $event)"
|
|
/>
|
|
</template>
|
|
<p v-if="!shouldShowTokenBody && !isFormVisible" class="gl-new-card-empty">
|
|
{{ emptyStateMessage }}
|
|
<gl-link
|
|
v-if="hasHelpPath"
|
|
:href="helpPath"
|
|
data-testid="help-link"
|
|
:aria-label="helpLinkText"
|
|
>
|
|
{{ __('Learn more.') }}
|
|
</gl-link>
|
|
</p>
|
|
</div>
|
|
</gl-card>
|
|
</div>
|
|
</template>
|