Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
李少辉-开发者
gitlab-foss
提交
4e7d4b4d
G
gitlab-foss
项目概览
李少辉-开发者
/
gitlab-foss
通知
15
Star
0
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
G
gitlab-foss
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
提交
Issue看板
体验新版 GitCode,发现更多精彩内容 >>
提交
4e7d4b4d
编写于
3月 16, 2017
作者:
F
Filipa Lacerda
浏览文件
操作
浏览文件
下载
差异文件
Merge branch 'issue-boards-modal-filter-bar' into 'master'
Added filter bar into add issues modal See merge request !9856
上级
4413d97b
addbf88c
变更
21
显示空白变更内容
内联
并排
Showing
21 changed file
with
210 addition
and
459 deletion
+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
未找到文件。
app/assets/javascripts/boards/components/modal/filters.js
浏览文件 @
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
已删除
100644 → 0
浏览文件 @
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
已删除
100644 → 0
浏览文件 @
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
已删除
100644 → 0
浏览文件 @
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
浏览文件 @
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
浏览文件 @
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
浏览文件 @
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
浏览文件 @
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
浏览文件 @
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
浏览文件 @
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
浏览文件 @
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
浏览文件 @
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
浏览文件 @
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
浏览文件 @
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
浏览文件 @
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
浏览文件 @
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
浏览文件 @
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
浏览文件 @
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
浏览文件 @
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
浏览文件 @
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
浏览文件 @
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
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录