diff options
Diffstat (limited to 'app/assets/javascripts/notes')
13 files changed, 107 insertions, 156 deletions
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 9cc53a320b8..0363173f912 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -3,23 +3,23 @@ import $ from 'jquery'; import { mapActions, mapGetters, mapState } from 'vuex'; import { isEmpty } from 'lodash'; import Autosize from 'autosize'; -import { GlAlert, GlIntersperse, GlLink, GlSprintf, GlButton, GlIcon } from '@gitlab/ui'; +import { GlButton, GlIcon } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; -import { deprecatedCreateFlash as Flash } from '../../flash'; -import Autosave from '../../autosave'; +import { deprecatedCreateFlash as Flash } from '~/flash'; +import Autosave from '~/autosave'; import { capitalizeFirstCharacter, convertToCamelCase, splitCamelCase, slugifyWithUnderscore, -} from '../../lib/utils/text_utility'; +} from '~/lib/utils/text_utility'; import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; import * as constants from '../constants'; import eventHub from '../event_hub'; -import NoteableWarning from '../../vue_shared/components/notes/noteable_warning.vue'; -import markdownField from '../../vue_shared/components/markdown/field.vue'; -import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; +import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue'; +import markdownField from '~/vue_shared/components/markdown/field.vue'; +import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import noteSignedOutWidget from './note_signed_out_widget.vue'; import discussionLockedWidget from './discussion_locked_widget.vue'; import issuableStateMixin from '../mixins/issuable_state'; @@ -34,10 +34,6 @@ export default { userAvatarLink, GlButton, TimelineEntryItem, - GlAlert, - GlIntersperse, - GlLink, - GlSprintf, GlIcon, }, mixins: [issuableStateMixin], @@ -63,9 +59,8 @@ export default { 'getNoteableDataByProp', 'getNotesData', 'openState', - 'getBlockedByIssues', ]), - ...mapState(['isToggleStateButtonLoading', 'isToggleBlockedIssueWarning']), + ...mapState(['isToggleStateButtonLoading']), noteableDisplayName() { return splitCamelCase(this.noteableType).toLowerCase(); }, @@ -143,7 +138,7 @@ export default { ? __('merge request') : __('issue'); }, - isIssueType() { + isIssue() { return this.noteableDisplayName === constants.ISSUE_NOTEABLE_TYPE; }, trackingLabel() { @@ -172,11 +167,9 @@ export default { 'stopPolling', 'restartPolling', 'removePlaceholderNotes', - 'closeIssue', - 'reopenIssue', + 'closeIssuable', + 'reopenIssuable', 'toggleIssueLocalState', - 'toggleStateButtonLoading', - 'toggleBlockedIssueWarning', ]), setIsSubmitButtonDisabled(note, isSubmitting) { if (!isEmpty(note) && !isSubmitting) { @@ -186,8 +179,6 @@ export default { } }, handleSave(withIssueAction) { - this.isSubmitting = true; - if (this.note.length) { const noteData = { endpoint: this.endpoint, @@ -210,9 +201,10 @@ export default { this.resizeTextarea(); this.stopPolling(); + this.isSubmitting = true; + this.saveNote(noteData) .then(() => { - this.enableButton(); this.restartPolling(); this.discard(); @@ -221,7 +213,6 @@ export default { } }) .catch(() => { - this.enableButton(); this.discard(false); const msg = __( 'Your comment could not be submitted! Please check your network connection and try again.', @@ -229,64 +220,27 @@ export default { Flash(msg, 'alert', this.$el); this.note = noteData.data.note.note; // Restore textarea content. this.removePlaceholderNotes(); + }) + .finally(() => { + this.isSubmitting = false; }); } else { this.toggleIssueState(); } }, - enableButton() { - this.isSubmitting = false; - }, toggleIssueState() { - if ( - this.noteableType.toLowerCase() === constants.ISSUE_NOTEABLE_TYPE && - this.isOpen && - this.getBlockedByIssues && - this.getBlockedByIssues.length > 0 - ) { - this.toggleBlockedIssueWarning(true); + if (this.isIssue) { + // We want to invoke the close/reopen logic in the issue header + // since that is where the blocked-by issues modal logic is also defined + eventHub.$emit('toggle.issuable.state'); return; } - if (this.isOpen) { - this.forceCloseIssue(); - } else { - this.reopenIssue() - .then(() => { - this.enableButton(); - refreshUserMergeRequestCounts(); - }) - .catch(({ data }) => { - this.enableButton(); - this.toggleStateButtonLoading(false); - let errorMessage = sprintf( - __('Something went wrong while reopening the %{issuable}. Please try again later'), - { issuable: this.noteableDisplayName }, - ); - if (data) { - errorMessage = Object.values(data).join('\n'); - } + const toggleState = this.isOpen ? this.closeIssuable : this.reopenIssuable; - Flash(errorMessage); - }); - } - }, - forceCloseIssue() { - this.closeIssue() - .then(() => { - this.enableButton(); - refreshUserMergeRequestCounts(); - }) - .catch(() => { - this.enableButton(); - this.toggleStateButtonLoading(false); - Flash( - sprintf( - __('Something went wrong while closing the %{issuable}. Please try again later'), - { issuable: this.noteableDisplayName }, - ), - ); - }); + toggleState() + .then(refreshUserMergeRequestCounts) + .catch(() => Flash(constants.toggleStateErrorMessage[this.noteableType][this.openState])); }, discard(shouldClear = true) { // `blur` is needed to clear slash commands autocomplete cache if event fired. @@ -384,6 +338,7 @@ export default { name="note[note]" class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area" data-qa-selector="comment_field" + data-testid="comment-field" data-supports-quick-actions="true" :aria-label="__('Description')" :placeholder="__('Write a comment or drag your files hereā¦')" @@ -392,36 +347,7 @@ export default { @keydown.ctrl.enter="handleSave()" ></textarea> </markdown-field> - <gl-alert - v-if="isToggleBlockedIssueWarning" - class="gl-mt-5" - :title="__('Are you sure you want to close this blocked issue?')" - :primary-button-text="__('Yes, close issue')" - :secondary-button-text="__('Cancel')" - variant="warning" - :dismissible="false" - @primaryAction="toggleBlockedIssueWarning(false) && forceCloseIssue()" - @secondaryAction="toggleBlockedIssueWarning(false) && enableButton()" - > - <p> - <gl-sprintf - :message=" - __('This issue is currently blocked by the following issues: %{issues}.') - " - > - <template #issues> - <gl-intersperse> - <gl-link - v-for="blockingIssue in getBlockedByIssues" - :key="blockingIssue.web_url" - :href="blockingIssue.web_url" - >#{{ blockingIssue.iid }}</gl-link - > - </gl-intersperse> - </template> - </gl-sprintf> - </p> - </gl-alert> + <div class="note-form-actions"> <div class="btn-group gl-mr-3 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" @@ -430,6 +356,7 @@ export default { :disabled="isSubmitButtonDisabled" class="js-comment-button js-comment-submit-button" data-qa-selector="comment_button" + data-testid="comment-button" type="submit" category="primary" variant="success" @@ -488,15 +415,13 @@ export default { </div> <gl-button - v-if="canToggleIssueState && !isToggleBlockedIssueWarning" + v-if="canToggleIssueState" :loading="isToggleStateButtonLoading" category="secondary" :variant="buttonVariant" - :class="[ - actionButtonClassNames, - 'btn-comment btn-comment-and-close js-action-button', - ]" - :disabled="isToggleStateButtonLoading || isSubmitting" + :class="[actionButtonClassNames, 'btn-comment btn-comment-and-close']" + :disabled="isSubmitting" + data-testid="close-reopen-button" @click="handleSave(true)" >{{ issueActionButtonTitle }}</gl-button > diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index 91cf682943e..1580c94658a 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -7,7 +7,7 @@ import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue'; import { getDiffMode } from '~/diffs/store/utils'; import { diffViewerModes } from '~/ide/constants'; -import { isCollapsed } from '../../diffs/diff_file'; +import { isCollapsed } from '../../diffs/utils/diff_file'; const FIRST_CHAR_REGEX = /^(\+|-| )/; @@ -131,14 +131,18 @@ export default { :file-hash="discussion.diff_file.file_hash" :project-path="projectPath" > - <image-diff-overlay - slot="image-overlay" - :discussions="discussion" - :file-hash="discussion.diff_file.file_hash" - :show-comment-icon="true" - :should-toggle-discussion="false" - badge-class="image-comment-badge" - /> + <template #image-overlay="{ renderedWidth, renderedHeight }"> + <image-diff-overlay + v-if="renderedWidth" + :rendered-width="renderedWidth" + :rendered-height="renderedHeight" + :discussions="discussion" + :file-hash="discussion.diff_file.file_hash" + :show-comment-icon="true" + :should-toggle-discussion="false" + badge-class="image-comment-badge gl-text-gray-500" + /> + </template> </diff-viewer> <slot></slot> </div> diff --git a/app/assets/javascripts/notes/components/multiline_comment_utils.js b/app/assets/javascripts/notes/components/multiline_comment_utils.js index dbae10c8f6c..2451400e980 100644 --- a/app/assets/javascripts/notes/components/multiline_comment_utils.js +++ b/app/assets/javascripts/notes/components/multiline_comment_utils.js @@ -103,9 +103,15 @@ export function getCommentedLines(selectedCommentPosition, diffLines) { }; } + const findLineCodeIndex = line => position => { + return [position.line_code, position.left?.line_code, position.right?.line_code].includes( + line.line_code, + ); + }; + const { start, end } = selectedCommentPosition; - const startLine = diffLines.findIndex(l => l.line_code === start.line_code); - const endLine = diffLines.findIndex(l => l.line_code === end.line_code); + const startLine = diffLines.findIndex(findLineCodeIndex(start)); + const endLine = diffLines.findIndex(findLineCodeIndex(end)); return { startLine, endLine }; } diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 43f17c5d65c..84769bfc7c8 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -422,7 +422,7 @@ export default { </button> <button v-if="discussion.resolvable" - class="btn btn-nr btn-default gl-mr-3 js-comment-resolve-button" + class="btn btn-default gl-mr-3 js-comment-resolve-button" @click.prevent="handleUpdate(true)" > {{ resolveButtonTitle }} diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index cacf209ed81..17a995018d3 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -85,7 +85,10 @@ export default { }; }, authorStatus() { - return this.author.status_tooltip_html; + if (this.author?.show_status) { + return this.author.status_tooltip_html; + } + return false; }, authorIsBusy() { const { status } = this.author; @@ -142,7 +145,7 @@ export default { type="button" @click="handleToggle" > - <gl-icon ref="chevronIcon" :name="toggleChevronIconName" aria-hidden="true" /> + <gl-icon ref="chevronIcon" :name="toggleChevronIconName" /> {{ __('Toggle thread') }} </button> </div> diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 9be53fe60f2..5073922e4a4 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -23,6 +23,7 @@ import { commentLineOptions, formatLineRange, } from './multiline_comment_utils'; +import { INLINE_DIFF_LINES_KEY } from '~/diffs/constants'; export default { name: 'NoteableNote', @@ -169,12 +170,8 @@ export default { return this.line && this.startLineNumber !== this.endLineNumber; }, commentLineOptions() { - const sideA = this.line.type === 'new' ? 'right' : 'left'; - const sideB = sideA === 'left' ? 'right' : 'left'; - const lines = this.diffFile.highlighted_diff_lines.length - ? this.diffFile.highlighted_diff_lines - : this.diffFile.parallel_diff_lines.map(l => l[sideA] || l[sideB]); - return commentLineOptions(lines, this.commentLineStart, this.line.line_code, sideA); + const lines = this.diffFile[INLINE_DIFF_LINES_KEY].length; + return commentLineOptions(lines, this.commentLineStart, this.line.line_code); }, diffFile() { if (this.commentLineStart.line_code) { diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js index 7acf2ad57c8..cc14ea42a89 100644 --- a/app/assets/javascripts/notes/constants.js +++ b/app/assets/javascripts/notes/constants.js @@ -1,3 +1,5 @@ +import { __ } from '~/locale'; + export const DISCUSSION_NOTE = 'DiscussionNote'; export const DIFF_NOTE = 'DiffNote'; export const DISCUSSION = 'discussion'; @@ -36,3 +38,16 @@ export const DISCUSSION_FILTER_TYPES = { COMMENTS: 'comments', HISTORY: 'history', }; + +export const toggleStateErrorMessage = { + Epic: { + [CLOSED]: __('Something went wrong while reopening the epic. Please try again later.'), + [OPENED]: __('Something went wrong while closing the epic. Please try again later.'), + [REOPENED]: __('Something went wrong while closing the epic. Please try again later.'), + }, + MergeRequest: { + [CLOSED]: __('Something went wrong while reopening the merge request. Please try again later.'), + [OPENED]: __('Something went wrong while closing the merge request. Please try again later.'), + [REOPENED]: __('Something went wrong while closing the merge request. Please try again later.'), + }, +}; diff --git a/app/assets/javascripts/notes/mixins/discussion_navigation.js b/app/assets/javascripts/notes/mixins/discussion_navigation.js index 61298a15c5d..c6932bfacae 100644 --- a/app/assets/javascripts/notes/mixins/discussion_navigation.js +++ b/app/assets/javascripts/notes/mixins/discussion_navigation.js @@ -1,16 +1,17 @@ import { mapGetters, mapActions, mapState } from 'vuex'; -import { scrollToElementWithContext } from '~/lib/utils/common_utils'; +import { scrollToElementWithContext, scrollToElement } from '~/lib/utils/common_utils'; import eventHub from '../event_hub'; /** * @param {string} selector * @returns {boolean} */ -function scrollTo(selector) { +function scrollTo(selector, { withoutContext = false } = {}) { const el = document.querySelector(selector); + const scrollFunction = withoutContext ? scrollToElement : scrollToElementWithContext; if (el) { - scrollToElementWithContext(el); + scrollFunction(el); return true; } @@ -35,7 +36,7 @@ function diffsJump({ expandDiscussion }, id) { function discussionJump({ expandDiscussion }, id) { const selector = `div.discussion[data-discussion-id="${id}"]`; expandDiscussion({ discussionId: id }); - return scrollTo(selector); + return scrollTo(selector, { withoutContext: true }); } /** diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 2c60b5ee84a..1fe5d6c2955 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -244,21 +244,7 @@ export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved, }); }; -export const toggleBlockedIssueWarning = ({ commit }, value) => { - commit(types.TOGGLE_BLOCKED_ISSUE_WARNING, value); - // Hides Close issue button at the top of issue page - const closeDropdown = document.querySelector('.js-issuable-close-dropdown'); - if (closeDropdown) { - closeDropdown.classList.toggle('d-none'); - } else { - const closeButton = document.querySelector( - '.detail-page-header-actions .btn-close.btn-grouped', - ); - closeButton.classList.toggle('d-md-block'); - } -}; - -export const closeIssue = ({ commit, dispatch, state }) => { +export const closeIssuable = ({ commit, dispatch, state }) => { dispatch('toggleStateButtonLoading', true); return axios.put(state.notesData.closePath).then(({ data }) => { commit(types.CLOSE_ISSUE); @@ -267,7 +253,7 @@ export const closeIssue = ({ commit, dispatch, state }) => { }); }; -export const reopenIssue = ({ commit, dispatch, state }) => { +export const reopenIssuable = ({ commit, dispatch, state }) => { dispatch('toggleStateButtonLoading', true); return axios.put(state.notesData.reopenPath).then(({ data }) => { commit(types.REOPEN_ISSUE); @@ -435,6 +421,10 @@ export const saveNote = ({ commit, dispatch }, noteData) => { }; const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => { + if (state.isResolvingDiscussion) { + return null; + } + if (resp.notes?.length) { dispatch('updateOrCreateNotes', resp.notes); dispatch('startTaskList'); @@ -574,6 +564,9 @@ export const submitSuggestion = ( const dispatchResolveDiscussion = () => dispatch('resolveDiscussion', { discussionId }).catch(() => {}); + commit(types.SET_RESOLVING_DISCUSSION, true); + dispatch('stopPolling'); + return Api.applySuggestion(suggestionId) .then(() => commit(types.APPLY_SUGGESTION, { discussionId, noteId, suggestionId })) .then(dispatchResolveDiscussion) @@ -587,6 +580,10 @@ export const submitSuggestion = ( const flashMessage = errorMessage || defaultMessage; Flash(__(flashMessage), 'alert', flashContainer); + }) + .finally(() => { + commit(types.SET_RESOLVING_DISCUSSION, false); + dispatch('restartPolling'); }); }; @@ -605,6 +602,8 @@ export const submitSuggestionBatch = ({ commit, dispatch, state }, { flashContai }); commit(types.SET_APPLYING_BATCH_STATE, true); + commit(types.SET_RESOLVING_DISCUSSION, true); + dispatch('stopPolling'); return Api.applySuggestionBatch(suggestionIds) .then(() => Promise.all(applyAllSuggestions())) @@ -621,7 +620,11 @@ export const submitSuggestionBatch = ({ commit, dispatch, state }, { flashContai Flash(__(flashMessage), 'alert', flashContainer); }) - .finally(() => commit(types.SET_APPLYING_BATCH_STATE, false)); + .finally(() => { + commit(types.SET_APPLYING_BATCH_STATE, false); + commit(types.SET_RESOLVING_DISCUSSION, false); + dispatch('restartPolling'); + }); }; export const addSuggestionInfoToBatch = ({ commit }, { suggestionId, noteId, discussionId }) => diff --git a/app/assets/javascripts/notes/stores/collapse_utils.js b/app/assets/javascripts/notes/stores/collapse_utils.js index d94fc626a3f..f34247d4eb0 100644 --- a/app/assets/javascripts/notes/stores/collapse_utils.js +++ b/app/assets/javascripts/notes/stores/collapse_utils.js @@ -70,6 +70,3 @@ export const collapseSystemNotes = notes => { return acc; }, []); }; - -// for babel-rewire -export default {}; diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js index a8738fa7c5f..4421a84a6b1 100644 --- a/app/assets/javascripts/notes/stores/modules/index.js +++ b/app/assets/javascripts/notes/stores/modules/index.js @@ -26,7 +26,6 @@ export default () => ({ // View layer isToggleStateButtonLoading: false, - isToggleBlockedIssueWarning: false, isNotesFetched: false, isLoading: true, isLoadingDescriptionVersion: false, @@ -42,6 +41,7 @@ export default () => ({ current_user: {}, preview_note_path: 'path/to/preview', }, + isResolvingDiscussion: false, commentsDisabled: false, resolvableDiscussionsCount: 0, unresolvedDiscussionsCount: 0, diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index 7496dd630f6..5c4f62f4575 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -38,12 +38,12 @@ export const SET_TIMELINE_VIEW = 'SET_TIMELINE_VIEW'; export const SET_SELECTED_COMMENT_POSITION = 'SET_SELECTED_COMMENT_POSITION'; export const SET_SELECTED_COMMENT_POSITION_HOVER = 'SET_SELECTED_COMMENT_POSITION_HOVER'; export const SET_FETCHING_DISCUSSIONS = 'SET_FETCHING_DISCUSSIONS'; +export const SET_RESOLVING_DISCUSSION = 'SET_RESOLVING_DISCUSSION'; // Issue export const CLOSE_ISSUE = 'CLOSE_ISSUE'; export const REOPEN_ISSUE = 'REOPEN_ISSUE'; export const TOGGLE_STATE_BUTTON_LOADING = 'TOGGLE_STATE_BUTTON_LOADING'; -export const TOGGLE_BLOCKED_ISSUE_WARNING = 'TOGGLE_BLOCKED_ISSUE_WARNING'; export const SET_ISSUE_CONFIDENTIAL = 'SET_ISSUE_CONFIDENTIAL'; export const SET_ISSUABLE_LOCK = 'SET_ISSUABLE_LOCK'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index 7cc619ec1c5..53387b2eaff 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -213,6 +213,10 @@ export default { } }, + [types.SET_RESOLVING_DISCUSSION](state, isResolving) { + state.isResolvingDiscussion = isResolving; + }, + [types.UPDATE_NOTE](state, note) { const noteObj = utils.findNoteObjectById(state.discussions, note.discussion_id); @@ -301,10 +305,6 @@ export default { Object.assign(state, { isToggleStateButtonLoading: value }); }, - [types.TOGGLE_BLOCKED_ISSUE_WARNING](state, value) { - Object.assign(state, { isToggleBlockedIssueWarning: value }); - }, - [types.SET_NOTES_FETCHED_STATE](state, value) { Object.assign(state, { isNotesFetched: value }); }, |