debian-mirror-gitlab/app/assets/javascripts/notes/components/noteable_note.vue

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

531 lines
16 KiB
Vue
Raw Normal View History

2018-03-17 18:26:18 +05:30
<script>
2023-03-04 22:38:38 +05:30
import { GlSprintf, GlAvatarLink, GlAvatar } from '@gitlab/ui';
2018-05-09 12:01:36 +05:30
import $ from 'jquery';
2021-04-17 20:07:23 +05:30
import { escape, isEmpty } from 'lodash';
2021-03-11 19:13:27 +05:30
import { mapGetters, mapActions } from 'vuex';
2023-03-04 22:38:38 +05:30
import SafeHtml from '~/vue_shared/directives/safe_html';
2022-04-04 11:22:00 +05:30
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
2021-03-11 19:13:27 +05:30
import { INLINE_DIFF_LINES_KEY } from '~/diffs/constants';
2023-05-27 22:25:52 +05:30
import { createAlert } from '~/alert';
2023-03-04 22:38:38 +05:30
import { HTTP_STATUS_GONE } from '~/lib/utils/http_status';
2022-05-07 20:08:51 +05:30
import { ignoreWhilePending } from '~/lib/utils/ignore_while_pending';
2019-03-02 22:35:43 +05:30
import { truncateSha } from '~/lib/utils/text_utility';
2019-02-15 15:39:39 +05:30
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
2022-06-21 17:19:12 +05:30
import { __, s__, sprintf } from '~/locale';
2023-03-04 22:38:38 +05:30
import { renderGFM } from '~/behaviors/markdown/render_gfm';
2018-05-09 12:01:36 +05:30
import eventHub from '../event_hub';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
2021-09-30 23:02:18 +05:30
import { renderMarkdown } from '../utils';
2020-06-23 00:09:42 +05:30
import {
getStartLineNumber,
getEndLineNumber,
getLineClasses,
commentLineOptions,
2020-07-28 23:09:34 +05:30
formatLineRange,
2020-06-23 00:09:42 +05:30
} from './multiline_comment_utils';
2022-10-11 01:57:18 +05:30
import NoteActions from './note_actions.vue';
2021-03-11 19:13:27 +05:30
import NoteBody from './note_body.vue';
2022-10-11 01:57:18 +05:30
import NoteHeader from './note_header.vue';
2018-03-17 18:26:18 +05:30
2018-05-09 12:01:36 +05:30
export default {
2018-11-08 19:23:39 +05:30
name: 'NoteableNote',
2018-05-09 12:01:36 +05:30
components: {
2020-06-23 00:09:42 +05:30
GlSprintf,
2022-10-11 01:57:18 +05:30
NoteHeader,
NoteActions,
2019-09-04 21:01:54 +05:30
NoteBody,
2019-02-15 15:39:39 +05:30
TimelineEntryItem,
2022-08-13 15:12:31 +05:30
GlAvatarLink,
GlAvatar,
2018-05-09 12:01:36 +05:30
},
2020-11-24 15:15:51 +05:30
directives: {
SafeHtml,
},
2021-03-11 19:13:27 +05:30
mixins: [noteable, resolvable],
2023-04-23 21:23:45 +05:30
inject: {
reportAbusePath: {
default: '',
},
},
2018-05-09 12:01:36 +05:30
props: {
note: {
type: Object,
required: true,
2018-03-17 18:26:18 +05:30
},
2019-02-15 15:39:39 +05:30
line: {
type: Object,
required: false,
default: null,
},
2021-04-29 21:17:54 +05:30
discussionFile: {
type: Object,
required: false,
default: null,
},
2019-02-15 15:39:39 +05:30
helpPagePath: {
type: String,
required: false,
default: '',
},
2019-03-02 22:35:43 +05:30
commit: {
type: Object,
required: false,
default: () => null,
},
2019-07-07 11:18:12 +05:30
showReplyButton: {
type: Boolean,
required: false,
default: false,
},
2020-06-23 00:09:42 +05:30
diffLines: {
2020-07-28 23:09:34 +05:30
type: Array,
2020-06-23 00:09:42 +05:30
required: false,
default: null,
},
2020-07-28 23:09:34 +05:30
discussionRoot: {
type: Boolean,
required: false,
default: false,
},
2021-01-29 00:20:46 +05:30
discussionResolvePath: {
type: String,
required: false,
default: '',
},
2021-11-18 22:05:49 +05:30
isOverviewTab: {
type: Boolean,
required: false,
default: false,
},
2023-01-13 00:05:48 +05:30
shouldScrollToNote: {
type: Boolean,
required: false,
default: true,
},
2018-05-09 12:01:36 +05:30
},
data() {
return {
isEditing: false,
isDeleting: false,
isRequesting: false,
isResolving: false,
2020-07-28 23:09:34 +05:30
commentLineStart: {},
2021-04-29 21:17:54 +05:30
resolveAsThread: true,
2018-05-09 12:01:36 +05:30
};
},
computed: {
2020-06-23 00:09:42 +05:30
...mapGetters('diffs', ['getDiffFileByHash']),
2019-03-02 22:35:43 +05:30
...mapGetters(['targetNoteHash', 'getNoteableData', 'getUserData', 'commentsDisabled']),
2018-05-09 12:01:36 +05:30
author() {
return this.note.author;
2018-03-17 18:26:18 +05:30
},
2022-07-16 23:28:13 +05:30
commentType() {
2022-10-11 01:57:18 +05:30
return this.note.internal ? __('internal note') : __('comment');
2022-07-16 23:28:13 +05:30
},
2018-05-09 12:01:36 +05:30
classNameBindings() {
2018-03-17 18:26:18 +05:30
return {
2018-11-08 19:23:39 +05:30
[`note-row-${this.note.id}`]: true,
2018-05-09 12:01:36 +05:30
'is-editing': this.isEditing && !this.isRequesting,
'is-requesting being-posted': this.isRequesting,
'disabled-content': this.isDeleting,
2018-11-08 19:23:39 +05:30
target: this.isTarget,
2019-02-15 15:39:39 +05:30
'is-editable': this.note.current_user.can_edit,
2018-03-17 18:26:18 +05:30
};
},
2018-05-09 12:01:36 +05:30
canReportAsAbuse() {
2023-04-23 21:23:45 +05:30
return Boolean(this.reportAbusePath) && this.author.id !== this.getUserData.id;
2018-03-17 18:26:18 +05:30
},
2018-05-09 12:01:36 +05:30
noteAnchorId() {
return `note_${this.note.id}`;
2018-03-17 18:26:18 +05:30
},
2018-11-08 19:23:39 +05:30
isTarget() {
return this.targetNoteHash === this.noteAnchorId;
},
2019-03-02 22:35:43 +05:30
discussionId() {
if (this.discussion) {
return this.discussion.id;
}
return '';
},
actionText() {
if (!this.commit) {
return '';
}
2019-07-07 11:18:12 +05:30
// We need to do this to ensure we have the correct sentence order
2019-03-02 22:35:43 +05:30
// when translating this as the sentence order may change from one
// language to the next. See:
2019-12-04 20:38:33 +05:30
// https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/24427#note_133713771
2019-03-02 22:35:43 +05:30
const { id, url } = this.commit;
const commitLink = `<a class="commit-sha monospace" href="${escape(url)}">${truncateSha(
id,
)}</a>`;
return sprintf(s__('MergeRequests|commented on commit %{commitLink}'), { commitLink }, false);
},
2020-06-23 00:09:42 +05:30
isDraft() {
return this.note.isDraft;
},
canResolve() {
2021-04-29 21:17:54 +05:30
if (!this.discussionRoot) return false;
2021-01-29 00:20:46 +05:30
2021-04-29 21:17:54 +05:30
return this.note.current_user.can_resolve_discussion;
2020-06-23 00:09:42 +05:30
},
lineRange() {
return this.note.position?.line_range;
},
startLineNumber() {
return getStartLineNumber(this.lineRange);
},
endLineNumber() {
return getEndLineNumber(this.lineRange);
},
showMultiLineComment() {
2020-10-24 23:57:45 +05:30
if (
!this.discussionRoot ||
this.startLineNumber.length === 0 ||
this.endLineNumber.length === 0
)
return false;
2020-07-28 23:09:34 +05:30
return this.line && this.startLineNumber !== this.endLineNumber;
2020-06-23 00:09:42 +05:30
},
commentLineOptions() {
2021-02-22 17:27:13 +05:30
const lines = this.diffFile[INLINE_DIFF_LINES_KEY].length;
return commentLineOptions(lines, this.commentLineStart, this.line.line_code);
2020-07-28 23:09:34 +05:30
},
diffFile() {
2021-04-29 21:17:54 +05:30
let fileResolvedFromAvailableSource;
2020-07-28 23:09:34 +05:30
if (this.commentLineStart.line_code) {
const lineCode = this.commentLineStart.line_code.split('_')[0];
2021-04-29 21:17:54 +05:30
fileResolvedFromAvailableSource = this.getDiffFileByHash(lineCode);
}
if (!fileResolvedFromAvailableSource && this.discussionFile) {
fileResolvedFromAvailableSource = this.discussionFile;
2020-06-23 00:09:42 +05:30
}
2021-04-29 21:17:54 +05:30
return fileResolvedFromAvailableSource || null;
2020-06-23 00:09:42 +05:30
},
2022-08-13 15:12:31 +05:30
isMRDiffView() {
return this.line && !this.isOverviewTab;
},
2018-05-09 12:01:36 +05:30
},
created() {
2020-07-28 23:09:34 +05:30
const line = this.note.position?.line_range?.start || this.line;
this.commentLineStart = line
? {
line_code: line.line_code,
type: line.type,
old_line: line.old_line,
new_line: line.new_line,
}
: {};
2018-05-09 12:01:36 +05:30
eventHub.$on('enterEditMode', ({ noteId }) => {
if (noteId === this.note.id) {
2018-03-17 18:26:18 +05:30
this.isEditing = true;
2020-07-28 23:09:34 +05:30
this.setSelectedCommentPositionHover();
2018-05-09 12:01:36 +05:30
this.scrollToNoteIfNeeded($(this.$el));
}
});
},
2018-03-17 18:26:18 +05:30
2018-11-08 19:23:39 +05:30
mounted() {
2023-01-13 00:05:48 +05:30
if (this.isTarget && this.shouldScrollToNote) {
2018-11-08 19:23:39 +05:30
this.scrollToNoteIfNeeded($(this.$el));
}
},
2018-05-09 12:01:36 +05:30
methods: {
2019-12-04 20:38:33 +05:30
...mapActions([
'deleteNote',
'removeNote',
'updateNote',
'toggleResolveNote',
'scrollToNoteIfNeeded',
2020-06-23 00:09:42 +05:30
'updateAssignees',
2020-07-28 23:09:34 +05:30
'setSelectedCommentPositionHover',
2020-10-24 23:57:45 +05:30
'updateDiscussionPosition',
2019-12-04 20:38:33 +05:30
]),
2018-05-09 12:01:36 +05:30
editHandler() {
this.isEditing = true;
2020-07-28 23:09:34 +05:30
this.setSelectedCommentPositionHover();
2018-12-05 23:21:45 +05:30
this.$emit('handleEdit');
2018-05-09 12:01:36 +05:30
},
2022-04-04 11:22:00 +05:30
async deleteHandler() {
2022-07-16 23:28:13 +05:30
let { commentType } = this;
if (this.note.isDraft) {
// Draft internal notes (i.e. MR review comments) are not supported.
commentType = __('pending comment');
}
2022-04-04 11:22:00 +05:30
2022-07-16 23:28:13 +05:30
const msg = sprintf(__('Are you sure you want to delete this %{commentType}?'), {
commentType,
2022-04-04 11:22:00 +05:30
});
const confirmed = await confirmAction(msg, {
primaryBtnVariant: 'danger',
2022-10-11 01:57:18 +05:30
primaryBtnText: this.note.internal ? __('Delete internal note') : __('Delete comment'),
2022-04-04 11:22:00 +05:30
});
if (confirmed) {
2018-05-09 12:01:36 +05:30
this.isDeleting = true;
2018-11-20 20:47:30 +05:30
this.$emit('handleDeleteNote', this.note);
2018-03-17 18:26:18 +05:30
2018-12-05 23:21:45 +05:30
if (this.note.isDraft) return;
2018-05-09 12:01:36 +05:30
this.deleteNote(this.note)
2018-03-17 18:26:18 +05:30
.then(() => {
2018-05-09 12:01:36 +05:30
this.isDeleting = false;
2018-03-17 18:26:18 +05:30
})
.catch(() => {
2022-11-25 23:54:43 +05:30
createAlert({
2021-09-30 23:02:18 +05:30
message: __('Something went wrong while deleting your note. Please try again.'),
});
2018-05-09 12:01:36 +05:30
this.isDeleting = false;
2018-03-17 18:26:18 +05:30
});
2018-05-09 12:01:36 +05:30
}
},
2018-12-05 23:21:45 +05:30
updateSuccess() {
this.isEditing = false;
this.isRequesting = false;
this.oldContent = null;
2023-03-04 22:38:38 +05:30
renderGFM(this.$refs.noteBody.$el);
2018-12-05 23:21:45 +05:30
this.$refs.noteBody.resetAutoSave();
this.$emit('updateSuccess');
},
2021-10-27 15:23:28 +05:30
formUpdateHandler({ noteText, callback, resolveDiscussion }) {
2020-06-23 00:09:42 +05:30
const position = {
...this.note.position,
};
2020-07-28 23:09:34 +05:30
2020-10-24 23:57:45 +05:30
if (this.discussionRoot && this.commentLineStart && this.line) {
2020-07-28 23:09:34 +05:30
position.line_range = formatLineRange(this.commentLineStart, this.line);
2020-10-24 23:57:45 +05:30
this.updateDiscussionPosition({
discussionId: this.note.discussion_id,
position,
});
}
2020-07-28 23:09:34 +05:30
2018-12-05 23:21:45 +05:30
this.$emit('handleUpdateNote', {
note: this.note,
noteText,
2019-07-31 22:56:46 +05:30
resolveDiscussion,
2020-06-23 00:09:42 +05:30
position,
2018-12-05 23:21:45 +05:30
callback: () => this.updateSuccess(),
});
2019-07-31 22:56:46 +05:30
if (this.isDraft) return;
2018-05-09 12:01:36 +05:30
const data = {
endpoint: this.note.path,
note: {
2018-11-08 19:23:39 +05:30
target_type: this.getNoteableData.targetType,
2018-05-09 12:01:36 +05:30
target_id: this.note.noteable_id,
2021-04-17 20:07:23 +05:30
note: { note: noteText },
2018-05-09 12:01:36 +05:30
},
};
2021-04-17 20:07:23 +05:30
// Stringifying an empty object yields `{}` which breaks graphql queries
// https://gitlab.com/gitlab-org/gitlab/-/issues/298827
if (!isEmpty(position)) data.note.note.position = JSON.stringify(position);
2018-05-09 12:01:36 +05:30
this.isRequesting = true;
this.oldContent = this.note.note_html;
2021-03-11 19:13:27 +05:30
// eslint-disable-next-line vue/no-mutating-props
2021-09-30 23:02:18 +05:30
this.note.note_html = renderMarkdown(noteText);
2018-05-09 12:01:36 +05:30
this.updateNote(data)
.then(() => {
2018-12-05 23:21:45 +05:30
this.updateSuccess();
2018-05-09 12:01:36 +05:30
callback();
})
2021-03-08 18:12:59 +05:30
.catch((response) => {
2023-03-04 22:38:38 +05:30
if (response.status === HTTP_STATUS_GONE) {
2019-12-04 20:38:33 +05:30
this.removeNote(this.note);
this.updateSuccess();
2018-05-09 12:01:36 +05:30
callback();
2019-12-04 20:38:33 +05:30
} else {
this.isRequesting = false;
this.isEditing = true;
2020-07-28 23:09:34 +05:30
this.setSelectedCommentPositionHover();
2019-12-04 20:38:33 +05:30
this.$nextTick(() => {
2022-01-26 12:08:38 +05:30
this.handleUpdateError(response); // The 'response' parameter is being used in JH, don't remove it
2019-12-04 20:38:33 +05:30
this.recoverNoteContent(noteText);
callback();
});
}
2018-05-09 12:01:36 +05:30
});
},
2022-01-26 12:08:38 +05:30
handleUpdateError() {
const msg = __('Something went wrong while editing your comment. Please try again.');
2022-11-25 23:54:43 +05:30
createAlert({
2022-01-26 12:08:38 +05:30
message: msg,
parent: this.$el,
});
},
2022-05-07 20:08:51 +05:30
formCancelHandler: ignoreWhilePending(async function formCancelHandler({
shouldConfirm,
isDirty,
}) {
2018-05-09 12:01:36 +05:30
if (shouldConfirm && isDirty) {
2022-07-16 23:28:13 +05:30
const msg = sprintf(__('Are you sure you want to cancel editing this %{commentType}?'), {
commentType: this.commentType,
});
2022-06-21 17:19:12 +05:30
const confirmed = await confirmAction(msg, {
primaryBtnText: __('Cancel editing'),
primaryBtnVariant: 'danger',
secondaryBtnVariant: 'default',
secondaryBtnText: __('Continue editing'),
hideCancel: true,
});
2022-04-04 11:22:00 +05:30
if (!confirmed) return;
2018-05-09 12:01:36 +05:30
}
this.$refs.noteBody.resetAutoSave();
if (this.oldContent) {
2021-03-11 19:13:27 +05:30
// eslint-disable-next-line vue/no-mutating-props
2018-05-09 12:01:36 +05:30
this.note.note_html = this.oldContent;
this.oldContent = null;
}
this.isEditing = false;
2018-12-05 23:21:45 +05:30
this.$emit('cancelForm');
2022-05-07 20:08:51 +05:30
}),
2018-05-09 12:01:36 +05:30
recoverNoteContent(noteText) {
// we need to do this to prevent noteForm inconsistent content warning
// this is something we intentionally do so we need to recover the content
2021-03-11 19:13:27 +05:30
// eslint-disable-next-line vue/no-mutating-props
2018-05-09 12:01:36 +05:30
this.note.note = noteText;
2019-09-04 21:01:54 +05:30
const { noteBody } = this.$refs;
if (noteBody) {
noteBody.note.note = noteText;
}
2018-03-17 18:26:18 +05:30
},
2020-06-23 00:09:42 +05:30
getLineClasses(lineNumber) {
return getLineClasses(lineNumber);
},
assigneesUpdate(assignees) {
this.updateAssignees(assignees);
},
2018-05-09 12:01:36 +05:30
},
};
2018-03-17 18:26:18 +05:30
</script>
<template>
2019-02-15 15:39:39 +05:30
<timeline-entry-item
2018-03-17 18:26:18 +05:30
:id="noteAnchorId"
2022-10-11 01:57:18 +05:30
:class="{ ...classNameBindings, 'internal-note': note.internal }"
2018-11-08 19:23:39 +05:30
:data-award-url="note.toggle_award_path"
:data-note-id="note.id"
2022-11-25 23:54:43 +05:30
class="note note-wrapper note-comment"
2021-01-29 00:20:46 +05:30
data-qa-selector="noteable_note_container"
2018-11-08 19:23:39 +05:30
>
2020-10-24 23:57:45 +05:30
<div
v-if="showMultiLineComment"
data-testid="multiline-comment"
2022-11-25 23:54:43 +05:30
class="gl-text-gray-500 gl-border-gray-100 gl-border-b-solid gl-border-b-1 gl-px-5 gl-py-3"
2020-10-24 23:57:45 +05:30
>
<gl-sprintf :message="__('Comment on lines %{startLine} to %{endLine}')">
<template #startLine>
<span :class="getLineClasses(startLineNumber)">{{ startLineNumber }}</span>
</template>
<template #endLine>
<span :class="getLineClasses(endLineNumber)">{{ endLineNumber }}</span>
</template>
</gl-sprintf>
2020-06-23 00:09:42 +05:30
</div>
2022-08-13 15:12:31 +05:30
2022-11-25 23:54:43 +05:30
<div v-if="isMRDiffView" class="timeline-avatar gl-float-left gl-pt-2">
2022-08-13 15:12:31 +05:30
<gl-avatar-link :href="author.path">
<gl-avatar
:src="author.avatar_url"
:entity-name="author.username"
:alt="author.name"
:size="24"
/>
<slot name="avatar-badge"></slot>
</gl-avatar-link>
2019-02-15 15:39:39 +05:30
</div>
2022-08-13 15:12:31 +05:30
2022-11-25 23:54:43 +05:30
<div v-else class="timeline-avatar gl-float-left">
2022-08-13 15:12:31 +05:30
<gl-avatar-link :href="author.path">
<gl-avatar
:src="author.avatar_url"
:entity-name="author.username"
:alt="author.name"
2022-11-25 23:54:43 +05:30
:size="32"
2022-08-13 15:12:31 +05:30
/>
<slot name="avatar-badge"></slot>
</gl-avatar-link>
</div>
2019-02-15 15:39:39 +05:30
<div class="timeline-content">
<div class="note-header">
2020-05-24 23:13:21 +05:30
<note-header
:author="author"
:created-at="note.created_at"
:note-id="note.id"
2022-10-11 01:57:18 +05:30
:is-internal-note="note.internal"
2022-06-21 17:19:12 +05:30
:noteable-type="noteableType"
2020-05-24 23:13:21 +05:30
>
2021-09-30 23:02:18 +05:30
<template #note-header-info>
<slot name="note-header-info"></slot>
</template>
2020-11-24 15:15:51 +05:30
<span v-if="commit" v-safe-html="actionText"></span>
2020-05-24 23:13:21 +05:30
<span v-else-if="note.created_at" class="d-none d-sm-inline">&middot;</span>
2019-03-02 22:35:43 +05:30
</note-header>
2019-02-15 15:39:39 +05:30
<note-actions
2020-06-23 00:09:42 +05:30
:author="author"
2019-02-15 15:39:39 +05:30
:author-id="author.id"
:note-id="note.id"
:note-url="note.noteable_note_url"
:access-level="note.human_access"
2020-11-24 15:15:51 +05:30
:is-contributor="note.is_contributor"
:is-author="note.is_noteable_author"
:project-name="note.project_name"
:noteable-type="note.noteable_type"
2019-03-02 22:35:43 +05:30
:show-reply="showReplyButton"
2019-02-15 15:39:39 +05:30
:can-edit="note.current_user.can_edit"
:can-award-emoji="note.current_user.can_award_emoji"
:can-delete="note.current_user.can_edit"
:can-report-as-abuse="canReportAsAbuse"
2019-07-31 22:56:46 +05:30
:can-resolve="canResolve"
:resolvable="note.resolvable || note.isDraft"
:is-resolved="note.resolved || note.resolve_discussion"
2019-02-15 15:39:39 +05:30
:is-resolving="isResolving"
:resolved-by="note.resolved_by"
2019-07-31 22:56:46 +05:30
:is-draft="note.isDraft"
:resolve-discussion="note.isDraft && note.resolve_discussion"
:discussion-id="discussionId"
2021-04-17 20:07:23 +05:30
:award-path="note.toggle_award_path"
2019-02-15 15:39:39 +05:30
@handleEdit="editHandler"
@handleDelete="deleteHandler"
@handleResolve="resolveHandler"
2019-07-07 11:18:12 +05:30
@startReplying="$emit('startReplying')"
2020-06-23 00:09:42 +05:30
@updateAssignees="assigneesUpdate"
2019-02-15 15:39:39 +05:30
/>
2019-01-03 12:48:30 +05:30
</div>
2019-02-15 15:39:39 +05:30
<div class="timeline-discussion-body">
<slot name="discussion-resolved-text"></slot>
2019-01-03 12:48:30 +05:30
<note-body
ref="noteBody"
:note="note"
2022-07-23 23:45:48 +05:30
:can-edit="note.current_user.can_edit"
2019-02-15 15:39:39 +05:30
:line="line"
2021-03-11 19:13:27 +05:30
:file="diffFile"
2019-01-03 12:48:30 +05:30
:is-editing="isEditing"
2019-02-15 15:39:39 +05:30
:help-page-path="helpPagePath"
2019-01-03 12:48:30 +05:30
@handleFormUpdate="formUpdateHandler"
@cancelForm="formCancelHandler"
2018-03-17 18:26:18 +05:30
/>
2023-03-04 22:38:38 +05:30
<div class="timeline-discussion-body-footer">
<slot name="after-note-body"></slot>
</div>
2018-03-17 18:26:18 +05:30
</div>
</div>
2019-02-15 15:39:39 +05:30
</timeline-entry-item>
2018-03-17 18:26:18 +05:30
</template>