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
0
Merge Requests
0
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
Tatuya Kamada
gitlab-ce
Commits
4e7d4b4d
Commit
4e7d4b4d
authored
Mar 16, 2017
by
Filipa Lacerda
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'issue-boards-modal-filter-bar' into 'master'
Added filter bar into add issues modal See merge request !9856
parents
4413d97b
addbf88c
Changes
21
Show whitespace changes
Inline
Side-by-side
Showing
21 changed files
with
210 additions
and
459 deletions
+210
-459
app/assets/javascripts/boards/components/modal/filters.js
app/assets/javascripts/boards/components/modal/filters.js
+16
-41
app/assets/javascripts/boards/components/modal/filters/label.js
...sets/javascripts/boards/components/modal/filters/label.js
+0
-54
app/assets/javascripts/boards/components/modal/filters/milestone.js
.../javascripts/boards/components/modal/filters/milestone.js
+0
-56
app/assets/javascripts/boards/components/modal/filters/user.js
...ssets/javascripts/boards/components/modal/filters/user.js
+0
-96
app/assets/javascripts/boards/components/modal/header.js
app/assets/javascripts/boards/components/modal/header.js
+4
-12
app/assets/javascripts/boards/components/modal/index.js
app/assets/javascripts/boards/components/modal/index.js
+3
-11
app/assets/javascripts/boards/filtered_search_boards.js
app/assets/javascripts/boards/filtered_search_boards.js
+9
-2
app/assets/javascripts/boards/models/list.js
app/assets/javascripts/boards/models/list.js
+2
-19
app/assets/javascripts/boards/stores/modal_store.js
app/assets/javascripts/boards/stores/modal_store.js
+3
-11
app/assets/javascripts/boards/utils/query_data.js
app/assets/javascripts/boards/utils/query_data.js
+21
-0
app/assets/javascripts/filtered_search/container.js
app/assets/javascripts/filtered_search/container.js
+14
-0
app/assets/javascripts/filtered_search/dropdown_hint.js
app/assets/javascripts/filtered_search/dropdown_hint.js
+1
-1
app/assets/javascripts/filtered_search/dropdown_utils.js
app/assets/javascripts/filtered_search/dropdown_utils.js
+4
-1
app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
...ripts/filtered_search/filtered_search_dropdown_manager.js
+11
-9
app/assets/javascripts/filtered_search/filtered_search_manager.js
...ts/javascripts/filtered_search/filtered_search_manager.js
+7
-4
app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
...ascripts/filtered_search/filtered_search_visual_tokens.js
+14
-12
app/assets/stylesheets/pages/boards.scss
app/assets/stylesheets/pages/boards.scss
+3
-6
app/views/projects/boards/_show.html.haml
app/views/projects/boards/_show.html.haml
+1
-0
app/views/shared/issuable/_search_bar.html.haml
app/views/shared/issuable/_search_bar.html.haml
+19
-17
spec/features/boards/add_issues_modal_spec.rb
spec/features/boards/add_issues_modal_spec.rb
+2
-0
spec/features/boards/modal_filter_spec.rb
spec/features/boards/modal_filter_spec.rb
+76
-107
No files found.
app/assets/javascripts/boards/components/modal/filters.js
View file @
4e7d4b4d
/* global Vue */
const
userFilter
=
require
(
'
./filters/user
'
);
const
milestoneFilter
=
require
(
'
./filters/milestone
'
);
const
labelFilter
=
require
(
'
./filters/label
'
);
import
FilteredSearchBoards
from
'
../../filtered_search_boards
'
;
import
FilteredSearchContainer
from
'
../../../filtered_search/container
'
;
module
.
exports
=
Vue
.
extend
(
{
export
default
{
name
:
'
modal-filters
'
,
props
:
{
projectId
:
{
type
:
Number
,
store
:
{
type
:
Object
,
required
:
true
,
},
milestonePath
:
{
type
:
String
,
required
:
true
,
},
labelPath
:
{
type
:
String
,
required
:
true
,
},
mounted
()
{
FilteredSearchContainer
.
container
=
this
.
$el
;
this
.
filteredSearch
=
new
FilteredSearchBoards
(
this
.
store
);
this
.
filteredSearch
.
removeTokens
();
},
destroyed
()
{
gl
.
issueBoards
.
ModalStore
.
setDefaultFilter
();
},
components
:
{
userFilter
,
milestoneFilter
,
labelFilter
,
beforeDestroy
()
{
this
.
filteredSearch
.
cleanup
();
FilteredSearchContainer
.
container
=
document
;
this
.
store
.
path
=
''
;
},
template
:
`
<div class="modal-filters">
<user-filter
dropdown-class-name="dropdown-menu-author"
toggle-class-name="js-user-search js-author-search"
toggle-label="Author"
field-name="author_id"
:project-id="projectId"></user-filter>
<user-filter
dropdown-class-name="dropdown-menu-author"
toggle-class-name="js-assignee-search"
toggle-label="Assignee"
field-name="assignee_id"
:null-user="true"
:project-id="projectId"></user-filter>
<milestone-filter :milestone-path="milestonePath"></milestone-filter>
<label-filter :label-path="labelPath"></label-filter>
</div>
`
,
});
template
:
'
#js-board-modal-filter
'
,
};
app/assets/javascripts/boards/components/modal/filters/label.js
deleted
100644 → 0
View file @
4413d97b
/* eslint-disable no-new */
/* global Vue */
/* global LabelsSelect */
module
.
exports
=
Vue
.
extend
({
name
:
'
filter-label
'
,
props
:
{
labelPath
:
{
type
:
String
,
required
:
true
,
},
},
mounted
()
{
new
LabelsSelect
(
this
.
$refs
.
dropdown
);
},
template
:
`
<div class="dropdown">
<button
class="dropdown-menu-toggle js-label-select js-multiselect js-extra-options"
type="button"
data-toggle="dropdown"
data-show-any="true"
data-show-no="true"
:data-labels="labelPath"
ref="dropdown">
<span class="dropdown-toggle-text">
Label
</span>
<i class="fa fa-chevron-down"></i>
</button>
<div class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable">
<div class="dropdown-title">
Filter by label
<button
class="dropdown-title-button dropdown-menu-close"
aria-label="Close"
type="button">
<i class="fa fa-times dropdown-menu-close-icon"></i>
</button>
</div>
<div class="dropdown-input">
<input
type="search"
class="dropdown-input-field"
placeholder="Search"
autocomplete="off" />
<i class="fa fa-search dropdown-input-search"></i>
<i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i>
</div>
<div class="dropdown-content"></div>
<div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div>
</div>
</div>
`
,
});
app/assets/javascripts/boards/components/modal/filters/milestone.js
deleted
100644 → 0
View file @
4413d97b
/* eslint-disable no-new */
/* global Vue */
/* global MilestoneSelect */
module
.
exports
=
Vue
.
extend
({
name
:
'
filter-milestone
'
,
props
:
{
milestonePath
:
{
type
:
String
,
required
:
true
,
},
},
mounted
()
{
new
MilestoneSelect
(
null
,
this
.
$refs
.
dropdown
);
},
template
:
`
<div class="dropdown">
<button
class="dropdown-menu-toggle js-milestone-select"
type="button"
data-toggle="dropdown"
data-show-any="true"
data-show-upcoming="true"
data-show-started="true"
data-field-name="milestone_title"
:data-milestones="milestonePath"
ref="dropdown">
<span class="dropdown-toggle-text">
Milestone
</span>
<i class="fa fa-chevron-down"></i>
</button>
<div class="dropdown-menu dropdown-select dropdown-menu-selectable dropdown-menu-milestone">
<div class="dropdown-title">
<span>Filter by milestone</span>
<button
class="dropdown-title-button dropdown-menu-close"
aria-label="Close"
type="button">
<i class="fa fa-times dropdown-menu-close-icon"></i>
</button>
</div>
<div class="dropdown-input">
<input
type="search"
class="dropdown-input-field"
placeholder="Search milestones"
autocomplete="off" />
<i class="fa fa-search dropdown-input-search"></i>
<i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i>
</div>
<div class="dropdown-content"></div>
<div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div>
</div>
</div>
`
,
});
app/assets/javascripts/boards/components/modal/filters/user.js
deleted
100644 → 0
View file @
4413d97b
/* eslint-disable no-new */
/* global Vue */
/* global UsersSelect */
module
.
exports
=
Vue
.
extend
({
name
:
'
filter-user
'
,
props
:
{
toggleClassName
:
{
type
:
String
,
required
:
true
,
},
dropdownClassName
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
toggleLabel
:
{
type
:
String
,
required
:
true
,
},
fieldName
:
{
type
:
String
,
required
:
true
,
},
nullUser
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
projectId
:
{
type
:
Number
,
required
:
true
,
},
},
mounted
()
{
new
UsersSelect
(
null
,
this
.
$refs
.
dropdown
);
},
computed
:
{
currentUsername
()
{
return
gon
.
current_username
;
},
dropdownTitle
()
{
return
`Filter by
${
this
.
toggleLabel
.
toLowerCase
()}
`
;
},
inputPlaceholder
()
{
return
`Search
${
this
.
toggleLabel
.
toLowerCase
()}
`
;
},
},
template
:
`
<div class="dropdown">
<button
class="dropdown-menu-toggle js-user-search"
:class="toggleClassName"
type="button"
data-toggle="dropdown"
data-current-user="true"
:data-any-user="'Any ' + toggleLabel"
:data-null-user="nullUser"
:data-field-name="fieldName"
:data-project-id="projectId"
:data-first-user="currentUsername"
ref="dropdown">
<span class="dropdown-toggle-text">
{{ toggleLabel }}
</span>
<i class="fa fa-chevron-down"></i>
</button>
<div
class="dropdown-menu dropdown-select dropdown-menu-user dropdown-menu-selectable"
:class="dropdownClassName">
<div class="dropdown-title">
{{ dropdownTitle }}
<button
class="dropdown-title-button dropdown-menu-close"
aria-label="Close"
type="button">
<i class="fa fa-times dropdown-menu-close-icon"></i>
</button>
</div>
<div class="dropdown-input">
<input
type="search"
class="dropdown-input-field"
autocomplete="off"
:placeholder="inputPlaceholder" />
<i class="fa fa-search dropdown-input-search"></i>
<i
role="button"
class="fa fa-times dropdown-input-clear js-dropdown-input-clear">
</i>
</div>
<div class="dropdown-content"></div>
<div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div>
</div>
</div>
`
,
});
app/assets/javascripts/boards/components/modal/header.js
View file @
4e7d4b4d
/* global Vue */
import
Vue
from
'
vue
'
;
import
modalFilters
from
'
./filters
'
;
require
(
'
./tabs
'
);
const
modalFilters
=
require
(
'
./filters
'
);
(()
=>
{
const
ModalStore
=
gl
.
issueBoards
.
ModalStore
;
...
...
@@ -66,16 +67,7 @@ const modalFilters = require('./filters');
<div
class="add-issues-search append-bottom-10"
v-if="showSearch">
<modal-filters
:project-id="projectId"
:milestone-path="milestonePath"
:label-path="labelPath">
</modal-filters>
<input
placeholder="Search issues..."
class="form-control"
type="search"
v-model="searchTerm" />
<modal-filters :store="filter" />
<button
type="button"
class="btn btn-success btn-inverted prepend-left-10"
...
...
app/assets/javascripts/boards/components/modal/index.js
View file @
4e7d4b4d
/* global Vue */
/* global ListIssue */
import
queryData
from
'
../../utils/query_data
'
;
require
(
'
./header
'
);
require
(
'
./list
'
);
...
...
@@ -47,9 +48,6 @@ require('./empty_state');
page
()
{
this
.
loadIssues
();
},
searchTerm
()
{
this
.
searchOperation
();
},
showAddIssuesModal
()
{
if
(
this
.
showAddIssuesModal
&&
!
this
.
issues
.
length
)
{
this
.
loading
=
true
;
...
...
@@ -72,19 +70,13 @@ require('./empty_state');
},
},
methods
:
{
searchOperation
:
_
.
debounce
(
function
searchOperationDebounce
()
{
this
.
loadIssues
(
true
);
},
500
),
loadIssues
(
clearIssues
=
false
)
{
if
(
!
this
.
showAddIssuesModal
)
return
false
;
const
queryData
=
Object
.
assign
({},
this
.
filter
,
{
search
:
this
.
searchTerm
,
return
gl
.
boardService
.
getBacklog
(
queryData
(
this
.
filter
.
path
,
{
page
:
this
.
page
,
per
:
this
.
perPage
,
});
return
gl
.
boardService
.
getBacklog
(
queryData
).
then
((
res
)
=>
{
})).
then
((
res
)
=>
{
const
data
=
res
.
json
();
if
(
clearIssues
)
{
...
...
app/assets/javascripts/boards/filtered_search_boards.js
View file @
4e7d4b4d
/* eslint-disable class-methods-use-this */
import
FilteredSearchContainer
from
'
../filtered_search/container
'
;
export
default
class
FilteredSearchBoards
extends
gl
.
FilteredSearchManager
{
constructor
(
store
,
updateUrl
=
false
)
{
super
(
'
boards
'
);
...
...
@@ -18,13 +21,17 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
}
}
updat
eTokens
()
{
const
tokens
=
document
.
querySelectorAll
(
'
.js-visual-token
'
);
remov
eTokens
()
{
const
tokens
=
FilteredSearchContainer
.
container
.
querySelectorAll
(
'
.js-visual-token
'
);
// Remove all the tokens as they will be replaced by the search manager
[].
forEach
.
call
(
tokens
,
(
el
)
=>
{
el
.
parentNode
.
removeChild
(
el
);
});
}
updateTokens
()
{
this
.
removeTokens
();
this
.
loadSearchParamsFromURL
();
...
...
app/assets/javascripts/boards/models/list.js
View file @
4e7d4b4d
/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len, no-unused-vars */
/* global ListIssue */
/* global ListLabel */
import
queryData
from
'
../utils/query_data
'
;
class
List
{
constructor
(
obj
)
{
...
...
@@ -64,25 +65,7 @@ class List {
}
getIssues
(
emptyIssues
=
true
)
{
const
data
=
gl
.
issueBoards
.
BoardsStore
.
filter
.
path
.
split
(
'
&
'
).
reduce
((
data
,
filterParam
)
=>
{
if
(
filterParam
===
''
)
return
data
;
const
paramSplit
=
filterParam
.
split
(
'
=
'
);
const
paramKeyNormalized
=
paramSplit
[
0
].
replace
(
'
[]
'
,
''
);
const
isArray
=
paramSplit
[
0
].
indexOf
(
'
[]
'
);
const
value
=
decodeURIComponent
(
paramSplit
[
1
]).
replace
(
/
\+
/g
,
'
'
);
if
(
isArray
!==
-
1
)
{
if
(
!
data
[
paramKeyNormalized
])
{
data
[
paramKeyNormalized
]
=
[];
}
data
[
paramKeyNormalized
].
push
(
value
);
}
else
{
data
[
paramKeyNormalized
]
=
value
;
}
return
data
;
},
{
page
:
this
.
page
});
const
data
=
queryData
(
gl
.
issueBoards
.
BoardsStore
.
filter
.
path
,
{
page
:
this
.
page
});
if
(
this
.
label
&&
data
.
label_name
)
{
data
.
label_name
=
data
.
label_name
.
filter
(
label
=>
label
!==
this
.
label
.
title
);
...
...
app/assets/javascripts/boards/stores/modal_store.js
View file @
4e7d4b4d
...
...
@@ -17,17 +17,9 @@
loadingNewPage
:
false
,
page
:
1
,
perPage
:
50
,
};
this
.
setDefaultFilter
();
}
setDefaultFilter
()
{
this
.
store
.
filter
=
{
author_id
:
''
,
assignee_id
:
''
,
milestone_title
:
''
,
label_name
:
[],
filter
:
{
path
:
''
,
},
};
}
...
...
app/assets/javascripts/boards/utils/query_data.js
0 → 100644
View file @
4e7d4b4d
export
default
(
path
,
extraData
)
=>
path
.
split
(
'
&
'
).
reduce
((
dataParam
,
filterParam
)
=>
{
if
(
filterParam
===
''
)
return
dataParam
;
const
data
=
dataParam
;
const
paramSplit
=
filterParam
.
split
(
'
=
'
);
const
paramKeyNormalized
=
paramSplit
[
0
].
replace
(
'
[]
'
,
''
);
const
isArray
=
paramSplit
[
0
].
indexOf
(
'
[]
'
);
const
value
=
decodeURIComponent
(
paramSplit
[
1
]).
replace
(
/
\+
/g
,
'
'
);
if
(
isArray
!==
-
1
)
{
if
(
!
data
[
paramKeyNormalized
])
{
data
[
paramKeyNormalized
]
=
[];
}
data
[
paramKeyNormalized
].
push
(
value
);
}
else
{
data
[
paramKeyNormalized
]
=
value
;
}
return
data
;
},
extraData
);
app/assets/javascripts/filtered_search/container.js
0 → 100644
View file @
4e7d4b4d
/* eslint-disable class-methods-use-this */
let
container
=
document
;
class
FilteredSearchContainerClass
{
set
container
(
containerParam
)
{
container
=
containerParam
;
}
get
container
()
{
return
container
;
}
}
export
default
new
FilteredSearchContainerClass
();
app/assets/javascripts/filtered_search/dropdown_hint.js
View file @
4e7d4b4d
...
...
@@ -45,7 +45,7 @@ require('./filtered_search_dropdown');
gl
.
FilteredSearchVisualTokens
.
addSearchVisualToken
(
searchTerms
.
join
(
'
'
));
}
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
token
.
replace
(
'
:
'
,
''
));
gl
.
FilteredSearchDropdownManager
.
addWordToInput
(
token
.
replace
(
'
:
'
,
''
)
,
''
,
false
,
this
.
container
);
}
this
.
dismissDropdown
();
this
.
dispatchInputEvent
();
...
...
app/assets/javascripts/filtered_search/dropdown_utils.js
View file @
4e7d4b4d
import
FilteredSearchContainer
from
'
./container
'
;
(()
=>
{
class
DropdownUtils
{
static
getEscapedText
(
text
)
{
...
...
@@ -85,7 +87,8 @@
// Determines the full search query (visual tokens + input)
static
getSearchQuery
(
untilInput
=
false
)
{
const
tokens
=
[].
slice
.
call
(
document
.
querySelectorAll
(
'
.tokens-container li
'
));
const
container
=
FilteredSearchContainer
.
container
;
const
tokens
=
[].
slice
.
call
(
container
.
querySelectorAll
(
'
.tokens-container li
'
));
const
values
=
[];
if
(
untilInput
)
{
...
...
app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
View file @
4e7d4b4d
/* global DropLab */
import
FilteredSearchContainer
from
'
./container
'
;
(()
=>
{
class
FilteredSearchDropdownManager
{
constructor
(
baseEndpoint
=
''
,
page
)
{
this
.
container
=
FilteredSearchContainer
.
container
;
this
.
baseEndpoint
=
baseEndpoint
.
replace
(
/
\/
$/
,
''
);
this
.
tokenizer
=
gl
.
FilteredSearchTokenizer
;
this
.
filteredSearchTokenKeys
=
gl
.
FilteredSearchTokenKeys
;
this
.
filteredSearchInput
=
document
.
querySelector
(
'
.filtered-search
'
);
this
.
filteredSearchInput
=
this
.
container
.
querySelector
(
'
.filtered-search
'
);
this
.
page
=
page
;
this
.
setupMapping
();
...
...
@@ -31,35 +33,35 @@
author
:
{
reference
:
null
,
gl
:
'
DropdownUser
'
,
element
:
document
.
querySelector
(
'
#js-dropdown-author
'
),
element
:
this
.
container
.
querySelector
(
'
#js-dropdown-author
'
),
},
assignee
:
{
reference
:
null
,
gl
:
'
DropdownUser
'
,
element
:
document
.
querySelector
(
'
#js-dropdown-assignee
'
),
element
:
this
.
container
.
querySelector
(
'
#js-dropdown-assignee
'
),
},
milestone
:
{
reference
:
null
,
gl
:
'
DropdownNonUser
'
,
extraArguments
:
[
`
${
this
.
baseEndpoint
}
/milestones.json`
,
'
%
'
],
element
:
document
.
querySelector
(
'
#js-dropdown-milestone
'
),
element
:
this
.
container
.
querySelector
(
'
#js-dropdown-milestone
'
),
},
label
:
{
reference
:
null
,
gl
:
'
DropdownNonUser
'
,
extraArguments
:
[
`
${
this
.
baseEndpoint
}
/labels.json`
,
'
~
'
],
element
:
document
.
querySelector
(
'
#js-dropdown-label
'
),
element
:
this
.
container
.
querySelector
(
'
#js-dropdown-label
'
),
},
hint
:
{
reference
:
null
,
gl
:
'
DropdownHint
'
,
element
:
document
.
querySelector
(
'
#js-dropdown-hint
'
),
element
:
this
.
container
.
querySelector
(
'
#js-dropdown-hint
'
),
},
};
}
static
addWordToInput
(
tokenName
,
tokenValue
=
''
,
clicked
=
false
)
{
const
input
=
document
.
querySelector
(
'
.filtered-search
'
);
const
input
=
FilteredSearchContainer
.
container
.
querySelector
(
'
.filtered-search
'
);
gl
.
FilteredSearchVisualTokens
.
addFilterVisualToken
(
tokenName
,
tokenValue
);
input
.
value
=
''
;
...
...
@@ -75,13 +77,13 @@
updateDropdownOffset
(
key
)
{
// Always align dropdown with the input field
let
offset
=
this
.
filteredSearchInput
.
getBoundingClientRect
().
left
-
document
.
querySelector
(
'
.scroll-container
'
).
getBoundingClientRect
().
left
;
let
offset
=
this
.
filteredSearchInput
.
getBoundingClientRect
().
left
-
this
.
container
.
querySelector
(
'
.scroll-container
'
).
getBoundingClientRect
().
left
;
const
maxInputWidth
=
240
;
const
currentDropdownWidth
=
this
.
mapping
[
key
].
element
.
clientWidth
||
maxInputWidth
;
// Make sure offset never exceeds the input container
const
offsetMaxWidth
=
document
.
querySelector
(
'
.scroll-container
'
).
clientWidth
-
currentDropdownWidth
;
const
offsetMaxWidth
=
this
.
container
.
querySelector
(
'
.scroll-container
'
).
clientWidth
-
currentDropdownWidth
;
if
(
offsetMaxWidth
<
offset
)
{
offset
=
offsetMaxWidth
;
}
...
...
app/assets/javascripts/filtered_search/filtered_search_manager.js
View file @
4e7d4b4d
import
FilteredSearchContainer
from
'
./container
'
;
(()
=>
{
class
FilteredSearchManager
{
constructor
(
page
)
{
this
.
filteredSearchInput
=
document
.
querySelector
(
'
.filtered-search
'
);
this
.
clearSearchButton
=
document
.
querySelector
(
'
.clear-search
'
);
this
.
tokensContainer
=
document
.
querySelector
(
'
.tokens-container
'
);
this
.
container
=
FilteredSearchContainer
.
container
;
this
.
filteredSearchInput
=
this
.
container
.
querySelector
(
'
.filtered-search
'
);
this
.
clearSearchButton
=
this
.
container
.
querySelector
(
'
.clear-search
'
);
this
.
tokensContainer
=
this
.
container
.
querySelector
(
'
.tokens-container
'
);
this
.
filteredSearchTokenKeys
=
gl
.
FilteredSearchTokenKeys
;
if
(
this
.
filteredSearchInput
)
{
...
...
@@ -132,7 +135,7 @@
}
unselectEditTokens
(
e
)
{
const
inputContainer
=
document
.
querySelector
(
'
.filtered-search-input-container
'
);
const
inputContainer
=
this
.
container
.
querySelector
(
'
.filtered-search-input-container
'
);
const
isElementInFilteredSearch
=
inputContainer
&&
inputContainer
.
contains
(
e
.
target
);
const
isElementInFilterDropdown
=
e
.
target
.
closest
(
'
.filter-dropdown
'
)
!==
null
;
const
isElementTokensContainer
=
e
.
target
.
classList
.
contains
(
'
tokens-container
'
);
...
...
app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
View file @
4e7d4b4d
import
FilteredSearchContainer
from
'
./container
'
;
class
FilteredSearchVisualTokens
{
static
getLastVisualTokenBeforeInput
()
{
const
inputLi
=
document
.
querySelector
(
'
.input-token
'
);
const
inputLi
=
FilteredSearchContainer
.
container
.
querySelector
(
'
.input-token
'
);
const
lastVisualToken
=
inputLi
&&
inputLi
.
previousElementSibling
;
return
{
...
...
@@ -10,7 +12,7 @@ class FilteredSearchVisualTokens {
}
static
unselectTokens
()
{
const
otherTokens
=
document
.
querySelectorAll
(
'
.js-visual-token .selectable.selected
'
);
const
otherTokens
=
FilteredSearchContainer
.
container
.
querySelectorAll
(
'
.js-visual-token .selectable.selected
'
);
[].
forEach
.
call
(
otherTokens
,
t
=>
t
.
classList
.
remove
(
'
selected
'
));
}
...
...
@@ -24,7 +26,7 @@ class FilteredSearchVisualTokens {
}
static
removeSelectedToken
()
{
const
selected
=
document
.
querySelector
(
'
.js-visual-token .selected
'
);
const
selected
=
FilteredSearchContainer
.
container
.
querySelector
(
'
.js-visual-token .selected
'
);
if
(
selected
)
{
const
li
=
selected
.
closest
(
'
.js-visual-token
'
);
...
...
@@ -54,8 +56,8 @@ class FilteredSearchVisualTokens {
}
li
.
querySelector
(
'
.name
'
).
innerText
=
name
;
const
tokensContainer
=
document
.
querySelector
(
'
.tokens-container
'
);
const
input
=
document
.
querySelector
(
'
.filtered-search
'
);
const
tokensContainer
=
FilteredSearchContainer
.
container
.
querySelector
(
'
.tokens-container
'
);
const
input
=
FilteredSearchContainer
.
container
.
querySelector
(
'
.filtered-search
'
);
tokensContainer
.
insertBefore
(
li
,
input
.
parentElement
);
}
...
...
@@ -77,14 +79,14 @@ class FilteredSearchVisualTokens {
const
addVisualTokenElement
=
FilteredSearchVisualTokens
.
addVisualTokenElement
;
if
(
isLastVisualTokenValid
)
{
addVisualTokenElement
(
tokenName
,
tokenValue
);
addVisualTokenElement
(
tokenName
,
tokenValue
,
false
);
}
else
{
const
previousTokenName
=
lastVisualToken
.
querySelector
(
'
.name
'
).
innerText
;
const
tokensContainer
=
document
.
querySelector
(
'
.tokens-container
'
);
const
tokensContainer
=
FilteredSearchContainer
.
container
.
querySelector
(
'
.tokens-container
'
);
tokensContainer
.
removeChild
(
lastVisualToken
);
const
value
=
tokenValue
||
tokenName
;
addVisualTokenElement
(
previousTokenName
,
value
);
addVisualTokenElement
(
previousTokenName
,
value
,
false
);
}
}
...
...
@@ -129,7 +131,7 @@ class FilteredSearchVisualTokens {
}
static
tokenizeInput
()
{
const
input
=
document
.
querySelector
(
'
.filtered-search
'
);
const
input
=
FilteredSearchContainer
.
container
.
querySelector
(
'
.filtered-search
'
);
const
{
isLastVisualTokenValid
}
=
gl
.
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
...
...
@@ -145,7 +147,7 @@ class FilteredSearchVisualTokens {
}
static
editToken
(
token
)
{
const
input
=
document
.
querySelector
(
'
.filtered-search
'
);
const
input
=
FilteredSearchContainer
.
container
.
querySelector
(
'
.filtered-search
'
);
FilteredSearchVisualTokens
.
tokenizeInput
();
...
...
@@ -174,9 +176,9 @@ class FilteredSearchVisualTokens {
}
static
moveInputToTheRight
()
{
const
input
=
document
.
querySelector
(
'
.filtered-search
'
);
const
input
=
FilteredSearchContainer
.
container
.
querySelector
(
'
.filtered-search
'
);
const
inputLi
=
input
.
parentElement
;
const
tokenContainer
=
document
.
querySelector
(
'
.tokens-container
'
);
const
tokenContainer
=
FilteredSearchContainer
.
container
.
querySelector
(
'
.tokens-container
'
);
FilteredSearchVisualTokens
.
tokenizeInput
();
...
...
app/assets/stylesheets/pages/boards.scss
View file @
4e7d4b4d
...
...
@@ -420,12 +420,9 @@
display
:
-
webkit-flex
;
display
:
flex
;
.form-control
{
margin-left
:
auto
;
@media
(
min-width
:
$screen-sm-min
)
{
max-width
:
200px
;
}
.issues-filters
{
-webkit-flex
:
1
;
flex
:
1
;
}
}
...
...
app/views/projects/boards/_show.html.haml
View file @
4e7d4b4d
...
...
@@ -10,6 +10,7 @@
%script
#js-board-template
{
type:
"text/x-template"
}=
render
"projects/boards/components/board"
%script
#js-board-list-template
{
type:
"text/x-template"
}=
render
"projects/boards/components/board_list"
%script
#js-board-modal-filter
{
type:
"text/x-template"
}=
render
"shared/issuable/search_bar"
,
type: :boards_modal
=
render
"projects/issues/head"
...
...
app/views/shared/issuable/_search_bar.html.haml
View file @
4e7d4b4d
-
type
=
local_assigns
.
fetch
(
:type
)
-
block_css_class
=
type
!=
:boards_modal
?
'row-content-block second-block'
:
''
.issues-filters
.issues-details-filters.
row-content-block.second-block.filtered-search-block
.issues-details-filters.
filtered-search-block
{
class:
block_css_class
,
"v-pre"
=>
type
==
:boards_modal
}
=
form_tag
page_filter_path
(
without:
[
:assignee_id
,
:author_id
,
:milestone_title
,
:label_name
,
:search
]),
method: :get
,
class:
'filter-form js-filter-form'
do
-
if
params
[
:search
].
present?
=
hidden_field_tag
:search
,
params
[
:search
]
...
...
@@ -14,7 +15,7 @@
.scroll-container
%ul
.tokens-container.list-unstyled
%li
.input-token
%input
.form-control.filtered-search
{
placeholder:
'Search or filter results...'
,
data:
{
id:
'filtered-search'
,
'project-id'
=>
@project
.
id
,
'username-params'
=>
@users
.
to_json
(
only:
[
:id
,
:username
]),
'base-endpoint'
=>
namespace_project_path
(
@project
.
namespace
,
@project
)
}
}
%input
.form-control.filtered-search
{
placeholder:
'Search or filter results...'
,
data:
{
id:
"filtered-search-#{type.to_s}"
,
'project-id'
=>
@project
.
id
,
'username-params'
=>
@users
.
to_json
(
only:
[
:id
,
:username
]),
'base-endpoint'
=>
namespace_project_path
(
@project
.
namespace
,
@project
)
}
}
=
icon
(
'filter'
)
%button
.clear-search.hidden
{
type:
'button'
}
=
icon
(
'times'
)
...
...
@@ -100,7 +101,7 @@
=
render
partial:
"shared/issuable/label_page_create"
=
dropdown_loading
#js-add-issues-btn
.prepend-left-10
-
els
e
-
els
if
type
!=
:boards_modal
=
render
'shared/sort_dropdown'
-
if
@bulk_edit
...
...
@@ -133,7 +134,8 @@
.filter-item.inline.update-issues-btn
=
button_tag
"Update
#{
type
.
to_s
.
humanize
(
capitalize:
false
)
}
"
,
class:
"btn update_selected_issues btn-save"
:javascript
-
unless
type
===
:boards_modal
:javascript
new
UsersSelect
();
new
LabelsSelect
();
new
MilestoneSelect
();
...
...
spec/features/boards/add_issues_modal_spec.rb
View file @
4e7d4b4d
...
...
@@ -107,6 +107,7 @@ describe 'Issue Boards add issue modal', :feature, :js do
it
'returns issues'
do
page
.
within
(
'.add-issues-modal'
)
do
find
(
'.form-control'
).
native
.
send_keys
(
issue
.
title
)
find
(
'.form-control'
).
native
.
send_keys
(
:enter
)
expect
(
page
).
to
have_selector
(
'.card'
,
count:
1
)
end
...
...
@@ -115,6 +116,7 @@ describe 'Issue Boards add issue modal', :feature, :js do
it
'returns no issues'
do
page
.
within
(
'.add-issues-modal'
)
do
find
(
'.form-control'
).
native
.
send_keys
(
'testing search'
)
find
(
'.form-control'
).
native
.
send_keys
(
:enter
)
expect
(
page
).
not_to
have_selector
(
'.card'
)
expect
(
page
).
not_to
have_content
(
"You haven't added any issues to your project yet"
)
...
...
spec/features/boards/modal_filter_spec.rb
View file @
4e7d4b4d
require
'rails_helper'
describe
'Issue Boards add issue modal filtering'
,
:feature
,
:js
do
include
WaitForAjax
include
WaitForVueResource
let
(
:project
)
{
create
(
:empty_project
,
:public
)
}
...
...
@@ -23,6 +22,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
page
.
within
(
'.add-issues-modal'
)
do
find
(
'.form-control'
).
native
.
send_keys
(
'testing empty state'
)
find
(
'.form-control'
).
native
.
send_keys
(
:enter
)
wait_for_vue_resource
...
...
@@ -33,13 +33,11 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
it
'restores filters when closing'
do
visit_board
page
.
within
(
'.add-issues-modal'
)
do
click_button
'Milestone'
wait_for_ajax
click_link
'Upcoming'
set_filter
(
'milestone'
)
click_filter_link
(
'Upcoming'
)
submit_filter
page
.
within
(
'.add-issues-modal'
)
do
wait_for_vue_resource
expect
(
page
).
to
have_selector
(
'.card'
,
count:
0
)
...
...
@@ -56,39 +54,44 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
end
end
context
'author'
do
let!
(
:issue
)
{
create
(
:issue
,
project:
project
,
author:
user2
)
}
before
do
project
.
team
<<
[
user2
,
:developer
]
it
'resotres filters after clicking clear button'
do
visit_board
end
it
'filters by any author'
do
set_filter
(
'milestone'
)
click_filter_link
(
'Upcoming'
)
submit_filter
page
.
within
(
'.add-issues-modal'
)
do
click_button
'Author'
wait_for_vue_resource
wait_for_ajax
expect
(
page
).
to
have_selector
(
'.card'
,
count:
0
)
click_link
'Any Author'
find
(
'.clear-search'
).
click
wait_for_vue_resource
expect
(
page
).
to
have_selector
(
'.card'
,
count:
2
)
expect
(
page
).
to
have_selector
(
'.card'
,
count:
1
)
end
end
it
'filters by selected user'
do
page
.
within
(
'.add-issues-modal'
)
do
click_button
'Author'
context
'author'
do
let!
(
:issue
)
{
create
(
:issue
,
project:
project
,
author:
user2
)
}
wait_for_ajax
before
do
project
.
team
<<
[
user2
,
:developer
]
click_link
user2
.
name
visit_board
end
it
'filters by selected user'
do
set_filter
(
'author'
)
click_filter_link
(
user2
.
name
)
submit_filter
page
.
within
(
'.add-issues-modal'
)
do
wait_for_vue_resource
expect
(
page
).
to
have_selector
(
'.js-visual-token'
,
text:
user2
.
username
)
expect
(
page
).
to
have_selector
(
'.card'
,
count:
1
)
end
end
...
...
@@ -103,46 +106,28 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
visit_board
end
it
'filters by any assignee'
do
page
.
within
(
'.add-issues-modal'
)
do
click_button
'Assignee'
wait_for_ajax
click_link
'Any Assignee'
wait_for_vue_resource
expect
(
page
).
to
have_selector
(
'.card'
,
count:
2
)
end
end
it
'filters by unassigned'
do
page
.
within
(
'.add-issues-modal'
)
do
click_button
'Assignee'
wait_for_ajax
click_link
'Unassigned'
set_filter
(
'assignee'
)
click_filter_link
(
'No Assignee'
)
submit_filter
page
.
within
(
'.add-issues-modal'
)
do
wait_for_vue_resource
expect
(
page
).
to
have_selector
(
'.js-visual-token'
,
text:
'none'
)
expect
(
page
).
to
have_selector
(
'.card'
,
count:
1
)
end
end
it
'filters by selected user'
do
page
.
within
(
'.add-issues-modal'
)
do
click_button
'Assignee'
wait_for_ajax
page
.
within
'.dropdown-menu-user'
do
click_link
user2
.
name
end
set_filter
(
'assignee'
)
click_filter_link
(
user2
.
name
)
submit_filter
page
.
within
(
'.add-issues-modal'
)
do
wait_for_vue_resource
expect
(
page
).
to
have_selector
(
'.js-visual-token'
,
text:
user2
.
username
)
expect
(
page
).
to
have_selector
(
'.card'
,
count:
1
)
end
end
...
...
@@ -156,44 +141,28 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
visit_board
end
it
'filters by any milestone'
do
page
.
within
(
'.add-issues-modal'
)
do
click_button
'Milestone'
wait_for_ajax
click_link
'Any Milestone'
wait_for_vue_resource
expect
(
page
).
to
have_selector
(
'.card'
,
count:
2
)
end
end
it
'filters by upcoming milestone'
do
page
.
within
(
'.add-issues-modal'
)
do
click_button
'Milestone'
wait_for_ajax
click_link
'Upcoming'
set_filter
(
'milestone'
)
click_filter_link
(
'Upcoming'
)
submit_filter
page
.
within
(
'.add-issues-modal'
)
do
wait_for_vue_resource
expect
(
page
).
to
have_selector
(
'.js-visual-token'
,
text:
'upcoming'
)
expect
(
page
).
to
have_selector
(
'.card'
,
count:
0
)
end
end
it
'filters by selected milestone'
do
page
.
within
(
'.add-issues-modal'
)
do
click_button
'Milestone'
wait_for_ajax
click_link
milestone
.
name
set_filter
(
'milestone'
)
click_filter_link
(
milestone
.
name
)
submit_filter
page
.
within
(
'.add-issues-modal'
)
do
wait_for_vue_resource
expect
(
page
).
to
have_selector
(
'.js-visual-token'
,
text:
milestone
.
name
)
expect
(
page
).
to
have_selector
(
'.card'
,
count:
1
)
end
end
...
...
@@ -207,44 +176,28 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
visit_board
end
it
'filters by any label'
do
page
.
within
(
'.add-issues-modal'
)
do
click_button
'Label'
wait_for_ajax
click_link
'Any Label'
wait_for_vue_resource
expect
(
page
).
to
have_selector
(
'.card'
,
count:
2
)
end
end
it
'filters by no label'
do
page
.
within
(
'.add-issues-modal'
)
do
click_button
'Label'
wait_for_ajax
click_link
'No Label'
set_filter
(
'label'
)
click_filter_link
(
'No Label'
)
submit_filter
page
.
within
(
'.add-issues-modal'
)
do
wait_for_vue_resource
expect
(
page
).
to
have_selector
(
'.js-visual-token'
,
text:
'none'
)
expect
(
page
).
to
have_selector
(
'.card'
,
count:
1
)
end
end
it
'filters by label'
do
page
.
within
(
'.add-issues-modal'
)
do
click_button
'Label'
wait_for_ajax
click_link
label
.
title
set_filter
(
'label'
)
click_filter_link
(
label
.
title
)
submit_filter
page
.
within
(
'.add-issues-modal'
)
do
wait_for_vue_resource
expect
(
page
).
to
have_selector
(
'.js-visual-token'
,
text:
label
.
title
)
expect
(
page
).
to
have_selector
(
'.card'
,
count:
1
)
end
end
...
...
@@ -256,4 +209,20 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
click_button
(
'Add issues'
)
end
def
set_filter
(
type
,
text
=
''
)
find
(
'.add-issues-modal .filtered-search'
).
native
.
send_keys
(
"
#{
type
}
:
#{
text
}
"
)
end
def
submit_filter
find
(
'.add-issues-modal .filtered-search'
).
native
.
send_keys
(
:enter
)
end
def
click_filter_link
(
link_text
)
page
.
within
(
'.add-issues-modal .filtered-search-input-container'
)
do
expect
(
page
).
to
have_button
(
link_text
)
click_button
(
link_text
)
end
end
end
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