Commit 8ea0444f authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab-ce master

parents 2e7f0316 e37abfbb
...@@ -9,7 +9,7 @@ See [the general developer security release guidelines](https://gitlab.com/gitla ...@@ -9,7 +9,7 @@ See [the general developer security release guidelines](https://gitlab.com/gitla
<!-- Mention the issue(s) this MR is related to --> <!-- Mention the issue(s) this MR is related to -->
## Author's checklist ## Developer checklist
- [ ] Link to the developer security workflow issue on `dev.gitlab.org` - [ ] Link to the developer security workflow issue on `dev.gitlab.org`
- [ ] MR targets `master` or `security-X-Y` for backports - [ ] MR targets `master` or `security-X-Y` for backports
...@@ -20,7 +20,7 @@ See [the general developer security release guidelines](https://gitlab.com/gitla ...@@ -20,7 +20,7 @@ See [the general developer security release guidelines](https://gitlab.com/gitla
- [ ] Add a link to an EE MR if required - [ ] Add a link to an EE MR if required
- [ ] Assign to a reviewer - [ ] Assign to a reviewer
## Reviewers checklist ## Reviewer checklist
- [ ] Correct milestone is applied and the title is matching across all backports - [ ] Correct milestone is applied and the title is matching across all backports
- [ ] Assigned to `@gitlab-release-tools-bot` with passing CI pipelines - [ ] Assigned to `@gitlab-release-tools-bot` with passing CI pipelines
......
...@@ -129,6 +129,10 @@ export default { ...@@ -129,6 +129,10 @@ export default {
created() { created() {
this.adjustView(); this.adjustView();
eventHub.$once('fetchedNotesData', this.setDiscussions); eventHub.$once('fetchedNotesData', this.setDiscussions);
eventHub.$once('fetchDiffData', this.fetchData);
},
beforeDestroy() {
eventHub.$off('fetchDiffData', this.fetchData);
}, },
methods: { methods: {
...mapActions(['startTaskList']), ...mapActions(['startTaskList']),
......
...@@ -13,39 +13,17 @@ export default { ...@@ -13,39 +13,17 @@ export default {
Icon, Icon,
FileRow, FileRow,
}, },
data() {
return {
search: '',
};
},
computed: { computed: {
...mapState('diffs', ['tree', 'addedLines', 'removedLines', 'renderTreeList']), ...mapState('diffs', ['tree', 'addedLines', 'removedLines', 'renderTreeList']),
...mapGetters('diffs', ['allBlobs', 'diffFilesLength']), ...mapGetters('diffs', ['allBlobs', 'diffFilesLength']),
filteredTreeList() { filteredTreeList() {
const search = this.search.toLowerCase().trim(); return this.renderTreeList ? this.tree : this.allBlobs;
if (search === '') return this.renderTreeList ? this.tree : this.allBlobs;
return this.allBlobs.reduce((acc, folder) => {
const tree = folder.tree.filter(f => f.path.toLowerCase().indexOf(search) >= 0);
if (tree.length) {
return acc.concat({
...folder,
tree,
});
}
return acc;
}, []);
}, },
}, },
methods: { methods: {
...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']), ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile', 'toggleFileFinder']),
clearSearch() {
this.search = '';
},
}, },
shortcutKeyCharacter: `${/Mac/i.test(navigator.userAgent) ? '&#8984;' : 'Ctrl'}+P`,
FileRowStats, FileRowStats,
}; };
</script> </script>
...@@ -55,21 +33,17 @@ export default { ...@@ -55,21 +33,17 @@ export default {
<div class="append-bottom-8 position-relative tree-list-search d-flex"> <div class="append-bottom-8 position-relative tree-list-search d-flex">
<div class="flex-fill d-flex"> <div class="flex-fill d-flex">
<icon name="search" class="position-absolute tree-list-icon" /> <icon name="search" class="position-absolute tree-list-icon" />
<input
v-model="search"
:placeholder="s__('MergeRequest|Filter files')"
type="search"
class="form-control"
/>
<button <button
v-show="search"
:aria-label="__('Clear search')"
type="button" type="button"
class="position-absolute bg-transparent tree-list-icon tree-list-clear-icon border-0 p-0" class="form-control text-left text-secondary"
@click="clearSearch" @click="toggleFileFinder(true)"
> >
<icon name="close" /> {{ s__('MergeRequest|Search files') }}
</button> </button>
<span
class="position-absolute text-secondary diff-tree-search-shortcut"
v-html="$options.shortcutKeyCharacter"
></span>
</div> </div>
</div> </div>
<div :class="{ 'pt-0 tree-list-blobs': !renderTreeList }" class="tree-list-scroll"> <div :class="{ 'pt-0 tree-list-blobs': !renderTreeList }" class="tree-list-scroll">
...@@ -104,4 +78,15 @@ export default { ...@@ -104,4 +78,15 @@ export default {
.tree-list-blobs .file-row-name { .tree-list-blobs .file-row-name {
margin-left: 12px; margin-left: 12px;
} }
.diff-tree-search-shortcut {
top: 50%;
right: 10px;
transform: translateY(-50%);
pointer-events: none;
}
.tree-list-icon {
pointer-events: none;
}
</style> </style>
import Vue from 'vue'; import Vue from 'vue';
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import { getParameterValues } from '~/lib/utils/url_utility'; import { getParameterValues } from '~/lib/utils/url_utility';
import FindFile from '~/vue_shared/components/file_finder/index.vue';
import eventHub from '../notes/event_hub';
import diffsApp from './components/app.vue'; import diffsApp from './components/app.vue';
import { TREE_LIST_STORAGE_KEY } from './constants'; import { TREE_LIST_STORAGE_KEY } from './constants';
export default function initDiffsApp(store) { export default function initDiffsApp(store) {
const fileFinderEl = document.getElementById('js-diff-file-finder');
if (fileFinderEl) {
// eslint-disable-next-line no-new
new Vue({
el: fileFinderEl,
store,
computed: {
...mapState('diffs', ['fileFinderVisible', 'isLoading']),
...mapGetters('diffs', ['flatBlobsList']),
},
watch: {
fileFinderVisible(newVal, oldVal) {
if (newVal && !oldVal && !this.flatBlobsList.length) {
eventHub.$emit('fetchDiffData');
}
},
},
methods: {
...mapActions('diffs', ['toggleFileFinder', 'scrollToFile']),
openFile(file) {
window.mrTabs.tabShown('diffs');
this.scrollToFile(file.path);
},
},
render(createElement) {
return createElement(FindFile, {
props: {
files: this.flatBlobsList,
visible: this.fileFinderVisible,
loading: this.isLoading,
showDiffStats: true,
clearSearchOnClose: false,
},
on: {
toggle: this.toggleFileFinder,
click: this.openFile,
},
class: ['diff-file-finder'],
style: {
display: this.fileFinderVisible ? '' : 'none',
},
});
},
});
}
return new Vue({ return new Vue({
el: '#js-diffs-app', el: '#js-diffs-app',
name: 'MergeRequestDiffs', name: 'MergeRequestDiffs',
......
...@@ -296,5 +296,9 @@ export const setShowWhitespace = ({ commit }, { showWhitespace, pushState = fals ...@@ -296,5 +296,9 @@ export const setShowWhitespace = ({ commit }, { showWhitespace, pushState = fals
} }
}; };
export const toggleFileFinder = ({ commit }, visible) => {
commit(types.TOGGLE_FILE_FINDER_VISIBLE, visible);
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -74,24 +74,25 @@ export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) = ...@@ -74,24 +74,25 @@ export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) =
export const getDiffFileByHash = state => fileHash => export const getDiffFileByHash = state => fileHash =>
state.diffFiles.find(file => file.file_hash === fileHash); state.diffFiles.find(file => file.file_hash === fileHash);
export const allBlobs = state => export const flatBlobsList = state =>
Object.values(state.treeEntries) Object.values(state.treeEntries).filter(f => f.type === 'blob');
.filter(f => f.type === 'blob')
.reduce((acc, file) => { export const allBlobs = (state, getters) =>
const { parentPath } = file; getters.flatBlobsList.reduce((acc, file) => {
const { parentPath } = file;
if (parentPath && !acc.some(f => f.path === parentPath)) {
acc.push({ if (parentPath && !acc.some(f => f.path === parentPath)) {
path: parentPath, acc.push({
isHeader: true, path: parentPath,
tree: [], isHeader: true,
}); tree: [],
} });
}
acc.find(f => f.path === parentPath).tree.push(file);
acc.find(f => f.path === parentPath).tree.push(file);
return acc;
}, []); return acc;
}, []);
export const diffFilesLength = state => state.diffFiles.length; export const diffFilesLength = state => state.diffFiles.length;
......
...@@ -29,4 +29,5 @@ export default () => ({ ...@@ -29,4 +29,5 @@ export default () => ({
highlightedRow: null, highlightedRow: null,
renderTreeList: true, renderTreeList: true,
showWhitespace: true, showWhitespace: true,
fileFinderVisible: false,
}); });
...@@ -22,3 +22,4 @@ export const SET_HIGHLIGHTED_ROW = 'SET_HIGHLIGHTED_ROW'; ...@@ -22,3 +22,4 @@ export const SET_HIGHLIGHTED_ROW = 'SET_HIGHLIGHTED_ROW';
export const SET_TREE_DATA = 'SET_TREE_DATA'; export const SET_TREE_DATA = 'SET_TREE_DATA';
export const SET_RENDER_TREE_LIST = 'SET_RENDER_TREE_LIST'; export const SET_RENDER_TREE_LIST = 'SET_RENDER_TREE_LIST';
export const SET_SHOW_WHITESPACE = 'SET_SHOW_WHITESPACE'; export const SET_SHOW_WHITESPACE = 'SET_SHOW_WHITESPACE';
export const TOGGLE_FILE_FINDER_VISIBLE = 'TOGGLE_FILE_FINDER_VISIBLE';
...@@ -244,4 +244,7 @@ export default { ...@@ -244,4 +244,7 @@ export default {
[types.SET_SHOW_WHITESPACE](state, showWhitespace) { [types.SET_SHOW_WHITESPACE](state, showWhitespace) {
state.showWhitespace = showWhitespace; state.showWhitespace = showWhitespace;
}, },
[types.TOGGLE_FILE_FINDER_VISIBLE](state, visible) {
state.fileFinderVisible = visible;
},
}; };
<script> <script>
import Vue from 'vue'; import Vue from 'vue';
import Mousetrap from 'mousetrap';
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import { __ } from '~/locale'; import { __ } from '~/locale';
import FindFile from '~/vue_shared/components/file_finder/index.vue';
import NewModal from './new_dropdown/modal.vue'; import NewModal from './new_dropdown/modal.vue';
import IdeSidebar from './ide_side_bar.vue'; import IdeSidebar from './ide_side_bar.vue';
import RepoTabs from './repo_tabs.vue'; import RepoTabs from './repo_tabs.vue';
import IdeStatusBar from './ide_status_bar.vue'; import IdeStatusBar from './ide_status_bar.vue';
import RepoEditor from './repo_editor.vue'; import RepoEditor from './repo_editor.vue';
import FindFile from './file_finder/index.vue';
import RightPane from './panes/right.vue'; import RightPane from './panes/right.vue';
import ErrorMessage from './error_message.vue'; import ErrorMessage from './error_message.vue';
import CommitEditorHeader from './commit_sidebar/editor_header.vue'; import CommitEditorHeader from './commit_sidebar/editor_header.vue';
const originalStopCallback = Mousetrap.stopCallback;
export default { export default {
components: { components: {
NewModal, NewModal,
...@@ -42,21 +39,18 @@ export default { ...@@ -42,21 +39,18 @@ export default {
'emptyStateSvgPath', 'emptyStateSvgPath',
'currentProjectId', 'currentProjectId',
'errorMessage', 'errorMessage',
'loading',
]),
...mapGetters([
'activeFile',
'hasChanges',
'someUncommittedChanges',
'isCommitModeActive',
'allBlobs',
]), ]),
...mapGetters(['activeFile', 'hasChanges', 'someUncommittedChanges', 'isCommitModeActive']),
}, },
mounted() { mounted() {
window.onbeforeunload = e => this.onBeforeUnload(e); window.onbeforeunload = e => this.onBeforeUnload(e);
Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
if (e.preventDefault) {
e.preventDefault();
}
this.toggleFileFinder(!this.fileFindVisible);
});
Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
}, },
methods: { methods: {
...mapActions(['toggleFileFinder']), ...mapActions(['toggleFileFinder']),
...@@ -70,17 +64,8 @@ export default { ...@@ -70,17 +64,8 @@ export default {
}); });
return returnValue; return returnValue;
}, },
mousetrapStopCallback(e, el, combo) { openFile(file) {
if ( this.$router.push(`/project${file.url}`);
(combo === 't' && el.classList.contains('dropdown-input-field')) ||
el.classList.contains('inputarea')
) {
return true;
} else if (combo === 'command+p' || combo === 'ctrl+p') {
return false;
}
return originalStopCallback(e, el, combo);
}, },
}, },
}; };
...@@ -90,7 +75,14 @@ export default { ...@@ -90,7 +75,14 @@ export default {
<article class="ide position-relative d-flex flex-column align-items-stretch"> <article class="ide position-relative d-flex flex-column align-items-stretch">
<error-message v-if="errorMessage" :message="errorMessage" /> <error-message v-if="errorMessage" :message="errorMessage" />
<div class="ide-view flex-grow d-flex"> <div class="ide-view flex-grow d-flex">
<find-file v-show="fileFindVisible" /> <find-file
v-show="fileFindVisible"
:files="allBlobs"
:visible="fileFindVisible"
:loading="loading"
@toggle="toggleFileFinder"
@click="openFile"
/>
<ide-sidebar /> <ide-sidebar />
<div class="multi-file-edit-pane"> <div class="multi-file-edit-pane">
<template v-if="activeFile"> <template v-if="activeFile">
......
// Fuzzy file finder
export const MAX_FILE_FINDER_RESULTS = 40;
export const FILE_FINDER_ROW_HEIGHT = 55;
export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
export const MAX_WINDOW_HEIGHT_COMPACT = 750; export const MAX_WINDOW_HEIGHT_COMPACT = 750;
// Commit message textarea // Commit message textarea
......
<script> <script>
import { mapActions, mapGetters, mapState } from 'vuex';
import fuzzaldrinPlus from 'fuzzaldrin-plus'; import fuzzaldrinPlus from 'fuzzaldrin-plus';
import Mousetrap from 'mousetrap';
import VirtualList from 'vue-virtual-scroll-list'; import VirtualList from 'vue-virtual-scroll-list';
import Item from './item.vue'; import Item from './item.vue';
import router from '../../ide_router'; import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
import {
MAX_FILE_FINDER_RESULTS, export const MAX_FILE_FINDER_RESULTS = 40;
FILE_FINDER_ROW_HEIGHT, export const FILE_FINDER_ROW_HEIGHT = 55;
FILE_FINDER_EMPTY_ROW_HEIGHT, export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
} from '../../constants';
import { const originalStopCallback = Mousetrap.stopCallback;
UP_KEY_CODE,
DOWN_KEY_CODE,
ENTER_KEY_CODE,
ESC_KEY_CODE,
} from '../../../lib/utils/keycodes';
export default { export default {
components: { components: {
Item, Item,
VirtualList, VirtualList,
}, },
props: {
files: {
type: Array,
required: true,
},
visible: {
type: Boolean,
required: true,
},
loading: {
type: Boolean,
required: true,
},
showDiffStats: {
type: Boolean,
required: false,
default: false,
},
clearSearchOnClose: {
type: Boolean,
required: false,
default: true,
},
},
data() { data() {
return { return {
focusedIndex: 0, focusedIndex: -1,
searchText: '', searchText: '',
mouseOver: false, mouseOver: false,
cancelMouseOver: false, cancelMouseOver: false,
}; };
}, },
computed: { computed: {
...mapGetters(['allBlobs']),
...mapState(['fileFindVisible', 'loading']),
filteredBlobs() { filteredBlobs() {
const searchText = this.searchText.trim(); const searchText = this.searchText.trim();
if (searchText === '') { if (searchText === '') {
return this.allBlobs.slice(0, MAX_FILE_FINDER_RESULTS); return this.files.slice(0, MAX_FILE_FINDER_RESULTS);
} }
return fuzzaldrinPlus.filter(this.allBlobs, searchText, { return fuzzaldrinPlus.filter(this.files, searchText, {
key: 'path', key: 'path',
maxResults: MAX_FILE_FINDER_RESULTS, maxResults: MAX_FILE_FINDER_RESULTS,
}); });
...@@ -58,10 +75,12 @@ export default { ...@@ -58,10 +75,12 @@ export default {
}, },
}, },
watch: { watch: {
fileFindVisible() { visible() {
this.$nextTick(() => { this.$nextTick(() => {
if (!this.fileFindVisible) { if (!this.visible) {
this.searchText = ''; if (this.clearSearchOnClose) {
this.searchText = '';
}
} else { } else {
this.focusedIndex = 0; this.focusedIndex = 0;
...@@ -72,7 +91,11 @@ export default { ...@@ -72,7 +91,11 @@ export default {
}); });
}, },
searchText() { searchText() {
this.focusedIndex = 0; this.focusedIndex = -1;
this.$nextTick(() => {
this.focusedIndex = 0;
});
}, },
focusedIndex() { focusedIndex() {
if (!this.mouseOver) { if (!this.mouseOver) {
...@@ -98,8 +121,25 @@ export default { ...@@ -98,8 +121,25 @@ export default {
} }
}, },
}, },
mounted() {
if (this.files.length) {
this.focusedIndex = 0;
}
Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
if (e.preventDefault) {
e.preventDefault();
}
this.toggle(!this.visible);
});
Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
},
methods: { methods: {
...mapActions(['toggleFileFinder']), toggle(visible) {
this.$emit('toggle', visible);
},
clearSearchInput() { clearSearchInput() {
this.searchText = ''; this.searchText = '';
...@@ -139,15 +179,15 @@ export default { ...@@ -139,15 +179,15 @@ export default {
this.openFile(this.filteredBlobs[this.focusedIndex]); this.openFile(this.filteredBlobs[this.focusedIndex]);
break; break;
case ESC_KEY_CODE: case ESC_KEY_CODE:
this.toggleFileFinder(false); this.toggle(false);
break; break;
default: default:
break; break;
} }
}, },
openFile(file) { openFile(file) {
this.toggleFileFinder(false); this.toggle(false);
router.push(`/project${file.url}`); this.$emit('click', file);
}, },
onMouseOver(index) { onMouseOver(index) {
if (!this.cancelMouseOver) { if (!this.cancelMouseOver) {
...@@ -159,14 +199,26 @@ export default { ...@@ -159,14 +199,26 @@ export default {
this.cancelMouseOver = false; this.cancelMouseOver = false;
this.onMouseOver(index); this.onMouseOver(index);
}, },
mousetrapStopCallback(e, el, combo) {
if (
(combo === 't' && el.classList.contains('dropdown-input-field')) ||
el.classList.contains('inputarea')
) {
return true;
} else if (combo === 'command+p' || combo === 'ctrl+p') {
return false;
}
return originalStopCallback(e, el, combo);
},
}, },
}; };
</script> </script>
<template> <template>
<div class="ide-file-finder-overlay" @mousedown.self="toggleFileFinder(false)"> <div class="file-finder-overlay" @mousedown.self="toggle(false)">
<div class="dropdown-menu diff-file-changes ide-file-finder show"> <div class="dropdown-menu diff-file-changes file-finder show">
<div class="dropdown-input"> <div :class="{ 'has-value': showClearInputButton }" class="dropdown-input">
<input <input
ref="searchInput" ref="searchInput"
v-model="searchText" v-model="searchText"
...@@ -186,9 +238,6 @@ export default { ...@@ -186,9 +238,6 @@ export default {
></i> ></i>
<i <i
:aria-label="__('Clear search input')" :aria-label="__('Clear search input')"
:class="{
show: showClearInputButton,
}"
role="button" role="button"
class="fa fa-times dropdown-input-clear" class="fa fa-times dropdown-input-clear"
@click="clearSearchInput" @click="clearSearchInput"
...@@ -203,6 +252,7 @@ export default { ...@@ -203,6 +252,7 @@ export default {
:search-text="searchText" :search-text="searchText"
:focused="index === focusedIndex" :focused="index === focusedIndex"
:index="index" :index="index"
:show-diff-stats="showDiffStats"
class="disable-hover" class="disable-hover"
@click="openFile" @click="openFile"
@mouseover="onMouseOver" @mouseover="onMouseOver"
...@@ -225,3 +275,25 @@ export default { ...@@ -225,3 +275,25 @@ export default {
</div> </div>
</div> </div>
</template> </template>
<style scoped>
.file-finder-overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 200;
}
.file-finder {
top: 10px;
left: 50%;
transform: translateX(-50%);
}
.diff-file-changes {
top: 50px;
max-height: 327px;
}
</style>
<script> <script>
import fuzzaldrinPlus from 'fuzzaldrin-plus'; import fuzzaldrinPlus from 'fuzzaldrin-plus';
import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '../../../vue_shared/components/file_icon.vue'; import FileIcon from '../../../vue_shared/components/file_icon.vue';
import ChangedFileIcon from '../../../vue_shared/components/changed_file_icon.vue'; import ChangedFileIcon from '../../../vue_shared/components/changed_file_icon.vue';
...@@ -7,6 +8,7 @@ const MAX_PATH_LENGTH = 60; ...@@ -7,6 +8,7 @@ const MAX_PATH_LENGTH = 60;
export default { export default {
components: { components: {
Icon,
ChangedFileIcon, ChangedFileIcon,
FileIcon, FileIcon,
}, },
...@@ -27,6 +29,11 @@ export default { ...@@ -27,6 +29,11 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
showDiffStats: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
pathWithEllipsis() { pathWithEllipsis() {
...@@ -97,8 +104,23 @@ export default { ...@@ -97,8 +104,23 @@ export default {
</span> </span>
</span> </span>
</span> </span>
<span v-if="file.changed || file.tempFile" class="diff-changed-stats"> <span v-if="file.changed || file.tempFile" v-once class="diff-changed-stats">
<changed-file-icon :file="file" /> <span v-if="showDiffStats">
<span class="cgreen bold">
<icon name="file-addition" class="align-text-top" /> {{ file.addedLines }}
</span>
<span class="cred bold ml-1">
<icon name="file-deletion" class="align-text-top" /> {{ file.removedLines }}
</span>
</span>
<changed-file-icon v-else :file="file" />
</span> </span>
</button> </button>
</template> </template>
<style scoped>
.highlighted {
color: #1f78d1;
font-weight: 600;
}
</style>
...@@ -816,26 +816,6 @@ $ide-commit-header-height: 48px; ...@@ -816,26 +816,6 @@ $ide-commit-header-height: 48px;
z-index: 1; z-index: 1;
} }
.ide-file-finder-overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 100;
}
.ide-file-finder {
top: 10px;
left: 50%;
transform: translateX(-50%);
.highlighted {
color: $blue-500;
font-weight: $gl-font-weight-bold;
}
}
.ide-commit-message-field { .ide-commit-message-field {
height: 200px; height: 200px;
background-color: $white-light; background-color: $white-light;
......
...@@ -986,3 +986,9 @@ ...@@ -986,3 +986,9 @@
width: $ci-action-icon-size-lg; width: $ci-action-icon-size-lg;
} }
} }
.merge-request-details .file-finder-overlay.diff-file-finder {
position: fixed;
z-index: 99999;
background: $black-transparent;
}
...@@ -59,6 +59,7 @@ ...@@ -59,6 +59,7 @@
#js-vue-discussion-counter #js-vue-discussion-counter
.tab-content#diff-notes-app .tab-content#diff-notes-app
#js-diff-file-finder
#notes.notes.tab-pane.voting_notes #notes.notes.tab-pane.voting_notes
.row .row
%section.col-md-12 %section.col-md-12
......
---
title: Added fuzzy file finder to merge requests
merge_request:
author:
type: changed
...@@ -5800,10 +5800,10 @@ msgstr "" ...@@ -5800,10 +5800,10 @@ msgstr ""
msgid "MergeRequest| %{paragraphStart}changed the description %{descriptionChangedTimes} times %{timeDifferenceMinutes}%{paragraphEnd}" msgid "MergeRequest| %{paragraphStart}changed the description %{descriptionChangedTimes} times %{timeDifferenceMinutes}%{paragraphEnd}"
msgstr "" msgstr ""
msgid "MergeRequest|Filter files" msgid "MergeRequest|No files found"
msgstr "" msgstr ""
msgid "MergeRequest|No files found" msgid "MergeRequest|Search files"
msgstr "" msgstr ""
msgid "Merged" msgid "Merged"
......
...@@ -83,17 +83,6 @@ describe('Diffs tree list component', () => { ...@@ -83,17 +83,6 @@ describe('Diffs tree list component', () => {
expect(vm.$el.querySelectorAll('.file-row')[1].textContent).toContain('app'); expect(vm.$el.querySelectorAll('.file-row')[1].textContent).toContain('app');
}); });
it('filters tree list to blobs matching search', done => {
vm.search = 'app/index';
vm.$nextTick(() => {
expect(vm.$el.querySelectorAll('.file-row').length).toBe(1);
expect(vm.$el.querySelectorAll('.file-row')[0].textContent).toContain('index.js');
done();
});
});
it('calls toggleTreeOpen when clicking folder', () => { it('calls toggleTreeOpen when clicking folder', () => {
spyOn(vm.$store, 'dispatch').and.stub(); spyOn(vm.$store, 'dispatch').and.stub();
...@@ -130,14 +119,4 @@ describe('Diffs tree list component', () => { ...@@ -130,14 +119,4 @@ describe('Diffs tree list component', () => {
}); });
}); });
}); });
describe('clearSearch', () => {
it('resets search', () => {
vm.search = 'test';
vm.$el.querySelector('.tree-list-clear-icon').click();
expect(vm.search).toBe('');
});
});
}); });
...@@ -242,7 +242,11 @@ describe('Diffs Module Getters', () => { ...@@ -242,7 +242,11 @@ describe('Diffs Module Getters', () => {
}, },
}; };
expect(getters.allBlobs(localState)).toEqual([ expect(
getters.allBlobs(localState, {
flatBlobsList: getters.flatBlobsList(localState),
}),
).toEqual([
{ {
isHeader: true, isHeader: true,
path: '/', path: '/',
......
import Vue from 'vue'; import Vue from 'vue';
import Mousetrap from 'mousetrap';
import store from '~/ide/stores'; import store from '~/ide/stores';
import ide from '~/ide/components/ide.vue'; import ide from '~/ide/components/ide.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
...@@ -72,73 +71,6 @@ describe('ide component', () => { ...@@ -72,73 +71,6 @@ describe('ide component', () => {
}); });
}); });
describe('file finder', () => {
beforeEach(done => {
spyOn(vm, 'toggleFileFinder');
vm.$store.state.fileFindVisible = true;
vm.$nextTick(done);
});
it('calls toggleFileFinder on `t` key press', done => {
Mousetrap.trigger('t');
vm.$nextTick()
.then(() => {
expect(vm.toggleFileFinder).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
it('calls toggleFileFinder on `command+p` key press', done => {
Mousetrap.trigger('command+p');
vm.$nextTick()
.then(() => {
expect(vm.toggleFileFinder).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
it('calls toggleFileFinder on `ctrl+p` key press', done => {
Mousetrap.trigger('ctrl+p');
vm.$nextTick()
.then(() => {
expect(vm.toggleFileFinder).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
it('always allows `command+p` to trigger toggleFileFinder', () => {
expect(
vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'command+p'),
).toBe(false);
});
it('always allows `ctrl+p` to trigger toggleFileFinder', () => {
expect(
vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'ctrl+p'),
).toBe(false);
});
it('onlys handles `t` when focused in input-field', () => {
expect(
vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 't'),
).toBe(true);
});
it('stops callback in monaco editor', () => {
setFixtures('<div class="inputarea"></div>');
expect(vm.mousetrapStopCallback(null, document.querySelector('.inputarea'), 't')).toBe(true);
});
});
it('shows error message when set', done => { it('shows error message when set', done => {
expect(vm.$el.querySelector('.flash-container')).toBe(null); expect(vm.$el.querySelector('.flash-container')).toBe(null);
......
import Vue from 'vue'; import Vue from 'vue';
import store from '~/ide/stores'; import Mousetrap from 'mousetrap';
import FindFileComponent from '~/ide/components/file_finder/index.vue'; import FindFileComponent from '~/vue_shared/components/file_finder/index.vue';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
import router from '~/ide/ide_router'; import { file } from 'spec/ide/helpers';
import { file, resetStore } from '../../helpers'; import timeoutPromise from 'spec/helpers/set_timeout_promise_helper';
import { mountComponentWithStore } from '../../../helpers/vue_mount_component_helper';
describe('IDE File finder item spec', () => { describe('File finder item spec', () => {
const Component = Vue.extend(FindFileComponent); const Component = Vue.extend(FindFileComponent);
let vm; let vm;
beforeEach(done => { function createComponent(props) {
setFixtures('<div id="app"></div>'); vm = new Component({
propsData: {
vm = mountComponentWithStore(Component, { files: [],
store, visible: true,
el: '#app', loading: false,
props: { ...props,
index: 0,
}, },
}); });
setTimeout(done); vm.$mount('#app');
}
beforeEach(() => {
setFixtures('<div id="app"></div>');
}); });
afterEach(() => { afterEach(() => {
vm.$destroy(); vm.$destroy();
resetStore(vm.$store);
}); });
describe('with entries', () => { describe('with entries', () => {
beforeEach(done => { beforeEach(done => {
Vue.set(vm.$store.state.entries, 'folder', { createComponent({
...file('folder'), files: [
path: 'folder', {
type: 'folder', ...file('index.js'),
}); path: 'index.js',
type: 'blob',
Vue.set(vm.$store.state.entries, 'index.js', { url: '/index.jsurl',
...file('index.js'), },
path: 'index.js', {
type: 'blob', ...file('component.js'),
url: '/index.jsurl', path: 'component.js',
}); type: 'blob',
},
Vue.set(vm.$store.state.entries, 'component.js', { ],
...file('component.js'),
path: 'component.js',
type: 'blob',
}); });
setTimeout(done); setTimeout(done);
...@@ -56,13 +53,14 @@ describe('IDE File finder item spec', () => { ...@@ -56,13 +53,14 @@ describe('IDE File finder item spec', () => {
it('renders list of blobs', () => { it('renders list of blobs', () => {
expect(vm.$el.textContent).toContain('index.js'); expect(vm.$el.textContent).toContain('index.js');
expect(vm.$el.textContent).toContain('component.js');
expect(vm.$el.textContent).not.toContain('folder'); expect(vm.$el.textContent).not.toContain('folder');
}); });
it('filters entries', done => { it('filters entries', done => {
vm.searchText = 'index'; vm.searchText = 'index';
vm.$nextTick(() => { setTimeout(() => {
expect(vm.$el.textContent).toContain('index.js'); expect(vm.$el.textContent).toContain('index.js');
expect(vm.$el.textContent).not.toContain('component.js'); expect(vm.$el.textContent).not.toContain('component.js');
...@@ -73,8 +71,8 @@ describe('IDE File finder item spec', () => { ...@@ -73,8 +71,8 @@ describe('IDE File finder item spec', () => {
it('shows clear button when searchText is not empty', done => { it('shows clear button when searchText is not empty', done => {
vm.searchText = 'index'; vm.searchText = 'index';
vm.$nextTick(() => { setTimeout(() => {
expect(vm.$el.querySelector('.dropdown-input-clear').classList).toContain('show'); expect(vm.$el.querySelector('.dropdown-input').classList).toContain('has-value');
expect(vm.$el.querySelector('.dropdown-input-search').classList).toContain('hidden'); expect(vm.$el.querySelector('.dropdown-input-search').classList).toContain('hidden');
done(); done();
...@@ -84,11 +82,11 @@ describe('IDE File finder item spec', () => { ...@@ -84,11 +82,11 @@ describe('IDE File finder item spec', () => {
it('clear button resets searchText', done => { it('clear button resets searchText', done => {
vm.searchText = 'index'; vm.searchText = 'index';
vm.$nextTick() timeoutPromise()
.then(() => { .then(() => {
vm.$el.querySelector('.dropdown-input-clear').click(); vm.$el.querySelector('.dropdown-input-clear').click();
}) })
.then(vm.$nextTick) .then(timeoutPromise)
.then(() => { .then(() => {
expect(vm.searchText).toBe(''); expect(vm.searchText).toBe('');
}) })
...@@ -100,11 +98,11 @@ describe('IDE File finder item spec', () => { ...@@ -100,11 +98,11 @@ describe('IDE File finder item spec', () => {
spyOn(vm.$refs.searchInput, 'focus'); spyOn(vm.$refs.searchInput, 'focus');
vm.searchText = 'index'; vm.searchText = 'index';
vm.$nextTick() timeoutPromise()
.then(() => { .then(() => {
vm.$el.querySelector('.dropdown-input-clear').click(); vm.$el.querySelector('.dropdown-input-clear').click();
}) })
.then(vm.$nextTick) .then(timeoutPromise)
.then(() => { .then(() => {
expect(vm.$refs.searchInput.focus).toHaveBeenCalled(); expect(vm.$refs.searchInput.focus).toHaveBeenCalled();
}) })
...@@ -116,7 +114,7 @@ describe('IDE File finder item spec', () => { ...@@ -116,7 +114,7 @@ describe('IDE File finder item spec', () => {
it('returns 1 when no filtered entries exist', done => { it('returns 1 when no filtered entries exist', done => {
vm.searchText = 'testing 123'; vm.searchText = 'testing 123';
vm.$nextTick(() => { setTimeout(() => {
expect(vm.listShowCount).toBe(1); expect(vm.listShowCount).toBe(1);
done(); done();
...@@ -136,7 +134,7 @@ describe('IDE File finder item spec', () => { ...@@ -136,7 +134,7 @@ describe('IDE File finder item spec', () => {
it('returns 33 when entries dont exist', done => { it('returns 33 when entries dont exist', done => {
vm.searchText = 'testing 123'; vm.searchText = 'testing 123';
vm.$nextTick(() => { setTimeout(() => {
expect(vm.listHeight).toBe(33); expect(vm.listHeight).toBe(33);
done(); done();
...@@ -148,7 +146,7 @@ describe('IDE File finder item spec', () => { ...@@ -148,7 +146,7 @@ describe('IDE File finder item spec', () => {
it('returns length of filtered blobs', done => { it('returns length of filtered blobs', done => {
vm.searchText = 'index'; vm.searchText = 'index';
vm.$nextTick(() => { setTimeout(() => {
expect(vm.filteredBlobsLength).toBe(1); expect(vm.filteredBlobsLength).toBe(1);
done(); done();
...@@ -162,7 +160,7 @@ describe('IDE File finder item spec', () => { ...@@ -162,7 +160,7 @@ describe('IDE File finder item spec', () => {
vm.focusedIndex = 1; vm.focusedIndex = 1;
vm.searchText = 'test'; vm.searchText = 'test';
vm.$nextTick(() => { setTimeout(() => {
expect(vm.focusedIndex).toBe(0); expect(vm.focusedIndex).toBe(0);
done(); done();
...@@ -170,16 +168,16 @@ describe('IDE File finder item spec', () => { ...@@ -170,16 +168,16 @@ describe('IDE File finder item spec', () => {
}); });
}); });
describe('fileFindVisible', () => { describe('visible', () => {
it('returns searchText when false', done => { it('returns searchText when false', done => {
vm.searchText = 'test'; vm.searchText = 'test';
vm.$store.state.fileFindVisible = true; vm.visible = true;
vm.$nextTick() timeoutPromise()
.then(() => { .then(() => {
vm.$store.state.fileFindVisible = false; vm.visible = false;
}) })
.then(vm.$nextTick) .then(timeoutPromise)
.then(() => { .then(() => {
expect(vm.searchText).toBe(''); expect(vm.searchText).toBe('');
}) })
...@@ -191,20 +189,19 @@ describe('IDE File finder item spec', () => { ...@@ -191,20 +189,19 @@ describe('IDE File finder item spec', () => {
describe('openFile', () => { describe('openFile', () => {
beforeEach(() => { beforeEach(() => {
spyOn(router, 'push'); spyOn(vm, '$emit');
spyOn(vm, 'toggleFileFinder');
}); });
it('closes file finder', () => { it('closes file finder', () => {
vm.openFile(vm.$store.state.entries['index.js']); vm.openFile(vm.files[0]);
expect(vm.toggleFileFinder).toHaveBeenCalled(); expect(vm.$emit).toHaveBeenCalledWith('toggle', false);
}); });
it('pushes to router', () => { it('pushes to router', () => {
vm.openFile(vm.$store.state.entries['index.js']); vm.openFile(vm.files[0]);
expect(router.push).toHaveBeenCalledWith('/project/index.jsurl'); expect(vm.$emit).toHaveBeenCalledWith('click', vm.files[0]);
}); });
}); });
...@@ -217,8 +214,8 @@ describe('IDE File finder item spec', () => { ...@@ -217,8 +214,8 @@ describe('IDE File finder item spec', () => {
vm.$refs.searchInput.dispatchEvent(event); vm.$refs.searchInput.dispatchEvent(event);
vm.$nextTick(() => { setTimeout(() => {
expect(vm.openFile).toHaveBeenCalledWith(vm.$store.state.entries['index.js']); expect(vm.openFile).toHaveBeenCalledWith(vm.files[0]);
done(); done();
}); });
...@@ -228,12 +225,12 @@ describe('IDE File finder item spec', () => { ...@@ -228,12 +225,12 @@ describe('IDE File finder item spec', () => {
const event = new CustomEvent('keyup'); const event = new CustomEvent('keyup');
event.keyCode = ESC_KEY_CODE; event.keyCode = ESC_KEY_CODE;
spyOn(vm, 'toggleFileFinder'); spyOn(vm, '$emit');
vm.$refs.searchInput.dispatchEvent(event); vm.$refs.searchInput.dispatchEvent(event);
vm.$nextTick(() => { setTimeout(() => {
expect(vm.toggleFileFinder).toHaveBeenCalled(); expect(vm.$emit).toHaveBeenCalledWith('toggle', false);
done(); done();
}); });
...@@ -287,18 +284,85 @@ describe('IDE File finder item spec', () => { ...@@ -287,18 +284,85 @@ describe('IDE File finder item spec', () => {
}); });
describe('without entries', () => { describe('without entries', () => {
it('renders loading text when loading', done => { it('renders loading text when loading', () => {
store.state.loading = true; createComponent({
loading: true,
vm.$nextTick(() => {
expect(vm.$el.textContent).toContain('Loading...');
done();
}); });
expect(vm.$el.textContent).toContain('Loading...');
}); });
it('renders no files text', () => { it('renders no files text', () => {
createComponent();
expect(vm.$el.textContent).toContain('No files found.'); expect(vm.$el.textContent).toContain('No files found.');
}); });
}); });
describe('keyboard shortcuts', () => {
beforeEach(done => {
createComponent();
spyOn(vm, 'toggle');
vm.$nextTick(done);
});
it('calls toggle on `t` key press', done => {
Mousetrap.trigger('t');
vm.$nextTick()
.then(() => {
expect(vm.toggle).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
it('calls toggle on `command+p` key press', done => {
Mousetrap.trigger('command+p');
vm.$nextTick()
.then(() => {
expect(vm.toggle).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
it('calls toggle on `ctrl+p` key press', done => {
Mousetrap.trigger('ctrl+p');
vm.$nextTick()
.then(() => {
expect(vm.toggle).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
it('always allows `command+p` to trigger toggle', () => {
expect(
vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'command+p'),
).toBe(false);
});
it('always allows `ctrl+p` to trigger toggle', () => {
expect(
vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'ctrl+p'),
).toBe(false);
});
it('onlys handles `t` when focused in input-field', () => {
expect(
vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 't'),
).toBe(true);
});
it('stops callback in monaco editor', () => {
setFixtures('<div class="inputarea"></div>');
expect(vm.mousetrapStopCallback(null, document.querySelector('.inputarea'), 't')).toBe(true);
});
});
}); });
import Vue from 'vue'; import Vue from 'vue';
import ItemComponent from '~/ide/components/file_finder/item.vue'; import ItemComponent from '~/vue_shared/components/file_finder/item.vue';
import { file } from '../../helpers'; import { file } from 'spec/ide/helpers';
import createComponent from '../../../helpers/vue_mount_component_helper'; import createComponent from '../../../helpers/vue_mount_component_helper';
describe('IDE File finder item spec', () => { describe('File finder item spec', () => {
const Component = Vue.extend(ItemComponent); const Component = Vue.extend(ItemComponent);
let vm; let vm;
let localFile; let localFile;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment