PageRenderTime 24ms CodeModel.GetById 46ms RepoModel.GetById 2ms app.codeStats 0ms

/app/assets/javascripts/notes/components/issue_comment_form.vue

https://gitlab.com/mehlah/gitlab-ce
Vue | 377 lines | 352 code | 17 blank | 8 comment | 17 complexity | ebb57e55b03020b86540165f4779d311 MD5 | raw file
  1. <script>
  2. import { mapActions, mapGetters } from 'vuex';
  3. import _ from 'underscore';
  4. import Autosize from 'autosize';
  5. import Flash from '../../flash';
  6. import Autosave from '../../autosave';
  7. import TaskList from '../../task_list';
  8. import * as constants from '../constants';
  9. import eventHub from '../event_hub';
  10. import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
  11. import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue';
  12. import issueDiscussionLockedWidget from './issue_discussion_locked_widget.vue';
  13. import markdownField from '../../vue_shared/components/markdown/field.vue';
  14. import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
  15. import issuableStateMixin from '../mixins/issuable_state';
  16. export default {
  17. name: 'issueCommentForm',
  18. data() {
  19. return {
  20. note: '',
  21. noteType: constants.COMMENT,
  22. // Can't use mapGetters,
  23. // this needs to be in the data object because it belongs to the state
  24. issueState: this.$store.getters.getIssueData.state,
  25. isSubmitting: false,
  26. isSubmitButtonDisabled: true,
  27. };
  28. },
  29. components: {
  30. issueWarning,
  31. issueNoteSignedOutWidget,
  32. issueDiscussionLockedWidget,
  33. markdownField,
  34. userAvatarLink,
  35. },
  36. watch: {
  37. note(newNote) {
  38. this.setIsSubmitButtonDisabled(newNote, this.isSubmitting);
  39. },
  40. isSubmitting(newValue) {
  41. this.setIsSubmitButtonDisabled(this.note, newValue);
  42. },
  43. },
  44. computed: {
  45. ...mapGetters([
  46. 'getCurrentUserLastNote',
  47. 'getUserData',
  48. 'getIssueData',
  49. 'getNotesData',
  50. ]),
  51. isLoggedIn() {
  52. return this.getUserData.id;
  53. },
  54. commentButtonTitle() {
  55. return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion';
  56. },
  57. isIssueOpen() {
  58. return this.issueState === constants.OPENED || this.issueState === constants.REOPENED;
  59. },
  60. canCreateNote() {
  61. return this.getIssueData.current_user.can_create_note;
  62. },
  63. issueActionButtonTitle() {
  64. if (this.note.length) {
  65. const actionText = this.isIssueOpen ? 'close' : 'reopen';
  66. return this.noteType === constants.COMMENT ? `Comment & ${actionText} issue` : `Start discussion & ${actionText} issue`;
  67. }
  68. return this.isIssueOpen ? 'Close issue' : 'Reopen issue';
  69. },
  70. actionButtonClassNames() {
  71. return {
  72. 'btn-reopen': !this.isIssueOpen,
  73. 'btn-close': this.isIssueOpen,
  74. 'js-note-target-close': this.isIssueOpen,
  75. 'js-note-target-reopen': !this.isIssueOpen,
  76. };
  77. },
  78. markdownDocsPath() {
  79. return this.getNotesData.markdownDocsPath;
  80. },
  81. quickActionsDocsPath() {
  82. return this.getNotesData.quickActionsDocsPath;
  83. },
  84. markdownPreviewPath() {
  85. return this.getIssueData.preview_note_path;
  86. },
  87. author() {
  88. return this.getUserData;
  89. },
  90. canUpdateIssue() {
  91. return this.getIssueData.current_user.can_update;
  92. },
  93. endpoint() {
  94. return this.getIssueData.create_note_path;
  95. },
  96. },
  97. methods: {
  98. ...mapActions([
  99. 'saveNote',
  100. 'stopPolling',
  101. 'restartPolling',
  102. 'removePlaceholderNotes',
  103. ]),
  104. setIsSubmitButtonDisabled(note, isSubmitting) {
  105. if (!_.isEmpty(note) && !isSubmitting) {
  106. this.isSubmitButtonDisabled = false;
  107. } else {
  108. this.isSubmitButtonDisabled = true;
  109. }
  110. },
  111. handleSave(withIssueAction) {
  112. if (this.note.length) {
  113. const noteData = {
  114. endpoint: this.endpoint,
  115. flashContainer: this.$el,
  116. data: {
  117. note: {
  118. noteable_type: constants.NOTEABLE_TYPE,
  119. noteable_id: this.getIssueData.id,
  120. note: this.note,
  121. },
  122. },
  123. };
  124. if (this.noteType === constants.DISCUSSION) {
  125. noteData.data.note.type = constants.DISCUSSION_NOTE;
  126. }
  127. this.isSubmitting = true;
  128. this.note = ''; // Empty textarea while being requested. Repopulate in catch
  129. this.resizeTextarea();
  130. this.stopPolling();
  131. this.saveNote(noteData)
  132. .then((res) => {
  133. this.isSubmitting = false;
  134. this.restartPolling();
  135. if (res.errors) {
  136. if (res.errors.commands_only) {
  137. this.discard();
  138. } else {
  139. Flash(
  140. 'Something went wrong while adding your comment. Please try again.',
  141. 'alert',
  142. this.$refs.commentForm,
  143. );
  144. }
  145. } else {
  146. this.discard();
  147. }
  148. if (withIssueAction) {
  149. this.toggleIssueState();
  150. }
  151. })
  152. .catch(() => {
  153. this.isSubmitting = false;
  154. this.discard(false);
  155. const msg = 'Your comment could not be submitted! Please check your network connection and try again.';
  156. Flash(msg, 'alert', this.$el);
  157. this.note = noteData.data.note.note; // Restore textarea content.
  158. this.removePlaceholderNotes();
  159. });
  160. } else {
  161. this.toggleIssueState();
  162. }
  163. },
  164. toggleIssueState() {
  165. this.issueState = this.isIssueOpen ? constants.CLOSED : constants.REOPENED;
  166. // This is out of scope for the Notes Vue component.
  167. // It was the shortest path to update the issue state and relevant places.
  168. const btnClass = this.isIssueOpen ? 'btn-reopen' : 'btn-close';
  169. $(`.js-btn-issue-action.${btnClass}:visible`).trigger('click');
  170. },
  171. discard(shouldClear = true) {
  172. // `blur` is needed to clear slash commands autocomplete cache if event fired.
  173. // `focus` is needed to remain cursor in the textarea.
  174. this.$refs.textarea.blur();
  175. this.$refs.textarea.focus();
  176. if (shouldClear) {
  177. this.note = '';
  178. this.resizeTextarea();
  179. this.$refs.markdownField.previewMarkdown = false;
  180. }
  181. // reset autostave
  182. this.autosave.reset();
  183. },
  184. setNoteType(type) {
  185. this.noteType = type;
  186. },
  187. editCurrentUserLastNote() {
  188. if (this.note === '') {
  189. const lastNote = this.getCurrentUserLastNote;
  190. if (lastNote) {
  191. eventHub.$emit('enterEditMode', {
  192. noteId: lastNote.id,
  193. });
  194. }
  195. }
  196. },
  197. initAutoSave() {
  198. if (this.isLoggedIn) {
  199. this.autosave = new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getIssueData.id], 'issue');
  200. }
  201. },
  202. initTaskList() {
  203. return new TaskList({
  204. dataType: 'note',
  205. fieldName: 'note',
  206. selector: '.notes',
  207. });
  208. },
  209. resizeTextarea() {
  210. this.$nextTick(() => {
  211. Autosize.update(this.$refs.textarea);
  212. });
  213. },
  214. },
  215. mixins: [
  216. issuableStateMixin,
  217. ],
  218. mounted() {
  219. // jQuery is needed here because it is a custom event being dispatched with jQuery.
  220. $(document).on('issuable:change', (e, isClosed) => {
  221. this.issueState = isClosed ? constants.CLOSED : constants.REOPENED;
  222. });
  223. this.initAutoSave();
  224. this.initTaskList();
  225. },
  226. };
  227. </script>
  228. <template>
  229. <div>
  230. <issue-note-signed-out-widget v-if="!isLoggedIn" />
  231. <issue-discussion-locked-widget v-else-if="!canCreateNote" />
  232. <ul
  233. v-else
  234. class="notes notes-form timeline">
  235. <li class="timeline-entry">
  236. <div class="timeline-entry-inner">
  237. <div class="flash-container error-alert timeline-content"></div>
  238. <div class="timeline-icon hidden-xs hidden-sm">
  239. <user-avatar-link
  240. v-if="author"
  241. :link-href="author.path"
  242. :img-src="author.avatar_url"
  243. :img-alt="author.name"
  244. :img-size="40"
  245. />
  246. </div>
  247. <div class="timeline-content timeline-content-form">
  248. <form
  249. ref="commentForm"
  250. class="new-note js-quick-submit common-note-form gfm-form js-main-target-form"
  251. >
  252. <div class="error-alert"></div>
  253. <issue-warning
  254. v-if="hasWarning(getIssueData)"
  255. :is-locked="isLocked(getIssueData)"
  256. :is-confidential="isConfidential(getIssueData)"
  257. />
  258. <markdown-field
  259. :markdown-preview-path="markdownPreviewPath"
  260. :markdown-docs-path="markdownDocsPath"
  261. :quick-actions-docs-path="quickActionsDocsPath"
  262. :add-spacing-classes="false"
  263. ref="markdownField">
  264. <textarea
  265. id="note-body"
  266. name="note[note]"
  267. class="note-textarea js-vue-comment-form js-gfm-input js-autosize markdown-area js-vue-textarea"
  268. data-supports-quick-actions="true"
  269. aria-label="Description"
  270. v-model="note"
  271. ref="textarea"
  272. slot="textarea"
  273. :disabled="isSubmitting"
  274. placeholder="Write a comment or drag your files here..."
  275. @keydown.up="editCurrentUserLastNote()"
  276. @keydown.meta.enter="handleSave()">
  277. </textarea>
  278. </markdown-field>
  279. <div class="note-form-actions">
  280. <div class="pull-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown">
  281. <button
  282. @click.prevent="handleSave()"
  283. :disabled="isSubmitButtonDisabled"
  284. class="btn btn-create comment-btn js-comment-button js-comment-submit-button"
  285. type="submit">
  286. {{commentButtonTitle}}
  287. </button>
  288. <button
  289. :disabled="isSubmitButtonDisabled"
  290. name="button"
  291. type="button"
  292. class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle"
  293. data-toggle="dropdown"
  294. aria-label="Open comment type dropdown">
  295. <i
  296. aria-hidden="true"
  297. class="fa fa-caret-down toggle-icon">
  298. </i>
  299. </button>
  300. <ul class="note-type-dropdown dropdown-open-top dropdown-menu">
  301. <li :class="{ 'droplab-item-selected': noteType === 'comment' }">
  302. <button
  303. type="button"
  304. class="btn btn-transparent"
  305. @click.prevent="setNoteType('comment')">
  306. <i
  307. aria-hidden="true"
  308. class="fa fa-check icon">
  309. </i>
  310. <div class="description">
  311. <strong>Comment</strong>
  312. <p>
  313. Add a general comment to this issue.
  314. </p>
  315. </div>
  316. </button>
  317. </li>
  318. <li class="divider droplab-item-ignore"></li>
  319. <li :class="{ 'droplab-item-selected': noteType === 'discussion' }">
  320. <button
  321. type="button"
  322. class="btn btn-transparent"
  323. @click.prevent="setNoteType('discussion')">
  324. <i
  325. aria-hidden="true"
  326. class="fa fa-check icon">
  327. </i>
  328. <div class="description">
  329. <strong>Start discussion</strong>
  330. <p>
  331. Discuss a specific suggestion or question.
  332. </p>
  333. </div>
  334. </button>
  335. </li>
  336. </ul>
  337. </div>
  338. <button
  339. type="button"
  340. @click="handleSave(true)"
  341. v-if="canUpdateIssue"
  342. :class="actionButtonClassNames"
  343. class="btn btn-comment btn-comment-and-close">
  344. {{issueActionButtonTitle}}
  345. </button>
  346. <button
  347. type="button"
  348. v-if="note.length"
  349. @click="discard"
  350. class="btn btn-cancel js-note-discard">
  351. Discard draft
  352. </button>
  353. </div>
  354. </form>
  355. </div>
  356. </div>
  357. </li>
  358. </ul>
  359. </div>
  360. </template>