diff options
Diffstat (limited to 'app/assets/javascripts/diffs')
22 files changed, 686 insertions, 1263 deletions
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 61946d345e3..e33b60f8ab5 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -14,7 +14,7 @@ import { } from '~/behaviors/shortcuts/keybindings'; import createFlash from '~/flash'; import { isSingleViewStyle } from '~/helpers/diffs_helper'; -import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils'; +import { parseBoolean } from '~/lib/utils/common_utils'; import { updateHistory } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; import PanelResizer from '~/vue_shared/components/panel_resizer.vue'; @@ -42,6 +42,7 @@ import { TRACKING_MULTIPLE_FILES_MODE, } from '../constants'; +import diffsEventHub from '../event_hub'; import { reviewStatuses } from '../utils/file_reviews'; import { diffsApp } from '../utils/performance'; import { fileByFile } from '../utils/preferences'; @@ -52,7 +53,9 @@ import DiffFile from './diff_file.vue'; import HiddenFilesWarning from './hidden_files_warning.vue'; import MergeConflictWarning from './merge_conflict_warning.vue'; import NoChanges from './no_changes.vue'; +import PreRenderer from './pre_renderer.vue'; import TreeList from './tree_list.vue'; +import VirtualScrollerScrollSync from './virtual_scroller_scroll_sync'; export default { name: 'DiffsApp', @@ -71,6 +74,8 @@ export default { GlSprintf, DynamicScroller, DynamicScrollerItem, + PreRenderer, + VirtualScrollerScrollSync, }, alerts: { ALERT_OVERFLOW_HIDDEN, @@ -166,6 +171,8 @@ export default { return { treeWidth, diffFilesLength: 0, + virtualScrollCurrentIndex: -1, + disableVirtualScroller: false, }; }, computed: { @@ -186,6 +193,7 @@ export default { 'showTreeList', 'isLoading', 'startVersion', + 'latestDiff', 'currentDiffFileId', 'isTreeLoaded', 'conflictResolutionPath', @@ -228,8 +236,8 @@ export default { isLimitedContainer() { return !this.renderFileTree && !this.isParallelView && !this.isFluidLayout; }, - isDiffHead() { - return parseBoolean(getParameterByName('diff_head')); + isFullChangeset() { + return this.startVersion === null && this.latestDiff; }, showFileByFileNavigation() { return this.diffFiles.length > 1 && this.viewDiffsFileByFile; @@ -252,7 +260,7 @@ export default { if (this.renderOverflowWarning) { visible = this.$options.alerts.ALERT_OVERFLOW_HIDDEN; - } else if (this.isDiffHead && this.hasConflicts) { + } else if (this.isFullChangeset && this.hasConflicts) { visible = this.$options.alerts.ALERT_MERGE_CONFLICT; } else if (this.whichCollapsedTypes.automatic && !this.viewDiffsFileByFile) { visible = this.$options.alerts.ALERT_COLLAPSED_FILES; @@ -323,6 +331,11 @@ export default { this.setHighlightedRow(id.split('diff-content').pop().slice(1)); } + if (window.gon?.features?.diffsVirtualScrolling) { + diffsEventHub.$on('scrollToFileHash', this.scrollVirtualScrollerToFileHash); + diffsEventHub.$on('scrollToIndex', this.scrollVirtualScrollerToIndex); + } + if (window.gon?.features?.diffSettingsUsageData) { if (this.renderTreeList) { api.trackRedisHllUserEvent(TRACKING_FILE_BROWSER_TREE); @@ -377,6 +390,11 @@ export default { diffsApp.deinstrument(); this.unsubscribeFromEvents(); this.removeEventListeners(); + + if (window.gon?.features?.diffsVirtualScrolling) { + diffsEventHub.$off('scrollToFileHash', this.scrollVirtualScrollerToFileHash); + diffsEventHub.$off('scrollToIndex', this.scrollVirtualScrollerToIndex); + } }, methods: { ...mapActions(['startTaskList']), @@ -458,7 +476,11 @@ export default { }, setDiscussions() { requestIdleCallback( - () => this.assignDiscussionsToDiff().then(this.$nextTick).then(this.startTaskList), + () => + this.assignDiscussionsToDiff() + .then(this.$nextTick) + .then(this.startTaskList) + .then(this.scrollVirtualScrollerToDiffNote), { timeout: 1000 }, ); }, @@ -483,12 +505,17 @@ export default { this.moveToNeighboringCommit({ direction: 'previous' }), ); } + + Mousetrap.bind(['ctrl+f', 'command+f'], () => { + this.disableVirtualScroller = true; + }); }, removeEventListeners() { Mousetrap.unbind(keysFor(MR_PREVIOUS_FILE_IN_DIFF)); Mousetrap.unbind(keysFor(MR_NEXT_FILE_IN_DIFF)); Mousetrap.unbind(keysFor(MR_COMMITS_NEXT_COMMIT)); Mousetrap.unbind(keysFor(MR_COMMITS_PREVIOUS_COMMIT)); + Mousetrap.unbind(['ctrl+f', 'command+f']); }, jumpToFile(step) { const targetIndex = this.currentDiffIndex + step; @@ -508,6 +535,36 @@ export default { return this.setShowTreeList({ showTreeList, saving: false }); }, + async scrollVirtualScrollerToFileHash(hash) { + const index = this.diffFiles.findIndex((f) => f.file_hash === hash); + + if (index !== -1) { + this.scrollVirtualScrollerToIndex(index); + } + }, + async scrollVirtualScrollerToIndex(index) { + this.virtualScrollCurrentIndex = index; + + await this.$nextTick(); + + this.virtualScrollCurrentIndex = -1; + }, + scrollVirtualScrollerToDiffNote() { + if (!window.gon?.features?.diffsVirtualScrolling) return; + + const id = window?.location?.hash; + + if (id.startsWith('#note_')) { + const noteId = id.replace('#note_', ''); + const discussion = this.$store.state.notes.discussions.find( + (d) => d.diff_file && d.notes.find((n) => n.id === noteId), + ); + + if (discussion) { + this.scrollVirtualScrollerToFileHash(discussion.diff_file.file_hash); + } + } + }, }, minTreeWidth: MIN_TREE_WIDTH, maxTreeWidth: MAX_TREE_WIDTH, @@ -571,7 +628,8 @@ export default { <div v-if="isBatchLoading" class="loading"><gl-loading-icon size="lg" /></div> <template v-else-if="renderDiffFiles"> <dynamic-scroller - v-if="isVirtualScrollingEnabled" + v-if="!disableVirtualScroller && isVirtualScrollingEnabled" + ref="virtualScroller" :items="diffs" :min-item-size="70" :buffer="1000" @@ -579,7 +637,7 @@ export default { page-mode > <template #default="{ item, index, active }"> - <dynamic-scroller-item :item="item" :active="active"> + <dynamic-scroller-item :item="item" :active="active" :class="{ active }"> <diff-file :file="item" :reviewed="fileReviews[item.id]" @@ -588,9 +646,29 @@ export default { :help-page-path="helpPagePath" :can-current-user-fork="canCurrentUserFork" :view-diffs-file-by-file="viewDiffsFileByFile" + :active="active" /> </dynamic-scroller-item> </template> + <template #before> + <pre-renderer :max-length="diffFilesLength"> + <template #default="{ item, index, active }"> + <dynamic-scroller-item :item="item" :active="active"> + <diff-file + :file="item" + :reviewed="fileReviews[item.id]" + :is-first-file="index === 0" + :is-last-file="index === diffFilesLength - 1" + :help-page-path="helpPagePath" + :can-current-user-fork="canCurrentUserFork" + :view-diffs-file-by-file="viewDiffsFileByFile" + pre-render + /> + </dynamic-scroller-item> + </template> + </pre-renderer> + <virtual-scroller-scroll-sync :index="virtualScrollCurrentIndex" /> + </template> </dynamic-scroller> <template v-else> <diff-file diff --git a/app/assets/javascripts/diffs/components/collapsed_files_warning.vue b/app/assets/javascripts/diffs/components/collapsed_files_warning.vue index 0cf1cdb17f8..240f102e600 100644 --- a/app/assets/javascripts/diffs/components/collapsed_files_warning.vue +++ b/app/assets/javascripts/diffs/components/collapsed_files_warning.vue @@ -1,5 +1,6 @@ <script> import { GlAlert, GlButton } from '@gitlab/ui'; +import { mapState } from 'vuex'; import { CENTERED_LIMITED_CONTAINER_CLASSES, EVT_EXPAND_ALL_FILES } from '../constants'; import eventHub from '../event_hub'; @@ -27,11 +28,15 @@ export default { }; }, computed: { + ...mapState('diffs', ['diffFiles']), containerClasses() { return { [CENTERED_LIMITED_CONTAINER_CLASSES]: this.limited, }; }, + shouldDisplay() { + return !this.isDismissed && this.diffFiles.length > 1; + }, }, methods: { @@ -48,7 +53,7 @@ export default { </script> <template> - <div v-if="!isDismissed" data-testid="root" :class="containerClasses" class="col-12"> + <div v-if="shouldDisplay" data-testid="root" :class="containerClasses" class="col-12"> <gl-alert :dismissible="true" :title="__('Some changes are not shown')" diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue index e2a1f7236c5..f098d20afd1 100644 --- a/app/assets/javascripts/diffs/components/compare_versions.vue +++ b/app/assets/javascripts/diffs/components/compare_versions.vue @@ -99,7 +99,7 @@ export default { v-gl-tooltip.hover variant="default" icon="file-tree" - class="gl-mr-3 js-toggle-tree-list" + class="gl-mr-3 js-toggle-tree-list btn-icon" :title="toggleFileBrowserTitle" :aria-label="toggleFileBrowserTitle" :selected="showTreeList" @@ -109,7 +109,7 @@ export default { {{ __('Viewing commit') }} <gl-link :href="commit.commit_url" class="monospace">{{ commit.short_id }}</gl-link> </div> - <div v-if="hasNeighborCommits" class="commit-nav-buttons ml-3"> + <div v-if="hasNeighborCommits" class="commit-nav-buttons"> <gl-button-group> <gl-button :href="previousCommitUrl" @@ -160,6 +160,15 @@ export default { /> </template> </gl-sprintf> + <gl-button + v-if="commit || startVersion" + :href="latestVersionPath" + variant="default" + class="js-latest-version" + :class="{ 'gl-ml-3': commit && !hasNeighborCommits }" + > + {{ __('Show latest version') }} + </gl-button> <div v-if="hasChanges" class="inline-parallel-buttons d-none d-md-flex ml-auto"> <diff-stats :diff-files-count-text="diffFilesCountText" @@ -167,14 +176,6 @@ export default { :removed-lines="removedLines" /> <gl-button - v-if="commit || startVersion" - :href="latestVersionPath" - variant="default" - class="gl-mr-3 js-latest-version" - > - {{ __('Show latest version') }} - </gl-button> - <gl-button v-show="whichCollapsedTypes.any" variant="default" class="gl-mr-3" diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue index cb74c7dc7cd..858d9e221ae 100644 --- a/app/assets/javascripts/diffs/components/diff_content.vue +++ b/app/assets/javascripts/diffs/components/diff_content.vue @@ -1,7 +1,7 @@ <script> import { GlLoadingIcon } from '@gitlab/ui'; import { mapActions, mapGetters, mapState } from 'vuex'; -import { mapInline, mapParallel } from 'ee_else_ce/diffs/components/diff_row_utils'; +import { mapParallel } from 'ee_else_ce/diffs/components/diff_row_utils'; import DiffFileDrafts from '~/batch_comments/components/diff_file_drafts.vue'; import draftCommentsMixin from '~/diffs/mixins/draft_comments'; import { diffViewerModes } from '~/ide/constants'; @@ -9,7 +9,6 @@ import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form'; import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; import NoPreviewViewer from '~/vue_shared/components/diff_viewer/viewers/no_preview.vue'; import NotDiffableViewer from '~/vue_shared/components/diff_viewer/viewers/not_diffable.vue'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import NoteForm from '../../notes/components/note_form.vue'; import eventHub from '../../notes/event_hub'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; @@ -18,14 +17,10 @@ import { getDiffMode } from '../store/utils'; import DiffDiscussions from './diff_discussions.vue'; import DiffView from './diff_view.vue'; import ImageDiffOverlay from './image_diff_overlay.vue'; -import InlineDiffView from './inline_diff_view.vue'; -import ParallelDiffView from './parallel_diff_view.vue'; export default { components: { GlLoadingIcon, - InlineDiffView, - ParallelDiffView, DiffView, DiffViewer, NoteForm, @@ -36,7 +31,7 @@ export default { userAvatarLink, DiffFileDrafts, }, - mixins: [diffLineNoteFormMixin, draftCommentsMixin, glFeatureFlagsMixin()], + mixins: [diffLineNoteFormMixin, draftCommentsMixin], props: { diffFile: { type: Object, @@ -52,7 +47,6 @@ export default { ...mapState('diffs', ['projectPath']), ...mapGetters('diffs', [ 'isInlineView', - 'isParallelView', 'getCommentFormForDiffFile', 'diffLines', 'fileLineCodequality', @@ -86,15 +80,8 @@ export default { return this.getUserData; }, mappedLines() { - if (this.glFeatures.unifiedDiffComponents) { - return this.diffLines(this.diffFile, true).map(mapParallel(this)) || []; - } - - // TODO: Everything below this line can be deleted when unifiedDiffComponents FF is removed - if (this.isInlineView) { - return this.diffFile.highlighted_diff_lines.map(mapInline(this)); - } - return this.diffLines(this.diffFile).map(mapParallel(this)); + // TODO: Do this data generation when we recieve a response to save a computed property being created + return this.diffLines(this.diffFile).map(mapParallel(this)) || []; }, }, updated() { @@ -126,7 +113,7 @@ export default { <template> <div class="diff-content"> <div class="diff-viewer"> - <template v-if="isTextFile && glFeatures.unifiedDiffComponents"> + <template v-if="isTextFile"> <diff-view :diff-file="diffFile" :diff-lines="mappedLines" @@ -135,21 +122,6 @@ export default { /> <gl-loading-icon v-if="diffFile.renderingLines" size="md" class="mt-3" /> </template> - <template v-else-if="isTextFile"> - <inline-diff-view - v-if="isInlineView" - :diff-file="diffFile" - :diff-lines="mappedLines" - :help-page-path="helpPagePath" - /> - <parallel-diff-view - v-else-if="isParallelView" - :diff-file="diffFile" - :diff-lines="mappedLines" - :help-page-path="helpPagePath" - /> - <gl-loading-icon v-if="diffFile.renderingLines" size="md" class="mt-3" /> - </template> <not-diffable-viewer v-else-if="notDiffable" /> <no-preview-viewer v-else-if="noPreview" /> <diff-viewer diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index ed8455f0c1c..dde5ea81e9a 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -2,6 +2,7 @@ import { GlButton, GlLoadingIcon, GlSafeHtmlDirective as SafeHtml, GlSprintf } from '@gitlab/ui'; import { escape } from 'lodash'; import { mapActions, mapGetters, mapState } from 'vuex'; +import { IdState } from 'vendor/vue-virtual-scroller'; import createFlash from '~/flash'; import { hasDiff } from '~/helpers/diffs_helper'; import { diffViewerErrors } from '~/ide/constants'; @@ -19,7 +20,7 @@ import { } from '../constants'; import eventHub from '../event_hub'; import { DIFF_FILE, GENERIC_ERROR } from '../i18n'; -import { collapsedType, isCollapsed, getShortShaFromFile } from '../utils/diff_file'; +import { collapsedType, getShortShaFromFile } from '../utils/diff_file'; import DiffContent from './diff_content.vue'; import DiffFileHeader from './diff_file_header.vue'; @@ -34,7 +35,7 @@ export default { directives: { SafeHtml, }, - mixins: [glFeatureFlagsMixin()], + mixins: [glFeatureFlagsMixin(), IdState({ idProp: (vm) => vm.file.file_hash })], props: { file: { type: Object, @@ -68,12 +69,22 @@ export default { type: Boolean, required: true, }, + active: { + type: Boolean, + required: false, + default: true, + }, + preRender: { + type: Boolean, + required: false, + default: false, + }, }, - data() { + idState() { return { isLoadingCollapsedDiff: false, forkMessageVisible: false, - isCollapsed: isCollapsed(this.file), + hasToggled: false, }; }, i18n: { @@ -91,7 +102,7 @@ export default { return getShortShaFromFile(this.file); }, showLoadingIcon() { - return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed); + return this.idState.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed); }, hasDiff() { return hasDiff(this.file); @@ -152,45 +163,39 @@ export default { codequalityDiffForFile() { return this.codequalityDiff?.files?.[this.file.file_path] || []; }, + isCollapsed() { + if (collapsedType(this.file) !== DIFF_FILE_MANUAL_COLLAPSE) { + return this.viewDiffsFileByFile ? false : this.file.viewer?.automaticallyCollapsed; + } + + return this.file.viewer?.manuallyCollapsed; + }, }, watch: { 'file.id': { handler: function fileIdHandler() { + if (this.preRender) return; + this.manageViewedEffects(); }, }, 'file.file_hash': { handler: function hashChangeWatch(newHash, oldHash) { - this.isCollapsed = isCollapsed(this.file); - - if (newHash && oldHash && !this.hasDiff) { + if (newHash && oldHash && !this.hasDiff && !this.preRender) { this.requestDiff(); } }, - immediate: true, - }, - 'file.viewer.automaticallyCollapsed': { - handler: function autoChangeWatch(automaticValue) { - if (collapsedType(this.file) !== DIFF_FILE_MANUAL_COLLAPSE) { - this.isCollapsed = this.viewDiffsFileByFile ? false : automaticValue; - } - }, - immediate: true, - }, - 'file.viewer.manuallyCollapsed': { - handler: function manualChangeWatch(manualValue) { - if (manualValue !== null) { - this.isCollapsed = manualValue; - } - }, - immediate: true, }, }, created() { + if (this.preRender) return; + notesEventHub.$on(`loadCollapsedDiff/${this.file.file_hash}`, this.requestDiff); eventHub.$on(EVT_EXPAND_ALL_FILES, this.expandAllListener); }, mounted() { + if (this.preRender) return; + if (this.hasDiff) { this.postRender(); } @@ -198,6 +203,8 @@ export default { this.manageViewedEffects(); }, beforeDestroy() { + if (this.preRender) return; + eventHub.$off(EVT_EXPAND_ALL_FILES, this.expandAllListener); }, methods: { @@ -208,8 +215,14 @@ export default { 'setFileCollapsedByUser', ]), manageViewedEffects() { - if (this.reviewed && !this.isCollapsed && this.showLocalFileReviews) { + if ( + !this.idState.hasToggled && + this.reviewed && + !this.isCollapsed && + this.showLocalFileReviews + ) { this.handleToggle(); + this.idState.hasToggled = true; } }, expandAllListener() { @@ -252,11 +265,11 @@ export default { } }, requestDiff() { - this.isLoadingCollapsedDiff = true; + this.idState.isLoadingCollapsedDiff = true; this.loadCollapsedDiff(this.file) .then(() => { - this.isLoadingCollapsedDiff = false; + this.idState.isLoadingCollapsedDiff = false; this.setRenderIt(this.file); }) .then(() => { @@ -269,17 +282,17 @@ export default { ); }) .catch(() => { - this.isLoadingCollapsedDiff = false; + this.idState.isLoadingCollapsedDiff = false; createFlash({ message: this.$options.i18n.genericError, }); }); }, showForkMessage() { - this.forkMessageVisible = true; + this.idState.forkMessageVisible = true; }, hideForkMessage() { - this.forkMessageVisible = false; + this.idState.forkMessageVisible = false; }, }, }; @@ -287,7 +300,7 @@ export default { <template> <div - :id="file.file_hash" + :id="!preRender && active && file.file_hash" :class="{ 'is-active': currentDiffFileId === file.file_hash, 'comments-disabled': Boolean(file.brokenSymlink), @@ -313,7 +326,10 @@ export default { @showForkMessage="showForkMessage" /> - <div v-if="forkMessageVisible" class="js-file-fork-suggestion-section file-fork-suggestion"> + <div + v-if="idState.forkMessageVisible" + class="js-file-fork-suggestion-section file-fork-suggestion" + > <span v-safe-html="forkMessage" class="file-fork-suggestion-note"></span> <a :href="file.fork_path" @@ -330,12 +346,13 @@ export default { </div> <template v-else> <div - :id="`diff-content-${file.file_hash}`" + :id="!preRender && active && `diff-content-${file.file_hash}`" :class="hasBodyClasses.contentByHash" data-testid="content-area" > <gl-loading-icon v-if="showLoadingIcon" + size="sm" class="diff-content loading gl-my-0 gl-pt-3" data-testid="loader-icon" /> @@ -357,7 +374,7 @@ export default { </div> <template v-else> <div - v-show="showWarning" + v-if="showWarning" class="collapsed-file-warning gl-p-7 gl-bg-orange-50 gl-text-center gl-rounded-bottom-left-base gl-rounded-bottom-right-base" > <p class="gl-mb-5"> @@ -373,7 +390,7 @@ export default { </gl-button> </div> <diff-content - v-show="showContent" + v-if="showContent" :class="hasBodyClasses.content" :diff-file="file" :help-page-path="helpPagePath" diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index 45c7fe35f03..667b8745f7b 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -13,6 +13,7 @@ import { } from '@gitlab/ui'; import { escape } from 'lodash'; import { mapActions, mapGetters, mapState } from 'vuex'; +import { IdState } from 'vendor/vue-virtual-scroller'; import { diffViewerModes } from '~/ide/constants'; import { scrollToElement } from '~/lib/utils/common_utils'; import { truncateSha } from '~/lib/utils/text_utility'; @@ -41,13 +42,12 @@ export default { GlDropdownDivider, GlFormCheckbox, GlLoadingIcon, - CodeQualityBadge: () => import('ee_component/diffs/components/code_quality_badge.vue'), }, directives: { GlTooltip: GlTooltipDirective, SafeHtml: GlSafeHtmlDirective, }, - mixins: [glFeatureFlagsMixin()], + mixins: [glFeatureFlagsMixin(), IdState({ idProp: (vm) => vm.diffFile.file_hash })], i18n: { ...DIFF_FILE_HEADER, compareButtonLabel: s__('Compare submodule commit revisions'), @@ -102,7 +102,7 @@ export default { default: () => [], }, }, - data() { + idState() { return { moreActionsShown: false, }; @@ -202,8 +202,18 @@ export default { externalUrlLabel() { return sprintf(__('View on %{url}'), { url: this.diffFile.formatted_external_url }); }, - showCodequalityBadge() { - return this.codequalityDiff?.length > 0 && !this.glFeatures.codequalityMrDiffAnnotations; + }, + watch: { + 'idState.moreActionsShown': { + handler(val) { + const el = this.$el.closest('.vue-recycle-scroller__item-view'); + + if (this.glFeatures.diffsVirtualScrolling && el) { + // We can't add a style with Vue because of the way the virtual + // scroller library renders the diff files + el.style.zIndex = val ? '1' : null; + } + }, }, }, methods: { @@ -239,7 +249,7 @@ export default { } }, setMoreActionsShown(val) { - this.moreActionsShown = val; + this.idState.moreActionsShown = val; }, toggleReview(newReviewedStatus) { const autoCollapsed = @@ -268,7 +278,7 @@ export default { <template> <div ref="header" - :class="{ 'gl-z-dropdown-menu!': moreActionsShown }" + :class="{ 'gl-z-dropdown-menu!': idState.moreActionsShown }" class="js-file-title file-title file-title-flex-parent" data-qa-selector="file_title_container" :data-qa-file-name="filePath" @@ -292,7 +302,7 @@ export default { > <file-icon :file-name="filePath" - :size="18" + :size="16" aria-hidden="true" css-classes="gl-mr-2" :submodule="diffFile.submodule" @@ -336,13 +346,6 @@ export default { data-track-property="diff_copy_file" /> - <code-quality-badge - v-if="showCodequalityBadge" - :file-name="filePath" - :codequality-diff="codequalityDiff" - class="gl-mr-2" - /> - <small v-if="isModeChanged" ref="fileMode" class="mr-1"> {{ diffFile.a_mode }} → {{ diffFile.b_mode }} </small> @@ -453,7 +456,7 @@ export default { :disabled="diffFile.isLoadingFullFile" @click="toggleFullDiff(diffFile.file_path)" > - <gl-loading-icon v-if="diffFile.isLoadingFullFile" inline /> + <gl-loading-icon v-if="diffFile.isLoadingFullFile" size="sm" inline /> {{ expandDiffToFullFileTitle }} </gl-dropdown-item> </template> diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue index c907b5dffaf..c445989f143 100644 --- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue +++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue @@ -106,10 +106,7 @@ export default { }; const getDiffLines = () => { if (this.diffViewType === PARALLEL_DIFF_VIEW_TYPE) { - return this.diffLines(this.diffFile, this.glFeatures.unifiedDiffComponents).reduce( - combineSides, - [], - ); + return this.diffLines(this.diffFile).reduce(combineSides, []); } return this.diffFile[INLINE_DIFF_LINES_KEY]; diff --git a/app/assets/javascripts/diffs/components/diff_row.vue b/app/assets/javascripts/diffs/components/diff_row.vue index 37dd7941b2e..c310bd9f31a 100644 --- a/app/assets/javascripts/diffs/components/diff_row.vue +++ b/app/assets/javascripts/diffs/components/diff_row.vue @@ -1,13 +1,9 @@ <script> /* eslint-disable vue/no-v-html */ -import { GlTooltipDirective } from '@gitlab/ui'; -import { mapActions, mapGetters, mapState } from 'vuex'; -import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { memoize } from 'lodash'; +import { isLoggedIn } from '~/lib/utils/common_utils'; import { - CONTEXT_LINE_CLASS_NAME, PARALLEL_DIFF_VIEW_TYPE, - CONFLICT_MARKER_OUR, CONFLICT_MARKER_THEIR, CONFLICT_OUR, CONFLICT_THEIR, @@ -22,15 +18,8 @@ import DiffGutterAvatars from './diff_gutter_avatars.vue'; import * as utils from './diff_row_utils'; export default { - components: { - DiffGutterAvatars, - CodeQualityGutterIcon: () => - import('ee_component/diffs/components/code_quality_gutter_icon.vue'), - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - mixins: [glFeatureFlagsMixin()], + DiffGutterAvatars, + CodeQualityGutterIcon: () => import('ee_component/diffs/components/code_quality_gutter_icon.vue'), props: { fileHash: { type: String, @@ -58,148 +47,109 @@ export default { type: Number, required: true, }, + isHighlighted: { + type: Boolean, + required: true, + }, + fileLineCoverage: { + type: Function, + required: true, + }, }, - data() { - return { - dragging: false, - }; - }, - computed: { - ...mapGetters('diffs', ['fileLineCoverage']), - ...mapGetters(['isLoggedIn']), - ...mapState({ - isHighlighted(state) { - const line = this.line.left?.line_code ? this.line.left : this.line.right; - return utils.isHighlighted(state, line, false); - }, - }), - classNameMap() { + classNameMap: memoize( + (props) => { return { - [CONTEXT_LINE_CLASS_NAME]: this.line.isContextLineLeft, - [PARALLEL_DIFF_VIEW_TYPE]: !this.inline, - commented: this.isCommented, + [PARALLEL_DIFF_VIEW_TYPE]: !props.inline, + commented: props.isCommented, }; }, - parallelViewLeftLineType() { - return utils.parallelViewLeftLineType(this.line, this.isHighlighted || this.isCommented); + (props) => [!props.inline, props.isCommented].join(':'), + ), + parallelViewLeftLineType: memoize( + (props) => { + return utils.parallelViewLeftLineType(props.line, props.isHighlighted || props.isCommented); }, - coverageStateLeft() { - if (!this.inline || !this.line.left) return {}; - return this.fileLineCoverage(this.filePath, this.line.left.new_line); + (props) => + [props.line.left?.type, props.line.right?.type, props.isHighlighted, props.isCommented].join( + ':', + ), + ), + coverageStateLeft: memoize( + (props) => { + if (!props.inline || !props.line.left) return {}; + return props.fileLineCoverage(props.filePath, props.line.left.new_line); }, - coverageStateRight() { - if (!this.line.right) return {}; - return this.fileLineCoverage(this.filePath, this.line.right.new_line); + (props) => [props.inline, props.filePath, props.line.left?.new_line].join(':'), + ), + coverageStateRight: memoize( + (props) => { + if (!props.line.right) return {}; + return props.fileLineCoverage(props.filePath, props.line.right.new_line); }, - showCodequalityLeft() { - return ( - this.glFeatures.codequalityMrDiffAnnotations && - this.inline && - this.line.left?.codequality?.length > 0 - ); + (props) => [props.line.right?.new_line, props.filePath].join(':'), + ), + showCodequalityLeft: memoize( + (props) => { + return props.inline && props.line.left?.codequality?.length > 0; }, - showCodequalityRight() { - return ( - this.glFeatures.codequalityMrDiffAnnotations && - !this.inline && - this.line.right?.codequality?.length > 0 - ); + (props) => [props.inline, props.line.left?.codequality?.length].join(':'), + ), + showCodequalityRight: memoize( + (props) => { + return !props.inline && props.line.right?.codequality?.length > 0; }, - classNameMapCellLeft() { + (props) => [props.inline, props.line.right?.codequality?.length].join(':'), + ), + classNameMapCellLeft: memoize( + (props) => { return utils.classNameMapCell({ - line: this.line.left, - hll: this.isHighlighted || this.isCommented, - isLoggedIn: this.isLoggedIn, + line: props.line.left, + hll: props.isHighlighted || props.isCommented, }); }, - classNameMapCellRight() { + (props) => [props.line.left.type, props.isHighlighted, props.isCommented].join(':'), + ), + classNameMapCellRight: memoize( + (props) => { return utils.classNameMapCell({ - line: this.line.right, - hll: this.isHighlighted || this.isCommented, - isLoggedIn: this.isLoggedIn, + line: props.line.right, + hll: props.isHighlighted || props.isCommented, }); }, - addCommentTooltipLeft() { - return utils.addCommentTooltip(this.line.left, this.glFeatures.dragCommentSelection); - }, - addCommentTooltipRight() { - return utils.addCommentTooltip(this.line.right, this.glFeatures.dragCommentSelection); + (props) => [props.line.right.type, props.isHighlighted, props.isCommented].join(':'), + ), + shouldRenderCommentButton: memoize( + (props) => { + return isLoggedIn() && !props.line.isContextLineLeft && !props.line.isMetaLineLeft; }, - emptyCellRightClassMap() { - return { conflict_their: this.line.left?.type === CONFLICT_OUR }; - }, - emptyCellLeftClassMap() { - return { conflict_our: this.line.right?.type === CONFLICT_THEIR }; - }, - shouldRenderCommentButton() { - return this.isLoggedIn && !this.line.isContextLineLeft && !this.line.isMetaLineLeft; - }, - isLeftConflictMarker() { - return [CONFLICT_MARKER_OUR, CONFLICT_MARKER_THEIR].includes(this.line.left?.type); - }, - interopLeftAttributes() { - if (this.inline) { - return getInteropInlineAttributes(this.line.left); - } + (props) => [props.line.isContextLineLeft, props.line.isMetaLineLeft].join(':'), + ), + interopLeftAttributes(props) { + if (props.inline) { + return getInteropInlineAttributes(props.line.left); + } - return getInteropOldSideAttributes(this.line.left); - }, - interopRightAttributes() { - return getInteropNewSideAttributes(this.line.right); - }, + return getInteropOldSideAttributes(props.line.left); }, - mounted() { - this.scrollToLineIfNeededParallel(this.line); + interopRightAttributes(props) { + return getInteropNewSideAttributes(props.line.right); }, - methods: { - ...mapActions('diffs', [ - 'scrollToLineIfNeededParallel', - 'showCommentForm', - 'setHighlightedRow', - 'toggleLineDiscussions', - ]), - // Prevent text selecting on both sides of parallel diff view - // Backport of the same code from legacy diff notes. - handleParallelLineMouseDown(e) { - const line = e.currentTarget; - const table = line.closest('.diff-table'); - - table.classList.remove('left-side-selected', 'right-side-selected'); - const [lineClass] = ['left-side', 'right-side'].filter((name) => - line.classList.contains(name), - ); - - if (lineClass) { - table.classList.add(`${lineClass}-selected`); - } - }, - handleCommentButton(line) { - this.showCommentForm({ lineCode: line.line_code, fileHash: this.fileHash }); - }, - conflictText(line) { - return line.type === CONFLICT_MARKER_THEIR - ? this.$options.THEIR_CHANGES - : this.$options.OUR_CHANGES; - }, - onDragEnd() { - this.dragging = false; - if (!this.glFeatures.dragCommentSelection) return; - - this.$emit('stopdragging'); + conflictText: memoize( + (line) => { + return line.type === CONFLICT_MARKER_THEIR ? 'HEAD//our changes' : 'origin//their changes'; }, - onDragEnter(line, index) { - if (!this.glFeatures.dragCommentSelection) return; + (line) => line.type, + ), + lineContent: memoize( + (line) => { + if (line.isConflictMarker) { + return line.type === CONFLICT_MARKER_THEIR ? 'HEAD//our changes' : 'origin//their changes'; + } - this.$emit('enterdragging', { ...line, index }); - }, - onDragStart(line) { - this.$root.$emit(BV_HIDE_TOOLTIP); - this.dragging = true; - this.$emit('startdragging', line); + return line.rich_text; }, - }, - OUR_CHANGES: 'HEAD//our changes', - THEIR_CHANGES: 'origin//their changes', + (line) => line.line_code, + ), CONFLICT_MARKER, CONFLICT_MARKER_THEIR, CONFLICT_OUR, @@ -207,250 +157,256 @@ export default { }; </script> -<template> - <div :class="classNameMap" class="diff-grid-row diff-tr line_holder"> +<!-- eslint-disable-next-line vue/no-deprecated-functional-template --> +<template functional> + <div :class="$options.classNameMap(props)" class="diff-grid-row diff-tr line_holder"> <div + :id="props.line.left && props.line.left.line_code" data-testid="left-side" class="diff-grid-left left-side" - v-bind="interopLeftAttributes" + v-bind="$options.interopLeftAttributes(props)" @dragover.prevent - @dragenter="onDragEnter(line.left, index)" - @dragend="onDragEnd" + @dragenter="listeners.enterdragging({ ...props.line.left, index: props.index })" + @dragend="listeners.stopdragging" > - <template v-if="line.left && line.left.type !== $options.CONFLICT_MARKER"> + <template v-if="props.line.left && props.line.left.type !== $options.CONFLICT_MARKER"> <div - :class="classNameMapCellLeft" + :class="$options.classNameMapCellLeft(props)" data-testid="left-line-number" class="diff-td diff-line-num" data-qa-selector="new_diff_line_link" > - <template v-if="!isLeftConflictMarker"> - <span - v-if="shouldRenderCommentButton && !line.hasDiscussionsLeft" - v-gl-tooltip - class="add-diff-note tooltip-wrapper" - :title="addCommentTooltipLeft" - > - <div - data-testid="left-comment-button" - role="button" - tabindex="0" - :draggable="!line.left.commentsDisabled && glFeatures.dragCommentSelection" - type="button" - class="add-diff-note unified-diff-components-diff-note-button note-button js-add-diff-note-button" - data-qa-selector="diff_comment_button" - :class="{ 'gl-cursor-grab': dragging }" - :disabled="line.left.commentsDisabled" - :aria-disabled="line.left.commentsDisabled" - @click="!line.left.commentsDisabled && handleCommentButton(line.left)" - @keydown.enter="!line.left.commentsDisabled && handleCommentButton(line.left)" - @keydown.space="!line.left.commentsDisabled && handleCommentButton(line.left)" - @dragstart="!line.left.commentsDisabled && onDragStart({ ...line.left, index })" - ></div> - </span> - </template> + <span + v-if=" + !props.line.left.isConflictMarker && + $options.shouldRenderCommentButton(props) && + !props.line.hasDiscussionsLeft + " + class="add-diff-note tooltip-wrapper has-tooltip" + :title="props.line.left.addCommentTooltip" + > + <div + data-testid="left-comment-button" + role="button" + tabindex="0" + :draggable="!props.line.left.commentsDisabled" + type="button" + class="add-diff-note unified-diff-components-diff-note-button note-button js-add-diff-note-button" + data-qa-selector="diff_comment_button" + :disabled="props.line.left.commentsDisabled" + :aria-disabled="props.line.left.commentsDisabled" + @click=" + !props.line.left.commentsDisabled && + listeners.showCommentForm(props.line.left.line_code) + " + @keydown.enter=" + !props.line.left.commentsDisabled && + listeners.showCommentForm(props.line.left.line_code) + " + @keydown.space=" + !props.line.left.commentsDisabled && + listeners.showCommentForm(props.line.left.line_code) + " + @dragstart=" + !props.line.left.commentsDisabled && + listeners.startdragging({ + event: $event, + line: { ...props.line.left, index: props.index }, + }) + " + ></div> + </span> <a - v-if="line.left.old_line && line.left.type !== $options.CONFLICT_THEIR" - :data-linenumber="line.left.old_line" - :href="line.lineHrefOld" - @click="setHighlightedRow(line.lineCode)" + v-if="props.line.left.old_line && props.line.left.type !== $options.CONFLICT_THEIR" + :data-linenumber="props.line.left.old_line" + :href="props.line.lineHrefOld" + @click="listeners.setHighlightedRow(props.line.lineCode)" > </a> - <diff-gutter-avatars - v-if="line.hasDiscussionsLeft" - :discussions="line.left.discussions" - :discussions-expanded="line.left.discussionsExpanded" + <component + :is="$options.DiffGutterAvatars" + v-if="props.line.hasDiscussionsLeft" + :discussions="props.line.left.discussions" + :discussions-expanded="props.line.left.discussionsExpanded" data-testid="left-discussions" @toggleLineDiscussions=" - toggleLineDiscussions({ - lineCode: line.left.line_code, - fileHash, - expanded: !line.left.discussionsExpanded, + listeners.toggleLineDiscussions({ + lineCode: props.line.left.line_code, + expanded: !props.line.left.discussionsExpanded, }) " /> </div> - <div v-if="inline" :class="classNameMapCellLeft" class="diff-td diff-line-num"> + <div + v-if="props.inline" + :class="$options.classNameMapCellLeft(props)" + class="diff-td diff-line-num" + > <a - v-if="line.left.new_line && line.left.type !== $options.CONFLICT_OUR" - :data-linenumber="line.left.new_line" - :href="line.lineHrefOld" - @click="setHighlightedRow(line.lineCode)" + v-if="props.line.left.new_line && props.line.left.type !== $options.CONFLICT_OUR" + :data-linenumber="props.line.left.new_line" + :href="props.line.lineHrefOld" + @click="listeners.setHighlightedRow(props.line.lineCode)" > </a> </div> <div - v-gl-tooltip.hover - :title="coverageStateLeft.text" - :class="[...parallelViewLeftLineType, coverageStateLeft.class]" - class="diff-td line-coverage left-side" + :title="$options.coverageStateLeft(props).text" + :class="[ + $options.parallelViewLeftLineType(props), + $options.coverageStateLeft(props).class, + ]" + class="diff-td line-coverage left-side has-tooltip" ></div> - <div class="diff-td line-codequality left-side" :class="[...parallelViewLeftLineType]"> - <code-quality-gutter-icon - v-if="showCodequalityLeft" - :file-path="filePath" - :codequality="line.left.codequality" + <div + class="diff-td line-codequality left-side" + :class="$options.parallelViewLeftLineType(props)" + > + <component + :is="$options.CodeQualityGutterIcon" + v-if="$options.showCodequalityLeft(props)" + :codequality="props.line.left.codequality" + :file-path="props.filePath" /> </div> <div - :id="line.left.line_code" - :key="line.left.line_code" - :class="[parallelViewLeftLineType, { parallel: !inline }]" + :key="props.line.left.line_code" + :class="[ + $options.parallelViewLeftLineType(props), + { parallel: !props.inline, 'gl-font-weight-bold': props.line.left.isConflictMarker }, + ]" class="diff-td line_content with-coverage left-side" data-testid="left-content" - @mousedown="handleParallelLineMouseDown" - > - <strong v-if="isLeftConflictMarker">{{ conflictText(line.left) }}</strong> - <span v-else v-html="line.left.rich_text"></span> - </div> + v-html="$options.lineContent(props.line.left)" + ></div> </template> - <template v-else-if="!inline || (line.left && line.left.type === $options.CONFLICT_MARKER)"> - <div - data-testid="left-empty-cell" - class="diff-td diff-line-num old_line empty-cell" - :class="emptyCellLeftClassMap" - > + <template + v-else-if=" + !props.inline || (props.line.left && props.line.left.type === $options.CONFLICT_MARKER) + " + > + <div data-testid="left-empty-cell" class="diff-td diff-line-num old_line empty-cell"> </div> - <div - v-if="inline" - class="diff-td diff-line-num old_line empty-cell" - :class="emptyCellLeftClassMap" - ></div> - <div - class="diff-td line-coverage left-side empty-cell" - :class="emptyCellLeftClassMap" - ></div> - <div - v-if="inline" - class="diff-td line-codequality left-side empty-cell" - :class="emptyCellLeftClassMap" - ></div> + <div v-if="props.inline" class="diff-td diff-line-num old_line empty-cell"></div> + <div class="diff-td line-coverage left-side empty-cell"></div> + <div v-if="props.inline" class="diff-td line-codequality left-side empty-cell"></div> <div class="diff-td line_content with-coverage left-side empty-cell" - :class="[emptyCellLeftClassMap, { parallel: !inline }]" + :class="[{ parallel: !props.inline }]" ></div> </template> </div> <div - v-if="!inline" + v-if="!props.inline" + :id="props.line.right && props.line.right.line_code" data-testid="right-side" class="diff-grid-right right-side" - v-bind="interopRightAttributes" + v-bind="$options.interopRightAttributes(props)" @dragover.prevent - @dragenter="onDragEnter(line.right, index)" - @dragend="onDragEnd" + @dragenter="listeners.enterdragging({ ...props.line.right, index: props.index })" + @dragend="listeners.stopdragging" > - <template v-if="line.right"> - <div :class="classNameMapCellRight" class="diff-td diff-line-num new_line"> - <template v-if="line.right.type !== $options.CONFLICT_MARKER_THEIR"> + <template v-if="props.line.right"> + <div :class="$options.classNameMapCellRight(props)" class="diff-td diff-line-num new_line"> + <template v-if="props.line.right.type !== $options.CONFLICT_MARKER_THEIR"> <span - v-if="shouldRenderCommentButton && !line.hasDiscussionsRight" - v-gl-tooltip - class="add-diff-note tooltip-wrapper" - :title="addCommentTooltipRight" + v-if="$options.shouldRenderCommentButton(props) && !props.line.hasDiscussionsRight" + class="add-diff-note tooltip-wrapper has-tooltip" + :title="props.line.right.addCommentTooltip" > <div data-testid="right-comment-button" role="button" tabindex="0" - :draggable="!line.right.commentsDisabled && glFeatures.dragCommentSelection" + :draggable="!props.line.right.commentsDisabled" type="button" class="add-diff-note unified-diff-components-diff-note-button note-button js-add-diff-note-button" - :class="{ 'gl-cursor-grab': dragging }" - :disabled="line.right.commentsDisabled" - :aria-disabled="line.right.commentsDisabled" - @click="!line.right.commentsDisabled && handleCommentButton(line.right)" - @keydown.enter="!line.right.commentsDisabled && handleCommentButton(line.right)" - @keydown.space="!line.right.commentsDisabled && handleCommentButton(line.right)" - @dragstart="!line.right.commentsDisabled && onDragStart({ ...line.right, index })" + :disabled="props.line.right.commentsDisabled" + :aria-disabled="props.line.right.commentsDisabled" + @click=" + !props.line.right.commentsDisabled && + listeners.showCommentForm(props.line.right.line_code) + " + @keydown.enter=" + !props.line.right.commentsDisabled && + listeners.showCommentForm(props.line.right.line_code) + " + @keydown.space=" + !props.line.right.commentsDisabled && + listeners.showCommentForm(props.line.right.line_code) + " + @dragstart=" + !props.line.right.commentsDisabled && + listeners.startdragging({ + event: $event, + line: { ...props.line.right, index: props.index }, + }) + " ></div> </span> </template> <a - v-if="line.right.new_line" - :data-linenumber="line.right.new_line" - :href="line.lineHrefNew" - @click="setHighlightedRow(line.lineCode)" + v-if="props.line.right.new_line" + :data-linenumber="props.line.right.new_line" + :href="props.line.lineHrefNew" + @click="listeners.setHighlightedRow(props.line.lineCode)" > </a> - <diff-gutter-avatars - v-if="line.hasDiscussionsRight" - :discussions="line.right.discussions" - :discussions-expanded="line.right.discussionsExpanded" + <component + :is="$options.DiffGutterAvatars" + v-if="props.line.hasDiscussionsRight" + :discussions="props.line.right.discussions" + :discussions-expanded="props.line.right.discussionsExpanded" data-testid="right-discussions" @toggleLineDiscussions=" - toggleLineDiscussions({ - lineCode: line.right.line_code, - fileHash, - expanded: !line.right.discussionsExpanded, + listeners.toggleLineDiscussions({ + lineCode: props.line.right.line_code, + expanded: !props.line.right.discussionsExpanded, }) " /> </div> <div - v-gl-tooltip.hover - :title="coverageStateRight.text" + :title="$options.coverageStateRight(props).text" :class="[ - line.right.type, - coverageStateRight.class, - { hll: isHighlighted, hll: isCommented }, + props.line.right.type, + $options.coverageStateRight(props).class, + { hll: props.isHighlighted, hll: props.isCommented }, ]" - class="diff-td line-coverage right-side" + class="diff-td line-coverage right-side has-tooltip" ></div> <div class="diff-td line-codequality right-side" - :class="[line.right.type, { hll: isHighlighted, hll: isCommented }]" + :class="[props.line.right.type, { hll: props.isHighlighted, hll: props.isCommented }]" > - <code-quality-gutter-icon - v-if="showCodequalityRight" - :file-path="filePath" - :codequality="line.right.codequality" + <component + :is="$options.CodeQualityGutterIcon" + v-if="$options.showCodequalityRight(props)" + :codequality="props.line.right.codequality" + :file-path="props.filePath" + data-testid="codeQualityIcon" /> </div> <div - :id="line.right.line_code" - :key="line.right.rich_text" + :key="props.line.right.rich_text" :class="[ - line.right.type, + props.line.right.type, { - hll: isHighlighted, - hll: isCommented, - parallel: !inline, + hll: props.isHighlighted, + hll: props.isCommented, + 'gl-font-weight-bold': props.line.right.type === $options.CONFLICT_MARKER_THEIR, }, ]" - class="diff-td line_content with-coverage right-side" - @mousedown="handleParallelLineMouseDown" - > - <strong v-if="line.right.type === $options.CONFLICT_MARKER_THEIR">{{ - conflictText(line.right) - }}</strong> - <span v-else v-html="line.right.rich_text"></span> - </div> + class="diff-td line_content with-coverage right-side parallel" + v-html="$options.lineContent(props.line.right)" + ></div> </template> <template v-else> - <div - data-testid="right-empty-cell" - class="diff-td diff-line-num old_line empty-cell" - :class="emptyCellRightClassMap" - ></div> - <div - v-if="inline" - class="diff-td diff-line-num old_line empty-cell" - :class="emptyCellRightClassMap" - ></div> - <div - class="diff-td line-coverage right-side empty-cell" - :class="emptyCellRightClassMap" - ></div> - <div - class="diff-td line-codequality right-side empty-cell" - :class="emptyCellRightClassMap" - ></div> - <div - class="diff-td line_content with-coverage right-side empty-cell" - :class="[emptyCellRightClassMap, { parallel: !inline }]" - ></div> + <div data-testid="right-empty-cell" class="diff-td diff-line-num old_line empty-cell"></div> + <div class="diff-td line-coverage right-side empty-cell"></div> + <div class="diff-td line-codequality right-side empty-cell"></div> + <div class="diff-td line_content with-coverage right-side empty-cell parallel"></div> </template> </div> </div> diff --git a/app/assets/javascripts/diffs/components/diff_row_utils.js b/app/assets/javascripts/diffs/components/diff_row_utils.js index cd45474afcd..99999445c43 100644 --- a/app/assets/javascripts/diffs/components/diff_row_utils.js +++ b/app/assets/javascripts/diffs/components/diff_row_utils.js @@ -6,13 +6,17 @@ import { OLD_NO_NEW_LINE_TYPE, NEW_NO_NEW_LINE_TYPE, EMPTY_CELL_TYPE, + CONFLICT_MARKER_OUR, + CONFLICT_MARKER_THEIR, + CONFLICT_THEIR, + CONFLICT_OUR, } from '../constants'; -export const isHighlighted = (state, line, isCommented) => { +export const isHighlighted = (highlightedRow, line, isCommented) => { if (isCommented) return true; const lineCode = line?.line_code; - return lineCode ? lineCode === state.diffs.highlightedRow : false; + return lineCode ? lineCode === highlightedRow : false; }; export const isContextLine = (type) => type === CONTEXT_LINE_TYPE; @@ -50,13 +54,11 @@ export const classNameMapCell = ({ line, hll, isLoggedIn, isHover }) => { ]; }; -export const addCommentTooltip = (line, dragCommentSelectionEnabled = false) => { +export const addCommentTooltip = (line) => { let tooltip; if (!line) return tooltip; - tooltip = dragCommentSelectionEnabled - ? __('Add a comment to this line or drag for multiple lines') - : __('Add a comment to this line'); + tooltip = __('Add a comment to this line or drag for multiple lines'); const brokenSymlinks = line.commentsDisabled; if (brokenSymlinks) { @@ -107,6 +109,10 @@ export const mapParallel = (content) => (line) => { hasDraft: content.hasParallelDraftLeft(content.diffFile.file_hash, line), lineDraft: content.draftForLine(content.diffFile.file_hash, line, 'left'), hasCommentForm: left.hasForm, + isConflictMarker: + line.left.type === CONFLICT_MARKER_OUR || line.left.type === CONFLICT_MARKER_THEIR, + emptyCellClassMap: { conflict_our: line.right?.type === CONFLICT_THEIR }, + addCommentTooltip: addCommentTooltip(line.left), }; } if (right) { @@ -116,6 +122,8 @@ export const mapParallel = (content) => (line) => { hasDraft: content.hasParallelDraftRight(content.diffFile.file_hash, line), lineDraft: content.draftForLine(content.diffFile.file_hash, line, 'right'), hasCommentForm: Boolean(right.hasForm && right.type), + emptyCellClassMap: { conflict_their: line.left?.type === CONFLICT_OUR }, + addCommentTooltip: addCommentTooltip(line.right), }; } @@ -139,24 +147,3 @@ export const mapParallel = (content) => (line) => { commentRowClasses: hasDiscussions(left) || hasDiscussions(right) ? '' : 'js-temp-notes-holder', }; }; - -// TODO: Delete this function when unifiedDiffComponents FF is removed -export const mapInline = (content) => (line) => { - // Discussions/Comments - const renderCommentRow = line.hasForm || (line.discussions?.length && line.discussionsExpanded); - - return { - ...line, - renderDiscussion: Boolean(line.discussions?.length), - isMatchLine: isMatchLine(line.type), - commentRowClasses: line.discussions?.length ? '' : 'js-temp-notes-holder', - renderCommentRow, - hasDraft: content.shouldRenderDraftRow(content.diffFile.file_hash, line), - hasCommentForm: line.hasForm, - isMetaLine: isMetaLine(line.type), - isContextLine: isContextLine(line.type), - hasDiscussions: hasDiscussions(line), - lineHref: lineHref(line), - lineCode: lineCode(line), - }; -}; diff --git a/app/assets/javascripts/diffs/components/diff_view.vue b/app/assets/javascripts/diffs/components/diff_view.vue index a2a6ebaeedf..5cf242b4ddd 100644 --- a/app/assets/javascripts/diffs/components/diff_view.vue +++ b/app/assets/javascripts/diffs/components/diff_view.vue @@ -1,12 +1,15 @@ <script> import { mapGetters, mapState, mapActions } from 'vuex'; +import { IdState } from 'vendor/vue-virtual-scroller'; import DraftNote from '~/batch_comments/components/draft_note.vue'; import draftCommentsMixin from '~/diffs/mixins/draft_comments'; import { getCommentedLines } from '~/notes/components/multiline_comment_utils'; +import { hide } from '~/tooltips'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import DiffCommentCell from './diff_comment_cell.vue'; import DiffExpansionCell from './diff_expansion_cell.vue'; import DiffRow from './diff_row.vue'; +import { isHighlighted } from './diff_row_utils'; export default { components: { @@ -15,7 +18,11 @@ export default { DiffCommentCell, DraftNote, }, - mixins: [draftCommentsMixin, glFeatureFlagsMixin()], + mixins: [ + draftCommentsMixin, + glFeatureFlagsMixin(), + IdState({ idProp: (vm) => vm.diffFile.file_hash }), + ], props: { diffFile: { type: Object, @@ -36,15 +43,15 @@ export default { default: false, }, }, - data() { + idState() { return { dragStart: null, updatedLineRange: null, }; }, computed: { - ...mapGetters('diffs', ['commitId']), - ...mapState('diffs', ['codequalityDiff']), + ...mapGetters('diffs', ['commitId', 'fileLineCoverage']), + ...mapState('diffs', ['codequalityDiff', 'highlightedRow']), ...mapState({ selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition, selectedCommentPositionHover: ({ notes }) => notes.selectedCommentPositionHover, @@ -59,45 +66,65 @@ export default { ); }, hasCodequalityChanges() { - return ( - this.glFeatures.codequalityMrDiffAnnotations && - this.codequalityDiff?.files?.[this.diffFile.file_path]?.length > 0 - ); + return this.codequalityDiff?.files?.[this.diffFile.file_path]?.length > 0; }, }, methods: { ...mapActions(['setSelectedCommentPosition']), - ...mapActions('diffs', ['showCommentForm']), + ...mapActions('diffs', ['showCommentForm', 'setHighlightedRow', 'toggleLineDiscussions']), showCommentLeft(line) { return line.left && !line.right; }, showCommentRight(line) { return line.right && !line.left; }, - onStartDragging(line) { - this.dragStart = line; + onStartDragging({ event = {}, line }) { + if (event.target?.parentNode) { + hide(event.target.parentNode); + } + this.idState.dragStart = line; }, onDragOver(line) { - if (line.chunk !== this.dragStart.chunk) return; + if (line.chunk !== this.idState.dragStart.chunk) return; - let start = this.dragStart; + let start = this.idState.dragStart; let end = line; - if (this.dragStart.index >= line.index) { + if (this.idState.dragStart.index >= line.index) { start = line; - end = this.dragStart; + end = this.idState.dragStart; } - this.updatedLineRange = { start, end }; + this.idState.updatedLineRange = { start, end }; - this.setSelectedCommentPosition(this.updatedLineRange); + this.setSelectedCommentPosition(this.idState.updatedLineRange); }, onStopDragging() { this.showCommentForm({ - lineCode: this.updatedLineRange?.end?.line_code, + lineCode: this.idState.updatedLineRange?.end?.line_code, fileHash: this.diffFile.file_hash, }); - this.dragStart = null; + this.idState.dragStart = null; + }, + isHighlighted(line) { + return isHighlighted( + this.highlightedRow, + line.left?.line_code ? line.left : line.right, + false, + ); + }, + handleParallelLineMouseDown(e) { + const line = e.target.closest('.diff-td'); + const table = line.closest('.diff-table'); + + table.classList.remove('left-side-selected', 'right-side-selected'); + const [lineClass] = ['left-side', 'right-side'].filter((name) => + line.classList.contains(name), + ); + + if (lineClass) { + table.classList.add(`${lineClass}-selected`); + } }, }, userColorScheme: window.gon.user_color_scheme, @@ -109,6 +136,7 @@ export default { :class="[$options.userColorScheme, { inline, 'with-codequality': hasCodequalityChanges }]" :data-commit-id="commitId" class="diff-grid diff-table code diff-wrap-lines js-syntax-highlight text-file" + @mousedown="handleParallelLineMouseDown" > <template v-for="(line, index) in diffLines"> <div @@ -136,6 +164,14 @@ export default { :is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine" :inline="inline" :index="index" + :is-highlighted="isHighlighted(line)" + :file-line-coverage="fileLineCoverage" + @showCommentForm="(lineCode) => showCommentForm({ lineCode, fileHash: diffFile.file_hash })" + @setHighlightedRow="setHighlightedRow" + @toggleLineDiscussions=" + ({ lineCode, expanded }) => + toggleLineDiscussions({ lineCode, fileHash: diffFile.file_hash, expanded }) + " @enterdragging="onDragOver" @startdragging="onStartDragging" @stopdragging="onStopDragging" diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue deleted file mode 100644 index f903fef72b7..00000000000 --- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue +++ /dev/null @@ -1,204 +0,0 @@ -<script> -import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; -import { mapActions, mapGetters, mapState } from 'vuex'; -import { CONTEXT_LINE_CLASS_NAME } from '../constants'; -import { getInteropInlineAttributes } from '../utils/interoperability'; -import DiffGutterAvatars from './diff_gutter_avatars.vue'; -import { - isHighlighted, - shouldShowCommentButton, - shouldRenderCommentButton, - classNameMapCell, - addCommentTooltip, -} from './diff_row_utils'; - -export default { - components: { - DiffGutterAvatars, - GlIcon, - }, - directives: { - GlTooltip: GlTooltipDirective, - SafeHtml, - }, - props: { - fileHash: { - type: String, - required: true, - }, - filePath: { - type: String, - required: true, - }, - line: { - type: Object, - required: true, - }, - isBottom: { - type: Boolean, - required: false, - default: false, - }, - isCommented: { - type: Boolean, - required: false, - default: false, - }, - }, - data() { - return { - isHover: false, - }; - }, - computed: { - ...mapGetters(['isLoggedIn']), - ...mapGetters('diffs', ['fileLineCoverage']), - ...mapState({ - isHighlighted(state) { - return isHighlighted(state, this.line, this.isCommented); - }, - }), - classNameMap() { - return [ - this.line.type, - { - [CONTEXT_LINE_CLASS_NAME]: this.line.isContextLine, - }, - ]; - }, - inlineRowId() { - return this.line.line_code || `${this.fileHash}_${this.line.old_line}_${this.line.new_line}`; - }, - coverageState() { - return this.fileLineCoverage(this.filePath, this.line.new_line); - }, - classNameMapCell() { - return classNameMapCell({ - line: this.line, - hll: this.isHighlighted, - isLoggedIn: this.isLoggedIn, - isHover: this.isHover, - }); - }, - addCommentTooltip() { - return addCommentTooltip(this.line); - }, - shouldRenderCommentButton() { - return shouldRenderCommentButton(this.isLoggedIn, true); - }, - shouldShowCommentButton() { - return shouldShowCommentButton( - this.isHover, - this.line.isContextLine, - this.line.isMetaLine, - this.line.hasDiscussions, - ); - }, - shouldShowAvatarsOnGutter() { - return this.line.hasDiscussions; - }, - interopAttrs() { - return getInteropInlineAttributes(this.line); - }, - }, - mounted() { - this.scrollToLineIfNeededInline(this.line); - }, - methods: { - ...mapActions('diffs', [ - 'scrollToLineIfNeededInline', - 'showCommentForm', - 'setHighlightedRow', - 'toggleLineDiscussions', - ]), - handleMouseMove(e) { - // To show the comment icon on the gutter we need to know if we hover the line. - // Current table structure doesn't allow us to do this with CSS in both of the diff view types - this.isHover = e.type === 'mouseover'; - }, - handleCommentButton() { - this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash }); - }, - }, -}; -</script> - -<template> - <tr - :id="inlineRowId" - :class="classNameMap" - class="line_holder" - v-bind="interopAttrs" - @mouseover="handleMouseMove" - @mouseout="handleMouseMove" - > - <td ref="oldTd" class="diff-line-num old_line" :class="classNameMapCell"> - <span - v-if="shouldRenderCommentButton" - ref="addNoteTooltip" - v-gl-tooltip - class="add-diff-note tooltip-wrapper" - :title="addCommentTooltip" - > - <button - v-show="shouldShowCommentButton" - ref="addDiffNoteButton" - type="button" - class="add-diff-note note-button js-add-diff-note-button" - :disabled="line.commentsDisabled" - :aria-label="addCommentTooltip" - @click="handleCommentButton" - > - <gl-icon :size="12" name="comment" /> - </button> - </span> - <a - v-if="line.old_line" - ref="lineNumberRefOld" - :data-linenumber="line.old_line" - :href="line.lineHref" - @click="setHighlightedRow(line.lineCode)" - > - </a> - <diff-gutter-avatars - v-if="shouldShowAvatarsOnGutter" - :discussions="line.discussions" - :discussions-expanded="line.discussionsExpanded" - @toggleLineDiscussions=" - toggleLineDiscussions({ - lineCode: line.lineCode, - fileHash, - expanded: !line.discussionsExpanded, - }) - " - /> - </td> - <td ref="newTd" class="diff-line-num new_line" :class="classNameMapCell"> - <a - v-if="line.new_line" - ref="lineNumberRefNew" - :data-linenumber="line.new_line" - :href="line.lineHref" - @click="setHighlightedRow(line.lineCode)" - > - </a> - </td> - <td - v-gl-tooltip.hover - :title="coverageState.text" - :class="[line.type, coverageState.class, { hll: isHighlighted }]" - class="line-coverage" - ></td> - <td - :key="line.line_code" - v-safe-html="line.rich_text" - :class="[ - line.type, - { - hll: isHighlighted, - }, - ]" - class="line_content with-coverage" - ></td> - </tr> -</template> diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue deleted file mode 100644 index e407609d9e9..00000000000 --- a/app/assets/javascripts/diffs/components/inline_diff_view.vue +++ /dev/null @@ -1,117 +0,0 @@ -<script> -import { mapGetters, mapState } from 'vuex'; -import DraftNote from '~/batch_comments/components/draft_note.vue'; -import draftCommentsMixin from '~/diffs/mixins/draft_comments'; -import { getCommentedLines } from '~/notes/components/multiline_comment_utils'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import DiffCommentCell from './diff_comment_cell.vue'; -import DiffExpansionCell from './diff_expansion_cell.vue'; -import inlineDiffTableRow from './inline_diff_table_row.vue'; - -export default { - components: { - DiffCommentCell, - inlineDiffTableRow, - DraftNote, - DiffExpansionCell, - }, - mixins: [draftCommentsMixin, glFeatureFlagsMixin()], - props: { - diffFile: { - type: Object, - required: true, - }, - diffLines: { - type: Array, - required: true, - }, - helpPagePath: { - type: String, - required: false, - default: '', - }, - }, - computed: { - ...mapGetters('diffs', ['commitId']), - ...mapState({ - selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition, - selectedCommentPositionHover: ({ notes }) => notes.selectedCommentPositionHover, - }), - diffLinesLength() { - return this.diffLines.length; - }, - commentedLines() { - return getCommentedLines( - this.selectedCommentPosition || this.selectedCommentPositionHover, - this.diffLines, - ); - }, - }, - userColorScheme: window.gon.user_color_scheme, -}; -</script> - -<template> - <table - :class="$options.userColorScheme" - :data-commit-id="commitId" - class="code diff-wrap-lines js-syntax-highlight text-file js-diff-inline-view" - > - <colgroup> - <col style="width: 50px" /> - <col style="width: 50px" /> - <col style="width: 8px" /> - <col /> - </colgroup> - <tbody> - <template v-for="(line, index) in diffLines"> - <tr v-if="line.isMatchLine" :key="`expand-${index}`" class="line_expansion match"> - <td colspan="4" class="text-center gl-font-regular"> - <diff-expansion-cell - :file-hash="diffFile.file_hash" - :context-lines-path="diffFile.context_lines_path" - :line="line" - :is-top="index === 0" - :is-bottom="index + 1 === diffLinesLength" - /> - </td> - </tr> - <inline-diff-table-row - v-if="!line.isMatchLine" - :key="`${line.line_code || index}`" - :file-hash="diffFile.file_hash" - :file-path="diffFile.file_path" - :line="line" - :is-bottom="index + 1 === diffLinesLength" - :is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine" - /> - <tr - v-if="line.renderCommentRow" - :key="`icr-${line.line_code || index}`" - :class="line.commentRowClasses" - class="notes_holder" - > - <td class="notes-content" colspan="4"> - <diff-comment-cell - :diff-file-hash="diffFile.file_hash" - :line="line" - :help-page-path="helpPagePath" - :has-draft="line.hasDraft" - /> - </td> - </tr> - <tr v-if="line.hasDraft" :key="`draft_${index}`" class="notes_holder js-temp-notes-holder"> - <td class="notes-content" colspan="4"> - <div class="content"> - <draft-note - :draft="draftForLine(diffFile.file_hash, line)" - :diff-file="diffFile" - :line="line" - /> - </div> - </td> - </tr> - </template> - </tbody> - </table> -</template> diff --git a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue deleted file mode 100644 index 2d33926c8aa..00000000000 --- a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue +++ /dev/null @@ -1,310 +0,0 @@ -<script> -import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; -import $ from 'jquery'; -import { mapActions, mapGetters, mapState } from 'vuex'; -import { CONTEXT_LINE_CLASS_NAME, PARALLEL_DIFF_VIEW_TYPE } from '../constants'; -import { - getInteropOldSideAttributes, - getInteropNewSideAttributes, -} from '../utils/interoperability'; -import DiffGutterAvatars from './diff_gutter_avatars.vue'; -import * as utils from './diff_row_utils'; - -export default { - components: { - GlIcon, - DiffGutterAvatars, - }, - directives: { - GlTooltip: GlTooltipDirective, - SafeHtml, - }, - props: { - fileHash: { - type: String, - required: true, - }, - filePath: { - type: String, - required: true, - }, - line: { - type: Object, - required: true, - }, - isBottom: { - type: Boolean, - required: false, - default: false, - }, - isCommented: { - type: Boolean, - required: false, - default: false, - }, - }, - data() { - return { - isLeftHover: false, - isRightHover: false, - isCommentButtonRendered: false, - }; - }, - computed: { - ...mapGetters('diffs', ['fileLineCoverage']), - ...mapGetters(['isLoggedIn']), - ...mapState({ - isHighlighted(state) { - const line = this.line.left?.line_code ? this.line.left : this.line.right; - return utils.isHighlighted(state, line, this.isCommented); - }, - }), - classNameMap() { - return { - [CONTEXT_LINE_CLASS_NAME]: this.line.isContextLineLeft, - [PARALLEL_DIFF_VIEW_TYPE]: true, - }; - }, - parallelViewLeftLineType() { - return utils.parallelViewLeftLineType(this.line, this.isHighlighted); - }, - coverageState() { - return this.fileLineCoverage(this.filePath, this.line.right.new_line); - }, - classNameMapCellLeft() { - return utils.classNameMapCell({ - line: this.line.left, - hll: this.isHighlighted, - isLoggedIn: this.isLoggedIn, - isHover: this.isLeftHover, - }); - }, - classNameMapCellRight() { - return utils.classNameMapCell({ - line: this.line.right, - hll: this.isHighlighted, - isLoggedIn: this.isLoggedIn, - isHover: this.isRightHover, - }); - }, - addCommentTooltipLeft() { - return utils.addCommentTooltip(this.line.left); - }, - addCommentTooltipRight() { - return utils.addCommentTooltip(this.line.right); - }, - shouldRenderCommentButton() { - return utils.shouldRenderCommentButton(this.isLoggedIn, this.isCommentButtonRendered); - }, - shouldShowCommentButtonLeft() { - return utils.shouldShowCommentButton( - this.isLeftHover, - this.line.isContextLineLeft, - this.line.isMetaLineLeft, - this.line.hasDiscussionsLeft, - ); - }, - shouldShowCommentButtonRight() { - return utils.shouldShowCommentButton( - this.isRightHover, - this.line.isContextLineRight, - this.line.isMetaLineRight, - this.line.hasDiscussionsRight, - ); - }, - interopLeftAttributes() { - return getInteropOldSideAttributes(this.line.left); - }, - interopRightAttributes() { - return getInteropNewSideAttributes(this.line.right); - }, - }, - mounted() { - this.scrollToLineIfNeededParallel(this.line); - this.unwatchShouldShowCommentButton = this.$watch( - (vm) => [vm.shouldShowCommentButtonLeft, vm.shouldShowCommentButtonRight].join(), - (newVal) => { - if (newVal) { - this.isCommentButtonRendered = true; - this.unwatchShouldShowCommentButton(); - } - }, - ); - }, - beforeDestroy() { - this.unwatchShouldShowCommentButton(); - }, - methods: { - ...mapActions('diffs', [ - 'scrollToLineIfNeededParallel', - 'showCommentForm', - 'setHighlightedRow', - 'toggleLineDiscussions', - ]), - handleMouseMove(e) { - const isHover = e.type === 'mouseover'; - const hoveringCell = e.target.closest('td'); - const allCellsInHoveringRow = Array.from(e.currentTarget.children); - const hoverIndex = allCellsInHoveringRow.indexOf(hoveringCell); - - if (hoverIndex >= 3) { - this.isRightHover = isHover; - } else { - this.isLeftHover = isHover; - } - }, - // Prevent text selecting on both sides of parallel diff view - // Backport of the same code from legacy diff notes. - handleParallelLineMouseDown(e) { - const line = $(e.currentTarget); - const table = line.closest('table'); - - table.removeClass('left-side-selected right-side-selected'); - const [lineClass] = ['left-side', 'right-side'].filter((name) => line.hasClass(name)); - - if (lineClass) { - table.addClass(`${lineClass}-selected`); - } - }, - handleCommentButton(line) { - this.showCommentForm({ lineCode: line.line_code, fileHash: this.fileHash }); - }, - }, -}; -</script> - -<template> - <tr - :class="classNameMap" - class="line_holder" - @mouseover="handleMouseMove" - @mouseout="handleMouseMove" - > - <template v-if="line.left && !line.isMatchLineLeft"> - <td ref="oldTd" :class="classNameMapCellLeft" class="diff-line-num old_line"> - <span - v-if="shouldRenderCommentButton" - ref="addNoteTooltipLeft" - v-gl-tooltip - class="add-diff-note tooltip-wrapper" - :title="addCommentTooltipLeft" - > - <button - v-show="shouldShowCommentButtonLeft" - ref="addDiffNoteButtonLeft" - type="button" - class="add-diff-note note-button js-add-diff-note-button" - :disabled="line.left.commentsDisabled" - :aria-label="addCommentTooltipLeft" - @click="handleCommentButton(line.left)" - > - <gl-icon :size="12" name="comment" /> - </button> - </span> - <a - v-if="line.left.old_line" - ref="lineNumberRefOld" - :data-linenumber="line.left.old_line" - :href="line.lineHrefOld" - @click="setHighlightedRow(line.lineCode)" - > - </a> - <diff-gutter-avatars - v-if="line.hasDiscussionsLeft" - :discussions="line.left.discussions" - :discussions-expanded="line.left.discussionsExpanded" - @toggleLineDiscussions=" - toggleLineDiscussions({ - lineCode: line.left.line_code, - fileHash, - expanded: !line.left.discussionsExpanded, - }) - " - /> - </td> - <td :class="parallelViewLeftLineType" class="line-coverage left-side"></td> - <td - :id="line.left.line_code" - :key="line.left.line_code" - v-safe-html="line.left.rich_text" - :class="parallelViewLeftLineType" - v-bind="interopLeftAttributes" - class="line_content with-coverage parallel left-side" - @mousedown="handleParallelLineMouseDown" - ></td> - </template> - <template v-else> - <td class="diff-line-num old_line empty-cell"></td> - <td class="line-coverage left-side empty-cell"></td> - <td class="line_content with-coverage parallel left-side empty-cell"></td> - </template> - <template v-if="line.right && !line.isMatchLineRight"> - <td ref="newTd" :class="classNameMapCellRight" class="diff-line-num new_line"> - <span - v-if="shouldRenderCommentButton" - ref="addNoteTooltipRight" - v-gl-tooltip - class="add-diff-note tooltip-wrapper" - :title="addCommentTooltipRight" - > - <button - v-show="shouldShowCommentButtonRight" - ref="addDiffNoteButtonRight" - type="button" - class="add-diff-note note-button js-add-diff-note-button" - :disabled="line.right.commentsDisabled" - :aria-label="addCommentTooltipRight" - @click="handleCommentButton(line.right)" - > - <gl-icon :size="12" name="comment" /> - </button> - </span> - <a - v-if="line.right.new_line" - ref="lineNumberRefNew" - :data-linenumber="line.right.new_line" - :href="line.lineHrefNew" - @click="setHighlightedRow(line.lineCode)" - > - </a> - <diff-gutter-avatars - v-if="line.hasDiscussionsRight" - :discussions="line.right.discussions" - :discussions-expanded="line.right.discussionsExpanded" - @toggleLineDiscussions=" - toggleLineDiscussions({ - lineCode: line.right.line_code, - fileHash, - expanded: !line.right.discussionsExpanded, - }) - " - /> - </td> - <td - v-gl-tooltip.hover - :title="coverageState.text" - :class="[line.right.type, coverageState.class, { hll: isHighlighted }]" - class="line-coverage right-side" - ></td> - <td - :id="line.right.line_code" - :key="line.right.rich_text" - v-safe-html="line.right.rich_text" - :class="[ - line.right.type, - { - hll: isHighlighted, - }, - ]" - v-bind="interopRightAttributes" - class="line_content with-coverage parallel right-side" - @mousedown="handleParallelLineMouseDown" - ></td> - </template> - <template v-else> - <td class="diff-line-num old_line empty-cell"></td> - <td class="line-coverage right-side empty-cell"></td> - <td class="line_content with-coverage parallel right-side empty-cell"></td> - </template> - </tr> -</template> diff --git a/app/assets/javascripts/diffs/components/parallel_diff_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue deleted file mode 100644 index b167081a379..00000000000 --- a/app/assets/javascripts/diffs/components/parallel_diff_view.vue +++ /dev/null @@ -1,142 +0,0 @@ -<script> -import { mapGetters, mapState } from 'vuex'; -import DraftNote from '~/batch_comments/components/draft_note.vue'; -import draftCommentsMixin from '~/diffs/mixins/draft_comments'; -import { getCommentedLines } from '~/notes/components/multiline_comment_utils'; -import DiffCommentCell from './diff_comment_cell.vue'; -import DiffExpansionCell from './diff_expansion_cell.vue'; -import parallelDiffTableRow from './parallel_diff_table_row.vue'; - -export default { - components: { - DiffExpansionCell, - parallelDiffTableRow, - DiffCommentCell, - DraftNote, - }, - mixins: [draftCommentsMixin], - props: { - diffFile: { - type: Object, - required: true, - }, - diffLines: { - type: Array, - required: true, - }, - helpPagePath: { - type: String, - required: false, - default: '', - }, - }, - computed: { - ...mapGetters('diffs', ['commitId']), - ...mapState({ - selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition, - selectedCommentPositionHover: ({ notes }) => notes.selectedCommentPositionHover, - }), - diffLinesLength() { - return this.diffLines.length; - }, - commentedLines() { - return getCommentedLines( - this.selectedCommentPosition || this.selectedCommentPositionHover, - this.diffLines, - ); - }, - }, - userColorScheme: window.gon.user_color_scheme, -}; -</script> - -<template> - <table - :class="$options.userColorScheme" - :data-commit-id="commitId" - class="code diff-wrap-lines js-syntax-highlight text-file" - > - <colgroup> - <col style="width: 50px" /> - <col style="width: 8px" /> - <col /> - <col style="width: 50px" /> - <col style="width: 8px" /> - <col /> - </colgroup> - <tbody> - <template v-for="(line, index) in diffLines"> - <tr - v-if="line.isMatchLineLeft || line.isMatchLineRight" - :key="`expand-${index}`" - class="line_expansion match" - > - <td colspan="6" class="text-center gl-font-regular"> - <diff-expansion-cell - :file-hash="diffFile.file_hash" - :context-lines-path="diffFile.context_lines_path" - :line="line.left" - :is-top="index === 0" - :is-bottom="index + 1 === diffLinesLength" - /> - </td> - </tr> - <parallel-diff-table-row - :key="line.line_code" - :file-hash="diffFile.file_hash" - :file-path="diffFile.file_path" - :line="line" - :is-bottom="index + 1 === diffLinesLength" - :is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine" - /> - <tr - v-if="line.renderCommentRow" - :key="`dcr-${line.line_code || index}`" - :class="line.commentRowClasses" - class="notes_holder" - > - <td class="notes-content parallel old" colspan="3"> - <diff-comment-cell - v-if="line.left" - :line="line.left" - :diff-file-hash="diffFile.file_hash" - :help-page-path="helpPagePath" - :has-draft="line.left.hasDraft" - line-position="left" - /> - </td> - <td class="notes-content parallel new" colspan="3"> - <diff-comment-cell - v-if="line.right" - :line="line.right" - :diff-file-hash="diffFile.file_hash" - :line-index="index" - :help-page-path="helpPagePath" - :has-draft="line.right.hasDraft" - line-position="right" - /> - </td> - </tr> - <tr - v-if="shouldRenderParallelDraftRow(diffFile.file_hash, line)" - :key="`drafts-${index}`" - :class="line.draftRowClasses" - class="notes_holder" - > - <td class="notes_line old"></td> - <td class="notes-content parallel old" colspan="2"> - <div v-if="line.left && line.left.lineDraft.isDraft" class="content"> - <draft-note :draft="line.left.lineDraft" :line="line.left" /> - </div> - </td> - <td class="notes_line new"></td> - <td class="notes-content parallel new" colspan="2"> - <div v-if="line.right && line.right.lineDraft.isDraft" class="content"> - <draft-note :draft="line.right.lineDraft" :line="line.right" /> - </div> - </td> - </tr> - </template> - </tbody> - </table> -</template> diff --git a/app/assets/javascripts/diffs/components/pre_renderer.vue b/app/assets/javascripts/diffs/components/pre_renderer.vue new file mode 100644 index 00000000000..c357aa2d924 --- /dev/null +++ b/app/assets/javascripts/diffs/components/pre_renderer.vue @@ -0,0 +1,84 @@ +<script> +export default { + inject: ['vscrollParent'], + props: { + maxLength: { + type: Number, + required: true, + }, + }, + data() { + return { + nextIndex: -1, + nextItem: null, + startedRender: false, + width: 0, + }; + }, + mounted() { + this.width = this.$el.parentNode.offsetWidth; + window.test = this; + + this.$_itemsWithSizeWatcher = this.$watch('vscrollParent.itemsWithSize', async () => { + await this.$nextTick(); + + const nextItem = this.findNextToRender(); + + if (nextItem) { + this.startedRender = true; + requestIdleCallback(() => { + this.nextItem = nextItem; + + if (this.nextIndex === this.maxLength - 1) { + this.$nextTick(() => { + if (this.vscrollParent.itemsWithSize[this.maxLength - 1].size !== 0) { + this.clearRendering(); + } + }); + } + }); + } else if (this.startedRender) { + this.clearRendering(); + } + }); + }, + beforeDestroy() { + this.$_itemsWithSizeWatcher(); + }, + methods: { + clearRendering() { + this.nextItem = null; + + if (this.maxLength === this.vscrollParent.itemsWithSize.length) { + this.$_itemsWithSizeWatcher(); + } + }, + findNextToRender() { + return this.vscrollParent.itemsWithSize.find(({ size }, index) => { + const isNext = size === 0; + + if (isNext) { + this.nextIndex = index; + } + + return isNext; + }); + }, + }, +}; +</script> + +<template> + <div v-if="nextItem" :style="{ width: `${width}px` }" class="gl-absolute diff-file-offscreen"> + <slot + v-bind="{ item: nextItem.item, index: nextIndex, active: true, itemWithSize: nextItem }" + ></slot> + </div> +</template> + +<style scoped> +.diff-file-offscreen { + top: -200%; + left: -200%; +} +</style> diff --git a/app/assets/javascripts/diffs/components/virtual_scroller_scroll_sync.js b/app/assets/javascripts/diffs/components/virtual_scroller_scroll_sync.js new file mode 100644 index 00000000000..984c6f8c0c9 --- /dev/null +++ b/app/assets/javascripts/diffs/components/virtual_scroller_scroll_sync.js @@ -0,0 +1,51 @@ +import { handleLocationHash } from '~/lib/utils/common_utils'; + +export default { + inject: ['vscrollParent'], + props: { + index: { + type: Number, + required: true, + }, + }, + watch: { + index: { + handler() { + const { index } = this; + + if (index < 0) return; + + if (this.vscrollParent.itemsWithSize[index].size) { + this.scrollToIndex(index); + } else { + this.$_itemsWithSizeWatcher = this.$watch('vscrollParent.itemsWithSize', async () => { + await this.$nextTick(); + + if (this.vscrollParent.itemsWithSize[index].size) { + this.$_itemsWithSizeWatcher(); + this.scrollToIndex(index); + + await this.$nextTick(); + } + }); + } + }, + immediate: true, + }, + }, + beforeDestroy() { + if (this.$_itemsWithSizeWatcher) this.$_itemsWithSizeWatcher(); + }, + methods: { + scrollToIndex(index) { + this.vscrollParent.scrollToItem(index); + + setTimeout(() => { + handleLocationHash(); + }); + }, + }, + render(h) { + return h(null); + }, +}; diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index d1e02fbc598..f1cf556fde0 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -59,7 +59,6 @@ export const MIN_RENDERING_MS = 2; export const START_RENDERING_INDEX = 200; export const INLINE_DIFF_LINES_KEY = 'highlighted_diff_lines'; export const PARALLEL_DIFF_LINES_KEY = 'parallel_diff_lines'; -export const DIFFS_PER_PAGE = 20; export const DIFF_COMPARE_BASE_VERSION_INDEX = -1; export const DIFF_COMPARE_HEAD_VERSION_INDEX = -2; diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js index 0ab72749760..ea83523008c 100644 --- a/app/assets/javascripts/diffs/index.js +++ b/app/assets/javascripts/diffs/index.js @@ -50,9 +50,6 @@ export default function initDiffsApp(store) { click: this.openFile, }, class: ['diff-file-finder'], - style: { - display: this.fileFinderVisible ? '' : 'none', - }, }); }, }); diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 2e94f147086..66510edf3db 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -25,7 +25,6 @@ import { MIN_RENDERING_MS, START_RENDERING_INDEX, INLINE_DIFF_LINES_KEY, - DIFFS_PER_PAGE, DIFF_FILE_MANUAL_COLLAPSE, DIFF_FILE_AUTOMATIC_COLLAPSE, EVT_PERF_MARK_FILE_TREE_START, @@ -92,22 +91,18 @@ export const setBaseConfig = ({ commit }, options) => { }; export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => { - const diffsGradualLoad = window.gon?.features?.diffsGradualLoad; - let perPage = DIFFS_PER_PAGE; + let perPage = state.viewDiffsFileByFile ? 1 : 5; let increaseAmount = 1.4; - - if (diffsGradualLoad) { - perPage = state.viewDiffsFileByFile ? 1 : 5; - } - - const startPage = diffsGradualLoad ? 0 : 1; + const startPage = 0; const id = window?.location?.hash; const isNoteLink = id.indexOf('#note') === 0; const urlParams = { w: state.showWhitespace ? '0' : '1', view: 'inline', }; + const hash = window.location.hash.replace('#', '').split('diff-content-').pop(); let totalLoaded = 0; + let scrolledVirtualScroller = false; commit(types.SET_BATCH_LOADING, true); commit(types.SET_RETRIEVING_BATCHES, true); @@ -122,6 +117,18 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => { commit(types.SET_DIFF_DATA_BATCH, { diff_files }); commit(types.SET_BATCH_LOADING, false); + if (window.gon?.features?.diffsVirtualScrolling && !scrolledVirtualScroller) { + const index = state.diffFiles.findIndex( + (f) => + f.file_hash === hash || f[INLINE_DIFF_LINES_KEY].find((l) => l.line_code === hash), + ); + + if (index >= 0) { + eventHub.$emit('scrollToIndex', index); + scrolledVirtualScroller = true; + } + } + if (!isNoteLink && !state.currentDiffFileId) { commit(types.VIEW_DIFF_FILE, diff_files[0].file_hash); } @@ -130,11 +137,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => { dispatch('setCurrentDiffFileIdFromNote', id.split('_').pop()); } - if ( - (diffsGradualLoad && - (totalLoaded === pagination.total_pages || pagination.total_pages === null)) || - (!diffsGradualLoad && !pagination.next_page) - ) { + if (totalLoaded === pagination.total_pages || pagination.total_pages === null) { commit(types.SET_RETRIEVING_BATCHES, false); // We need to check that the currentDiffFileId points to a file that exists @@ -164,15 +167,11 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => { return null; } - if (diffsGradualLoad) { - const nextPage = page + perPage; - perPage = Math.min(Math.ceil(perPage * increaseAmount), 30); - increaseAmount = Math.min(increaseAmount + 0.2, 2); - - return nextPage; - } + const nextPage = page + perPage; + perPage = Math.min(Math.ceil(perPage * increaseAmount), 30); + increaseAmount = Math.min(increaseAmount + 0.2, 2); - return pagination.next_page; + return nextPage; }) .then((nextPage) => { dispatch('startRenderDiffsQueue'); @@ -186,7 +185,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => { .catch(() => commit(types.SET_RETRIEVING_BATCHES, false)); return getBatch() - .then(handleLocationHash) + .then(() => !window.gon?.features?.diffsVirtualScrolling && handleLocationHash()) .catch(() => null); }; @@ -250,6 +249,8 @@ export const setHighlightedRow = ({ commit }, lineCode) => { const fileHash = lineCode.split('_')[0]; commit(types.SET_HIGHLIGHTED_ROW, lineCode); commit(types.VIEW_DIFF_FILE, fileHash); + + handleLocationHash(); }; // This is adding line discussions to the actual lines in the diff tree @@ -523,9 +524,18 @@ export const scrollToFile = ({ state, commit }, path) => { if (!state.treeEntries[path]) return; const { fileHash } = state.treeEntries[path]; - document.location.hash = fileHash; commit(types.VIEW_DIFF_FILE, fileHash); + + if (window.gon?.features?.diffsVirtualScrolling) { + eventHub.$emit('scrollToFileHash', fileHash); + + setTimeout(() => { + window.history.replaceState(null, null, `#${fileHash}`); + }); + } else { + document.location.hash = fileHash; + } }; export const setShowTreeList = ({ commit }, { showTreeList, saving = true }) => { @@ -570,7 +580,7 @@ export const setShowWhitespace = async ( { state, commit }, { url, showWhitespace, updateDatabase = true }, ) => { - if (updateDatabase) { + if (updateDatabase && Boolean(window.gon?.current_user_id)) { await axios.put(url || state.endpointUpdateUser, { show_whitespace_in_diffs: showWhitespace }); } diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js index a536db5c417..1b6a673925f 100644 --- a/app/assets/javascripts/diffs/store/getters.js +++ b/app/assets/javascripts/diffs/store/getters.js @@ -151,11 +151,7 @@ export const currentDiffIndex = (state) => state.diffFiles.findIndex((diff) => diff.file_hash === state.currentDiffFileId), ); -export const diffLines = (state) => (file, unifiedDiffComponents) => { - if (!unifiedDiffComponents && state.diffViewType === INLINE_DIFF_VIEW_TYPE) { - return null; - } - +export const diffLines = (state) => (file) => { return parallelizeDiffLines( file.highlighted_diff_lines || [], state.diffViewType === INLINE_DIFF_VIEW_TYPE, diff --git a/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js b/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js index 673ec821b58..65ffd42fa27 100644 --- a/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js +++ b/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js @@ -1,4 +1,5 @@ -import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import { getParameterByName } from '~/lib/utils/url_utility'; import { __, n__, sprintf } from '~/locale'; import { DIFF_COMPARE_BASE_VERSION_INDEX, DIFF_COMPARE_HEAD_VERSION_INDEX } from '../constants'; diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index 75d2cf43b94..3f1af68e37a 100644 --- a/app/assets/javascripts/diffs/store/utils.js +++ b/app/assets/javascripts/diffs/store/utils.js @@ -381,9 +381,15 @@ function prepareDiffFileLines(file) { } function finalizeDiffFile(file, index) { + let renderIt = Boolean(window.gon?.features?.diffsVirtualScrolling); + + if (!window.gon?.features?.diffsVirtualScrolling) { + renderIt = + index < 3 ? file[INLINE_DIFF_LINES_KEY].length < LINES_TO_BE_RENDERED_DIRECTLY : false; + } + Object.assign(file, { - renderIt: - index < 3 ? file[INLINE_DIFF_LINES_KEY].length < LINES_TO_BE_RENDERED_DIRECTLY : false, + renderIt, isShowingFullFile: false, isLoadingFullFile: false, discussions: [], |