summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/diffs
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/diffs')
-rw-r--r--app/assets/javascripts/diffs/components/app.vue55
-rw-r--r--app/assets/javascripts/diffs/components/changed_files.vue171
-rw-r--r--app/assets/javascripts/diffs/components/changed_files_dropdown.vue126
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue10
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue128
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions_dropdown.vue10
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue23
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue10
-rw-r--r--app/assets/javascripts/diffs/components/file_row_stats.vue30
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue101
-rw-r--r--app/assets/javascripts/diffs/constants.js2
-rw-r--r--app/assets/javascripts/diffs/mixins/changed_files.js38
-rw-r--r--app/assets/javascripts/diffs/store/actions.js19
-rw-r--r--app/assets/javascripts/diffs/store/getters.js4
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js7
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js3
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js14
-rw-r--r--app/assets/javascripts/diffs/store/utils.js51
18 files changed, 419 insertions, 383 deletions
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index fc41ee4b777..e60c53338fe 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -5,22 +5,22 @@ import { __ } from '~/locale';
import createFlash from '~/flash';
import eventHub from '../../notes/event_hub';
import CompareVersions from './compare_versions.vue';
-import ChangedFiles from './changed_files.vue';
import DiffFile from './diff_file.vue';
import NoChanges from './no_changes.vue';
import HiddenFilesWarning from './hidden_files_warning.vue';
import CommitWidget from './commit_widget.vue';
+import TreeList from './tree_list.vue';
export default {
name: 'DiffsApp',
components: {
Icon,
CompareVersions,
- ChangedFiles,
DiffFile,
NoChanges,
HiddenFilesWarning,
CommitWidget,
+ TreeList,
},
props: {
endpoint: {
@@ -58,6 +58,7 @@ export default {
plainDiffPath: state => state.diffs.plainDiffPath,
emailPatchPath: state => state.diffs.emailPatchPath,
}),
+ ...mapState('diffs', ['showTreeList']),
...mapGetters('diffs', ['isParallelView']),
...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']),
targetBranch() {
@@ -88,6 +89,9 @@ export default {
canCurrentUserFork() {
return this.currentUser.canFork === true && this.currentUser.canCreateMergeRequest;
},
+ showCompareVersions() {
+ return this.mergeRequestDiffs && this.mergeRequestDiff;
+ },
},
watch: {
diffViewType() {
@@ -102,6 +106,8 @@ export default {
this.adjustView();
},
+ isLoading: 'adjustView',
+ showTreeList: 'adjustView',
},
mounted() {
this.setBaseConfig({ endpoint: this.endpoint, projectPath: this.projectPath });
@@ -152,10 +158,11 @@ export default {
}
},
adjustView() {
- if (this.shouldShow && this.isParallelView) {
- window.mrTabs.expandViewContainer();
- } else {
- window.mrTabs.resetViewContainer();
+ if (this.shouldShow) {
+ this.$nextTick(() => {
+ window.mrTabs.resetViewContainer();
+ window.mrTabs.expandViewContainer(this.showTreeList);
+ });
}
},
},
@@ -177,7 +184,7 @@ export default {
class="diffs tab-pane"
>
<compare-versions
- v-if="!commit && mergeRequestDiffs.length > 1"
+ v-if="showCompareVersions"
:merge-request-diffs="mergeRequestDiffs"
:merge-request-diff="mergeRequestDiff"
:start-version="startVersion"
@@ -215,22 +222,26 @@ export default {
:commit="commit"
/>
- <changed-files
- :diff-files="diffFiles"
- />
-
- <div
- v-if="diffFiles.length > 0"
- class="files"
- >
- <diff-file
- v-for="file in diffFiles"
- :key="file.newPath"
- :file="file"
- :can-current-user-fork="canCurrentUserFork"
- />
+ <div class="files d-flex prepend-top-default">
+ <div
+ v-show="showTreeList"
+ class="diff-tree-list"
+ >
+ <tree-list />
+ </div>
+ <div
+ v-if="diffFiles.length > 0"
+ class="diff-files-holder"
+ >
+ <diff-file
+ v-for="file in diffFiles"
+ :key="file.newPath"
+ :file="file"
+ :can-current-user-fork="canCurrentUserFork"
+ />
+ </div>
+ <no-changes v-else />
</div>
- <no-changes v-else />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/diffs/components/changed_files.vue b/app/assets/javascripts/diffs/components/changed_files.vue
deleted file mode 100644
index 97751db1254..00000000000
--- a/app/assets/javascripts/diffs/components/changed_files.vue
+++ /dev/null
@@ -1,171 +0,0 @@
-<script>
-import { mapGetters, mapActions } from 'vuex';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import Icon from '~/vue_shared/components/icon.vue';
-import { pluralize } from '~/lib/utils/text_utility';
-import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
-import { contentTop } from '~/lib/utils/common_utils';
-import { __ } from '~/locale';
-import ChangedFilesDropdown from './changed_files_dropdown.vue';
-import changedFilesMixin from '../mixins/changed_files';
-
-export default {
- components: {
- Icon,
- ChangedFilesDropdown,
- ClipboardButton,
- },
- mixins: [changedFilesMixin],
- data() {
- return {
- isStuck: false,
- maxWidth: 'auto',
- offsetTop: 0,
- };
- },
- computed: {
- ...mapGetters('diffs', ['isInlineView', 'isParallelView', 'areAllFilesCollapsed']),
- sumAddedLines() {
- return this.sumValues('addedLines');
- },
- sumRemovedLines() {
- return this.sumValues('removedLines');
- },
- whitespaceVisible() {
- return !getParameterValues('w')[0];
- },
- toggleWhitespaceText() {
- if (this.whitespaceVisible) {
- return __('Hide whitespace changes');
- }
- return __('Show whitespace changes');
- },
- toggleWhitespacePath() {
- if (this.whitespaceVisible) {
- return mergeUrlParams({ w: 1 }, window.location.href);
- }
-
- return mergeUrlParams({ w: 0 }, window.location.href);
- },
- top() {
- return `${this.offsetTop}px`;
- },
- },
- created() {
- document.addEventListener('scroll', this.handleScroll);
- this.offsetTop = contentTop();
- },
- beforeDestroy() {
- document.removeEventListener('scroll', this.handleScroll);
- },
- methods: {
- ...mapActions('diffs', ['setInlineDiffViewType', 'setParallelDiffViewType', 'expandAllFiles']),
- pluralize,
- handleScroll() {
- if (!this.updating) {
- this.$nextTick(this.updateIsStuck);
- this.updating = true;
- }
- },
- updateIsStuck() {
- if (!this.$refs.wrapper) {
- return;
- }
-
- const scrollPosition = window.scrollY;
-
- this.isStuck = scrollPosition + this.offsetTop >= this.$refs.placeholder.offsetTop;
- this.updating = false;
- },
- sumValues(key) {
- return this.diffFiles.reduce((total, file) => total + file[key], 0);
- },
- },
-};
-</script>
-
-<template>
- <span>
- <div ref="placeholder"></div>
- <div
- ref="wrapper"
- :style="{ top }"
- :class="{'is-stuck': isStuck}"
- class="content-block oneline-block diff-files-changed diff-files-changed-merge-request
- files-changed js-diff-files-changed"
- >
- <div class="files-changed-inner">
- <div
- class="inline-parallel-buttons d-none d-md-block"
- >
- <a
- v-if="areAllFilesCollapsed"
- class="btn btn-default"
- @click="expandAllFiles"
- >
- {{ __('Expand all') }}
- </a>
- <a
- :href="toggleWhitespacePath"
- class="btn btn-default"
- >
- {{ toggleWhitespaceText }}
- </a>
- <div class="btn-group">
- <button
- id="inline-diff-btn"
- :class="{ active: isInlineView }"
- type="button"
- class="btn js-inline-diff-button"
- data-view-type="inline"
- @click="setInlineDiffViewType"
- >
- {{ __('Inline') }}
- </button>
- <button
- id="parallel-diff-btn"
- :class="{ active: isParallelView }"
- type="button"
- class="btn js-parallel-diff-button"
- data-view-type="parallel"
- @click="setParallelDiffViewType"
- >
- {{ __('Side-by-side') }}
- </button>
- </div>
- </div>
-
- <div class="commit-stat-summary dropdown">
- <changed-files-dropdown
- :diff-files="diffFiles"
- />
-
- <span
- class="js-diff-stats-additions-deletions-expanded
- diff-stats-additions-deletions-expanded"
- >
- with
- <strong class="cgreen">
- {{ pluralize(`${sumAddedLines} addition`, sumAddedLines) }}
- </strong>
- and
- <strong class="cred">
- {{ pluralize(`${sumRemovedLines} deletion`, sumRemovedLines) }}
- </strong>
- </span>
- <div
- class="js-diff-stats-additions-deletions-collapsed
- diff-stats-additions-deletions-collapsed float-right d-sm-none"
- >
- <strong class="cgreen">
- +{{ sumAddedLines }}
- </strong>
- <strong class="cred">
- -{{ sumRemovedLines }}
- </strong>
- </div>
- </div>
- </div>
- </div>
- </span>
-</template>
diff --git a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue b/app/assets/javascripts/diffs/components/changed_files_dropdown.vue
deleted file mode 100644
index 0ec6b8b7f21..00000000000
--- a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue
+++ /dev/null
@@ -1,126 +0,0 @@
-<script>
-import Icon from '~/vue_shared/components/icon.vue';
-import changedFilesMixin from '../mixins/changed_files';
-
-export default {
- components: {
- Icon,
- },
- mixins: [changedFilesMixin],
- data() {
- return {
- searchText: '',
- };
- },
- computed: {
- filteredDiffFiles() {
- return this.diffFiles.filter(file =>
- file.filePath.toLowerCase().includes(this.searchText.toLowerCase()),
- );
- },
- },
- methods: {
- clearSearch() {
- this.searchText = '';
- },
- },
-};
-</script>
-
-<template>
- <span>
- Showing
- <button
- class="diff-stats-summary-toggler"
- data-toggle="dropdown"
- type="button"
- aria-expanded="false"
- >
- <span>
- {{ n__('%d changed file', '%d changed files', diffFiles.length) }}
- </span>
- <icon
- class="caret-icon"
- name="chevron-down"
- />
- </button>
- <div class="dropdown-menu diff-file-changes">
- <div class="dropdown-input">
- <input
- v-model="searchText"
- type="search"
- class="dropdown-input-field"
- placeholder="Search files"
- autocomplete="off"
- />
- <i
- v-if="searchText.length === 0"
- aria-hidden="true"
- data-hidden="true"
- class="fa fa-search dropdown-input-search">
- </i>
- <i
- v-else
- role="button"
- class="fa fa-times dropdown-input-search"
- @click.stop.prevent="clearSearch"
- ></i>
- </div>
- <div class="dropdown-content">
- <ul>
- <li
- v-for="diffFile in filteredDiffFiles"
- :key="diffFile.name"
- >
- <a
- :href="`#${diffFile.fileHash}`"
- :title="diffFile.newPath"
- class="diff-changed-file"
- >
- <icon
- :name="fileChangedIcon(diffFile)"
- :size="16"
- :class="fileChangedClass(diffFile)"
- class="diff-file-changed-icon append-right-8"
- />
- <span class="diff-changed-file-content append-right-8">
- <strong
- v-if="diffFile.blob && diffFile.blob.name"
- class="diff-changed-file-name"
- >
- {{ diffFile.blob.name }}
- </strong>
- <strong
- v-else
- class="diff-changed-blank-file-name"
- >
- {{ s__('Diffs|No file name available') }}
- </strong>
- <span class="diff-changed-file-path prepend-top-5">
- {{ truncatedDiffPath(diffFile.blob.path) }}
- </span>
- </span>
- <span class="diff-changed-stats">
- <span class="cgreen">
- +{{ diffFile.addedLines }}
- </span>
- <span class="cred">
- -{{ diffFile.removedLines }}
- </span>
- </span>
- </a>
- </li>
-
- <li
- v-show="filteredDiffFiles.length === 0"
- class="dropdown-menu-empty-item"
- >
- <a>
- {{ __('No files found') }}
- </a>
- </li>
- </ul>
- </div>
- </div>
- </span>
-</template>
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index 5758588e82e..993206b2e73 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -5,6 +5,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import CIIcon from '~/vue_shared/components/ci_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
/**
* CommitItem
@@ -29,6 +30,7 @@ export default {
ClipboardButton,
CIIcon,
TimeAgoTooltip,
+ CommitPipelineStatus,
},
props: {
commit: {
@@ -102,6 +104,14 @@ export default {
></pre>
</div>
<div class="commit-actions flex-row d-none d-sm-flex">
+ <div
+ v-if="commit.signatureHtml"
+ v-html="commit.signatureHtml"
+ ></div>
+ <commit-pipeline-status
+ v-if="commit.pipelineStatusPath"
+ :endpoint="commit.pipelineStatusPath"
+ />
<div class="commit-sha-group">
<div
class="label label-monospace"
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index 1c9ad8e77f1..9bbf62c0eb6 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -1,9 +1,18 @@
<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import Tooltip from '@gitlab-org/gitlab-ui/dist/directives/tooltip';
+import { __ } from '~/locale';
+import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
+import Icon from '~/vue_shared/components/icon.vue';
import CompareVersionsDropdown from './compare_versions_dropdown.vue';
export default {
components: {
CompareVersionsDropdown,
+ Icon,
+ },
+ directives: {
+ Tooltip,
},
props: {
mergeRequestDiffs: {
@@ -26,30 +35,119 @@ export default {
},
},
computed: {
+ ...mapState('diffs', ['commit', 'showTreeList']),
+ ...mapGetters('diffs', ['isInlineView', 'isParallelView', 'areAllFilesCollapsed']),
comparableDiffs() {
return this.mergeRequestDiffs.slice(1);
},
+ isWhitespaceVisible() {
+ return !getParameterValues('w')[0];
+ },
+ toggleWhitespaceText() {
+ if (this.isWhitespaceVisible) {
+ return __('Hide whitespace changes');
+ }
+ return __('Show whitespace changes');
+ },
+ toggleWhitespacePath() {
+ if (this.isWhitespaceVisible) {
+ return mergeUrlParams({ w: 1 }, window.location.href);
+ }
+
+ return mergeUrlParams({ w: 0 }, window.location.href);
+ },
+ showDropdowns() {
+ return !this.commit && this.mergeRequestDiffs.length;
+ },
+ },
+ methods: {
+ ...mapActions('diffs', [
+ 'setInlineDiffViewType',
+ 'setParallelDiffViewType',
+ 'expandAllFiles',
+ 'toggleShowTreeList',
+ ]),
},
};
</script>
<template>
<div class="mr-version-controls">
- <div class="mr-version-menus-container content-block">
- Changes between
- <compare-versions-dropdown
- :other-versions="mergeRequestDiffs"
- :merge-request-version="mergeRequestDiff"
- :show-commit-count="true"
- class="mr-version-dropdown"
- />
- and
- <compare-versions-dropdown
- :other-versions="comparableDiffs"
- :start-version="startVersion"
- :target-branch="targetBranch"
- class="mr-version-compare-dropdown"
- />
+ <div
+ class="mr-version-menus-container content-block"
+ >
+ <button
+ v-tooltip.hover
+ type="button"
+ class="btn btn-default append-right-8 js-toggle-tree-list"
+ :class="{
+ active: showTreeList
+ }"
+ :title="__('Toggle file browser')"
+ @click="toggleShowTreeList"
+ >
+ <icon
+ name="hamburger"
+ />
+ </button>
+ <div
+ v-if="showDropdowns"
+ class="d-flex align-items-center compare-versions-container"
+ >
+ Changes between
+ <compare-versions-dropdown
+ :other-versions="mergeRequestDiffs"
+ :merge-request-version="mergeRequestDiff"
+ :show-commit-count="true"
+ class="mr-version-dropdown"
+ />
+ and
+ <compare-versions-dropdown
+ :other-versions="comparableDiffs"
+ :start-version="startVersion"
+ :target-branch="targetBranch"
+ class="mr-version-compare-dropdown"
+ />
+ </div>
+ <div
+ class="inline-parallel-buttons d-none d-md-flex ml-auto"
+ >
+ <a
+ v-if="areAllFilesCollapsed"
+ class="btn btn-default"
+ @click="expandAllFiles"
+ >
+ {{ __('Expand all') }}
+ </a>
+ <a
+ :href="toggleWhitespacePath"
+ class="btn btn-default"
+ >
+ {{ toggleWhitespaceText }}
+ </a>
+ <div class="btn-group prepend-left-8">
+ <button
+ id="inline-diff-btn"
+ :class="{ active: isInlineView }"
+ type="button"
+ class="btn js-inline-diff-button"
+ data-view-type="inline"
+ @click="setInlineDiffViewType"
+ >
+ {{ __('Inline') }}
+ </button>
+ <button
+ id="parallel-diff-btn"
+ :class="{ active: isParallelView }"
+ type="button"
+ class="btn js-parallel-diff-button"
+ data-view-type="parallel"
+ @click="setParallelDiffViewType"
+ >
+ {{ __('Side-by-side') }}
+ </button>
+ </div>
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
index 96cccb49378..c3acc352d5e 100644
--- a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
@@ -108,7 +108,7 @@ export default {
<template>
<span class="dropdown inline">
<a
- class="dropdown-toggle btn btn-default"
+ class="dropdown-menu-toggle btn btn-default w-100"
data-toggle="dropdown"
aria-expanded="false"
>
@@ -118,6 +118,7 @@ export default {
<Icon
:size="12"
name="angle-down"
+ class="position-absolute"
/>
</a>
<div class="dropdown-menu dropdown-select dropdown-menu-selectable">
@@ -163,3 +164,10 @@ export default {
</div>
</span>
</template>
+
+<style>
+.dropdown {
+ min-width: 0;
+ max-height: 170px;
+}
+</style>
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index bcbe374a90c..4e04e50c52a 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -1,5 +1,5 @@
<script>
-import { mapActions, mapGetters } from 'vuex';
+import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import { __, sprintf } from '~/locale';
import createFlash from '~/flash';
@@ -28,6 +28,7 @@ export default {
};
},
computed: {
+ ...mapState('diffs', ['currentDiffFileId']),
...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']),
isCollapsed() {
return this.file.collapsed || false;
@@ -101,6 +102,9 @@ export default {
<template>
<div
:id="file.fileHash"
+ :class="{
+ 'is-active': currentDiffFileId === file.fileHash
+ }"
class="diff-file file-holder"
>
<diff-file-header
@@ -168,3 +172,20 @@ export default {
</div>
</div>
</template>
+
+<style>
+@keyframes shadow-fade {
+ from {
+ box-shadow: 0 0 4px #919191;
+ }
+
+ to {
+ box-shadow: 0 0 0 #dfdfdf;
+ }
+}
+
+.diff-file.is-active {
+ box-shadow: 0 0 0 #dfdfdf;
+ animation: shadow-fade 1.2s 0.1s 1;
+}
+</style>
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 517fbf400e8..15b37243030 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -166,18 +166,16 @@ export default {
:title="diffFile.oldPath"
class="file-title-name"
data-container="body"
- >
- {{ diffFile.oldPath }}
- </strong>
+ v-html="diffFile.oldPathHtml"
+ ></strong>
<strong
v-tooltip
:title="diffFile.newPath"
class="file-title-name"
data-container="body"
- >
- {{ diffFile.newPath }}
- </strong>
+ v-html="diffFile.newPathHtml"
+ ></strong>
</span>
<strong
diff --git a/app/assets/javascripts/diffs/components/file_row_stats.vue b/app/assets/javascripts/diffs/components/file_row_stats.vue
new file mode 100644
index 00000000000..105f7ebdbed
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/file_row_stats.vue
@@ -0,0 +1,30 @@
+<script>
+export default {
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <span
+ v-once
+ class="file-row-stats"
+ >
+ <span class="cgreen">
+ +{{ file.addedLines }}
+ </span>
+ <span class="cred">
+ -{{ file.removedLines }}
+ </span>
+ </span>
+</template>
+
+<style>
+.file-row-stats {
+ font-size: 12px;
+}
+</style>
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
new file mode 100644
index 00000000000..cfe4273742f
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -0,0 +1,101 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import FileRow from '~/vue_shared/components/file_row.vue';
+import FileRowStats from './file_row_stats.vue';
+
+export default {
+ components: {
+ Icon,
+ FileRow,
+ },
+ data() {
+ return {
+ search: '',
+ };
+ },
+ computed: {
+ ...mapState('diffs', ['tree', 'addedLines', 'removedLines']),
+ ...mapGetters('diffs', ['allBlobs', 'diffFilesLength']),
+ filteredTreeList() {
+ const search = this.search.toLowerCase().trim();
+
+ if (search === '') return this.tree;
+
+ return this.allBlobs.filter(f => f.name.toLowerCase().indexOf(search) >= 0);
+ },
+ },
+ methods: {
+ ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']),
+ clearSearch() {
+ this.search = '';
+ },
+ },
+ FileRowStats,
+};
+</script>
+
+<template>
+ <div class="tree-list-holder d-flex flex-column">
+ <div class="append-bottom-8 position-relative tree-list-search">
+ <icon
+ name="search"
+ class="position-absolute tree-list-icon"
+ />
+ <input
+ v-model="search"
+ :placeholder="s__('MergeRequest|Filter files')"
+ type="search"
+ class="form-control"
+ />
+ <button
+ v-show="search"
+ :aria-label="__('Clear search')"
+ type="button"
+ class="position-absolute tree-list-icon tree-list-clear-icon border-0 p-0"
+ @click="clearSearch"
+ >
+ <icon
+ name="close"
+ />
+ </button>
+ </div>
+ <div
+ class="tree-list-scroll"
+ >
+ <template v-if="filteredTreeList.length">
+ <file-row
+ v-for="file in filteredTreeList"
+ :key="file.key"
+ :file="file"
+ :level="0"
+ :hide-extra-on-tree="true"
+ :extra-component="$options.FileRowStats"
+ :show-changed-icon="true"
+ @toggleTreeOpen="toggleTreeOpen"
+ @clickFile="scrollToFile"
+ />
+ </template>
+ <p
+ v-else
+ class="prepend-top-20 append-bottom-20 text-center"
+ >
+ {{ s__('MergeRequest|No files found') }}
+ </p>
+ </div>
+ <div
+ v-once
+ class="pt-3 pb-3 text-center"
+ >
+ {{ n__('%d changed file', '%d changed files', diffFilesLength) }}
+ <div>
+ <span class="cgreen">
+ {{ n__('%d addition', '%d additions', addedLines) }}
+ </span>
+ <span class="cred">
+ {{ n__('%d deleted', '%d deletions', removedLines) }}
+ </span>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index 2795dddfc48..6a50d2c1426 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -29,3 +29,5 @@ export const LENGTH_OF_AVATAR_TOOLTIP = 17;
export const LINES_TO_BE_RENDERED_DIRECTLY = 100;
export const MAX_LINES_TO_BE_RENDERED = 2000;
+
+export const MR_TREE_SHOW_KEY = 'mr_tree_show';
diff --git a/app/assets/javascripts/diffs/mixins/changed_files.js b/app/assets/javascripts/diffs/mixins/changed_files.js
deleted file mode 100644
index da1339f0ffa..00000000000
--- a/app/assets/javascripts/diffs/mixins/changed_files.js
+++ /dev/null
@@ -1,38 +0,0 @@
-export default {
- props: {
- diffFiles: {
- type: Array,
- required: true,
- },
- },
- methods: {
- fileChangedIcon(diffFile) {
- if (diffFile.deletedFile) {
- return 'file-deletion';
- } else if (diffFile.newFile) {
- return 'file-addition';
- }
- return 'file-modified';
- },
- fileChangedClass(diffFile) {
- if (diffFile.deletedFile) {
- return 'cred';
- } else if (diffFile.newFile) {
- return 'cgreen';
- }
-
- return '';
- },
- truncatedDiffPath(path) {
- const maxLength = 60;
-
- if (path.length > maxLength) {
- const start = path.length - maxLength;
- const end = start + maxLength;
- return `...${path.slice(start, end)}`;
- }
-
- return path;
- },
- },
-};
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 98d8d5943f9..1e0b27b538d 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -12,6 +12,7 @@ import {
PARALLEL_DIFF_VIEW_TYPE,
INLINE_DIFF_VIEW_TYPE,
DIFF_VIEW_COOKIE_NAME,
+ MR_TREE_SHOW_KEY,
} from '../constants';
export const setBaseConfig = ({ commit }, options) => {
@@ -195,5 +196,23 @@ export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => {
.catch(() => createFlash(s__('MergeRequests|Saving the comment failed')));
};
+export const toggleTreeOpen = ({ commit }, path) => {
+ commit(types.TOGGLE_FOLDER_OPEN, path);
+};
+
+export const scrollToFile = ({ state, commit }, path) => {
+ const { fileHash } = state.treeEntries[path];
+ document.location.hash = fileHash;
+
+ commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
+
+ setTimeout(() => commit(types.UPDATE_CURRENT_DIFF_FILE_ID, ''), 1000);
+};
+
+export const toggleShowTreeList = ({ commit, state }) => {
+ commit(types.TOGGLE_SHOW_TREE_LIST);
+ localStorage.setItem(MR_TREE_SHOW_KEY, state.showTreeList);
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index 968ba3c5e13..d4c205882ff 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -110,5 +110,9 @@ export const shouldRenderInlineCommentRow = state => line => {
export const getDiffFileByHash = state => fileHash =>
state.diffFiles.find(file => file.fileHash === fileHash);
+export const allBlobs = state => Object.values(state.treeEntries).filter(f => f.type === 'blob');
+
+export const diffFilesLength = state => state.diffFiles.length;
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
index eb596b251c1..ae8930c8968 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -1,10 +1,11 @@
import Cookies from 'js-cookie';
import { getParameterValues } from '~/lib/utils/url_utility';
-import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME } from '../../constants';
+import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME, MR_TREE_SHOW_KEY } from '../../constants';
const viewTypeFromQueryString = getParameterValues('view')[0];
const viewTypeFromCookie = Cookies.get(DIFF_VIEW_COOKIE_NAME);
const defaultViewType = INLINE_DIFF_VIEW_TYPE;
+const storedTreeShow = localStorage.getItem(MR_TREE_SHOW_KEY);
export default () => ({
isLoading: true,
@@ -17,4 +18,8 @@ export default () => ({
mergeRequestDiff: null,
diffLineCommentForms: {},
diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType,
+ tree: [],
+ treeEntries: {},
+ showTreeList: storedTreeShow === null ? true : storedTreeShow === 'true',
+ currentDiffFileId: '',
});
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index f61efbe6e1e..6474ee628e2 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -11,3 +11,6 @@ export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES';
export const RENDER_FILE = 'RENDER_FILE';
export const SET_LINE_DISCUSSIONS_FOR_FILE = 'SET_LINE_DISCUSSIONS_FOR_FILE';
export const REMOVE_LINE_DISCUSSIONS_FOR_FILE = 'REMOVE_LINE_DISCUSSIONS_FOR_FILE';
+export const TOGGLE_FOLDER_OPEN = 'TOGGLE_FOLDER_OPEN';
+export const TOGGLE_SHOW_TREE_LIST = 'TOGGLE_SHOW_TREE_LIST';
+export const UPDATE_CURRENT_DIFF_FILE_ID = 'UPDATE_CURRENT_DIFF_FILE_ID';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 59a2c09e54f..0b4485ecdb5 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { sortTree } from '~/ide/stores/utils';
import {
findDiffFile,
addLineReferences,
@@ -7,6 +8,7 @@ import {
addContextLines,
prepareDiffData,
isDiscussionApplicableToLine,
+ generateTreeList,
} from './utils';
import * as types from './mutation_types';
@@ -23,9 +25,12 @@ export default {
[types.SET_DIFF_DATA](state, data) {
const diffData = convertObjectPropsToCamelCase(data, { deep: true });
prepareDiffData(diffData);
+ const { tree, treeEntries } = generateTreeList(diffData.diffFiles);
Object.assign(state, {
...diffData,
+ tree: sortTree(tree),
+ treeEntries,
});
},
@@ -163,4 +168,13 @@ export default {
}
}
},
+ [types.TOGGLE_FOLDER_OPEN](state, path) {
+ state.treeEntries[path].opened = !state.treeEntries[path].opened;
+ },
+ [types.TOGGLE_SHOW_TREE_LIST](state) {
+ state.showTreeList = !state.showTreeList;
+ },
+ [types.UPDATE_CURRENT_DIFF_FILE_ID](state, fileId) {
+ state.currentDiffFileId = fileId;
+ },
};
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index 631e3de311e..c39403f1021 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -244,6 +244,7 @@ export function getDiffPositionByLineCode(diffFiles) {
oldLine,
newLine,
lineCode,
+ positionType: 'text',
};
}
});
@@ -259,11 +260,57 @@ export function isDiscussionApplicableToLine({ discussion, diffPosition, latestD
const { lineCode, ...diffPositionCopy } = diffPosition;
if (discussion.original_position && discussion.position) {
- const originalRefs = convertObjectPropsToCamelCase(discussion.original_position.formatter);
- const refs = convertObjectPropsToCamelCase(discussion.position.formatter);
+ const originalRefs = convertObjectPropsToCamelCase(discussion.original_position);
+ const refs = convertObjectPropsToCamelCase(discussion.position);
return _.isEqual(refs, diffPositionCopy) || _.isEqual(originalRefs, diffPositionCopy);
}
return latestDiff && discussion.active && lineCode === discussion.line_code;
}
+
+export const generateTreeList = files =>
+ files.reduce(
+ (acc, file) => {
+ const { fileHash, addedLines, removedLines, newFile, deletedFile, newPath } = file;
+ const split = newPath.split('/');
+
+ split.forEach((name, i) => {
+ const parent = acc.treeEntries[split.slice(0, i).join('/')];
+ const path = `${parent ? `${parent.path}/` : ''}${name}`;
+
+ if (!acc.treeEntries[path]) {
+ const type = path === newPath ? 'blob' : 'tree';
+ acc.treeEntries[path] = {
+ key: path,
+ path,
+ name,
+ type,
+ tree: [],
+ };
+
+ const entry = acc.treeEntries[path];
+
+ if (type === 'blob') {
+ Object.assign(entry, {
+ changed: true,
+ tempFile: newFile,
+ deleted: deletedFile,
+ fileHash,
+ addedLines,
+ removedLines,
+ });
+ } else {
+ Object.assign(entry, {
+ opened: true,
+ });
+ }
+
+ (parent ? parent.tree : acc.tree).push(entry);
+ }
+ });
+
+ return acc;
+ },
+ { treeEntries: {}, tree: [] },
+ );