diff options
Diffstat (limited to 'app/assets/javascripts/issuables_list/components')
-rw-r--r-- | app/assets/javascripts/issuables_list/components/issuable.vue | 154 | ||||
-rw-r--r-- | app/assets/javascripts/issuables_list/components/issuables_list_app.vue | 65 |
2 files changed, 143 insertions, 76 deletions
diff --git a/app/assets/javascripts/issuables_list/components/issuable.vue b/app/assets/javascripts/issuables_list/components/issuable.vue index b7f4292a126..4fc614f8da4 100644 --- a/app/assets/javascripts/issuables_list/components/issuable.vue +++ b/app/assets/javascripts/issuables_list/components/issuable.vue @@ -23,6 +23,8 @@ import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue'; import { isScopedLabel } from '~/lib/utils/common_utils'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { convertToCamelCase } from '~/lib/utils/text_utility'; + export default { i18n: { openedAgo: __('opened %{timeAgoString} by %{user}'), @@ -34,6 +36,8 @@ export default { GlLabel, GlIcon, GlSprintf, + IssueHealthStatus: () => + import('ee_component/related_items_tree/components/issue_health_status.vue'), }, directives: { GlTooltip, @@ -85,9 +89,6 @@ export default { dueDateWords() { return this.dueDate ? dateInWords(this.dueDate, true) : undefined; }, - hasNoComments() { - return !this.userNotesCount; - }, isOverdue() { return this.dueDate ? this.dueDate < new Date() : false; }, @@ -148,34 +149,59 @@ export default { time_ago: escape(getTimeago().format(this.issuable.updated_at)), }); }, - userNotesCount() { - return this.issuable.user_notes_count; - }, issuableMeta() { return [ { key: 'merge-requests', + visible: this.issuable.merge_requests_count > 0, value: this.issuable.merge_requests_count, title: __('Related merge requests'), - class: 'js-merge-requests', + dataTestId: 'merge-requests', + class: 'js-merge-requests icon-merge-request-unmerged', icon: 'merge-request', }, { key: 'upvotes', + visible: this.issuable.upvotes > 0, value: this.issuable.upvotes, title: __('Upvotes'), - class: 'js-upvotes', + dataTestId: 'upvotes', + class: 'js-upvotes issuable-upvotes', icon: 'thumb-up', }, { key: 'downvotes', + visible: this.issuable.downvotes > 0, value: this.issuable.downvotes, title: __('Downvotes'), - class: 'js-downvotes', + dataTestId: 'downvotes', + class: 'js-downvotes issuable-downvotes', icon: 'thumb-down', }, + { + key: 'blocking-issues', + visible: this.issuable.blocking_issues_count > 0, + value: this.issuable.blocking_issues_count, + title: __('Blocking issues'), + dataTestId: 'blocking-issues', + href: `${this.issuable.web_url}#related-issues`, + icon: 'issue-block', + }, + { + key: 'comments-count', + visible: !this.isJiraIssue, + value: this.issuable.user_notes_count, + title: __('Comments'), + dataTestId: 'notes-count', + href: `${this.issuable.web_url}#notes`, + class: { 'no-comments': !this.issuable.user_notes_count, 'issuable-comments': true }, + icon: 'comments', + }, ]; }, + healthStatus() { + return convertToCamelCase(this.issuable.health_status); + }, }, mounted() { // TODO: Refactor user popover to use its own component instead of @@ -202,6 +228,9 @@ export default { selected: ev.target.checked, }); }, + issuableMetaComponent(href) { + return href ? 'gl-link' : 'span'; + }, }, confidentialTooltipText: __('Confidential'), @@ -215,11 +244,14 @@ export default { :data-id="issuable.id" :data-labels="labelIdsString" :data-url="issuable.web_url" + data-qa-selector="issue_container" + :data-qa-issue-title="issuable.title" > - <div class="d-flex"> + <div class="gl-display-flex"> <!-- Bulk edit checkbox --> - <div v-if="isBulkEditing" class="mr-2"> + <div v-if="isBulkEditing" class="gl-mr-3"> <input + :id="`selected_issue_${issuable.id}`" :checked="selected" class="selected-issuable" type="checkbox" @@ -230,7 +262,7 @@ export default { <!-- Issuable info container --> <!-- Issuable main info --> - <div class="flex-grow-1"> + <div class="gl-flex-grow-1"> <div class="title"> <span class="issue-title-text"> <gl-icon @@ -242,22 +274,28 @@ export default { :title="$options.confidentialTooltipText" :aria-label="$options.confidentialTooltipText" /> - <gl-link :href="issuable.web_url" :target="linkTarget" data-testid="issuable-title"> - {{ issuable.title }} - <gl-icon + <gl-link + :href="issuable.web_url" + :target="linkTarget" + data-testid="issuable-title" + data-qa-selector="issue_link" + >{{ issuable.title + }}<gl-icon v-if="isJiraIssue" name="external-link" - class="gl-vertical-align-text-bottom" + class="gl-vertical-align-text-bottom gl-ml-2" /> </gl-link> </span> - <span v-if="issuable.has_tasks" class="ml-1 task-status d-none d-sm-inline-block"> - {{ issuable.task_status }} - </span> + <span + v-if="issuable.has_tasks" + class="gl-ml-2 task-status gl-display-none d-sm-inline-block" + >{{ issuable.task_status }}</span + > </div> <div class="issuable-info"> - <span class="js-ref-path"> + <span class="js-ref-path gl-mr-4 mr-sm-0"> <span v-if="isJiraIssue" class="svg-container jira-logo-container" @@ -267,7 +305,7 @@ export default { {{ referencePath }} </span> - <span data-testid="openedByMessage" class="d-none d-sm-inline-block mr-1"> + <span data-testid="openedByMessage" class="gl-display-none d-sm-inline-block gl-mr-4"> · <gl-sprintf :message="isJiraIssue ? $options.i18n.openedAgoJira : $options.i18n.openedAgo" @@ -281,9 +319,8 @@ export default { v-bind="popoverDataAttrs" :href="issuableAuthor.web_url" :target="linkTarget" + >{{ issuableAuthor.name }}</gl-link > - {{ issuableAuthor.name }} - </gl-link> </template> </gl-sprintf> </span> @@ -291,18 +328,18 @@ export default { <gl-link v-if="issuable.milestone" v-gl-tooltip - class="d-none d-sm-inline-block mr-1 js-milestone" + class="gl-display-none d-sm-inline-block gl-mr-4 js-milestone milestone" :href="milestoneLink" :title="milestoneTooltipText" > - <i class="fa fa-clock-o"></i> + <gl-icon name="clock" class="s16 gl-vertical-align-text-bottom" /> {{ issuable.milestone.title }} </gl-link> <span v-if="dueDate" v-gl-tooltip - class="d-none d-sm-inline-block mr-1 js-due-date" + class="gl-display-none d-sm-inline-block gl-mr-4 js-due-date" :class="{ cred: isOverdue }" :title="__('Due date')" > @@ -310,6 +347,24 @@ export default { {{ dueDateWords }} </span> + <span + v-if="hasWeight" + v-gl-tooltip + :title="__('Weight')" + class="gl-display-none d-sm-inline-block gl-mr-4" + data-testid="weight" + data-qa-selector="issuable_weight_content" + > + <gl-icon name="weight" class="align-text-bottom" /> + {{ issuable.weight }} + </span> + + <issue-health-status + v-if="issuable.health_status" + :health-status="healthStatus" + class="gl-mr-4 issuable-tag-valign" + /> + <gl-label v-for="label in issuable.labels" :key="label.id" @@ -321,61 +376,44 @@ export default { :title="label.name" :scoped="isScoped(label)" size="sm" - class="mr-1" + class="gl-mr-2 issuable-tag-valign" >{{ label.name }}</gl-label > - - <span - v-if="hasWeight" - v-gl-tooltip - :title="__('Weight')" - class="d-none d-sm-inline-block js-weight" - data-testid="weight" - > - <gl-icon name="weight" class="align-text-bottom" /> - {{ issuable.weight }} - </span> </div> </div> <!-- Issuable meta --> - <div class="flex-shrink-0 d-flex flex-column align-items-end justify-content-center"> - <div class="controls d-flex"> + <div + class="gl-flex-shrink-0 gl-display-flex gl-flex-direction-column align-items-end gl-justify-content-center" + > + <div class="controls gl-display-flex"> <span v-if="isJiraIssue" data-testid="issuable-status">{{ issuable.status }}</span> <span v-else-if="isClosed" class="issuable-status">{{ __('CLOSED') }}</span> <issue-assignees :assignees="issuable.assignees" - class="align-items-center d-flex ml-2" + class="gl-align-items-center gl-display-flex gl-ml-3" :icon-size="16" - img-css-classes="mr-1" + img-css-classes="gl-mr-2!" :max-visible="4" /> <template v-for="meta in issuableMeta"> <span - v-if="meta.value" + v-if="meta.visible" :key="meta.key" v-gl-tooltip - :class="['d-none d-sm-inline-block ml-2 vertical-align-middle', meta.class]" + class="gl-display-none gl-display-sm-flex gl-align-items-center gl-ml-3" + :class="meta.class" + :data-testid="meta.dataTestId" :title="meta.title" > - <gl-icon v-if="meta.icon" :name="meta.icon" /> - {{ meta.value }} + <component :is="issuableMetaComponent(meta.href)" :href="meta.href"> + <gl-icon v-if="meta.icon" :name="meta.icon" /> + {{ meta.value }} + </component> </span> </template> - - <gl-link - v-if="!isJiraIssue" - v-gl-tooltip - class="ml-2 js-notes" - :href="`${issuable.web_url}#notes`" - :title="__('Comments')" - :class="{ 'no-comments': hasNoComments }" - > - <gl-icon name="comments" class="gl-vertical-align-text-bottom" /> - {{ userNotesCount }} - </gl-link> </div> <div v-gl-tooltip class="issuable-updated-at" :title="updatedDateString"> {{ updatedDateAgo }} diff --git a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue b/app/assets/javascripts/issuables_list/components/issuables_list_app.vue index 21aeb2ca143..fecb7353efb 100644 --- a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue +++ b/app/assets/javascripts/issuables_list/components/issuables_list_app.vue @@ -1,7 +1,12 @@ <script> import { toNumber, omit } from 'lodash'; -import { GlEmptyState, GlPagination, GlSkeletonLoading } from '@gitlab/ui'; -import flash from '~/flash'; +import { + GlEmptyState, + GlPagination, + GlSkeletonLoading, + GlSafeHtmlDirective as SafeHtml, +} from '@gitlab/ui'; +import { deprecatedCreateFlash as flash } from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { scrollToElement, @@ -23,9 +28,13 @@ import { } from '../constants'; import { setUrlParams } from '~/lib/utils/url_utility'; import issueableEventHub from '../eventhub'; +import { emptyStateHelper } from '../service_desk_helper'; export default { LOADING_LIST_ITEMS_LENGTH, + directives: { + SafeHtml, + }, components: { GlEmptyState, GlPagination, @@ -39,15 +48,9 @@ export default { required: false, default: false, }, - createIssuePath: { - type: String, - required: false, - default: '', - }, - emptySvgPath: { - type: String, - required: false, - default: '', + emptyStateMeta: { + type: Object, + required: true, }, endpoint: { type: String, @@ -94,26 +97,40 @@ export default { emptyState() { if (this.issuables.length) { return {}; // Empty state shouldn't be shown here - } else if (this.hasFilters) { + } + + if (this.isServiceDesk) { + return emptyStateHelper(this.emptyStateMeta); + } + + if (this.hasFilters) { return { title: __('Sorry, your filter produced no results'), + svgPath: this.emptyStateMeta.svgPath, description: __('To widen your search, change or remove filters above'), + primaryLink: this.emptyStateMeta.createIssuePath, + primaryText: __('New issue'), }; - } else if (this.filters.state === 'opened') { + } + + if (this.filters.state === 'opened') { return { title: __('There are no open issues'), + svgPath: this.emptyStateMeta.svgPath, description: __('To keep this project going, create a new issue'), - primaryLink: this.createIssuePath, + primaryLink: this.emptyStateMeta.createIssuePath, primaryText: __('New issue'), }; } else if (this.filters.state === 'closed') { return { title: __('There are no closed issues'), + svgPath: this.emptyStateMeta.svgPath, }; } return { title: __('There are no issues to show'), + svgPath: this.emptyStateMeta.svgPath, description: __( 'The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project.', ), @@ -155,6 +172,9 @@ export default { nextPage: this.paginationNext, }; }, + isServiceDesk() { + return this.type === 'service_desk'; + }, isJira() { return this.type === 'jira'; }, @@ -356,7 +376,13 @@ export default { </ul> <div v-else-if="issuables.length"> <div v-if="isBulkEditing" class="issue px-3 py-3 border-bottom border-light"> - <input type="checkbox" :checked="allIssuablesSelected" class="mr-2" @click="onSelectAll" /> + <input + id="check-all-issues" + type="checkbox" + :checked="allIssuablesSelected" + class="mr-2" + @click="onSelectAll" + /> <strong>{{ __('Select all') }}</strong> </div> <ul @@ -386,10 +412,13 @@ export default { <gl-empty-state v-else :title="emptyState.title" - :description="emptyState.description" - :svg-path="emptySvgPath" + :svg-path="emptyState.svgPath" :primary-button-link="emptyState.primaryLink" :primary-button-text="emptyState.primaryText" - /> + > + <template #description> + <div v-safe-html="emptyState.description"></div> + </template> + </gl-empty-state> </div> </template> |