Merge branch 'diff-file-finder' into 'master'
Added fuzzy file finder to merge requests Closes #53304 See merge request gitlab-org/gitlab-ce!24434
This commit is contained in:
commit
3bed077c57
22 changed files with 401 additions and 301 deletions
|
@ -129,6 +129,10 @@ export default {
|
|||
created() {
|
||||
this.adjustView();
|
||||
eventHub.$once('fetchedNotesData', this.setDiscussions);
|
||||
eventHub.$once('fetchDiffData', this.fetchData);
|
||||
},
|
||||
beforeDestroy() {
|
||||
eventHub.$off('fetchDiffData', this.fetchData);
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['startTaskList']),
|
||||
|
|
|
@ -13,39 +13,17 @@ export default {
|
|||
Icon,
|
||||
FileRow,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
search: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState('diffs', ['tree', 'addedLines', 'removedLines', 'renderTreeList']),
|
||||
...mapGetters('diffs', ['allBlobs', 'diffFilesLength']),
|
||||
filteredTreeList() {
|
||||
const search = this.search.toLowerCase().trim();
|
||||
|
||||
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;
|
||||
}, []);
|
||||
return this.renderTreeList ? this.tree : this.allBlobs;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']),
|
||||
clearSearch() {
|
||||
this.search = '';
|
||||
},
|
||||
...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile', 'toggleFileFinder']),
|
||||
},
|
||||
shortcutKeyCharacter: `${/Mac/i.test(navigator.userAgent) ? '⌘' : 'Ctrl'}+P`,
|
||||
FileRowStats,
|
||||
};
|
||||
</script>
|
||||
|
@ -55,21 +33,17 @@ export default {
|
|||
<div class="append-bottom-8 position-relative tree-list-search d-flex">
|
||||
<div class="flex-fill d-flex">
|
||||
<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 bg-transparent tree-list-icon tree-list-clear-icon border-0 p-0"
|
||||
@click="clearSearch"
|
||||
class="form-control text-left text-secondary"
|
||||
@click="toggleFileFinder(true)"
|
||||
>
|
||||
<icon name="close" />
|
||||
{{ s__('MergeRequest|Search files') }}
|
||||
</button>
|
||||
<span
|
||||
class="position-absolute text-secondary diff-tree-search-shortcut"
|
||||
v-html="$options.shortcutKeyCharacter"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="{ 'pt-0 tree-list-blobs': !renderTreeList }" class="tree-list-scroll">
|
||||
|
@ -104,4 +78,15 @@ export default {
|
|||
.tree-list-blobs .file-row-name {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.diff-tree-search-shortcut {
|
||||
top: 50%;
|
||||
right: 10px;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tree-list-icon {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,11 +1,60 @@
|
|||
import Vue from 'vue';
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import { mapActions, mapState, mapGetters } from 'vuex';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
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 { TREE_LIST_STORAGE_KEY } from './constants';
|
||||
|
||||
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({
|
||||
el: '#js-diffs-app',
|
||||
name: 'MergeRequestDiffs',
|
||||
|
|
|
@ -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
|
||||
export default () => {};
|
||||
|
|
|
@ -74,24 +74,25 @@ export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) =
|
|||
export const getDiffFileByHash = state => fileHash =>
|
||||
state.diffFiles.find(file => file.file_hash === fileHash);
|
||||
|
||||
export const allBlobs = state =>
|
||||
Object.values(state.treeEntries)
|
||||
.filter(f => f.type === 'blob')
|
||||
.reduce((acc, file) => {
|
||||
const { parentPath } = file;
|
||||
export const flatBlobsList = state =>
|
||||
Object.values(state.treeEntries).filter(f => f.type === 'blob');
|
||||
|
||||
if (parentPath && !acc.some(f => f.path === parentPath)) {
|
||||
acc.push({
|
||||
path: parentPath,
|
||||
isHeader: true,
|
||||
tree: [],
|
||||
});
|
||||
}
|
||||
export const allBlobs = (state, getters) =>
|
||||
getters.flatBlobsList.reduce((acc, file) => {
|
||||
const { parentPath } = file;
|
||||
|
||||
acc.find(f => f.path === parentPath).tree.push(file);
|
||||
if (parentPath && !acc.some(f => f.path === parentPath)) {
|
||||
acc.push({
|
||||
path: parentPath,
|
||||
isHeader: true,
|
||||
tree: [],
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
acc.find(f => f.path === parentPath).tree.push(file);
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
export const diffFilesLength = state => state.diffFiles.length;
|
||||
|
||||
|
|
|
@ -29,4 +29,5 @@ export default () => ({
|
|||
highlightedRow: null,
|
||||
renderTreeList: true,
|
||||
showWhitespace: true,
|
||||
fileFinderVisible: false,
|
||||
});
|
||||
|
|
|
@ -22,3 +22,4 @@ export const SET_HIGHLIGHTED_ROW = 'SET_HIGHLIGHTED_ROW';
|
|||
export const SET_TREE_DATA = 'SET_TREE_DATA';
|
||||
export const SET_RENDER_TREE_LIST = 'SET_RENDER_TREE_LIST';
|
||||
export const SET_SHOW_WHITESPACE = 'SET_SHOW_WHITESPACE';
|
||||
export const TOGGLE_FILE_FINDER_VISIBLE = 'TOGGLE_FILE_FINDER_VISIBLE';
|
||||
|
|
|
@ -244,4 +244,7 @@ export default {
|
|||
[types.SET_SHOW_WHITESPACE](state, showWhitespace) {
|
||||
state.showWhitespace = showWhitespace;
|
||||
},
|
||||
[types.TOGGLE_FILE_FINDER_VISIBLE](state, visible) {
|
||||
state.fileFinderVisible = visible;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
<script>
|
||||
import Vue from 'vue';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import { mapActions, mapState, mapGetters } from 'vuex';
|
||||
import { __ } from '~/locale';
|
||||
import FindFile from '~/vue_shared/components/file_finder/index.vue';
|
||||
import NewModal from './new_dropdown/modal.vue';
|
||||
import IdeSidebar from './ide_side_bar.vue';
|
||||
import RepoTabs from './repo_tabs.vue';
|
||||
import IdeStatusBar from './ide_status_bar.vue';
|
||||
import RepoEditor from './repo_editor.vue';
|
||||
import FindFile from './file_finder/index.vue';
|
||||
import RightPane from './panes/right.vue';
|
||||
import ErrorMessage from './error_message.vue';
|
||||
import CommitEditorHeader from './commit_sidebar/editor_header.vue';
|
||||
|
||||
const originalStopCallback = Mousetrap.stopCallback;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NewModal,
|
||||
|
@ -42,21 +39,18 @@ export default {
|
|||
'emptyStateSvgPath',
|
||||
'currentProjectId',
|
||||
'errorMessage',
|
||||
'loading',
|
||||
]),
|
||||
...mapGetters([
|
||||
'activeFile',
|
||||
'hasChanges',
|
||||
'someUncommittedChanges',
|
||||
'isCommitModeActive',
|
||||
'allBlobs',
|
||||
]),
|
||||
...mapGetters(['activeFile', 'hasChanges', 'someUncommittedChanges', 'isCommitModeActive']),
|
||||
},
|
||||
mounted() {
|
||||
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: {
|
||||
...mapActions(['toggleFileFinder']),
|
||||
|
@ -70,17 +64,8 @@ export default {
|
|||
});
|
||||
return returnValue;
|
||||
},
|
||||
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);
|
||||
openFile(file) {
|
||||
this.$router.push(`/project${file.url}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -90,7 +75,14 @@ export default {
|
|||
<article class="ide position-relative d-flex flex-column align-items-stretch">
|
||||
<error-message v-if="errorMessage" :message="errorMessage" />
|
||||
<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 />
|
||||
<div class="multi-file-edit-pane">
|
||||
<template v-if="activeFile">
|
||||
|
|
|
@ -1,8 +1,3 @@
|
|||
// 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;
|
||||
|
||||
// Commit message textarea
|
||||
|
|
|
@ -1,45 +1,62 @@
|
|||
<script>
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import VirtualList from 'vue-virtual-scroll-list';
|
||||
import Item from './item.vue';
|
||||
import router from '../../ide_router';
|
||||
import {
|
||||
MAX_FILE_FINDER_RESULTS,
|
||||
FILE_FINDER_ROW_HEIGHT,
|
||||
FILE_FINDER_EMPTY_ROW_HEIGHT,
|
||||
} from '../../constants';
|
||||
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';
|
||||
|
||||
export const MAX_FILE_FINDER_RESULTS = 40;
|
||||
export const FILE_FINDER_ROW_HEIGHT = 55;
|
||||
export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
|
||||
|
||||
const originalStopCallback = Mousetrap.stopCallback;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Item,
|
||||
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() {
|
||||
return {
|
||||
focusedIndex: 0,
|
||||
focusedIndex: -1,
|
||||
searchText: '',
|
||||
mouseOver: false,
|
||||
cancelMouseOver: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['allBlobs']),
|
||||
...mapState(['fileFindVisible', 'loading']),
|
||||
filteredBlobs() {
|
||||
const searchText = this.searchText.trim();
|
||||
|
||||
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',
|
||||
maxResults: MAX_FILE_FINDER_RESULTS,
|
||||
});
|
||||
|
@ -58,10 +75,12 @@ export default {
|
|||
},
|
||||
},
|
||||
watch: {
|
||||
fileFindVisible() {
|
||||
visible() {
|
||||
this.$nextTick(() => {
|
||||
if (!this.fileFindVisible) {
|
||||
this.searchText = '';
|
||||
if (!this.visible) {
|
||||
if (this.clearSearchOnClose) {
|
||||
this.searchText = '';
|
||||
}
|
||||
} else {
|
||||
this.focusedIndex = 0;
|
||||
|
||||
|
@ -72,7 +91,11 @@ export default {
|
|||
});
|
||||
},
|
||||
searchText() {
|
||||
this.focusedIndex = 0;
|
||||
this.focusedIndex = -1;
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.focusedIndex = 0;
|
||||
});
|
||||
},
|
||||
focusedIndex() {
|
||||
if (!this.mouseOver) {
|
||||
|
@ -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: {
|
||||
...mapActions(['toggleFileFinder']),
|
||||
toggle(visible) {
|
||||
this.$emit('toggle', visible);
|
||||
},
|
||||
clearSearchInput() {
|
||||
this.searchText = '';
|
||||
|
||||
|
@ -139,15 +179,15 @@ export default {
|
|||
this.openFile(this.filteredBlobs[this.focusedIndex]);
|
||||
break;
|
||||
case ESC_KEY_CODE:
|
||||
this.toggleFileFinder(false);
|
||||
this.toggle(false);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
openFile(file) {
|
||||
this.toggleFileFinder(false);
|
||||
router.push(`/project${file.url}`);
|
||||
this.toggle(false);
|
||||
this.$emit('click', file);
|
||||
},
|
||||
onMouseOver(index) {
|
||||
if (!this.cancelMouseOver) {
|
||||
|
@ -159,14 +199,26 @@ export default {
|
|||
this.cancelMouseOver = false;
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div class="ide-file-finder-overlay" @mousedown.self="toggleFileFinder(false)">
|
||||
<div class="dropdown-menu diff-file-changes ide-file-finder show">
|
||||
<div class="dropdown-input">
|
||||
<div class="file-finder-overlay" @mousedown.self="toggle(false)">
|
||||
<div class="dropdown-menu diff-file-changes file-finder show">
|
||||
<div :class="{ 'has-value': showClearInputButton }" class="dropdown-input">
|
||||
<input
|
||||
ref="searchInput"
|
||||
v-model="searchText"
|
||||
|
@ -186,9 +238,6 @@ export default {
|
|||
></i>
|
||||
<i
|
||||
:aria-label="__('Clear search input')"
|
||||
:class="{
|
||||
show: showClearInputButton,
|
||||
}"
|
||||
role="button"
|
||||
class="fa fa-times dropdown-input-clear"
|
||||
@click="clearSearchInput"
|
||||
|
@ -203,6 +252,7 @@ export default {
|
|||
:search-text="searchText"
|
||||
:focused="index === focusedIndex"
|
||||
:index="index"
|
||||
:show-diff-stats="showDiffStats"
|
||||
class="disable-hover"
|
||||
@click="openFile"
|
||||
@mouseover="onMouseOver"
|
||||
|
@ -225,3 +275,25 @@ export default {
|
|||
</div>
|
||||
</div>
|
||||
</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>
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import FileIcon from '../../../vue_shared/components/file_icon.vue';
|
||||
import ChangedFileIcon from '../../../vue_shared/components/changed_file_icon.vue';
|
||||
|
||||
|
@ -7,6 +8,7 @@ const MAX_PATH_LENGTH = 60;
|
|||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
ChangedFileIcon,
|
||||
FileIcon,
|
||||
},
|
||||
|
@ -27,6 +29,11 @@ export default {
|
|||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
showDiffStats: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
pathWithEllipsis() {
|
||||
|
@ -97,8 +104,23 @@ export default {
|
|||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span v-if="file.changed || file.tempFile" class="diff-changed-stats">
|
||||
<changed-file-icon :file="file" />
|
||||
<span v-if="file.changed || file.tempFile" v-once class="diff-changed-stats">
|
||||
<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>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.highlighted {
|
||||
color: #1f78d1;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
|
@ -816,26 +816,6 @@ $ide-commit-header-height: 48px;
|
|||
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 {
|
||||
height: 200px;
|
||||
background-color: $white-light;
|
||||
|
|
|
@ -986,3 +986,9 @@
|
|||
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 @@
|
|||
#js-vue-discussion-counter
|
||||
|
||||
.tab-content#diff-notes-app
|
||||
#js-diff-file-finder
|
||||
#notes.notes.tab-pane.voting_notes
|
||||
.row
|
||||
%section.col-md-12
|
||||
|
|
5
changelogs/unreleased/diff-file-finder.yml
Normal file
5
changelogs/unreleased/diff-file-finder.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Added fuzzy file finder to merge requests
|
||||
merge_request:
|
||||
author:
|
||||
type: changed
|
|
@ -4438,10 +4438,10 @@ msgstr ""
|
|||
msgid "MergeRequest| %{paragraphStart}changed the description %{descriptionChangedTimes} times %{timeDifferenceMinutes}%{paragraphEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "MergeRequest|Filter files"
|
||||
msgid "MergeRequest|No files found"
|
||||
msgstr ""
|
||||
|
||||
msgid "MergeRequest|No files found"
|
||||
msgid "MergeRequest|Search files"
|
||||
msgstr ""
|
||||
|
||||
msgid "Merged"
|
||||
|
|
|
@ -83,17 +83,6 @@ describe('Diffs tree list component', () => {
|
|||
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', () => {
|
||||
spyOn(vm.$store, 'dispatch').and.stub();
|
||||
|
||||
|
@ -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', () => {
|
|||
},
|
||||
};
|
||||
|
||||
expect(getters.allBlobs(localState)).toEqual([
|
||||
expect(
|
||||
getters.allBlobs(localState, {
|
||||
flatBlobsList: getters.flatBlobsList(localState),
|
||||
}),
|
||||
).toEqual([
|
||||
{
|
||||
isHeader: true,
|
||||
path: '/',
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Vue from 'vue';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import store from '~/ide/stores';
|
||||
import ide from '~/ide/components/ide.vue';
|
||||
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
|
||||
|
@ -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 => {
|
||||
expect(vm.$el.querySelector('.flash-container')).toBe(null);
|
||||
|
||||
|
|
|
@ -1,54 +1,51 @@
|
|||
import Vue from 'vue';
|
||||
import store from '~/ide/stores';
|
||||
import FindFileComponent from '~/ide/components/file_finder/index.vue';
|
||||
import Mousetrap from 'mousetrap';
|
||||
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 router from '~/ide/ide_router';
|
||||
import { file, resetStore } from '../../helpers';
|
||||
import { mountComponentWithStore } from '../../../helpers/vue_mount_component_helper';
|
||||
import { file } from 'spec/ide/helpers';
|
||||
import timeoutPromise from 'spec/helpers/set_timeout_promise_helper';
|
||||
|
||||
describe('IDE File finder item spec', () => {
|
||||
describe('File finder item spec', () => {
|
||||
const Component = Vue.extend(FindFileComponent);
|
||||
let vm;
|
||||
|
||||
beforeEach(done => {
|
||||
setFixtures('<div id="app"></div>');
|
||||
|
||||
vm = mountComponentWithStore(Component, {
|
||||
store,
|
||||
el: '#app',
|
||||
props: {
|
||||
index: 0,
|
||||
function createComponent(props) {
|
||||
vm = new Component({
|
||||
propsData: {
|
||||
files: [],
|
||||
visible: true,
|
||||
loading: false,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
|
||||
setTimeout(done);
|
||||
vm.$mount('#app');
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
setFixtures('<div id="app"></div>');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
|
||||
resetStore(vm.$store);
|
||||
});
|
||||
|
||||
describe('with entries', () => {
|
||||
beforeEach(done => {
|
||||
Vue.set(vm.$store.state.entries, 'folder', {
|
||||
...file('folder'),
|
||||
path: 'folder',
|
||||
type: 'folder',
|
||||
});
|
||||
|
||||
Vue.set(vm.$store.state.entries, 'index.js', {
|
||||
...file('index.js'),
|
||||
path: 'index.js',
|
||||
type: 'blob',
|
||||
url: '/index.jsurl',
|
||||
});
|
||||
|
||||
Vue.set(vm.$store.state.entries, 'component.js', {
|
||||
...file('component.js'),
|
||||
path: 'component.js',
|
||||
type: 'blob',
|
||||
createComponent({
|
||||
files: [
|
||||
{
|
||||
...file('index.js'),
|
||||
path: 'index.js',
|
||||
type: 'blob',
|
||||
url: '/index.jsurl',
|
||||
},
|
||||
{
|
||||
...file('component.js'),
|
||||
path: 'component.js',
|
||||
type: 'blob',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
setTimeout(done);
|
||||
|
@ -56,13 +53,14 @@ describe('IDE File finder item spec', () => {
|
|||
|
||||
it('renders list of blobs', () => {
|
||||
expect(vm.$el.textContent).toContain('index.js');
|
||||
expect(vm.$el.textContent).toContain('component.js');
|
||||
expect(vm.$el.textContent).not.toContain('folder');
|
||||
});
|
||||
|
||||
it('filters entries', done => {
|
||||
vm.searchText = 'index';
|
||||
|
||||
vm.$nextTick(() => {
|
||||
setTimeout(() => {
|
||||
expect(vm.$el.textContent).toContain('index.js');
|
||||
expect(vm.$el.textContent).not.toContain('component.js');
|
||||
|
||||
|
@ -73,8 +71,8 @@ describe('IDE File finder item spec', () => {
|
|||
it('shows clear button when searchText is not empty', done => {
|
||||
vm.searchText = 'index';
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelector('.dropdown-input-clear').classList).toContain('show');
|
||||
setTimeout(() => {
|
||||
expect(vm.$el.querySelector('.dropdown-input').classList).toContain('has-value');
|
||||
expect(vm.$el.querySelector('.dropdown-input-search').classList).toContain('hidden');
|
||||
|
||||
done();
|
||||
|
@ -84,11 +82,11 @@ describe('IDE File finder item spec', () => {
|
|||
it('clear button resets searchText', done => {
|
||||
vm.searchText = 'index';
|
||||
|
||||
vm.$nextTick()
|
||||
timeoutPromise()
|
||||
.then(() => {
|
||||
vm.$el.querySelector('.dropdown-input-clear').click();
|
||||
})
|
||||
.then(vm.$nextTick)
|
||||
.then(timeoutPromise)
|
||||
.then(() => {
|
||||
expect(vm.searchText).toBe('');
|
||||
})
|
||||
|
@ -100,11 +98,11 @@ describe('IDE File finder item spec', () => {
|
|||
spyOn(vm.$refs.searchInput, 'focus');
|
||||
vm.searchText = 'index';
|
||||
|
||||
vm.$nextTick()
|
||||
timeoutPromise()
|
||||
.then(() => {
|
||||
vm.$el.querySelector('.dropdown-input-clear').click();
|
||||
})
|
||||
.then(vm.$nextTick)
|
||||
.then(timeoutPromise)
|
||||
.then(() => {
|
||||
expect(vm.$refs.searchInput.focus).toHaveBeenCalled();
|
||||
})
|
||||
|
@ -116,7 +114,7 @@ describe('IDE File finder item spec', () => {
|
|||
it('returns 1 when no filtered entries exist', done => {
|
||||
vm.searchText = 'testing 123';
|
||||
|
||||
vm.$nextTick(() => {
|
||||
setTimeout(() => {
|
||||
expect(vm.listShowCount).toBe(1);
|
||||
|
||||
done();
|
||||
|
@ -136,7 +134,7 @@ describe('IDE File finder item spec', () => {
|
|||
it('returns 33 when entries dont exist', done => {
|
||||
vm.searchText = 'testing 123';
|
||||
|
||||
vm.$nextTick(() => {
|
||||
setTimeout(() => {
|
||||
expect(vm.listHeight).toBe(33);
|
||||
|
||||
done();
|
||||
|
@ -148,7 +146,7 @@ describe('IDE File finder item spec', () => {
|
|||
it('returns length of filtered blobs', done => {
|
||||
vm.searchText = 'index';
|
||||
|
||||
vm.$nextTick(() => {
|
||||
setTimeout(() => {
|
||||
expect(vm.filteredBlobsLength).toBe(1);
|
||||
|
||||
done();
|
||||
|
@ -162,7 +160,7 @@ describe('IDE File finder item spec', () => {
|
|||
vm.focusedIndex = 1;
|
||||
vm.searchText = 'test';
|
||||
|
||||
vm.$nextTick(() => {
|
||||
setTimeout(() => {
|
||||
expect(vm.focusedIndex).toBe(0);
|
||||
|
||||
done();
|
||||
|
@ -170,16 +168,16 @@ describe('IDE File finder item spec', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('fileFindVisible', () => {
|
||||
describe('visible', () => {
|
||||
it('returns searchText when false', done => {
|
||||
vm.searchText = 'test';
|
||||
vm.$store.state.fileFindVisible = true;
|
||||
vm.visible = true;
|
||||
|
||||
vm.$nextTick()
|
||||
timeoutPromise()
|
||||
.then(() => {
|
||||
vm.$store.state.fileFindVisible = false;
|
||||
vm.visible = false;
|
||||
})
|
||||
.then(vm.$nextTick)
|
||||
.then(timeoutPromise)
|
||||
.then(() => {
|
||||
expect(vm.searchText).toBe('');
|
||||
})
|
||||
|
@ -191,20 +189,19 @@ describe('IDE File finder item spec', () => {
|
|||
|
||||
describe('openFile', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(router, 'push');
|
||||
spyOn(vm, 'toggleFileFinder');
|
||||
spyOn(vm, '$emit');
|
||||
});
|
||||
|
||||
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', () => {
|
||||
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', () => {
|
|||
|
||||
vm.$refs.searchInput.dispatchEvent(event);
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.openFile).toHaveBeenCalledWith(vm.$store.state.entries['index.js']);
|
||||
setTimeout(() => {
|
||||
expect(vm.openFile).toHaveBeenCalledWith(vm.files[0]);
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -228,12 +225,12 @@ describe('IDE File finder item spec', () => {
|
|||
const event = new CustomEvent('keyup');
|
||||
event.keyCode = ESC_KEY_CODE;
|
||||
|
||||
spyOn(vm, 'toggleFileFinder');
|
||||
spyOn(vm, '$emit');
|
||||
|
||||
vm.$refs.searchInput.dispatchEvent(event);
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.toggleFileFinder).toHaveBeenCalled();
|
||||
setTimeout(() => {
|
||||
expect(vm.$emit).toHaveBeenCalledWith('toggle', false);
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -287,18 +284,85 @@ describe('IDE File finder item spec', () => {
|
|||
});
|
||||
|
||||
describe('without entries', () => {
|
||||
it('renders loading text when loading', done => {
|
||||
store.state.loading = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.textContent).toContain('Loading...');
|
||||
|
||||
done();
|
||||
it('renders loading text when loading', () => {
|
||||
createComponent({
|
||||
loading: true,
|
||||
});
|
||||
|
||||
expect(vm.$el.textContent).toContain('Loading...');
|
||||
});
|
||||
|
||||
it('renders no files text', () => {
|
||||
createComponent();
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,9 +1,9 @@
|
|||
import Vue from 'vue';
|
||||
import ItemComponent from '~/ide/components/file_finder/item.vue';
|
||||
import { file } from '../../helpers';
|
||||
import ItemComponent from '~/vue_shared/components/file_finder/item.vue';
|
||||
import { file } from 'spec/ide/helpers';
|
||||
import createComponent from '../../../helpers/vue_mount_component_helper';
|
||||
|
||||
describe('IDE File finder item spec', () => {
|
||||
describe('File finder item spec', () => {
|
||||
const Component = Vue.extend(ItemComponent);
|
||||
let vm;
|
||||
let localFile;
|
Loading…
Reference in a new issue