Skip to content
Snippets Groups Projects
Unverified Commit adf8ad9e authored by Phil Hughes's avatar Phil Hughes
Browse files

Improve discussion rendering performance

Improve the renderign of new and existing discussions
by reducing the number of watchers on each object & array.
Previously every discussion change would trigger an update for every
discussion component.

Also tidied up some components to get them closer to our docs.

Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/51506
parent 921d6b1a
No related branches found
No related tags found
No related merge requests found
Showing
with 240 additions and 333 deletions
Loading
Loading
@@ -99,7 +99,7 @@ export default {
methods: {
...mapActions('diffs', ['loadMoreLines', 'showCommentForm']),
handleCommentButton() {
this.showCommentForm({ lineCode: this.line.line_code });
this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash });
},
handleLoadMoreLines() {
if (this.isRequesting) {
Loading
Loading
@@ -160,7 +160,7 @@ export default {
>
<template v-else>
<button
v-if="shouldShowCommentButton"
v-show="shouldShowCommentButton"
type="button"
class="add-diff-note js-add-diff-note-button qa-diff-comment"
title="Add a comment to this line"
Loading
Loading
Loading
Loading
@@ -73,6 +73,7 @@ export default {
 
this.cancelCommentForm({
lineCode: this.line.line_code,
fileHash: this.diffFileHash,
});
this.$nextTick(() => {
this.resetAutoSave();
Loading
Loading
<script>
import { mapState } from 'vuex';
import diffDiscussions from './diff_discussions.vue';
import diffLineNoteForm from './diff_line_note_form.vue';
 
Loading
Loading
@@ -17,29 +16,31 @@ export default {
type: String,
required: true,
},
lineIndex: {
type: Number,
required: true,
},
},
computed: {
...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}),
className() {
return this.line.discussions.length ? '' : 'js-temp-notes-holder';
},
shouldRender() {
if (this.line.hasForm) return true;
if (!this.line.discussions || !this.line.discussions.length) {
return false;
}
return this.line.discussions.every(discussion => discussion.expanded);
},
},
};
</script>
 
<template>
<tr :class="className" class="notes_holder">
<tr v-if="shouldRender" :class="className" class="notes_holder">
<td class="notes_content" colspan="3">
<div class="content">
<diff-discussions v-if="line.discussions.length" :discussions="line.discussions" />
<diff-line-note-form
v-if="diffLineCommentForms[line.line_code]"
v-if="line.hasForm"
:diff-file-hash="diffFileHash"
:line="line"
:note-target-line="line"
Loading
Loading
<script>
import { mapGetters, mapState } from 'vuex';
import { mapGetters } from 'vuex';
import inlineDiffTableRow from './inline_diff_table_row.vue';
import inlineDiffCommentRow from './inline_diff_comment_row.vue';
 
Loading
Loading
@@ -19,23 +19,18 @@ export default {
},
},
computed: {
...mapGetters('diffs', ['commitId', 'shouldRenderInlineCommentRow']),
...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}),
...mapGetters('diffs', ['commitId']),
diffLinesLength() {
return this.diffLines.length;
},
userColorScheme() {
return window.gon.user_color_scheme;
},
},
userColorScheme: window.gon.user_color_scheme,
};
</script>
 
<template>
<table
:class="userColorScheme"
:class="$options.userColorScheme"
:data-commit-id="commitId"
class="code diff-wrap-lines js-syntax-highlight text-file js-diff-inline-view"
>
Loading
Loading
@@ -49,11 +44,9 @@ export default {
:is-bottom="index + 1 === diffLinesLength"
/>
<inline-diff-comment-row
v-if="shouldRenderInlineCommentRow(line)"
:key="index"
:key="`icr-${index}`"
:diff-file-hash="diffFile.file_hash"
:line="line"
:line-index="index"
/>
</template>
</tbody>
Loading
Loading
<script>
import { mapState } from 'vuex';
import diffDiscussions from './diff_discussions.vue';
import diffLineNoteForm from './diff_line_note_form.vue';
 
Loading
Loading
@@ -23,22 +22,13 @@ export default {
},
},
computed: {
...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}),
leftLineCode() {
return this.line.left && this.line.left.line_code;
},
rightLineCode() {
return this.line.right && this.line.right.line_code;
},
hasExpandedDiscussionOnLeft() {
return this.line.left && this.line.left.discussions
return this.line.left && this.line.left.discussions.length
? this.line.left.discussions.every(discussion => discussion.expanded)
: false;
},
hasExpandedDiscussionOnRight() {
return this.line.right && this.line.right.discussions
return this.line.right && this.line.right.discussions.length
? this.line.right.discussions.every(discussion => discussion.expanded)
: false;
},
Loading
Loading
@@ -57,9 +47,10 @@ export default {
);
},
showRightSideCommentForm() {
return (
this.line.right && this.line.right.type && this.diffLineCommentForms[this.rightLineCode]
);
return this.line.right && this.line.right.type && this.line.right.hasForm;
},
showLeftSideCommentForm() {
return this.line.left && this.line.left.hasForm;
},
className() {
return (this.left && this.line.left.discussions.length > 0) ||
Loading
Loading
@@ -67,12 +58,30 @@ export default {
? ''
: 'js-temp-notes-holder';
},
shouldRender() {
const { line } = this;
const hasDiscussion =
(line.left && line.left.discussions && line.left.discussions.length) ||
(line.right && line.right.discussions && line.right.discussions.length);
if (
hasDiscussion &&
(this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight)
) {
return true;
}
const hasCommentFormOnLeft = line.left && line.left.hasForm;
const hasCommentFormOnRight = line.right && line.right.hasForm;
return hasCommentFormOnLeft || hasCommentFormOnRight;
},
},
};
</script>
 
<template>
<tr :class="className" class="notes_holder">
<tr v-if="shouldRender" :class="className" class="notes_holder">
<td class="notes_content parallel old" colspan="2">
<div v-if="shouldRenderDiscussionsOnLeft" class="content">
<diff-discussions
Loading
Loading
@@ -81,7 +90,7 @@ export default {
/>
</div>
<diff-line-note-form
v-if="diffLineCommentForms[leftLineCode]"
v-if="showLeftSideCommentForm"
:diff-file-hash="diffFileHash"
:line="line.left"
:note-target-line="line.left"
Loading
Loading
<script>
import { mapState, mapGetters } from 'vuex';
import { mapGetters } from 'vuex';
import parallelDiffTableRow from './parallel_diff_table_row.vue';
import parallelDiffCommentRow from './parallel_diff_comment_row.vue';
 
Loading
Loading
@@ -19,23 +19,18 @@ export default {
},
},
computed: {
...mapGetters('diffs', ['commitId', 'shouldRenderParallelCommentRow']),
...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}),
...mapGetters('diffs', ['commitId']),
diffLinesLength() {
return this.diffLines.length;
},
userColorScheme() {
return window.gon.user_color_scheme;
},
},
userColorScheme: window.gon.user_color_scheme,
};
</script>
 
<template>
<div
:class="userColorScheme"
:class="$options.userColorScheme"
:data-commit-id="commitId"
class="code diff-wrap-lines js-syntax-highlight text-file"
>
Loading
Loading
@@ -50,7 +45,6 @@ export default {
:is-bottom="index + 1 === diffLinesLength"
/>
<parallel-diff-comment-row
v-if="shouldRenderParallelCommentRow(line)"
:key="`dcr-${index}`"
:line="line"
:diff-file-hash="diffFile.file_hash"
Loading
Loading
Loading
Loading
@@ -99,12 +99,12 @@ export const setParallelDiffViewType = ({ commit }) => {
historyPushState(url);
};
 
export const showCommentForm = ({ commit }, params) => {
commit(types.ADD_COMMENT_FORM_LINE, params);
export const showCommentForm = ({ commit }, { lineCode, fileHash }) => {
commit(types.TOGGLE_LINE_HAS_FORM, { lineCode, fileHash, hasForm: true });
};
 
export const cancelCommentForm = ({ commit }, params) => {
commit(types.REMOVE_COMMENT_FORM_LINE, params);
export const cancelCommentForm = ({ commit }, { lineCode, fileHash }) => {
commit(types.TOGGLE_LINE_HAS_FORM, { lineCode, fileHash, hasForm: false });
};
 
export const loadMoreLines = ({ commit }, options) => {
Loading
Loading
@@ -191,8 +191,8 @@ export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => {
return dispatch('saveNote', postData, { root: true })
.then(result => dispatch('updateDiscussion', result.discussion, { root: true }))
.then(discussion => dispatch('assignDiscussionsToDiff', [discussion]))
.then(() => dispatch('updateResolvableDiscussonsCounts', null, { root: true }))
.then(() => dispatch('closeDiffFileCommentForm', formData.diffFile.file_hash))
.then(() => dispatch('startTaskList', null, { root: true }))
.catch(() => createFlash(s__('MergeRequests|Saving the comment failed')));
};
 
Loading
Loading
Loading
Loading
@@ -70,40 +70,6 @@ export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) =
discussion => discussion.diff_discussion && discussion.diff_file.file_hash === diff.file_hash,
) || [];
 
export const shouldRenderParallelCommentRow = state => line => {
const hasDiscussion =
(line.left && line.left.discussions && line.left.discussions.length) ||
(line.right && line.right.discussions && line.right.discussions.length);
const hasExpandedDiscussionOnLeft =
line.left && line.left.discussions && line.left.discussions.length
? line.left.discussions.every(discussion => discussion.expanded)
: false;
const hasExpandedDiscussionOnRight =
line.right && line.right.discussions && line.right.discussions.length
? line.right.discussions.every(discussion => discussion.expanded)
: false;
if (hasDiscussion && (hasExpandedDiscussionOnLeft || hasExpandedDiscussionOnRight)) {
return true;
}
const hasCommentFormOnLeft = line.left && state.diffLineCommentForms[line.left.line_code];
const hasCommentFormOnRight = line.right && state.diffLineCommentForms[line.right.line_code];
return hasCommentFormOnLeft || hasCommentFormOnRight;
};
export const shouldRenderInlineCommentRow = state => line => {
if (state.diffLineCommentForms[line.line_code]) return true;
if (!line.discussions || line.discussions.length === 0) {
return false;
}
return line.discussions.every(discussion => discussion.expanded);
};
// prevent babel-plugin-rewire from generating an invalid default during karma∂ tests
export const getDiffFileByHash = state => fileHash =>
state.diffFiles.find(file => file.file_hash === fileHash);
Loading
Loading
Loading
Loading
@@ -17,7 +17,6 @@ export default () => ({
diffFiles: [],
mergeRequestDiffs: [],
mergeRequestDiff: null,
diffLineCommentForms: {},
diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType,
tree: [],
treeEntries: {},
Loading
Loading
Loading
Loading
@@ -3,8 +3,7 @@ export const SET_LOADING = 'SET_LOADING';
export const SET_DIFF_DATA = 'SET_DIFF_DATA';
export const SET_DIFF_VIEW_TYPE = 'SET_DIFF_VIEW_TYPE';
export const SET_MERGE_REQUEST_DIFFS = 'SET_MERGE_REQUEST_DIFFS';
export const ADD_COMMENT_FORM_LINE = 'ADD_COMMENT_FORM_LINE';
export const REMOVE_COMMENT_FORM_LINE = 'REMOVE_COMMENT_FORM_LINE';
export const TOGGLE_LINE_HAS_FORM = 'TOGGLE_LINE_HAS_FORM';
export const ADD_CONTEXT_LINES = 'ADD_CONTEXT_LINES';
export const ADD_COLLAPSED_DIFFS = 'ADD_COLLAPSED_DIFFS';
export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES';
Loading
Loading
import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { sortTree } from '~/ide/stores/utils';
import {
Loading
Loading
@@ -49,12 +48,30 @@ export default {
Object.assign(state, { diffViewType });
},
 
[types.ADD_COMMENT_FORM_LINE](state, { lineCode }) {
Vue.set(state.diffLineCommentForms, lineCode, true);
},
[types.TOGGLE_LINE_HAS_FORM](state, { lineCode, fileHash, hasForm }) {
const diffFile = state.diffFiles.find(f => f.file_hash === fileHash);
if (!diffFile) return;
if (diffFile.highlighted_diff_lines) {
diffFile.highlighted_diff_lines.find(l => l.line_code === lineCode).hasForm = hasForm;
}
if (diffFile.parallel_diff_lines) {
const line = diffFile.parallel_diff_lines.find(l => {
const { left, right } = l;
 
[types.REMOVE_COMMENT_FORM_LINE](state, { lineCode }) {
Vue.delete(state.diffLineCommentForms, lineCode);
return (left && left.line_code === lineCode) || (right && right.line_code === lineCode);
});
if (line.left && line.left.line_code === lineCode) {
line.left.hasForm = hasForm;
}
if (line.right && line.right.line_code === lineCode) {
line.right.hasForm = hasForm;
}
}
},
 
[types.ADD_CONTEXT_LINES](state, options) {
Loading
Loading
@@ -68,6 +85,7 @@ export default {
...line,
line_code: line.line_code || `${fileHash}_${line.old_line}_${line.new_line}`,
discussions: line.discussions || [],
hasForm: false,
}));
 
addContextLines({
Loading
Loading
Loading
Loading
@@ -209,9 +209,11 @@ export function prepareDiffData(diffData) {
const line = file.parallel_diff_lines[u];
if (line.left) {
line.left = trimFirstCharOfLineContent(line.left);
line.left.hasForm = false;
}
if (line.right) {
line.right = trimFirstCharOfLineContent(line.right);
line.right.hasForm = false;
}
}
}
Loading
Loading
@@ -220,7 +222,7 @@ export function prepareDiffData(diffData) {
const linesLength = file.highlighted_diff_lines.length;
for (let u = 0; u < linesLength; u += 1) {
const line = file.highlighted_diff_lines[u];
Object.assign(line, { ...trimFirstCharOfLineContent(line) });
Object.assign(line, { ...trimFirstCharOfLineContent(line), hasForm: false });
}
showingLines += file.parallel_diff_lines.length;
}
Loading
Loading
Loading
Loading
@@ -4,7 +4,9 @@ import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
import { GlSkeletonLoading } from '@gitlab/ui';
import { trimFirstCharOfLineContent, getDiffMode } from '~/diffs/store/utils';
import { getDiffMode } from '~/diffs/store/utils';
const FIRST_CHAR_REGEX = /^(\+|-| )/;
 
export default {
components: {
Loading
Loading
@@ -26,46 +28,16 @@ export default {
},
computed: {
...mapState({
noteableData: state => state.notes.noteableData,
projectPath: state => state.diffs.projectPath,
}),
diffMode() {
return getDiffMode(this.diffFile);
return getDiffMode(this.discussion.diff_file);
},
hasTruncatedDiffLines() {
return (
this.discussion.truncated_diff_lines && this.discussion.truncated_diff_lines.length !== 0
);
},
isDiscussionsExpanded() {
return true; // TODO: @fatihacet - Fix this.
},
isCollapsed() {
return this.diffFile.collapsed || false;
},
isImageDiff() {
return !this.diffFile.text;
},
diffFileClass() {
const { text } = this.diffFile;
return text ? 'text-file' : 'js-image-file';
},
diffFile() {
return this.discussion.diff_file;
},
imageDiffHtml() {
return this.discussion.image_diff_html;
},
userColorScheme() {
return window.gon.user_color_scheme;
},
normalizedDiffLines() {
if (this.discussion.truncated_diff_lines) {
return this.discussion.truncated_diff_lines.map(line => trimFirstCharOfLineContent(line));
}
return [];
},
},
mounted() {
if (!this.hasTruncatedDiffLines) {
Loading
Loading
@@ -74,9 +46,6 @@ export default {
},
methods: {
...mapActions(['fetchDiscussionDiffLines']),
rowTag(html) {
return html.outerHTML ? 'tr' : 'template';
},
fetchDiff() {
this.error = false;
this.fetchDiscussionDiffLines(this.discussion)
Loading
Loading
@@ -85,31 +54,45 @@ export default {
this.error = true;
});
},
trimChar(line) {
return line.replace(FIRST_CHAR_REGEX, '');
},
},
userColorSchemeClass: window.gon.user_color_scheme,
};
</script>
 
<template>
<div ref="fileHolder" :class="diffFileClass" class="diff-file file-holder">
<div :class="{ 'text-file': discussion.diff_file.text }" class="diff-file file-holder">
<diff-file-header
:discussion-path="discussion.discussion_path"
:diff-file="diffFile"
:diff-file="discussion.diff_file"
:can-current-user-fork="false"
:discussions-expanded="isDiscussionsExpanded"
:expanded="!isCollapsed"
:expanded="!discussion.diff_file.collapsed"
/>
<div v-if="diffFile.text" :class="userColorScheme" class="diff-content code">
<div
v-if="discussion.diff_file.text"
:class="$options.userColorSchemeClass"
class="diff-content code"
>
<table>
<tr v-for="line in normalizedDiffLines" :key="line.line_code" class="line_holder">
<td class="diff-line-num old_line">{{ line.old_line }}</td>
<td class="diff-line-num new_line">{{ line.new_line }}</td>
<td :class="line.type" class="line_content" v-html="line.rich_text"></td>
</tr>
<template v-if="hasTruncatedDiffLines">
<tr
v-for="line in discussion.truncated_diff_lines"
v-once
:key="line.line_code"
class="line_holder"
>
<td class="diff-line-num old_line">{{ line.old_line }}</td>
<td class="diff-line-num new_line">{{ line.new_line }}</td>
<td :class="line.type" class="line_content" v-html="trimChar(line.rich_text)"></td>
</tr>
</template>
<tr v-if="!hasTruncatedDiffLines" class="line_holder line-holder-placeholder">
<td class="old_line diff-line-num"></td>
<td class="new_line diff-line-num"></td>
<td v-if="error" class="js-error-lazy-load-diff diff-loading-error-block">
Unable to load the diff
{{ error }} Unable to load the diff
<button
class="btn-link btn-link-retry btn-no-padding js-toggle-lazy-diff-retry-button"
@click="fetchDiff"
Loading
Loading
@@ -131,17 +114,17 @@ export default {
<div v-else>
<diff-viewer
:diff-mode="diffMode"
:new-path="diffFile.new_path"
:new-sha="diffFile.diff_refs.head_sha"
:old-path="diffFile.old_path"
:old-sha="diffFile.diff_refs.base_sha"
:file-hash="diffFile.file_hash"
:new-path="discussion.diff_file.new_path"
:new-sha="discussion.diff_file.diff_refs.head_sha"
:old-path="discussion.diff_file.old_path"
:old-sha="discussion.diff_file.diff_refs.base_sha"
:file-hash="discussion.diff_file.file_hash"
:project-path="projectPath"
>
<image-diff-overlay
slot="image-overlay"
:discussions="discussion"
:file-hash="diffFile.file_hash"
:file-hash="discussion.diff_file.file_hash"
:show-comment-icon="true"
:should-toggle-discussion="false"
badge-class="image-comment-badge"
Loading
Loading
<script>
import { mapActions, mapGetters } from 'vuex';
import { GlTooltipDirective } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import { pluralize } from '../../lib/utils/text_utility';
import discussionNavigation from '../mixins/discussion_navigation';
import tooltip from '../../vue_shared/directives/tooltip';
 
export default {
directives: {
tooltip,
GlTooltip: GlTooltipDirective,
},
components: {
Icon,
Loading
Loading
@@ -17,9 +16,9 @@ export default {
...mapGetters([
'getUserData',
'getNoteableData',
'discussionCount',
'resolvableDiscussionsCount',
'firstUnresolvedDiscussionId',
'resolvedDiscussionCount',
'unresolvedDiscussionsCount',
]),
isLoggedIn() {
return this.getUserData.id;
Loading
Loading
@@ -27,15 +26,15 @@ export default {
hasNextButton() {
return this.isLoggedIn && !this.allResolved;
},
countText() {
return pluralize('discussion', this.discussionCount);
},
allResolved() {
return this.resolvedDiscussionCount === this.discussionCount;
return this.unresolvedDiscussionsCount === 0;
},
resolveAllDiscussionsIssuePath() {
return this.getNoteableData.create_issue_to_resolve_discussions_path;
},
resolvedDiscussionsCount() {
return this.resolvableDiscussionsCount - this.unresolvedDiscussionsCount;
},
},
methods: {
...mapActions(['expandDiscussion']),
Loading
Loading
@@ -50,7 +49,7 @@ export default {
</script>
 
<template>
<div v-if="discussionCount > 0" class="line-resolve-all-container prepend-top-8">
<div v-if="resolvableDiscussionsCount > 0" class="line-resolve-all-container prepend-top-8">
<div>
<div :class="{ 'has-next-btn': hasNextButton }" class="line-resolve-all">
<span
Loading
Loading
@@ -61,15 +60,15 @@ export default {
<icon name="check-circle" />
</span>
<span class="line-resolve-text">
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ countText }} resolved
{{ resolvedDiscussionsCount }}/{{ resolvableDiscussionsCount }}
{{ n__('discussion resolved', 'discussions resolved', resolvableDiscussionsCount) }}
</span>
</div>
<div v-if="resolveAllDiscussionsIssuePath && !allResolved" class="btn-group" role="group">
<a
v-tooltip
v-gl-tooltip
:href="resolveAllDiscussionsIssuePath"
:title="s__('Resolve all discussions in new issue')"
data-container="body"
class="new-issue-for-discussion btn btn-default discussion-create-issue-btn"
>
<icon name="issue-new" />
Loading
Loading
@@ -77,9 +76,8 @@ export default {
</div>
<div v-if="isLoggedIn && !allResolved" class="btn-group" role="group">
<button
v-tooltip
v-gl-tooltip
title="Jump to first unresolved discussion"
data-container="body"
class="btn btn-default discussion-next-btn"
@click="jumpToFirstUnresolvedDiscussion"
>
Loading
Loading
<script>
import { mapGetters } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { GlLoadingIcon } from '@gitlab/ui';
import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
 
export default {
name: 'NoteActions',
Loading
Loading
@@ -11,7 +10,7 @@ export default {
GlLoadingIcon,
},
directives: {
tooltip,
GlTooltip: GlTooltipDirective,
},
props: {
authorId: {
Loading
Loading
@@ -119,10 +118,10 @@ export default {
 
<template>
<div class="note-actions">
<span v-if="accessLevel" class="note-role user-access-role"> {{ accessLevel }} </span>
<span v-if="accessLevel" class="note-role user-access-role">{{ accessLevel }}</span>
<div v-if="canResolve" class="note-actions-item">
<button
v-tooltip
v-gl-tooltip
:class="{ 'is-disabled': !resolvable, 'is-active': isResolved }"
:title="resolveButtonTitle"
:aria-label="resolveButtonTitle"
Loading
Loading
@@ -138,12 +137,10 @@ export default {
</div>
<div v-if="canAwardEmoji" class="note-actions-item">
<a
v-tooltip
v-gl-tooltip.bottom
:class="{ 'js-user-authored': isAuthoredByCurrentUser }"
class="note-action-button note-emoji-button js-add-award js-note-emoji"
data-position="right"
data-placement="bottom"
data-container="body"
href="#"
title="Add reaction"
>
Loading
Loading
@@ -158,12 +155,10 @@ export default {
</div>
<div v-if="canEdit" class="note-actions-item">
<button
v-tooltip
v-gl-tooltip.bottom
type="button"
title="Edit comment"
class="note-action-button js-note-edit btn btn-transparent"
data-container="body"
data-placement="bottom"
@click="onEdit"
>
<icon name="pencil" css-classes="link-highlight" />
Loading
Loading
@@ -171,12 +166,10 @@ export default {
</div>
<div v-if="showDeleteAction" class="note-actions-item">
<button
v-tooltip
v-gl-tooltip.bottom
type="button"
title="Delete comment"
class="note-action-button js-note-delete btn btn-transparent"
data-container="body"
data-placement="bottom"
@click="onDelete"
>
<icon name="remove" class="link-highlight" />
Loading
Loading
@@ -184,19 +177,17 @@ export default {
</div>
<div v-else-if="shouldShowActionsDropdown" class="dropdown more-actions note-actions-item">
<button
v-tooltip
v-gl-tooltip.bottom
type="button"
title="More actions"
class="note-action-button more-actions-toggle btn btn-transparent"
data-toggle="dropdown"
data-container="body"
data-placement="bottom"
>
<icon css-classes="icon" name="ellipsis_v" />
</button>
<ul class="dropdown-menu more-actions-dropdown dropdown-open-left">
<li v-if="canReportAsAbuse">
<a :href="reportAbusePath"> {{ __('Report abuse to GitLab') }} </a>
<a :href="reportAbusePath">{{ __('Report abuse to GitLab') }}</a>
</li>
<li v-if="noteUrl">
<button
Loading
Loading
@@ -213,7 +204,7 @@ export default {
type="button"
@click.prevent="onDelete"
>
<span class="text-danger"> {{ __('Delete comment') }} </span>
<span class="text-danger">{{ __('Delete comment') }}</span>
</button>
</li>
</ul>
Loading
Loading
<script>
import { mapActions, mapGetters } from 'vuex';
import { GlTooltipDirective } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import Flash from '../../flash';
import { glEmojiTag } from '../../emoji';
import tooltip from '../../vue_shared/directives/tooltip';
 
export default {
components: {
Icon,
},
directives: {
tooltip,
GlTooltip: GlTooltipDirective,
},
props: {
awards: {
Loading
Loading
@@ -167,21 +167,19 @@ export default {
<button
v-for="(awardList, awardName, index) in groupedAwards"
:key="index"
v-tooltip
v-gl-tooltip.bottom="{ boundary: 'viewport' }"
:class="getAwardClassBindings(awardList)"
:title="awardTitle(awardList)"
class="btn award-control"
data-boundary="viewport"
data-placement="bottom"
type="button"
@click="handleAward(awardName);"
>
<span v-html="getAwardHTML(awardName)"></span>
<span class="award-control-text js-counter"> {{ awardList.length }} </span>
<span class="award-control-text js-counter">{{ awardList.length }}</span>
</button>
<div v-if="canAwardEmoji" class="award-menu-holder">
<button
v-tooltip
v-gl-tooltip
:class="{ 'js-user-authored': isAuthoredByMe }"
class="award-control btn js-add-award"
title="Add reaction"
Loading
Loading
Loading
Loading
@@ -73,7 +73,7 @@ export default {
{{ __('Toggle discussion') }}
</button>
</div>
<a v-if="hasAuthor" :href="author.path">
<a v-if="hasAuthor" v-once :href="author.path">
<span class="note-header-author-name">{{ author.name }}</span>
<span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span>
<span class="note-headline-light"> @{{ author.username }} </span>
Loading
Loading
<script>
import { mapActions, mapGetters } from 'vuex';
import { GlTooltipDirective } from '@gitlab/ui';
import { truncateSha } from '~/lib/utils/text_utility';
import { s__ } from '~/locale';
import { s__, __ } from '~/locale';
import systemNote from '~/vue_shared/components/notes/system_note.vue';
import icon from '~/vue_shared/components/icon.vue';
import Flash from '../../flash';
Loading
Loading
@@ -20,14 +21,12 @@ import autosave from '../mixins/autosave';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
import discussionNavigation from '../mixins/discussion_navigation';
import tooltip from '../../vue_shared/directives/tooltip';
 
export default {
name: 'NoteableDiscussion',
components: {
icon,
noteableNote,
diffWithNote,
userAvatarLink,
noteHeader,
noteSignedOutWidget,
Loading
Loading
@@ -39,7 +38,7 @@ export default {
systemNote,
},
directives: {
tooltip,
GlTooltip: GlTooltipDirective,
},
mixins: [autosave, noteable, resolvable, discussionNavigation],
props: {
Loading
Loading
@@ -74,33 +73,12 @@ export default {
computed: {
...mapGetters([
'getNoteableData',
'discussionCount',
'resolvedDiscussionCount',
'allDiscussions',
'unresolvedDiscussionsIdsByDiff',
'unresolvedDiscussionsIdsByDate',
'unresolvedDiscussions',
'unresolvedDiscussionsIdsOrdered',
'nextUnresolvedDiscussionId',
'isLastUnresolvedDiscussion',
'unresolvedDiscussionsCount',
'hasUnresolvedDiscussions',
]),
transformedDiscussion() {
return {
...this.discussion.notes[0],
truncated_diff_lines: this.discussion.truncated_diff_lines || [],
truncated_diff_lines_path: this.discussion.truncated_diff_lines_path,
diff_file: this.discussion.diff_file,
diff_discussion: this.discussion.diff_discussion,
active: this.discussion.active,
discussion_path: this.discussion.discussion_path,
resolved: this.discussion.resolved,
resolved_by: this.discussion.resolved_by,
resolved_by_push: this.discussion.resolved_by_push,
resolved_at: this.discussion.resolved_at,
};
},
author() {
return this.transformedDiscussion.author;
return this.initialDiscussion.author;
},
canReply() {
return this.getNoteableData.current_user.can_create_note;
Loading
Loading
@@ -136,29 +114,13 @@ export default {
return null;
},
resolvedText() {
return this.transformedDiscussion.resolved_by_push ? 'Automatically resolved' : 'Resolved';
},
hasMultipleUnresolvedDiscussions() {
return this.unresolvedDiscussions.length > 1;
},
showJumpToNextDiscussion() {
return (
this.hasMultipleUnresolvedDiscussions &&
!this.isLastUnresolvedDiscussion(this.discussion.id, this.discussionsByDiffOrder)
);
return this.discussion.resolved_by_push ? __('Automatically resolved') : __('Resolved');
},
shouldRenderDiffs() {
return (
this.transformedDiscussion.diff_discussion &&
this.transformedDiscussion.diff_file &&
this.renderDiffFile
);
return this.discussion.diff_discussion && this.renderDiffFile;
},
shouldGroupReplies() {
return !this.shouldRenderDiffs && !this.transformedDiscussion.diff_discussion;
},
shouldRenderHeader() {
return this.shouldRenderDiffs;
return !this.shouldRenderDiffs && !this.discussion.diff_discussion;
},
wrapperComponent() {
return this.shouldRenderDiffs ? diffWithNote : 'div';
Loading
Loading
@@ -170,9 +132,6 @@ export default {
 
return {};
},
wrapperClass() {
return this.isDiffDiscussion ? '' : 'card discussion-wrapper';
},
componentClassName() {
if (this.shouldRenderDiffs) {
if (!this.lastUpdatedAt && !this.discussion.resolved) {
Loading
Loading
@@ -183,11 +142,10 @@ export default {
return '';
},
shouldShowDiscussions() {
const isExpanded = this.discussion.expanded;
const { resolved } = this.transformedDiscussion;
const isResolvedNonDiffDiscussion = !this.transformedDiscussion.diff_discussion && resolved;
const { expanded, resolved } = this.discussion;
const isResolvedNonDiffDiscussion = !this.discussion.diff_discussion && resolved;
 
return isExpanded || this.alwaysExpanded || isResolvedNonDiffDiscussion;
return expanded || this.alwaysExpanded || isResolvedNonDiffDiscussion;
},
isRepliesCollapsed() {
const { discussion, isRepliesToggledByUser } = this;
Loading
Loading
@@ -204,7 +162,7 @@ export default {
if (this.isReplying) {
this.$nextTick(() => {
// Pass an extra key to separate reply and note edit forms
this.initAutoSave(this.transformedDiscussion, ['Reply']);
this.initAutoSave({ ...this.initialDiscussion, ...this.discussion }, ['Reply']);
});
} else {
this.disposeAutoSave();
Loading
Loading
@@ -314,12 +272,9 @@ Please check your network connection and try again.`;
<li class="note note-discussion timeline-entry" :class="componentClassName">
<div class="timeline-entry-inner">
<div class="timeline-content">
<div
:data-discussion-id="transformedDiscussion.discussion_id"
class="discussion js-discussion-container"
>
<div v-if="shouldRenderHeader" class="discussion-header note-wrapper">
<div class="timeline-icon">
<div :data-discussion-id="discussion.id" class="discussion js-discussion-container">
<div v-if="shouldRenderDiffs" class="discussion-header note-wrapper">
<div v-once class="timeline-icon">
<user-avatar-link
v-if="author"
:link-href="author.path"
Loading
Loading
@@ -330,35 +285,35 @@ Please check your network connection and try again.`;
</div>
<note-header
:author="author"
:created-at="transformedDiscussion.created_at"
:note-id="transformedDiscussion.id"
:created-at="initialDiscussion.created_at"
:note-id="initialDiscussion.id"
:include-toggle="true"
:expanded="discussion.expanded"
@toggleHandler="toggleDiscussionHandler"
>
<template v-if="transformedDiscussion.diff_discussion">
<template v-if="discussion.diff_discussion">
started a discussion on
<a :href="transformedDiscussion.discussion_path">
<template v-if="transformedDiscussion.active">
the diff
</template>
<template v-else>
an old version of the diff
</template>
<a :href="discussion.discussion_path">
<template v-if="discussion.active"
>the diff</template
>
<template v-else
>an old version of the diff</template
>
</a>
</template>
<template v-else-if="discussion.for_commit">
started a discussion on commit
<a :href="discussion.discussion_path"> {{ truncateSha(discussion.commit_id) }} </a>
</template>
<template v-else>
started a discussion
<a :href="discussion.discussion_path">{{ truncateSha(discussion.commit_id) }}</a>
</template>
<template v-else
>started a discussion</template
>
</note-header>
<note-edited-text
v-if="transformedDiscussion.resolved"
:edited-at="transformedDiscussion.resolved_at"
:edited-by="transformedDiscussion.resolved_by"
v-if="discussion.resolved"
:edited-at="discussion.resolved_at"
:edited-by="discussion.resolved_by"
:action-text="resolvedText"
class-name="discussion-headline-light js-discussion-headline"
/>
Loading
Loading
@@ -371,7 +326,11 @@ Please check your network connection and try again.`;
/>
</div>
<div v-if="shouldShowDiscussions" class="discussion-body">
<component :is="wrapperComponent" v-bind="wrapperComponentProps" :class="wrapperClass">
<component
:is="wrapperComponent"
v-bind="wrapperComponentProps"
class="card discussion-wrapper"
>
<div class="discussion-notes">
<ul class="notes">
<template v-if="shouldGroupReplies">
Loading
Loading
@@ -380,7 +339,7 @@ Please check your network connection and try again.`;
:note="componentData(initialDiscussion)"
@handleDeleteNote="deleteNoteHandler"
>
<slot slot="avatar-badge" name="avatar-badge"> </slot>
<slot slot="avatar-badge" name="avatar-badge"></slot>
</component>
<toggle-replies-widget
v-if="hasReplies"
Loading
Loading
@@ -406,7 +365,7 @@ Please check your network connection and try again.`;
:note="componentData(note)"
@handleDeleteNote="deleteNoteHandler"
>
<slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"> </slot>
<slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot>
</component>
</template>
</ul>
Loading
Loading
@@ -446,22 +405,19 @@ Please check your network connection and try again.`;
>
<div v-if="!discussionResolved" class="btn-group" role="group">
<a
v-tooltip
v-gl-tooltip
:href="discussion.resolve_with_issue_path"
:title="s__('MergeRequests|Resolve this discussion in a new issue')"
class="new-issue-for-discussion btn
btn-default discussion-create-issue-btn"
data-container="body"
class="new-issue-for-discussion btn btn-default discussion-create-issue-btn"
>
<icon name="issue-new" />
</a>
</div>
<div v-if="showJumpToNextDiscussion" class="btn-group" role="group">
<div v-if="hasUnresolvedDiscussions" class="btn-group" role="group">
<button
v-tooltip
v-gl-tooltip
class="btn btn-default discussion-next-btn"
title="Jump to next unresolved discussion"
data-container="body"
@click="jumpToNextDiscussion"
>
<icon name="comment-next" />
Loading
Loading
Loading
Loading
@@ -177,7 +177,7 @@ export default {
class="note timeline-entry note-wrapper"
>
<div class="timeline-entry-inner">
<div class="timeline-icon">
<div v-once class="timeline-icon">
<user-avatar-link
:link-href="author.path"
:img-src="author.avatar_url"
Loading
Loading
@@ -190,6 +190,7 @@ export default {
<div class="timeline-content">
<div class="note-header">
<note-header
v-once
:author="author"
:created-at="note.created_at"
:note-id="note.id"
Loading
Loading
Loading
Loading
@@ -22,6 +22,7 @@ export default {
commentForm,
placeholderNote,
placeholderSystemNote,
skeletonLoadingContainer,
},
props: {
noteableData: {
Loading
Loading
@@ -59,7 +60,6 @@ export default {
'isNotesFetched',
'discussions',
'getNotesDataByProp',
'discussionCount',
'isLoading',
'commentsDisabled',
]),
Loading
Loading
@@ -109,39 +109,22 @@ export default {
this.$nextTick(() => highlightCurrentUser(this.$el.querySelectorAll('.gfm-project_member')));
},
methods: {
...mapActions({
setLoadingState: 'setLoadingState',
fetchDiscussions: 'fetchDiscussions',
poll: 'poll',
actionToggleAward: 'toggleAward',
scrollToNoteIfNeeded: 'scrollToNoteIfNeeded',
setNotesData: 'setNotesData',
setNoteableData: 'setNoteableData',
setUserData: 'setUserData',
setLastFetchedAt: 'setLastFetchedAt',
setTargetNoteHash: 'setTargetNoteHash',
toggleDiscussion: 'toggleDiscussion',
setNotesFetchedState: 'setNotesFetchedState',
startTaskList: 'startTaskList',
}),
getComponentName(discussion) {
if (discussion.isSkeletonNote) {
return skeletonLoadingContainer;
}
if (discussion.isPlaceholderNote) {
if (discussion.placeholderType === constants.SYSTEM_NOTE) {
return placeholderSystemNote;
}
return placeholderNote;
} else if (discussion.individual_note) {
return discussion.notes[0].system ? systemNote : noteableNote;
}
return noteableDiscussion;
},
getComponentData(discussion) {
return discussion.individual_note ? { note: discussion.notes[0] } : { discussion };
},
...mapActions([
'setLoadingState',
'fetchDiscussions',
'poll',
'toggleAward',
'scrollToNoteIfNeeded',
'setNotesData',
'setNoteableData',
'setUserData',
'setLastFetchedAt',
'setTargetNoteHash',
'toggleDiscussion',
'setNotesFetchedState',
'expandDiscussion',
'startTaskList',
]),
fetchNotes() {
if (this.isFetching) return null;
 
Loading
Loading
@@ -181,31 +164,46 @@ export default {
const noteId = hash && hash.replace(/^note_/, '');
 
if (noteId) {
this.discussions.forEach(discussion => {
if (discussion.notes) {
discussion.notes.forEach(note => {
if (`${note.id}` === `${noteId}`) {
// FIXME: this modifies the store state without using a mutation/action
Object.assign(discussion, { expanded: true });
}
});
}
});
const discussion = this.discussions.find(d => d.notes.some(({ id }) => id === noteId));
if (discussion) {
this.expandDiscussion({ discussionId: discussion.id });
}
}
},
},
systemNote: constants.SYSTEM_NOTE,
};
</script>
 
<template>
<div v-show="shouldShow" id="notes">
<ul id="notes-list" class="notes main-notes-list timeline">
<component
:is="getComponentName(discussion)"
v-for="discussion in allDiscussions"
:key="discussion.id"
v-bind="getComponentData(discussion)"
/>
<template v-for="discussion in allDiscussions">
<skeleton-loading-container v-if="discussion.isSkeletonNote" :key="discussion.id" />
<template v-else-if="discussion.isPlaceholderNote">
<placeholder-system-note
v-if="discussion.placeholderType === $options.systemNote"
:key="discussion.id"
:note="discussion.notes[0]"
/>
<placeholder-note v-else :key="discussion.id" :note="discussion.notes[0]" />
</template>
<template v-else-if="discussion.individual_note">
<system-note
v-if="discussion.notes[0].system"
:key="discussion.id"
:note="discussion.notes[0]"
/>
<noteable-note v-else :key="discussion.id" :note="discussion.notes[0]" />
</template>
<noteable-discussion
v-else
:key="discussion.id"
:discussion="discussion"
:render-diff-file="true"
/>
</template>
</ul>
 
<comment-form
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment