diff options
Diffstat (limited to 'app/assets/javascripts/issuable_show')
5 files changed, 169 insertions, 7 deletions
diff --git a/app/assets/javascripts/issuable_show/components/issuable_body.vue b/app/assets/javascripts/issuable_show/components/issuable_body.vue index 02cf7a67727..fe102e942c9 100644 --- a/app/assets/javascripts/issuable_show/components/issuable_body.vue +++ b/app/assets/javascripts/issuable_show/components/issuable_body.vue @@ -1,6 +1,8 @@ <script> import { GlLink } from '@gitlab/ui'; +import TaskList from '~/task_list'; + import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import IssuableDescription from './issuable_description.vue'; @@ -40,6 +42,11 @@ export default { type: Boolean, required: true, }, + enableTaskList: { + type: Boolean, + required: false, + default: false, + }, editFormVisible: { type: Boolean, required: true, @@ -56,6 +63,16 @@ export default { type: String, required: true, }, + taskListUpdatePath: { + type: String, + required: false, + default: '', + }, + taskListLockVersion: { + type: Number, + required: false, + default: 0, + }, }, computed: { isUpdated() { @@ -65,7 +82,50 @@ export default { return this.issuable.updatedBy; }, }, + watch: { + /** + * When user switches between view and edit modes, + * taskList instance becomes invalid so whenever + * view mode is rendered, we need to re-initialize + * taskList to ensure the behaviour functional. + */ + editFormVisible(value) { + if (!value) { + this.$nextTick(() => { + this.initTaskList(); + }); + } + }, + }, + mounted() { + if (this.enableEdit && this.enableTaskList) { + this.initTaskList(); + } + }, methods: { + initTaskList() { + this.taskList = new TaskList({ + /** + * We have hard-coded dataType to `issue` + * as currently only `issue` types can handle + * task-lists, however, we can still use + * task lists in Issue, Test Cases and Incidents + * as all of those are derived from `issue`. + */ + dataType: 'issue', + fieldName: 'description', + lockVersion: this.taskListLockVersion, + selector: '.js-detail-page-description', + onSuccess: this.handleTaskListUpdateSuccess.bind(this), + onError: this.handleTaskListUpdateFailure.bind(this), + }); + }, + handleTaskListUpdateSuccess(updatedIssuable) { + this.$emit('task-list-update-success', updatedIssuable); + }, + handleTaskListUpdateFailure() { + this.$emit('task-list-update-failure'); + }, handleKeydownTitle(e, issuableMeta) { this.$emit('keydown-title', e, issuableMeta); }, @@ -78,7 +138,7 @@ export default { <template> <div class="issue-details issuable-details"> - <div class="detail-page-description content-block"> + <div class="detail-page-description js-detail-page-description content-block"> <issuable-edit-form v-if="editFormVisible" :issuable="issuable" @@ -106,7 +166,13 @@ export default { <slot name="status-badge"></slot> </template> </issuable-title> - <issuable-description v-if="issuable.descriptionHtml" :issuable="issuable" /> + <issuable-description + v-if="issuable.descriptionHtml" + :issuable="issuable" + :enable-task-list="enableTaskList" + :can-edit="enableEdit" + :task-list-update-path="taskListUpdatePath" + /> <small v-if="isUpdated" class="edited-text gl-font-sm!"> {{ __('Edited') }} <time-ago-tooltip :time="issuable.updatedAt" tooltip-placement="bottom" /> diff --git a/app/assets/javascripts/issuable_show/components/issuable_description.vue b/app/assets/javascripts/issuable_show/components/issuable_description.vue index aa7e530972f..f57b5b2deb4 100644 --- a/app/assets/javascripts/issuable_show/components/issuable_description.vue +++ b/app/assets/javascripts/issuable_show/components/issuable_description.vue @@ -12,6 +12,18 @@ export default { type: Object, required: true, }, + enableTaskList: { + type: Boolean, + required: true, + }, + canEdit: { + type: Boolean, + required: true, + }, + taskListUpdatePath: { + type: String, + required: true, + }, }, mounted() { this.renderGFM(); @@ -25,7 +37,16 @@ export default { </script> <template> - <div class="description"> + <div class="description" :class="{ 'js-task-list-container': canEdit && enableTaskList }"> <div ref="gfmContainer" v-safe-html="issuable.descriptionHtml" class="md"></div> + <textarea + v-if="issuable.description && enableTaskList" + ref="textarea" + :value="issuable.description" + :data-update-url="taskListUpdatePath" + class="gl-display-none js-task-list-field" + dir="auto" + > + </textarea> </div> </template> diff --git a/app/assets/javascripts/issuable_show/components/issuable_discussion.vue b/app/assets/javascripts/issuable_show/components/issuable_discussion.vue new file mode 100644 index 00000000000..5858af6cc51 --- /dev/null +++ b/app/assets/javascripts/issuable_show/components/issuable_discussion.vue @@ -0,0 +1,15 @@ +<script> +export default { + name: 'IssuableDiscussion', +}; +</script> + +<template> + <section class="issuable-discussion"> + <div> + <ul class="notes main-notes-list timeline"> + <slot name="discussion"></slot> + </ul> + </div> + </section> +</template> diff --git a/app/assets/javascripts/issuable_show/components/issuable_header.vue b/app/assets/javascripts/issuable_show/components/issuable_header.vue index de17f7e7f6b..d7da533d055 100644 --- a/app/assets/javascripts/issuable_show/components/issuable_header.vue +++ b/app/assets/javascripts/issuable_show/components/issuable_header.vue @@ -3,6 +3,7 @@ import { GlIcon, GlButton, GlTooltipDirective, GlAvatarLink, GlAvatarLabeled } f import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { isExternal } from '~/lib/utils/url_utility'; +import { n__, sprintf } from '~/locale'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; export default { @@ -45,6 +46,11 @@ export default { required: false, default: false, }, + taskCompletionStatus: { + type: Object, + required: false, + default: null, + }, }, computed: { authorId() { @@ -53,6 +59,18 @@ export default { isAuthorExternal() { return isExternal(this.author.webUrl); }, + taskStatusString() { + const { count, completedCount } = this.taskCompletionStatus; + + return sprintf( + n__( + '%{completedCount} of %{count} task completed', + '%{completedCount} of %{count} tasks completed', + count, + ), + { completedCount, count }, + ); + }, }, mounted() { this.toggleSidebarButtonEl = document.querySelector('.js-toggle-right-sidebar-button'); @@ -74,8 +92,8 @@ export default { <gl-icon v-if="statusIcon" :name="statusIcon" class="d-block d-sm-none" /> <span class="d-none d-sm-block"><slot name="status-badge"></slot></span> </div> - <div class="issuable-meta gl-display-flex gl-align-items-center"> - <div class="gl-display-inline-block"> + <div class="issuable-meta gl-display-flex gl-align-items-center d-md-inline-block"> + <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> @@ -95,13 +113,13 @@ export default { :data-name="author.name" :href="author.webUrl" target="_blank" - class="js-user-link gl-ml-2" + class="js-user-link gl-vertical-align-middle gl-ml-2" > <gl-avatar-labeled :size="24" :src="author.avatarUrl" :label="author.name" - class="d-none d-sm-inline-flex gl-ml-1" + class="d-none d-sm-inline-flex gl-mx-1" > <template #meta> <gl-icon v-if="isAuthorExternal" name="external-link" /> @@ -109,6 +127,12 @@ export default { </gl-avatar-labeled> <strong class="author d-sm-none d-inline">@{{ author.username }}</strong> </gl-avatar-link> + <span + v-if="taskCompletionStatus" + data-testid="task-status" + class="gl-display-none gl-md-display-block gl-lg-display-inline-block" + >{{ taskStatusString }}</span + > </div> <gl-button data-testid="sidebar-toggle" diff --git a/app/assets/javascripts/issuable_show/components/issuable_show_root.vue b/app/assets/javascripts/issuable_show/components/issuable_show_root.vue index 240f35b74c8..b514a6b01d8 100644 --- a/app/assets/javascripts/issuable_show/components/issuable_show_root.vue +++ b/app/assets/javascripts/issuable_show/components/issuable_show_root.vue @@ -2,6 +2,7 @@ import IssuableSidebar from '~/issuable_sidebar/components/issuable_sidebar_root.vue'; import IssuableBody from './issuable_body.vue'; +import IssuableDiscussion from './issuable_discussion.vue'; import IssuableHeader from './issuable_header.vue'; export default { @@ -9,6 +10,7 @@ export default { IssuableSidebar, IssuableHeader, IssuableBody, + IssuableDiscussion, }, props: { issuable: { @@ -40,6 +42,11 @@ export default { required: false, default: true, }, + enableTaskList: { + type: Boolean, + required: false, + default: false, + }, editFormVisible: { type: Boolean, required: false, @@ -60,6 +67,21 @@ export default { required: false, default: '', }, + taskCompletionStatus: { + type: Object, + required: false, + default: null, + }, + taskListUpdatePath: { + type: String, + required: false, + default: '', + }, + taskListLockVersion: { + type: Number, + required: false, + default: 0, + }, }, methods: { handleKeydownTitle(e, issuableMeta) { @@ -81,6 +103,7 @@ export default { :confidential="issuable.confidential" :created-at="issuable.createdAt" :author="issuable.author" + :task-completion-status="taskCompletionStatus" > <template #status-badge> <slot name="status-badge"></slot> @@ -89,6 +112,7 @@ export default { <slot name="header-actions"></slot> </template> </issuable-header> + <issuable-body :issuable="issuable" :status-badge-class="statusBadgeClass" @@ -96,11 +120,16 @@ export default { :enable-edit="enableEdit" :enable-autocomplete="enableAutocomplete" :enable-autosave="enableAutosave" + :enable-task-list="enableTaskList" :edit-form-visible="editFormVisible" :show-field-title="showFieldTitle" :description-preview-path="descriptionPreviewPath" :description-help-path="descriptionHelpPath" + :task-list-update-path="taskListUpdatePath" + :task-list-lock-version="taskListLockVersion" @edit-issuable="$emit('edit-issuable', $event)" + @task-list-update-success="$emit('task-list-update-success', $event)" + @task-list-update-failure="$emit('task-list-update-failure')" @keydown-title="handleKeydownTitle" @keydown-description="handleKeydownDescription" > @@ -111,6 +140,13 @@ export default { <slot name="edit-form-actions" v-bind="actionsProps"></slot> </template> </issuable-body> + + <issuable-discussion> + <template #discussion> + <slot name="discussion"></slot> + </template> + </issuable-discussion> + <issuable-sidebar @sidebar-toggle="$emit('sidebar-toggle', $event)"> <template #right-sidebar-items="sidebarProps"> <slot name="right-sidebar-items" v-bind="sidebarProps"></slot> |