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
cb0b32d7
Commit
cb0b32d7
authored
Mar 10, 2017
by
Phil Hughes
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Port of issue boards filter bar in modal window to EE
parent
623932ec
Changes
21
Hide whitespace changes
Inline
Side-by-side
Showing
21 changed files
with
211 additions
and
460 deletions
+211
-460
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
+10
-3
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 @
cb0b32d7
/* 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
,
required
:
true
,
},
milestonePath
:
{
type
:
String
,
required
:
true
,
},
labelPath
:
{
type
:
String
,
store
:
{
type
:
Object
,
required
:
true
,
},
},
destroyed
()
{
gl
.
issueBoards
.
ModalStore
.
setDefaultFilter
();
mounted
()
{
FilteredSearchContainer
.
container
=
this
.
$el
;
this
.
filteredSearch
=
new
FilteredSearchBoards
(
this
.
store
);
this
.
filteredSearch
.
removeTokens
();
},
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 @
623932ec
/* 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 @
623932ec
/* 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 @
623932ec
/* 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 @
cb0b32d7
/* 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 @
cb0b32d7
/* 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 @
cb0b32d7
/* eslint-disable class-methods-use-this */
import
FilteredSearchContainer
from
'
../filtered_search/container
'
;
export
default
class
FilteredSearchBoards
extends
gl
.
FilteredSearchManager
{
constructor
(
store
,
updateUrl
=
false
,
can
t
Edit
=
[])
{
constructor
(
store
,
updateUrl
=
false
,
canEdit
=
[])
{
super
(
'
boards
'
);
this
.
store
=
store
;
...
...
@@ -19,13 +22,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 @
cb0b32d7
/* 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 @
cb0b32d7
...
...
@@ -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 @
cb0b32d7
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 @
cb0b32d7
/* 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 @
cb0b32d7
...
...
@@ -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 @
cb0b32d7
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 @
cb0b32d7
/* 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
;
if
(
this
.
page
===
'
issues
'
||
this
.
page
===
'
boards
'
)
{
...
...
@@ -35,29 +37,29 @@
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
'
),
},
};
...
...
@@ -71,7 +73,7 @@
}
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
=
''
;
...
...
@@ -87,13 +89,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 @
cb0b32d7
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
(
page
===
'
issues
'
||
page
===
'
boards
'
)
{
...
...
@@ -137,7 +140,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 @
cb0b32d7
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 @
cb0b32d7
...
...
@@ -449,12 +449,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 @
cb0b32d7
...
...
@@ -11,6 +11,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 @
cb0b32d7
-
type
=
local_assigns
.
fetch
(
:type
)
-
board
=
local_assigns
.
fetch
(
:board
,
nil
)
-
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
}
-
if
type
==
:boards
&&
board
#js-multiple-boards-switcher
.inline.boards-switcher
{
"v-cloak"
=>
true
}
=
render
"projects/boards/switcher"
,
board:
board
...
...
@@ -18,7 +19,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'
)
...
...
@@ -120,7 +121,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
...
...
@@ -153,19 +154,20 @@
.filter-item.inline.update-issues-btn
=
button_tag
"Update
#{
type
.
to_s
.
humanize
(
capitalize:
false
)
}
"
,
class:
"btn update_selected_issues btn-save"
:javascript
new
UsersSelect
();
new
LabelsSelect
();
new
MilestoneSelect
();
new
IssueStatusSelect
();
new
SubscriptionSelect
();
-
unless
type
===
:boards_modal
:javascript
new
UsersSelect
();
new
LabelsSelect
();
new
MilestoneSelect
();
new
IssueStatusSelect
();
new
SubscriptionSelect
();
$
(
document
).
off
(
'
page:restore
'
).
on
(
'
page:restore
'
,
function
(
event
)
{
if
(
gl
.
FilteredSearchManager
)
{
new
gl
.
FilteredSearchManager
();
}
Issuable
.
init
();
new
gl
.
IssuableBulkActions
({
prefixId
:
'
issue_
'
,
$
(
document
).
off
(
'
page:restore
'
).
on
(
'
page:restore
'
,
function
(
event
)
{
if
(
gl
.
FilteredSearchManager
)
{
new
gl
.
FilteredSearchManager
();
}
Issuable
.
init
();
new
gl
.
IssuableBulkActions
({
prefixId
:
'
issue_
'
,
});
});
});
spec/features/boards/add_issues_modal_spec.rb
View file @
cb0b32d7
...
...
@@ -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 @
cb0b32d7
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
visit_board
end
set_filter
(
'milestone'
)
click_filter_link
(
'Upcoming'
)
submit_filter
it
'filters by any author'
do
page
.
within
(
'.add-issues-modal'
)
do
click_button
'Author'
page
.
within
(
'.add-issues-modal'
)
do
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
wait_for_vue_resource
expect
(
page
).
to
have_selector
(
'.card'
,
count:
2
)
end
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
)
}
before
do
project
.
team
<<
[
user2
,
:developer
]
wait_for_ajax
visit_board
end
click_link
user2
.
name
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