diff options
Diffstat (limited to 'app/assets/javascripts/batch_comments/components')
9 files changed, 625 insertions, 0 deletions
diff --git a/app/assets/javascripts/batch_comments/components/diff_file_drafts.vue b/app/assets/javascripts/batch_comments/components/diff_file_drafts.vue new file mode 100644 index 00000000000..570954c7200 --- /dev/null +++ b/app/assets/javascripts/batch_comments/components/diff_file_drafts.vue @@ -0,0 +1,41 @@ +<script> +import { mapGetters } from 'vuex'; +import imageDiff from '~/diffs/mixins/image_diff'; +import DraftNote from './draft_note.vue'; + +export default { + components: { + DraftNote, + }, + mixins: [imageDiff], + props: { + fileHash: { + type: String, + required: true, + }, + }, + computed: { + ...mapGetters('batchComments', ['draftsForFile']), + drafts() { + return this.draftsForFile(this.fileHash); + }, + }, +}; +</script> + +<template> + <div> + <div + v-for="(draft, index) in drafts" + :key="draft.id" + class="discussion-notes diff-discussions position-relative" + > + <div class="notes"> + <span class="d-block btn-transparent badge badge-pill is-draft js-diff-notes-index"> + {{ toggleText(draft, index) }} + </span> + <draft-note :draft="draft" /> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/batch_comments/components/draft_note.vue b/app/assets/javascripts/batch_comments/components/draft_note.vue new file mode 100644 index 00000000000..963d104b6b3 --- /dev/null +++ b/app/assets/javascripts/batch_comments/components/draft_note.vue @@ -0,0 +1,113 @@ +<script> +import { mapActions, mapGetters, mapState } from 'vuex'; +import NoteableNote from '~/notes/components/noteable_note.vue'; +import LoadingButton from '~/vue_shared/components/loading_button.vue'; +import PublishButton from './publish_button.vue'; + +export default { + components: { + NoteableNote, + PublishButton, + LoadingButton, + }, + props: { + draft: { + type: Object, + required: true, + }, + diffFile: { + type: Object, + required: false, + default: () => ({}), + }, + line: { + type: Object, + required: false, + default: null, + }, + }, + data() { + return { + isEditingDraft: false, + }; + }, + computed: { + ...mapState('batchComments', ['isPublishing']), + ...mapGetters('batchComments', ['isPublishingDraft']), + draftCommands() { + return this.draft.references.commands; + }, + }, + mounted() { + if (window.location.hash && window.location.hash === `#note_${this.draft.id}`) { + this.scrollToDraft(this.draft); + } + }, + methods: { + ...mapActions('batchComments', [ + 'deleteDraft', + 'updateDraft', + 'publishSingleDraft', + 'scrollToDraft', + 'toggleResolveDiscussion', + ]), + update(data) { + this.updateDraft(data); + }, + publishNow() { + this.publishSingleDraft(this.draft.id); + }, + handleEditing() { + this.isEditingDraft = true; + }, + handleNotEditing() { + this.isEditingDraft = false; + }, + }, +}; +</script> +<template> + <article class="draft-note-component note-wrapper"> + <ul class="notes draft-notes"> + <noteable-note + :note="draft" + :diff-lines="diffFile.highlighted_diff_lines" + :line="line" + class="draft-note" + @handleEdit="handleEditing" + @cancelForm="handleNotEditing" + @updateSuccess="handleNotEditing" + @handleDeleteNote="deleteDraft" + @handleUpdateNote="update" + @toggleResolveStatus="toggleResolveDiscussion(draft.id)" + > + <strong slot="note-header-info" class="badge draft-pending-label append-right-4"> + {{ __('Pending') }} + </strong> + </noteable-note> + </ul> + + <template v-if="!isEditingDraft"> + <div + v-if="draftCommands" + class="referenced-commands draft-note-commands" + v-html="draftCommands" + ></div> + + <p class="draft-note-actions d-flex"> + <publish-button + :show-count="true" + :should-publish="false" + class="btn btn-success btn-inverted gl-mr-3" + /> + <loading-button + ref="publishNowButton" + :loading="isPublishingDraft(draft.id) || isPublishing" + :label="__('Add comment now')" + container-class="btn btn-inverted" + @click="publishNow" + /> + </p> + </template> + </article> +</template> diff --git a/app/assets/javascripts/batch_comments/components/drafts_count.vue b/app/assets/javascripts/batch_comments/components/drafts_count.vue new file mode 100644 index 00000000000..f1180760c4d --- /dev/null +++ b/app/assets/javascripts/batch_comments/components/drafts_count.vue @@ -0,0 +1,15 @@ +<script> +import { mapGetters } from 'vuex'; + +export default { + computed: { + ...mapGetters('batchComments', ['draftsCount']), + }, +}; +</script> +<template> + <span class="drafts-count-component"> + <span class="drafts-count-number">{{ draftsCount }}</span> + <span class="sr-only"> {{ n__('draft', 'drafts', draftsCount) }} </span> + </span> +</template> diff --git a/app/assets/javascripts/batch_comments/components/inline_draft_comment_row.vue b/app/assets/javascripts/batch_comments/components/inline_draft_comment_row.vue new file mode 100644 index 00000000000..385725cd109 --- /dev/null +++ b/app/assets/javascripts/batch_comments/components/inline_draft_comment_row.vue @@ -0,0 +1,32 @@ +<script> +import DraftNote from './draft_note.vue'; + +export default { + components: { + DraftNote, + }, + props: { + draft: { + type: Object, + required: true, + }, + diffFile: { + type: Object, + required: true, + }, + line: { + type: Object, + required: false, + default: null, + }, + }, +}; +</script> + +<template> + <tr class="notes_holder js-temp-notes-holder"> + <td class="notes-content" colspan="4"> + <div class="content"><draft-note :draft="draft" :diff-file="diffFile" :line="line" /></div> + </td> + </tr> +</template> diff --git a/app/assets/javascripts/batch_comments/components/parallel_draft_comment_row.vue b/app/assets/javascripts/batch_comments/components/parallel_draft_comment_row.vue new file mode 100644 index 00000000000..68fd20e56bc --- /dev/null +++ b/app/assets/javascripts/batch_comments/components/parallel_draft_comment_row.vue @@ -0,0 +1,45 @@ +<script> +import { mapGetters } from 'vuex'; +import DraftNote from './draft_note.vue'; + +export default { + components: { + DraftNote, + }, + props: { + line: { + type: Object, + required: true, + }, + diffFileContentSha: { + type: String, + required: true, + }, + }, + computed: { + ...mapGetters('batchComments', ['draftForLine']), + className() { + return this.leftDraft > 0 || this.rightDraft > 0 ? '' : 'js-temp-notes-holder'; + }, + leftDraft() { + return this.draftForLine(this.diffFileContentSha, this.line, 'left'); + }, + rightDraft() { + return this.draftForLine(this.diffFileContentSha, this.line, 'right'); + }, + }, +}; +</script> + +<template> + <tr :class="className" class="notes_holder"> + <td class="notes_line old"></td> + <td class="notes-content parallel old" colspan="2"> + <div v-if="leftDraft.isDraft" class="content"><draft-note :draft="leftDraft" /></div> + </td> + <td class="notes_line new"></td> + <td class="notes-content parallel new" colspan="2"> + <div v-if="rightDraft.isDraft" class="content"><draft-note :draft="rightDraft" /></div> + </td> + </tr> +</template> diff --git a/app/assets/javascripts/batch_comments/components/preview_dropdown.vue b/app/assets/javascripts/batch_comments/components/preview_dropdown.vue new file mode 100644 index 00000000000..195e1b7ec5c --- /dev/null +++ b/app/assets/javascripts/batch_comments/components/preview_dropdown.vue @@ -0,0 +1,111 @@ +<script> +import { mapActions, mapGetters, mapState } from 'vuex'; +import { GlLoadingIcon } from '@gitlab/ui'; +import { sprintf, n__ } from '~/locale'; +import Icon from '~/vue_shared/components/icon.vue'; +import DraftsCount from './drafts_count.vue'; +import PublishButton from './publish_button.vue'; +import PreviewItem from './preview_item.vue'; + +export default { + components: { + GlLoadingIcon, + Icon, + DraftsCount, + PublishButton, + PreviewItem, + }, + computed: { + ...mapGetters(['isNotesFetched']), + ...mapGetters('batchComments', ['draftsCount', 'sortedDrafts']), + ...mapState('batchComments', ['showPreviewDropdown']), + dropdownTitle() { + return sprintf( + n__('%{count} pending comment', '%{count} pending comments', this.draftsCount), + { count: this.draftsCount }, + ); + }, + }, + watch: { + showPreviewDropdown() { + if (this.showPreviewDropdown && this.$refs.dropdown) { + this.$nextTick(() => this.$refs.dropdown.focus()); + } + }, + }, + mounted() { + document.addEventListener('click', this.onClickDocument); + }, + beforeDestroy() { + document.removeEventListener('click', this.onClickDocument); + }, + methods: { + ...mapActions('batchComments', ['toggleReviewDropdown']), + isLast(index) { + return index === this.sortedDrafts.length - 1; + }, + onClickDocument({ target }) { + if ( + this.showPreviewDropdown && + !target.closest('.review-preview-dropdown, .js-publish-draft-button') + ) { + this.toggleReviewDropdown(); + } + }, + }, +}; +</script> + +<template> + <div + class="dropdown float-right review-preview-dropdown" + :class="{ + show: showPreviewDropdown, + }" + > + <button + ref="dropdown" + type="button" + class="btn btn-success review-preview-dropdown-toggle qa-review-preview-toggle" + @click="toggleReviewDropdown" + > + {{ __('Finish review') }} + <drafts-count /> + <icon name="angle-up" /> + </button> + <div + class="dropdown-menu dropdown-menu-large dropdown-menu-right dropdown-open-top" + :class="{ + show: showPreviewDropdown, + }" + > + <div class="dropdown-title"> + {{ dropdownTitle }} + <button + :aria-label="__('Close')" + type="button" + class="dropdown-title-button dropdown-menu-close" + @click="toggleReviewDropdown" + > + <icon name="close" /> + </button> + </div> + <div class="dropdown-content"> + <ul v-if="isNotesFetched"> + <li v-for="(draft, index) in sortedDrafts" :key="draft.id"> + <preview-item :draft="draft" :is-last="isLast(index)" /> + </li> + </ul> + <gl-loading-icon v-else size="lg" class="prepend-top-default append-bottom-default" /> + </div> + <div class="dropdown-footer"> + <publish-button + :show-count="false" + :should-publish="true" + :label="__('Submit review')" + class="float-right gl-mr-3" + /> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/batch_comments/components/preview_item.vue b/app/assets/javascripts/batch_comments/components/preview_item.vue new file mode 100644 index 00000000000..22495eb4d7d --- /dev/null +++ b/app/assets/javascripts/batch_comments/components/preview_item.vue @@ -0,0 +1,143 @@ +<script> +import { mapActions, mapGetters } from 'vuex'; +import { IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants'; +import { sprintf, __ } from '~/locale'; +import Icon from '~/vue_shared/components/icon.vue'; +import resolvedStatusMixin from '../mixins/resolved_status'; +import { GlSprintf } from '@gitlab/ui'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { + getStartLineNumber, + getEndLineNumber, + getLineClasses, +} from '~/notes/components/multiline_comment_utils'; + +export default { + components: { + Icon, + GlSprintf, + }, + mixins: [resolvedStatusMixin, glFeatureFlagsMixin()], + props: { + draft: { + type: Object, + required: true, + }, + isLast: { + type: Boolean, + required: true, + }, + }, + computed: { + ...mapGetters('diffs', ['getDiffFileByHash']), + ...mapGetters(['getDiscussion']), + iconName() { + return this.isDiffDiscussion || this.draft.line_code ? 'doc-text' : 'comment'; + }, + discussion() { + return this.getDiscussion(this.draft.discussion_id); + }, + isDiffDiscussion() { + return this.discussion && this.discussion.diff_discussion; + }, + titleText() { + const file = this.discussion ? this.discussion.diff_file : this.draft; + + if (file) { + return file.file_path; + } + + return sprintf(__("%{authorsName}'s thread"), { + authorsName: this.discussion.notes.find(note => !note.system).author.name, + }); + }, + linePosition() { + if (this.draft.position && this.draft.position.position_type === IMAGE_DIFF_POSITION_TYPE) { + // eslint-disable-next-line @gitlab/require-i18n-strings + return `${this.draft.position.x}x ${this.draft.position.y}y`; + } + + const position = this.discussion ? this.discussion.position : this.draft.position; + + return position?.new_line || position?.old_line; + }, + content() { + const el = document.createElement('div'); + el.innerHTML = this.draft.note_html; + + return el.textContent; + }, + showLinePosition() { + return this.draft.file_hash || this.isDiffDiscussion; + }, + startLineNumber() { + return getStartLineNumber(this.draft.position?.line_range); + }, + endLineNumber() { + return getEndLineNumber(this.draft.position?.line_range); + }, + }, + methods: { + ...mapActions('batchComments', ['scrollToDraft']), + getLineClasses(lineNumber) { + return getLineClasses(lineNumber); + }, + }, + showStaysResolved: false, +}; +</script> + +<template> + <button + type="button" + class="review-preview-item menu-item" + :class="[ + componentClasses, + { + 'is-last': isLast, + }, + ]" + @click="scrollToDraft(draft)" + > + <span class="review-preview-item-header"> + <icon class="flex-shrink-0" :name="iconName" /> + <span + class="bold text-nowrap" + :class="{ 'gl-align-items-center': glFeatures.multilineComments }" + > + <span class="review-preview-item-header-text block-truncated"> + {{ titleText }} + </span> + <template v-if="showLinePosition"> + <template v-if="!glFeatures.multilineComments" + >:{{ linePosition }}</template + > + <template v-else-if="startLineNumber === endLineNumber"> + :<span :class="getLineClasses(startLineNumber)">{{ startLineNumber }}</span> + </template> + <gl-sprintf v-else :message="__(':%{startLine} to %{endLine}')"> + <template #startLine> + <span class="gl-mr-2" :class="getLineClasses(startLineNumber)">{{ + startLineNumber + }}</span> + </template> + <template #endLine> + <span class="gl-ml-2" :class="getLineClasses(endLineNumber)">{{ + endLineNumber + }}</span> + </template> + </gl-sprintf> + </template> + </span> + </span> + <span class="review-preview-item-content"> + <p>{{ content }}</p> + </span> + <span + v-if="draft.discussion_id && resolvedStatusMessage" + class="review-preview-item-footer draft-note-resolution p-0" + > + <icon class="gl-mr-3" name="status_success" /> {{ resolvedStatusMessage }} + </span> + </button> +</template> diff --git a/app/assets/javascripts/batch_comments/components/publish_button.vue b/app/assets/javascripts/batch_comments/components/publish_button.vue new file mode 100644 index 00000000000..f4dc0f04dc3 --- /dev/null +++ b/app/assets/javascripts/batch_comments/components/publish_button.vue @@ -0,0 +1,55 @@ +<script> +import { mapActions, mapState } from 'vuex'; +import { __ } from '~/locale'; +import LoadingButton from '~/vue_shared/components/loading_button.vue'; +import DraftsCount from './drafts_count.vue'; + +export default { + components: { + LoadingButton, + DraftsCount, + }, + props: { + showCount: { + type: Boolean, + required: false, + default: false, + }, + label: { + type: String, + required: false, + default: __('Finish review'), + }, + shouldPublish: { + type: Boolean, + required: true, + }, + }, + computed: { + ...mapState('batchComments', ['isPublishing']), + }, + methods: { + ...mapActions('batchComments', ['publishReview', 'toggleReviewDropdown']), + onClick() { + if (this.shouldPublish) { + this.publishReview(); + } else { + this.toggleReviewDropdown(); + } + }, + }, +}; +</script> + +<template> + <loading-button + :loading="isPublishing" + container-class="btn btn-success js-publish-draft-button qa-submit-review" + @click="onClick" + > + <span> + {{ label }} + <drafts-count v-if="showCount" /> + </span> + </loading-button> +</template> diff --git a/app/assets/javascripts/batch_comments/components/review_bar.vue b/app/assets/javascripts/batch_comments/components/review_bar.vue new file mode 100644 index 00000000000..b0e8b806701 --- /dev/null +++ b/app/assets/javascripts/batch_comments/components/review_bar.vue @@ -0,0 +1,70 @@ +<script> +import { mapActions, mapState, mapGetters } from 'vuex'; +import { GlModal, GlModalDirective } from '@gitlab/ui'; +import { sprintf, s__ } from '~/locale'; +import LoadingButton from '~/vue_shared/components/loading_button.vue'; +import PreviewDropdown from './preview_dropdown.vue'; + +export default { + components: { + LoadingButton, + GlModal, + PreviewDropdown, + }, + directives: { + 'gl-modal': GlModalDirective, + }, + computed: { + ...mapGetters(['isNotesFetched']), + ...mapState('batchComments', ['isDiscarding']), + ...mapGetters('batchComments', ['draftsCount']), + }, + watch: { + isNotesFetched() { + if (this.isNotesFetched) { + this.expandAllDiscussions(); + } + }, + }, + methods: { + ...mapActions('batchComments', ['discardReview', 'expandAllDiscussions']), + }, + modalId: 'discard-draft-review', + text: sprintf( + s__( + `BatchComments|You're about to discard your review which will delete all of your pending comments. + The deleted comments %{strong_start}cannot%{strong_end} be restored.`, + ), + { + strong_start: '<strong>', + strong_end: '</strong>', + }, + false, + ), +}; +</script> +<template> + <div v-show="draftsCount > 0"> + <nav class="review-bar-component"> + <div class="review-bar-content qa-review-bar"> + <preview-dropdown /> + <loading-button + v-gl-modal="$options.modalId" + :loading="isDiscarding" + :label="__('Discard review')" + class="qa-discard-review float-right" + /> + </div> + </nav> + <gl-modal + :title="s__('BatchComments|Discard review?')" + :ok-title="s__('BatchComments|Delete all pending comments')" + :modal-id="$options.modalId" + title-tag="h4" + ok-variant="danger qa-modal-delete-pending-comments" + @ok="discardReview" + > + <p v-html="$options.text"></p> + </gl-modal> + </div> +</template> |