2022-08-13 15:12:31 +05:30
/ * e s l i n t - d i s a b l e c a m e l c a s e ,
2020-01-01 13:55:28 +05:30
no - unused - expressions , default - case ,
2022-05-07 20:08:51 +05:30
consistent - return , no - param - reassign ,
2020-01-01 13:55:28 +05:30
no - shadow , no - useless - escape ,
2019-12-26 22:10:19 +05:30
class - methods - use - this * /
2018-03-17 18:26:18 +05:30
2017-08-17 22:00:37 +05:30
/* global ResolveService */
2019-09-04 21:01:54 +05:30
/ *
2021-11-11 11:23:49 +05:30
deprecated _notes _spec . js is the spec for the legacy , jQuery notes application . It has nothing to do with the new , fancy Vue notes app .
2019-09-04 21:01:54 +05:30
* /
2022-08-13 15:12:31 +05:30
import { GlSkeletonLoader } from '@gitlab/ui' ;
2021-03-11 19:13:27 +05:30
import Autosize from 'autosize' ;
2017-08-17 22:00:37 +05:30
import $ from 'jquery' ;
2021-03-11 19:13:27 +05:30
import { escape , uniqueId } from 'lodash' ;
2018-05-09 12:01:36 +05:30
import Vue from 'vue' ;
2023-03-17 16:20:25 +05:30
import { renderGFM } from '~/behaviors/markdown/render_gfm' ;
2023-05-27 22:25:52 +05:30
import { createAlert , VARIANT _INFO } from '~/alert' ;
2023-04-23 21:23:45 +05:30
import { sanitize } from '~/lib/dompurify' ;
2021-03-11 19:13:27 +05:30
import '~/lib/utils/jquery_at_who' ;
2020-01-01 13:55:28 +05:30
import AjaxCache from '~/lib/utils/ajax_cache' ;
2022-05-07 20:08:51 +05:30
import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js' ;
2020-01-01 13:55:28 +05:30
import syntaxHighlight from '~/syntax_highlight' ;
2021-11-18 22:05:49 +05:30
import CommentTypeDropdown from '~/notes/components/comment_type_dropdown.vue' ;
import * as constants from '~/notes/constants' ;
2022-05-07 20:08:51 +05:30
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal' ;
2021-03-11 19:13:27 +05:30
import Autosave from './autosave' ;
import loadAwardsHandler from './awards_handler' ;
2018-11-08 19:23:39 +05:30
import { defaultAutocompleteConfig } from './gfm_auto_complete' ;
2018-03-17 18:26:18 +05:30
import GLForm from './gl_form' ;
2021-03-11 19:13:27 +05:30
import axios from './lib/utils/axios_utils' ;
2018-05-09 12:01:36 +05:30
import {
2022-04-04 11:22:00 +05:30
getCookie ,
2018-05-09 12:01:36 +05:30
isInViewport ,
getPagePath ,
scrollToElement ,
isMetaKey ,
2018-11-08 19:23:39 +05:30
isInMRPage ,
2018-05-09 12:01:36 +05:30
} from './lib/utils/common_utils' ;
2018-03-17 18:26:18 +05:30
import { localTimeAgo } from './lib/utils/datetime_utility' ;
2021-03-11 19:13:27 +05:30
import { getLocationHash } from './lib/utils/url_utility' ;
2019-09-04 21:01:54 +05:30
import { sprintf , s _ _ , _ _ } from './locale' ;
2021-03-11 19:13:27 +05:30
import TaskList from './task_list' ;
2017-09-10 17:25:29 +05:30
2018-03-17 18:26:18 +05:30
window . autosize = Autosize ;
2017-09-10 17:25:29 +05:30
function normalizeNewlines ( str ) {
return str . replace ( /\r\n/g , '\n' ) ;
}
const MAX _VISIBLE _COMMIT _LIST _COUNT = 3 ;
const REGEX _QUICK _ACTIONS = /^\/\w+.*$/gm ;
export default class Notes {
2023-05-27 22:25:52 +05:30
static initialize ( notes _url , last _fetched _at , view , enableGFM ) {
2018-03-17 18:26:18 +05:30
if ( ! this . instance ) {
2023-05-27 22:25:52 +05:30
this . instance = new Notes ( notes _url , last _fetched _at , view , enableGFM ) ;
2018-03-17 18:26:18 +05:30
}
}
2018-03-27 19:54:05 +05:30
static getInstance ( ) {
return this . instance ;
}
2023-05-27 22:25:52 +05:30
constructor ( notes _url , last _fetched _at , view , enableGFM = defaultAutocompleteConfig ) {
2017-09-10 17:25:29 +05:30
this . updateTargetButtons = this . updateTargetButtons . bind ( this ) ;
this . updateComment = this . updateComment . bind ( this ) ;
this . visibilityChange = this . visibilityChange . bind ( this ) ;
this . cancelDiscussionForm = this . cancelDiscussionForm . bind ( this ) ;
this . onAddDiffNote = this . onAddDiffNote . bind ( this ) ;
2018-03-17 18:26:18 +05:30
this . onAddImageDiffNote = this . onAddImageDiffNote . bind ( this ) ;
2017-09-10 17:25:29 +05:30
this . setupDiscussionNoteForm = this . setupDiscussionNoteForm . bind ( this ) ;
this . onReplyToDiscussionNote = this . onReplyToDiscussionNote . bind ( this ) ;
this . removeNote = this . removeNote . bind ( this ) ;
this . cancelEdit = this . cancelEdit . bind ( this ) ;
this . updateNote = this . updateNote . bind ( this ) ;
this . addDiscussionNote = this . addDiscussionNote . bind ( this ) ;
this . addNoteError = this . addNoteError . bind ( this ) ;
this . addNote = this . addNote . bind ( this ) ;
this . resetMainTargetForm = this . resetMainTargetForm . bind ( this ) ;
this . refresh = this . refresh . bind ( this ) ;
this . keydownNoteText = this . keydownNoteText . bind ( this ) ;
this . toggleCommitList = this . toggleCommitList . bind ( this ) ;
this . postComment = this . postComment . bind ( this ) ;
2023-03-04 22:38:38 +05:30
this . clearAlertWrapper = this . clearAlert . bind ( this ) ;
2017-09-10 17:25:29 +05:30
this . onHashChange = this . onHashChange . bind ( this ) ;
2023-05-27 22:25:52 +05:30
this . note _ids = [ ] ;
2017-09-10 17:25:29 +05:30
this . notes _url = notes _url ;
this . enableGFM = enableGFM ;
// Used to keep track of updated notes while people are editing things
this . updatedNotesTrackingMap = { } ;
this . last _fetched _at = last _fetched _at ;
this . noteable _url = document . URL ;
2018-05-09 12:01:36 +05:30
this . notesCountBadge ||
( this . notesCountBadge = $ ( '.issuable-details' ) . find ( '.notes-tab .badge' ) ) ;
2017-09-10 17:25:29 +05:30
this . basePollingInterval = 15000 ;
this . maxPollingSteps = 4 ;
2018-11-08 19:23:39 +05:30
this . $wrapperEl = isInMRPage ( ) ? $ ( document ) . find ( '.diffs' ) : $ ( document ) ;
2017-09-10 17:25:29 +05:30
this . cleanBinding ( ) ;
this . addBinding ( ) ;
this . setPollingInterval ( ) ;
2018-11-08 19:23:39 +05:30
this . setupMainTargetNoteForm ( enableGFM ) ;
2017-09-10 17:25:29 +05:30
this . taskList = new TaskList ( {
dataType : 'note' ,
fieldName : 'note' ,
2018-05-09 12:01:36 +05:30
selector : '.notes' ,
2017-09-10 17:25:29 +05:30
} ) ;
this . collapseLongCommitList ( ) ;
this . setViewType ( view ) ;
2021-04-29 21:17:54 +05:30
// We are in the merge requests page so we need another edit form for Changes tab
2018-03-17 18:26:18 +05:30
if ( getPagePath ( 1 ) === 'merge_requests' ) {
2021-03-08 18:12:59 +05:30
$ ( '.note-edit-form' ) . clone ( ) . addClass ( 'mr-note-edit-form' ) . insertAfter ( '.note-edit-form' ) ;
2018-05-09 12:01:36 +05:30
}
const hash = getLocationHash ( ) ;
const $anchor = hash && document . getElementById ( hash ) ;
if ( $anchor ) {
this . loadLazyDiff ( { currentTarget : $anchor } ) ;
2017-09-10 17:25:29 +05:30
}
}
setViewType ( view ) {
2022-04-04 11:22:00 +05:30
this . view = getCookie ( 'diff_view' ) || view ;
2017-09-10 17:25:29 +05:30
}
addBinding ( ) {
// Edit note link
2018-03-27 19:54:05 +05:30
this . $wrapperEl . on ( 'click' , '.js-note-edit' , this . showEditForm . bind ( this ) ) ;
this . $wrapperEl . on ( 'click' , '.note-edit-cancel' , this . cancelEdit ) ;
2017-09-10 17:25:29 +05:30
// Reopen and close actions for Issue/MR combined with note form submit
2021-11-18 22:05:49 +05:30
this . $wrapperEl . on (
'click' ,
// this oddly written selector needs to match the old style (input with class) as
// well as the new DOM styling from the Vue-based note form
'input.js-comment-submit-button, .js-comment-submit-button > button:first-child' ,
this . postComment ,
) ;
2018-03-27 19:54:05 +05:30
this . $wrapperEl . on ( 'click' , '.js-comment-save-button' , this . updateComment ) ;
2018-11-08 19:23:39 +05:30
this . $wrapperEl . on ( 'keyup input' , '.js-note-text' , this . updateTargetButtons ) ;
2017-09-10 17:25:29 +05:30
// resolve a discussion
2018-03-27 19:54:05 +05:30
this . $wrapperEl . on ( 'click' , '.js-comment-resolve-button' , this . postComment ) ;
2017-09-10 17:25:29 +05:30
// remove a note (in general)
2022-06-21 17:19:12 +05:30
this . $wrapperEl . on ( 'ajax:success' , '.js-note-delete' , this . removeNote ) ;
2017-09-10 17:25:29 +05:30
// delete note attachment
2018-11-08 19:23:39 +05:30
this . $wrapperEl . on ( 'click' , '.js-note-attachment-delete' , this . removeAttachment ) ;
2017-09-10 17:25:29 +05:30
// update the file name when an attachment is selected
2018-11-08 19:23:39 +05:30
this . $wrapperEl . on ( 'change' , '.js-note-attachment-input' , this . updateFormAttachment ) ;
2017-09-10 17:25:29 +05:30
// reply to diff/discussion notes
2018-11-08 19:23:39 +05:30
this . $wrapperEl . on ( 'click' , '.js-discussion-reply-button' , this . onReplyToDiscussionNote ) ;
2017-09-10 17:25:29 +05:30
// add diff note
2018-03-27 19:54:05 +05:30
this . $wrapperEl . on ( 'click' , '.js-add-diff-note-button' , this . onAddDiffNote ) ;
2018-03-17 18:26:18 +05:30
// add diff note for images
2018-11-08 19:23:39 +05:30
this . $wrapperEl . on ( 'click' , '.js-add-image-diff-note-button' , this . onAddImageDiffNote ) ;
2017-09-10 17:25:29 +05:30
// hide diff note form
2018-11-08 19:23:39 +05:30
this . $wrapperEl . on ( 'click' , '.js-close-discussion-note-form' , this . cancelDiscussionForm ) ;
2017-09-10 17:25:29 +05:30
// toggle commit list
2018-11-08 19:23:39 +05:30
this . $wrapperEl . on ( 'click' , '.system-note-commit-list-toggler' , this . toggleCommitList ) ;
2018-05-09 12:01:36 +05:30
this . $wrapperEl . on ( 'click' , '.js-toggle-lazy-diff' , this . loadLazyDiff ) ;
2018-11-20 20:47:30 +05:30
this . $wrapperEl . on (
'click' ,
'.js-toggle-lazy-diff-retry-button' ,
this . onClickRetryLazyLoad . bind ( this ) ,
) ;
2018-05-09 12:01:36 +05:30
2017-09-10 17:25:29 +05:30
// fetch notes when tab becomes visible
2018-03-27 19:54:05 +05:30
this . $wrapperEl . on ( 'visibilitychange' , this . visibilityChange ) ;
2017-09-10 17:25:29 +05:30
// when issue status changes, we need to refresh data
2018-03-27 19:54:05 +05:30
this . $wrapperEl . on ( 'issuable:change' , this . refresh ) ;
2017-09-10 17:25:29 +05:30
// ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs.
2018-03-27 19:54:05 +05:30
this . $wrapperEl . on ( 'ajax:success' , '.js-main-target-form' , this . addNote ) ;
2018-11-08 19:23:39 +05:30
this . $wrapperEl . on ( 'ajax:success' , '.js-discussion-note-form' , this . addDiscussionNote ) ;
this . $wrapperEl . on ( 'ajax:success' , '.js-main-target-form' , this . resetMainTargetForm ) ;
2018-05-09 12:01:36 +05:30
this . $wrapperEl . on (
'ajax:complete' ,
'.js-main-target-form' ,
this . reenableTargetFormSubmitButton ,
) ;
2017-09-10 17:25:29 +05:30
// when a key is clicked on the notes
2018-03-27 19:54:05 +05:30
this . $wrapperEl . on ( 'keydown' , '.js-note-text' , this . keydownNoteText ) ;
2017-09-10 17:25:29 +05:30
// When the URL fragment/hash has changed, `#note_xxx`
2018-03-27 19:54:05 +05:30
$ ( window ) . on ( 'hashchange' , this . onHashChange ) ;
2017-09-10 17:25:29 +05:30
}
cleanBinding ( ) {
2018-03-27 19:54:05 +05:30
this . $wrapperEl . off ( 'click' , '.js-note-edit' ) ;
this . $wrapperEl . off ( 'click' , '.note-edit-cancel' ) ;
2022-06-21 17:19:12 +05:30
this . $wrapperEl . off ( 'ajax:success' , '.js-note-delete' ) ;
2018-03-27 19:54:05 +05:30
this . $wrapperEl . off ( 'click' , '.js-note-attachment-delete' ) ;
this . $wrapperEl . off ( 'click' , '.js-discussion-reply-button' ) ;
this . $wrapperEl . off ( 'click' , '.js-add-diff-note-button' ) ;
this . $wrapperEl . off ( 'click' , '.js-add-image-diff-note-button' ) ;
2021-02-22 17:27:13 +05:30
// eslint-disable-next-line @gitlab/no-global-event-off
2018-03-27 19:54:05 +05:30
this . $wrapperEl . off ( 'visibilitychange' ) ;
this . $wrapperEl . off ( 'keyup input' , '.js-note-text' ) ;
this . $wrapperEl . off ( 'click' , '.js-note-target-reopen' ) ;
this . $wrapperEl . off ( 'click' , '.js-note-target-close' ) ;
this . $wrapperEl . off ( 'keydown' , '.js-note-text' ) ;
this . $wrapperEl . off ( 'click' , '.js-comment-resolve-button' ) ;
this . $wrapperEl . off ( 'click' , '.system-note-commit-list-toggler' ) ;
2018-05-09 12:01:36 +05:30
this . $wrapperEl . off ( 'click' , '.js-toggle-lazy-diff' ) ;
this . $wrapperEl . off ( 'click' , '.js-toggle-lazy-diff-retry-button' ) ;
2018-03-27 19:54:05 +05:30
this . $wrapperEl . off ( 'ajax:success' , '.js-main-target-form' ) ;
this . $wrapperEl . off ( 'ajax:success' , '.js-discussion-note-form' ) ;
this . $wrapperEl . off ( 'ajax:complete' , '.js-main-target-form' ) ;
2017-09-10 17:25:29 +05:30
$ ( window ) . off ( 'hashchange' , this . onHashChange ) ;
}
static initCommentTypeToggle ( form ) {
2021-11-18 22:05:49 +05:30
const el = form . querySelector ( '.js-comment-type-dropdown' ) ;
const { noteableName } = el . dataset ;
2017-09-10 17:25:29 +05:30
const noteTypeInput = form . querySelector ( '#note_type' ) ;
2021-11-18 22:05:49 +05:30
const formHasContent = form . querySelector ( '.js-note-text' ) . value . trim ( ) . length > 0 ;
2017-09-10 17:25:29 +05:30
2021-11-18 22:05:49 +05:30
form . commentTypeComponent = new Vue ( {
el ,
data ( ) {
return {
noteType : constants . COMMENT ,
disabled : ! formHasContent ,
} ;
} ,
render ( createElement ) {
return createElement ( CommentTypeDropdown , {
props : {
noteType : this . noteType ,
noteableDisplayName : noteableName ,
disabled : this . disabled ,
} ,
on : {
change : ( arg ) => {
this . noteType = arg ;
if ( this . noteType === constants . DISCUSSION ) {
noteTypeInput . value = constants . DISCUSSION _NOTE ;
} else {
noteTypeInput . value = '' ;
}
} ,
} ,
} ) ;
} ,
} ) ;
2017-09-10 17:25:29 +05:30
}
2022-05-07 20:08:51 +05:30
async keydownNoteText ( e ) {
2020-01-01 13:55:28 +05:30
let discussionNoteForm ;
let editNote ;
let myLastNote ;
let myLastNoteEditBtn ;
let newText ;
let originalText ;
2018-03-17 18:26:18 +05:30
if ( isMetaKey ( e ) ) {
2017-09-10 17:25:29 +05:30
return ;
2016-09-13 17:45:13 +05:30
}
2020-01-01 13:55:28 +05:30
const $textarea = $ ( e . target ) ;
2017-09-10 17:25:29 +05:30
// Edit previous note when UP arrow is hit
switch ( e . which ) {
case 38 :
if ( $textarea . val ( ) !== '' ) {
return ;
}
2018-05-09 12:01:36 +05:30
myLastNote = $ (
2018-11-08 19:23:39 +05:30
` li.note[data-author-id=' ${ gon . current _user _id } '][data-editable]:last ` ,
2018-05-09 12:01:36 +05:30
$textarea . closest ( '.note, .notes_holder, #notes' ) ,
) ;
2017-09-10 17:25:29 +05:30
if ( myLastNote . length ) {
myLastNoteEditBtn = myLastNote . find ( '.js-note-edit' ) ;
return myLastNoteEditBtn . trigger ( 'click' , [ true , myLastNote ] ) ;
}
break ;
// Cancel creating diff note or editing any note when ESCAPE is hit
case 27 :
discussionNoteForm = $textarea . closest ( '.js-discussion-note-form' ) ;
if ( discussionNoteForm . length ) {
2016-09-13 17:45:13 +05:30
if ( $textarea . val ( ) !== '' ) {
2022-05-07 20:08:51 +05:30
const confirmed = await confirmAction ( _ _ ( 'Your comment will be discarded.' ) , {
primaryBtnVariant : 'danger' ,
primaryBtnText : _ _ ( 'Discard' ) ,
} ) ;
if ( ! confirmed ) return ;
2016-09-13 17:45:13 +05:30
}
2017-09-10 17:25:29 +05:30
this . removeDiscussionNoteForm ( discussionNoteForm ) ;
return ;
}
editNote = $textarea . closest ( '.note' ) ;
if ( editNote . length ) {
2018-03-27 19:54:05 +05:30
originalText = $textarea . closest ( 'form' ) . data ( 'originalNote' ) ;
2017-09-10 17:25:29 +05:30
newText = $textarea . val ( ) ;
if ( originalText !== newText ) {
2022-05-07 20:08:51 +05:30
const confirmed = await confirmAction (
_ _ ( 'Are you sure you want to discard this comment?' ) ,
{
primaryBtnVariant : 'danger' ,
primaryBtnText : _ _ ( 'Discard' ) ,
} ,
) ;
if ( ! confirmed ) return ;
2016-09-13 17:45:13 +05:30
}
2017-09-10 17:25:29 +05:30
return this . removeNoteEditForm ( editNote ) ;
}
}
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
initRefresh ( ) {
if ( Notes . interval ) {
2016-09-13 17:45:13 +05:30
clearInterval ( Notes . interval ) ;
2017-09-10 17:25:29 +05:30
}
2019-12-26 22:10:19 +05:30
Notes . interval = setInterval ( ( ) => this . refresh ( ) , this . pollingInterval ) ;
2017-09-10 17:25:29 +05:30
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
refresh ( ) {
if ( ! document . hidden ) {
return this . getContent ( ) ;
}
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
getContent ( ) {
if ( this . refreshing ) {
return ;
}
2018-03-27 19:54:05 +05:30
2017-09-10 17:25:29 +05:30
this . refreshing = true ;
2018-03-27 19:54:05 +05:30
2018-05-09 12:01:36 +05:30
axios
. get ( ` ${ this . notes _url } ?html=true ` , {
headers : {
'X-Last-Fetched-At' : this . last _fetched _at ,
} ,
} )
. then ( ( { data } ) => {
2018-11-08 19:23:39 +05:30
const { notes } = data ;
2018-05-09 12:01:36 +05:30
this . last _fetched _at = data . last _fetched _at ;
this . setPollingInterval ( data . notes . length ) ;
$ . each ( notes , ( i , note ) => this . renderNote ( note ) ) ;
this . refreshing = false ;
} )
. catch ( ( ) => {
this . refreshing = false ;
} ) ;
2017-09-10 17:25:29 +05:30
}
/ * *
* Increase @ pollingInterval up to 120 seconds on every function call ,
* if ` shouldReset ` has a truthy value , 'null' or 'undefined' the variable
* will reset to @ basePollingInterval .
*
* Note : this function is used to gradually increase the polling interval
* if there aren ' t new notes coming from the server
* /
setPollingInterval ( shouldReset ) {
if ( shouldReset == null ) {
shouldReset = true ;
}
2022-08-13 15:12:31 +05:30
const nthInterval = this . basePollingInterval * 2 * * ( this . maxPollingSteps - 1 ) ;
2017-09-10 17:25:29 +05:30
if ( shouldReset ) {
this . pollingInterval = this . basePollingInterval ;
} else if ( this . pollingInterval < nthInterval ) {
this . pollingInterval *= 2 ;
}
return this . initRefresh ( ) ;
}
handleQuickActions ( noteEntity ) {
2020-01-01 13:55:28 +05:30
let votesBlock ;
2017-09-10 17:25:29 +05:30
if ( noteEntity . commands _changes ) {
if ( 'merge' in noteEntity . commands _changes ) {
Notes . checkMergeRequestStatus ( ) ;
2016-09-13 17:45:13 +05:30
}
2017-09-10 17:25:29 +05:30
if ( 'emoji_award' in noteEntity . commands _changes ) {
votesBlock = $ ( '.js-awards-block' ) . eq ( 0 ) ;
2017-08-17 22:00:37 +05:30
2018-05-09 12:01:36 +05:30
loadAwardsHandler ( )
2021-03-08 18:12:59 +05:30
. then ( ( awardsHandler ) => {
2018-11-08 19:23:39 +05:30
awardsHandler . addAwardToEmojiBar ( votesBlock , noteEntity . commands _changes . emoji _award ) ;
2018-05-09 12:01:36 +05:30
awardsHandler . scrollToAwards ( ) ;
} )
. catch ( ( ) => {
// ignore
} ) ;
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
}
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
setupNewNote ( $note ) {
// Update datetime format on the recent note
2021-09-30 23:02:18 +05:30
localTimeAgo ( $note . find ( '.js-timeago' ) . get ( ) , false ) ;
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
this . collapseLongCommitList ( ) ;
this . taskList . init ( ) ;
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
// This stops the note highlight, #note_xxx`, from being removed after real time update
// The `:target` selector does not re-evaluate after we replace element in the DOM
Notes . updateNoteTargetSelector ( $note ) ;
this . $noteToCleanHighlight = $note ;
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
onHashChange ( ) {
if ( this . $noteToCleanHighlight ) {
Notes . updateNoteTargetSelector ( this . $noteToCleanHighlight ) ;
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
this . $noteToCleanHighlight = null ;
}
static updateNoteTargetSelector ( $note ) {
2018-03-17 18:26:18 +05:30
const hash = getLocationHash ( ) ;
2017-09-10 17:25:29 +05:30
// Needs to be an explicit true/false for the jQuery `toggleClass(force)`
const addTargetClass = Boolean ( hash && $note . filter ( ` # ${ hash } ` ) . length > 0 ) ;
$note . toggleClass ( 'target' , addTargetClass ) ;
}
/ * *
* Render note in main comments area .
*
* Note : for rendering inline notes use renderDiscussionNote
* /
renderNote ( noteEntity , $form , $notesList = $ ( '.main-notes-list' ) ) {
if ( noteEntity . discussion _html ) {
return this . renderDiscussionNote ( noteEntity , $form ) ;
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
if ( ! noteEntity . valid ) {
2018-03-27 19:54:05 +05:30
if ( noteEntity . errors && noteEntity . errors . commands _only ) {
2018-11-08 19:23:39 +05:30
if ( noteEntity . commands _changes && Object . keys ( noteEntity . commands _changes ) . length > 0 ) {
2017-09-10 17:25:29 +05:30
$notesList . find ( '.system-note.being-posted' ) . remove ( ) ;
2017-08-17 22:00:37 +05:30
}
2023-03-04 22:38:38 +05:30
this . addAlert ( {
2021-04-29 21:17:54 +05:30
message : noteEntity . errors . commands _only ,
2023-03-04 22:38:38 +05:30
variant : VARIANT _INFO ,
2021-04-29 21:17:54 +05:30
parent : this . parentTimeline . get ( 0 ) ,
} ) ;
2017-09-10 17:25:29 +05:30
this . refresh ( ) ;
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
return ;
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
const $note = $notesList . find ( ` #note_ ${ noteEntity . id } ` ) ;
if ( Notes . isNewNote ( noteEntity , this . note _ids ) ) {
2018-11-08 19:23:39 +05:30
if ( isInMRPage ( ) ) {
2018-03-27 19:54:05 +05:30
return ;
}
2017-09-10 17:25:29 +05:30
if ( $notesList . length ) {
$notesList . find ( '.system-note.being-posted' ) . remove ( ) ;
2016-09-13 17:45:13 +05:30
}
2017-09-10 17:25:29 +05:30
const $newNote = Notes . animateAppendNote ( noteEntity . html , $notesList ) ;
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
this . setupNewNote ( $newNote ) ;
this . refresh ( ) ;
2016-09-13 17:45:13 +05:30
return this . updateNotesCount ( 1 ) ;
2018-05-09 12:01:36 +05:30
} else if ( Notes . isUpdatedNote ( noteEntity , $note ) ) {
// The server can send the same update multiple times so we need to make sure to only update once per actual update.
2017-09-10 17:25:29 +05:30
const isEditing = $note . hasClass ( 'is-editing' ) ;
2021-03-08 18:12:59 +05:30
const initialContent = normalizeNewlines ( $note . find ( '.original-note-content' ) . text ( ) . trim ( ) ) ;
2017-09-10 17:25:29 +05:30
const $textarea = $note . find ( '.js-note-text' ) ;
const currentContent = $textarea . val ( ) ;
// There can be CRLF vs LF mismatches if we don't sanitize and compare the same way
const sanitizedNoteNote = normalizeNewlines ( noteEntity . note ) ;
2018-05-09 12:01:36 +05:30
const isTextareaUntouched =
2018-11-08 19:23:39 +05:30
currentContent === initialContent || currentContent === sanitizedNoteNote ;
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
if ( isEditing && isTextareaUntouched ) {
$textarea . val ( noteEntity . note ) ;
this . updatedNotesTrackingMap [ noteEntity . id ] = noteEntity ;
2018-05-09 12:01:36 +05:30
} else if ( isEditing && ! isTextareaUntouched ) {
2017-09-10 17:25:29 +05:30
this . putConflictEditWarningInPlace ( noteEntity , $note ) ;
this . updatedNotesTrackingMap [ noteEntity . id ] = noteEntity ;
2018-05-09 12:01:36 +05:30
} else {
2017-09-10 17:25:29 +05:30
const $updatedNote = Notes . animateUpdateNote ( noteEntity . html , $note ) ;
this . setupNewNote ( $updatedNote ) ;
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
}
}
isParallelView ( ) {
2022-04-04 11:22:00 +05:30
return getCookie ( 'diff_view' ) === 'parallel' ;
2017-09-10 17:25:29 +05:30
}
/ * *
2018-03-27 19:54:05 +05:30
* Render note in discussion area . To render inline notes use renderDiscussionNote .
2017-09-10 17:25:29 +05:30
* /
renderDiscussionNote ( noteEntity , $form ) {
2020-01-01 13:55:28 +05:30
let discussionContainer ;
let row ;
2018-03-27 19:54:05 +05:30
2017-09-10 17:25:29 +05:30
if ( ! Notes . isNewNote ( noteEntity , this . note _ids ) ) {
return ;
}
2018-03-17 18:26:18 +05:30
2020-01-01 13:55:28 +05:30
const form =
$form || $ ( ` .js-discussion-note-form[data-discussion-id=" ${ noteEntity . discussion _id } "] ` ) ;
2018-05-09 12:01:36 +05:30
row =
form . length || ! noteEntity . discussion _line _code
? form . closest ( 'tr' )
: $ ( ` # ${ noteEntity . discussion _line _code } ` ) ;
2018-03-17 18:26:18 +05:30
if ( noteEntity . on _image ) {
row = form ;
}
2017-09-10 17:25:29 +05:30
// is this the first note of discussion?
2018-11-08 19:23:39 +05:30
discussionContainer = $ ( ` .notes[data-discussion-id=" ${ noteEntity . discussion _id } "] ` ) ;
2017-09-10 17:25:29 +05:30
if ( ! discussionContainer . length ) {
discussionContainer = form . closest ( '.discussion' ) . find ( '.notes' ) ;
}
if ( discussionContainer . length === 0 ) {
if ( noteEntity . diff _discussion _html ) {
2023-03-17 16:20:25 +05:30
const discussionElement = document . createElement ( 'table' ) ;
2023-04-23 21:23:45 +05:30
let internalNote ;
let discussionDOM ;
if ( ! noteEntity . on _image ) {
/ *
DOMPurify will strip table - less < tr > / < t d > , s o t o g e t i t t o s t o p d e l e t i n g
nodes ( since our note HTML starts with a table - less < tr > ) , we need to wrap
the noteEntity discussion HTML in a < table > to perform the other
sanitization .
* /
internalNote = sanitize ( ` <table> ${ noteEntity . diff _discussion _html } </table> ` , {
RETURN _DOM : true ,
} ) ;
/ *
Since we wrapped the < tr > in a < table > , we need to extract the < tr > back out .
DOMPurify returns a Body Element , so we have to start there , then get the
wrapping table , and then get the content we actually want .
Curiously , DOMPurify * * ADDS * * a totally novel < tbody > , so we ' re actually
inserting a completely as - yet - unseen < tbody > element here .
* /
discussionDOM = internalNote . querySelector ( 'table' ) . firstChild ;
} else {
// Image comments don't need <table> manipulation, they're already <div>s
internalNote = sanitize ( noteEntity . diff _discussion _html , {
RETURN _DOM : true ,
} ) ;
discussionDOM = internalNote . firstChild ;
}
discussionElement . insertAdjacentElement ( 'afterbegin' , discussionDOM ) ;
2023-03-17 16:20:25 +05:30
renderGFM ( discussionElement ) ;
const $discussion = $ ( discussionElement ) . unwrap ( ) ;
2016-09-13 17:45:13 +05:30
2018-11-08 19:23:39 +05:30
if ( ! this . isParallelView ( ) || row . hasClass ( 'js-temp-notes-holder' ) || noteEntity . on _image ) {
2017-09-10 17:25:29 +05:30
// insert the note and the reply button after the temp row
row . after ( $discussion ) ;
} else {
// Merge new discussion HTML in
2020-01-01 13:55:28 +05:30
const $notes = $discussion . find (
` .notes[data-discussion-id=" ${ noteEntity . discussion _id } "] ` ,
) ;
const contentContainerClass = $notes
2019-12-21 20:55:43 +05:30
. closest ( '.notes-content' )
. attr ( 'class' )
. split ( ' ' )
. join ( '.' ) ;
2018-05-09 12:01:36 +05:30
row
2019-12-21 20:55:43 +05:30
. find ( ` . ${ contentContainerClass } .content ` )
2018-05-09 12:01:36 +05:30
. append ( $notes . closest ( '.content' ) . children ( ) ) ;
2016-09-13 17:45:13 +05:30
}
2018-11-08 19:23:39 +05:30
} else {
Notes . animateAppendNote ( noteEntity . discussion _html , $ ( '.main-notes-list' ) ) ;
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
} else {
// append new note to all matching discussions
Notes . animateAppendNote ( noteEntity . html , discussionContainer ) ;
}
2016-09-13 17:45:13 +05:30
2021-09-30 23:02:18 +05:30
localTimeAgo ( document . querySelectorAll ( '.js-timeago' ) , false ) ;
2017-09-10 17:25:29 +05:30
Notes . checkMergeRequestStatus ( ) ;
return this . updateNotesCount ( 1 ) ;
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
getLineHolder ( changesDiscussionContainer ) {
2018-05-09 12:01:36 +05:30
return $ ( changesDiscussionContainer )
. closest ( '.notes_holder' )
2017-09-10 17:25:29 +05:30
. prevAll ( '.line_holder' )
. first ( )
. get ( 0 ) ;
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
/ * *
* Called in response the main target form has been successfully submitted .
*
* Removes any errors .
* Resets text and preview .
* Resets buttons .
* /
resetMainTargetForm ( e ) {
2020-01-01 13:55:28 +05:30
const form = $ ( '.js-main-target-form' ) ;
2017-09-10 17:25:29 +05:30
// remove validation errors
form . find ( '.js-errors' ) . remove ( ) ;
// reset text and preview
form . find ( '.js-md-write-button' ) . click ( ) ;
2021-03-08 18:12:59 +05:30
form . find ( '.js-note-text' ) . val ( '' ) . trigger ( 'input' ) ;
2023-03-17 16:20:25 +05:30
form . find ( '.js-note-text' ) . each ( function reset ( ) {
this . $autosave . reset ( ) ;
} ) ;
2017-09-10 17:25:29 +05:30
2020-01-01 13:55:28 +05:30
const event = document . createEvent ( 'Event' ) ;
2017-09-10 17:25:29 +05:30
event . initEvent ( 'autosize:update' , true , false ) ;
form . find ( '.js-autosize' ) [ 0 ] . dispatchEvent ( event ) ;
this . updateTargetButtons ( e ) ;
}
reenableTargetFormSubmitButton ( ) {
2020-01-01 13:55:28 +05:30
const form = $ ( '.js-main-target-form' ) ;
2017-09-10 17:25:29 +05:30
return form . find ( '.js-note-text' ) . trigger ( 'input' ) ;
}
/ * *
* Shows the main form and does some setup on it .
*
* Sets some hidden fields in the form .
* /
2018-11-08 19:23:39 +05:30
setupMainTargetNoteForm ( enableGFM ) {
2017-09-10 17:25:29 +05:30
// find the form
2020-01-01 13:55:28 +05:30
const form = $ ( '.js-new-note-form' ) ;
2017-09-10 17:25:29 +05:30
// Set a global clone of the form for later cloning
this . formClone = form . clone ( ) ;
// show the form
2018-11-08 19:23:39 +05:30
this . setupNoteForm ( form , enableGFM ) ;
2017-09-10 17:25:29 +05:30
// fix classes
form . removeClass ( 'js-new-note-form' ) ;
form . addClass ( 'js-main-target-form' ) ;
form . find ( '#note_line_code' ) . remove ( ) ;
form . find ( '#note_position' ) . remove ( ) ;
form . find ( '#note_type' ) . val ( '' ) ;
form . find ( '#note_project_id' ) . remove ( ) ;
form . find ( '#in_reply_to_discussion_id' ) . remove ( ) ;
this . parentTimeline = form . parents ( '.timeline' ) ;
if ( form . length ) {
Notes . initCommentTypeToggle ( form . get ( 0 ) ) ;
}
}
/ * *
* General note form setup .
*
* deactivates the submit button when text is empty
* hides the preview button when text is empty
2018-12-05 23:21:45 +05:30
* set up GFM auto complete
2017-09-10 17:25:29 +05:30
* show the form
* /
2018-11-08 19:23:39 +05:30
setupNoteForm ( form , enableGFM = defaultAutocompleteConfig ) {
this . glForm = new GLForm ( form , enableGFM ) ;
2020-01-01 13:55:28 +05:30
const textarea = form . find ( '.js-note-text' ) ;
const key = [
2019-09-04 21:01:54 +05:30
s _ _ ( 'NoteForm|Note' ) ,
2017-09-10 17:25:29 +05:30
form . find ( '#note_noteable_type' ) . val ( ) ,
form . find ( '#note_noteable_id' ) . val ( ) ,
form . find ( '#note_commit_id' ) . val ( ) ,
form . find ( '#note_type' ) . val ( ) ,
form . find ( '#note_project_id' ) . val ( ) ,
form . find ( '#in_reply_to_discussion_id' ) . val ( ) ,
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
// LegacyDiffNote
form . find ( '#note_line_code' ) . val ( ) ,
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
// DiffNote
2018-03-17 18:26:18 +05:30
form . find ( '#note_position' ) . val ( ) ,
2017-09-10 17:25:29 +05:30
] ;
2023-03-17 16:20:25 +05:30
const textareaEl = textarea . get ( 0 ) ;
// eslint-disable-next-line no-new
if ( textareaEl ) new Autosave ( textareaEl , key ) ;
2017-09-10 17:25:29 +05:30
}
/ * *
* Called in response to the new note form being submitted
*
* Adds new note to list .
* /
addNote ( $form , note ) {
return this . renderNote ( note ) ;
}
addNoteError ( $form ) {
let formParentTimeline ;
if ( $form . hasClass ( 'js-main-target-form' ) ) {
formParentTimeline = $form . parents ( '.timeline' ) ;
} else if ( $form . hasClass ( 'js-discussion-note-form' ) ) {
formParentTimeline = $form . closest ( '.discussion-notes' ) . find ( '.notes' ) ;
}
2023-03-04 22:38:38 +05:30
return this . addAlert ( {
2021-04-29 21:17:54 +05:30
message : _ _ (
2019-09-04 21:01:54 +05:30
'Your comment could not be submitted! Please check your network connection and try again.' ,
) ,
2021-04-29 21:17:54 +05:30
parent : formParentTimeline . get ( 0 ) ,
} ) ;
2017-09-10 17:25:29 +05:30
}
2019-12-04 20:38:33 +05:30
updateNoteError ( ) {
2023-03-04 22:38:38 +05:30
createAlert ( {
2021-04-29 21:17:54 +05:30
message : _ _ (
'Your comment could not be updated! Please check your network connection and try again.' ,
) ,
} ) ;
2017-09-10 17:25:29 +05:30
}
/ * *
* Called in response to the new note form being submitted
*
* Adds new note to list .
* /
addDiscussionNote ( $form , note , isNewDiffComment ) {
if ( $form . attr ( 'data-resolve-all' ) != null ) {
2020-01-01 13:55:28 +05:30
const discussionId = $form . data ( 'discussionId' ) ;
const mergeRequestId = $form . data ( 'noteableIid' ) ;
2017-09-10 17:25:29 +05:30
if ( ResolveService != null ) {
ResolveService . toggleResolveForDiscussion ( mergeRequestId , discussionId ) ;
}
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
this . renderNote ( note , $form ) ;
// cleanup after successfully creating a diff/discussion note
if ( isNewDiffComment ) {
this . removeDiscussionNoteForm ( $form ) ;
}
}
/ * *
* Called in response to the edit note form being submitted
*
* Updates the current note field .
* /
updateNote ( noteEntity , $targetNote ) {
// Convert returned HTML to a jQuery object so we can modify it further
2020-01-01 13:55:28 +05:30
const $noteEntityEl = $ ( noteEntity . html ) ;
2021-11-11 11:23:49 +05:30
const $noteAvatar = $noteEntityEl . find ( '.image-diff-avatar-link' ) ;
2022-04-04 11:22:00 +05:30
const $targetNoteBadge = $targetNote . find ( '.design-note-pin' ) ;
2021-11-11 11:23:49 +05:30
$noteAvatar . append ( $targetNoteBadge ) ;
2017-09-10 17:25:29 +05:30
this . revertNoteEditForm ( $targetNote ) ;
2023-05-27 22:25:52 +05:30
renderGFM ( Notes . getNodeToRender ( $noteEntityEl ) ) ;
2017-09-10 17:25:29 +05:30
// Find the note's `li` element by ID and replace it with the updated HTML
2020-01-01 13:55:28 +05:30
const $note _li = $ ( ` .note-row- ${ noteEntity . id } ` ) ;
2017-09-10 17:25:29 +05:30
$note _li . replaceWith ( $noteEntityEl ) ;
this . setupNewNote ( $noteEntityEl ) ;
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
checkContentToAllowEditing ( $el ) {
2021-03-08 18:12:59 +05:30
const initialContent = $el . find ( '.original-note-content' ) . text ( ) . trim ( ) ;
2020-01-01 13:55:28 +05:30
const currentContent = $el . find ( '.js-note-text' ) . val ( ) ;
let isAllowed = true ;
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
if ( currentContent === initialContent ) {
this . removeNoteEditForm ( $el ) ;
2018-05-09 12:01:36 +05:30
} else {
2020-01-01 13:55:28 +05:30
const isWidgetVisible = isInViewport ( $el . get ( 0 ) ) ;
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
if ( ! isWidgetVisible ) {
2018-03-17 18:26:18 +05:30
scrollToElement ( $el ) ;
2016-09-13 17:45:13 +05:30
}
2017-09-10 17:25:29 +05:30
$el . find ( '.js-finish-edit-warning' ) . show ( ) ;
isAllowed = false ;
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
return isAllowed ;
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
/ * *
* Called in response to clicking the edit note link
*
* Replaces the note text with the note edit form
* Adds a data attribute to the form with the original content of the note for cancellations
* /
2019-12-04 20:38:33 +05:30
showEditForm ( e ) {
2017-09-10 17:25:29 +05:30
e . preventDefault ( ) ;
2016-09-13 17:45:13 +05:30
2020-01-01 13:55:28 +05:30
const $target = $ ( e . target ) ;
const $editForm = $ ( this . getEditFormSelector ( $target ) ) ;
const $note = $target . closest ( '.note' ) ;
const $currentlyEditing = $ ( '.note.is-editing:visible' ) ;
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
if ( $currentlyEditing . length ) {
2020-01-01 13:55:28 +05:30
const isEditAllowed = this . checkContentToAllowEditing ( $currentlyEditing ) ;
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
if ( ! isEditAllowed ) {
return ;
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
$note . find ( '.js-note-attachment-delete' ) . show ( ) ;
$editForm . addClass ( 'current-note-edit-form' ) ;
$note . addClass ( 'is-editing' ) ;
this . putEditFormInPlace ( $target ) ;
}
/ * *
* Called in response to clicking the edit note link
*
* Hides edit form and restores the original note text to the editor textarea .
* /
cancelEdit ( e ) {
e . preventDefault ( ) ;
const $target = $ ( e . target ) ;
const $note = $target . closest ( '.note' ) ;
const noteId = $note . attr ( 'data-note-id' ) ;
this . revertNoteEditForm ( $target ) ;
if ( this . updatedNotesTrackingMap [ noteId ] ) {
const $newNote = $ ( this . updatedNotesTrackingMap [ noteId ] . html ) ;
$note . replaceWith ( $newNote ) ;
this . setupNewNote ( $newNote ) ;
// Now that we have taken care of the update, clear it out
delete this . updatedNotesTrackingMap [ noteId ] ;
2018-05-09 12:01:36 +05:30
} else {
2017-09-10 17:25:29 +05:30
$note . find ( '.js-finish-edit-warning' ) . hide ( ) ;
this . removeNoteEditForm ( $note ) ;
}
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
revertNoteEditForm ( $target ) {
$target = $target || $ ( '.note.is-editing:visible' ) ;
2020-01-01 13:55:28 +05:30
const selector = this . getEditFormSelector ( $target ) ;
const $editForm = $ ( selector ) ;
2017-08-17 22:00:37 +05:30
2018-03-27 19:54:05 +05:30
$editForm . insertBefore ( '.diffs' ) ;
2017-09-10 17:25:29 +05:30
$editForm . find ( '.js-comment-save-button' ) . enable ( ) ;
$editForm . find ( '.js-finish-edit-warning' ) . hide ( ) ;
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
getEditFormSelector ( $el ) {
2020-01-01 13:55:28 +05:30
let selector = '.note-edit-form:not(.mr-note-edit-form)' ;
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
if ( $el . parents ( '#diffs' ) . length ) {
selector = '.note-edit-form.mr-note-edit-form' ;
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
return selector ;
}
removeNoteEditForm ( $note ) {
2020-01-01 13:55:28 +05:30
const form = $note . find ( '.diffs .current-note-edit-form' ) ;
2018-03-27 19:54:05 +05:30
2017-09-10 17:25:29 +05:30
$note . removeClass ( 'is-editing' ) ;
form . removeClass ( 'current-note-edit-form' ) ;
form . find ( '.js-finish-edit-warning' ) . hide ( ) ;
// Replace markdown textarea text with original note text.
2018-11-08 19:23:39 +05:30
return form . find ( '.js-note-text' ) . val ( form . find ( 'form.edit-note' ) . data ( 'originalNote' ) ) ;
2017-09-10 17:25:29 +05:30
}
/ * *
* Called in response to deleting a note of any kind .
*
* Removes the actual note from view .
* Removes the whole discussion if the last note is being removed .
* /
removeNote ( e ) {
2020-01-01 13:55:28 +05:30
const $note = $ ( e . currentTarget ) . closest ( '.note' ) ;
2018-05-09 12:01:36 +05:30
2022-06-21 17:19:12 +05:30
$note . one ( 'ajax:complete' , ( ) => {
const noteElId = $note . attr ( 'id' ) ;
$ ( ` .note[id=" ${ noteElId } "] ` ) . each ( ( i , el ) => {
// A same note appears in the "Discussion" and in the "Changes" tab, we have
// to remove all. Using $('.note[id='noteId']') ensure we get all the notes,
// where $('#noteId') would return only one.
const $note = $ ( el ) ;
const $notes = $note . closest ( '.discussion-notes' ) ;
const discussionId = $ ( '.notes' , $notes ) . data ( 'discussionId' ) ;
$note . remove ( ) ;
// check if this is the last note for this line
if ( $notes . find ( '.note' ) . length === 0 ) {
const notesTr = $notes . closest ( 'tr' ) ;
// "Discussions" tab
$notes . closest ( '.timeline-entry' ) . remove ( ) ;
$ ( ` .js-diff-avatars- ${ discussionId } ` ) . trigger ( 'remove.vue' ) ;
// The notes tr can contain multiple lists of notes, like on the parallel diff
// notesTr does not exist for image diffs
if ( notesTr . find ( '.discussion-notes' ) . length > 1 || notesTr . length === 0 ) {
const $diffFile = $notes . closest ( '.diff-file' ) ;
if ( $diffFile . length > 0 ) {
const removeBadgeEvent = new CustomEvent ( 'removeBadge.imageDiff' , {
detail : {
// badgeNumber's start with 1 and index starts with 0
badgeNumber : $notes . index ( ) + 1 ,
} ,
} ) ;
$diffFile [ 0 ] . dispatchEvent ( removeBadgeEvent ) ;
}
$notes . remove ( ) ;
} else if ( notesTr . length > 0 ) {
notesTr . remove ( ) ;
2016-09-13 17:45:13 +05:30
}
2019-12-26 22:10:19 +05:30
}
2022-06-21 17:19:12 +05:30
} ) ;
2017-09-10 17:25:29 +05:30
2022-06-21 17:19:12 +05:30
Notes . checkMergeRequestStatus ( ) ;
return this . updateNotesCount ( - 1 ) ;
} ) ;
2017-09-10 17:25:29 +05:30
}
/ * *
* Called in response to clicking the delete attachment link
*
* Removes the attachment wrapper view , including image tag if it exists
* Resets the note editing form
* /
removeAttachment ( ) {
const $note = $ ( this ) . closest ( '.note' ) ;
$note . find ( '.note-attachment' ) . remove ( ) ;
$note . find ( '.note-body > .note-text' ) . show ( ) ;
$note . find ( '.note-header' ) . show ( ) ;
2018-11-08 19:23:39 +05:30
return $note . find ( '.diffs .current-note-edit-form' ) . remove ( ) ;
2017-09-10 17:25:29 +05:30
}
/ * *
* Called when clicking on the "reply" button for a diff line .
*
* Shows the note form below the notes .
* /
onReplyToDiscussionNote ( e ) {
this . replyToDiscussionNote ( e . target ) ;
}
replyToDiscussionNote ( target ) {
2020-01-01 13:55:28 +05:30
const form = this . cleanForm ( this . formClone . clone ( ) ) ;
const replyLink = $ ( target ) . closest ( '.js-discussion-reply-button' ) ;
2017-09-10 17:25:29 +05:30
// insert the form after the button
2021-03-08 18:12:59 +05:30
replyLink . closest ( '.discussion-reply-holder' ) . hide ( ) . after ( form ) ;
2017-09-10 17:25:29 +05:30
// show the form
return this . setupDiscussionNoteForm ( replyLink , form ) ;
}
/ * *
* Shows the diff or discussion form and does some setup on it .
*
* Sets some hidden fields in the form .
*
* Note : dataHolder must have the "discussionId" and "lineCode" data attributes set .
* /
setupDiscussionNoteForm ( dataHolder , form ) {
2018-12-05 23:21:45 +05:30
// set up note target
2018-03-17 18:26:18 +05:30
let diffFileData = dataHolder . closest ( '.text-file' ) ;
if ( diffFileData . length === 0 ) {
diffFileData = dataHolder . closest ( '.image' ) ;
}
2017-09-10 17:25:29 +05:30
2020-01-01 13:55:28 +05:30
const discussionID = dataHolder . data ( 'discussionId' ) ;
2017-09-10 17:25:29 +05:30
if ( discussionID ) {
form . attr ( 'data-discussion-id' , discussionID ) ;
form . find ( '#in_reply_to_discussion_id' ) . val ( discussionID ) ;
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
form . find ( '#note_project_id' ) . val ( dataHolder . data ( 'discussionProjectId' ) ) ;
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
form . attr ( 'data-line-code' , dataHolder . data ( 'lineCode' ) ) ;
form . find ( '#line_type' ) . val ( dataHolder . data ( 'lineType' ) ) ;
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
form . find ( '#note_noteable_type' ) . val ( diffFileData . data ( 'noteableType' ) ) ;
form . find ( '#note_noteable_id' ) . val ( diffFileData . data ( 'noteableId' ) ) ;
form . find ( '#note_commit_id' ) . val ( diffFileData . data ( 'commitId' ) ) ;
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
form . find ( '#note_type' ) . val ( dataHolder . data ( 'noteType' ) ) ;
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
// LegacyDiffNote
form . find ( '#note_line_code' ) . val ( dataHolder . data ( 'lineCode' ) ) ;
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
// DiffNote
form . find ( '#note_position' ) . val ( dataHolder . attr ( 'data-position' ) ) ;
2017-08-17 22:00:37 +05:30
2021-03-11 19:13:27 +05:30
form . append ( '</div>' ) . find ( '.js-close-discussion-note-form' ) . show ( ) . removeClass ( 'hide' ) ;
2017-09-10 17:25:29 +05:30
form . find ( '.js-note-target-close' ) . remove ( ) ;
form . find ( '.js-note-new-discussion' ) . remove ( ) ;
this . setupNoteForm ( form ) ;
2016-09-13 17:45:13 +05:30
2018-11-08 19:23:39 +05:30
form . removeClass ( 'js-main-target-form' ) . addClass ( 'discussion-form js-discussion-note-form' ) ;
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
form . find ( '.js-note-text' ) . focus ( ) ;
2018-11-08 19:23:39 +05:30
form . find ( '.js-comment-resolve-button' ) . attr ( 'data-discussion-id' , discussionID ) ;
2017-09-10 17:25:29 +05:30
}
/ * *
* Called when clicking on the "add a comment" button on the side of a diff line .
*
* Inserts a temporary row for the form below the line .
* Sets up the form and shows it .
* /
onAddDiffNote ( e ) {
e . preventDefault ( ) ;
const link = e . currentTarget || e . target ;
const $link = $ ( link ) ;
const showReplyInput = ! $link . hasClass ( 'js-diff-comment-avatar' ) ;
this . toggleDiffNote ( {
target : $link ,
lineType : link . dataset . lineType ,
2018-05-09 12:01:36 +05:30
showReplyInput ,
2019-09-04 21:01:54 +05:30
currentUsername : gon . current _username ,
currentUserAvatar : gon . current _user _avatar _url ,
currentUserFullname : gon . current _user _fullname ,
2017-09-10 17:25:29 +05:30
} ) ;
}
2018-03-17 18:26:18 +05:30
onAddImageDiffNote ( e ) {
const $link = $ ( e . currentTarget || e . target ) ;
const $diffFile = $link . closest ( '.diff-file' ) ;
const clickEvent = new CustomEvent ( 'click.imageDiff' , {
detail : e ,
} ) ;
$diffFile [ 0 ] . dispatchEvent ( clickEvent ) ;
2018-12-05 23:21:45 +05:30
// Set up comment form
2018-03-17 18:26:18 +05:30
let newForm ;
2018-11-08 19:23:39 +05:30
const $noteContainer = $link . closest ( '.diff-viewer' ) . find ( '.note-container' ) ;
2018-03-17 18:26:18 +05:30
const $form = $noteContainer . find ( '> .discussion-form' ) ;
if ( $form . length === 0 ) {
newForm = this . cleanForm ( this . formClone . clone ( ) ) ;
newForm . appendTo ( $noteContainer ) ;
} else {
newForm = $form ;
}
this . setupDiscussionNoteForm ( $link , newForm ) ;
}
2019-12-04 20:38:33 +05:30
toggleDiffNote ( { target , lineType , forceShow , showReplyInput = false } ) {
2020-01-01 13:55:28 +05:30
let addForm ;
let newForm ;
let noteForm ;
let replyButton ;
let rowCssToAdd ;
const $link = $ ( target ) ;
const row = $link . closest ( 'tr' ) ;
2017-09-10 17:25:29 +05:30
const nextRow = row . next ( ) ;
let targetRow = row ;
if ( nextRow . is ( '.notes_holder' ) ) {
targetRow = nextRow ;
}
2017-08-17 22:00:37 +05:30
2020-01-01 13:55:28 +05:30
const hasNotes = nextRow . is ( '.notes_holder' ) ;
2017-09-10 17:25:29 +05:30
addForm = false ;
let lineTypeSelector = '' ;
2018-05-09 12:01:36 +05:30
rowCssToAdd =
2019-07-31 22:56:46 +05:30
'<tr class="notes_holder js-temp-notes-holder"><td class="notes-content" colspan="3"><div class="content"></div></td></tr>' ;
2017-09-10 17:25:29 +05:30
// In parallel view, look inside the correct left/right pane
if ( this . isParallelView ( ) ) {
lineTypeSelector = ` . ${ lineType } ` ;
2018-05-09 12:01:36 +05:30
rowCssToAdd =
2019-07-31 22:56:46 +05:30
'<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes-content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes-content parallel new"><div class="content"></div></td></tr>' ;
2017-09-10 17:25:29 +05:30
}
2019-07-31 22:56:46 +05:30
const notesContentSelector = ` .notes-content ${ lineTypeSelector } .content ` ;
2017-09-10 17:25:29 +05:30
let notesContent = targetRow . find ( notesContentSelector ) ;
if ( hasNotes && showReplyInput ) {
targetRow . show ( ) ;
notesContent = targetRow . find ( notesContentSelector ) ;
if ( notesContent . length ) {
notesContent . show ( ) ;
replyButton = notesContent . find ( '.js-discussion-reply-button:visible' ) ;
if ( replyButton . length ) {
this . replyToDiscussionNote ( replyButton [ 0 ] ) ;
} else {
// In parallel view, the form may not be present in one of the panes
noteForm = notesContent . find ( '.js-discussion-note-form' ) ;
if ( noteForm . length === 0 ) {
addForm = true ;
}
}
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
} else if ( showReplyInput ) {
// add a notes row and insert the form
row . after ( rowCssToAdd ) ;
targetRow = row . next ( ) ;
notesContent = targetRow . find ( notesContentSelector ) ;
addForm = true ;
} else {
2018-11-08 19:23:39 +05:30
const isCurrentlyShown = targetRow . find ( '.content:not(:empty)' ) . is ( ':visible' ) ;
2017-09-10 17:25:29 +05:30
const isForced = forceShow === true || forceShow === false ;
const showNow = forceShow === true || ( ! isCurrentlyShown && ! isForced ) ;
2018-11-08 19:23:39 +05:30
targetRow . toggleClass ( 'hide' , ! showNow ) ;
notesContent . toggleClass ( 'hide' , ! showNow ) ;
2017-09-10 17:25:29 +05:30
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
if ( addForm ) {
newForm = this . cleanForm ( this . formClone . clone ( ) ) ;
newForm . appendTo ( notesContent ) ;
// show the form
return this . setupDiscussionNoteForm ( $link , newForm ) ;
}
}
/ * *
* Called in response to "cancel" on a diff note form .
*
* Shows the reply button again .
* Removes the form and if necessary it ' s temporary row .
* /
removeDiscussionNoteForm ( form ) {
2020-01-01 13:55:28 +05:30
const row = form . closest ( 'tr' ) ;
const glForm = form . data ( 'glForm' ) ;
2017-09-10 17:25:29 +05:30
glForm . destroy ( ) ;
2023-03-17 16:20:25 +05:30
form . find ( '.js-note-text' ) . each ( function reset ( ) {
this . $autosave . reset ( ) ;
} ) ;
2018-05-09 12:01:36 +05:30
// show the reply button (will only work for replies)
form . prev ( '.discussion-reply-holder' ) . show ( ) ;
2017-09-10 17:25:29 +05:30
if ( row . is ( '.js-temp-notes-holder' ) ) {
// remove temporary row for diff lines
return row . remove ( ) ;
}
2020-05-24 23:13:21 +05:30
// only remove the form
return form . remove ( ) ;
2017-09-10 17:25:29 +05:30
}
cancelDiscussionForm ( e ) {
e . preventDefault ( ) ;
2018-03-17 18:26:18 +05:30
const $form = $ ( e . target ) . closest ( '.js-discussion-note-form' ) ;
const $discussionNote = $ ( e . target ) . closest ( '.discussion-notes' ) ;
if ( $discussionNote . length === 0 ) {
// Only send blur event when the discussion form
// is not part of a discussion note
const $diffFile = $form . closest ( '.diff-file' ) ;
if ( $diffFile . length > 0 ) {
const blurEvent = new CustomEvent ( 'blur.imageDiff' , {
detail : e ,
} ) ;
$diffFile [ 0 ] . dispatchEvent ( blurEvent ) ;
}
}
return this . removeDiscussionNoteForm ( $form ) ;
2017-09-10 17:25:29 +05:30
}
/ * *
* Called after an attachment file has been selected .
*
* Updates the file name for the selected attachment .
* /
updateFormAttachment ( ) {
2020-01-01 13:55:28 +05:30
const form = $ ( this ) . closest ( 'form' ) ;
2017-09-10 17:25:29 +05:30
// get only the basename
2020-01-01 13:55:28 +05:30
const filename = $ ( this )
2018-05-09 12:01:36 +05:30
. val ( )
. replace ( /^.*[\\\/]/ , '' ) ;
2017-09-10 17:25:29 +05:30
return form . find ( '.js-attachment-filename' ) . text ( filename ) ;
}
/ * *
* Called when the tab visibility changes
* /
visibilityChange ( ) {
return this . refresh ( ) ;
}
updateTargetButtons ( e ) {
2020-01-01 13:55:28 +05:30
let closetext ;
let reopentext ;
const textarea = $ ( e . target ) ;
const form = textarea . parents ( 'form' ) ;
const reopenbtn = form . find ( '.js-note-target-reopen' ) ;
const closebtn = form . find ( '.js-note-target-close' ) ;
2023-06-20 00:43:36 +05:30
const savebtn = form . find ( '.js-comment-save-button' ) ;
2021-11-18 22:05:49 +05:30
const commentTypeComponent = form . get ( 0 ) ? . commentTypeComponent ;
2017-09-10 17:25:29 +05:30
if ( textarea . val ( ) . trim ( ) . length > 0 ) {
2023-06-20 00:43:36 +05:30
savebtn . enable ( ) ;
2017-09-10 17:25:29 +05:30
reopentext = reopenbtn . attr ( 'data-alternative-text' ) ;
closetext = closebtn . attr ( 'data-alternative-text' ) ;
if ( reopenbtn . text ( ) !== reopentext ) {
reopenbtn . text ( reopentext ) ;
2016-09-13 17:45:13 +05:30
}
2017-09-10 17:25:29 +05:30
if ( closebtn . text ( ) !== closetext ) {
closebtn . text ( closetext ) ;
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
if ( reopenbtn . is ( ':not(.btn-comment-and-reopen)' ) ) {
reopenbtn . addClass ( 'btn-comment-and-reopen' ) ;
2016-09-13 17:45:13 +05:30
}
2017-09-10 17:25:29 +05:30
if ( closebtn . is ( ':not(.btn-comment-and-close)' ) ) {
closebtn . addClass ( 'btn-comment-and-close' ) ;
2016-09-13 17:45:13 +05:30
}
2021-11-18 22:05:49 +05:30
if ( commentTypeComponent ) {
commentTypeComponent . disabled = false ;
}
2017-09-10 17:25:29 +05:30
} else {
2023-06-20 00:43:36 +05:30
savebtn . disable ( ) ;
2018-03-27 19:54:05 +05:30
reopentext = reopenbtn . data ( 'originalText' ) ;
closetext = closebtn . data ( 'originalText' ) ;
2017-09-10 17:25:29 +05:30
if ( reopenbtn . text ( ) !== reopentext ) {
reopenbtn . text ( reopentext ) ;
2016-09-13 17:45:13 +05:30
}
2017-09-10 17:25:29 +05:30
if ( closebtn . text ( ) !== closetext ) {
closebtn . text ( closetext ) ;
2016-09-13 17:45:13 +05:30
}
2017-09-10 17:25:29 +05:30
if ( reopenbtn . is ( '.btn-comment-and-reopen' ) ) {
reopenbtn . removeClass ( 'btn-comment-and-reopen' ) ;
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
if ( closebtn . is ( '.btn-comment-and-close' ) ) {
closebtn . removeClass ( 'btn-comment-and-close' ) ;
}
2021-11-18 22:05:49 +05:30
if ( commentTypeComponent ) {
commentTypeComponent . disabled = true ;
}
2017-09-10 17:25:29 +05:30
}
}
putEditFormInPlace ( $el ) {
2020-01-01 13:55:28 +05:30
const $editForm = $ ( this . getEditFormSelector ( $el ) ) ;
const $note = $el . closest ( '.note' ) ;
2017-09-10 17:25:29 +05:30
$editForm . insertAfter ( $note . find ( '.note-text' ) ) ;
2020-01-01 13:55:28 +05:30
const $originalContentEl = $note . find ( '.original-note-content' ) ;
const originalContent = $originalContentEl . text ( ) . trim ( ) ;
const postUrl = $originalContentEl . data ( 'postUrl' ) ;
const targetId = $originalContentEl . data ( 'targetId' ) ;
const targetType = $originalContentEl . data ( 'targetType' ) ;
2017-09-10 17:25:29 +05:30
2018-03-17 18:26:18 +05:30
this . glForm = new GLForm ( $editForm . find ( 'form' ) , this . enableGFM ) ;
2017-09-10 17:25:29 +05:30
2021-03-08 18:12:59 +05:30
$editForm . find ( 'form' ) . attr ( 'action' , ` ${ postUrl } ?html=true ` ) . attr ( 'data-remote' , 'true' ) ;
2017-09-10 17:25:29 +05:30
$editForm . find ( '.js-form-target-id' ) . val ( targetId ) ;
$editForm . find ( '.js-form-target-type' ) . val ( targetType ) ;
2021-03-08 18:12:59 +05:30
$editForm . find ( '.js-note-text' ) . focus ( ) . val ( originalContent ) ;
2017-09-10 17:25:29 +05:30
$editForm . find ( '.js-md-write-button' ) . trigger ( 'click' ) ;
$editForm . find ( '.referenced-users' ) . hide ( ) ;
}
putConflictEditWarningInPlace ( noteEntity , $note ) {
if ( $note . find ( '.js-conflict-edit-warning' ) . length === 0 ) {
2019-09-30 21:07:59 +05:30
const open _link = ` <a href="#note_ ${ noteEntity . id } " target="_blank" rel="noopener noreferrer"> ` ;
2017-09-10 17:25:29 +05:30
const $alert = $ ( ` <div class="js-conflict-edit-warning alert alert-danger">
2019-09-04 21:01:54 +05:30
$ { sprintf (
s _ _ (
'Notes|This comment has changed since you started editing, please review the %{open_link}updated comment%{close_link} to ensure information is not lost' ,
) ,
{
open _link ,
close _link : '</a>' ,
} ,
) }
2017-09-10 17:25:29 +05:30
< / d i v > ` ) ;
$alert . insertAfter ( $note . find ( '.note-text' ) ) ;
}
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
updateNotesCount ( updateCount ) {
2018-11-08 19:23:39 +05:30
return this . notesCountBadge . text ( parseInt ( this . notesCountBadge . text ( ) , 10 ) + updateCount ) ;
2018-05-09 12:01:36 +05:30
}
static renderPlaceholderComponent ( $container ) {
const el = $container . find ( '.js-code-placeholder' ) . get ( 0 ) ;
2018-11-08 19:23:39 +05:30
// eslint-disable-next-line no-new
2018-05-09 12:01:36 +05:30
new Vue ( {
el ,
components : {
2022-08-13 15:12:31 +05:30
GlSkeletonLoader ,
2018-05-09 12:01:36 +05:30
} ,
render ( createElement ) {
2022-08-13 15:12:31 +05:30
return createElement ( 'gl-skeleton-loader' ) ;
2018-05-09 12:01:36 +05:30
} ,
} ) ;
}
static renderDiffContent ( $container , data ) {
const { discussion _html } = data ;
const lines = $ ( discussion _html ) . find ( '.line_holder' ) ;
lines . addClass ( 'fade-in' ) ;
$container . find ( '.diff-content > table > tbody' ) . prepend ( lines ) ;
const fileHolder = $container . find ( '.file-holder' ) ;
$container . find ( '.line-holder-placeholder' ) . remove ( ) ;
syntaxHighlight ( fileHolder ) ;
}
onClickRetryLazyLoad ( e ) {
const $retryButton = $ ( e . currentTarget ) ;
$retryButton . prop ( 'disabled' , true ) ;
2018-11-20 20:47:30 +05:30
return this . loadLazyDiff ( e ) . then ( ( ) => {
2018-05-09 12:01:36 +05:30
$retryButton . prop ( 'disabled' , false ) ;
} ) ;
}
loadLazyDiff ( e ) {
const $container = $ ( e . currentTarget ) . closest ( '.js-toggle-container' ) ;
Notes . renderPlaceholderComponent ( $container ) ;
$container . find ( '.js-toggle-lazy-diff' ) . removeClass ( 'js-toggle-lazy-diff' ) ;
const $tableEl = $container . find ( 'tbody' ) ;
if ( $tableEl . length === 0 ) return ;
const fileHolder = $container . find ( '.file-holder' ) ;
const url = fileHolder . data ( 'linesPath' ) ;
const $errorContainer = $container . find ( '.js-error-lazy-load-diff' ) ;
const $successContainer = $container . find ( '.js-success-lazy-load' ) ;
/ * *
* We only fetch resolved discussions .
* Unresolved discussions don ' t have an endpoint being provided .
* /
if ( url ) {
return axios
2018-11-20 20:47:30 +05:30
. get ( url )
. then ( ( { data } ) => {
// Reset state in case last request returned error
$successContainer . removeClass ( 'hidden' ) ;
$errorContainer . addClass ( 'hidden' ) ;
Notes . renderDiffContent ( $container , data ) ;
} )
. catch ( ( ) => {
$successContainer . addClass ( 'hidden' ) ;
$errorContainer . removeClass ( 'hidden' ) ;
} ) ;
2018-05-09 12:01:36 +05:30
}
return Promise . resolve ( ) ;
2017-09-10 17:25:29 +05:30
}
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
toggleCommitList ( e ) {
const $element = $ ( e . currentTarget ) ;
2018-11-08 19:23:39 +05:30
const $closestSystemCommitList = $element . siblings ( '.system-note-commit-list' ) ;
2020-10-24 23:57:45 +05:30
const $svgChevronUpElement = $element . find ( 'svg.js-chevron-up' ) ;
const $svgChevronDownElement = $element . find ( 'svg.js-chevron-down' ) ;
$svgChevronUpElement . toggleClass ( 'gl-display-none' ) ;
$svgChevronDownElement . toggleClass ( 'gl-display-none' ) ;
2016-09-13 17:45:13 +05:30
2017-09-10 17:25:29 +05:30
$closestSystemCommitList . toggleClass ( 'hide-shade' ) ;
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
/ * *
* Scans system notes with ` ul ` elements in system note body
* then collapse long commit list pushed by user to make it less
* intrusive .
* /
collapseLongCommitList ( ) {
2021-03-08 18:12:59 +05:30
const systemNotes = $ ( '#notes-list' ) . find ( 'li.system-note' ) . has ( 'ul' ) ;
2017-08-17 22:00:37 +05:30
2019-12-21 20:55:43 +05:30
$ . each ( systemNotes , ( index , systemNote ) => {
2017-09-10 17:25:29 +05:30
const $systemNote = $ ( systemNote ) ;
2018-05-09 12:01:36 +05:30
const headerMessage = $systemNote
. find ( '.note-text' )
2020-03-13 15:44:24 +05:30
. find ( 'p' )
. first ( )
2018-05-09 12:01:36 +05:30
. text ( )
. replace ( ':' , '' ) ;
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
$systemNote . find ( '.note-header .system-note-message' ) . html ( headerMessage ) ;
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
if ( $systemNote . find ( 'li' ) . length > MAX _VISIBLE _COMMIT _LIST _COUNT ) {
$systemNote . find ( '.note-text' ) . addClass ( 'system-note-commit-list' ) ;
$systemNote . find ( '.system-note-commit-list-toggler' ) . show ( ) ;
} else {
2018-11-08 19:23:39 +05:30
$systemNote . find ( '.note-text' ) . addClass ( 'system-note-commit-list hide-shade' ) ;
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
} ) ;
}
2017-08-17 22:00:37 +05:30
2023-03-04 22:38:38 +05:30
addAlert ( ... alertParams ) {
this . alert = createAlert ( ... alertParams ) ;
2017-09-10 17:25:29 +05:30
}
2017-08-17 22:00:37 +05:30
2023-03-04 22:38:38 +05:30
clearAlert ( ) {
this . alert ? . dismiss ( ) ;
2017-09-10 17:25:29 +05:30
}
cleanForm ( $form ) {
// Remove dropdown
2018-05-09 12:01:36 +05:30
$form . find ( '.dropdown-menu' ) . remove ( ) ;
2017-09-10 17:25:29 +05:30
return $form ;
}
/ * *
2020-05-24 23:13:21 +05:30
* Check if note does not exist on page
2017-09-10 17:25:29 +05:30
* /
2023-05-27 22:25:52 +05:30
static isNewNote ( noteEntity , note _ids ) {
if ( note _ids . length === 0 ) {
2023-06-20 00:43:36 +05:30
note _ids = Notes . getNotesIds ( ) ;
2023-05-27 22:25:52 +05:30
}
const isNewEntry = $ . inArray ( noteEntity . id , note _ids ) === - 1 ;
if ( isNewEntry ) {
note _ids . push ( noteEntity . id ) ;
}
return isNewEntry ;
}
/ * *
2023-06-20 00:43:36 +05:30
* Get notes ids
2023-05-27 22:25:52 +05:30
* /
2023-06-20 00:43:36 +05:30
static getNotesIds ( ) {
/ * *
* The selector covers following notes
* - notes and thread below the snippets and commit page
* - notes on the file of commit page
* - notes on an image file of commit page
* /
const notesList = [ ... document . querySelectorAll ( '.notes:not(.notes-form) li[id]' ) ] ;
return notesList . map ( ( noteItem ) => parseInt ( noteItem . dataset . noteId , 10 ) ) ;
2017-09-10 17:25:29 +05:30
}
/ * *
* Check if $note already contains the ` noteEntity ` content
* /
static isUpdatedNote ( noteEntity , $note ) {
// There can be CRLF vs LF mismatches if we don't sanitize and compare the same way
const sanitizedNoteEntityText = normalizeNewlines ( noteEntity . note . trim ( ) ) ;
const currentNoteText = normalizeNewlines (
2021-03-08 18:12:59 +05:30
$note . find ( '.original-note-content' ) . first ( ) . text ( ) . trim ( ) ,
2017-09-10 17:25:29 +05:30
) ;
return sanitizedNoteEntityText !== currentNoteText ;
}
static checkMergeRequestStatus ( ) {
2018-03-17 18:26:18 +05:30
if ( getPagePath ( 1 ) === 'merge_requests' && gl . mrWidget ) {
2017-09-10 17:25:29 +05:30
gl . mrWidget . checkStatus ( ) ;
}
}
static animateAppendNote ( noteHtml , $notesList ) {
const $note = $ ( noteHtml ) ;
2023-03-17 16:20:25 +05:30
$note . addClass ( 'fade-in-full' ) ;
2023-05-27 22:25:52 +05:30
renderGFM ( Notes . getNodeToRender ( $note ) ) ;
2017-09-10 17:25:29 +05:30
$notesList . append ( $note ) ;
return $note ;
}
static animateUpdateNote ( noteHtml , $note ) {
const $updatedNote = $ ( noteHtml ) ;
2023-03-17 16:20:25 +05:30
$updatedNote . addClass ( 'fade-in' ) ;
2023-05-27 22:25:52 +05:30
renderGFM ( Notes . getNodeToRender ( $updatedNote ) ) ;
2017-09-10 17:25:29 +05:30
$note . replaceWith ( $updatedNote ) ;
return $updatedNote ;
}
2023-05-27 22:25:52 +05:30
static getNodeToRender ( $note ) {
for ( const $item of $note ) {
if ( Notes . isNodeTypeElement ( $item ) ) {
return $item ;
}
}
return '' ;
}
2017-09-10 17:25:29 +05:30
/ * *
* Get data from Form attributes to use for saving / submitting comment .
* /
getFormData ( $form ) {
2018-03-17 18:26:18 +05:30
const content = $form . find ( '.js-note-text' ) . val ( ) ;
2017-09-10 17:25:29 +05:30
return {
2019-12-21 20:55:43 +05:30
// eslint-disable-next-line no-jquery/no-serialize
2017-09-10 17:25:29 +05:30
formData : $form . serialize ( ) ,
2020-03-13 15:44:24 +05:30
formContent : escape ( content ) ,
2017-09-10 17:25:29 +05:30
formAction : $form . attr ( 'action' ) ,
2018-03-17 18:26:18 +05:30
formContentOriginal : content ,
2017-08-17 22:00:37 +05:30
} ;
2017-09-10 17:25:29 +05:30
}
/ * *
* Identify if comment has any quick actions
* /
hasQuickActions ( formContent ) {
return REGEX _QUICK _ACTIONS . test ( formContent ) ;
}
/ * *
* Remove quick actions and leave comment with pure message
* /
stripQuickActions ( formContent ) {
return formContent . replace ( REGEX _QUICK _ACTIONS , '' ) . trim ( ) ;
}
/ * *
* Gets appropriate description from quick actions found in provided ` formContent `
* /
getQuickActionDescription ( formContent , availableQuickActions = [ ] ) {
let tempFormContent ;
// Identify executed quick actions from `formContent`
2021-03-08 18:12:59 +05:30
const executedCommands = availableQuickActions . filter ( ( command ) => {
2017-09-10 17:25:29 +05:30
const commandRegex = new RegExp ( ` / ${ command . name } ` ) ;
return commandRegex . test ( formContent ) ;
} ) ;
if ( executedCommands && executedCommands . length ) {
if ( executedCommands . length > 1 ) {
2019-09-04 21:01:54 +05:30
tempFormContent = _ _ ( 'Applying multiple commands' ) ;
2017-09-10 17:25:29 +05:30
} else {
const commandDescription = executedCommands [ 0 ] . description . toLowerCase ( ) ;
2019-09-04 21:01:54 +05:30
tempFormContent = sprintf ( _ _ ( 'Applying command to %{commandDescription}' ) , {
commandDescription ,
} ) ;
2017-09-10 17:25:29 +05:30
}
} else {
2019-09-04 21:01:54 +05:30
tempFormContent = _ _ ( 'Applying command' ) ;
2017-09-10 17:25:29 +05:30
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
return tempFormContent ;
}
/ * *
* Create placeholder note DOM element populated with comment body
* that we will show while comment is being posted .
* Once comment is _actually _ posted on server , we will have final element
* in response that we will show in place of this temporary element .
* /
2018-05-09 12:01:36 +05:30
createPlaceholderNote ( {
formContent ,
uniqueId ,
isDiscussionNote ,
currentUsername ,
currentUserFullname ,
currentUserAvatar ,
} ) {
2017-09-10 17:25:29 +05:30
const discussionClass = isDiscussionNote ? 'discussion' : '' ;
const $tempNote = $ (
` <li id=" ${ uniqueId } " class="note being-posted fade-in-half timeline-entry">
< div class = "timeline-entry-inner" >
< div class = "timeline-icon" >
2020-03-13 15:44:24 +05:30
< a href = "/${escape(currentUsername)}" >
2017-09-10 17:25:29 +05:30
< img class = "avatar s40" src = "${currentUserAvatar}" / >
< / a >
< / d i v >
< div class = "timeline-content ${discussionClass}" >
< div class = "note-header" >
< div class = "note-header-info" >
2020-03-13 15:44:24 +05:30
< a href = "/${escape(currentUsername)}" >
< span class = "d-none d-sm-inline-block bold" > $ { escape ( currentUsername ) } < / s p a n >
< span class = "note-headline-light" > $ { escape ( currentUsername ) } < / s p a n >
2017-09-10 17:25:29 +05:30
< / a >
< / d i v >
< / d i v >
< div class = "note-body" >
< div class = "note-text" >
< p > $ { formContent } < / p >
2017-08-17 22:00:37 +05:30
< / d i v >
2017-09-10 17:25:29 +05:30
< / d i v >
< / d i v >
< / d i v >
2018-05-09 12:01:36 +05:30
< / l i > ` ,
2017-09-10 17:25:29 +05:30
) ;
2020-03-13 15:44:24 +05:30
$tempNote . find ( '.d-none.d-sm-inline-block' ) . text ( escape ( currentUserFullname ) ) ;
$tempNote . find ( '.note-headline-light' ) . text ( ` @ ${ escape ( currentUsername ) } ` ) ;
2017-09-10 17:25:29 +05:30
return $tempNote ;
}
/ * *
* Create Placeholder System Note DOM element populated with quick action description
* /
createPlaceholderSystemNote ( { formContent , uniqueId } ) {
const $tempNote = $ (
` <li id=" ${ uniqueId } " class="note system-note timeline-entry being-posted fade-in-half">
< div class = "timeline-entry-inner" >
< div class = "timeline-content" >
< i > $ { formContent } < / i >
2017-08-17 22:00:37 +05:30
< / d i v >
2017-09-10 17:25:29 +05:30
< / d i v >
2018-05-09 12:01:36 +05:30
< / l i > ` ,
2017-09-10 17:25:29 +05:30
) ;
return $tempNote ;
}
/ * *
* This method does following tasks step - by - step whenever a new comment
* is submitted by user ( both main thread comments as well as discussion comments ) .
*
* 1 ) Get Form metadata
* 2 ) Identify comment type ; a ) Main thread b ) Discussion thread c ) Discussion resolve
* 3 ) Build temporary placeholder element ( using ` createPlaceholderNote ` )
* 4 ) Show placeholder note on UI
2018-03-17 18:26:18 +05:30
* 5 ) Perform network request to submit the note using ` axios.post `
2017-09-10 17:25:29 +05:30
* a ) If request is successfully completed
* 1. Remove placeholder element
* 2. Show submitted Note element
* 3. Perform post - submit errands
* a . Mark discussion as resolved if comment submission was for resolve .
* b . Reset comment form to original state .
* b ) If request failed
* 1. Remove placeholder element
2023-03-04 22:38:38 +05:30
* 2. Show error alert message about failure
2017-09-10 17:25:29 +05:30
* /
postComment ( e ) {
e . preventDefault ( ) ;
// Get Form metadata
const $submitBtn = $ ( e . target ) ;
2018-05-09 12:01:36 +05:30
$submitBtn . prop ( 'disabled' , true ) ;
2017-09-10 17:25:29 +05:30
let $form = $submitBtn . parents ( 'form' ) ;
2021-11-18 22:05:49 +05:30
const commentTypeComponent = $form . get ( 0 ) ? . commentTypeComponent ;
if ( commentTypeComponent ) commentTypeComponent . disabled = true ;
2017-09-10 17:25:29 +05:30
const $closeBtn = $form . find ( '.js-note-target-close' ) ;
2018-05-09 12:01:36 +05:30
const isDiscussionNote =
2021-03-08 18:12:59 +05:30
$submitBtn . parent ( ) . find ( 'li.droplab-item-selected' ) . attr ( 'id' ) === 'discussion' ;
2017-09-10 17:25:29 +05:30
const isMainForm = $form . hasClass ( 'js-main-target-form' ) ;
const isDiscussionForm = $form . hasClass ( 'js-discussion-note-form' ) ;
2018-11-08 19:23:39 +05:30
const isDiscussionResolve = $submitBtn . hasClass ( 'js-comment-resolve-button' ) ;
const { formData , formContent , formAction , formContentOriginal } = this . getFormData ( $form ) ;
2017-09-10 17:25:29 +05:30
let noteUniqueId ;
let systemNoteUniqueId ;
let hasQuickActions = false ;
let $notesContainer ;
let tempFormContent ;
// Get reference to notes container based on type of comment
if ( isDiscussionForm ) {
$notesContainer = $form . parent ( '.discussion-notes' ) . find ( '.notes' ) ;
} else if ( isMainForm ) {
$notesContainer = $ ( 'ul.main-notes-list' ) ;
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
// If comment is to resolve discussion, disable submit buttons while
// comment posting is finished.
if ( isDiscussionResolve ) {
$form . find ( '.js-comment-submit-button' ) . disable ( ) ;
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
tempFormContent = formContent ;
2020-07-28 23:09:34 +05:30
if ( this . glForm . supportsQuickActions && this . hasQuickActions ( formContent ) ) {
2017-09-10 17:25:29 +05:30
tempFormContent = this . stripQuickActions ( formContent ) ;
hasQuickActions = true ;
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
// Show placeholder note
if ( tempFormContent ) {
2020-03-13 15:44:24 +05:30
noteUniqueId = uniqueId ( 'tempNote_' ) ;
2018-05-09 12:01:36 +05:30
$notesContainer . append (
this . createPlaceholderNote ( {
formContent : tempFormContent ,
uniqueId : noteUniqueId ,
isDiscussionNote ,
currentUsername : gon . current _username ,
currentUserFullname : gon . current _user _fullname ,
currentUserAvatar : gon . current _user _avatar _url ,
} ) ,
) ;
2017-09-10 17:25:29 +05:30
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
// Show placeholder system note
if ( hasQuickActions ) {
2020-03-13 15:44:24 +05:30
systemNoteUniqueId = uniqueId ( 'tempSystemNote_' ) ;
2018-05-09 12:01:36 +05:30
$notesContainer . append (
this . createPlaceholderSystemNote ( {
formContent : this . getQuickActionDescription (
formContent ,
AjaxCache . get ( gl . GfmAutoComplete . dataSources . commands ) ,
) ,
uniqueId : systemNoteUniqueId ,
} ) ,
) ;
2017-09-10 17:25:29 +05:30
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
// Clear the form textarea
if ( $notesContainer . length ) {
if ( isMainForm ) {
this . resetMainTargetForm ( e ) ;
} else if ( isDiscussionForm ) {
this . removeDiscussionNoteForm ( $form ) ;
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
}
2017-08-17 22:00:37 +05:30
2018-05-09 12:01:36 +05:30
$closeBtn . text ( $closeBtn . data ( 'originalText' ) ) ;
2017-09-10 17:25:29 +05:30
// Make request to submit comment on server
2018-05-09 12:01:36 +05:30
return axios
. post ( ` ${ formAction } ?html=true ` , formData )
2021-03-08 18:12:59 +05:30
. then ( ( res ) => {
2018-03-17 18:26:18 +05:30
const note = res . data ;
2018-05-09 12:01:36 +05:30
$submitBtn . prop ( 'disabled' , false ) ;
2021-11-18 22:05:49 +05:30
if ( commentTypeComponent ) commentTypeComponent . disabled = false ;
2017-09-10 17:25:29 +05:30
// Submission successful! remove placeholder
$notesContainer . find ( ` # ${ noteUniqueId } ` ) . remove ( ) ;
2018-03-17 18:26:18 +05:30
const $diffFile = $form . closest ( '.diff-file' ) ;
if ( $diffFile . length > 0 ) {
const blurEvent = new CustomEvent ( 'blur.imageDiff' , {
detail : e ,
} ) ;
$diffFile [ 0 ] . dispatchEvent ( blurEvent ) ;
}
2017-09-10 17:25:29 +05:30
// Reset cached commands list when command is applied
if ( hasQuickActions ) {
2018-11-08 19:23:39 +05:30
$form . find ( 'textarea.js-note-text' ) . trigger ( 'clear-commands-cache.atwho' ) ;
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
// Clear previous form errors
2023-03-04 22:38:38 +05:30
this . clearAlertWrapper ( ) ;
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
// Check if this was discussion comment
if ( isDiscussionForm ) {
// Remove flash-container
$notesContainer . find ( '.flash-container' ) . remove ( ) ;
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
// If comment intends to resolve discussion, do the same.
if ( isDiscussionResolve ) {
$form
2018-03-27 19:54:05 +05:30
. attr ( 'data-discussion-id' , $submitBtn . data ( 'discussionId' ) )
2017-09-10 17:25:29 +05:30
. attr ( 'data-resolve-all' , 'true' )
2018-03-27 19:54:05 +05:30
. attr ( 'data-project-path' , $submitBtn . data ( 'projectPath' ) ) ;
2017-08-17 22:00:37 +05:30
}
2017-09-10 17:25:29 +05:30
// Show final note element on UI
2018-03-17 18:26:18 +05:30
const isNewDiffComment = $notesContainer . length === 0 ;
this . addDiscussionNote ( $form , note , isNewDiffComment ) ;
if ( isNewDiffComment ) {
// Add image badge, avatar badge and toggle discussion badge for new image diffs
const notePosition = $form . find ( '#note_position' ) . val ( ) ;
if ( $diffFile . length > 0 && notePosition . length > 0 ) {
const { x , y , width , height } = JSON . parse ( notePosition ) ;
const addBadgeEvent = new CustomEvent ( 'addBadge.imageDiff' , {
detail : {
x ,
y ,
width ,
height ,
noteId : ` note_ ${ note . id } ` ,
discussionId : note . discussion _id ,
} ,
} ) ;
$diffFile [ 0 ] . dispatchEvent ( addBadgeEvent ) ;
}
}
2017-09-10 17:25:29 +05:30
// append flash-container to the Notes list
if ( $notesContainer . length ) {
2018-11-08 19:23:39 +05:30
$notesContainer . append ( '<div class="flash-container" style="display: none;"></div>' ) ;
2017-08-17 22:00:37 +05:30
}
2018-05-09 12:01:36 +05:30
} else if ( isMainForm ) {
// Check if this was main thread comment
2017-09-10 17:25:29 +05:30
// Show final note element on UI and perform form and action buttons cleanup
this . addNote ( $form , note ) ;
this . reenableTargetFormSubmitButton ( e ) ;
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
if ( note . commands _changes ) {
this . handleQuickActions ( note ) ;
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
$form . trigger ( 'ajax:success' , [ note ] ) ;
2018-05-09 12:01:36 +05:30
} )
. catch ( ( ) => {
2017-09-10 17:25:29 +05:30
// Submission failed, remove placeholder note and show Flash error message
$notesContainer . find ( ` # ${ noteUniqueId } ` ) . remove ( ) ;
2018-05-09 12:01:36 +05:30
$submitBtn . prop ( 'disabled' , false ) ;
2021-11-18 22:05:49 +05:30
if ( commentTypeComponent ) commentTypeComponent . disabled = false ;
2018-03-17 18:26:18 +05:30
const blurEvent = new CustomEvent ( 'blur.imageDiff' , {
detail : e ,
} ) ;
const closestDiffFile = $form . closest ( '.diff-file' ) ;
if ( closestDiffFile . length ) {
closestDiffFile [ 0 ] . dispatchEvent ( blurEvent ) ;
}
2017-09-10 17:25:29 +05:30
if ( hasQuickActions ) {
$notesContainer . find ( ` # ${ systemNoteUniqueId } ` ) . remove ( ) ;
}
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
// Show form again on UI on failure
if ( isDiscussionForm && $notesContainer . length ) {
2018-11-08 19:23:39 +05:30
const replyButton = $notesContainer . parent ( ) . find ( '.js-discussion-reply-button' ) ;
2017-09-10 17:25:29 +05:30
this . replyToDiscussionNote ( replyButton [ 0 ] ) ;
$form = $notesContainer . parent ( ) . find ( 'form' ) ;
}
2017-08-17 22:00:37 +05:30
2018-03-17 18:26:18 +05:30
$form . find ( '.js-note-text' ) . val ( formContentOriginal ) ;
2017-09-10 17:25:29 +05:30
this . reenableTargetFormSubmitButton ( e ) ;
this . addNoteError ( $form ) ;
} ) ;
}
/ * *
* This method does following tasks step - by - step whenever an existing comment
* is updated by user ( both main thread comments as well as discussion comments ) .
*
* 1 ) Get Form metadata
* 2 ) Update note element with new content
2018-03-17 18:26:18 +05:30
* 3 ) Perform network request to submit the updated note using ` axios.post `
2017-09-10 17:25:29 +05:30
* a ) If request is successfully completed
* 1. Show submitted Note element
* b ) If request failed
* 1. Revert Note element to original content
* 2. Show error Flash message about failure
* /
updateComment ( e ) {
e . preventDefault ( ) ;
// Get Form metadata
const $submitBtn = $ ( e . target ) ;
const $form = $submitBtn . parents ( 'form' ) ;
const $closeBtn = $form . find ( '.js-note-target-close' ) ;
const $editingNote = $form . parents ( '.note.is-editing' ) ;
const $noteBody = $editingNote . find ( '.js-task-list-container' ) ;
const $noteBodyText = $noteBody . find ( '.note-text' ) ;
const { formData , formContent , formAction } = this . getFormData ( $form ) ;
// Cache original comment content
const cachedNoteBodyText = $noteBodyText . html ( ) ;
// Show updated comment content temporarily
$noteBodyText . html ( formContent ) ;
2018-11-08 19:23:39 +05:30
$editingNote . removeClass ( 'is-editing fade-in-full' ) . addClass ( 'being-posted fade-in-half' ) ;
2022-05-07 20:08:51 +05:30
const $timeAgo = $editingNote . find ( '.note-headline-meta a' ) ;
$timeAgo . empty ( ) ;
$timeAgo . append ( loadingIconForLegacyJS ( { inline : true , size : 'sm' } ) ) ;
2017-09-10 17:25:29 +05:30
// Make request to update comment on server
2018-05-09 12:01:36 +05:30
axios
. post ( ` ${ formAction } ?html=true ` , formData )
2018-03-17 18:26:18 +05:30
. then ( ( { data } ) => {
2017-09-10 17:25:29 +05:30
// Submission successful! render final note element
2018-03-17 18:26:18 +05:30
this . updateNote ( data , $editingNote ) ;
2017-09-10 17:25:29 +05:30
} )
2018-03-17 18:26:18 +05:30
. catch ( ( ) => {
2017-09-10 17:25:29 +05:30
// Submission failed, revert back to original note
2020-03-13 15:44:24 +05:30
$noteBodyText . html ( escape ( cachedNoteBodyText ) ) ;
2017-09-10 17:25:29 +05:30
$editingNote . removeClass ( 'being-posted fade-in' ) ;
2021-06-08 01:23:25 +05:30
$editingNote . find ( '.gl-spinner' ) . remove ( ) ;
2017-09-10 17:25:29 +05:30
// Show Flash message about failure
this . updateNoteError ( ) ;
} ) ;
2018-03-27 19:54:05 +05:30
return $closeBtn . text ( $closeBtn . data ( 'originalText' ) ) ;
2017-09-10 17:25:29 +05:30
}
2023-05-27 22:25:52 +05:30
/ * *
* Function to check if node is element to avoid comment and text
* /
static isNodeTypeElement ( $node ) {
return $node . nodeType === Node . ELEMENT _NODE ;
}
2017-09-10 17:25:29 +05:30
}