Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
6e5461d6
Commit
6e5461d6
authored
Jan 16, 2019
by
Phil Hughes
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Added fuzzy file finder to merge requests
Closes
https://gitlab.com/gitlab-org/gitlab-ce/issues/53304
parent
55cb4bc9
Changes
22
Hide whitespace changes
Inline
Side-by-side
Showing
22 changed files
with
404 additions
and
304 deletions
+404
-304
app/assets/javascripts/diffs/components/app.vue
app/assets/javascripts/diffs/components/app.vue
+4
-0
app/assets/javascripts/diffs/components/tree_list.vue
app/assets/javascripts/diffs/components/tree_list.vue
+21
-36
app/assets/javascripts/diffs/index.js
app/assets/javascripts/diffs/index.js
+50
-1
app/assets/javascripts/diffs/store/actions.js
app/assets/javascripts/diffs/store/actions.js
+4
-0
app/assets/javascripts/diffs/store/getters.js
app/assets/javascripts/diffs/store/getters.js
+19
-18
app/assets/javascripts/diffs/store/modules/diff_state.js
app/assets/javascripts/diffs/store/modules/diff_state.js
+1
-0
app/assets/javascripts/diffs/store/mutation_types.js
app/assets/javascripts/diffs/store/mutation_types.js
+1
-0
app/assets/javascripts/diffs/store/mutations.js
app/assets/javascripts/diffs/store/mutations.js
+3
-0
app/assets/javascripts/ide/components/ide.vue
app/assets/javascripts/ide/components/ide.vue
+19
-27
app/assets/javascripts/ide/constants.js
app/assets/javascripts/ide/constants.js
+0
-5
app/assets/javascripts/vue_shared/components/file_finder/index.vue
...s/javascripts/vue_shared/components/file_finder/index.vue
+104
-32
app/assets/javascripts/vue_shared/components/file_finder/item.vue
...ts/javascripts/vue_shared/components/file_finder/item.vue
+24
-2
app/assets/stylesheets/page_bundles/ide.scss
app/assets/stylesheets/page_bundles/ide.scss
+0
-20
app/assets/stylesheets/pages/merge_requests.scss
app/assets/stylesheets/pages/merge_requests.scss
+6
-0
app/views/projects/merge_requests/show.html.haml
app/views/projects/merge_requests/show.html.haml
+1
-0
changelogs/unreleased/diff-file-finder.yml
changelogs/unreleased/diff-file-finder.yml
+5
-0
locale/gitlab.pot
locale/gitlab.pot
+2
-2
spec/javascripts/diffs/components/tree_list_spec.js
spec/javascripts/diffs/components/tree_list_spec.js
+0
-21
spec/javascripts/diffs/store/getters_spec.js
spec/javascripts/diffs/store/getters_spec.js
+5
-1
spec/javascripts/ide/components/ide_spec.js
spec/javascripts/ide/components/ide_spec.js
+0
-68
spec/javascripts/vue_shared/components/file_finder/index_spec.js
...vascripts/vue_shared/components/file_finder/index_spec.js
+132
-68
spec/javascripts/vue_shared/components/file_finder/item_spec.js
...avascripts/vue_shared/components/file_finder/item_spec.js
+3
-3
No files found.
app/assets/javascripts/diffs/components/app.vue
View file @
6e5461d6
...
...
@@ -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
'
]),
...
...
app/assets/javascripts/diffs/components/tree_list.vue
View file @
6e5461d6
...
...
@@ -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
>
app/assets/javascripts/diffs/index.js
View file @
6e5461d6
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
'
,
...
...
app/assets/javascripts/diffs/store/actions.js
View file @
6e5461d6
...
...
@@ -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
()
=>
{};
app/assets/javascripts/diffs/store/getters.js
View file @
6e5461d6
...
...
@@ -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
;
if
(
parentPath
&&
!
acc
.
some
(
f
=>
f
.
path
===
parentPath
))
{
acc
.
push
({
path
:
parentPath
,
isHeader
:
true
,
tree
:
[],
});
}
acc
.
find
(
f
=>
f
.
path
===
parentPath
).
tree
.
push
(
file
);
return
acc
;
},
[]);
export
const
flatBlobsList
=
state
=>
Object
.
values
(
state
.
treeEntries
).
filter
(
f
=>
f
.
type
===
'
blob
'
);
export
const
allBlobs
=
(
state
,
getters
)
=>
getters
.
flatBlobsList
.
reduce
((
acc
,
file
)
=>
{
const
{
parentPath
}
=
file
;
if
(
parentPath
&&
!
acc
.
some
(
f
=>
f
.
path
===
parentPath
))
{
acc
.
push
({
path
:
parentPath
,
isHeader
:
true
,
tree
:
[],
});
}
acc
.
find
(
f
=>
f
.
path
===
parentPath
).
tree
.
push
(
file
);
return
acc
;
},
[]);
export
const
diffFilesLength
=
state
=>
state
.
diffFiles
.
length
;
...
...
app/assets/javascripts/diffs/store/modules/diff_state.js
View file @
6e5461d6
...
...
@@ -29,4 +29,5 @@ export default () => ({
highlightedRow
:
null
,
renderTreeList
:
true
,
showWhitespace
:
true
,
fileFinderVisible
:
false
,
});
app/assets/javascripts/diffs/store/mutation_types.js
View file @
6e5461d6
...
...
@@ -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
'
;
app/assets/javascripts/diffs/store/mutations.js
View file @
6e5461d6
...
...
@@ -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
;
},
};
app/assets/javascripts/ide/components/ide.vue
View file @
6e5461d6
<
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"
>
...
...
app/assets/javascripts/ide/constants.js
View file @
6e5461d6
// 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
...
...
app/assets/javascripts/
ide
/components/file_finder/index.vue
→
app/assets/javascripts/
vue_shared
/components/file_finder/index.vue
View file @
6e5461d6
<
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
.
allBlob
s
.
slice
(
0
,
MAX_FILE_FINDER_RESULTS
);
return
this
.
file
s
.
slice
(
0
,
MAX_FILE_FINDER_RESULTS
);
}
return
fuzzaldrinPlus
.
filter
(
this
.
allBlob
s
,
searchText
,
{
return
fuzzaldrinPlus
.
filter
(
this
.
file
s
,
searchText
,
{
key
:
'
path
'
,
maxResults
:
MAX_FILE_FINDER_RESULTS
,
});
...
...
@@ -58,10 +75,12 @@ export default {
},
},
watch
:
{
fileFindV
isible
()
{
v
isible
()
{
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
.
toggle
FileFinder
(
false
);
this
.
toggle
(
false
);
break
;
default
:
break
;
}
},
openFile
(
file
)
{
this
.
toggle
FileFinder
(
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
>
app/assets/javascripts/
ide
/components/file_finder/item.vue
→
app/assets/javascripts/
vue_shared
/components/file_finder/item.vue
View file @
6e5461d6
<
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
>
app/assets/stylesheets/page_bundles/ide.scss
View file @
6e5461d6
...
...
@@ -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
;
...
...
app/assets/stylesheets/pages/merge_requests.scss
View file @
6e5461d6
...
...
@@ -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
;
}
app/views/projects/merge_requests/show.html.haml
View file @
6e5461d6
...
...
@@ -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
...
...
changelogs/unreleased/diff-file-finder.yml
0 → 100644
View file @
6e5461d6
---
title
:
Added fuzzy file finder to merge requests
merge_request
:
author
:
type
:
changed
locale/gitlab.pot
View file @
6e5461d6
...
...
@@ -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"
...
...
spec/javascripts/diffs/components/tree_list_spec.js
View file @
6e5461d6
...
...
@@ -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
(
''
);
});
});
});
spec/javascripts/diffs/store/getters_spec.js
View file @
6e5461d6
...
...
@@ -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
:
'
/
'
,
...
...
spec/javascripts/ide/components/ide_spec.js
View file @
6e5461d6
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
);
...
...
spec/javascripts/
ide
/components/file_finder/index_spec.js
→
spec/javascripts/
vue_shared
/components/file_finder/index_spec.js
View file @
6e5461d6
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
(
'
fileFindV
isible
'
,
()
=>
{
describe
(
'
v
isible
'
,
()
=>
{
it
(
'
returns searchText when false
'
,
done
=>
{
vm
.
searchText
=
'
test
'
;
vm
.
$store
.
state
.
fileFindV
isible
=
true
;
vm
.
v
isible
=
true
;
vm
.
$nextTick
()
timeoutPromise
()
.
then
(()
=>
{
vm
.
$store
.
state
.
fileFindV
isible
=
false
;
vm
.
v
isible
=
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
);
});
});
});
spec/javascripts/
ide
/components/file_finder/item_spec.js
→
spec/javascripts/
vue_shared
/components/file_finder/item_spec.js
View file @
6e5461d6
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
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment