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
7e9f44fa
Commit
7e9f44fa
authored
Oct 31, 2018
by
Mike Greiling
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Prettify remaining files with differences in CE and EE
parent
2accb0d3
Changes
91
Hide whitespace changes
Inline
Side-by-side
Showing
91 changed files
with
2391 additions
and
2179 deletions
+2391
-2179
app/assets/javascripts/blob_edit/blob_bundle.js
app/assets/javascripts/blob_edit/blob_bundle.js
+1
-1
app/assets/javascripts/boards/components/board.js
app/assets/javascripts/boards/components/board.js
+15
-12
app/assets/javascripts/boards/components/board_blank_state.vue
...ssets/javascripts/boards/components/board_blank_state.vue
+7
-8
app/assets/javascripts/boards/components/board_card.vue
app/assets/javascripts/boards/components/board_card.vue
+64
-64
app/assets/javascripts/boards/components/board_new_issue.vue
app/assets/javascripts/boards/components/board_new_issue.vue
+2
-1
app/assets/javascripts/boards/components/board_sidebar.js
app/assets/javascripts/boards/components/board_sidebar.js
+23
-18
app/assets/javascripts/boards/components/issue_card_inner.vue
...assets/javascripts/boards/components/issue_card_inner.vue
+130
-130
app/assets/javascripts/boards/components/modal/empty_state.vue
...ssets/javascripts/boards/components/modal/empty_state.vue
+2
-2
app/assets/javascripts/boards/components/modal/footer.vue
app/assets/javascripts/boards/components/modal/footer.vue
+7
-9
app/assets/javascripts/boards/components/modal/header.vue
app/assets/javascripts/boards/components/modal/header.vue
+40
-40
app/assets/javascripts/boards/components/modal/index.vue
app/assets/javascripts/boards/components/modal/index.vue
+115
-114
app/assets/javascripts/boards/components/modal/list.vue
app/assets/javascripts/boards/components/modal/list.vue
+99
-99
app/assets/javascripts/boards/components/modal/tabs.vue
app/assets/javascripts/boards/components/modal/tabs.vue
+15
-15
app/assets/javascripts/boards/components/new_list_dropdown.js
...assets/javascripts/boards/components/new_list_dropdown.js
+24
-19
app/assets/javascripts/boards/components/project_select.vue
app/assets/javascripts/boards/components/project_select.vue
+4
-2
app/assets/javascripts/boards/components/sidebar/remove_issue.vue
...ts/javascripts/boards/components/sidebar/remove_issue.vue
+60
-62
app/assets/javascripts/boards/filtered_search_boards.js
app/assets/javascripts/boards/filtered_search_boards.js
+6
-3
app/assets/javascripts/boards/index.js
app/assets/javascripts/boards/index.js
+3
-3
app/assets/javascripts/boards/mixins/sortable_default_options.js
...ets/javascripts/boards/mixins/sortable_default_options.js
+7
-3
app/assets/javascripts/boards/models/issue.js
app/assets/javascripts/boards/models/issue.js
+17
-17
app/assets/javascripts/boards/models/list.js
app/assets/javascripts/boards/models/list.js
+2
-2
app/assets/javascripts/boards/services/board_service.js
app/assets/javascripts/boards/services/board_service.js
+9
-3
app/assets/javascripts/boards/stores/boards_store.js
app/assets/javascripts/boards/stores/boards_store.js
+31
-26
app/assets/javascripts/boards/stores/modal_store.js
app/assets/javascripts/boards/stores/modal_store.js
+6
-6
app/assets/javascripts/commons/gitlab_ui.js
app/assets/javascripts/commons/gitlab_ui.js
+1
-5
app/assets/javascripts/diffs/components/tree_list.vue
app/assets/javascripts/diffs/components/tree_list.vue
+2
-2
app/assets/javascripts/environments/components/environments_app.vue
.../javascripts/environments/components/environments_app.vue
+84
-86
app/assets/javascripts/environments/stores/environments_store.js
...ets/javascripts/environments/stores/environments_store.js
+12
-10
app/assets/javascripts/filtered_search/dropdown_user.js
app/assets/javascripts/filtered_search/dropdown_user.js
+4
-3
app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
...ripts/filtered_search/filtered_search_dropdown_manager.js
+1
-4
app/assets/javascripts/filtered_search/filtered_search_manager.js
...ts/javascripts/filtered_search/filtered_search_manager.js
+65
-48
app/assets/javascripts/flash.js
app/assets/javascripts/flash.js
+6
-2
app/assets/javascripts/gfm_auto_complete.js
app/assets/javascripts/gfm_auto_complete.js
+40
-21
app/assets/javascripts/jobs/components/job_app.vue
app/assets/javascripts/jobs/components/job_app.vue
+150
-150
app/assets/javascripts/jobs/components/job_log.vue
app/assets/javascripts/jobs/components/job_log.vue
+37
-37
app/assets/javascripts/jobs/index.js
app/assets/javascripts/jobs/index.js
+0
-1
app/assets/javascripts/jobs/store/getters.js
app/assets/javascripts/jobs/store/getters.js
+6
-3
app/assets/javascripts/labels_select.js
app/assets/javascripts/labels_select.js
+149
-97
app/assets/javascripts/lib/utils/ace_utils.js
app/assets/javascripts/lib/utils/ace_utils.js
+2
-2
app/assets/javascripts/members.js
app/assets/javascripts/members.js
+35
-21
app/assets/javascripts/milestone_select.js
app/assets/javascripts/milestone_select.js
+4
-1
app/assets/javascripts/monitoring/components/dashboard.vue
app/assets/javascripts/monitoring/components/dashboard.vue
+8
-4
app/assets/javascripts/notes/components/notes_app.vue
app/assets/javascripts/notes/components/notes_app.vue
+7
-1
app/assets/javascripts/notes/discussion_filters.js
app/assets/javascripts/notes/discussion_filters.js
+5
-3
app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
.../milestones/shared/components/promote_milestone_modal.vue
+0
-1
app/assets/javascripts/pages/projects/project.js
app/assets/javascripts/pages/projects/project.js
+3
-1
app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
...projects/shared/permissions/components/settings_panel.vue
+186
-188
app/assets/javascripts/projects/project_new.js
app/assets/javascripts/projects/project_new.js
+29
-15
app/assets/javascripts/right_sidebar.js
app/assets/javascripts/right_sidebar.js
+39
-18
app/assets/javascripts/search_autocomplete.js
app/assets/javascripts/search_autocomplete.js
+3
-1
app/assets/javascripts/sidebar/components/assignees/assignees.vue
...ts/javascripts/sidebar/components/assignees/assignees.vue
+2
-2
app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
...cripts/sidebar/components/subscriptions/subscriptions.vue
+63
-63
app/assets/javascripts/sidebar/sidebar_mediator.js
app/assets/javascripts/sidebar/sidebar_mediator.js
+12
-8
app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
...ue_merge_request_widget/components/mr_widget_pipeline.vue
+10
-7
app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
...merge_request_widget/components/states/ready_to_merge.vue
+22
-12
app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
...avascripts/vue_merge_request_widget/mr_widget_options.vue
+8
-4
app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
...ts/vue_merge_request_widget/services/mr_widget_service.js
+2
-2
app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
...cripts/vue_merge_request_widget/stores/mr_widget_store.js
+1
-2
app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue
...cripts/vue_shared/components/filtered_search_dropdown.vue
+4
-2
app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
...vue_shared/components/sidebar/collapsed_calendar_icon.vue
+31
-31
app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
...ared/components/sidebar/collapsed_grouped_date_picker.vue
+68
-73
app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
...onents/sidebar/labels_select/dropdown_value_collapsed.vue
+6
-1
ee/spec/javascripts/boards/models/list_spec.js
ee/spec/javascripts/boards/models/list_spec.js
+22
-14
ee/spec/javascripts/epics/sidebar/components/sidebar_app_spec.js
.../javascripts/epics/sidebar/components/sidebar_app_spec.js
+8
-4
ee/spec/javascripts/jobs/shared_runner_limit_block_spec.js
ee/spec/javascripts/jobs/shared_runner_limit_block_spec.js
+9
-4
ee/spec/javascripts/vue_mr_widget/ee_mr_widget_options_spec.js
...ec/javascripts/vue_mr_widget/ee_mr_widget_options_spec.js
+10
-5
spec/javascripts/boards/board_list_spec.js
spec/javascripts/boards/board_list_spec.js
+33
-53
spec/javascripts/boards/components/board_spec.js
spec/javascripts/boards/components/board_spec.js
+13
-24
spec/javascripts/boards/issue_card_spec.js
spec/javascripts/boards/issue_card_spec.js
+20
-24
spec/javascripts/environments/emtpy_state_spec.js
spec/javascripts/environments/emtpy_state_spec.js
+10
-12
spec/javascripts/environments/environment_item_spec.js
spec/javascripts/environments/environment_item_spec.js
+25
-31
spec/javascripts/environments/environment_table_spec.js
spec/javascripts/environments/environment_table_spec.js
+3
-5
spec/javascripts/environments/environments_app_spec.js
spec/javascripts/environments/environments_app_spec.js
+51
-42
spec/javascripts/environments/folder/environments_folder_view_spec.js
...ipts/environments/folder/environments_folder_view_spec.js
+54
-50
spec/javascripts/issue_show/components/title_spec.js
spec/javascripts/issue_show/components/title_spec.js
+6
-12
spec/javascripts/jobs/components/job_app_spec.js
spec/javascripts/jobs/components/job_app_spec.js
+1
-1
spec/javascripts/jobs/store/actions_spec.js
spec/javascripts/jobs/store/actions_spec.js
+4
-32
spec/javascripts/jobs/store/getters_spec.js
spec/javascripts/jobs/store/getters_spec.js
+3
-3
spec/javascripts/lib/utils/common_utils_spec.js
spec/javascripts/lib/utils/common_utils_spec.js
+118
-103
spec/javascripts/monitoring/graph/flag_spec.js
spec/javascripts/monitoring/graph/flag_spec.js
+17
-18
spec/javascripts/notes/components/discussion_filter_spec.js
spec/javascripts/notes/components/discussion_filter_spec.js
+13
-7
spec/javascripts/notes/components/note_app_spec.js
spec/javascripts/notes/components/note_app_spec.js
+2
-1
spec/javascripts/pipelines/graph/graph_component_spec.js
spec/javascripts/pipelines/graph/graph_component_spec.js
+39
-31
spec/javascripts/sidebar/assignees_spec.js
spec/javascripts/sidebar/assignees_spec.js
+49
-34
spec/javascripts/test_bundle.js
spec/javascripts/test_bundle.js
+4
-2
spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
...ripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
+36
-36
spec/javascripts/vue_mr_widget/mock_data.js
spec/javascripts/vue_mr_widget/mock_data.js
+1
-1
spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+31
-28
spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
.../components/sidebar/collapsed_grouped_date_picker_spec.js
+4
-4
spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js
...scripts/vue_shared/components/sidebar/date_picker_spec.js
+6
-6
spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
...ts/sidebar/labels_select/dropdown_value_collapsed_spec.js
+3
-1
No files found.
app/assets/javascripts/blob_edit/blob_bundle.js
View file @
7e9f44fa
...
...
@@ -13,7 +13,7 @@ export default () => {
if
(
editBlobForm
.
length
)
{
const
urlRoot
=
editBlobForm
.
data
(
'
relativeUrlRoot
'
);
const
assetsPath
=
editBlobForm
.
data
(
'
assetsPrefix
'
);
const
filePath
=
editBlobForm
.
data
(
'
blobFilename
'
)
const
filePath
=
editBlobForm
.
data
(
'
blobFilename
'
)
;
const
currentAction
=
$
(
'
.js-file-title
'
).
data
(
'
currentAction
'
);
const
projectId
=
editBlobForm
.
data
(
'
project-id
'
);
...
...
app/assets/javascripts/boards/components/board.js
View file @
7e9f44fa
...
...
@@ -42,7 +42,7 @@ export default Vue.extend({
required
:
true
,
},
},
data
()
{
data
()
{
return
{
detailIssue
:
boardsStore
.
detail
,
filter
:
boardsStore
.
filter
,
...
...
@@ -55,27 +55,26 @@ export default Vue.extend({
},
isNewIssueShown
()
{
return
this
.
list
.
type
===
'
backlog
'
||
(
!
this
.
disabled
&&
this
.
list
.
type
!==
'
closed
'
);
}
}
,
},
watch
:
{
filter
:
{
handler
()
{
this
.
list
.
page
=
1
;
this
.
list
.
getIssues
(
true
)
.
catch
(()
=>
{
// TODO: handle request error
});
this
.
list
.
getIssues
(
true
).
catch
(()
=>
{
// TODO: handle request error
});
},
deep
:
true
,
}
}
,
},
mounted
()
{
mounted
()
{
this
.
sortableOptions
=
getBoardSortableDefaultOptions
({
disabled
:
this
.
disabled
,
group
:
'
boards
'
,
draggable
:
'
.is-draggable
'
,
handle
:
'
.js-board-handle
'
,
onEnd
:
(
e
)
=>
{
onEnd
:
e
=>
{
sortableEnd
();
if
(
e
.
newIndex
!==
undefined
&&
e
.
oldIndex
!==
e
.
newIndex
)
{
...
...
@@ -86,14 +85,15 @@ export default Vue.extend({
boardsStore
.
moveList
(
list
,
order
);
});
}
}
}
,
});
this
.
sortable
=
Sortable
.
create
(
this
.
$el
.
parentNode
,
this
.
sortableOptions
);
},
created
()
{
if
(
this
.
list
.
isExpandable
&&
AccessorUtilities
.
isLocalStorageAccessSafe
())
{
const
isCollapsed
=
localStorage
.
getItem
(
`boards.
${
this
.
boardId
}
.
${
this
.
list
.
type
}
.expanded`
)
===
'
false
'
;
const
isCollapsed
=
localStorage
.
getItem
(
`boards.
${
this
.
boardId
}
.
${
this
.
list
.
type
}
.expanded`
)
===
'
false
'
;
this
.
list
.
isExpanded
=
!
isCollapsed
;
}
...
...
@@ -107,7 +107,10 @@ export default Vue.extend({
this
.
list
.
isExpanded
=
!
this
.
list
.
isExpanded
;
if
(
AccessorUtilities
.
isLocalStorageAccessSafe
())
{
localStorage
.
setItem
(
`boards.
${
this
.
boardId
}
.
${
this
.
list
.
type
}
.expanded`
,
this
.
list
.
isExpanded
);
localStorage
.
setItem
(
`boards.
${
this
.
boardId
}
.
${
this
.
list
.
type
}
.expanded`
,
this
.
list
.
isExpanded
,
);
}
}
},
...
...
app/assets/javascripts/boards/components/board_blank_state.vue
View file @
7e9f44fa
...
...
@@ -32,18 +32,18 @@ export default {
boardsStore
.
state
.
lists
=
_
.
sortBy
(
boardsStore
.
state
.
lists
,
'
position
'
);
// Save the labels
gl
.
boardService
.
generateDefaultLists
()
gl
.
boardService
.
generateDefaultLists
()
.
then
(
res
=>
res
.
data
)
.
then
(
(
data
)
=>
{
data
.
forEach
(
(
listObj
)
=>
{
.
then
(
data
=>
{
data
.
forEach
(
listObj
=>
{
const
list
=
boardsStore
.
findList
(
'
title
'
,
listObj
.
title
);
list
.
id
=
listObj
.
id
;
list
.
label
.
id
=
listObj
.
label
.
id
;
list
.
getIssues
()
.
catch
(()
=>
{
// TODO: handle request error
});
list
.
getIssues
().
catch
(()
=>
{
// TODO: handle request error
});
});
})
.
catch
(()
=>
{
...
...
@@ -57,7 +57,6 @@ export default {
clearBlankState
:
boardsStore
.
removeBlankState
.
bind
(
boardsStore
),
},
};
</
script
>
<
template
>
...
...
app/assets/javascripts/boards/components/board_card.vue
View file @
7e9f44fa
<
script
>
/* eslint-disable vue/require-default-prop */
import
IssueCardInner
from
'
./issue_card_inner.vue
'
;
import
eventHub
from
'
../eventhub
'
;
import
boardsStore
from
'
../stores/boards_store
'
;
/* eslint-disable vue/require-default-prop */
import
IssueCardInner
from
'
./issue_card_inner.vue
'
;
import
eventHub
from
'
../eventhub
'
;
import
boardsStore
from
'
../stores/boards_store
'
;
export
default
{
name
:
'
BoardsIssueCard
'
,
components
:
{
IssueCardInner
,
export
default
{
name
:
'
BoardsIssueCard
'
,
components
:
{
IssueCardInner
,
},
props
:
{
list
:
{
type
:
Object
,
default
:
()
=>
({}),
},
props
:
{
list
:
{
type
:
Object
,
default
:
()
=>
({}),
},
issue
:
{
type
:
Object
,
default
:
()
=>
({}),
},
issueLinkBase
:
{
type
:
String
,
default
:
''
,
},
disabled
:
{
type
:
Boolean
,
default
:
false
,
},
index
:
{
type
:
Number
,
default
:
0
,
},
rootPath
:
{
type
:
String
,
default
:
''
,
},
groupId
:
{
type
:
Number
,
},
issue
:
{
type
:
Object
,
default
:
()
=>
({}),
},
data
()
{
return
{
showDetail
:
false
,
detailIssue
:
boardsStore
.
detail
,
};
issueLinkBase
:
{
type
:
String
,
default
:
''
,
},
computed
:
{
issueDetailVisible
()
{
return
this
.
detailIssue
.
issue
&&
this
.
detailIssue
.
issue
.
id
===
this
.
issue
.
id
;
},
disabled
:
{
type
:
Boolean
,
default
:
false
,
},
methods
:
{
mouseDown
()
{
this
.
showDetail
=
true
;
},
mouseMove
()
{
this
.
showDetail
=
false
;
},
showIssue
(
e
)
{
if
(
e
.
target
.
classList
.
contains
(
'
js-no-trigger
'
))
return
;
index
:
{
type
:
Number
,
default
:
0
,
},
rootPath
:
{
type
:
String
,
default
:
''
,
},
groupId
:
{
type
:
Number
,
},
},
data
()
{
return
{
showDetail
:
false
,
detailIssue
:
boardsStore
.
detail
,
};
},
computed
:
{
issueDetailVisible
()
{
return
this
.
detailIssue
.
issue
&&
this
.
detailIssue
.
issue
.
id
===
this
.
issue
.
id
;
},
},
methods
:
{
mouseDown
()
{
this
.
showDetail
=
true
;
},
mouseMove
()
{
this
.
showDetail
=
false
;
},
showIssue
(
e
)
{
if
(
e
.
target
.
classList
.
contains
(
'
js-no-trigger
'
))
return
;
if
(
this
.
showDetail
)
{
this
.
showDetail
=
false
;
if
(
this
.
showDetail
)
{
this
.
showDetail
=
false
;
if
(
boardsStore
.
detail
.
issue
&&
boardsStore
.
detail
.
issue
.
id
===
this
.
issue
.
id
)
{
eventHub
.
$emit
(
'
clearDetailIssue
'
);
}
else
{
eventHub
.
$emit
(
'
newDetailIssue
'
,
this
.
issue
);
boardsStore
.
detail
.
list
=
this
.
list
;
}
if
(
boardsStore
.
detail
.
issue
&&
boardsStore
.
detail
.
issue
.
id
===
this
.
issue
.
id
)
{
eventHub
.
$emit
(
'
clearDetailIssue
'
);
}
else
{
eventHub
.
$emit
(
'
newDetailIssue
'
,
this
.
issue
);
boardsStore
.
detail
.
list
=
this
.
list
;
}
}
,
}
},
};
},
};
</
script
>
<
template
>
...
...
app/assets/javascripts/boards/components/board_new_issue.vue
View file @
7e9f44fa
...
...
@@ -65,7 +65,8 @@ export default {
eventHub
.
$emit
(
`scroll-board-list-
${
this
.
list
.
id
}
`
);
this
.
cancel
();
return
this
.
list
.
newIssue
(
issue
)
return
this
.
list
.
newIssue
(
issue
)
.
then
(()
=>
{
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$
(
this
.
$refs
.
submitButton
).
enable
();
...
...
app/assets/javascripts/boards/components/board_sidebar.js
View file @
7e9f44fa
...
...
@@ -40,7 +40,7 @@ export default Vue.extend({
};
},
computed
:
{
showSidebar
()
{
showSidebar
()
{
return
Object
.
keys
(
this
.
issue
).
length
;
},
milestoneTitle
()
{
...
...
@@ -53,18 +53,20 @@ export default Vue.extend({
return
this
.
issue
.
labels
&&
this
.
issue
.
labels
.
length
;
},
labelDropdownTitle
()
{
return
this
.
hasLabels
?
sprintf
(
__
(
'
%{firstLabel} +%{labelCount} more
'
),
{
firstLabel
:
this
.
issue
.
labels
[
0
].
title
,
labelCount
:
this
.
issue
.
labels
.
length
-
1
})
:
__
(
'
Label
'
);
return
this
.
hasLabels
?
sprintf
(
__
(
'
%{firstLabel} +%{labelCount} more
'
),
{
firstLabel
:
this
.
issue
.
labels
[
0
].
title
,
labelCount
:
this
.
issue
.
labels
.
length
-
1
,
})
:
__
(
'
Label
'
);
},
selectedLabels
()
{
return
this
.
hasLabels
?
this
.
issue
.
labels
.
map
(
l
=>
l
.
title
).
join
(
'
,
'
)
:
''
;
}
}
,
},
watch
:
{
detail
:
{
handler
()
{
handler
()
{
if
(
this
.
issue
.
id
!==
this
.
detail
.
issue
.
id
)
{
$
(
'
.block.assignee
'
)
.
find
(
'
input:not(.js-vue)[name="issue[assignee_ids][]"]
'
)
...
...
@@ -73,17 +75,19 @@ export default Vue.extend({
});
$
(
'
.js-issue-board-sidebar
'
,
this
.
$el
).
each
((
i
,
el
)
=>
{
$
(
el
).
data
(
'
glDropdown
'
).
clearMenu
();
$
(
el
)
.
data
(
'
glDropdown
'
)
.
clearMenu
();
});
}
this
.
issue
=
this
.
detail
.
issue
;
this
.
list
=
this
.
detail
.
list
;
},
deep
:
true
deep
:
true
,
},
},
created
()
{
created
()
{
// Get events from glDropdown
eventHub
.
$on
(
'
sidebar.removeAssignee
'
,
this
.
removeAssignee
);
eventHub
.
$on
(
'
sidebar.addAssignee
'
,
this
.
addAssignee
);
...
...
@@ -96,7 +100,7 @@ export default Vue.extend({
eventHub
.
$off
(
'
sidebar.removeAllAssignees
'
,
this
.
removeAllAssignees
);
eventHub
.
$off
(
'
sidebar.saveAssignees
'
,
this
.
saveAssignees
);
},
mounted
()
{
mounted
()
{
new
IssuableContext
(
this
.
currentUser
);
new
MilestoneSelect
();
new
DueDateSelectors
();
...
...
@@ -104,29 +108,30 @@ export default Vue.extend({
new
Sidebar
();
},
methods
:
{
closeSidebar
()
{
closeSidebar
()
{
this
.
detail
.
issue
=
{};
},
assignSelf
()
{
assignSelf
()
{
// Notify gl dropdown that we are now assigning to current user
this
.
$refs
.
assigneeBlock
.
dispatchEvent
(
new
Event
(
'
assignYourself
'
));
this
.
addAssignee
(
this
.
currentUser
);
this
.
saveAssignees
();
},
removeAssignee
(
a
)
{
removeAssignee
(
a
)
{
boardsStore
.
detail
.
issue
.
removeAssignee
(
a
);
},
addAssignee
(
a
)
{
addAssignee
(
a
)
{
boardsStore
.
detail
.
issue
.
addAssignee
(
a
);
},
removeAllAssignees
()
{
removeAllAssignees
()
{
boardsStore
.
detail
.
issue
.
removeAllAssignees
();
},
saveAssignees
()
{
saveAssignees
()
{
this
.
loadingAssignees
=
true
;
boardsStore
.
detail
.
issue
.
update
()
boardsStore
.
detail
.
issue
.
update
()
.
then
(()
=>
{
this
.
loadingAssignees
=
false
;
})
...
...
app/assets/javascripts/boards/components/issue_card_inner.vue
View file @
7e9f44fa
<
script
>
import
$
from
'
jquery
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
IssueCardWeight
from
'
ee/boards/components/issue_card_weight.vue
'
;
import
UserAvatarLink
from
'
../../vue_shared/components/user_avatar/user_avatar_link.vue
'
;
import
eventHub
from
'
../eventhub
'
;
import
tooltip
from
'
../../vue_shared/directives/tooltip
'
;
import
boardsStore
from
'
../stores/boards_store
'
;
import
$
from
'
jquery
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
IssueCardWeight
from
'
ee/boards/components/issue_card_weight.vue
'
;
import
UserAvatarLink
from
'
../../vue_shared/components/user_avatar/user_avatar_link.vue
'
;
import
eventHub
from
'
../eventhub
'
;
import
tooltip
from
'
../../vue_shared/directives/tooltip
'
;
import
boardsStore
from
'
../stores/boards_store
'
;
export
default
{
components
:
{
UserAvatarLink
,
Icon
,
IssueCardWeight
,
},
directives
:
{
tooltip
,
},
props
:
{
issue
:
{
type
:
Object
,
required
:
true
,
},
issueLinkBase
:
{
type
:
String
,
required
:
true
,
},
list
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
rootPath
:
{
type
:
String
,
required
:
true
,
},
updateFilters
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
groupId
:
{
type
:
Number
,
required
:
false
,
default
:
null
,
},
},
data
()
{
return
{
limitBeforeCounter
:
3
,
maxRender
:
4
,
maxCounter
:
99
,
};
export
default
{
components
:
{
UserAvatarLink
,
Icon
,
IssueCardWeight
,
},
directives
:
{
tooltip
,
},
props
:
{
issue
:
{
type
:
Object
,
required
:
true
,
},
computed
:
{
numberOverLimit
()
{
return
this
.
issue
.
assignees
.
length
-
this
.
limitBeforeCounter
;
},
assigneeCounterTooltip
()
{
return
`
${
this
.
assigneeCounterLabel
}
more`
;
},
assigneeCounterLabel
()
{
if
(
this
.
numberOverLimit
>
this
.
maxCounter
)
{
return
`
${
this
.
maxCounter
}
+`
;
}
return
`+
${
this
.
numberOverLimit
}
`
;
},
shouldRenderCounter
()
{
if
(
this
.
issue
.
assignees
.
length
<=
this
.
maxRender
)
{
return
false
;
}
issueLinkBase
:
{
type
:
String
,
required
:
true
,
},
list
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
rootPath
:
{
type
:
String
,
required
:
true
,
},
updateFilters
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
groupId
:
{
type
:
Number
,
required
:
false
,
default
:
null
,
},
},
data
()
{
return
{
limitBeforeCounter
:
3
,
maxRender
:
4
,
maxCounter
:
99
,
};
},
computed
:
{
numberOverLimit
()
{
return
this
.
issue
.
assignees
.
length
-
this
.
limitBeforeCounter
;
},
assigneeCounterTooltip
()
{
return
`
${
this
.
assigneeCounterLabel
}
more`
;
},
assigneeCounterLabel
()
{
if
(
this
.
numberOverLimit
>
this
.
maxCounter
)
{
return
`
${
this
.
maxCounter
}
+`
;
}
return
this
.
issue
.
assignees
.
length
>
this
.
numberOverLimit
;
},
issueId
()
{
if
(
this
.
issue
.
iid
)
{
return
`#
${
this
.
issue
.
iid
}
`
;
}
return
`+
${
this
.
numberOverLimit
}
`
;
},
shouldRenderCounter
()
{
if
(
this
.
issue
.
assignees
.
length
<=
this
.
maxRender
)
{
return
false
;
},
showLabelFooter
()
{
return
this
.
issue
.
labels
.
find
(
l
=>
this
.
showLabel
(
l
))
!==
undefined
;
},
},
methods
:
{
isIndexLessThanlimit
(
index
)
{
return
index
<
this
.
limitBeforeCounter
;
},
shouldRenderAssignee
(
index
)
{
// Eg. maxRender is 4,
// Render up to all 4 assignees if there are only 4 assigness
// Otherwise render up to the limitBeforeCounter
if
(
this
.
issue
.
assignees
.
length
<=
this
.
maxRender
)
{
return
index
<
this
.
maxRender
;
}
}
return
index
<
this
.
limitBeforeCounter
;
},
assigneeUrl
(
assignee
)
{
return
`
${
this
.
rootPath
}${
assignee
.
username
}
`
;
},
assigneeUrlTitle
(
assignee
)
{
return
`Assigned to
${
assignee
.
name
}
`
;
},
avatarUrlTitle
(
assignee
)
{
return
`Avatar for
${
assignee
.
name
}
`
;
},
showLabel
(
label
)
{
if
(
!
label
.
id
)
return
false
;
return
true
;
},
filterByLabel
(
label
,
e
)
{
if
(
!
this
.
updateFilters
)
return
;
return
this
.
issue
.
assignees
.
length
>
this
.
numberOverLimit
;
},
issueId
()
{
if
(
this
.
issue
.
iid
)
{
return
`#
${
this
.
issue
.
iid
}
`
;
}
return
false
;
},
showLabelFooter
()
{
return
this
.
issue
.
labels
.
find
(
l
=>
this
.
showLabel
(
l
))
!==
undefined
;
},
},
methods
:
{
isIndexLessThanlimit
(
index
)
{
return
index
<
this
.
limitBeforeCounter
;
},
shouldRenderAssignee
(
index
)
{
// Eg. maxRender is 4,
// Render up to all 4 assignees if there are only 4 assigness
// Otherwise render up to the limitBeforeCounter
if
(
this
.
issue
.
assignees
.
length
<=
this
.
maxRender
)
{
return
index
<
this
.
maxRender
;
}
return
index
<
this
.
limitBeforeCounter
;
},
assigneeUrl
(
assignee
)
{
return
`
${
this
.
rootPath
}${
assignee
.
username
}
`
;
},
assigneeUrlTitle
(
assignee
)
{
return
`Assigned to
${
assignee
.
name
}
`
;
},
avatarUrlTitle
(
assignee
)
{
return
`Avatar for
${
assignee
.
name
}
`
;
},
showLabel
(
label
)
{
if
(
!
label
.
id
)
return
false
;
return
true
;
},
filterByLabel
(
label
,
e
)
{
if
(
!
this
.
updateFilters
)
return
;
const
filterPath
=
boardsStore
.
filter
.
path
.
split
(
'
&
'
);
const
labelTitle
=
encodeURIComponent
(
label
.
title
);
const
param
=
`label_name[]=
${
labelTitle
}
`
;
const
labelIndex
=
filterPath
.
indexOf
(
param
);
$
(
e
.
currentTarget
).
tooltip
(
'
hide
'
);
const
filterPath
=
boardsStore
.
filter
.
path
.
split
(
'
&
'
);
const
labelTitle
=
encodeURIComponent
(
label
.
title
);
const
param
=
`label_name[]=
${
labelTitle
}
`
;
const
labelIndex
=
filterPath
.
indexOf
(
param
);
$
(
e
.
currentTarget
).
tooltip
(
'
hide
'
);
if
(
labelIndex
===
-
1
)
{
filterPath
.
push
(
param
);
}
else
{
filterPath
.
splice
(
labelIndex
,
1
);
}
if
(
labelIndex
===
-
1
)
{
filterPath
.
push
(
param
);
}
else
{
filterPath
.
splice
(
labelIndex
,
1
);
}
boardsStore
.
filter
.
path
=
filterPath
.
join
(
'
&
'
);
boardsStore
.
filter
.
path
=
filterPath
.
join
(
'
&
'
);
boardsStore
.
updateFiltersUrl
();
boardsStore
.
updateFiltersUrl
();
eventHub
.
$emit
(
'
updateTokens
'
);
},
labelStyle
(
label
)
{
return
{
backgroundColor
:
label
.
color
,
color
:
label
.
textColor
,
};
},
},
};
eventHub
.
$emit
(
'
updateTokens
'
);
},
labelStyle
(
label
)
{
return
{
backgroundColor
:
label
.
color
,
color
:
label
.
textColor
,
};
},
},
};
</
script
>
<
template
>
<div>
...
...
app/assets/javascripts/boards/components/modal/empty_state.vue
View file @
7e9f44fa
...
...
@@ -20,7 +20,7 @@ export default {
computed
:
{
contents
()
{
const
obj
=
{
title
:
'
You haven
\'
t added any issues to your project yet
'
,
title
:
"
You haven't added any issues to your project yet
"
,
content
:
`
An issue can be a bug, a todo or a feature request that needs to be
discussed in a project. Besides, issues are searchable and filterable.
...
...
@@ -28,7 +28,7 @@ export default {
};
if
(
this
.
activeTab
===
'
selected
'
)
{
obj
.
title
=
'
You haven
\'
t selected any issues yet
'
;
obj
.
title
=
"
You haven't selected any issues yet
"
;
obj
.
content
=
`
Go back to <strong>Open issues</strong> and select some issues
to add to your board.
...
...
app/assets/javascripts/boards/components/modal/footer.vue
View file @
7e9f44fa
...
...
@@ -42,19 +42,17 @@ export default {
const
req
=
this
.
buildUpdateRequest
(
list
);
// Post the data to the backend
gl
.
boardService
.
bulkUpdate
(
issueIds
,
req
)
.
catch
(()
=>
{
Flash
(
__
(
'
Failed to update issues, please try again.
'
));
gl
.
boardService
.
bulkUpdate
(
issueIds
,
req
).
catch
(()
=>
{
Flash
(
__
(
'
Failed to update issues, please try again.
'
));
selectedIssues
.
forEach
((
issue
)
=>
{
list
.
removeIssue
(
issue
);
list
.
issuesSize
-=
1
;
});
selectedIssues
.
forEach
(
issue
=>
{
list
.
removeIssue
(
issue
);
list
.
issuesSize
-=
1
;
});
});
// Add the issues on the frontend
selectedIssues
.
forEach
(
(
issue
)
=>
{
selectedIssues
.
forEach
(
issue
=>
{
list
.
addIssue
(
issue
);
list
.
issuesSize
+=
1
;
});
...
...
app/assets/javascripts/boards/components/modal/header.vue
View file @
7e9f44fa
<
script
>
import
ModalFilters
from
'
./filters
'
;
import
ModalTabs
from
'
./tabs.vue
'
;
import
ModalStore
from
'
../../stores/modal_store
'
;
import
modalMixin
from
'
../../mixins/modal_mixins
'
;
import
ModalFilters
from
'
./filters
'
;
import
ModalTabs
from
'
./tabs.vue
'
;
import
ModalStore
from
'
../../stores/modal_store
'
;
import
modalMixin
from
'
../../mixins/modal_mixins
'
;
export
default
{
components
:
{
ModalTabs
,
ModalFilters
,
export
default
{
components
:
{
ModalTabs
,
ModalFilters
,
},
mixins
:
[
modalMixin
],
props
:
{
projectId
:
{
type
:
Number
,
required
:
true
,
},
mixins
:
[
modalMixin
],
props
:
{
projectId
:
{
type
:
Number
,
required
:
true
,
},
milestonePath
:
{
type
:
String
,
required
:
true
,
},
labelPath
:
{
type
:
String
,
required
:
true
,
},
milestonePath
:
{
type
:
String
,
required
:
true
,
},
data
()
{
return
ModalStore
.
store
;
labelPath
:
{
type
:
String
,
required
:
true
,
},
computed
:
{
selectAllText
()
{
if
(
ModalStore
.
selectedCount
()
!==
this
.
issues
.
length
||
this
.
issues
.
length
===
0
)
{
return
'
Select all
'
;
}
},
data
()
{
return
ModalStore
.
store
;
},
computed
:
{
selectAllText
()
{
if
(
ModalStore
.
selectedCount
()
!==
this
.
issues
.
length
||
this
.
issues
.
length
===
0
)
{
return
'
Select all
'
;
}
return
'
Deselect all
'
;
},
showSearch
()
{
return
this
.
activeTab
===
'
all
'
&&
!
this
.
loading
&&
this
.
issuesCount
>
0
;
},
return
'
Deselect all
'
;
},
methods
:
{
toggleAll
()
{
this
.
$refs
.
selectAllBtn
.
blur
();
showSearch
()
{
return
this
.
activeTab
===
'
all
'
&&
!
this
.
loading
&&
this
.
issuesCount
>
0
;
},
},
methods
:
{
toggleAll
()
{
this
.
$refs
.
selectAllBtn
.
blur
();
ModalStore
.
toggleAll
();
},
ModalStore
.
toggleAll
();
},
};
},
};
</
script
>
<
template
>
<div>
...
...
app/assets/javascripts/boards/components/modal/index.vue
View file @
7e9f44fa
<
script
>
/* global ListIssue */
import
{
urlParamsToObject
}
from
'
~/lib/utils/common_utils
'
;
import
ModalHeader
from
'
./header.vue
'
;
import
ModalList
from
'
./list.vue
'
;
import
ModalFooter
from
'
./footer.vue
'
;
import
EmptyState
from
'
./empty_state.vue
'
;
import
ModalStore
from
'
../../stores/modal_store
'
;
/* global ListIssue */
import
{
urlParamsToObject
}
from
'
~/lib/utils/common_utils
'
;
import
ModalHeader
from
'
./header.vue
'
;
import
ModalList
from
'
./list.vue
'
;
import
ModalFooter
from
'
./footer.vue
'
;
import
EmptyState
from
'
./empty_state.vue
'
;
import
ModalStore
from
'
../../stores/modal_store
'
;
export
default
{
components
:
{
EmptyState
,
ModalHeader
,
ModalList
,
ModalFooter
,
export
default
{
components
:
{
EmptyState
,
ModalHeader
,
ModalList
,
ModalFooter
,
},
props
:
{
newIssuePath
:
{
type
:
String
,
required
:
true
,
},
props
:
{
newIssuePath
:
{
type
:
String
,
required
:
true
,
},
emptyStateSvg
:
{
type
:
String
,
required
:
true
,
},
issueLinkBase
:
{
type
:
String
,
required
:
true
,
},
rootPath
:
{
type
:
String
,
required
:
true
,
},
projectId
:
{
type
:
Number
,
required
:
true
,
},
milestonePath
:
{
type
:
String
,
required
:
true
,
},
labelPath
:
{
type
:
String
,
required
:
true
,
},
emptyStateSvg
:
{
type
:
String
,
required
:
true
,
},
data
()
{
return
ModalStore
.
store
;
issueLinkBase
:
{
type
:
String
,
required
:
true
,
},
computed
:
{
showList
()
{
if
(
this
.
activeTab
===
'
selected
'
)
{
return
this
.
selectedIssues
.
length
>
0
;
}
rootPath
:
{
type
:
String
,
required
:
true
,
},
projectId
:
{
type
:
Number
,
required
:
true
,
},
milestonePath
:
{
type
:
String
,
required
:
true
,
},
labelPath
:
{
type
:
String
,
required
:
true
,
},
},
data
()
{
return
ModalStore
.
store
;
},
computed
:
{
showList
()
{
if
(
this
.
activeTab
===
'
selected
'
)
{
return
this
.
selectedIssues
.
length
>
0
;
}
return
this
.
issuesCount
>
0
;
},
showEmptyState
()
{
if
(
!
this
.
loading
&&
this
.
issuesCount
===
0
)
{
return
true
;
}
return
this
.
issuesCount
>
0
;
},
showEmptyState
()
{
if
(
!
this
.
loading
&&
this
.
issuesCount
===
0
)
{
return
true
;
}
return
this
.
activeTab
===
'
selected
'
&&
this
.
selectedIssues
.
length
===
0
;
},
return
this
.
activeTab
===
'
selected
'
&&
this
.
selectedIssues
.
length
===
0
;
},
watch
:
{
page
()
{
this
.
loadIssues
();
},
showAddIssuesModal
()
{
if
(
this
.
showAddIssuesModal
&&
!
this
.
issues
.
length
)
{
this
.
loading
=
true
;
},
watch
:
{
page
()
{
this
.
loadIssues
();
},
showAddIssuesModal
()
{
if
(
this
.
showAddIssuesModal
&&
!
this
.
issues
.
length
)
{
this
.
loading
=
true
;
const
loadingDone
=
()
=>
{
this
.
loading
=
false
;
};
this
.
loadIssues
()
.
then
(
loadingDone
)
.
catch
(
loadingDone
);
}
else
if
(
!
this
.
showAddIssuesModal
)
{
this
.
issues
=
[];
this
.
selectedIssues
=
[];
this
.
issuesCount
=
false
;
}
},
filter
:
{
handler
()
{
if
(
this
.
$el
.
tagName
)
{
this
.
page
=
1
;
this
.
filterLoading
=
true
;
const
loadingDone
=
()
=>
{
this
.
l
oading
=
false
;
this
.
filterL
oading
=
false
;
};
this
.
loadIssues
()
this
.
loadIssues
(
true
)
.
then
(
loadingDone
)
.
catch
(
loadingDone
);
}
else
if
(
!
this
.
showAddIssuesModal
)
{
this
.
issues
=
[];
this
.
selectedIssues
=
[];
this
.
issuesCount
=
false
;
}
},
filter
:
{
handler
()
{
if
(
this
.
$el
.
tagName
)
{
this
.
page
=
1
;
this
.
filterLoading
=
true
;
const
loadingDone
=
()
=>
{
this
.
filterLoading
=
false
;
};
this
.
loadIssues
(
true
)
.
then
(
loadingDone
)
.
catch
(
loadingDone
);
}
},
deep
:
true
,
},
deep
:
true
,
},
created
()
{
this
.
page
=
1
;
},
methods
:
{
loadIssues
(
clearIssues
=
false
)
{
if
(
!
this
.
showAddIssuesModal
)
return
false
;
},
created
()
{
this
.
page
=
1
;
},
methods
:
{
loadIssues
(
clearIssues
=
false
)
{
if
(
!
this
.
showAddIssuesModal
)
return
false
;
return
gl
.
boardService
.
getBacklog
({
return
gl
.
boardService
.
getBacklog
({
...
urlParamsToObject
(
this
.
filter
.
path
),
page
:
this
.
page
,
per
:
this
.
perPage
,
})
.
then
(
res
=>
res
.
data
)
.
then
(
data
=>
{
if
(
clearIssues
)
{
this
.
issues
=
[];
}
.
then
(
res
=>
res
.
data
)
.
then
(
data
=>
{
if
(
clearIssues
)
{
this
.
issues
=
[];
}
data
.
issues
.
forEach
(
issueObj
=>
{
const
issue
=
new
ListIssue
(
issueObj
);
const
foundSelectedIssue
=
ModalStore
.
findSelectedIssue
(
issue
);
issue
.
selected
=
!!
foundSelectedIssue
;
data
.
issues
.
forEach
(
issueObj
=>
{
const
issue
=
new
ListIssue
(
issueObj
);
const
foundSelectedIssue
=
ModalStore
.
findSelectedIssue
(
issue
);
issue
.
selected
=
!!
foundSelectedIssue
;
this
.
issues
.
push
(
issue
);
});
this
.
issues
.
push
(
issue
);
});
this
.
loadingNewPage
=
false
;
this
.
loadingNewPage
=
false
;
if
(
!
this
.
issuesCount
)
{
this
.
issuesCount
=
data
.
size
;
}
})
.
catch
(()
=>
{
// TODO: handle request error
});
},
if
(
!
this
.
issuesCount
)
{
this
.
issuesCount
=
data
.
size
;
}
})
.
catch
(()
=>
{
// TODO: handle request error
});
},
};
},
};
</
script
>
<
template
>
<div
...
...
app/assets/javascripts/boards/components/modal/list.vue
View file @
7e9f44fa
<
script
>
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
bp
from
'
../../../breakpoints
'
;
import
ModalStore
from
'
../../stores/modal_store
'
;
import
IssueCardInner
from
'
../issue_card_inner.vue
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
bp
from
'
../../../breakpoints
'
;
import
ModalStore
from
'
../../stores/modal_store
'
;
import
IssueCardInner
from
'
../issue_card_inner.vue
'
;
export
default
{
components
:
{
IssueCardInner
,
Icon
,
export
default
{
components
:
{
IssueCardInner
,
Icon
,
},
props
:
{
issueLinkBase
:
{
type
:
String
,
required
:
true
,
},
props
:
{
issueLinkBase
:
{
type
:
String
,
required
:
true
,
},
rootPath
:
{
type
:
String
,
required
:
true
,
},
emptyStateSvg
:
{
type
:
String
,
required
:
true
,
},
rootPath
:
{
type
:
String
,
required
:
true
,
},
data
()
{
return
ModalStore
.
store
;
emptyStateSvg
:
{
type
:
String
,
required
:
true
,
},
computed
:
{
loopIssues
()
{
if
(
this
.
activeTab
===
'
all
'
)
{
return
this
.
issues
;
}
},
data
()
{
return
ModalStore
.
store
;
},
computed
:
{
loopIssues
()
{
if
(
this
.
activeTab
===
'
all
'
)
{
return
this
.
issues
;
}
return
this
.
selectedIssues
;
},
groupedIssues
()
{
const
groups
=
[];
this
.
loopIssues
.
forEach
((
issue
,
i
)
=>
{
const
index
=
i
%
this
.
columns
;
return
this
.
selectedIssues
;
},
groupedIssues
()
{
const
groups
=
[];
this
.
loopIssues
.
forEach
((
issue
,
i
)
=>
{
const
index
=
i
%
this
.
columns
;
if
(
!
groups
[
index
])
{
groups
.
push
([]);
}
if
(
!
groups
[
index
])
{
groups
.
push
([]);
}
groups
[
index
].
push
(
issue
);
});
groups
[
index
].
push
(
issue
);
});
return
groups
;
},
return
groups
;
},
watch
:
{
activeTab
()
{
if
(
this
.
activeTab
===
'
all
'
)
{
ModalStore
.
purgeUnselectedIssues
();
}
}
,
},
watch
:
{
activeTab
(
)
{
if
(
this
.
activeTab
===
'
all
'
)
{
ModalStore
.
purgeUnselectedIssues
();
}
},
mounted
()
{
this
.
scrollHandlerWrapper
=
this
.
scrollHandler
.
bind
(
this
);
this
.
setColumnCountWrapper
=
this
.
setColumnCount
.
bind
(
this
);
this
.
setColumnCount
();
},
mounted
()
{
this
.
scrollHandlerWrapper
=
this
.
scrollHandler
.
bind
(
this
);
this
.
setColumnCountWrapper
=
this
.
setColumnCount
.
bind
(
this
);
this
.
setColumnCount
();
this
.
$refs
.
list
.
addEventListener
(
'
scroll
'
,
this
.
scrollHandlerWrapper
);
window
.
addEventListener
(
'
resize
'
,
this
.
setColumnCountWrapper
);
this
.
$refs
.
list
.
addEventListener
(
'
scroll
'
,
this
.
scrollHandlerWrapper
);
window
.
addEventListener
(
'
resize
'
,
this
.
setColumnCountWrapper
);
},
beforeDestroy
()
{
this
.
$refs
.
list
.
removeEventListener
(
'
scroll
'
,
this
.
scrollHandlerWrapper
);
window
.
removeEventListener
(
'
resize
'
,
this
.
setColumnCountWrapper
);
},
methods
:
{
scrollHandler
()
{
const
currentPage
=
Math
.
floor
(
this
.
issues
.
length
/
this
.
perPage
);
if
(
this
.
scrollTop
()
>
this
.
scrollHeight
()
-
100
&&
!
this
.
loadingNewPage
&&
currentPage
===
this
.
page
)
{
this
.
loadingNewPage
=
true
;
this
.
page
+=
1
;
}
},
beforeDestroy
()
{
this
.
$refs
.
list
.
removeEventListener
(
'
scroll
'
,
this
.
scrollHandlerWrapper
);
window
.
removeEventListener
(
'
resize
'
,
this
.
setColumnCountWrapper
);
toggleIssue
(
e
,
issue
)
{
if
(
e
.
target
.
tagName
!==
'
A
'
)
{
ModalStore
.
toggleIssue
(
issue
);
}
},
methods
:
{
scrollHandler
()
{
const
currentPage
=
Math
.
floor
(
this
.
issues
.
length
/
this
.
perPage
);
if
(
this
.
scrollTop
()
>
this
.
scrollHeight
()
-
100
&&
!
this
.
loadingNewPage
&&
currentPage
===
this
.
page
)
{
this
.
loadingNewPage
=
true
;
this
.
page
+=
1
;
}
},
toggleIssue
(
e
,
issue
)
{
if
(
e
.
target
.
tagName
!==
'
A
'
)
{
ModalStore
.
toggleIssue
(
issue
);
}
},
listHeight
()
{
return
this
.
$refs
.
list
.
getBoundingClientRect
().
height
;
},
scrollHeight
()
{
return
this
.
$refs
.
list
.
scrollHeight
;
},
scrollTop
()
{
return
this
.
$refs
.
list
.
scrollTop
+
this
.
listHeight
();
},
showIssue
(
issue
)
{
if
(
this
.
activeTab
===
'
all
'
)
return
true
;
listHeight
()
{
return
this
.
$refs
.
list
.
getBoundingClientRect
().
height
;
},
scrollHeight
()
{
return
this
.
$refs
.
list
.
scrollHeight
;
},
scrollTop
()
{
return
this
.
$refs
.
list
.
scrollTop
+
this
.
listHeight
();
},
showIssue
(
issue
)
{
if
(
this
.
activeTab
===
'
all
'
)
return
true
;
const
index
=
ModalStore
.
selectedIssueIndex
(
issue
);
const
index
=
ModalStore
.
selectedIssueIndex
(
issue
);
return
index
!==
-
1
;
},
setColumnCount
()
{
const
breakpoint
=
bp
.
getBreakpointSize
();
return
index
!==
-
1
;
},
setColumnCount
()
{
const
breakpoint
=
bp
.
getBreakpointSize
();
if
(
breakpoint
===
'
lg
'
||
breakpoint
===
'
md
'
)
{
this
.
columns
=
3
;
}
else
if
(
breakpoint
===
'
sm
'
)
{
this
.
columns
=
2
;
}
else
{
this
.
columns
=
1
;
}
},
if
(
breakpoint
===
'
lg
'
||
breakpoint
===
'
md
'
)
{
this
.
columns
=
3
;
}
else
if
(
breakpoint
===
'
sm
'
)
{
this
.
columns
=
2
;
}
else
{
this
.
columns
=
1
;
}
},
};
},
};
</
script
>
<
template
>
<section
...
...
app/assets/javascripts/boards/components/modal/tabs.vue
View file @
7e9f44fa
<
script
>
import
ModalStore
from
'
../../stores/modal_store
'
;
import
modalMixin
from
'
../../mixins/modal_mixins
'
;
import
ModalStore
from
'
../../stores/modal_store
'
;
import
modalMixin
from
'
../../mixins/modal_mixins
'
;
export
default
{
mixins
:
[
modalMixin
],
data
()
{
return
ModalStore
.
store
;
export
default
{
mixins
:
[
modalMixin
],
data
()
{
return
ModalStore
.
store
;
},
computed
:
{
selectedCount
()
{
return
ModalStore
.
selectedCount
();
},
computed
:
{
selectedCount
()
{
return
ModalStore
.
selectedCount
();
},
},
destroyed
()
{
this
.
activeTab
=
'
all
'
;
},
};
},
destroyed
()
{
this
.
activeTab
=
'
all
'
;
},
};
</
script
>
<
template
>
<div
class=
"top-area prepend-top-10 append-bottom-10"
>
...
...
app/assets/javascripts/boards/components/new_list_dropdown.js
View file @
7e9f44fa
...
...
@@ -6,36 +6,41 @@ import _ from 'underscore';
import
CreateLabelDropdown
from
'
../../create_label
'
;
import
boardsStore
from
'
../stores/boards_store
'
;
$
(
document
).
off
(
'
created.label
'
).
on
(
'
created.label
'
,
(
e
,
label
)
=>
{
boardsStore
.
new
({
title
:
label
.
title
,
position
:
boardsStore
.
state
.
lists
.
length
-
2
,
list_type
:
'
label
'
,
label
:
{
id
:
label
.
id
,
$
(
document
)
.
off
(
'
created.label
'
)
.
on
(
'
created.label
'
,
(
e
,
label
)
=>
{
boardsStore
.
new
({
title
:
label
.
title
,
color
:
label
.
color
,
},
position
:
boardsStore
.
state
.
lists
.
length
-
2
,
list_type
:
'
label
'
,
label
:
{
id
:
label
.
id
,
title
:
label
.
title
,
color
:
label
.
color
,
},
});
});
});
export
default
function
initNewListDropdown
()
{
$
(
'
.js-new-board-list
'
).
each
(
function
()
{
$
(
'
.js-new-board-list
'
).
each
(
function
()
{
const
$this
=
$
(
this
);
new
CreateLabelDropdown
(
$this
.
closest
(
'
.dropdown
'
).
find
(
'
.dropdown-new-label
'
),
$this
.
data
(
'
namespacePath
'
),
$this
.
data
(
'
projectPath
'
));
new
CreateLabelDropdown
(
$this
.
closest
(
'
.dropdown
'
).
find
(
'
.dropdown-new-label
'
),
$this
.
data
(
'
namespacePath
'
),
$this
.
data
(
'
projectPath
'
),
);
$this
.
glDropdown
({
data
(
term
,
callback
)
{
axios
.
get
(
$this
.
attr
(
'
data-list-labels-path
'
))
.
then
(({
data
})
=>
{
callback
(
data
);
});
axios
.
get
(
$this
.
attr
(
'
data-list-labels-path
'
)).
then
(({
data
})
=>
{
callback
(
data
);
});
},
renderRow
(
label
)
{
renderRow
(
label
)
{
const
active
=
boardsStore
.
findList
(
'
title
'
,
label
.
title
);
const
$li
=
$
(
'
<li />
'
);
const
$a
=
$
(
'
<a />
'
,
{
class
:
(
active
?
`is-active js-board-list-
${
active
.
id
}
`
:
''
)
,
class
:
active
?
`is-active js-board-list-
${
active
.
id
}
`
:
''
,
text
:
label
.
title
,
href
:
'
#
'
,
});
...
...
@@ -53,7 +58,7 @@ export default function initNewListDropdown() {
selectable
:
true
,
multiSelect
:
true
,
containerSelector
:
'
.js-tab-container-labels .dropdown-page-one .dropdown-content
'
,
clicked
(
options
)
{
clicked
(
options
)
{
const
{
e
}
=
options
;
const
label
=
options
.
selectedObj
;
e
.
preventDefault
();
...
...
app/assets/javascripts/boards/components/project_select.vue
View file @
7e9f44fa
...
...
@@ -46,7 +46,7 @@ export default {
selectable
:
true
,
data
:
(
term
,
callback
)
=>
{
this
.
loading
=
true
;
return
Api
.
groupProjects
(
this
.
groupId
,
term
,
{
with_issues_enabled
:
true
},
projects
=>
{
return
Api
.
groupProjects
(
this
.
groupId
,
term
,
{
with_issues_enabled
:
true
},
projects
=>
{
this
.
loading
=
false
;
callback
(
projects
);
});
...
...
@@ -54,7 +54,9 @@ export default {
renderRow
(
project
)
{
return
`
<li>
<a href='#' class='dropdown-menu-link' data-project-id="
${
project
.
id
}
" data-project-name="
${
project
.
name
}
">
<a href='#' class='dropdown-menu-link' data-project-id="
${
project
.
id
}
" data-project-name="
${
project
.
name
}
">
${
_
.
escape
(
project
.
name
)}
</a>
</li>
...
...
app/assets/javascripts/boards/components/sidebar/remove_issue.vue
View file @
7e9f44fa
<
script
>
import
Vue
from
'
vue
'
;
import
Flash
from
'
../../../flash
'
;
import
{
__
}
from
'
../../../locale
'
;
import
boardsStore
from
'
../../stores/boards_store
'
;
import
Vue
from
'
vue
'
;
import
Flash
from
'
../../../flash
'
;
import
{
__
}
from
'
../../../locale
'
;
import
boardsStore
from
'
../../stores/boards_store
'
;
export
default
Vue
.
extend
({
props
:
{
issue
:
{
type
:
Object
,
required
:
true
,
},
list
:
{
type
:
Object
,
required
:
true
,
},
export
default
Vue
.
extend
({
props
:
{
issue
:
{
type
:
Object
,
required
:
true
,
},
computed
:
{
updateUrl
()
{
return
this
.
issue
.
path
;
},
list
:
{
type
:
Object
,
required
:
true
,
},
methods
:
{
removeIssue
()
{
const
{
issue
}
=
this
;
const
lists
=
issue
.
getLists
();
const
req
=
this
.
buildPatchRequest
(
issue
,
lists
);
},
computed
:
{
updateUrl
()
{
return
this
.
issue
.
path
;
},
},
methods
:
{
removeIssue
()
{
const
{
issue
}
=
this
;
const
lists
=
issue
.
getLists
();
const
req
=
this
.
buildPatchRequest
(
issue
,
lists
);
const
data
=
{
issue
:
this
.
seedPatchRequest
(
issue
,
req
),
};
const
data
=
{
issue
:
this
.
seedPatchRequest
(
issue
,
req
),
};
if
(
data
.
issue
.
label_ids
.
length
===
0
)
{
data
.
issue
.
label_ids
=
[
''
];
}
if
(
data
.
issue
.
label_ids
.
length
===
0
)
{
data
.
issue
.
label_ids
=
[
''
];
}
// Post the remove data
Vue
.
http
.
patch
(
this
.
updateUrl
,
data
).
catch
(()
=>
{
Flash
(
__
(
'
Failed to remove issue from board, please try again.
'
));
// Post the remove data
Vue
.
http
.
patch
(
this
.
updateUrl
,
data
).
catch
(()
=>
{
Flash
(
__
(
'
Failed to remove issue from board, please try again.
'
));
lists
.
forEach
((
list
)
=>
{
list
.
addIssue
(
issue
);
});
lists
.
forEach
(
list
=>
{
list
.
addIssue
(
issue
);
});
});
// Remove from the frontend store
lists
.
forEach
((
list
)
=>
{
list
.
removeIssue
(
issue
);
});
// Remove from the frontend store
lists
.
forEach
(
list
=>
{
list
.
removeIssue
(
issue
);
});
boardsStore
.
detail
.
issue
=
{};
},
/**
* Build the default patch request.
*/
buildPatchRequest
(
issue
,
lists
)
{
const
listLabelIds
=
lists
.
map
(
list
=>
list
.
label
.
id
);
boardsStore
.
detail
.
issue
=
{};
},
/**
* Build the default patch request.
*/
buildPatchRequest
(
issue
,
lists
)
{
const
listLabelIds
=
lists
.
map
(
list
=>
list
.
label
.
id
);
const
labelIds
=
issue
.
labels
.
map
(
label
=>
label
.
id
)
.
filter
(
id
=>
!
listLabelIds
.
includes
(
id
));
const
labelIds
=
issue
.
labels
.
map
(
label
=>
label
.
id
).
filter
(
id
=>
!
listLabelIds
.
includes
(
id
));
return
{
label_ids
:
labelIds
,
};
},
/**
* Seed the given patch request.
*
* (This is overridden in EE)
*/
seedPatchRequest
(
issue
,
req
)
{
return
req
;
},
return
{
label_ids
:
labelIds
,
};
},
/**
* Seed the given patch request.
*
* (This is overridden in EE)
*/
seedPatchRequest
(
issue
,
req
)
{
return
req
;
},
});
},
});
</
script
>
<
template
>
<div
...
...
app/assets/javascripts/boards/filtered_search_boards.js
View file @
7e9f44fa
...
...
@@ -35,7 +35,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
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
)
=>
{
[].
forEach
.
call
(
tokens
,
el
=>
{
el
.
parentNode
.
removeChild
(
el
);
});
...
...
@@ -53,7 +53,10 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
canEdit
(
tokenName
,
tokenValue
)
{
if
(
this
.
cantEdit
.
includes
(
tokenName
))
return
false
;
return
this
.
cantEditWithValue
.
findIndex
(
token
=>
token
.
name
===
tokenName
&&
token
.
value
===
tokenValue
)
===
-
1
;
return
(
this
.
cantEditWithValue
.
findIndex
(
token
=>
token
.
name
===
tokenName
&&
token
.
value
===
tokenValue
,
)
===
-
1
);
}
}
app/assets/javascripts/boards/index.js
View file @
7e9f44fa
...
...
@@ -39,9 +39,9 @@ export default () => {
const
issueBoardsContent
=
document
.
querySelector
(
'
.content-wrapper > .js-focus-mode-board
'
);
// check for browser back and trigger a hard reload to circumvent browser caching.
window
.
addEventListener
(
'
pageshow
'
,
(
event
)
=>
{
const
isNavTypeBackForward
=
window
.
performance
&&
window
.
performance
.
navigation
.
type
===
NavigationType
.
TYPE_BACK_FORWARD
;
window
.
addEventListener
(
'
pageshow
'
,
event
=>
{
const
isNavTypeBackForward
=
window
.
performance
&&
window
.
performance
.
navigation
.
type
===
NavigationType
.
TYPE_BACK_FORWARD
;
if
(
event
.
persisted
||
isNavTypeBackForward
)
{
window
.
location
.
reload
();
...
...
app/assets/javascripts/boards/mixins/sortable_default_options.js
View file @
7e9f44fa
...
...
@@ -4,7 +4,8 @@ import $ from 'jquery';
import
sortableConfig
from
'
ee/sortable/sortable_config
'
;
export
function
sortableStart
()
{
$
(
'
.has-tooltip
'
).
tooltip
(
'
hide
'
)
$
(
'
.has-tooltip
'
)
.
tooltip
(
'
hide
'
)
.
tooltip
(
'
disable
'
);
document
.
body
.
classList
.
add
(
'
is-dragging
'
);
}
...
...
@@ -15,7 +16,8 @@ export function sortableEnd() {
}
export
function
getBoardSortableDefaultOptions
(
obj
)
{
const
touchEnabled
=
(
'
ontouchstart
'
in
window
)
||
window
.
DocumentTouch
&&
document
instanceof
DocumentTouch
;
const
touchEnabled
=
'
ontouchstart
'
in
window
||
(
window
.
DocumentTouch
&&
document
instanceof
DocumentTouch
);
const
defaultSortOptions
=
Object
.
assign
({},
sortableConfig
,
{
filter
:
'
.board-delete, .btn
'
,
...
...
@@ -26,6 +28,8 @@ export function getBoardSortableDefaultOptions(obj) {
onEnd
:
sortableEnd
,
});
Object
.
keys
(
obj
).
forEach
((
key
)
=>
{
defaultSortOptions
[
key
]
=
obj
[
key
];
});
Object
.
keys
(
obj
).
forEach
(
key
=>
{
defaultSortOptions
[
key
]
=
obj
[
key
];
});
return
defaultSortOptions
;
}
app/assets/javascripts/boards/models/issue.js
View file @
7e9f44fa
...
...
@@ -9,7 +9,7 @@ import IssueProject from './project';
import
boardsStore
from
'
../stores/boards_store
'
;
class
ListIssue
{
constructor
(
obj
,
defaultAvatar
)
{
constructor
(
obj
,
defaultAvatar
)
{
this
.
id
=
obj
.
id
;
this
.
iid
=
obj
.
iid
;
this
.
title
=
obj
.
title
;
...
...
@@ -39,67 +39,67 @@ class ListIssue {
this
.
milestone_id
=
obj
.
milestone
.
id
;
}
obj
.
labels
.
forEach
(
(
label
)
=>
{
obj
.
labels
.
forEach
(
label
=>
{
this
.
labels
.
push
(
new
ListLabel
(
label
));
});
this
.
assignees
=
obj
.
assignees
.
map
(
a
=>
new
ListAssignee
(
a
,
defaultAvatar
));
}
addLabel
(
label
)
{
addLabel
(
label
)
{
if
(
!
this
.
findLabel
(
label
))
{
this
.
labels
.
push
(
new
ListLabel
(
label
));
}
}
findLabel
(
findLabel
)
{
findLabel
(
findLabel
)
{
return
this
.
labels
.
filter
(
label
=>
label
.
title
===
findLabel
.
title
)[
0
];
}
removeLabel
(
removeLabel
)
{
removeLabel
(
removeLabel
)
{
if
(
removeLabel
)
{
this
.
labels
=
this
.
labels
.
filter
(
label
=>
removeLabel
.
title
!==
label
.
title
);
}
}
removeLabels
(
labels
)
{
removeLabels
(
labels
)
{
labels
.
forEach
(
this
.
removeLabel
.
bind
(
this
));
}
addAssignee
(
assignee
)
{
addAssignee
(
assignee
)
{
if
(
!
this
.
findAssignee
(
assignee
))
{
this
.
assignees
.
push
(
new
ListAssignee
(
assignee
));
}
}
findAssignee
(
findAssignee
)
{
findAssignee
(
findAssignee
)
{
return
this
.
assignees
.
filter
(
assignee
=>
assignee
.
id
===
findAssignee
.
id
)[
0
];
}
removeAssignee
(
removeAssignee
)
{
removeAssignee
(
removeAssignee
)
{
if
(
removeAssignee
)
{
this
.
assignees
=
this
.
assignees
.
filter
(
assignee
=>
assignee
.
id
!==
removeAssignee
.
id
);
}
}
removeAllAssignees
()
{
removeAllAssignees
()
{
this
.
assignees
=
[];
}
addMilestone
(
milestone
)
{
addMilestone
(
milestone
)
{
const
miletoneId
=
this
.
milestone
?
this
.
milestone
.
id
:
null
;
if
(
milestone
.
id
!==
miletoneId
)
{
this
.
milestone
=
new
ListMilestone
(
milestone
);
}
}
removeMilestone
(
removeMilestone
)
{
removeMilestone
(
removeMilestone
)
{
if
(
removeMilestone
&&
removeMilestone
.
id
===
this
.
milestone
.
id
)
{
this
.
milestone
=
{};
}
}
getLists
()
{
getLists
()
{
return
boardsStore
.
state
.
lists
.
filter
(
list
=>
list
.
findIssue
(
this
.
id
));
}
...
...
@@ -115,14 +115,14 @@ class ListIssue {
this
.
isLoading
[
key
]
=
value
;
}
update
()
{
update
()
{
const
data
=
{
issue
:
{
milestone_id
:
this
.
milestone
?
this
.
milestone
.
id
:
null
,
due_date
:
this
.
dueDate
,
assignee_ids
:
this
.
assignees
.
length
>
0
?
this
.
assignees
.
map
(
(
u
)
=>
u
.
id
)
:
[
0
],
label_ids
:
this
.
labels
.
map
(
(
label
)
=>
label
.
id
)
}
assignee_ids
:
this
.
assignees
.
length
>
0
?
this
.
assignees
.
map
(
u
=>
u
.
id
)
:
[
0
],
label_ids
:
this
.
labels
.
map
(
label
=>
label
.
id
),
}
,
};
if
(
!
data
.
issue
.
label_ids
.
length
)
{
...
...
app/assets/javascripts/boards/models/list.js
View file @
7e9f44fa
...
...
@@ -247,11 +247,11 @@ class List {
});
}
getTypeInfo
(
type
)
{
getTypeInfo
(
type
)
{
return
TYPES
[
type
]
||
{};
}
onNewIssueResponse
(
issue
,
data
)
{
onNewIssueResponse
(
issue
,
data
)
{
issue
.
id
=
data
.
id
;
issue
.
iid
=
data
.
iid
;
issue
.
project
=
data
.
project
;
...
...
app/assets/javascripts/boards/services/board_service.js
View file @
7e9f44fa
...
...
@@ -19,7 +19,9 @@ export default class BoardService {
}
static
generateIssuePath
(
boardId
,
id
)
{
return
`
${
gon
.
relative_url_root
}
/-/boards/
${
boardId
?
`
${
boardId
}
`
:
''
}
/issues
${
id
?
`/
${
id
}
`
:
''
}
`
;
return
`
${
gon
.
relative_url_root
}
/-/boards/
${
boardId
?
`
${
boardId
}
`
:
''
}
/issues
${
id
?
`/
${
id
}
`
:
''
}
`
;
}
all
()
{
...
...
@@ -54,7 +56,9 @@ export default class BoardService {
getIssuesForList
(
id
,
filter
=
{})
{
const
data
=
{
id
};
Object
.
keys
(
filter
).
forEach
((
key
)
=>
{
data
[
key
]
=
filter
[
key
];
});
Object
.
keys
(
filter
).
forEach
(
key
=>
{
data
[
key
]
=
filter
[
key
];
});
return
axios
.
get
(
mergeUrlParams
(
data
,
this
.
generateIssuesPath
(
id
)));
}
...
...
@@ -75,7 +79,9 @@ export default class BoardService {
}
getBacklog
(
data
)
{
return
axios
.
get
(
mergeUrlParams
(
data
,
`
${
gon
.
relative_url_root
}
/-/boards/
${
this
.
boardId
}
/issues.json`
));
return
axios
.
get
(
mergeUrlParams
(
data
,
`
${
gon
.
relative_url_root
}
/-/boards/
${
this
.
boardId
}
/issues.json`
),
);
}
bulkUpdate
(
issueIds
,
extraData
=
{})
{
...
...
app/assets/javascripts/boards/stores/boards_store.js
View file @
7e9f44fa
...
...
@@ -27,7 +27,7 @@ const boardsStore = {
issue
:
{},
list
:
{},
},
create
()
{
create
()
{
this
.
state
.
lists
=
[];
this
.
filter
.
path
=
getUrlParamsArray
().
join
(
'
&
'
);
this
.
detail
=
{
...
...
@@ -45,13 +45,13 @@ const boardsStore = {
this
.
state
.
reload
=
false
;
this
.
state
.
currentPage
=
page
;
},
addList
(
listObj
,
defaultAvatar
)
{
addList
(
listObj
,
defaultAvatar
)
{
const
list
=
new
List
(
listObj
,
defaultAvatar
);
this
.
state
.
lists
.
push
(
list
);
return
list
;
},
new
(
listObj
)
{
new
(
listObj
)
{
const
list
=
this
.
addList
(
listObj
);
const
backlogList
=
this
.
findList
(
'
type
'
,
'
backlog
'
,
'
backlog
'
);
...
...
@@ -68,44 +68,44 @@ const boardsStore = {
});
this
.
removeBlankState
();
},
updateNewListDropdown
(
listId
)
{
updateNewListDropdown
(
listId
)
{
$
(
`.js-board-list-
${
listId
}
`
).
removeClass
(
'
is-active
'
);
},
shouldAddBlankState
()
{
shouldAddBlankState
()
{
// Decide whether to add the blank state
return
!
(
this
.
state
.
lists
.
filter
(
list
=>
list
.
type
!==
'
backlog
'
&&
list
.
type
!==
'
closed
'
)[
0
])
;
return
!
this
.
state
.
lists
.
filter
(
list
=>
list
.
type
!==
'
backlog
'
&&
list
.
type
!==
'
closed
'
)[
0
]
;
},
addBlankState
()
{
addBlankState
()
{
if
(
!
this
.
shouldAddBlankState
()
||
this
.
welcomeIsHidden
()
||
this
.
disabled
)
return
;
this
.
addList
({
id
:
'
blank
'
,
list_type
:
'
blank
'
,
title
:
'
Welcome to your Issue Board!
'
,
position
:
0
position
:
0
,
});
this
.
state
.
lists
=
_
.
sortBy
(
this
.
state
.
lists
,
'
position
'
);
},
removeBlankState
()
{
removeBlankState
()
{
this
.
removeList
(
'
blank
'
);
Cookies
.
set
(
'
issue_board_welcome_hidden
'
,
'
true
'
,
{
expires
:
365
*
10
,
path
:
''
path
:
''
,
});
},
welcomeIsHidden
()
{
welcomeIsHidden
()
{
return
Cookies
.
get
(
'
issue_board_welcome_hidden
'
)
===
'
true
'
;
},
removeList
(
id
,
type
=
'
blank
'
)
{
removeList
(
id
,
type
=
'
blank
'
)
{
const
list
=
this
.
findList
(
'
id
'
,
id
,
type
);
if
(
!
list
)
return
;
this
.
state
.
lists
=
this
.
state
.
lists
.
filter
(
list
=>
list
.
id
!==
id
);
},
moveList
(
listFrom
,
orderLists
)
{
moveList
(
listFrom
,
orderLists
)
{
orderLists
.
forEach
((
id
,
i
)
=>
{
const
list
=
this
.
findList
(
'
id
'
,
parseInt
(
id
,
10
));
...
...
@@ -113,22 +113,25 @@ const boardsStore = {
});
listFrom
.
update
();
},
moveIssueToList
(
listFrom
,
listTo
,
issue
,
newIndex
)
{
moveIssueToList
(
listFrom
,
listTo
,
issue
,
newIndex
)
{
const
issueTo
=
listTo
.
findIssue
(
issue
.
id
);
const
issueLists
=
issue
.
getLists
();
const
listLabels
=
issueLists
.
map
(
listIssue
=>
listIssue
.
label
);
if
(
!
issueTo
)
{
// Check if target list assignee is already present in this issue
if
((
listTo
.
type
===
'
assignee
'
&&
listFrom
.
type
===
'
assignee
'
)
&&
issue
.
findAssignee
(
listTo
.
assignee
))
{
if
(
listTo
.
type
===
'
assignee
'
&&
listFrom
.
type
===
'
assignee
'
&&
issue
.
findAssignee
(
listTo
.
assignee
)
)
{
const
targetIssue
=
listTo
.
findIssue
(
issue
.
id
);
targetIssue
.
removeAssignee
(
listFrom
.
assignee
);
}
else
if
(
listTo
.
type
===
'
milestone
'
)
{
const
currentMilestone
=
issue
.
milestone
;
const
currentLists
=
this
.
state
.
lists
.
filter
(
list
=>
(
list
.
type
===
'
milestone
'
&&
list
.
id
!==
listTo
.
id
)
)
.
filter
(
list
=>
list
.
issues
.
some
(
listIssue
=>
issue
.
id
===
listIssue
.
id
));
.
filter
(
list
=>
list
.
type
===
'
milestone
'
&&
list
.
id
!==
listTo
.
id
)
.
filter
(
list
=>
list
.
issues
.
some
(
listIssue
=>
issue
.
id
===
listIssue
.
id
));
issue
.
removeMilestone
(
currentMilestone
);
issue
.
addMilestone
(
listTo
.
milestone
);
...
...
@@ -144,7 +147,7 @@ const boardsStore = {
}
if
(
listTo
.
type
===
'
closed
'
&&
listFrom
.
type
!==
'
backlog
'
)
{
issueLists
.
forEach
(
(
list
)
=>
{
issueLists
.
forEach
(
list
=>
{
list
.
removeIssue
(
issue
);
});
issue
.
removeLabels
(
listLabels
);
...
...
@@ -162,26 +165,28 @@ const boardsStore = {
return
(
(
listTo
.
type
!==
'
label
'
&&
listFrom
.
type
===
'
assignee
'
)
||
(
listTo
.
type
!==
'
assignee
'
&&
listFrom
.
type
===
'
label
'
)
||
(
listFrom
.
type
===
'
backlog
'
)
listFrom
.
type
===
'
backlog
'
);
},
moveIssueInList
(
list
,
issue
,
oldIndex
,
newIndex
,
idArray
)
{
moveIssueInList
(
list
,
issue
,
oldIndex
,
newIndex
,
idArray
)
{
const
beforeId
=
parseInt
(
idArray
[
newIndex
-
1
],
10
)
||
null
;
const
afterId
=
parseInt
(
idArray
[
newIndex
+
1
],
10
)
||
null
;
list
.
moveIssue
(
issue
,
oldIndex
,
newIndex
,
beforeId
,
afterId
);
},
findList
(
key
,
val
,
type
=
'
label
'
)
{
const
filteredList
=
this
.
state
.
lists
.
filter
((
list
)
=>
{
const
byType
=
type
?
(
list
.
type
===
type
)
||
(
list
.
type
===
'
assignee
'
)
||
(
list
.
type
===
'
milestone
'
)
:
true
;
findList
(
key
,
val
,
type
=
'
label
'
)
{
const
filteredList
=
this
.
state
.
lists
.
filter
(
list
=>
{
const
byType
=
type
?
list
.
type
===
type
||
list
.
type
===
'
assignee
'
||
list
.
type
===
'
milestone
'
:
true
;
return
list
[
key
]
===
val
&&
byType
;
});
return
filteredList
[
0
];
},
updateFiltersUrl
()
{
updateFiltersUrl
()
{
window
.
history
.
pushState
(
null
,
null
,
`?
${
this
.
filter
.
path
}
`
);
}
}
,
};
BoardsStoreEE
.
initEESpecific
(
boardsStore
);
...
...
app/assets/javascripts/boards/stores/modal_store.js
View file @
7e9f44fa
...
...
@@ -40,7 +40,7 @@ class ModalStore {
toggleAll
()
{
const
select
=
this
.
selectedCount
()
!==
this
.
store
.
issues
.
length
;
this
.
store
.
issues
.
forEach
(
(
issue
)
=>
{
this
.
store
.
issues
.
forEach
(
issue
=>
{
const
issueUpdate
=
issue
;
if
(
issueUpdate
.
selected
!==
select
)
{
...
...
@@ -69,13 +69,14 @@ class ModalStore {
removeSelectedIssue
(
issue
,
forcePurge
=
false
)
{
if
(
this
.
store
.
activeTab
===
'
all
'
||
forcePurge
)
{
this
.
store
.
selectedIssues
=
this
.
store
.
selectedIssues
.
filter
(
fIssue
=>
fIssue
.
id
!==
issue
.
id
);
this
.
store
.
selectedIssues
=
this
.
store
.
selectedIssues
.
filter
(
fIssue
=>
fIssue
.
id
!==
issue
.
id
,
);
}
}
purgeUnselectedIssues
()
{
this
.
store
.
selectedIssues
.
forEach
(
(
issue
)
=>
{
this
.
store
.
selectedIssues
.
forEach
(
issue
=>
{
if
(
!
issue
.
selected
)
{
this
.
removeSelectedIssue
(
issue
,
true
);
}
...
...
@@ -87,8 +88,7 @@ class ModalStore {
}
findSelectedIssue
(
issue
)
{
return
this
.
store
.
selectedIssues
.
filter
(
filteredIssue
=>
filteredIssue
.
id
===
issue
.
id
)[
0
];
return
this
.
store
.
selectedIssues
.
filter
(
filteredIssue
=>
filteredIssue
.
id
===
issue
.
id
)[
0
];
}
}
...
...
app/assets/javascripts/commons/gitlab_ui.js
View file @
7e9f44fa
import
Vue
from
'
vue
'
;
import
{
GlProgressBar
,
GlLoadingIcon
,
GlTooltipDirective
,
}
from
'
@gitlab-org/gitlab-ui
'
;
import
{
GlProgressBar
,
GlLoadingIcon
,
GlTooltipDirective
}
from
'
@gitlab-org/gitlab-ui
'
;
Vue
.
component
(
'
gl-progress-bar
'
,
GlProgressBar
);
Vue
.
component
(
'
gl-loading-icon
'
,
GlLoadingIcon
);
...
...
app/assets/javascripts/diffs/components/tree_list.vue
View file @
7e9f44fa
...
...
@@ -18,8 +18,8 @@ export default {
},
data
()
{
const
treeListStored
=
localStorage
.
getItem
(
treeListStorageKey
);
const
renderTreeList
=
treeListStored
!==
null
?
convertPermissionToBoolean
(
treeListStored
)
:
true
;
const
renderTreeList
=
treeListStored
!==
null
?
convertPermissionToBoolean
(
treeListStored
)
:
true
;
return
{
search
:
''
,
...
...
app/assets/javascripts/environments/components/environments_app.vue
View file @
7e9f44fa
<
script
>
import
Flash
from
'
../../flash
'
;
import
{
s__
}
from
'
../../locale
'
;
import
emptyState
from
'
./empty_state.vue
'
;
import
eventHub
from
'
../event_hub
'
;
import
environmentsMixin
from
'
../mixins/environments_mixin
'
;
import
CIPaginationMixin
from
'
../../vue_shared/mixins/ci_pagination_api_mixin
'
;
import
StopEnvironmentModal
from
'
./stop_environment_modal.vue
'
;
import
Flash
from
'
../../flash
'
;
import
{
s__
}
from
'
../../locale
'
;
import
emptyState
from
'
./empty_state.vue
'
;
import
eventHub
from
'
../event_hub
'
;
import
environmentsMixin
from
'
../mixins/environments_mixin
'
;
import
CIPaginationMixin
from
'
../../vue_shared/mixins/ci_pagination_api_mixin
'
;
import
StopEnvironmentModal
from
'
./stop_environment_modal.vue
'
;
export
default
{
components
:
{
emptyState
,
StopEnvironmentModal
,
},
export
default
{
components
:
{
emptyState
,
StopEnvironmentModal
,
},
mixins
:
[
CIPaginationMixin
,
environmentsMixin
,
],
mixins
:
[
CIPaginationMixin
,
environmentsMixin
],
props
:
{
endpoint
:
{
type
:
String
,
required
:
true
,
},
canCreateEnvironment
:
{
type
:
Boolean
,
required
:
true
,
},
canCreateDeployment
:
{
type
:
Boolean
,
required
:
true
,
},
canReadEnvironment
:
{
type
:
Boolean
,
required
:
true
,
},
cssContainerClass
:
{
type
:
String
,
required
:
true
,
},
newEnvironmentPath
:
{
type
:
String
,
required
:
true
,
},
helpPagePath
:
{
type
:
String
,
required
:
true
,
},
props
:
{
endpoint
:
{
type
:
String
,
required
:
true
,
},
created
()
{
eventHub
.
$on
(
'
toggleFolder
'
,
this
.
toggleFolder
);
eventHub
.
$on
(
'
toggleDeployBoard
'
,
this
.
toggleDeployBoard
);
canCreateEnvironment
:
{
type
:
Boolean
,
required
:
true
,
},
beforeDestroy
()
{
eventHub
.
$off
(
'
toggleFolder
'
);
eventHub
.
$off
(
'
toggleDeployBoard
'
);
canCreateDeployment
:
{
type
:
Boolean
,
required
:
true
,
},
canReadEnvironment
:
{
type
:
Boolean
,
required
:
true
,
},
cssContainerClass
:
{
type
:
String
,
required
:
true
,
},
newEnvironmentPath
:
{
type
:
String
,
required
:
true
,
},
helpPagePath
:
{
type
:
String
,
required
:
true
,
},
},
created
()
{
eventHub
.
$on
(
'
toggleFolder
'
,
this
.
toggleFolder
);
eventHub
.
$on
(
'
toggleDeployBoard
'
,
this
.
toggleDeployBoard
);
},
beforeDestroy
()
{
eventHub
.
$off
(
'
toggleFolder
'
);
eventHub
.
$off
(
'
toggleDeployBoard
'
);
},
methods
:
{
/**
* Toggles the visibility of the deploy boards of the clicked environment.
* @param {Object} model
*/
toggleDeployBoard
(
model
)
{
this
.
store
.
toggleDeployBoard
(
model
.
id
);
},
methods
:
{
/**
* Toggles the visibility of the deploy boards of the clicked environment.
* @param {Object} model
*/
toggleDeployBoard
(
model
)
{
this
.
store
.
toggleDeployBoard
(
model
.
id
);
},
toggleFolder
(
folder
)
{
this
.
store
.
toggleFolder
(
folder
);
toggleFolder
(
folder
)
{
this
.
store
.
toggleFolder
(
folder
);
if
(
!
folder
.
isOpen
)
{
this
.
fetchChildEnvironments
(
folder
,
true
);
}
},
if
(
!
folder
.
isOpen
)
{
this
.
fetchChildEnvironments
(
folder
,
true
);
}
},
fetchChildEnvironments
(
folder
,
showLoader
=
false
)
{
this
.
store
.
updateEnvironmentProp
(
folder
,
'
isLoadingFolderContent
'
,
showLoader
);
fetchChildEnvironments
(
folder
,
showLoader
=
false
)
{
this
.
store
.
updateEnvironmentProp
(
folder
,
'
isLoadingFolderContent
'
,
showLoader
);
this
.
service
.
getFolderContent
(
folder
.
folder_path
)
.
then
(
response
=>
this
.
store
.
setfolderContent
(
folder
,
response
.
data
.
environments
))
.
then
(()
=>
this
.
store
.
updateEnvironmentProp
(
folder
,
'
isLoadingFolderContent
'
,
false
))
.
catch
(()
=>
{
Flash
(
s__
(
'
Environments|An error occurred while fetching the environments.
'
));
this
.
store
.
updateEnvironmentProp
(
folder
,
'
isLoadingFolderContent
'
,
false
);
});
},
this
.
service
.
getFolderContent
(
folder
.
folder_path
)
.
then
(
response
=>
this
.
store
.
setfolderContent
(
folder
,
response
.
data
.
environments
))
.
then
(()
=>
this
.
store
.
updateEnvironmentProp
(
folder
,
'
isLoadingFolderContent
'
,
false
))
.
catch
(()
=>
{
Flash
(
s__
(
'
Environments|An error occurred while fetching the environments.
'
));
this
.
store
.
updateEnvironmentProp
(
folder
,
'
isLoadingFolderContent
'
,
false
);
});
},
successCallback
(
resp
)
{
this
.
saveData
(
resp
);
successCallback
(
resp
)
{
this
.
saveData
(
resp
);
// We need to verify if any folder is open to also update it
const
openFolders
=
this
.
store
.
getOpenFolders
();
if
(
openFolders
.
length
)
{
openFolders
.
forEach
(
folder
=>
this
.
fetchChildEnvironments
(
folder
));
}
},
// We need to verify if any folder is open to also update it
const
openFolders
=
this
.
store
.
getOpenFolders
();
if
(
openFolders
.
length
)
{
openFolders
.
forEach
(
folder
=>
this
.
fetchChildEnvironments
(
folder
));
}
},
};
},
};
</
script
>
<
template
>
<div
:class=
"cssContainerClass"
>
...
...
app/assets/javascripts/environments/stores/environments_store.js
View file @
7e9f44fa
...
...
@@ -42,9 +42,9 @@ export default class EnvironmentsStore {
* @returns {Array}
*/
storeEnvironments
(
environments
=
[])
{
const
filteredEnvironments
=
environments
.
map
(
(
env
)
=>
{
const
oldEnvironmentState
=
this
.
state
.
environments
.
find
((
element
)
=>
{
const
filteredEnvironments
=
environments
.
map
(
env
=>
{
const
oldEnvironmentState
=
this
.
state
.
environments
.
find
(
element
=>
{
if
(
env
.
latest
)
{
return
element
.
id
===
env
.
latest
.
id
;
}
...
...
@@ -73,10 +73,12 @@ export default class EnvironmentsStore {
if
(
filtered
.
size
===
1
&&
filtered
.
rollout_status
)
{
filtered
=
Object
.
assign
({},
filtered
,
{
hasDeployBoard
:
true
,
isDeployBoardVisible
:
oldEnvironmentState
.
isDeployBoardVisible
===
false
?
oldEnvironmentState
.
isDeployBoardVisible
:
true
,
deployBoardData
:
filtered
.
rollout_status
.
status
===
'
found
'
?
filtered
.
rollout_status
:
{},
isDeployBoardVisible
:
oldEnvironmentState
.
isDeployBoardVisible
===
false
?
oldEnvironmentState
.
isDeployBoardVisible
:
true
,
deployBoardData
:
filtered
.
rollout_status
.
status
===
'
found
'
?
filtered
.
rollout_status
:
{},
isLoadingDeployBoard
:
filtered
.
rollout_status
.
status
===
'
loading
'
,
isEmptyDeployBoard
:
filtered
.
rollout_status
.
status
===
'
not_found
'
,
});
...
...
@@ -152,7 +154,7 @@ export default class EnvironmentsStore {
* @return {Object}
*/
setfolderContent
(
folder
,
environments
)
{
const
updatedEnvironments
=
environments
.
map
(
(
env
)
=>
{
const
updatedEnvironments
=
environments
.
map
(
env
=>
{
let
updated
=
env
;
if
(
env
.
latest
)
{
...
...
@@ -181,7 +183,7 @@ export default class EnvironmentsStore {
updateEnvironmentProp
(
environment
,
prop
,
newValue
)
{
const
{
environments
}
=
this
.
state
;
const
updatedEnvironments
=
environments
.
map
(
(
env
)
=>
{
const
updatedEnvironments
=
environments
.
map
(
env
=>
{
const
updateEnv
=
Object
.
assign
({},
env
);
if
(
env
.
id
===
environment
.
id
)
{
updateEnv
[
prop
]
=
newValue
;
...
...
@@ -208,7 +210,7 @@ export default class EnvironmentsStore {
toggleDeployBoard
(
environmentID
)
{
const
environments
=
this
.
state
.
environments
.
slice
();
this
.
state
.
environments
=
environments
.
map
(
(
env
)
=>
{
this
.
state
.
environments
=
environments
.
map
(
env
=>
{
let
updated
=
Object
.
assign
({},
env
);
if
(
env
.
id
===
environmentID
)
{
...
...
app/assets/javascripts/filtered_search/dropdown_user.js
View file @
7e9f44fa
...
...
@@ -40,8 +40,9 @@ export default class DropdownUser extends FilteredSearchDropdown {
}
itemClicked
(
e
)
{
super
.
itemClicked
(
e
,
selected
=>
selected
.
querySelector
(
'
.dropdown-light-content
'
).
innerText
.
trim
());
super
.
itemClicked
(
e
,
selected
=>
selected
.
querySelector
(
'
.dropdown-light-content
'
).
innerText
.
trim
(),
);
}
renderContent
(
forceShowList
=
false
)
{
...
...
@@ -82,7 +83,7 @@ export default class DropdownUser extends FilteredSearchDropdown {
// Removes the first character if it is a quotation so that we can search
// with multiple words
if
(
value
[
0
]
===
'
"
'
||
value
[
0
]
===
'
\'
'
)
{
if
(
value
[
0
]
===
'
"
'
||
value
[
0
]
===
"
'
"
)
{
value
=
value
.
slice
(
1
);
}
...
...
app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
View file @
7e9f44fa
...
...
@@ -156,10 +156,7 @@ export default class FilteredSearchDropdownManager {
}
static
addWordToInput
(
tokenName
,
tokenValue
=
''
,
clicked
=
false
,
options
=
{})
{
const
{
uppercaseTokenName
=
false
,
capitalizeTokenValue
=
false
,
}
=
options
;
const
{
uppercaseTokenName
=
false
,
capitalizeTokenValue
=
false
}
=
options
;
const
input
=
FilteredSearchContainer
.
container
.
querySelector
(
'
.filtered-search
'
);
FilteredSearchVisualTokens
.
addFilterVisualToken
(
tokenName
,
tokenValue
,
{
uppercaseTokenName
,
...
...
app/assets/javascripts/filtered_search/filtered_search_manager.js
View file @
7e9f44fa
import
_
from
'
underscore
'
;
import
{
getParameterByName
,
getUrlParamsArray
,
}
from
'
~/lib/utils/common_utils
'
;
import
{
getParameterByName
,
getUrlParamsArray
}
from
'
~/lib/utils/common_utils
'
;
import
IssuableFilteredSearchTokenKeys
from
'
~/filtered_search/issuable_filtered_search_token_keys
'
;
import
{
visitUrl
}
from
'
../lib/utils/url_utility
'
;
import
Flash
from
'
../flash
'
;
...
...
@@ -51,9 +48,12 @@ export default class FilteredSearchManager {
isLocalStorageAvailable
:
RecentSearchesService
.
isAvailable
(),
allowedKeys
:
this
.
filteredSearchTokenKeys
.
getKeys
(),
});
this
.
searchHistoryDropdownElement
=
document
.
querySelector
(
'
.js-filtered-search-history-dropdown
'
);
const
fullPath
=
this
.
searchHistoryDropdownElement
?
this
.
searchHistoryDropdownElement
.
dataset
.
fullPath
:
'
project
'
;
this
.
searchHistoryDropdownElement
=
document
.
querySelector
(
'
.js-filtered-search-history-dropdown
'
,
);
const
fullPath
=
this
.
searchHistoryDropdownElement
?
this
.
searchHistoryDropdownElement
.
dataset
.
fullPath
:
'
project
'
;
const
recentSearchesKey
=
`
${
fullPath
}
-
${
this
.
recentsStorageKeyNames
[
this
.
page
]}
`
;
this
.
recentSearchesService
=
new
RecentSearchesService
(
recentSearchesKey
);
}
...
...
@@ -80,15 +80,16 @@ export default class FilteredSearchManager {
setup
()
{
// Fetch recent searches from localStorage
this
.
fetchingRecentSearchesPromise
=
this
.
recentSearchesService
.
fetch
()
.
catch
((
error
)
=>
{
this
.
fetchingRecentSearchesPromise
=
this
.
recentSearchesService
.
fetch
()
.
catch
(
error
=>
{
if
(
error
.
name
===
'
RecentSearchesServiceError
'
)
return
undefined
;
// eslint-disable-next-line no-new
new
Flash
(
'
An error occurred while parsing recent searches
'
);
// Gracefully fail to empty array
return
[];
})
.
then
(
(
searches
)
=>
{
.
then
(
searches
=>
{
if
(
!
searches
)
{
return
;
}
...
...
@@ -143,7 +144,7 @@ export default class FilteredSearchManager {
if
(
this
.
stateFilters
)
{
this
.
searchStateWrapper
=
this
.
searchState
.
bind
(
this
);
this
.
applyToStateFilters
(
(
filterEl
)
=>
{
this
.
applyToStateFilters
(
filterEl
=>
{
filterEl
.
addEventListener
(
'
click
'
,
this
.
searchStateWrapper
);
});
}
...
...
@@ -151,14 +152,14 @@ export default class FilteredSearchManager {
unbindStateEvents
()
{
if
(
this
.
stateFilters
)
{
this
.
applyToStateFilters
(
(
filterEl
)
=>
{
this
.
applyToStateFilters
(
filterEl
=>
{
filterEl
.
removeEventListener
(
'
click
'
,
this
.
searchStateWrapper
);
});
}
}
applyToStateFilters
(
callback
)
{
this
.
stateFilters
.
querySelectorAll
(
'
a[data-state]
'
).
forEach
(
(
filterEl
)
=>
{
this
.
stateFilters
.
querySelectorAll
(
'
a[data-state]
'
).
forEach
(
filterEl
=>
{
if
(
this
.
states
.
indexOf
(
filterEl
.
dataset
.
state
)
>
-
1
)
{
callback
(
filterEl
);
}
...
...
@@ -230,7 +231,7 @@ export default class FilteredSearchManager {
let
backspaceCount
=
0
;
// closure for keeping track of the number of backspace keystrokes
return
(
e
)
=>
{
return
e
=>
{
// 8 = Backspace Key
// 46 = Delete Key
if
(
e
.
keyCode
===
8
||
e
.
keyCode
===
46
)
{
...
...
@@ -297,8 +298,12 @@ export default class FilteredSearchManager {
const
isElementInDynamicFilterDropdown
=
e
.
target
.
closest
(
'
.filter-dropdown
'
)
!==
null
;
const
isElementInStaticFilterDropdown
=
e
.
target
.
closest
(
'
ul[data-dropdown]
'
)
!==
null
;
if
(
!
isElementInFilteredSearch
&&
!
isElementInDynamicFilterDropdown
&&
!
isElementInStaticFilterDropdown
&&
inputContainer
)
{
if
(
!
isElementInFilteredSearch
&&
!
isElementInDynamicFilterDropdown
&&
!
isElementInStaticFilterDropdown
&&
inputContainer
)
{
inputContainer
.
classList
.
remove
(
'
focus
'
);
}
}
...
...
@@ -391,7 +396,7 @@ export default class FilteredSearchManager {
const
removeElements
=
[];
[].
forEach
.
call
(
this
.
tokensContainer
.
children
,
(
t
)
=>
{
[].
forEach
.
call
(
this
.
tokensContainer
.
children
,
t
=>
{
let
canClearToken
=
t
.
classList
.
contains
(
'
js-visual-token
'
);
if
(
canClearToken
)
{
...
...
@@ -404,7 +409,7 @@ export default class FilteredSearchManager {
}
});
removeElements
.
forEach
(
(
el
)
=>
{
removeElements
.
forEach
(
el
=>
{
el
.
parentElement
.
removeChild
(
el
);
});
...
...
@@ -420,13 +425,14 @@ export default class FilteredSearchManager {
handleInputVisualToken
()
{
const
input
=
this
.
filteredSearchInput
;
const
{
tokens
,
searchToken
}
=
this
.
tokenizer
.
processTokens
(
input
.
value
,
this
.
filteredSearchTokenKeys
.
getKeys
());
const
{
isLastVisualTokenValid
}
=
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
const
{
tokens
,
searchToken
}
=
this
.
tokenizer
.
processTokens
(
input
.
value
,
this
.
filteredSearchTokenKeys
.
getKeys
(),
);
const
{
isLastVisualTokenValid
}
=
FilteredSearchVisualTokens
.
getLastVisualTokenBeforeInput
();
if
(
isLastVisualTokenValid
)
{
tokens
.
forEach
(
(
t
)
=>
{
tokens
.
forEach
(
t
=>
{
input
.
value
=
input
.
value
.
replace
(
`
${
t
.
key
}
:
${
t
.
symbol
}${
t
.
value
}
`
,
''
);
FilteredSearchVisualTokens
.
addFilterVisualToken
(
t
.
key
,
`
${
t
.
symbol
}${
t
.
value
}
`
,
{
uppercaseTokenName
:
this
.
filteredSearchTokenKeys
.
shouldUppercaseTokenName
(
t
.
key
),
...
...
@@ -476,15 +482,17 @@ export default class FilteredSearchManager {
saveCurrentSearchQuery
()
{
// Don't save before we have fetched the already saved searches
this
.
fetchingRecentSearchesPromise
.
then
(()
=>
{
const
searchQuery
=
DropdownUtils
.
getSearchQuery
();
if
(
searchQuery
.
length
>
0
)
{
const
resultantSearches
=
this
.
recentSearchesStore
.
addRecentSearch
(
searchQuery
);
this
.
recentSearchesService
.
save
(
resultantSearches
);
}
}).
catch
(()
=>
{
// https://gitlab.com/gitlab-org/gitlab-ce/issues/30821
});
this
.
fetchingRecentSearchesPromise
.
then
(()
=>
{
const
searchQuery
=
DropdownUtils
.
getSearchQuery
();
if
(
searchQuery
.
length
>
0
)
{
const
resultantSearches
=
this
.
recentSearchesStore
.
addRecentSearch
(
searchQuery
);
this
.
recentSearchesService
.
save
(
resultantSearches
);
}
})
.
catch
(()
=>
{
// https://gitlab.com/gitlab-org/gitlab-ce/issues/30821
});
}
// allows for modifying params array when a param can't be included in the URL (e.g. Service Desk)
...
...
@@ -498,7 +506,7 @@ export default class FilteredSearchManager {
const
usernameParams
=
this
.
getUsernameParams
();
let
hasFilteredSearch
=
false
;
params
.
forEach
(
(
p
)
=>
{
params
.
forEach
(
p
=>
{
const
split
=
p
.
split
(
'
=
'
);
const
keyParam
=
decodeURIComponent
(
split
[
0
]);
const
value
=
split
[
1
];
...
...
@@ -509,11 +517,9 @@ export default class FilteredSearchManager {
if
(
condition
)
{
hasFilteredSearch
=
true
;
const
canEdit
=
this
.
canEdit
&&
this
.
canEdit
(
condition
.
tokenKey
);
FilteredSearchVisualTokens
.
addFilterVisualToken
(
condition
.
tokenKey
,
condition
.
value
,
{
canEdit
},
);
FilteredSearchVisualTokens
.
addFilterVisualToken
(
condition
.
tokenKey
,
condition
.
value
,
{
canEdit
,
});
}
else
{
// Sanitize value since URL converts spaces into +
// Replace before decode so that we know what was originally + versus the encoded +
...
...
@@ -533,7 +539,7 @@ export default class FilteredSearchManager {
if
(
sanitizedValue
.
indexOf
(
'
'
)
!==
-
1
)
{
// Prefer ", but use ' if required
quotationsToUse
=
sanitizedValue
.
indexOf
(
'
"
'
)
===
-
1
?
'
"
'
:
'
\'
'
;
quotationsToUse
=
sanitizedValue
.
indexOf
(
'
"
'
)
===
-
1
?
'
"
'
:
"
'
"
;
}
hasFilteredSearch
=
true
;
...
...
@@ -554,7 +560,9 @@ export default class FilteredSearchManager {
hasFilteredSearch
=
true
;
const
tokenName
=
'
assignee
'
;
const
canEdit
=
this
.
canEdit
&&
this
.
canEdit
(
tokenName
);
FilteredSearchVisualTokens
.
addFilterVisualToken
(
tokenName
,
`@
${
usernameParams
[
id
]}
`
,
{
canEdit
});
FilteredSearchVisualTokens
.
addFilterVisualToken
(
tokenName
,
`@
${
usernameParams
[
id
]}
`
,
{
canEdit
,
});
}
}
else
if
(
!
match
&&
keyParam
===
'
author_id
'
)
{
const
id
=
parseInt
(
value
,
10
);
...
...
@@ -562,7 +570,9 @@ export default class FilteredSearchManager {
hasFilteredSearch
=
true
;
const
tokenName
=
'
author
'
;
const
canEdit
=
this
.
canEdit
&&
this
.
canEdit
(
tokenName
);
FilteredSearchVisualTokens
.
addFilterVisualToken
(
tokenName
,
`@
${
usernameParams
[
id
]}
`
,
{
canEdit
});
FilteredSearchVisualTokens
.
addFilterVisualToken
(
tokenName
,
`@
${
usernameParams
[
id
]}
`
,
{
canEdit
,
});
}
}
else
if
(
!
match
&&
keyParam
===
'
search
'
)
{
hasFilteredSearch
=
true
;
...
...
@@ -603,9 +613,11 @@ export default class FilteredSearchManager {
const
currentState
=
state
||
getParameterByName
(
'
state
'
)
||
'
opened
'
;
paths
.
push
(
`state=
${
currentState
}
`
);
tokens
.
forEach
((
token
)
=>
{
const
condition
=
this
.
filteredSearchTokenKeys
.
searchByConditionKeyValue
(
token
.
key
,
token
.
value
.
toLowerCase
());
tokens
.
forEach
(
token
=>
{
const
condition
=
this
.
filteredSearchTokenKeys
.
searchByConditionKeyValue
(
token
.
key
,
token
.
value
.
toLowerCase
(),
);
const
tokenConfig
=
this
.
filteredSearchTokenKeys
.
searchByKey
(
token
.
key
)
||
{};
const
{
param
}
=
tokenConfig
;
...
...
@@ -624,8 +636,10 @@ export default class FilteredSearchManager {
tokenValue
=
tokenValue
.
toLowerCase
();
}
if
((
tokenValue
[
0
]
===
'
\'
'
&&
tokenValue
[
tokenValue
.
length
-
1
]
===
'
\'
'
)
||
(
tokenValue
[
0
]
===
'
"
'
&&
tokenValue
[
tokenValue
.
length
-
1
]
===
'
"
'
))
{
if
(
(
tokenValue
[
0
]
===
"
'
"
&&
tokenValue
[
tokenValue
.
length
-
1
]
===
"
'
"
)
||
(
tokenValue
[
0
]
===
'
"
'
&&
tokenValue
[
tokenValue
.
length
-
1
]
===
'
"
'
)
)
{
tokenValue
=
tokenValue
.
slice
(
1
,
tokenValue
.
length
-
1
);
}
...
...
@@ -636,7 +650,10 @@ export default class FilteredSearchManager {
});
if
(
searchToken
)
{
const
sanitized
=
searchToken
.
split
(
'
'
).
map
(
t
=>
encodeURIComponent
(
t
)).
join
(
'
+
'
);
const
sanitized
=
searchToken
.
split
(
'
'
)
.
map
(
t
=>
encodeURIComponent
(
t
))
.
join
(
'
+
'
);
paths
.
push
(
`search=
${
sanitized
}
`
);
}
...
...
@@ -653,7 +670,7 @@ export default class FilteredSearchManager {
const
usernamesById
=
{};
try
{
const
attribute
=
this
.
filteredSearchInput
.
getAttribute
(
'
data-username-params
'
);
JSON
.
parse
(
attribute
).
forEach
(
(
user
)
=>
{
JSON
.
parse
(
attribute
).
forEach
(
user
=>
{
usernamesById
[
user
.
id
]
=
user
.
username
;
});
}
catch
(
e
)
{
...
...
app/assets/javascripts/flash.js
View file @
7e9f44fa
...
...
@@ -40,7 +40,9 @@ const createFlashEl = (message, type, isFixedLayout = false) => `
class="flash-
${
type
}
"
>
<div
class="flash-text
${
isFixedLayout
?
'
container-fluid container-limited limit-container-width
'
:
''
}
"
class="flash-text
${
isFixedLayout
?
'
container-fluid container-limited limit-container-width
'
:
''
}
"
>
${
_
.
escape
(
message
)}
</div>
...
...
@@ -78,7 +80,9 @@ const createFlash = function createFlash(
if
(
!
flashContainer
)
return
null
;
const
isFixedLayout
=
navigation
?
navigation
.
parentNode
.
classList
.
contains
(
'
container-limited
'
)
:
true
;
const
isFixedLayout
=
navigation
?
navigation
.
parentNode
.
classList
.
contains
(
'
container-limited
'
)
:
true
;
flashContainer
.
innerHTML
=
createFlashEl
(
message
,
type
,
isFixedLayout
);
...
...
app/assets/javascripts/gfm_auto_complete.js
View file @
7e9f44fa
...
...
@@ -101,7 +101,7 @@ class GfmAutoComplete {
...
this
.
getDefaultCallbacks
(),
beforeSave
(
commands
)
{
if
(
GfmAutoComplete
.
isLoading
(
commands
))
return
commands
;
return
$
.
map
(
commands
,
(
c
)
=>
{
return
$
.
map
(
commands
,
c
=>
{
let
search
=
c
.
name
;
if
(
c
.
aliases
.
length
>
0
)
{
search
=
`
${
search
}
${
c
.
aliases
.
join
(
'
'
)}
`
;
...
...
@@ -174,7 +174,7 @@ class GfmAutoComplete {
callbacks
:
{
...
this
.
getDefaultCallbacks
(),
beforeSave
(
members
)
{
return
$
.
map
(
members
,
(
m
)
=>
{
return
$
.
map
(
members
,
m
=>
{
let
title
=
''
;
if
(
m
.
username
==
null
)
{
return
m
;
...
...
@@ -185,7 +185,9 @@ class GfmAutoComplete {
}
const
autoCompleteAvatar
=
m
.
avatar_url
||
m
.
username
.
charAt
(
0
).
toUpperCase
();
const
imgAvatar
=
`<img src="
${
m
.
avatar_url
}
" alt="
${
m
.
username
}
" class="avatar avatar-inline center s26"/>`
;
const
imgAvatar
=
`<img src="
${
m
.
avatar_url
}
" alt="
${
m
.
username
}
" class="avatar avatar-inline center s26"/>`
;
const
txtAvatar
=
`<div class="avatar center avatar-inline s26">
${
autoCompleteAvatar
}
</div>`
;
return
{
...
...
@@ -218,7 +220,7 @@ class GfmAutoComplete {
callbacks
:
{
...
this
.
getDefaultCallbacks
(),
beforeSave
(
issues
)
{
return
$
.
map
(
issues
,
(
i
)
=>
{
return
$
.
map
(
issues
,
i
=>
{
if
(
i
.
title
==
null
)
{
return
i
;
}
...
...
@@ -251,7 +253,7 @@ class GfmAutoComplete {
callbacks
:
{
...
this
.
getDefaultCallbacks
(),
beforeSave
(
milestones
)
{
return
$
.
map
(
milestones
,
(
m
)
=>
{
return
$
.
map
(
milestones
,
m
=>
{
if
(
m
.
title
==
null
)
{
return
m
;
}
...
...
@@ -284,7 +286,7 @@ class GfmAutoComplete {
callbacks
:
{
...
this
.
getDefaultCallbacks
(),
beforeSave
(
merges
)
{
return
$
.
map
(
merges
,
(
m
)
=>
{
return
$
.
map
(
merges
,
m
=>
{
if
(
m
.
title
==
null
)
{
return
m
;
}
...
...
@@ -331,13 +333,20 @@ class GfmAutoComplete {
},
matcher
(
flag
,
subtext
)
{
const
match
=
GfmAutoComplete
.
defaultMatcher
(
flag
,
subtext
,
this
.
app
.
controllers
);
const
subtextNodes
=
subtext
.
split
(
/
\n
+/g
).
pop
().
split
(
GfmAutoComplete
.
regexSubtext
);
const
subtextNodes
=
subtext
.
split
(
/
\n
+/g
)
.
pop
()
.
split
(
GfmAutoComplete
.
regexSubtext
);
// Check if ~ is followed by '/label', '/relabel' or '/unlabel' commands.
command
=
subtextNodes
.
find
((
node
)
=>
{
if
(
node
===
LABEL_COMMAND
.
LABEL
||
node
===
LABEL_COMMAND
.
RELABEL
||
node
===
LABEL_COMMAND
.
UNLABEL
)
{
return
node
;
}
command
=
subtextNodes
.
find
(
node
=>
{
if
(
node
===
LABEL_COMMAND
.
LABEL
||
node
===
LABEL_COMMAND
.
RELABEL
||
node
===
LABEL_COMMAND
.
UNLABEL
)
{
return
node
;
}
return
null
;
});
...
...
@@ -387,7 +396,7 @@ class GfmAutoComplete {
callbacks
:
{
...
this
.
getDefaultCallbacks
(),
beforeSave
(
snippets
)
{
return
$
.
map
(
snippets
,
(
m
)
=>
{
return
$
.
map
(
snippets
,
m
=>
{
if
(
m
.
title
==
null
)
{
return
m
;
}
...
...
@@ -465,13 +474,17 @@ class GfmAutoComplete {
this
.
loadData
(
$input
,
at
,
validEmojiNames
);
GfmAutoComplete
.
glEmojiTag
=
glEmojiTag
;
})
.
catch
(()
=>
{
this
.
isLoadingData
[
at
]
=
false
;
});
.
catch
(()
=>
{
this
.
isLoadingData
[
at
]
=
false
;
});
}
else
if
(
dataSource
)
{
AjaxCache
.
retrieve
(
dataSource
,
true
)
.
then
(
(
data
)
=>
{
.
then
(
data
=>
{
this
.
loadData
(
$input
,
at
,
data
);
})
.
catch
(()
=>
{
this
.
isLoadingData
[
at
]
=
false
;
});
.
catch
(()
=>
{
this
.
isLoadingData
[
at
]
=
false
;
});
}
else
{
this
.
isLoadingData
[
at
]
=
false
;
}
...
...
@@ -504,15 +517,16 @@ class GfmAutoComplete {
}
const
loadingState
=
GfmAutoComplete
.
defaultLoadingData
[
0
];
return
dataToInspect
&&
(
dataToInspect
===
loadingState
||
dataToInspect
.
name
===
loadingState
);
return
dataToInspect
&&
(
dataToInspect
===
loadingState
||
dataToInspect
.
name
===
loadingState
);
}
static
defaultMatcher
(
flag
,
subtext
,
controllers
)
{
// The below is taken from At.js source
// Tweaked to commands to start without a space only if char before is a non-word character
// https://github.com/ichord/At.js
const
atSymbolsWithBar
=
Object
.
keys
(
controllers
).
join
(
'
|
'
).
replace
(
/
[
$
]
/
,
'
\\
$&
'
);
const
atSymbolsWithBar
=
Object
.
keys
(
controllers
)
.
join
(
'
|
'
)
.
replace
(
/
[
$
]
/
,
'
\\
$&
'
);
const
atSymbolsWithoutBar
=
Object
.
keys
(
controllers
).
join
(
''
);
const
targetSubtext
=
subtext
.
split
(
GfmAutoComplete
.
regexSubtext
).
pop
();
const
resultantFlag
=
flag
.
replace
(
/
[
-[
\]/
{}()*+?.
\\
^$|
]
/g
,
'
\\
$&
'
);
...
...
@@ -520,7 +534,10 @@ class GfmAutoComplete {
const
accentAChar
=
decodeURI
(
'
%C3%80
'
);
const
accentYChar
=
decodeURI
(
'
%C3%BF
'
);
const
regexp
=
new
RegExp
(
`^(?:\\B|[^a-zA-Z0-9_\`
${
atSymbolsWithoutBar
}
]|\\s)
${
resultantFlag
}
(?!
${
atSymbolsWithBar
}
)((?:[A-Za-z
${
accentAChar
}
-
${
accentYChar
}
0-9_'.+-]|[^\\x00-\\x7a])*)$`
,
'
gi
'
);
const
regexp
=
new
RegExp
(
`^(?:\\B|[^a-zA-Z0-9_\`
${
atSymbolsWithoutBar
}
]|\\s)
${
resultantFlag
}
(?!
${
atSymbolsWithBar
}
)((?:[A-Za-z
${
accentAChar
}
-
${
accentYChar
}
0-9_'.+-]|[^\\x00-\\x7a])*)$`
,
'
gi
'
,
);
return
regexp
.
exec
(
targetSubtext
);
}
...
...
@@ -560,7 +577,8 @@ GfmAutoComplete.Members = {
};
GfmAutoComplete
.
Labels
=
{
// eslint-disable-next-line no-template-curly-in-string
template
:
'
<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>
'
,
template
:
'
<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>
'
,
};
// Issues, MergeRequests and Snippets
GfmAutoComplete
.
Issues
=
{
...
...
@@ -574,7 +592,8 @@ GfmAutoComplete.Milestones = {
template
:
'
<li>${title}</li>
'
,
};
GfmAutoComplete
.
Loading
=
{
template
:
'
<li style="pointer-events: none;"><i class="fa fa-spinner fa-spin"></i> Loading...</li>
'
,
template
:
'
<li style="pointer-events: none;"><i class="fa fa-spinner fa-spin"></i> Loading...</li>
'
,
};
export
default
GfmAutoComplete
;
app/assets/javascripts/jobs/components/job_app.vue
View file @
7e9f44fa
<
script
>
import
_
from
'
underscore
'
;
import
{
mapGetters
,
mapState
,
mapActions
}
from
'
vuex
'
;
import
{
isScrolledToBottom
}
from
'
~/lib/utils/scroll_utils
'
;
import
bp
from
'
~/breakpoints
'
;
import
CiHeader
from
'
~/vue_shared/components/header_ci_component.vue
'
;
import
Callout
from
'
~/vue_shared/components/callout.vue
'
;
// ee-only start
import
SharedRunner
from
'
ee/jobs/components/shared_runner_limit_block.vue
'
;
// ee-only end
import
createStore
from
'
../store
'
;
import
EmptyState
from
'
./empty_state.vue
'
;
import
EnvironmentsBlock
from
'
./environments_block.vue
'
;
import
ErasedBlock
from
'
./erased_block.vue
'
;
import
Log
from
'
./job_log.vue
'
;
import
LogTopBar
from
'
./job_log_controllers.vue
'
;
import
StuckBlock
from
'
./stuck_block.vue
'
;
import
Sidebar
from
'
./sidebar.vue
'
;
import
_
from
'
underscore
'
;
import
{
mapGetters
,
mapState
,
mapActions
}
from
'
vuex
'
;
import
{
isScrolledToBottom
}
from
'
~/lib/utils/scroll_utils
'
;
import
bp
from
'
~/breakpoints
'
;
import
CiHeader
from
'
~/vue_shared/components/header_ci_component.vue
'
;
import
Callout
from
'
~/vue_shared/components/callout.vue
'
;
// ee-only start
import
SharedRunner
from
'
ee/jobs/components/shared_runner_limit_block.vue
'
;
// ee-only end
import
createStore
from
'
../store
'
;
import
EmptyState
from
'
./empty_state.vue
'
;
import
EnvironmentsBlock
from
'
./environments_block.vue
'
;
import
ErasedBlock
from
'
./erased_block.vue
'
;
import
Log
from
'
./job_log.vue
'
;
import
LogTopBar
from
'
./job_log_controllers.vue
'
;
import
StuckBlock
from
'
./stuck_block.vue
'
;
import
Sidebar
from
'
./sidebar.vue
'
;
export
default
{
name
:
'
JobPageApp
'
,
store
:
createStore
(),
components
:
{
CiHeader
,
Callout
,
EmptyState
,
EnvironmentsBlock
,
ErasedBlock
,
Log
,
LogTopBar
,
StuckBlock
,
SharedRunner
,
Sidebar
,
export
default
{
name
:
'
JobPageApp
'
,
store
:
createStore
(),
components
:
{
CiHeader
,
Callout
,
EmptyState
,
EnvironmentsBlock
,
ErasedBlock
,
Log
,
LogTopBar
,
StuckBlock
,
SharedRunner
,
Sidebar
,
},
props
:
{
runnerSettingsUrl
:
{
type
:
String
,
required
:
false
,
default
:
null
,
},
props
:
{
runnerSettingsUrl
:
{
type
:
String
,
required
:
false
,
default
:
null
,
},
runnerHelpUrl
:
{
type
:
String
,
required
:
false
,
default
:
null
,
},
endpoint
:
{
type
:
String
,
required
:
true
,
},
terminalPath
:
{
type
:
String
,
required
:
false
,
default
:
null
,
},
pagePath
:
{
type
:
String
,
required
:
true
,
},
logState
:
{
type
:
String
,
required
:
true
,
},
runnerHelpUrl
:
{
type
:
String
,
required
:
false
,
default
:
null
,
},
computed
:
{
...
mapState
([
'
isLoading
'
,
'
job
'
,
'
isSidebarOpen
'
,
'
trace
'
,
'
isTraceComplete
'
,
'
traceSize
'
,
'
isTraceSizeVisible
'
,
'
isScrollBottomDisabled
'
,
'
isScrollTopDisabled
'
,
'
isScrolledToBottomBeforeReceivingTrace
'
,
'
hasError
'
,
]),
...
mapGetters
([
'
headerActions
'
,
'
headerTime
'
,
'
shouldRenderCalloutMessage
'
,
'
shouldRenderTriggeredLabel
'
,
'
hasEnvironment
'
,
'
shouldRenderSharedRunnerLimitWarning
'
,
'
hasTrace
'
,
'
emptyStateIllustration
'
,
'
isScrollingDown
'
,
'
emptyStateAction
'
,
'
hasRunnersForProject
'
,
]),
endpoint
:
{
type
:
String
,
required
:
true
,
},
terminalPath
:
{
type
:
String
,
required
:
false
,
default
:
null
,
},
pagePath
:
{
type
:
String
,
required
:
true
,
},
logState
:
{
type
:
String
,
required
:
true
,
},
},
computed
:
{
...
mapState
([
'
isLoading
'
,
'
job
'
,
'
isSidebarOpen
'
,
'
trace
'
,
'
isTraceComplete
'
,
'
traceSize
'
,
'
isTraceSizeVisible
'
,
'
isScrollBottomDisabled
'
,
'
isScrollTopDisabled
'
,
'
isScrolledToBottomBeforeReceivingTrace
'
,
'
hasError
'
,
]),
...
mapGetters
([
'
headerActions
'
,
'
headerTime
'
,
'
shouldRenderCalloutMessage
'
,
'
shouldRenderTriggeredLabel
'
,
'
hasEnvironment
'
,
'
shouldRenderSharedRunnerLimitWarning
'
,
'
hasTrace
'
,
'
emptyStateIllustration
'
,
'
isScrollingDown
'
,
'
emptyStateAction
'
,
'
hasRunnersForProject
'
,
]),
shouldRenderContent
()
{
return
!
this
.
isLoading
&&
!
this
.
hasError
;
}
shouldRenderContent
()
{
return
!
this
.
isLoading
&&
!
this
.
hasError
;
},
watch
:
{
// Once the job log is loaded,
// fetch the stages for the dropdown on the sidebar
job
(
newVal
,
oldVal
)
{
if
(
_
.
isEmpty
(
oldVal
)
&&
!
_
.
isEmpty
(
newVal
.
pipeline
)
)
{
this
.
fetchStages
();
}
}
,
},
watch
:
{
// Once the job log is loaded,
// fetch the stages for the dropdown on the sidebar
job
(
newVal
,
oldVal
)
{
if
(
_
.
isEmpty
(
oldVal
)
&&
!
_
.
isEmpty
(
newVal
.
pipeline
))
{
this
.
fetchStages
();
}
},
created
()
{
this
.
throttled
=
_
.
throttle
(
this
.
toggleScrollButtons
,
100
);
},
created
()
{
this
.
throttled
=
_
.
throttle
(
this
.
toggleScrollButtons
,
100
);
this
.
setJobEndpoint
(
this
.
endpoint
);
this
.
setTraceOptions
({
logState
:
this
.
logState
,
pagePath
:
this
.
pagePath
,
});
this
.
setJobEndpoint
(
this
.
endpoint
);
this
.
setTraceOptions
({
logState
:
this
.
logState
,
pagePath
:
this
.
pagePath
,
});
this
.
fetchJob
();
this
.
fetchTrace
();
this
.
fetchJob
();
this
.
fetchTrace
();
window
.
addEventListener
(
'
resize
'
,
this
.
onResize
);
window
.
addEventListener
(
'
scroll
'
,
this
.
updateScroll
);
},
window
.
addEventListener
(
'
resize
'
,
this
.
onResize
);
window
.
addEventListener
(
'
scroll
'
,
this
.
updateScroll
);
},
mounted
()
{
mounted
()
{
this
.
updateSidebar
();
},
destroyed
()
{
window
.
removeEventListener
(
'
resize
'
,
this
.
onResize
);
window
.
removeEventListener
(
'
scroll
'
,
this
.
updateScroll
);
},
methods
:
{
...
mapActions
([
'
setJobEndpoint
'
,
'
setTraceOptions
'
,
'
fetchJob
'
,
'
fetchStages
'
,
'
hideSidebar
'
,
'
showSidebar
'
,
'
toggleSidebar
'
,
'
fetchTrace
'
,
'
scrollBottom
'
,
'
scrollTop
'
,
'
toggleScrollButtons
'
,
'
toggleScrollAnimation
'
,
]),
onResize
()
{
this
.
updateSidebar
();
this
.
updateScroll
();
},
destroyed
()
{
window
.
removeEventListener
(
'
resize
'
,
this
.
onResize
);
window
.
removeEventListener
(
'
scroll
'
,
this
.
updateScroll
);
updateSidebar
()
{
if
(
bp
.
getBreakpointSize
()
===
'
xs
'
)
{
this
.
hideSidebar
();
}
else
if
(
!
this
.
isSidebarOpen
)
{
this
.
showSidebar
();
}
},
updateScroll
()
{
if
(
!
isScrolledToBottom
())
{
this
.
toggleScrollAnimation
(
false
);
}
else
if
(
this
.
isScrollingDown
)
{
this
.
toggleScrollAnimation
(
true
);
}
methods
:
{
...
mapActions
([
'
setJobEndpoint
'
,
'
setTraceOptions
'
,
'
fetchJob
'
,
'
fetchStages
'
,
'
hideSidebar
'
,
'
showSidebar
'
,
'
toggleSidebar
'
,
'
fetchTrace
'
,
'
scrollBottom
'
,
'
scrollTop
'
,
'
toggleScrollButtons
'
,
'
toggleScrollAnimation
'
,
]),
onResize
()
{
this
.
updateSidebar
();
this
.
updateScroll
();
},
updateSidebar
()
{
if
(
bp
.
getBreakpointSize
()
===
'
xs
'
)
{
this
.
hideSidebar
();
}
else
if
(
!
this
.
isSidebarOpen
)
{
this
.
showSidebar
();
}
},
updateScroll
()
{
if
(
!
isScrolledToBottom
())
{
this
.
toggleScrollAnimation
(
false
);
}
else
if
(
this
.
isScrollingDown
)
{
this
.
toggleScrollAnimation
(
true
);
}
this
.
throttled
();
},
this
.
throttled
();
},
};
},
};
</
script
>
<
template
>
<div>
...
...
app/assets/javascripts/jobs/components/job_log.vue
View file @
7e9f44fa
<
script
>
import
{
mapState
,
mapActions
}
from
'
vuex
'
;
import
{
mapState
,
mapActions
}
from
'
vuex
'
;
export
default
{
name
:
'
JobLog
'
,
props
:
{
trace
:
{
type
:
String
,
required
:
true
,
},
isComplete
:
{
type
:
Boolean
,
required
:
true
,
},
export
default
{
name
:
'
JobLog
'
,
props
:
{
trace
:
{
type
:
String
,
required
:
true
,
},
computed
:
{
...
mapState
([
'
isScrolledToBottomBeforeReceivingTrace
'
]),
isComplete
:
{
type
:
Boolean
,
required
:
true
,
},
updated
()
{
this
.
$nextTick
(()
=>
this
.
handleScrollDown
());
},
computed
:
{
...
mapState
([
'
isScrolledToBottomBeforeReceivingTrace
'
]),
},
updated
()
{
this
.
$nextTick
(()
=>
this
.
handleScrollDown
());
},
mounted
()
{
this
.
$nextTick
(()
=>
this
.
handleScrollDown
());
},
methods
:
{
...
mapActions
([
'
scrollBottom
'
]),
/**
* The job log is sent in HTML, which means we need to use `v-html` to render it
* Using the updated hook with $nextTick is not enough to wait for the DOM to be updated
* in this case because it runs before `v-html` has finished running, since there's no
* Vue binding.
* In order to scroll the page down after `v-html` has finished, we need to use setTimeout
*/
handleScrollDown
()
{
if
(
this
.
isScrolledToBottomBeforeReceivingTrace
)
{
setTimeout
(()
=>
{
this
.
scrollBottom
();
},
0
);
}
},
mounted
()
{
this
.
$nextTick
(()
=>
this
.
handleScrollDown
());
},
methods
:
{
...
mapActions
([
'
scrollBottom
'
]),
/**
* The job log is sent in HTML, which means we need to use `v-html` to render it
* Using the updated hook with $nextTick is not enough to wait for the DOM to be updated
* in this case because it runs before `v-html` has finished running, since there's no
* Vue binding.
* In order to scroll the page down after `v-html` has finished, we need to use setTimeout
*/
handleScrollDown
()
{
if
(
this
.
isScrolledToBottomBeforeReceivingTrace
)
{
setTimeout
(()
=>
{
this
.
scrollBottom
();
},
0
);
}
},
},
};
},
};
</
script
>
<
template
>
<pre
class=
"js-build-trace build-trace qa-build-trace"
>
...
...
app/assets/javascripts/jobs/index.js
View file @
7e9f44fa
...
...
@@ -23,4 +23,3 @@ export default () => {
},
});
};
app/assets/javascripts/jobs/store/getters.js
View file @
7e9f44fa
...
...
@@ -35,12 +35,14 @@ export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status);
* Used to check if it should render the job log or the empty state
* @returns {Boolean}
*/
export
const
hasTrace
=
state
=>
state
.
job
.
has_trace
||
(
!
_
.
isEmpty
(
state
.
job
.
status
)
&&
state
.
job
.
status
.
group
===
'
running
'
);
export
const
hasTrace
=
state
=>
state
.
job
.
has_trace
||
(
!
_
.
isEmpty
(
state
.
job
.
status
)
&&
state
.
job
.
status
.
group
===
'
running
'
);
export
const
emptyStateIllustration
=
state
=>
(
state
.
job
&&
state
.
job
.
status
&&
state
.
job
.
status
.
illustration
)
||
{};
export
const
emptyStateAction
=
state
=>
(
state
.
job
&&
state
.
job
.
status
&&
state
.
job
.
status
.
action
)
||
{};
export
const
emptyStateAction
=
state
=>
(
state
.
job
&&
state
.
job
.
status
&&
state
.
job
.
status
.
action
)
||
{};
// ee-only start
/**
...
...
@@ -57,7 +59,8 @@ export const shouldRenderSharedRunnerLimitWarning = state =>
export
const
isScrollingDown
=
state
=>
isScrolledToBottom
()
&&
!
state
.
isTraceComplete
;
export
const
hasRunnersForProject
=
state
=>
state
.
job
.
runners
.
available
&&
!
state
.
job
.
runners
.
online
;
export
const
hasRunnersForProject
=
state
=>
state
.
job
.
runners
.
available
&&
!
state
.
job
.
runners
.
online
;
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export
default
()
=>
{};
app/assets/javascripts/labels_select.js
View file @
7e9f44fa
...
...
@@ -25,7 +25,35 @@ export default class LabelsSelect {
}
$els
.
each
(
function
(
i
,
dropdown
)
{
var
$block
,
$colorPreview
,
$dropdown
,
$form
,
$loading
,
$selectbox
,
$sidebarCollapsedValue
,
$value
,
abilityName
,
defaultLabel
,
enableLabelCreateButton
,
issueURLSplit
,
issueUpdateURL
,
labelUrl
,
namespacePath
,
projectPath
,
saveLabelData
,
selectedLabel
,
showAny
,
showNo
,
$sidebarLabelTooltip
,
initialSelected
,
$toggleText
,
fieldName
,
useId
,
propertyName
,
showMenuAbove
,
$container
,
$dropdownContainer
;
var
$block
,
$colorPreview
,
$dropdown
,
$form
,
$loading
,
$selectbox
,
$sidebarCollapsedValue
,
$value
,
abilityName
,
defaultLabel
,
enableLabelCreateButton
,
issueURLSplit
,
issueUpdateURL
,
labelUrl
,
namespacePath
,
projectPath
,
saveLabelData
,
selectedLabel
,
showAny
,
showNo
,
$sidebarLabelTooltip
,
initialSelected
,
$toggleText
,
fieldName
,
useId
,
propertyName
,
showMenuAbove
,
$container
,
$dropdownContainer
;
$dropdown
=
$
(
dropdown
);
$dropdownContainer
=
$dropdown
.
closest
(
'
.labels-filter
'
);
$toggleText
=
$dropdown
.
find
(
'
.dropdown-toggle-text
'
);
...
...
@@ -34,7 +62,7 @@ export default class LabelsSelect {
labelUrl
=
$dropdown
.
data
(
'
labels
'
);
issueUpdateURL
=
$dropdown
.
data
(
'
issueUpdate
'
);
selectedLabel
=
$dropdown
.
data
(
'
selected
'
);
if
(
(
selectedLabel
!=
null
)
&&
!
$dropdown
.
hasClass
(
'
js-multiselect
'
))
{
if
(
selectedLabel
!=
null
&&
!
$dropdown
.
hasClass
(
'
js-multiselect
'
))
{
selectedLabel
=
selectedLabel
.
split
(
'
,
'
);
}
showNo
=
$dropdown
.
data
(
'
showNo
'
);
...
...
@@ -50,26 +78,37 @@ export default class LabelsSelect {
$value
=
$block
.
find
(
'
.value
'
);
$loading
=
$block
.
find
(
'
.block-loading
'
).
fadeOut
();
fieldName
=
$dropdown
.
data
(
'
fieldName
'
);
useId
=
$dropdown
.
is
(
'
.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown
'
);
useId
=
$dropdown
.
is
(
'
.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown
'
,
);
propertyName
=
useId
?
'
id
'
:
'
title
'
;
initialSelected
=
$selectbox
.
find
(
'
input[name="
'
+
$dropdown
.
data
(
'
fieldName
'
)
+
'
"]
'
)
.
map
(
function
()
{
.
map
(
function
()
{
return
this
.
value
;
}).
get
();
})
.
get
();
const
{
handleClick
}
=
options
;
$sidebarLabelTooltip
.
tooltip
();
if
(
$dropdown
.
closest
(
'
.dropdown
'
).
find
(
'
.dropdown-new-label
'
).
length
)
{
new
CreateLabelDropdown
(
$dropdown
.
closest
(
'
.dropdown
'
).
find
(
'
.dropdown-new-label
'
),
namespacePath
,
projectPath
);
new
CreateLabelDropdown
(
$dropdown
.
closest
(
'
.dropdown
'
).
find
(
'
.dropdown-new-label
'
),
namespacePath
,
projectPath
,
);
}
saveLabelData
=
function
()
{
var
data
,
selected
;
selected
=
$dropdown
.
closest
(
'
.selectbox
'
).
find
(
"
input[name='
"
+
fieldName
+
"
']
"
).
map
(
function
()
{
return
this
.
value
;
}).
get
();
selected
=
$dropdown
.
closest
(
'
.selectbox
'
)
.
find
(
"
input[name='
"
+
fieldName
+
"
']
"
)
.
map
(
function
()
{
return
this
.
value
;
})
.
get
();
if
(
_
.
isEqual
(
initialSelected
,
selected
))
return
;
initialSelected
=
selected
;
...
...
@@ -82,7 +121,8 @@ export default class LabelsSelect {
}
$loading
.
removeClass
(
'
hidden
'
).
fadeIn
();
$dropdown
.
trigger
(
'
loading.gl.dropdown
'
);
axios
.
put
(
issueUpdateURL
,
data
)
axios
.
put
(
issueUpdateURL
,
data
)
.
then
(({
data
})
=>
{
var
labelCount
,
template
,
labelTooltipTitle
,
labelTitles
,
formattedLabels
;
$loading
.
fadeOut
();
...
...
@@ -96,8 +136,7 @@ export default class LabelsSelect {
issueUpdateURL
,
});
labelCount
=
data
.
labels
.
length
;
}
else
{
}
else
{
template
=
'
<span class="no-value">None</span>
'
;
}
$value
.
removeAttr
(
'
style
'
).
html
(
template
);
...
...
@@ -114,17 +153,14 @@ export default class LabelsSelect {
}
labelTooltipTitle
=
labelTitles
.
join
(
'
,
'
);
}
else
{
}
else
{
labelTooltipTitle
=
__
(
'
Labels
'
);
}
$sidebarLabelTooltip
.
attr
(
'
title
'
,
labelTooltipTitle
)
.
tooltip
(
'
_fixTitle
'
);
$sidebarLabelTooltip
.
attr
(
'
title
'
,
labelTooltipTitle
).
tooltip
(
'
_fixTitle
'
);
$
(
'
.has-tooltip
'
,
$value
).
tooltip
({
container
:
'
body
'
container
:
'
body
'
,
});
})
.
catch
(()
=>
flash
(
__
(
'
Error saving label update.
'
)));
...
...
@@ -132,34 +168,38 @@ export default class LabelsSelect {
$dropdown
.
glDropdown
({
showMenuAbove
:
showMenuAbove
,
data
:
function
(
term
,
callback
)
{
axios
.
get
(
labelUrl
)
.
then
((
res
)
=>
{
let
data
=
_
.
chain
(
res
.
data
).
groupBy
(
function
(
label
)
{
return
label
.
title
;
}).
map
(
function
(
label
)
{
var
color
;
color
=
_
.
map
(
label
,
function
(
dup
)
{
return
dup
.
color
;
});
return
{
id
:
label
[
0
].
id
,
title
:
label
[
0
].
title
,
color
:
color
,
duplicate
:
color
.
length
>
1
};
}).
value
();
axios
.
get
(
labelUrl
)
.
then
(
res
=>
{
let
data
=
_
.
chain
(
res
.
data
)
.
groupBy
(
function
(
label
)
{
return
label
.
title
;
})
.
map
(
function
(
label
)
{
var
color
;
color
=
_
.
map
(
label
,
function
(
dup
)
{
return
dup
.
color
;
});
return
{
id
:
label
[
0
].
id
,
title
:
label
[
0
].
title
,
color
:
color
,
duplicate
:
color
.
length
>
1
,
};
})
.
value
();
if
(
$dropdown
.
hasClass
(
'
js-extra-options
'
))
{
var
extraData
=
[];
if
(
showNo
)
{
extraData
.
unshift
({
id
:
0
,
title
:
'
No Label
'
title
:
'
No Label
'
,
});
}
if
(
showAny
)
{
extraData
.
unshift
({
isAny
:
true
,
title
:
'
Any Label
'
title
:
'
Any Label
'
,
});
}
if
(
extraData
.
length
)
{
...
...
@@ -176,11 +216,22 @@ export default class LabelsSelect {
.
catch
(()
=>
flash
(
__
(
'
Error fetching labels.
'
)));
},
renderRow
:
function
(
label
,
instance
)
{
var
$a
,
$li
,
color
,
colorEl
,
indeterminate
,
removesAll
,
selectedClass
,
spacing
,
i
,
marked
,
dropdownName
,
dropdownValue
;
var
$a
,
$li
,
color
,
colorEl
,
indeterminate
,
removesAll
,
selectedClass
,
spacing
,
i
,
marked
,
dropdownName
,
dropdownValue
;
$li
=
$
(
'
<li>
'
);
$a
=
$
(
'
<a href="#">
'
);
selectedClass
=
[];
removesAll
=
label
.
id
<=
0
||
(
label
.
id
==
null
)
;
removesAll
=
label
.
id
<=
0
||
label
.
id
==
null
;
if
(
$dropdown
.
hasClass
(
'
js-filter-bulk-update
'
))
{
indeterminate
=
$dropdown
.
data
(
'
indeterminate
'
)
||
[];
marked
=
$dropdown
.
data
(
'
marked
'
)
||
[];
...
...
@@ -200,9 +251,19 @@ export default class LabelsSelect {
}
else
{
if
(
this
.
id
(
label
))
{
dropdownName
=
$dropdown
.
data
(
'
fieldName
'
);
dropdownValue
=
this
.
id
(
label
).
toString
().
replace
(
/'/g
,
'
\\\'
'
);
if
(
$form
.
find
(
"
input[type='hidden'][name='
"
+
dropdownName
+
"
'][value='
"
+
dropdownValue
+
"
']
"
).
length
)
{
dropdownValue
=
this
.
id
(
label
)
.
toString
()
.
replace
(
/'/g
,
"
\\
'
"
);
if
(
$form
.
find
(
"
input[type='hidden'][name='
"
+
dropdownName
+
"
'][value='
"
+
dropdownValue
+
"
']
"
,
).
length
)
{
selectedClass
.
push
(
'
is-active
'
);
}
}
...
...
@@ -213,16 +274,14 @@ export default class LabelsSelect {
}
if
(
label
.
duplicate
)
{
color
=
DropdownUtils
.
duplicateLabelColor
(
label
.
color
);
}
else
{
}
else
{
if
(
label
.
color
!=
null
)
{
[
color
]
=
label
.
color
;
}
}
if
(
color
)
{
colorEl
=
"
<span class='dropdown-label-box' style='background:
"
+
color
+
"
'></span>
"
;
}
else
{
}
else
{
colorEl
=
''
;
}
// We need to identify which items are actually labels
...
...
@@ -235,7 +294,7 @@ export default class LabelsSelect {
return
$li
.
html
(
$a
).
prop
(
'
outerHTML
'
);
},
search
:
{
fields
:
[
'
title
'
]
fields
:
[
'
title
'
]
,
},
selectable
:
true
,
filterable
:
true
,
...
...
@@ -255,25 +314,21 @@ export default class LabelsSelect {
if
(
selected
&&
selected
.
id
===
0
)
{
this
.
selected
=
[];
return
'
No Label
'
;
}
else
if
(
isSelected
)
{
}
else
if
(
isSelected
)
{
this
.
selected
.
push
(
title
);
}
else
if
(
!
isSelected
&&
title
)
{
}
else
if
(
!
isSelected
&&
title
)
{
var
index
=
this
.
selected
.
indexOf
(
title
);
this
.
selected
.
splice
(
index
,
1
);
}
if
(
selectedLabels
.
length
===
1
)
{
return
selectedLabels
;
}
else
if
(
selectedLabels
.
length
)
{
}
else
if
(
selectedLabels
.
length
)
{
return
sprintf
(
__
(
'
%{firstLabel} +%{labelCount} more
'
),
{
firstLabel
:
selectedLabels
[
0
],
labelCount
:
selectedLabels
.
length
-
1
labelCount
:
selectedLabels
.
length
-
1
,
});
}
else
{
}
else
{
return
defaultLabel
;
}
},
...
...
@@ -285,10 +340,9 @@ export default class LabelsSelect {
return
label
.
id
;
}
if
(
$dropdown
.
hasClass
(
"
js-filter-submit
"
)
&&
(
label
.
isAny
==
null
)
)
{
if
(
$dropdown
.
hasClass
(
'
js-filter-submit
'
)
&&
label
.
isAny
==
null
)
{
return
label
.
title
;
}
else
{
}
else
{
return
label
.
id
;
}
},
...
...
@@ -310,13 +364,13 @@ export default class LabelsSelect {
}
if
(
$dropdown
.
hasClass
(
'
js-multiselect
'
))
{
if
(
$dropdown
.
hasClass
(
'
js-filter-submit
'
)
&&
(
isIssueIndex
||
isMRIndex
))
{
selectedLabels
=
$dropdown
.
closest
(
'
form
'
).
find
(
"
input:hidden[name='
"
+
(
$dropdown
.
data
(
'
fieldName
'
))
+
"
']
"
);
selectedLabels
=
$dropdown
.
closest
(
'
form
'
)
.
find
(
"
input:hidden[name='
"
+
$dropdown
.
data
(
'
fieldName
'
)
+
"
']
"
);
Issuable
.
filterResults
(
$dropdown
.
closest
(
'
form
'
));
}
else
if
(
$dropdown
.
hasClass
(
'
js-filter-submit
'
))
{
}
else
if
(
$dropdown
.
hasClass
(
'
js-filter-submit
'
))
{
$dropdown
.
closest
(
'
form
'
).
submit
();
}
else
{
}
else
{
if
(
!
$dropdown
.
hasClass
(
'
js-filter-bulk-update
'
))
{
saveLabelData
();
}
...
...
@@ -325,7 +379,7 @@ export default class LabelsSelect {
},
multiSelect
:
$dropdown
.
hasClass
(
'
js-multiselect
'
),
vue
:
$dropdown
.
hasClass
(
'
js-issue-board-sidebar
'
),
clicked
:
function
(
clickEvent
)
{
clicked
:
function
(
clickEvent
)
{
const
{
$el
,
e
,
isMarking
}
=
clickEvent
;
const
label
=
clickEvent
.
selectedObj
;
...
...
@@ -339,7 +393,8 @@ export default class LabelsSelect {
isMRIndex
=
page
===
'
projects:merge_requests:index
'
;
if
(
$dropdown
.
parent
().
find
(
'
.is-active:not(.dropdown-clear-active)
'
).
length
)
{
$dropdown
.
parent
()
$dropdown
.
parent
()
.
find
(
'
.dropdown-clear-active
'
)
.
removeClass
(
'
is-active
'
);
}
...
...
@@ -367,28 +422,26 @@ export default class LabelsSelect {
e
.
preventDefault
();
return
;
}
else
if
(
$dropdown
.
hasClass
(
'
js-filter-submit
'
)
&&
(
isIssueIndex
||
isMRIndex
))
{
}
else
if
(
$dropdown
.
hasClass
(
'
js-filter-submit
'
)
&&
(
isIssueIndex
||
isMRIndex
))
{
if
(
!
$dropdown
.
hasClass
(
'
js-multiselect
'
))
{
selectedLabel
=
label
.
title
;
return
Issuable
.
filterResults
(
$dropdown
.
closest
(
'
form
'
));
}
}
else
if
(
$dropdown
.
hasClass
(
'
js-filter-submit
'
))
{
}
else
if
(
$dropdown
.
hasClass
(
'
js-filter-submit
'
))
{
return
$dropdown
.
closest
(
'
form
'
).
submit
();
}
else
if
(
$dropdown
.
hasClass
(
'
js-issue-board-sidebar
'
))
{
}
else
if
(
$dropdown
.
hasClass
(
'
js-issue-board-sidebar
'
))
{
if
(
$el
.
hasClass
(
'
is-active
'
))
{
boardsStore
.
detail
.
issue
.
labels
.
push
(
new
ListLabel
({
id
:
label
.
id
,
title
:
label
.
title
,
color
:
label
.
color
[
0
],
textColor
:
'
#fff
'
}));
}
else
{
boardsStore
.
detail
.
issue
.
labels
.
push
(
new
ListLabel
({
id
:
label
.
id
,
title
:
label
.
title
,
color
:
label
.
color
[
0
],
textColor
:
'
#fff
'
,
}),
);
}
else
{
var
{
labels
}
=
boardsStore
.
detail
.
issue
;
labels
=
labels
.
filter
(
function
(
selectedLabel
)
{
labels
=
labels
.
filter
(
function
(
selectedLabel
)
{
return
selectedLabel
.
id
!==
label
.
id
;
});
boardsStore
.
detail
.
issue
.
labels
=
labels
;
...
...
@@ -396,19 +449,16 @@ export default class LabelsSelect {
$loading
.
fadeIn
();
boardsStore
.
detail
.
issue
.
update
(
$dropdown
.
attr
(
'
data-issue-update
'
))
boardsStore
.
detail
.
issue
.
update
(
$dropdown
.
attr
(
'
data-issue-update
'
))
.
then
(
fadeOutLoader
)
.
catch
(
fadeOutLoader
);
}
else
if
(
handleClick
)
{
}
else
if
(
handleClick
)
{
e
.
preventDefault
();
handleClick
(
label
);
}
else
{
}
else
{
if
(
$dropdown
.
hasClass
(
'
js-multiselect
'
))
{
}
else
{
}
else
{
return
saveLabelData
();
}
}
...
...
@@ -436,15 +486,17 @@ export default class LabelsSelect {
// so best approach is to use traditional way of
// concatenation
// see: http://2ality.com/2016/05/template-literal-whitespace.html#joining-arrays
const
tpl
=
_
.
template
([
'
<% _.each(labels, function(label){ %>
'
,
'
<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">
'
,
'
<span class="badge label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">
'
,
'
<%- label.title %>
'
,
'
</span>
'
,
'
</a>
'
,
'
<% }); %>
'
,
].
join
(
''
));
const
tpl
=
_
.
template
(
[
'
<% _.each(labels, function(label){ %>
'
,
'
<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">
'
,
'
<span class="badge label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">
'
,
'
<%- label.title %>
'
,
'
</span>
'
,
'
</a>
'
,
'
<% }); %>
'
,
].
join
(
''
),
);
return
tpl
(
tplData
);
}
...
...
app/assets/javascripts/lib/utils/ace_utils.js
View file @
7e9f44fa
/* global ace */
export
default
function
getModeByFileExtension
(
path
)
{
const
modelist
=
ace
.
require
(
"
ace/ext/modelist
"
);
const
modelist
=
ace
.
require
(
'
ace/ext/modelist
'
);
return
modelist
.
getModeForPath
(
path
).
mode
;
}
;
}
app/assets/javascripts/members.js
View file @
7e9f44fa
...
...
@@ -10,11 +10,21 @@ export default class Members {
}
addListeners
()
{
$
(
'
.js-ldap-permissions
'
).
off
(
'
click
'
).
on
(
'
click
'
,
this
.
showLDAPPermissionsWarning
.
bind
(
this
));
$
(
'
.js-ldap-override
'
).
off
(
'
click
'
).
on
(
'
click
'
,
this
.
toggleMemberAccessToggle
.
bind
(
this
));
$
(
'
.project_member, .group_member
'
).
off
(
'
ajax:success
'
).
on
(
'
ajax:success
'
,
this
.
removeRow
);
$
(
'
.js-member-update-control
'
).
off
(
'
change
'
).
on
(
'
change
'
,
this
.
formSubmit
.
bind
(
this
));
$
(
'
.js-edit-member-form
'
).
off
(
'
ajax:success
'
).
on
(
'
ajax:success
'
,
this
.
formSuccess
.
bind
(
this
));
$
(
'
.js-ldap-permissions
'
)
.
off
(
'
click
'
)
.
on
(
'
click
'
,
this
.
showLDAPPermissionsWarning
.
bind
(
this
));
$
(
'
.js-ldap-override
'
)
.
off
(
'
click
'
)
.
on
(
'
click
'
,
this
.
toggleMemberAccessToggle
.
bind
(
this
));
$
(
'
.project_member, .group_member
'
)
.
off
(
'
ajax:success
'
)
.
on
(
'
ajax:success
'
,
this
.
removeRow
);
$
(
'
.js-member-update-control
'
)
.
off
(
'
change
'
)
.
on
(
'
change
'
,
this
.
formSubmit
.
bind
(
this
));
$
(
'
.js-edit-member-form
'
)
.
off
(
'
ajax:success
'
)
.
on
(
'
ajax:success
'
,
this
.
formSuccess
.
bind
(
this
));
gl
.
utils
.
disableButtonIfEmptyField
(
'
#user_ids
'
,
'
input[name=commit]
'
,
'
change
'
);
}
...
...
@@ -42,7 +52,7 @@ export default class Members {
return
$el
.
text
();
},
clicked
:
(
options
)
=>
{
clicked
:
options
=>
{
const
$link
=
options
.
$el
;
if
(
!
$link
.
data
(
'
revert
'
))
{
...
...
@@ -53,11 +63,10 @@ export default class Members {
$toggle
.
disable
();
$dateInput
.
disable
();
this
.
overrideLdap
(
$memberListItem
,
$link
.
data
(
'
endpoint
'
),
false
)
.
catch
(()
=>
{
$toggle
.
enable
();
$dateInput
.
enable
();
});
this
.
overrideLdap
(
$memberListItem
,
$link
.
data
(
'
endpoint
'
),
false
).
catch
(()
=>
{
$toggle
.
enable
();
$dateInput
.
enable
();
});
}
},
});
...
...
@@ -111,11 +120,15 @@ export default class Members {
$toggle
.
enable
();
$dateInput
.
enable
();
})
.
catch
(
(
xhr
)
=>
{
.
catch
(
xhr
=>
{
$btn
.
enable
();
if
(
xhr
.
status
===
403
)
{
Flash
(
__
(
'
You do not have the correct permissions to override the settings from the LDAP group sync.
'
));
Flash
(
__
(
'
You do not have the correct permissions to override the settings from the LDAP group sync.
'
,
),
);
}
else
{
Flash
(
__
(
'
An error occurred while saving LDAP override status. Please try again.
'
));
}
...
...
@@ -123,13 +136,14 @@ export default class Members {
}
// eslint-disable-next-line class-methods-use-this
overrideLdap
(
$memberListitem
,
endpoint
,
override
)
{
return
axios
.
patch
(
endpoint
,
{
group_member
:
{
override
,
},
})
.
then
(()
=>
{
$memberListitem
.
toggleClass
(
'
is-overriden
'
,
override
);
});
return
axios
.
patch
(
endpoint
,
{
group_member
:
{
override
,
},
})
.
then
(()
=>
{
$memberListitem
.
toggleClass
(
'
is-overriden
'
,
override
);
});
}
}
app/assets/javascripts/milestone_select.js
View file @
7e9f44fa
...
...
@@ -9,7 +9,10 @@ import '~/gl_dropdown';
import
axios
from
'
./lib/utils/axios_utils
'
;
import
{
timeFor
}
from
'
./lib/utils/datetime_utility
'
;
import
ModalStore
from
'
./boards/stores/modal_store
'
;
import
boardsStore
,
{
boardStoreIssueSet
,
boardStoreIssueDelete
}
from
'
./boards/stores/boards_store
'
;
import
boardsStore
,
{
boardStoreIssueSet
,
boardStoreIssueDelete
,
}
from
'
./boards/stores/boards_store
'
;
export
default
class
MilestoneSelect
{
constructor
(
currentProject
,
els
,
options
=
{})
{
...
...
app/assets/javascripts/monitoring/components/dashboard.vue
View file @
7e9f44fa
...
...
@@ -152,10 +152,14 @@ export default {
this
.
state
=
'
gettingStarted
'
;
}
else
{
if
(
this
.
showEnvironmentDropdown
)
{
this
.
servicePromises
.
push
(
this
.
service
.
getEnvironmentsData
()
.
then
((
data
)
=>
this
.
store
.
storeEnvironmentsData
(
data
))
.
catch
(()
=>
Flash
(
s__
(
'
Metrics|There was an error getting environments information.
'
))));
this
.
servicePromises
.
push
(
this
.
service
.
getEnvironmentsData
()
.
then
(
data
=>
this
.
store
.
storeEnvironmentsData
(
data
))
.
catch
(()
=>
Flash
(
s__
(
'
Metrics|There was an error getting environments information.
'
)),
),
);
}
this
.
getGraphsData
();
window
.
addEventListener
(
'
resize
'
,
this
.
resizeThrottled
,
false
);
...
...
app/assets/javascripts/notes/components/notes_app.vue
View file @
7e9f44fa
...
...
@@ -54,7 +54,13 @@ export default {
};
},
computed
:
{
...
mapGetters
([
'
isNotesFetched
'
,
'
discussions
'
,
'
getNotesDataByProp
'
,
'
discussionCount
'
,
'
isLoading
'
]),
...
mapGetters
([
'
isNotesFetched
'
,
'
discussions
'
,
'
getNotesDataByProp
'
,
'
discussionCount
'
,
'
isLoading
'
,
]),
noteableType
()
{
return
this
.
noteableData
.
noteableType
;
},
...
...
app/assets/javascripts/notes/discussion_filters.js
View file @
7e9f44fa
import
Vue
from
'
vue
'
;
import
DiscussionFilter
from
'
./components/discussion_filter.vue
'
;
export
default
(
store
)
=>
{
export
default
store
=>
{
const
discussionFilterEl
=
document
.
getElementById
(
'
js-vue-discussion-filter
'
);
if
(
discussionFilterEl
)
{
const
{
defaultFilter
,
notesFilters
}
=
discussionFilterEl
.
dataset
;
const
defaultValue
=
defaultFilter
?
parseInt
(
defaultFilter
,
10
)
:
null
;
const
filterValues
=
notesFilters
?
JSON
.
parse
(
notesFilters
)
:
{};
const
filters
=
Object
.
keys
(
filterValues
).
map
(
entry
=>
({
title
:
entry
,
value
:
filterValues
[
entry
]
}));
const
filters
=
Object
.
keys
(
filterValues
).
map
(
entry
=>
({
title
:
entry
,
value
:
filterValues
[
entry
],
}));
return
new
Vue
({
el
:
discussionFilterEl
,
...
...
app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
View file @
7e9f44fa
...
...
@@ -95,4 +95,3 @@ export default {
</div>
</gl-modal>
</template>
app/assets/javascripts/pages/projects/project.js
View file @
7e9f44fa
...
...
@@ -76,7 +76,9 @@ export default class Project {
const
projectId
=
$
(
this
).
data
(
'
project-id
'
);
const
cookieKey
=
`hide_auto_devops_implicitly_enabled_banner_
${
projectId
}
`
;
Cookies
.
set
(
cookieKey
,
'
false
'
);
$
(
this
).
parents
(
'
.auto-devops-implicitly-enabled-banner
'
).
remove
();
$
(
this
)
.
parents
(
'
.auto-devops-implicitly-enabled-banner
'
)
.
remove
();
return
e
.
preventDefault
();
});
Project
.
projectSelectDropdown
();
...
...
app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
View file @
7e9f44fa
<
script
>
import
projectFeatureSetting
from
'
./project_feature_setting.vue
'
;
import
projectFeatureToggle
from
'
../../../../../vue_shared/components/toggle_button.vue
'
;
import
projectSettingRow
from
'
./project_setting_row.vue
'
;
import
{
visibilityOptions
,
visibilityLevelDescriptions
}
from
'
../constants
'
;
import
{
toggleHiddenClassBySelector
}
from
'
../external
'
;
import
projectFeatureSetting
from
'
./project_feature_setting.vue
'
;
import
projectFeatureToggle
from
'
../../../../../vue_shared/components/toggle_button.vue
'
;
import
projectSettingRow
from
'
./project_setting_row.vue
'
;
import
{
visibilityOptions
,
visibilityLevelDescriptions
}
from
'
../constants
'
;
import
{
toggleHiddenClassBySelector
}
from
'
../external
'
;
export
default
{
components
:
{
projectFeatureSetting
,
projectFeatureToggle
,
projectSettingRow
,
},
export
default
{
components
:
{
projectFeatureSetting
,
projectFeatureToggle
,
projectSettingRow
,
},
props
:
{
currentSettings
:
{
type
:
Object
,
required
:
true
,
},
canChangeVisibilityLevel
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
allowedVisibilityOptions
:
{
type
:
Array
,
required
:
false
,
default
:
()
=>
[
0
,
10
,
20
],
},
lfsAvailable
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
registryAvailable
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
packagesAvailable
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
visibilityHelpPath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
lfsHelpPath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
registryHelpPath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
pagesAvailable
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
pagesAccessControlEnabled
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
pagesHelpPath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
packagesHelpPath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
props
:
{
currentSettings
:
{
type
:
Object
,
required
:
true
,
},
canChangeVisibilityLevel
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
allowedVisibilityOptions
:
{
type
:
Array
,
required
:
false
,
default
:
()
=>
[
0
,
10
,
20
],
},
lfsAvailable
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
registryAvailable
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
packagesAvailable
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
visibilityHelpPath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
lfsHelpPath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
registryHelpPath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
pagesAvailable
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
pagesAccessControlEnabled
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
pagesHelpPath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
packagesHelpPath
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
},
data
()
{
const
defaults
=
{
visibilityOptions
,
visibilityLevel
:
visibilityOptions
.
PUBLIC
,
issuesAccessLevel
:
20
,
repositoryAccessLevel
:
20
,
mergeRequestsAccessLevel
:
20
,
buildsAccessLevel
:
20
,
wikiAccessLevel
:
20
,
snippetsAccessLevel
:
20
,
pagesAccessLevel
:
20
,
containerRegistryEnabled
:
true
,
lfsEnabled
:
true
,
packagesEnabled
:
true
,
requestAccessEnabled
:
true
,
highlightChangesClass
:
false
,
};
data
()
{
const
defaults
=
{
visibilityOptions
,
visibilityLevel
:
visibilityOptions
.
PUBLIC
,
issuesAccessLevel
:
20
,
repositoryAccessLevel
:
20
,
mergeRequestsAccessLevel
:
20
,
buildsAccessLevel
:
20
,
wikiAccessLevel
:
20
,
snippetsAccessLevel
:
20
,
pagesAccessLevel
:
20
,
containerRegistryEnabled
:
true
,
lfsEnabled
:
true
,
packagesEnabled
:
true
,
requestAccessEnabled
:
true
,
highlightChangesClass
:
false
,
};
return
{
...
defaults
,
...
this
.
currentSettings
};
},
return
{
...
defaults
,
...
this
.
currentSettings
};
},
computed
:
{
featureAccessLevelOptions
()
{
const
options
=
[
[
10
,
'
Only Project Members
'
],
];
if
(
this
.
visibilityLevel
!==
visibilityOptions
.
PRIVATE
)
{
options
.
push
([
20
,
'
Everyone With Access
'
]);
}
return
options
;
},
computed
:
{
featureAccessLevelOptions
()
{
const
options
=
[[
10
,
'
Only Project Members
'
]];
if
(
this
.
visibilityLevel
!==
visibilityOptions
.
PRIVATE
)
{
options
.
push
([
20
,
'
Everyone With Access
'
]);
}
return
options
;
},
repoFeatureAccessLevelOptions
()
{
return
this
.
featureAccessLevelOptions
.
filter
(
([
value
])
=>
value
<=
this
.
repositoryAccessLevel
,
);
},
repoFeatureAccessLevelOptions
()
{
return
this
.
featureAccessLevelOptions
.
filter
(
([
value
])
=>
value
<=
this
.
repositoryAccessLevel
,
);
},
pagesFeatureAccessLevelOptions
()
{
if
(
this
.
visibilityLevel
!==
visibilityOptions
.
PUBLIC
)
{
return
this
.
featureAccessLevelOptions
.
concat
([[
30
,
'
Everyone
'
]]);
}
return
this
.
featureAccessLevelOptions
;
},
pagesFeatureAccessLevelOptions
()
{
if
(
this
.
visibilityLevel
!==
visibilityOptions
.
PUBLIC
)
{
return
this
.
featureAccessLevelOptions
.
concat
([[
30
,
'
Everyone
'
]]);
}
return
this
.
featureAccessLevelOptions
;
},
repositoryEnabled
()
{
return
this
.
repositoryAccessLevel
>
0
;
},
repositoryEnabled
()
{
return
this
.
repositoryAccessLevel
>
0
;
},
visibilityLevelDescription
()
{
return
visibilityLevelDescriptions
[
this
.
visibilityLevel
];
},
visibilityLevelDescription
()
{
return
visibilityLevelDescriptions
[
this
.
visibilityLevel
];
},
},
watch
:
{
visibilityLevel
(
value
,
oldValue
)
{
if
(
value
===
visibilityOptions
.
PRIVATE
)
{
// when private, features are restricted to "only team members"
this
.
issuesAccessLevel
=
Math
.
min
(
10
,
this
.
issuesAccessLevel
);
this
.
repositoryAccessLevel
=
Math
.
min
(
10
,
this
.
repositoryAccessLevel
);
this
.
mergeRequestsAccessLevel
=
Math
.
min
(
10
,
this
.
mergeRequestsAccessLevel
);
this
.
buildsAccessLevel
=
Math
.
min
(
10
,
this
.
buildsAccessLevel
);
this
.
wikiAccessLevel
=
Math
.
min
(
10
,
this
.
wikiAccessLevel
);
this
.
snippetsAccessLevel
=
Math
.
min
(
10
,
this
.
snippetsAccessLevel
);
if
(
this
.
pagesAccessLevel
===
20
)
{
// When from Internal->Private narrow access for only members
this
.
pagesAccessLevel
=
10
;
}
this
.
highlightChanges
();
}
else
if
(
oldValue
===
visibilityOptions
.
PRIVATE
)
{
// if changing away from private, make enabled features more permissive
if
(
this
.
issuesAccessLevel
>
0
)
this
.
issuesAccessLevel
=
20
;
if
(
this
.
repositoryAccessLevel
>
0
)
this
.
repositoryAccessLevel
=
20
;
if
(
this
.
mergeRequestsAccessLevel
>
0
)
this
.
mergeRequestsAccessLevel
=
20
;
if
(
this
.
buildsAccessLevel
>
0
)
this
.
buildsAccessLevel
=
20
;
if
(
this
.
wikiAccessLevel
>
0
)
this
.
wikiAccessLevel
=
20
;
if
(
this
.
snippetsAccessLevel
>
0
)
this
.
snippetsAccessLevel
=
20
;
if
(
this
.
pagesAccessLevel
===
10
)
this
.
pagesAccessLevel
=
20
;
this
.
highlightChanges
();
watch
:
{
visibilityLevel
(
value
,
oldValue
)
{
if
(
value
===
visibilityOptions
.
PRIVATE
)
{
// when private, features are restricted to "only team members"
this
.
issuesAccessLevel
=
Math
.
min
(
10
,
this
.
issuesAccessLevel
);
this
.
repositoryAccessLevel
=
Math
.
min
(
10
,
this
.
repositoryAccessLevel
);
this
.
mergeRequestsAccessLevel
=
Math
.
min
(
10
,
this
.
mergeRequestsAccessLevel
);
this
.
buildsAccessLevel
=
Math
.
min
(
10
,
this
.
buildsAccessLevel
);
this
.
wikiAccessLevel
=
Math
.
min
(
10
,
this
.
wikiAccessLevel
);
this
.
snippetsAccessLevel
=
Math
.
min
(
10
,
this
.
snippetsAccessLevel
);
if
(
this
.
pagesAccessLevel
===
20
)
{
// When from Internal->Private narrow access for only members
this
.
pagesAccessLevel
=
10
;
}
},
this
.
highlightChanges
();
}
else
if
(
oldValue
===
visibilityOptions
.
PRIVATE
)
{
// if changing away from private, make enabled features more permissive
if
(
this
.
issuesAccessLevel
>
0
)
this
.
issuesAccessLevel
=
20
;
if
(
this
.
repositoryAccessLevel
>
0
)
this
.
repositoryAccessLevel
=
20
;
if
(
this
.
mergeRequestsAccessLevel
>
0
)
this
.
mergeRequestsAccessLevel
=
20
;
if
(
this
.
buildsAccessLevel
>
0
)
this
.
buildsAccessLevel
=
20
;
if
(
this
.
wikiAccessLevel
>
0
)
this
.
wikiAccessLevel
=
20
;
if
(
this
.
snippetsAccessLevel
>
0
)
this
.
snippetsAccessLevel
=
20
;
if
(
this
.
pagesAccessLevel
===
10
)
this
.
pagesAccessLevel
=
20
;
this
.
highlightChanges
();
}
},
repositoryAccessLevel
(
value
,
oldValue
)
{
if
(
value
<
oldValue
)
{
// sub-features cannot have more premissive access level
this
.
mergeRequestsAccessLevel
=
Math
.
min
(
this
.
mergeRequestsAccessLevel
,
value
);
this
.
buildsAccessLevel
=
Math
.
min
(
this
.
buildsAccessLevel
,
value
);
repositoryAccessLevel
(
value
,
oldValue
)
{
if
(
value
<
oldValue
)
{
// sub-features cannot have more premissive access level
this
.
mergeRequestsAccessLevel
=
Math
.
min
(
this
.
mergeRequestsAccessLevel
,
value
);
this
.
buildsAccessLevel
=
Math
.
min
(
this
.
buildsAccessLevel
,
value
);
if
(
value
===
0
)
{
this
.
containerRegistryEnabled
=
false
;
this
.
lfsEnabled
=
false
;
this
.
packagesEnabled
=
false
;
}
}
else
if
(
oldValue
===
0
)
{
this
.
mergeRequestsAccessLevel
=
value
;
this
.
buildsAccessLevel
=
value
;
this
.
containerRegistryEnabled
=
true
;
this
.
lfsEnabled
=
true
;
this
.
packagesEnabled
=
true
;
if
(
value
===
0
)
{
this
.
containerRegistryEnabled
=
false
;
this
.
lfsEnabled
=
false
;
this
.
packagesEnabled
=
false
;
}
},
}
else
if
(
oldValue
===
0
)
{
this
.
mergeRequestsAccessLevel
=
value
;
this
.
buildsAccessLevel
=
value
;
this
.
containerRegistryEnabled
=
true
;
this
.
lfsEnabled
=
true
;
this
.
packagesEnabled
=
true
;
}
},
issuesAccessLevel
(
value
,
oldValue
)
{
if
(
value
===
0
)
toggleHiddenClassBySelector
(
'
.issues-feature
'
,
true
);
else
if
(
oldValue
===
0
)
toggleHiddenClassBySelector
(
'
.issues-feature
'
,
false
);
},
issuesAccessLevel
(
value
,
oldValue
)
{
if
(
value
===
0
)
toggleHiddenClassBySelector
(
'
.issues-feature
'
,
true
);
else
if
(
oldValue
===
0
)
toggleHiddenClassBySelector
(
'
.issues-feature
'
,
false
);
},
mergeRequestsAccessLevel
(
value
,
oldValue
)
{
if
(
value
===
0
)
toggleHiddenClassBySelector
(
'
.merge-requests-feature
'
,
true
);
else
if
(
oldValue
===
0
)
toggleHiddenClassBySelector
(
'
.merge-requests-feature
'
,
false
);
},
mergeRequestsAccessLevel
(
value
,
oldValue
)
{
if
(
value
===
0
)
toggleHiddenClassBySelector
(
'
.merge-requests-feature
'
,
true
);
else
if
(
oldValue
===
0
)
toggleHiddenClassBySelector
(
'
.merge-requests-feature
'
,
false
);
},
buildsAccessLevel
(
value
,
oldValue
)
{
if
(
value
===
0
)
toggleHiddenClassBySelector
(
'
.builds-feature
'
,
true
);
else
if
(
oldValue
===
0
)
toggleHiddenClassBySelector
(
'
.builds-feature
'
,
false
);
},
buildsAccessLevel
(
value
,
oldValue
)
{
if
(
value
===
0
)
toggleHiddenClassBySelector
(
'
.builds-feature
'
,
true
);
else
if
(
oldValue
===
0
)
toggleHiddenClassBySelector
(
'
.builds-feature
'
,
false
);
},
},
methods
:
{
highlightChanges
()
{
this
.
highlightChangesClass
=
true
;
this
.
$nextTick
(()
=>
{
this
.
highlightChangesClass
=
false
;
});
},
methods
:
{
highlightChanges
()
{
this
.
highlightChangesClass
=
true
;
this
.
$nextTick
(()
=>
{
this
.
highlightChangesClass
=
false
;
});
},
visibilityAllowed
(
option
)
{
return
this
.
allowedVisibilityOptions
.
includes
(
option
);
},
visibilityAllowed
(
option
)
{
return
this
.
allowedVisibilityOptions
.
includes
(
option
);
},
};
},
};
</
script
>
<
template
>
...
...
app/assets/javascripts/projects/project_new.js
View file @
7e9f44fa
...
...
@@ -4,8 +4,10 @@ import { slugifyWithHyphens } from '../lib/utils/text_utility';
let
hasUserDefinedProjectPath
=
false
;
const
deriveProjectPathFromUrl
=
(
$projectImportUrl
)
=>
{
const
$currentProjectPath
=
$projectImportUrl
.
parents
(
'
.toggle-import-form
'
).
find
(
'
#project_path
'
);
const
deriveProjectPathFromUrl
=
$projectImportUrl
=>
{
const
$currentProjectPath
=
$projectImportUrl
.
parents
(
'
.toggle-import-form
'
)
.
find
(
'
#project_path
'
);
if
(
hasUserDefinedProjectPath
)
{
return
;
}
...
...
@@ -52,9 +54,11 @@ const bindEvents = () => {
return
;
}
$
(
'
.how_to_import_link
'
).
on
(
'
click
'
,
(
e
)
=>
{
$
(
'
.how_to_import_link
'
).
on
(
'
click
'
,
e
=>
{
e
.
preventDefault
();
$
(
e
.
currentTarget
).
next
(
'
.modal
'
).
show
();
$
(
e
.
currentTarget
)
.
next
(
'
.modal
'
)
.
show
();
});
$
(
'
.modal-header .close
'
).
on
(
'
click
'
,
()
=>
{
...
...
@@ -63,15 +67,21 @@ const bindEvents = () => {
$
(
'
.btn_import_gitlab_project
'
).
on
(
'
click
'
,
()
=>
{
const
importHref
=
$
(
'
a.btn_import_gitlab_project
'
).
attr
(
'
href
'
);
$
(
'
.btn_import_gitlab_project
'
)
.
attr
(
'
href
'
,
`
${
importHref
}
?namespace_id=
${
$
(
'
#project_namespace_id
'
).
val
()}
&name=
${
$projectName
.
val
()}
&path=
${
$projectPath
.
val
()}
`
);
$
(
'
.btn_import_gitlab_project
'
).
attr
(
'
href
'
,
`
${
importHref
}
?namespace_id=
${
$
(
'
#project_namespace_id
'
,
).
val
()}
&name=
${
$projectName
.
val
()}
&path=
${
$projectPath
.
val
()}
`
,
);
});
if
(
$pushNewProjectTipTrigger
)
{
$pushNewProjectTipTrigger
.
removeAttr
(
'
rel
'
)
.
removeAttr
(
'
target
'
)
.
on
(
'
click
'
,
(
e
)
=>
{
e
.
preventDefault
();
})
.
on
(
'
click
'
,
e
=>
{
e
.
preventDefault
();
})
.
popover
({
title
:
$pushNewProjectTipTrigger
.
data
(
'
title
'
),
placement
:
'
bottom
'
,
...
...
@@ -79,13 +89,15 @@ const bindEvents = () => {
content
:
$
(
'
.push-new-project-tip-template
'
).
html
(),
})
.
on
(
'
shown.bs.popover
'
,
()
=>
{
$
(
document
).
on
(
'
click.popover touchstart.popover
'
,
(
event
)
=>
{
$
(
document
).
on
(
'
click.popover touchstart.popover
'
,
event
=>
{
if
(
$
(
event
.
target
).
closest
(
'
.popover
'
).
length
===
0
)
{
$pushNewProjectTipTrigger
.
trigger
(
'
click
'
);
}
});
const
target
=
$
(
`#
${
$pushNewProjectTipTrigger
.
attr
(
'
aria-describedby
'
)}
`
).
find
(
'
.js-select-on-focus
'
);
const
target
=
$
(
`#
${
$pushNewProjectTipTrigger
.
attr
(
'
aria-describedby
'
)}
`
).
find
(
'
.js-select-on-focus
'
,
);
addSelectOnFocusBehaviour
(
target
);
target
.
focus
();
...
...
@@ -117,16 +129,18 @@ const bindEvents = () => {
const
selectedTemplate
=
templates
[
value
];
$selectedTemplateText
.
text
(
selectedTemplate
.
text
);
$
(
selectedTemplate
.
icon
).
clone
().
addClass
(
'
d-block
'
).
appendTo
(
$selectedIcon
);
$
(
selectedTemplate
.
icon
)
.
clone
()
.
addClass
(
'
d-block
'
)
.
appendTo
(
$selectedIcon
);
const
$activeTabProjectName
=
$
(
'
.tab-pane.active #project_name
'
);
const
$activeTabProjectPath
=
$
(
'
.tab-pane.active #project_path
'
);
$activeTabProjectName
.
focus
();
$activeTabProjectName
.
keyup
(()
=>
{
onProjectNameChange
(
$activeTabProjectName
,
$activeTabProjectPath
);
hasUserDefinedProjectPath
=
$activeTabProjectPath
.
val
().
trim
().
length
>
0
;
});
$activeTabProjectName
.
keyup
(()
=>
{
onProjectNameChange
(
$activeTabProjectName
,
$activeTabProjectPath
);
hasUserDefinedProjectPath
=
$activeTabProjectPath
.
val
().
trim
().
length
>
0
;
});
}
$useTemplateBtn
.
on
(
'
change
'
,
chooseTemplate
);
...
...
app/assets/javascripts/right_sidebar.js
View file @
7e9f44fa
...
...
@@ -21,7 +21,7 @@ Sidebar.initialize = function(currentUser) {
}
};
Sidebar
.
prototype
.
removeListeners
=
function
()
{
Sidebar
.
prototype
.
removeListeners
=
function
()
{
this
.
sidebar
.
off
(
'
click
'
,
'
.sidebar-collapsed-icon
'
);
this
.
sidebar
.
off
(
'
hidden.gl.dropdown
'
);
$
(
'
.dropdown
'
).
off
(
'
loading.gl.dropdown
'
);
...
...
@@ -38,10 +38,12 @@ Sidebar.prototype.addEventListeners = function() {
$
(
'
.dropdown
'
).
on
(
'
loaded.gl.dropdown
'
,
this
.
sidebarDropdownLoaded
);
$document
.
on
(
'
click
'
,
'
.js-sidebar-toggle
'
,
this
.
sidebarToggleClicked
);
return
$
(
document
).
off
(
'
click
'
,
'
.js-issuable-todo
'
).
on
(
'
click
'
,
'
.js-issuable-todo
'
,
this
.
toggleTodo
);
return
$
(
document
)
.
off
(
'
click
'
,
'
.js-issuable-todo
'
)
.
on
(
'
click
'
,
'
.js-issuable-todo
'
,
this
.
toggleTodo
);
};
Sidebar
.
prototype
.
sidebarToggleClicked
=
function
(
e
,
triggered
)
{
Sidebar
.
prototype
.
sidebarToggleClicked
=
function
(
e
,
triggered
)
{
var
$allGutterToggleIcons
,
$this
,
isExpanded
,
tooltipLabel
;
e
.
preventDefault
();
$this
=
$
(
this
);
...
...
@@ -51,18 +53,26 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
if
(
isExpanded
)
{
$allGutterToggleIcons
.
removeClass
(
'
fa-angle-double-right
'
).
addClass
(
'
fa-angle-double-left
'
);
$
(
'
aside.right-sidebar
'
).
removeClass
(
'
right-sidebar-expanded
'
).
addClass
(
'
right-sidebar-collapsed
'
);
$
(
'
.layout-page
'
).
removeClass
(
'
right-sidebar-expanded
'
).
addClass
(
'
right-sidebar-collapsed
'
);
$
(
'
aside.right-sidebar
'
)
.
removeClass
(
'
right-sidebar-expanded
'
)
.
addClass
(
'
right-sidebar-collapsed
'
);
$
(
'
.layout-page
'
)
.
removeClass
(
'
right-sidebar-expanded
'
)
.
addClass
(
'
right-sidebar-collapsed
'
);
}
else
{
$allGutterToggleIcons
.
removeClass
(
'
fa-angle-double-left
'
).
addClass
(
'
fa-angle-double-right
'
);
$
(
'
aside.right-sidebar
'
).
removeClass
(
'
right-sidebar-collapsed
'
).
addClass
(
'
right-sidebar-expanded
'
);
$
(
'
.layout-page
'
).
removeClass
(
'
right-sidebar-collapsed
'
).
addClass
(
'
right-sidebar-expanded
'
);
$
(
'
aside.right-sidebar
'
)
.
removeClass
(
'
right-sidebar-collapsed
'
)
.
addClass
(
'
right-sidebar-expanded
'
);
$
(
'
.layout-page
'
)
.
removeClass
(
'
right-sidebar-collapsed
'
)
.
addClass
(
'
right-sidebar-expanded
'
);
}
$this
.
attr
(
'
data-original-title
'
,
tooltipLabel
);
if
(
!
triggered
)
{
Cookies
.
set
(
"
collapsed_gutter
"
,
$
(
'
.right-sidebar
'
).
hasClass
(
'
right-sidebar-collapsed
'
));
Cookies
.
set
(
'
collapsed_gutter
'
,
$
(
'
.right-sidebar
'
).
hasClass
(
'
right-sidebar-collapsed
'
));
}
};
...
...
@@ -71,21 +81,27 @@ Sidebar.prototype.toggleTodo = function(e) {
$this
=
$
(
e
.
currentTarget
);
ajaxType
=
$this
.
attr
(
'
data-delete-path
'
)
?
'
delete
'
:
'
post
'
;
if
(
$this
.
attr
(
'
data-delete-path
'
))
{
url
=
""
+
(
$this
.
attr
(
'
data-delete-path
'
)
);
url
=
''
+
$this
.
attr
(
'
data-delete-path
'
);
}
else
{
url
=
""
+
(
$this
.
data
(
'
url
'
)
);
url
=
''
+
$this
.
data
(
'
url
'
);
}
$this
.
tooltip
(
'
hide
'
);
$
(
'
.js-issuable-todo
'
).
disable
().
addClass
(
'
is-loading
'
);
$
(
'
.js-issuable-todo
'
)
.
disable
()
.
addClass
(
'
is-loading
'
);
axios
[
ajaxType
](
url
,
{
issuable_id
:
$this
.
data
(
'
issuableId
'
),
issuable_type
:
$this
.
data
(
'
issuableType
'
),
}).
then
(({
data
})
=>
{
this
.
todoUpdateDone
(
data
);
}).
catch
(()
=>
flash
(
`There was an error
${
ajaxType
===
'
post
'
?
'
adding a
'
:
'
deleting the
'
}
todo.`
));
})
.
then
(({
data
})
=>
{
this
.
todoUpdateDone
(
data
);
})
.
catch
(()
=>
flash
(
`There was an error
${
ajaxType
===
'
post
'
?
'
adding a
'
:
'
deleting the
'
}
todo.`
),
);
};
Sidebar
.
prototype
.
todoUpdateDone
=
function
(
data
)
{
...
...
@@ -99,7 +115,8 @@ Sidebar.prototype.todoUpdateDone = function(data) {
const
$el
=
$
(
el
);
const
$elText
=
$el
.
find
(
'
.js-issuable-todo-inner
'
);
$el
.
removeClass
(
'
is-loading
'
)
$el
.
removeClass
(
'
is-loading
'
)
.
enable
()
.
attr
(
'
aria-label
'
,
$el
.
data
(
`
${
attrPrefix
}
Text`
))
.
attr
(
'
data-delete-path
'
,
deletePath
)
...
...
@@ -119,7 +136,9 @@ Sidebar.prototype.todoUpdateDone = function(data) {
Sidebar
.
prototype
.
sidebarDropdownLoading
=
function
(
e
)
{
var
$loading
,
$sidebarCollapsedIcon
,
i
,
img
;
$sidebarCollapsedIcon
=
$
(
this
).
closest
(
'
.block
'
).
find
(
'
.sidebar-collapsed-icon
'
);
$sidebarCollapsedIcon
=
$
(
this
)
.
closest
(
'
.block
'
)
.
find
(
'
.sidebar-collapsed-icon
'
);
img
=
$sidebarCollapsedIcon
.
find
(
'
img
'
);
i
=
$sidebarCollapsedIcon
.
find
(
'
i
'
);
$loading
=
$
(
'
<i class="fa fa-spinner fa-spin"></i>
'
);
...
...
@@ -134,7 +153,9 @@ Sidebar.prototype.sidebarDropdownLoading = function(e) {
Sidebar
.
prototype
.
sidebarDropdownLoaded
=
function
(
e
)
{
var
$sidebarCollapsedIcon
,
i
,
img
;
$sidebarCollapsedIcon
=
$
(
this
).
closest
(
'
.block
'
).
find
(
'
.sidebar-collapsed-icon
'
);
$sidebarCollapsedIcon
=
$
(
this
)
.
closest
(
'
.block
'
)
.
find
(
'
.sidebar-collapsed-icon
'
);
img
=
$sidebarCollapsedIcon
.
find
(
'
img
'
);
$sidebarCollapsedIcon
.
find
(
'
i.fa-spin
'
).
remove
();
i
=
$sidebarCollapsedIcon
.
find
(
'
i
'
);
...
...
@@ -220,7 +241,7 @@ Sidebar.prototype.isOpen = function() {
};
Sidebar
.
prototype
.
getBlock
=
function
(
name
)
{
return
this
.
sidebar
.
find
(
"
.block.
"
+
name
);
return
this
.
sidebar
.
find
(
'
.block.
'
+
name
);
};
export
default
Sidebar
;
app/assets/javascripts/search_autocomplete.js
View file @
7e9f44fa
...
...
@@ -234,7 +234,9 @@ export class SearchAutocomplete {
icon
,
text
:
term
,
template
,
url
:
`
${
gon
.
relative_url_root
}
/search?search=
${
term
}
&project_id=
${
this
.
projectInputEl
.
val
()}
&group_id=
${
this
.
groupInputEl
.
val
()}
`
,
url
:
`
${
gon
.
relative_url_root
}
/search?search=
${
term
}
&project_id=
${
this
.
projectInputEl
.
val
()}
&group_id=
${
this
.
groupInputEl
.
val
()}
`
,
});
}
}
...
...
app/assets/javascripts/sidebar/components/assignees/assignees.vue
View file @
7e9f44fa
...
...
@@ -74,8 +74,8 @@ export default {
}
if
(
!
this
.
users
.
length
)
{
const
emptyTooltipLabel
=
this
.
issuableType
===
'
issue
'
?
__
(
'
Assignee(s)
'
)
:
__
(
'
Assignee
'
);
const
emptyTooltipLabel
=
this
.
issuableType
===
'
issue
'
?
__
(
'
Assignee(s)
'
)
:
__
(
'
Assignee
'
);
names
.
push
(
emptyTooltipLabel
);
}
...
...
app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
View file @
7e9f44fa
<
script
>
import
{
__
}
from
'
~/locale
'
;
import
icon
from
'
~/vue_shared/components/icon.vue
'
;
import
toggleButton
from
'
~/vue_shared/components/toggle_button.vue
'
;
import
tooltip
from
'
~/vue_shared/directives/tooltip
'
;
import
eventHub
from
'
../../event_hub
'
;
import
{
__
}
from
'
~/locale
'
;
import
icon
from
'
~/vue_shared/components/icon.vue
'
;
import
toggleButton
from
'
~/vue_shared/components/toggle_button.vue
'
;
import
tooltip
from
'
~/vue_shared/directives/tooltip
'
;
import
eventHub
from
'
../../event_hub
'
;
const
ICON_ON
=
'
notifications
'
;
const
ICON_OFF
=
'
notifications-off
'
;
const
LABEL_ON
=
__
(
'
Notifications on
'
);
const
LABEL_OFF
=
__
(
'
Notifications off
'
);
const
ICON_ON
=
'
notifications
'
;
const
ICON_OFF
=
'
notifications-off
'
;
const
LABEL_ON
=
__
(
'
Notifications on
'
);
const
LABEL_OFF
=
__
(
'
Notifications off
'
);
export
default
{
directives
:
{
tooltip
,
export
default
{
directives
:
{
tooltip
,
},
components
:
{
icon
,
toggleButton
,
},
props
:
{
loading
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
components
:
{
icon
,
toggleButton
,
subscribed
:
{
type
:
Boolean
,
required
:
false
,
default
:
null
,
},
props
:
{
loading
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
subscribed
:
{
type
:
Boolean
,
required
:
false
,
default
:
null
,
},
id
:
{
type
:
Number
,
required
:
false
,
default
:
null
,
},
id
:
{
type
:
Number
,
required
:
false
,
default
:
null
,
},
computed
:
{
showLoadingState
()
{
return
this
.
subscribed
===
null
;
},
notificationIcon
()
{
return
this
.
subscribed
?
ICON_ON
:
ICON_OFF
;
},
notificationTooltip
()
{
return
this
.
subscribed
?
LABEL_ON
:
LABEL_OFF
;
},
},
computed
:
{
showLoadingState
()
{
return
this
.
subscribed
===
null
;
},
methods
:
{
/**
* We need to emit this event on both component & eventHub
* for 2 dependencies;
*
* Make Epic sidebar auto-expand when participants & label icon is clicked
* 1. eventHub: This component is used in Issue Boards sidebar
* where component template is part of HAML
* and event listeners are tied to app's eventHub.
* 2. Component: This compone is also used in Epics in EE
* where listeners are tied to component event.
*/
toggleSubscription
()
{
// App's eventHub event emission.
eventHub
.
$emit
(
'
toggleSubscription
'
,
this
.
id
);
notificationIcon
()
{
return
this
.
subscribed
?
ICON_ON
:
ICON_OFF
;
},
notificationTooltip
()
{
return
this
.
subscribed
?
LABEL_ON
:
LABEL_OFF
;
},
},
methods
:
{
/**
* We need to emit this event on both component & eventHub
* for 2 dependencies;
*
* Make Epic sidebar auto-expand when participants & label icon is clicked
* 1. eventHub: This component is used in Issue Boards sidebar
* where component template is part of HAML
* and event listeners are tied to app's eventHub.
* 2. Component: This compone is also used in Epics in EE
* where listeners are tied to component event.
*/
toggleSubscription
()
{
// App's eventHub event emission.
eventHub
.
$emit
(
'
toggleSubscription
'
,
this
.
id
);
// Component event emission.
this
.
$emit
(
'
toggleSubscription
'
,
this
.
id
);
},
onClickCollapsedIcon
()
{
this
.
$emit
(
'
toggleSidebar
'
);
},
// Component event emission.
this
.
$emit
(
'
toggleSubscription
'
,
this
.
id
);
},
onClickCollapsedIcon
()
{
this
.
$emit
(
'
toggleSidebar
'
);
},
};
},
};
</
script
>
<
template
>
...
...
app/assets/javascripts/sidebar/sidebar_mediator.js
View file @
7e9f44fa
...
...
@@ -39,9 +39,10 @@ export default class SidebarMediator {
}
fetch
()
{
return
this
.
service
.
get
()
return
this
.
service
.
get
()
.
then
(
response
=>
response
.
json
())
.
then
(
(
data
)
=>
{
.
then
(
data
=>
{
this
.
processFetchedData
(
data
);
})
.
catch
(()
=>
new
Flash
(
'
Error occurred when fetching sidebar data
'
));
...
...
@@ -56,30 +57,33 @@ export default class SidebarMediator {
toggleSubscription
()
{
this
.
store
.
setFetchingState
(
'
subscriptions
'
,
true
);
return
this
.
service
.
toggleSubscription
()
return
this
.
service
.
toggleSubscription
()
.
then
(()
=>
{
this
.
store
.
setSubscribedState
(
!
this
.
store
.
subscribed
);
this
.
store
.
setFetchingState
(
'
subscriptions
'
,
false
);
})
.
catch
(
(
err
)
=>
{
.
catch
(
err
=>
{
this
.
store
.
setFetchingState
(
'
subscriptions
'
,
false
);
throw
err
;
});
}
fetchAutocompleteProjects
(
searchTerm
)
{
return
this
.
service
.
getProjectsAutocomplete
(
searchTerm
)
return
this
.
service
.
getProjectsAutocomplete
(
searchTerm
)
.
then
(
response
=>
response
.
json
())
.
then
(
(
data
)
=>
{
.
then
(
data
=>
{
this
.
store
.
setAutocompleteProjects
(
data
);
return
this
.
store
.
autocompleteProjects
;
});
}
moveIssue
()
{
return
this
.
service
.
moveIssue
(
this
.
store
.
moveToProjectId
)
return
this
.
service
.
moveIssue
(
this
.
store
.
moveToProjectId
)
.
then
(
response
=>
response
.
json
())
.
then
(
(
data
)
=>
{
.
then
(
data
=>
{
if
(
window
.
location
.
pathname
!==
data
.
web_url
)
{
visitUrl
(
data
.
web_url
);
}
...
...
app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
View file @
7e9f44fa
...
...
@@ -58,19 +58,22 @@ export default {
},
hasStages
()
{
return
(
this
.
pipeline
.
details
&&
this
.
pipeline
.
details
.
stages
&&
this
.
pipeline
.
details
.
stages
.
length
this
.
pipeline
.
details
&&
this
.
pipeline
.
details
.
stages
&&
this
.
pipeline
.
details
.
stages
.
length
);
},
hasCommitInfo
()
{
return
this
.
pipeline
.
commit
&&
Object
.
keys
(
this
.
pipeline
.
commit
).
length
>
0
;
},
errorText
()
{
return
sprintf
(
__
(
'
Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation.%{linkEnd}
'
),
{
linkStart
:
`<a href="
${
this
.
troubleshootingDocsPath
}
">`
,
linkEnd
:
'
</a>
'
,
});
return
sprintf
(
__
(
'
Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation.%{linkEnd}
'
,
),
{
linkStart
:
`<a href="
${
this
.
troubleshootingDocsPath
}
">`
,
linkEnd
:
'
</a>
'
,
},
);
},
/* We typically set defaults ([]) in the store or prop declarations, but because triggered
* and triggeredBy are appended to `pipeline`, we can't set defaults in the store, and we
...
...
app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
View file @
7e9f44fa
...
...
@@ -71,7 +71,12 @@ export default {
return
defaultClass
;
},
iconClass
()
{
if
(
this
.
status
===
'
failed
'
||
!
this
.
commitMessage
.
length
||
!
this
.
mr
.
isMergeAllowed
||
this
.
mr
.
preventMerge
)
{
if
(
this
.
status
===
'
failed
'
||
!
this
.
commitMessage
.
length
||
!
this
.
mr
.
isMergeAllowed
||
this
.
mr
.
preventMerge
)
{
return
'
warning
'
;
}
return
'
success
'
;
...
...
@@ -90,11 +95,13 @@ export default {
},
isMergeButtonDisabled
()
{
const
{
commitMessage
}
=
this
;
return
Boolean
(
!
commitMessage
.
length
||
!
this
.
shouldShowMergeControls
()
||
this
.
isMakingRequest
||
this
.
isApprovalNeeded
||
this
.
mr
.
preventMerge
);
return
Boolean
(
!
commitMessage
.
length
||
!
this
.
shouldShowMergeControls
()
||
this
.
isMakingRequest
||
this
.
isApprovalNeeded
||
this
.
mr
.
preventMerge
,
);
},
isRemoveSourceBranchButtonDisabled
()
{
return
this
.
isMergeButtonDisabled
;
...
...
@@ -144,9 +151,10 @@ export default {
};
this
.
isMakingRequest
=
true
;
this
.
service
.
merge
(
options
)
this
.
service
.
merge
(
options
)
.
then
(
res
=>
res
.
data
)
.
then
(
(
data
)
=>
{
.
then
(
data
=>
{
const
hasError
=
data
.
status
===
'
failed
'
||
data
.
status
===
'
hook_validation_error
'
;
if
(
data
.
status
===
'
merge_when_pipeline_succeeds
'
)
{
...
...
@@ -171,9 +179,10 @@ export default {
});
},
handleMergePolling
(
continuePolling
,
stopPolling
)
{
this
.
service
.
poll
()
this
.
service
.
poll
()
.
then
(
res
=>
res
.
data
)
.
then
(
(
data
)
=>
{
.
then
(
data
=>
{
if
(
data
.
state
===
'
merged
'
)
{
// If state is merged we should update the widget and stop the polling
eventHub
.
$emit
(
'
MRWidgetUpdateRequested
'
);
...
...
@@ -209,9 +218,10 @@ export default {
});
},
handleRemoveBranchPolling
(
continuePolling
,
stopPolling
)
{
this
.
service
.
poll
()
this
.
service
.
poll
()
.
then
(
res
=>
res
.
data
)
.
then
(
(
data
)
=>
{
.
then
(
data
=>
{
// If source branch exists then we should continue polling
// because removing a source branch is a background task and takes time
if
(
data
.
source_branch_exists
)
{
...
...
app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
View file @
7e9f44fa
...
...
@@ -116,7 +116,7 @@ export default {
// init polling
this
.
initPostMergeDeploymentsPolling
();
}
}
}
,
},
created
()
{
this
.
initPolling
();
...
...
@@ -214,17 +214,21 @@ export default {
})
.
catch
(()
=>
this
.
throwDeploymentsError
());
},
fetchPostMergeDeployments
(){
fetchPostMergeDeployments
()
{
return
this
.
fetchDeployments
(
'
merge_commit
'
)
.
then
(({
data
})
=>
{
if
(
data
.
length
)
{
this
.
mr
.
postMergeDeployments
=
data
;
}
})
.
catch
(()
=>
this
.
throwDeploymentsError
());
.
catch
(()
=>
this
.
throwDeploymentsError
());
},
throwDeploymentsError
()
{
createFlash
(
__
(
'
Something went wrong while fetching the environments for this merge request. Please try again.
'
));
createFlash
(
__
(
'
Something went wrong while fetching the environments for this merge request. Please try again.
'
,
),
);
},
fetchActionsContent
()
{
this
.
service
...
...
app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
View file @
7e9f44fa
...
...
@@ -24,8 +24,8 @@ export default class MRWidgetService {
fetchDeployments
(
targetParam
)
{
return
axios
.
get
(
this
.
endpoints
.
ciEnvironmentsStatusPath
,
{
params
:
{
environment_target
:
targetParam
}
environment_target
:
targetParam
,
}
,
});
}
...
...
app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
View file @
7e9f44fa
...
...
@@ -18,8 +18,7 @@ export default class MergeRequestStore {
this
.
squash
=
data
.
squash
;
this
.
squashBeforeMergeHelpPath
=
this
.
squashBeforeMergeHelpPath
||
data
.
squash_before_merge_help_path
;
this
.
troubleshootingDocsPath
=
this
.
troubleshootingDocsPath
||
data
.
troubleshooting_docs_path
;
this
.
troubleshootingDocsPath
=
this
.
troubleshootingDocsPath
||
data
.
troubleshooting_docs_path
;
this
.
enableSquashBeforeMerge
=
this
.
enableSquashBeforeMerge
||
true
;
this
.
iid
=
data
.
iid
;
...
...
app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue
View file @
7e9f44fa
...
...
@@ -56,12 +56,14 @@ export default {
filteredResults
()
{
if
(
this
.
filter
!==
''
)
{
return
this
.
items
.
filter
(
item
=>
item
[
this
.
filterKey
]
&&
item
[
this
.
filterKey
].
toLowerCase
().
includes
(
this
.
filter
.
toLowerCase
()),
item
=>
item
[
this
.
filterKey
]
&&
item
[
this
.
filterKey
].
toLowerCase
().
includes
(
this
.
filter
.
toLowerCase
()),
);
}
return
this
.
items
.
slice
(
0
,
this
.
visibleItems
);
}
}
,
},
mounted
()
{
/**
...
...
app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
View file @
7e9f44fa
<
script
>
import
tooltip
from
'
~/vue_shared/directives/tooltip
'
;
import
tooltip
from
'
~/vue_shared/directives/tooltip
'
;
export
default
{
name
:
'
CollapsedCalendarIcon
'
,
directives
:
{
tooltip
,
export
default
{
name
:
'
CollapsedCalendarIcon
'
,
directives
:
{
tooltip
,
},
props
:
{
containerClass
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
props
:
{
containerClass
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
text
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
showIcon
:
{
type
:
Boolean
,
required
:
false
,
default
:
true
,
},
tooltipText
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
text
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
methods
:
{
click
()
{
this
.
$emit
(
'
click
'
);
}
,
showIcon
:
{
type
:
Boolean
,
required
:
false
,
default
:
true
,
},
};
tooltipText
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
},
methods
:
{
click
()
{
this
.
$emit
(
'
click
'
);
},
},
};
</
script
>
<
template
>
...
...
app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
View file @
7e9f44fa
<
script
>
import
{
__
}
from
'
~/locale
'
;
import
timeagoMixin
from
'
~/vue_shared/mixins/timeago
'
;
import
{
dateInWords
,
timeFor
}
from
'
~/lib/utils/datetime_utility
'
;
import
collapsedCalendarIcon
from
'
./collapsed_calendar_icon.vue
'
;
import
{
__
}
from
'
~/locale
'
;
import
timeagoMixin
from
'
~/vue_shared/mixins/timeago
'
;
import
{
dateInWords
,
timeFor
}
from
'
~/lib/utils/datetime_utility
'
;
import
collapsedCalendarIcon
from
'
./collapsed_calendar_icon.vue
'
;
export
default
{
name
:
'
SidebarCollapsedGroupedDatePicker
'
,
components
:
{
collapsedCalendarIcon
,
export
default
{
name
:
'
SidebarCollapsedGroupedDatePicker
'
,
components
:
{
collapsedCalendarIcon
,
},
mixins
:
[
timeagoMixin
],
props
:
{
collapsed
:
{
type
:
Boolean
,
required
:
false
,
default
:
true
,
},
mixins
:
[
timeagoMixin
,
],
props
:
{
collapsed
:
{
type
:
Boolean
,
required
:
false
,
default
:
true
,
},
minDate
:
{
type
:
Date
,
required
:
false
,
default
:
null
,
},
maxDate
:
{
type
:
Date
,
required
:
false
,
default
:
null
,
},
disableClickableIcons
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
minDate
:
{
type
:
Date
,
required
:
false
,
default
:
null
,
},
computed
:
{
hasMinAndMaxDates
()
{
return
this
.
minDate
&&
this
.
maxDate
;
},
hasNoMinAndMaxDates
()
{
return
!
this
.
minDate
&&
!
this
.
maxDate
;
},
showMinDateBlock
()
{
return
this
.
minDate
||
this
.
hasNoMinAndMaxDates
;
},
showFromText
()
{
return
!
this
.
maxDate
&&
this
.
minDate
;
},
iconClass
()
{
const
disabledClass
=
this
.
disableClickableIcons
?
'
disabled
'
:
''
;
return
`sidebar-collapsed-icon calendar-icon
${
disabledClass
}
`
;
},
maxDate
:
{
type
:
Date
,
required
:
false
,
default
:
null
,
},
methods
:
{
toggleSidebar
()
{
this
.
$emit
(
'
toggleCollapse
'
);
},
dateText
(
dateType
=
'
min
'
)
{
const
date
=
this
[
`
${
dateType
}
Date`
];
const
dateWords
=
dateInWords
(
date
,
true
);
const
parsedDateWords
=
dateWords
?
dateWords
.
replace
(
'
,
'
,
''
)
:
dateWords
;
disableClickableIcons
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
},
computed
:
{
hasMinAndMaxDates
()
{
return
this
.
minDate
&&
this
.
maxDate
;
},
hasNoMinAndMaxDates
()
{
return
!
this
.
minDate
&&
!
this
.
maxDate
;
},
showMinDateBlock
()
{
return
this
.
minDate
||
this
.
hasNoMinAndMaxDates
;
},
showFromText
()
{
return
!
this
.
maxDate
&&
this
.
minDate
;
},
iconClass
()
{
const
disabledClass
=
this
.
disableClickableIcons
?
'
disabled
'
:
''
;
return
`sidebar-collapsed-icon calendar-icon
${
disabledClass
}
`
;
},
},
methods
:
{
toggleSidebar
()
{
this
.
$emit
(
'
toggleCollapse
'
);
},
dateText
(
dateType
=
'
min
'
)
{
const
date
=
this
[
`
${
dateType
}
Date`
];
const
dateWords
=
dateInWords
(
date
,
true
);
const
parsedDateWords
=
dateWords
?
dateWords
.
replace
(
'
,
'
,
''
)
:
dateWords
;
return
date
?
parsedDateWords
:
__
(
'
None
'
);
},
tooltipText
(
dateType
=
'
min
'
)
{
const
defaultText
=
dateType
===
'
min
'
?
__
(
'
Start date
'
)
:
__
(
'
Due date
'
);
const
date
=
this
[
`
${
dateType
}
Date`
];
const
timeAgo
=
dateType
===
'
min
'
?
this
.
timeFormated
(
date
)
:
timeFor
(
date
);
const
dateText
=
date
?
[
this
.
dateText
(
dateType
),
`(
${
timeAgo
}
)`
,
].
join
(
'
'
)
:
''
;
return
date
?
parsedDateWords
:
__
(
'
None
'
);
},
tooltipText
(
dateType
=
'
min
'
)
{
const
defaultText
=
dateType
===
'
min
'
?
__
(
'
Start date
'
)
:
__
(
'
Due date
'
);
const
date
=
this
[
`
${
dateType
}
Date`
];
const
timeAgo
=
dateType
===
'
min
'
?
this
.
timeFormated
(
date
)
:
timeFor
(
date
);
const
dateText
=
date
?
[
this
.
dateText
(
dateType
),
`(
${
timeAgo
}
)`
].
join
(
'
'
)
:
''
;
if
(
date
)
{
return
[
defaultText
,
dateText
].
join
(
'
<br />
'
);
}
return
__
(
'
Start and due date
'
);
},
if
(
date
)
{
return
[
defaultText
,
dateText
].
join
(
'
<br />
'
);
}
return
__
(
'
Start and due date
'
);
},
};
},
};
</
script
>
<
template
>
...
...
app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
View file @
7e9f44fa
...
...
@@ -14,7 +14,12 @@ export default {
},
computed
:
{
labelsList
()
{
const
labelsString
=
this
.
labels
.
length
?
this
.
labels
.
slice
(
0
,
5
).
map
(
label
=>
label
.
title
).
join
(
'
,
'
)
:
s__
(
'
LabelSelect|Labels
'
);
const
labelsString
=
this
.
labels
.
length
?
this
.
labels
.
slice
(
0
,
5
)
.
map
(
label
=>
label
.
title
)
.
join
(
'
,
'
)
:
s__
(
'
LabelSelect|Labels
'
);
if
(
this
.
labels
.
length
>
5
)
{
return
sprintf
(
s__
(
'
LabelSelect|%{labelsString}, and %{remainingLabelCount} more
'
),
{
...
...
ee/spec/javascripts/boards/models/list_spec.js
View file @
7e9f44fa
...
...
@@ -31,24 +31,32 @@ describe('List model', () => {
});
describe
(
'
getIssues
'
,
()
=>
{
it
(
'
calls CE getIssues
'
,
(
done
)
=>
{
it
(
'
calls CE getIssues
'
,
done
=>
{
const
ceGetIssues
=
spyOn
(
CeList
.
prototype
,
'
getIssues
'
).
and
.
returnValue
(
Promise
.
resolve
({}));
list
.
getIssues
().
then
(()
=>
{
expect
(
ceGetIssues
).
toHaveBeenCalled
();
done
();
}).
catch
(
done
.
fail
);
list
.
getIssues
()
.
then
(()
=>
{
expect
(
ceGetIssues
).
toHaveBeenCalled
();
done
();
})
.
catch
(
done
.
fail
);
});
it
(
'
sets total weight
'
,
(
done
)
=>
{
spyOn
(
CeList
.
prototype
,
'
getIssues
'
).
and
.
returnValue
(
Promise
.
resolve
({
total_weight
:
11
,
}));
list
.
getIssues
().
then
(()
=>
{
expect
(
list
.
totalWeight
).
toBe
(
11
);
done
();
}).
catch
(
done
.
fail
);
it
(
'
sets total weight
'
,
done
=>
{
spyOn
(
CeList
.
prototype
,
'
getIssues
'
).
and
.
returnValue
(
Promise
.
resolve
({
total_weight
:
11
,
}),
);
list
.
getIssues
()
.
then
(()
=>
{
expect
(
list
.
totalWeight
).
toBe
(
11
);
done
();
})
.
catch
(
done
.
fail
);
});
});
...
...
ee/spec/javascripts/epics/sidebar/components/sidebar_app_spec.js
View file @
7e9f44fa
...
...
@@ -205,10 +205,12 @@ describe('epicSidebar', () => {
Object
.
assign
({},
defaultPropsData
,
{
startDateFromMilestones
:
startDateSourcingMilestoneDates
.
startDate
,
dueDateFromMilestones
:
dueDateSourcingMilestoneDates
.
dueDate
,
})
})
,
);
expect
(
vmDatesFromMilestones
.
getDateFromMilestonesTooltip
(
'
start
'
)).
toBe
(
'
Milestone for Start Date<br/><span class="text-tertiary">Jan 1, 2010 – Dec 31, 2019</span>
'
);
expect
(
vmDatesFromMilestones
.
getDateFromMilestonesTooltip
(
'
start
'
)).
toBe
(
'
Milestone for Start Date<br/><span class="text-tertiary">Jan 1, 2010 – Dec 31, 2019</span>
'
,
);
vmDatesFromMilestones
.
$destroy
();
});
...
...
@@ -229,10 +231,12 @@ describe('epicSidebar', () => {
},
startDateFromMilestones
:
startDate
,
dueDateFromMilestones
:
dueDate
,
})
})
,
);
expect
(
vmDatesFromMilestones
.
getDateFromMilestonesTooltip
(
'
start
'
)).
toBe
(
'
Milestone for Start Date<br/><span class="text-tertiary">Jan 1 – Mar 31, 2018</span>
'
);
expect
(
vmDatesFromMilestones
.
getDateFromMilestonesTooltip
(
'
start
'
)).
toBe
(
'
Milestone for Start Date<br/><span class="text-tertiary">Jan 1 – Mar 31, 2018</span>
'
,
);
vmDatesFromMilestones
.
$destroy
();
});
...
...
ee/spec/javascripts/jobs/shared_runner_limit_block_spec.js
View file @
7e9f44fa
...
...
@@ -19,7 +19,9 @@ describe('Shared Runner Limit Block', () => {
runnersPath
:
'
root/project/runners
'
,
});
expect
(
vm
.
$el
.
textContent
).
toContain
(
'
You have used all your shared Runners pipeline minutes.
'
);
expect
(
vm
.
$el
.
textContent
).
toContain
(
'
You have used all your shared Runners pipeline minutes.
'
,
);
expect
(
vm
.
$el
.
textContent
).
toContain
(
'
1000 of 4000
'
);
});
});
...
...
@@ -32,9 +34,10 @@ describe('Shared Runner Limit Block', () => {
runnersPath
:
'
root/project/runners
'
,
});
expect
(
trimText
(
vm
.
$el
.
textContent
)).
toContain
(
'
For more information, go to the Runners page.
'
);
expect
(
trimText
(
vm
.
$el
.
textContent
)).
toContain
(
'
For more information, go to the Runners page.
'
,
);
});
});
describe
(
'
without runnersPath
'
,
()
=>
{
...
...
@@ -44,7 +47,9 @@ describe('Shared Runner Limit Block', () => {
quotaLimit
:
4000
,
});
expect
(
trimText
(
vm
.
$el
.
textContent
)).
not
.
toContain
(
'
For more information, go to the Runners page.
'
);
expect
(
trimText
(
vm
.
$el
.
textContent
)).
not
.
toContain
(
'
For more information, go to the Runners page.
'
,
);
});
});
});
ee/spec/javascripts/vue_mr_widget/ee_mr_widget_options_spec.js
View file @
7e9f44fa
...
...
@@ -8,7 +8,12 @@ import mountComponent from 'spec/helpers/vue_mount_component_helper';
import
{
TEST_HOST
}
from
'
spec/test_constants
'
;
import
state
from
'
ee/vue_shared/security_reports/store/state
'
;
import
mockData
,
{
baseIssues
,
headIssues
,
basePerformance
,
headPerformance
}
from
'
spec/vue_mr_widget/mock_data
'
;
import
mockData
,
{
baseIssues
,
headIssues
,
basePerformance
,
headPerformance
,
}
from
'
spec/vue_mr_widget/mock_data
'
;
import
{
sastIssues
,
...
...
@@ -492,9 +497,7 @@ describe('ee merge request widget options', () => {
it
(
'
should render provided data
'
,
()
=>
{
expect
(
removeBreakLine
(
vm
.
$el
.
querySelector
(
'
.js-performance-widget .js-code-text
'
).
textContent
,
),
removeBreakLine
(
vm
.
$el
.
querySelector
(
'
.js-performance-widget .js-code-text
'
).
textContent
),
).
toEqual
(
'
No changes to performance metrics
'
);
});
...
...
@@ -505,7 +508,9 @@ describe('ee merge request widget options', () => {
});
it
(
'
shows success icon
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.js-performance-widget .js-ci-status-icon-success
'
)).
not
.
toBeNull
();
expect
(
vm
.
$el
.
querySelector
(
'
.js-performance-widget .js-ci-status-icon-success
'
),
).
not
.
toBeNull
();
});
});
...
...
spec/javascripts/boards/board_list_spec.js
View file @
7e9f44fa
...
...
@@ -18,7 +18,7 @@ describe('Board list component', () => {
let
mock
;
let
component
;
beforeEach
(
(
done
)
=>
{
beforeEach
(
done
=>
{
const
el
=
document
.
createElement
(
'
div
'
);
document
.
body
.
appendChild
(
el
);
...
...
@@ -66,27 +66,21 @@ describe('Board list component', () => {
});
it
(
'
renders component
'
,
()
=>
{
expect
(
component
.
$el
.
classList
.
contains
(
'
board-list-component
'
),
).
toBe
(
true
);
expect
(
component
.
$el
.
classList
.
contains
(
'
board-list-component
'
)).
toBe
(
true
);
});
it
(
'
renders loading icon
'
,
(
done
)
=>
{
it
(
'
renders loading icon
'
,
done
=>
{
component
.
loading
=
true
;
Vue
.
nextTick
(()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.board-list-loading
'
),
).
not
.
toBeNull
();
expect
(
component
.
$el
.
querySelector
(
'
.board-list-loading
'
)).
not
.
toBeNull
();
done
();
});
});
it
(
'
renders issues
'
,
()
=>
{
expect
(
component
.
$el
.
querySelectorAll
(
'
.board-card
'
).
length
,
).
toBe
(
1
);
expect
(
component
.
$el
.
querySelectorAll
(
'
.board-card
'
).
length
).
toBe
(
1
);
});
it
(
'
renders link properly in issue
'
,
()
=>
{
...
...
@@ -96,98 +90,84 @@ describe('Board list component', () => {
});
it
(
'
sets data attribute with issue id
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.board-card
'
).
getAttribute
(
'
data-issue-id
'
),
).
toBe
(
'
1
'
);
expect
(
component
.
$el
.
querySelector
(
'
.board-card
'
).
getAttribute
(
'
data-issue-id
'
)).
toBe
(
'
1
'
);
});
it
(
'
shows new issue form
'
,
(
done
)
=>
{
it
(
'
shows new issue form
'
,
done
=>
{
component
.
toggleForm
();
Vue
.
nextTick
(()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.board-new-issue-form
'
),
).
not
.
toBeNull
();
expect
(
component
.
$el
.
querySelector
(
'
.board-new-issue-form
'
)).
not
.
toBeNull
();
expect
(
component
.
$el
.
querySelector
(
'
.is-smaller
'
),
).
not
.
toBeNull
();
expect
(
component
.
$el
.
querySelector
(
'
.is-smaller
'
)).
not
.
toBeNull
();
done
();
});
});
it
(
'
shows new issue form after eventhub event
'
,
(
done
)
=>
{
it
(
'
shows new issue form after eventhub event
'
,
done
=>
{
eventHub
.
$emit
(
`hide-issue-form-
${
component
.
list
.
id
}
`
);
Vue
.
nextTick
(()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.board-new-issue-form
'
),
).
not
.
toBeNull
();
expect
(
component
.
$el
.
querySelector
(
'
.board-new-issue-form
'
)).
not
.
toBeNull
();
expect
(
component
.
$el
.
querySelector
(
'
.is-smaller
'
),
).
not
.
toBeNull
();
expect
(
component
.
$el
.
querySelector
(
'
.is-smaller
'
)).
not
.
toBeNull
();
done
();
});
});
it
(
'
does not show new issue form for closed list
'
,
(
done
)
=>
{
it
(
'
does not show new issue form for closed list
'
,
done
=>
{
component
.
list
.
type
=
'
closed
'
;
component
.
toggleForm
();
Vue
.
nextTick
(()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.board-new-issue-form
'
),
).
toBeNull
();
expect
(
component
.
$el
.
querySelector
(
'
.board-new-issue-form
'
)).
toBeNull
();
done
();
});
});
it
(
'
shows count list item
'
,
(
done
)
=>
{
it
(
'
shows count list item
'
,
done
=>
{
component
.
showCount
=
true
;
Vue
.
nextTick
(()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.board-list-count
'
),
).
not
.
toBeNull
();
expect
(
component
.
$el
.
querySelector
(
'
.board-list-count
'
)).
not
.
toBeNull
();
expect
(
component
.
$el
.
querySelector
(
'
.board-list-count
'
).
textContent
.
trim
()
,
)
.
toBe
(
'
Showing all issues
'
)
;
expect
(
component
.
$el
.
querySelector
(
'
.board-list-count
'
).
textContent
.
trim
()).
toBe
(
'
Showing all issues
'
,
);
done
();
});
});
it
(
'
sets data attribute with invalid id
'
,
(
done
)
=>
{
it
(
'
sets data attribute with invalid id
'
,
done
=>
{
component
.
showCount
=
true
;
Vue
.
nextTick
(()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.board-list-count
'
).
getAttribute
(
'
data-issue-id
'
)
,
)
.
toBe
(
'
-1
'
)
;
expect
(
component
.
$el
.
querySelector
(
'
.board-list-count
'
).
getAttribute
(
'
data-issue-id
'
)).
toBe
(
'
-1
'
,
);
done
();
});
});
it
(
'
shows how many more issues to load
'
,
(
done
)
=>
{
it
(
'
shows how many more issues to load
'
,
done
=>
{
component
.
showCount
=
true
;
component
.
list
.
issuesSize
=
20
;
Vue
.
nextTick
(()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.board-list-count
'
).
textContent
.
trim
()
,
)
.
toBe
(
'
Showing 1 of 20 issues
'
)
;
expect
(
component
.
$el
.
querySelector
(
'
.board-list-count
'
).
textContent
.
trim
()).
toBe
(
'
Showing 1 of 20 issues
'
,
);
done
();
});
});
it
(
'
loads more issues after scrolling
'
,
(
done
)
=>
{
it
(
'
loads more issues after scrolling
'
,
done
=>
{
spyOn
(
component
.
list
,
'
nextPage
'
);
component
.
$refs
.
list
.
style
.
height
=
'
100px
'
;
component
.
$refs
.
list
.
style
.
overflow
=
'
scroll
'
;
...
...
@@ -210,7 +190,9 @@ describe('Board list component', () => {
});
it
(
'
does not load issues if already loading
'
,
()
=>
{
component
.
list
.
nextPage
=
spyOn
(
component
.
list
,
'
nextPage
'
).
and
.
returnValue
(
new
Promise
(()
=>
{}));
component
.
list
.
nextPage
=
spyOn
(
component
.
list
,
'
nextPage
'
).
and
.
returnValue
(
new
Promise
(()
=>
{}),
);
component
.
onScroll
();
component
.
onScroll
();
...
...
@@ -218,14 +200,12 @@ describe('Board list component', () => {
expect
(
component
.
list
.
nextPage
).
toHaveBeenCalledTimes
(
1
);
});
it
(
'
shows loading more spinner
'
,
(
done
)
=>
{
it
(
'
shows loading more spinner
'
,
done
=>
{
component
.
showCount
=
true
;
component
.
list
.
loadingMore
=
true
;
Vue
.
nextTick
(()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.board-list-count .fa-spinner
'
),
).
not
.
toBeNull
();
expect
(
component
.
$el
.
querySelector
(
'
.board-list-count .fa-spinner
'
)).
not
.
toBeNull
();
done
();
});
...
...
spec/javascripts/boards/components/board_spec.js
View file @
7e9f44fa
...
...
@@ -8,7 +8,7 @@ describe('Board component', () => {
let
vm
;
let
el
;
beforeEach
(
(
done
)
=>
{
beforeEach
(
done
=>
{
loadFixtures
(
'
boards/show.html.raw
'
);
el
=
document
.
createElement
(
'
div
'
);
...
...
@@ -50,56 +50,46 @@ describe('Board component', () => {
});
it
(
'
board is expandable when list type is backlog
'
,
()
=>
{
expect
(
vm
.
$el
.
classList
.
contains
(
'
is-expandable
'
),
).
toBe
(
true
);
expect
(
vm
.
$el
.
classList
.
contains
(
'
is-expandable
'
)).
toBe
(
true
);
});
it
(
'
board is expandable when list type is closed
'
,
(
done
)
=>
{
it
(
'
board is expandable when list type is closed
'
,
done
=>
{
vm
.
list
.
type
=
'
closed
'
;
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
classList
.
contains
(
'
is-expandable
'
),
).
toBe
(
true
);
expect
(
vm
.
$el
.
classList
.
contains
(
'
is-expandable
'
)).
toBe
(
true
);
done
();
});
});
it
(
'
board is not expandable when list type is label
'
,
(
done
)
=>
{
it
(
'
board is not expandable when list type is label
'
,
done
=>
{
vm
.
list
.
type
=
'
label
'
;
vm
.
list
.
isExpandable
=
false
;
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
classList
.
contains
(
'
is-expandable
'
),
).
toBe
(
false
);
expect
(
vm
.
$el
.
classList
.
contains
(
'
is-expandable
'
)).
toBe
(
false
);
done
();
});
});
it
(
'
collapses when clicking header
'
,
(
done
)
=>
{
it
(
'
collapses when clicking header
'
,
done
=>
{
vm
.
$el
.
querySelector
(
'
.board-header
'
).
click
();
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
classList
.
contains
(
'
is-collapsed
'
),
).
toBe
(
true
);
expect
(
vm
.
$el
.
classList
.
contains
(
'
is-collapsed
'
)).
toBe
(
true
);
done
();
});
});
it
(
'
created sets isExpanded to true from localStorage
'
,
(
done
)
=>
{
it
(
'
created sets isExpanded to true from localStorage
'
,
done
=>
{
vm
.
$el
.
querySelector
(
'
.board-header
'
).
click
();
return
Vue
.
nextTick
()
.
then
(()
=>
{
expect
(
vm
.
$el
.
classList
.
contains
(
'
is-collapsed
'
),
).
toBe
(
true
);
expect
(
vm
.
$el
.
classList
.
contains
(
'
is-collapsed
'
)).
toBe
(
true
);
// call created manually
vm
.
$options
.
created
[
0
].
call
(
vm
);
...
...
@@ -107,11 +97,10 @@ describe('Board component', () => {
return
Vue
.
nextTick
();
})
.
then
(()
=>
{
expect
(
vm
.
$el
.
classList
.
contains
(
'
is-collapsed
'
),
).
toBe
(
true
);
expect
(
vm
.
$el
.
classList
.
contains
(
'
is-collapsed
'
)).
toBe
(
true
);
done
();
}).
catch
(
done
.
fail
);
})
.
catch
(
done
.
fail
);
});
});
spec/javascripts/boards/issue_card_spec.js
View file @
7e9f44fa
...
...
@@ -291,46 +291,42 @@ describe('Issue card component', () => {
.
catch
(
done
.
fail
);
});
it
(
'
shows group labels on group boards
'
,
(
done
)
=>
{
component
.
issue
.
addLabel
(
new
ListLabel
({
id
:
_
.
random
(
10000
),
title
:
'
Group label
'
,
type
:
'
GroupLabel
'
,
}));
it
(
'
shows group labels on group boards
'
,
done
=>
{
component
.
issue
.
addLabel
(
new
ListLabel
({
id
:
_
.
random
(
10000
),
title
:
'
Group label
'
,
type
:
'
GroupLabel
'
,
}),
);
component
.
groupId
=
1
;
Vue
.
nextTick
()
.
then
(()
=>
{
expect
(
component
.
$el
.
querySelectorAll
(
'
.badge
'
).
length
,
).
toBe
(
3
);
expect
(
component
.
$el
.
querySelectorAll
(
'
.badge
'
).
length
).
toBe
(
3
);
expect
(
component
.
$el
.
textContent
,
).
toContain
(
'
Group label
'
);
expect
(
component
.
$el
.
textContent
).
toContain
(
'
Group label
'
);
done
();
})
.
catch
(
done
.
fail
);
});
it
(
'
shows project labels on group boards
'
,
(
done
)
=>
{
component
.
issue
.
addLabel
(
new
ListLabel
({
id
:
123
,
title
:
'
Project label
'
,
type
:
'
ProjectLabel
'
,
}));
it
(
'
shows project labels on group boards
'
,
done
=>
{
component
.
issue
.
addLabel
(
new
ListLabel
({
id
:
123
,
title
:
'
Project label
'
,
type
:
'
ProjectLabel
'
,
}),
);
component
.
groupId
=
1
;
Vue
.
nextTick
()
.
then
(()
=>
{
expect
(
component
.
$el
.
querySelectorAll
(
'
.badge
'
).
length
,
).
toBe
(
3
);
expect
(
component
.
$el
.
querySelectorAll
(
'
.badge
'
).
length
).
toBe
(
3
);
expect
(
component
.
$el
.
textContent
,
).
toContain
(
'
Project label
'
);
expect
(
component
.
$el
.
textContent
).
toContain
(
'
Project label
'
);
done
();
})
...
...
spec/javascripts/environments/emtpy_state_spec.js
View file @
7e9f44fa
...
...
@@ -24,13 +24,13 @@ describe('environments empty state', () => {
});
it
(
'
renders empty state and new environment button
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.js-blank-state-title
'
).
textContent
.
trim
()
,
)
.
toEqual
(
'
You don
\'
t have any environments right now
'
)
;
expect
(
vm
.
$el
.
querySelector
(
'
.js-blank-state-title
'
).
textContent
.
trim
()).
toEqual
(
"
You don't have any environments right now
"
,
);
expect
(
vm
.
$el
.
querySelector
(
'
.js-new-environment-button
'
).
getAttribute
(
'
href
'
)
,
)
.
toEqual
(
'
foo
'
)
;
expect
(
vm
.
$el
.
querySelector
(
'
.js-new-environment-button
'
).
getAttribute
(
'
href
'
)).
toEqual
(
'
foo
'
,
);
});
});
...
...
@@ -44,13 +44,11 @@ describe('environments empty state', () => {
});
it
(
'
renders empty state without new button
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.js-blank-state-title
'
).
textContent
.
trim
()
,
)
.
toEqual
(
'
You don
\'
t have any environments right now
'
)
;
expect
(
vm
.
$el
.
querySelector
(
'
.js-blank-state-title
'
).
textContent
.
trim
()).
toEqual
(
"
You don't have any environments right now
"
,
);
expect
(
vm
.
$el
.
querySelector
(
'
.js-new-environment-button
'
),
).
toBeNull
();
expect
(
vm
.
$el
.
querySelector
(
'
.js-new-environment-button
'
)).
toBeNull
();
});
});
});
spec/javascripts/environments/environment_item_spec.js
View file @
7e9f44fa
...
...
@@ -41,7 +41,9 @@ describe('Environment item', () => {
});
it
(
'
Should render the number of children in a badge
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.folder-name .badge
'
).
textContent
).
toContain
(
mockItem
.
size
);
expect
(
component
.
$el
.
querySelector
(
'
.folder-name .badge
'
).
textContent
).
toContain
(
mockItem
.
size
,
);
});
});
...
...
@@ -71,7 +73,8 @@ describe('Environment item', () => {
username
:
'
root
'
,
id
:
1
,
state
:
'
active
'
,
avatar_url
:
'
https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80
\
u0026d=identicon
'
,
avatar_url
:
'
https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80
\
u0026d=identicon
'
,
web_url
:
'
http://localhost:3000/root
'
,
},
commit
:
{
...
...
@@ -87,7 +90,8 @@ describe('Environment item', () => {
username
:
'
root
'
,
id
:
1
,
state
:
'
active
'
,
avatar_url
:
'
https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80
\
u0026d=identicon
'
,
avatar_url
:
'
https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80
\
u0026d=identicon
'
,
web_url
:
'
http://localhost:3000/root
'
,
},
commit_path
:
'
/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd
'
,
...
...
@@ -127,18 +131,18 @@ describe('Environment item', () => {
});
it
(
'
should render environment name
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.environment-name
'
).
textContent
).
toContain
(
environment
.
name
);
expect
(
component
.
$el
.
querySelector
(
'
.environment-name
'
).
textContent
).
toContain
(
environment
.
name
,
);
});
describe
(
'
With deployment
'
,
()
=>
{
it
(
'
should render deployment internal id
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.deployment-column span
'
).
textContent
,
)
.
toContain
(
environment
.
last_deployment
.
iid
)
;
expect
(
component
.
$el
.
querySelector
(
'
.deployment-column span
'
).
textContent
).
toContain
(
environment
.
last_deployment
.
iid
,
);
expect
(
component
.
$el
.
querySelector
(
'
.deployment-column span
'
).
textContent
,
).
toContain
(
'
#
'
);
expect
(
component
.
$el
.
querySelector
(
'
.deployment-column span
'
).
textContent
).
toContain
(
'
#
'
);
});
it
(
'
should render last deployment date
'
,
()
=>
{
...
...
@@ -162,56 +166,46 @@ describe('Environment item', () => {
describe
(
'
With build url
'
,
()
=>
{
it
(
'
Should link to build url provided
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.build-link
'
).
getAttribute
(
'
href
'
)
,
)
.
toEqual
(
environment
.
last_deployment
.
deployable
.
build_path
)
;
expect
(
component
.
$el
.
querySelector
(
'
.build-link
'
).
getAttribute
(
'
href
'
)).
toEqual
(
environment
.
last_deployment
.
deployable
.
build_path
,
);
});
it
(
'
Should render deployable name and id
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.build-link
'
).
getAttribute
(
'
href
'
)
,
)
.
toEqual
(
environment
.
last_deployment
.
deployable
.
build_path
)
;
expect
(
component
.
$el
.
querySelector
(
'
.build-link
'
).
getAttribute
(
'
href
'
)).
toEqual
(
environment
.
last_deployment
.
deployable
.
build_path
,
);
});
});
describe
(
'
With commit information
'
,
()
=>
{
it
(
'
should render commit component
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.js-commit-component
'
),
).
toBeDefined
();
expect
(
component
.
$el
.
querySelector
(
'
.js-commit-component
'
)).
toBeDefined
();
});
});
});
describe
(
'
With manual actions
'
,
()
=>
{
it
(
'
Should render actions component
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.js-manual-actions-container
'
),
).
toBeDefined
();
expect
(
component
.
$el
.
querySelector
(
'
.js-manual-actions-container
'
)).
toBeDefined
();
});
});
describe
(
'
With external URL
'
,
()
=>
{
it
(
'
should render external url component
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.js-external-url-container
'
),
).
toBeDefined
();
expect
(
component
.
$el
.
querySelector
(
'
.js-external-url-container
'
)).
toBeDefined
();
});
});
describe
(
'
With stop action
'
,
()
=>
{
it
(
'
Should render stop action component
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.js-stop-component-container
'
),
).
toBeDefined
();
expect
(
component
.
$el
.
querySelector
(
'
.js-stop-component-container
'
)).
toBeDefined
();
});
});
describe
(
'
With retry action
'
,
()
=>
{
it
(
'
Should render rollback component
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.js-rollback-component-container
'
),
).
toBeDefined
();
expect
(
component
.
$el
.
querySelector
(
'
.js-rollback-component-container
'
)).
toBeDefined
();
});
});
});
...
...
spec/javascripts/environments/environment_table_spec.js
View file @
7e9f44fa
...
...
@@ -57,7 +57,7 @@ describe('Environment table', () => {
expect
(
vm
.
$el
.
querySelector
(
'
.deploy-board-icon
'
)).
not
.
toBeNull
();
});
it
(
'
should toggle deploy board visibility when arrow is clicked
'
,
(
done
)
=>
{
it
(
'
should toggle deploy board visibility when arrow is clicked
'
,
done
=>
{
const
mockItem
=
{
name
:
'
review
'
,
size
:
1
,
...
...
@@ -65,9 +65,7 @@ describe('Environment table', () => {
id
:
1
,
hasDeployBoard
:
true
,
deployBoardData
:
{
instances
:
[
{
status
:
'
ready
'
,
tooltip
:
'
foo
'
},
],
instances
:
[{
status
:
'
ready
'
,
tooltip
:
'
foo
'
}],
abort_url
:
'
url
'
,
rollback_url
:
'
url
'
,
completion
:
100
,
...
...
@@ -76,7 +74,7 @@ describe('Environment table', () => {
isDeployBoardVisible
:
false
,
};
eventHub
.
$on
(
'
toggleDeployBoard
'
,
(
env
)
=>
{
eventHub
.
$on
(
'
toggleDeployBoard
'
,
env
=>
{
expect
(
env
.
id
).
toEqual
(
mockItem
.
id
);
done
();
});
...
...
spec/javascripts/environments/environments_app_spec.js
View file @
7e9f44fa
...
...
@@ -33,7 +33,7 @@ describe('Environment', () => {
describe
(
'
successfull request
'
,
()
=>
{
describe
(
'
without environments
'
,
()
=>
{
beforeEach
(
(
done
)
=>
{
beforeEach
(
done
=>
{
mock
.
onGet
(
mockData
.
endpoint
).
reply
(
200
,
{
environments
:
[]
});
component
=
mountComponent
(
EnvironmentsComponent
,
mockData
);
...
...
@@ -44,30 +44,34 @@ describe('Environment', () => {
});
it
(
'
should render the empty state
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.js-new-environment-button
'
).
textContent
,
)
.
toContain
(
'
New environment
'
)
;
expect
(
component
.
$el
.
querySelector
(
'
.js-new-environment-button
'
).
textContent
).
toContain
(
'
New environment
'
,
);
expect
(
component
.
$el
.
querySelector
(
'
.js-blank-state-title
'
).
textContent
,
)
.
toContain
(
'
You don
\'
t have any environments right now
'
)
;
expect
(
component
.
$el
.
querySelector
(
'
.js-blank-state-title
'
).
textContent
).
toContain
(
"
You don't have any environments right now
"
,
);
});
});
describe
(
'
with paginated environments
'
,
()
=>
{
beforeEach
((
done
)
=>
{
mock
.
onGet
(
mockData
.
endpoint
).
reply
(
200
,
{
environments
:
[
environment
],
stopped_count
:
1
,
available_count
:
0
,
},
{
'
X-nExt-pAge
'
:
'
2
'
,
'
x-page
'
:
'
1
'
,
'
X-Per-Page
'
:
'
1
'
,
'
X-Prev-Page
'
:
''
,
'
X-TOTAL
'
:
'
37
'
,
'
X-Total-Pages
'
:
'
2
'
,
});
beforeEach
(
done
=>
{
mock
.
onGet
(
mockData
.
endpoint
).
reply
(
200
,
{
environments
:
[
environment
],
stopped_count
:
1
,
available_count
:
0
,
},
{
'
X-nExt-pAge
'
:
'
2
'
,
'
x-page
'
:
'
1
'
,
'
X-Per-Page
'
:
'
1
'
,
'
X-Prev-Page
'
:
''
,
'
X-TOTAL
'
:
'
37
'
,
'
X-Total-Pages
'
:
'
2
'
,
},
);
component
=
mountComponent
(
EnvironmentsComponent
,
mockData
);
...
...
@@ -78,19 +82,17 @@ describe('Environment', () => {
it
(
'
should render a table with environments
'
,
()
=>
{
expect
(
component
.
$el
.
querySelectorAll
(
'
table
'
)).
not
.
toBeNull
();
expect
(
component
.
$el
.
querySelector
(
'
.environment-name
'
).
textContent
.
trim
()
,
)
.
toEqual
(
environment
.
name
)
;
expect
(
component
.
$el
.
querySelector
(
'
.environment-name
'
).
textContent
.
trim
()).
toEqual
(
environment
.
name
,
);
});
describe
(
'
pagination
'
,
()
=>
{
it
(
'
should render pagination
'
,
()
=>
{
expect
(
component
.
$el
.
querySelectorAll
(
'
.gl-pagination li
'
).
length
,
).
toEqual
(
5
);
expect
(
component
.
$el
.
querySelectorAll
(
'
.gl-pagination li
'
).
length
).
toEqual
(
5
);
});
it
(
'
should make an API request when page is clicked
'
,
(
done
)
=>
{
it
(
'
should make an API request when page is clicked
'
,
done
=>
{
spyOn
(
component
,
'
updateContent
'
);
setTimeout
(()
=>
{
component
.
$el
.
querySelector
(
'
.gl-pagination li:nth-child(5) a
'
).
click
();
...
...
@@ -100,7 +102,7 @@ describe('Environment', () => {
},
0
);
});
it
(
'
should make an API request when using tabs
'
,
(
done
)
=>
{
it
(
'
should make an API request when using tabs
'
,
done
=>
{
setTimeout
(()
=>
{
spyOn
(
component
,
'
updateContent
'
);
component
.
$el
.
querySelector
(
'
.js-environments-tab-stopped
'
).
click
();
...
...
@@ -112,9 +114,11 @@ describe('Environment', () => {
});
describe
(
'
deploy boards
'
,
()
=>
{
it
(
'
should render arrow to open deploy boards
'
,
(
done
)
=>
{
it
(
'
should render arrow to open deploy boards
'
,
done
=>
{
setTimeout
(()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.deploy-board-icon.ic-chevron-right
'
)).
toBeDefined
();
expect
(
component
.
$el
.
querySelector
(
'
.deploy-board-icon.ic-chevron-right
'
),
).
toBeDefined
();
done
();
},
0
);
});
...
...
@@ -123,7 +127,7 @@ describe('Environment', () => {
});
describe
(
'
unsuccessfull request
'
,
()
=>
{
beforeEach
(
(
done
)
=>
{
beforeEach
(
done
=>
{
mock
.
onGet
(
mockData
.
endpoint
).
reply
(
500
,
{});
component
=
mountComponent
(
EnvironmentsComponent
,
mockData
);
...
...
@@ -134,15 +138,16 @@ describe('Environment', () => {
});
it
(
'
should render empty state
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.js-blank-state-title
'
).
textContent
,
)
.
toContain
(
'
You don
\'
t have any environments right now
'
)
;
expect
(
component
.
$el
.
querySelector
(
'
.js-blank-state-title
'
).
textContent
).
toContain
(
"
You don't have any environments right now
"
,
);
});
});
describe
(
'
expandable folders
'
,
()
=>
{
beforeEach
(()
=>
{
mock
.
onGet
(
mockData
.
endpoint
).
reply
(
200
,
mock
.
onGet
(
mockData
.
endpoint
).
reply
(
200
,
{
environments
:
[
folder
],
stopped_count
:
0
,
...
...
@@ -163,7 +168,7 @@ describe('Environment', () => {
component
=
mountComponent
(
EnvironmentsComponent
,
mockData
);
});
it
(
'
should open a closed folder
'
,
(
done
)
=>
{
it
(
'
should open a closed folder
'
,
done
=>
{
setTimeout
(()
=>
{
component
.
$el
.
querySelector
(
'
.folder-name
'
).
click
();
...
...
@@ -174,7 +179,7 @@ describe('Environment', () => {
},
0
);
});
it
(
'
should close an opened folder
'
,
(
done
)
=>
{
it
(
'
should close an opened folder
'
,
done
=>
{
setTimeout
(()
=>
{
// open folder
component
.
$el
.
querySelector
(
'
.folder-name
'
).
click
();
...
...
@@ -191,7 +196,7 @@ describe('Environment', () => {
},
0
);
});
it
(
'
should show children environments and a button to show all environments
'
,
(
done
)
=>
{
it
(
'
should show children environments and a button to show all environments
'
,
done
=>
{
setTimeout
(()
=>
{
// open folder
component
.
$el
.
querySelector
(
'
.folder-name
'
).
click
();
...
...
@@ -200,7 +205,9 @@ describe('Environment', () => {
// wait for next async request
setTimeout
(()
=>
{
expect
(
component
.
$el
.
querySelectorAll
(
'
.js-child-row
'
).
length
).
toEqual
(
1
);
expect
(
component
.
$el
.
querySelector
(
'
.text-center > a.btn
'
).
textContent
).
toContain
(
'
Show all
'
);
expect
(
component
.
$el
.
querySelector
(
'
.text-center > a.btn
'
).
textContent
).
toContain
(
'
Show all
'
,
);
done
();
});
});
...
...
@@ -210,7 +217,8 @@ describe('Environment', () => {
describe
(
'
methods
'
,
()
=>
{
beforeEach
(()
=>
{
mock
.
onGet
(
mockData
.
endpoint
).
reply
(
200
,
mock
.
onGet
(
mockData
.
endpoint
).
reply
(
200
,
{
environments
:
[],
stopped_count
:
0
,
...
...
@@ -224,8 +232,9 @@ describe('Environment', () => {
});
describe
(
'
updateContent
'
,
()
=>
{
it
(
'
should set given parameters
'
,
(
done
)
=>
{
component
.
updateContent
({
scope
:
'
stopped
'
,
page
:
'
3
'
})
it
(
'
should set given parameters
'
,
done
=>
{
component
.
updateContent
({
scope
:
'
stopped
'
,
page
:
'
3
'
})
.
then
(()
=>
{
expect
(
component
.
page
).
toEqual
(
'
3
'
);
expect
(
component
.
scope
).
toEqual
(
'
stopped
'
);
...
...
spec/javascripts/environments/folder/environments_folder_view_spec.js
View file @
7e9f44fa
...
...
@@ -32,37 +32,41 @@ describe('Environments Folder View', () => {
describe
(
'
successfull request
'
,
()
=>
{
beforeEach
(()
=>
{
mock
.
onGet
(
mockData
.
endpoint
).
reply
(
200
,
{
environments
:
environmentsList
,
stopped_count
:
1
,
available_count
:
0
,
},
{
'
X-nExt-pAge
'
:
'
2
'
,
'
x-page
'
:
'
1
'
,
'
X-Per-Page
'
:
'
2
'
,
'
X-Prev-Page
'
:
''
,
'
X-TOTAL
'
:
'
20
'
,
'
X-Total-Pages
'
:
'
10
'
,
});
mock
.
onGet
(
mockData
.
endpoint
).
reply
(
200
,
{
environments
:
environmentsList
,
stopped_count
:
1
,
available_count
:
0
,
},
{
'
X-nExt-pAge
'
:
'
2
'
,
'
x-page
'
:
'
1
'
,
'
X-Per-Page
'
:
'
2
'
,
'
X-Prev-Page
'
:
''
,
'
X-TOTAL
'
:
'
20
'
,
'
X-Total-Pages
'
:
'
10
'
,
},
);
component
=
mountComponent
(
Component
,
mockData
);
});
it
(
'
should render a table with environments
'
,
(
done
)
=>
{
it
(
'
should render a table with environments
'
,
done
=>
{
setTimeout
(()
=>
{
expect
(
component
.
$el
.
querySelectorAll
(
'
table
'
)).
not
.
toBeNull
();
expect
(
component
.
$el
.
querySelector
(
'
.environment-name
'
).
textContent
.
trim
()
,
)
.
toEqual
(
environmentsList
[
0
].
name
)
;
expect
(
component
.
$el
.
querySelector
(
'
.environment-name
'
).
textContent
.
trim
()).
toEqual
(
environmentsList
[
0
].
name
,
);
done
();
},
0
);
});
it
(
'
should render available tab with count
'
,
(
done
)
=>
{
it
(
'
should render available tab with count
'
,
done
=>
{
setTimeout
(()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.js-environments-tab-available
'
).
textContent
,
)
.
toContain
(
'
Available
'
)
;
expect
(
component
.
$el
.
querySelector
(
'
.js-environments-tab-available
'
).
textContent
).
toContain
(
'
Available
'
,
);
expect
(
component
.
$el
.
querySelector
(
'
.js-environments-tab-available .badge
'
).
textContent
,
...
...
@@ -71,11 +75,11 @@ describe('Environments Folder View', () => {
},
0
);
});
it
(
'
should render stopped tab with count
'
,
(
done
)
=>
{
it
(
'
should render stopped tab with count
'
,
done
=>
{
setTimeout
(()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.js-environments-tab-stopped
'
).
textContent
,
)
.
toContain
(
'
Stopped
'
)
;
expect
(
component
.
$el
.
querySelector
(
'
.js-environments-tab-stopped
'
).
textContent
).
toContain
(
'
Stopped
'
,
);
expect
(
component
.
$el
.
querySelector
(
'
.js-environments-tab-stopped .badge
'
).
textContent
,
...
...
@@ -84,36 +88,37 @@ describe('Environments Folder View', () => {
},
0
);
});
it
(
'
should render parent folder name
'
,
(
done
)
=>
{
it
(
'
should render parent folder name
'
,
done
=>
{
setTimeout
(()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.js-folder-name
'
).
textContent
,
)
.
toContain
(
'
Environments / review
'
)
;
expect
(
component
.
$el
.
querySelector
(
'
.js-folder-name
'
).
textContent
).
toContain
(
'
Environments / review
'
,
);
done
();
},
0
);
});
describe
(
'
pagination
'
,
()
=>
{
it
(
'
should render pagination
'
,
(
done
)
=>
{
it
(
'
should render pagination
'
,
done
=>
{
setTimeout
(()
=>
{
expect
(
component
.
$el
.
querySelectorAll
(
'
.gl-pagination
'
),
).
not
.
toBeNull
();
expect
(
component
.
$el
.
querySelectorAll
(
'
.gl-pagination
'
)).
not
.
toBeNull
();
done
();
},
0
);
});
it
(
'
should make an API request when changing page
'
,
(
done
)
=>
{
it
(
'
should make an API request when changing page
'
,
done
=>
{
spyOn
(
component
,
'
updateContent
'
);
setTimeout
(()
=>
{
component
.
$el
.
querySelector
(
'
.gl-pagination .js-last-button a
'
).
click
();
expect
(
component
.
updateContent
).
toHaveBeenCalledWith
({
scope
:
component
.
scope
,
page
:
'
10
'
});
expect
(
component
.
updateContent
).
toHaveBeenCalledWith
({
scope
:
component
.
scope
,
page
:
'
10
'
,
});
done
();
},
0
);
});
it
(
'
should make an API request when using tabs
'
,
(
done
)
=>
{
it
(
'
should make an API request when using tabs
'
,
done
=>
{
setTimeout
(()
=>
{
spyOn
(
component
,
'
updateContent
'
);
component
.
$el
.
querySelector
(
'
.js-environments-tab-stopped
'
).
click
();
...
...
@@ -125,7 +130,7 @@ describe('Environments Folder View', () => {
});
describe
(
'
deploy boards
'
,
()
=>
{
it
(
'
should render arrow to open deploy boards
'
,
(
done
)
=>
{
it
(
'
should render arrow to open deploy boards
'
,
done
=>
{
setTimeout
(()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.folder-icon.ic-chevron-right
'
)).
not
.
toBeNull
();
done
();
...
...
@@ -143,20 +148,18 @@ describe('Environments Folder View', () => {
component
=
mountComponent
(
Component
,
mockData
);
});
it
(
'
should not render a table
'
,
(
done
)
=>
{
it
(
'
should not render a table
'
,
done
=>
{
setTimeout
(()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
table
'
),
).
toBe
(
null
);
expect
(
component
.
$el
.
querySelector
(
'
table
'
)).
toBe
(
null
);
done
();
},
0
);
});
it
(
'
should render available tab with count 0
'
,
(
done
)
=>
{
it
(
'
should render available tab with count 0
'
,
done
=>
{
setTimeout
(()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.js-environments-tab-available
'
).
textContent
,
)
.
toContain
(
'
Available
'
)
;
expect
(
component
.
$el
.
querySelector
(
'
.js-environments-tab-available
'
).
textContent
).
toContain
(
'
Available
'
,
);
expect
(
component
.
$el
.
querySelector
(
'
.js-environments-tab-available .badge
'
).
textContent
,
...
...
@@ -165,11 +168,11 @@ describe('Environments Folder View', () => {
},
0
);
});
it
(
'
should render stopped tab with count 0
'
,
(
done
)
=>
{
it
(
'
should render stopped tab with count 0
'
,
done
=>
{
setTimeout
(()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.js-environments-tab-stopped
'
).
textContent
,
)
.
toContain
(
'
Stopped
'
)
;
expect
(
component
.
$el
.
querySelector
(
'
.js-environments-tab-stopped
'
).
textContent
).
toContain
(
'
Stopped
'
,
);
expect
(
component
.
$el
.
querySelector
(
'
.js-environments-tab-stopped .badge
'
).
textContent
,
...
...
@@ -190,8 +193,9 @@ describe('Environments Folder View', () => {
});
describe
(
'
updateContent
'
,
()
=>
{
it
(
'
should set given parameters
'
,
(
done
)
=>
{
component
.
updateContent
({
scope
:
'
stopped
'
,
page
:
'
4
'
})
it
(
'
should set given parameters
'
,
done
=>
{
component
.
updateContent
({
scope
:
'
stopped
'
,
page
:
'
4
'
})
.
then
(()
=>
{
expect
(
component
.
page
).
toEqual
(
'
4
'
);
expect
(
component
.
scope
).
toEqual
(
'
stopped
'
);
...
...
spec/javascripts/issue_show/components/title_spec.js
View file @
7e9f44fa
...
...
@@ -25,25 +25,21 @@ describe('Title component', () => {
});
it
(
'
renders title HTML
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.title
'
).
innerHTML
.
trim
(),
).
toBe
(
'
Testing <img>
'
);
expect
(
vm
.
$el
.
querySelector
(
'
.title
'
).
innerHTML
.
trim
()).
toBe
(
'
Testing <img>
'
);
});
it
(
'
updates page title when changing titleHtml
'
,
(
done
)
=>
{
it
(
'
updates page title when changing titleHtml
'
,
done
=>
{
spyOn
(
vm
,
'
setPageTitle
'
);
vm
.
titleHtml
=
'
test
'
;
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
setPageTitle
,
).
toHaveBeenCalled
();
expect
(
vm
.
setPageTitle
).
toHaveBeenCalled
();
done
();
});
});
it
(
'
animates title changes
'
,
(
done
)
=>
{
it
(
'
animates title changes
'
,
done
=>
{
vm
.
titleHtml
=
'
test
'
;
Vue
.
nextTick
(()
=>
{
...
...
@@ -61,14 +57,12 @@ describe('Title component', () => {
});
});
it
(
'
updates page title after changing title
'
,
(
done
)
=>
{
it
(
'
updates page title after changing title
'
,
done
=>
{
vm
.
titleHtml
=
'
changed
'
;
vm
.
titleText
=
'
changed
'
;
Vue
.
nextTick
(()
=>
{
expect
(
document
.
querySelector
(
'
title
'
).
textContent
.
trim
(),
).
toContain
(
'
changed
'
);
expect
(
document
.
querySelector
(
'
title
'
).
textContent
.
trim
()).
toContain
(
'
changed
'
);
done
();
});
...
...
spec/javascripts/jobs/components/job_app_spec.js
View file @
7e9f44fa
...
...
@@ -234,7 +234,7 @@ describe('Job App ', () => {
);
done
();
},
0
);
})
})
;
});
it
(
'
does not renders stuck block when there are no runners
'
,
done
=>
{
...
...
spec/javascripts/jobs/store/actions_spec.js
View file @
7e9f44fa
...
...
@@ -68,41 +68,20 @@ describe('Job State actions', () => {
describe
(
'
hideSidebar
'
,
()
=>
{
it
(
'
should commit HIDE_SIDEBAR mutation
'
,
done
=>
{
testAction
(
hideSidebar
,
null
,
mockedState
,
[{
type
:
types
.
HIDE_SIDEBAR
}],
[],
done
,
);
testAction
(
hideSidebar
,
null
,
mockedState
,
[{
type
:
types
.
HIDE_SIDEBAR
}],
[],
done
);
});
});
describe
(
'
showSidebar
'
,
()
=>
{
it
(
'
should commit HIDE_SIDEBAR mutation
'
,
done
=>
{
testAction
(
showSidebar
,
null
,
mockedState
,
[{
type
:
types
.
SHOW_SIDEBAR
}],
[],
done
,
);
testAction
(
showSidebar
,
null
,
mockedState
,
[{
type
:
types
.
SHOW_SIDEBAR
}],
[],
done
);
});
});
describe
(
'
toggleSidebar
'
,
()
=>
{
describe
(
'
when isSidebarOpen is true
'
,
()
=>
{
it
(
'
should dispatch hideSidebar
'
,
done
=>
{
testAction
(
toggleSidebar
,
null
,
mockedState
,
[],
[{
type
:
'
hideSidebar
'
}],
done
,
);
testAction
(
toggleSidebar
,
null
,
mockedState
,
[],
[{
type
:
'
hideSidebar
'
}],
done
);
});
});
...
...
@@ -110,14 +89,7 @@ describe('Job State actions', () => {
it
(
'
should dispatch showSidebar
'
,
done
=>
{
mockedState
.
isSidebarOpen
=
false
;
testAction
(
toggleSidebar
,
null
,
mockedState
,
[],
[{
type
:
'
showSidebar
'
}],
done
,
);
testAction
(
toggleSidebar
,
null
,
mockedState
,
[],
[{
type
:
'
showSidebar
'
}],
done
);
});
});
});
...
...
spec/javascripts/jobs/store/getters_spec.js
View file @
7e9f44fa
...
...
@@ -235,7 +235,7 @@ describe('Job Store Getters', () => {
it
(
'
returns true
'
,
()
=>
{
localState
.
job
.
runners
=
{
available
:
true
,
online
:
false
online
:
false
,
};
expect
(
getters
.
hasRunnersForProject
(
localState
)).
toEqual
(
true
);
...
...
@@ -246,7 +246,7 @@ describe('Job Store Getters', () => {
it
(
'
returns false
'
,
()
=>
{
localState
.
job
.
runners
=
{
available
:
false
,
online
:
false
online
:
false
,
};
expect
(
getters
.
hasRunnersForProject
(
localState
)).
toEqual
(
false
);
...
...
@@ -257,7 +257,7 @@ describe('Job Store Getters', () => {
it
(
'
returns false
'
,
()
=>
{
localState
.
job
.
runners
=
{
available
:
false
,
online
:
true
online
:
true
,
};
expect
(
getters
.
hasRunnersForProject
(
localState
)).
toEqual
(
false
);
...
...
spec/javascripts/lib/utils/common_utils_spec.js
View file @
7e9f44fa
...
...
@@ -35,9 +35,7 @@ describe('common_utils', () => {
});
it
(
'
should decode params
'
,
()
=>
{
expect
(
commonUtils
.
urlParamsToArray
(
'
?label_name%5B%5D=test
'
)[
0
],
).
toBe
(
'
label_name[]=test
'
);
expect
(
commonUtils
.
urlParamsToArray
(
'
?label_name%5B%5D=test
'
)[
0
]).
toBe
(
'
label_name[]=test
'
);
});
it
(
'
should remove the question mark from the search params
'
,
()
=>
{
...
...
@@ -49,25 +47,19 @@ describe('common_utils', () => {
describe
(
'
urlParamsToObject
'
,
()
=>
{
it
(
'
parses path for label with trailing +
'
,
()
=>
{
expect
(
commonUtils
.
urlParamsToObject
(
'
label_name[]=label%2B
'
,
{}),
).
toEqual
({
expect
(
commonUtils
.
urlParamsToObject
(
'
label_name[]=label%2B
'
,
{})).
toEqual
({
label_name
:
[
'
label+
'
],
});
});
it
(
'
parses path for milestone with trailing +
'
,
()
=>
{
expect
(
commonUtils
.
urlParamsToObject
(
'
milestone_title=A%2B
'
,
{}),
).
toEqual
({
expect
(
commonUtils
.
urlParamsToObject
(
'
milestone_title=A%2B
'
,
{})).
toEqual
({
milestone_title
:
'
A+
'
,
});
});
it
(
'
parses path for search terms with spaces
'
,
()
=>
{
expect
(
commonUtils
.
urlParamsToObject
(
'
search=two+words
'
,
{}),
).
toEqual
({
expect
(
commonUtils
.
urlParamsToObject
(
'
search=two+words
'
,
{})).
toEqual
({
search
:
'
two words
'
,
});
});
...
...
@@ -187,7 +179,10 @@ describe('common_utils', () => {
describe
(
'
parseQueryStringIntoObject
'
,
()
=>
{
it
(
'
should return object with query parameters
'
,
()
=>
{
expect
(
commonUtils
.
parseQueryStringIntoObject
(
'
scope=all&page=2
'
)).
toEqual
({
scope
:
'
all
'
,
page
:
'
2
'
});
expect
(
commonUtils
.
parseQueryStringIntoObject
(
'
scope=all&page=2
'
)).
toEqual
({
scope
:
'
all
'
,
page
:
'
2
'
,
});
expect
(
commonUtils
.
parseQueryStringIntoObject
(
'
scope=all
'
)).
toEqual
({
scope
:
'
all
'
});
expect
(
commonUtils
.
parseQueryStringIntoObject
()).
toEqual
({});
});
...
...
@@ -211,7 +206,9 @@ describe('common_utils', () => {
describe
(
'
buildUrlWithCurrentLocation
'
,
()
=>
{
it
(
'
should build an url with current location and given parameters
'
,
()
=>
{
expect
(
commonUtils
.
buildUrlWithCurrentLocation
()).
toEqual
(
window
.
location
.
pathname
);
expect
(
commonUtils
.
buildUrlWithCurrentLocation
(
'
?page=2
'
)).
toEqual
(
`
${
window
.
location
.
pathname
}
?page=2`
);
expect
(
commonUtils
.
buildUrlWithCurrentLocation
(
'
?page=2
'
)).
toEqual
(
`
${
window
.
location
.
pathname
}
?page=2`
,
);
});
});
...
...
@@ -266,21 +263,24 @@ describe('common_utils', () => {
});
describe
(
'
normalizeCRLFHeaders
'
,
()
=>
{
beforeEach
(
function
()
{
this
.
CLRFHeaders
=
'
a-header: a-value
\n
Another-Header: ANOTHER-VALUE
\n
LaSt-HeAdEr: last-VALUE
'
;
beforeEach
(
function
()
{
this
.
CLRFHeaders
=
'
a-header: a-value
\n
Another-Header: ANOTHER-VALUE
\n
LaSt-HeAdEr: last-VALUE
'
;
spyOn
(
String
.
prototype
,
'
split
'
).
and
.
callThrough
();
this
.
normalizeCRLFHeaders
=
commonUtils
.
normalizeCRLFHeaders
(
this
.
CLRFHeaders
);
});
it
(
'
should split by newline
'
,
function
()
{
it
(
'
should split by newline
'
,
function
()
{
expect
(
String
.
prototype
.
split
).
toHaveBeenCalledWith
(
'
\n
'
);
});
it
(
'
should split by colon+space for each header
'
,
function
()
{
expect
(
String
.
prototype
.
split
.
calls
.
allArgs
().
filter
(
args
=>
args
[
0
]
===
'
:
'
).
length
).
toBe
(
3
);
it
(
'
should split by colon+space for each header
'
,
function
()
{
expect
(
String
.
prototype
.
split
.
calls
.
allArgs
().
filter
(
args
=>
args
[
0
]
===
'
:
'
).
length
).
toBe
(
3
,
);
});
it
(
'
should return a normalized headers object
'
,
function
()
{
it
(
'
should return a normalized headers object
'
,
function
()
{
expect
(
this
.
normalizeCRLFHeaders
).
toEqual
({
'
A-HEADER
'
:
'
a-value
'
,
'
ANOTHER-HEADER
'
:
'
ANOTHER-VALUE
'
,
...
...
@@ -359,67 +359,79 @@ describe('common_utils', () => {
spyOn
(
window
,
'
setTimeout
'
).
and
.
callFake
(
cb
=>
origSetTimeout
(
cb
,
0
));
});
it
(
'
solves the promise from the callback
'
,
(
done
)
=>
{
it
(
'
solves the promise from the callback
'
,
done
=>
{
const
expectedResponseValue
=
'
Success!
'
;
commonUtils
.
backOff
((
next
,
stop
)
=>
(
new
Promise
((
resolve
)
=>
{
resolve
(
expectedResponseValue
);
}).
then
((
resp
)
=>
{
stop
(
resp
);
commonUtils
.
backOff
((
next
,
stop
)
=>
new
Promise
(
resolve
=>
{
resolve
(
expectedResponseValue
);
})
.
then
(
resp
=>
{
stop
(
resp
);
})
.
catch
(
done
.
fail
),
)
.
then
(
respBackoff
=>
{
expect
(
respBackoff
).
toBe
(
expectedResponseValue
);
done
();
})
).
catch
(
done
.
fail
)).
then
((
respBackoff
)
=>
{
expect
(
respBackoff
).
toBe
(
expectedResponseValue
);
done
();
}).
catch
(
done
.
fail
);
.
catch
(
done
.
fail
);
});
it
(
'
catches the rejected promise from the callback
'
,
(
done
)
=>
{
it
(
'
catches the rejected promise from the callback
'
,
done
=>
{
const
errorMessage
=
'
Mistakes were made!
'
;
commonUtils
.
backOff
((
next
,
stop
)
=>
{
new
Promise
((
resolve
,
reject
)
=>
{
reject
(
new
Error
(
errorMessage
));
}).
then
((
resp
)
=>
{
stop
(
resp
);
}).
catch
(
err
=>
stop
(
err
));
}).
catch
((
errBackoffResp
)
=>
{
expect
(
errBackoffResp
instanceof
Error
).
toBe
(
true
);
expect
(
errBackoffResp
.
message
).
toBe
(
errorMessage
);
done
();
});
commonUtils
.
backOff
((
next
,
stop
)
=>
{
new
Promise
((
resolve
,
reject
)
=>
{
reject
(
new
Error
(
errorMessage
));
})
.
then
(
resp
=>
{
stop
(
resp
);
})
.
catch
(
err
=>
stop
(
err
));
})
.
catch
(
errBackoffResp
=>
{
expect
(
errBackoffResp
instanceof
Error
).
toBe
(
true
);
expect
(
errBackoffResp
.
message
).
toBe
(
errorMessage
);
done
();
});
});
it
(
'
solves the promise correctly after retrying a third time
'
,
(
done
)
=>
{
it
(
'
solves the promise correctly after retrying a third time
'
,
done
=>
{
let
numberOfCalls
=
1
;
const
expectedResponseValue
=
'
Success!
'
;
commonUtils
.
backOff
((
next
,
stop
)
=>
(
Promise
.
resolve
(
expectedResponseValue
)
.
then
((
resp
)
=>
{
if
(
numberOfCalls
<
3
)
{
numberOfCalls
+=
1
;
next
();
}
else
{
stop
(
resp
);
}
})
).
catch
(
done
.
fail
)).
then
((
respBackoff
)
=>
{
const
timeouts
=
window
.
setTimeout
.
calls
.
allArgs
().
map
(([,
timeout
])
=>
timeout
);
commonUtils
.
backOff
((
next
,
stop
)
=>
Promise
.
resolve
(
expectedResponseValue
)
.
then
(
resp
=>
{
if
(
numberOfCalls
<
3
)
{
numberOfCalls
+=
1
;
next
();
}
else
{
stop
(
resp
);
}
})
.
catch
(
done
.
fail
),
)
.
then
(
respBackoff
=>
{
const
timeouts
=
window
.
setTimeout
.
calls
.
allArgs
().
map
(([,
timeout
])
=>
timeout
);
expect
(
timeouts
).
toEqual
([
2000
,
4000
]);
expect
(
respBackoff
).
toBe
(
expectedResponseValue
);
done
();
}).
catch
(
done
.
fail
);
expect
(
timeouts
).
toEqual
([
2000
,
4000
]);
expect
(
respBackoff
).
toBe
(
expectedResponseValue
);
done
();
})
.
catch
(
done
.
fail
);
});
it
(
'
rejects the backOff promise after timing out
'
,
(
done
)
=>
{
commonUtils
.
backOff
(
next
=>
next
(),
64000
)
.
catch
((
errBackoffResp
)
=>
{
const
timeouts
=
window
.
setTimeout
.
calls
.
allArgs
().
map
(([,
timeout
])
=>
timeout
);
it
(
'
rejects the backOff promise after timing out
'
,
done
=>
{
commonUtils
.
backOff
(
next
=>
next
(),
64000
).
catch
(
errBackoffResp
=>
{
const
timeouts
=
window
.
setTimeout
.
calls
.
allArgs
().
map
(([,
timeout
])
=>
timeout
);
expect
(
timeouts
).
toEqual
([
2000
,
4000
,
8000
,
16000
,
32000
,
32000
]);
expect
(
errBackoffResp
instanceof
Error
).
toBe
(
true
);
expect
(
errBackoffResp
.
message
).
toBe
(
'
BACKOFF_TIMEOUT
'
);
done
();
});
expect
(
timeouts
).
toEqual
([
2000
,
4000
,
8000
,
16000
,
32000
,
32000
]);
expect
(
errBackoffResp
instanceof
Error
).
toBe
(
true
);
expect
(
errBackoffResp
.
message
).
toBe
(
'
BACKOFF_TIMEOUT
'
);
done
();
});
});
});
...
...
@@ -466,11 +478,14 @@ describe('common_utils', () => {
});
describe
(
'
createOverlayIcon
'
,
()
=>
{
it
(
'
should return the favicon with the overlay
'
,
(
done
)
=>
{
commonUtils
.
createOverlayIcon
(
faviconDataUrl
,
overlayDataUrl
).
then
((
url
)
=>
{
expect
(
url
).
toEqual
(
faviconWithOverlayDataUrl
);
done
();
}).
catch
(
done
.
fail
);
it
(
'
should return the favicon with the overlay
'
,
done
=>
{
commonUtils
.
createOverlayIcon
(
faviconDataUrl
,
overlayDataUrl
)
.
then
(
url
=>
{
expect
(
url
).
toEqual
(
faviconWithOverlayDataUrl
);
done
();
})
.
catch
(
done
.
fail
);
});
});
...
...
@@ -486,11 +501,16 @@ describe('common_utils', () => {
document
.
body
.
removeChild
(
document
.
getElementById
(
'
favicon
'
));
});
it
(
'
should set page favicon to provided favicon overlay
'
,
(
done
)
=>
{
commonUtils
.
setFaviconOverlay
(
overlayDataUrl
).
then
(()
=>
{
expect
(
document
.
getElementById
(
'
favicon
'
).
getAttribute
(
'
href
'
)).
toEqual
(
faviconWithOverlayDataUrl
);
done
();
}).
catch
(
done
.
fail
);
it
(
'
should set page favicon to provided favicon overlay
'
,
done
=>
{
commonUtils
.
setFaviconOverlay
(
overlayDataUrl
)
.
then
(()
=>
{
expect
(
document
.
getElementById
(
'
favicon
'
).
getAttribute
(
'
href
'
)).
toEqual
(
faviconWithOverlayDataUrl
,
);
done
();
})
.
catch
(
done
.
fail
);
});
});
...
...
@@ -512,24 +532,24 @@ describe('common_utils', () => {
document
.
body
.
removeChild
(
document
.
getElementById
(
'
favicon
'
));
});
it
(
'
should reset favicon in case of error
'
,
(
done
)
=>
{
it
(
'
should reset favicon in case of error
'
,
done
=>
{
mock
.
onGet
(
BUILD_URL
).
replyOnce
(
500
);
commonUtils
.
setCiStatusFavicon
(
BUILD_URL
)
.
catch
(()
=>
{
const
favicon
=
document
.
getElementById
(
'
favicon
'
);
commonUtils
.
setCiStatusFavicon
(
BUILD_URL
).
catch
(()
=>
{
const
favicon
=
document
.
getElementById
(
'
favicon
'
);
expect
(
favicon
.
getAttribute
(
'
href
'
)).
toEqual
(
faviconDataUrl
);
done
();
});
expect
(
favicon
.
getAttribute
(
'
href
'
)).
toEqual
(
faviconDataUrl
);
done
();
});
});
it
(
'
should set page favicon to CI status favicon based on provided status
'
,
(
done
)
=>
{
it
(
'
should set page favicon to CI status favicon based on provided status
'
,
done
=>
{
mock
.
onGet
(
BUILD_URL
).
reply
(
200
,
{
favicon
:
overlayDataUrl
,
});
commonUtils
.
setCiStatusFavicon
(
BUILD_URL
)
commonUtils
.
setCiStatusFavicon
(
BUILD_URL
)
.
then
(()
=>
{
const
favicon
=
document
.
getElementById
(
'
favicon
'
);
...
...
@@ -554,11 +574,15 @@ describe('common_utils', () => {
});
it
(
'
should return the svg for a linked icon
'
,
()
=>
{
expect
(
commonUtils
.
spriteIcon
(
'
test
'
)).
toEqual
(
'
<svg ><use xlink:href="icons.svg#test" /></svg>
'
);
expect
(
commonUtils
.
spriteIcon
(
'
test
'
)).
toEqual
(
'
<svg ><use xlink:href="icons.svg#test" /></svg>
'
,
);
});
it
(
'
should set svg className when passed
'
,
()
=>
{
expect
(
commonUtils
.
spriteIcon
(
'
test
'
,
'
fa fa-test
'
)).
toEqual
(
'
<svg class="fa fa-test"><use xlink:href="icons.svg#test" /></svg>
'
);
expect
(
commonUtils
.
spriteIcon
(
'
test
'
,
'
fa fa-test
'
)).
toEqual
(
'
<svg class="fa fa-test"><use xlink:href="icons.svg#test" /></svg>
'
,
);
});
});
...
...
@@ -578,7 +602,7 @@ describe('common_utils', () => {
const
convertedObj
=
commonUtils
.
convertObjectPropsToCamelCase
(
mockObj
);
Object
.
keys
(
convertedObj
).
forEach
(
(
prop
)
=>
{
Object
.
keys
(
convertedObj
).
forEach
(
prop
=>
{
expect
(
snakeRegEx
.
test
(
prop
)).
toBeFalsy
();
expect
(
convertedObj
[
prop
]).
toBe
(
mockObj
[
mappings
[
prop
]]);
});
...
...
@@ -597,9 +621,7 @@ describe('common_utils', () => {
},
};
expect
(
commonUtils
.
convertObjectPropsToCamelCase
(
obj
),
).
toEqual
({
expect
(
commonUtils
.
convertObjectPropsToCamelCase
(
obj
)).
toEqual
({
snakeKey
:
{
child_snake_key
:
'
value
'
,
},
...
...
@@ -614,9 +636,7 @@ describe('common_utils', () => {
},
};
expect
(
commonUtils
.
convertObjectPropsToCamelCase
(
obj
,
{
deep
:
true
}),
).
toEqual
({
expect
(
commonUtils
.
convertObjectPropsToCamelCase
(
obj
,
{
deep
:
true
})).
toEqual
({
snakeKey
:
{
childSnakeKey
:
'
value
'
,
},
...
...
@@ -630,9 +650,7 @@ describe('common_utils', () => {
},
];
expect
(
commonUtils
.
convertObjectPropsToCamelCase
(
arr
,
{
deep
:
true
}),
).
toEqual
([
expect
(
commonUtils
.
convertObjectPropsToCamelCase
(
arr
,
{
deep
:
true
})).
toEqual
([
{
childSnakeKey
:
'
value
'
,
},
...
...
@@ -648,9 +666,7 @@ describe('common_utils', () => {
],
];
expect
(
commonUtils
.
convertObjectPropsToCamelCase
(
arr
,
{
deep
:
true
}),
).
toEqual
([
expect
(
commonUtils
.
convertObjectPropsToCamelCase
(
arr
,
{
deep
:
true
})).
toEqual
([
[
{
childSnakeKey
:
'
value
'
,
...
...
@@ -680,4 +696,3 @@ describe('common_utils', () => {
});
});
});
spec/javascripts/monitoring/graph/flag_spec.js
View file @
7e9f44fa
...
...
@@ -2,7 +2,7 @@ import Vue from 'vue';
import
GraphFlag
from
'
~/monitoring/components/graph/flag.vue
'
;
import
{
deploymentData
}
from
'
../mock_data
'
;
const
createComponent
=
(
propsData
)
=>
{
const
createComponent
=
propsData
=>
{
const
Component
=
Vue
.
extend
(
GraphFlag
);
return
new
Component
({
...
...
@@ -51,8 +51,7 @@ describe('GraphFlag', () => {
it
(
'
has a line at the currentXCoordinate
'
,
()
=>
{
component
=
createComponent
(
defaultValuesComponent
);
expect
(
component
.
$el
.
style
.
left
)
.
toEqual
(
`
${
70
+
component
.
currentXCoordinate
}
px`
);
expect
(
component
.
$el
.
style
.
left
).
toEqual
(
`
${
70
+
component
.
currentXCoordinate
}
px`
);
});
describe
(
'
Deployment flag
'
,
()
=>
{
...
...
@@ -62,9 +61,9 @@ describe('GraphFlag', () => {
deploymentFlagData
,
});
expect
(
deploymentFlagComponent
.
$el
.
querySelector
(
'
.popover-header
'
)
,
)
.
toContainText
(
'
Deployed
'
)
;
expect
(
deploymentFlagComponent
.
$el
.
querySelector
(
'
.popover-header
'
)).
toContainText
(
'
Deployed
'
,
);
});
it
(
'
contains the ref when a tag is available
'
,
()
=>
{
...
...
@@ -78,13 +77,13 @@ describe('GraphFlag', () => {
},
});
expect
(
deploymentFlagComponent
.
$el
.
querySelector
(
'
.deploy-meta-content
'
)
,
)
.
toContainText
(
'
f5bcd1d9
'
)
;
expect
(
deploymentFlagComponent
.
$el
.
querySelector
(
'
.deploy-meta-content
'
)).
toContainText
(
'
f5bcd1d9
'
,
);
expect
(
deploymentFlagComponent
.
$el
.
querySelector
(
'
.deploy-meta-content
'
)
,
)
.
toContainText
(
'
1.0
'
)
;
expect
(
deploymentFlagComponent
.
$el
.
querySelector
(
'
.deploy-meta-content
'
)).
toContainText
(
'
1.0
'
,
);
});
it
(
'
does not contain the ref when a tag is unavailable
'
,
()
=>
{
...
...
@@ -98,13 +97,13 @@ describe('GraphFlag', () => {
},
});
expect
(
deploymentFlagComponent
.
$el
.
querySelector
(
'
.deploy-meta-content
'
)
,
)
.
toContainText
(
'
f5bcd1d9
'
)
;
expect
(
deploymentFlagComponent
.
$el
.
querySelector
(
'
.deploy-meta-content
'
)).
toContainText
(
'
f5bcd1d9
'
,
);
expect
(
deploymentFlagComponent
.
$el
.
querySelector
(
'
.deploy-meta-content
'
)
,
)
.
not
.
toContainText
(
'
1.0
'
)
;
expect
(
deploymentFlagComponent
.
$el
.
querySelector
(
'
.deploy-meta-content
'
)).
not
.
toContainText
(
'
1.0
'
,
);
});
});
...
...
spec/javascripts/notes/components/discussion_filter_spec.js
View file @
7e9f44fa
...
...
@@ -11,11 +11,13 @@ describe('DiscussionFilter component', () => {
beforeEach
(()
=>
{
store
=
createStore
();
const
discussions
=
[{
...
discussionMock
,
id
:
discussionMock
.
id
,
notes
:
[{
...
discussionMock
.
notes
[
0
],
resolvable
:
true
,
resolved
:
true
}],
}];
const
discussions
=
[
{
...
discussionMock
,
id
:
discussionMock
.
id
,
notes
:
[{
...
discussionMock
.
notes
[
0
],
resolvable
:
true
,
resolved
:
true
}],
},
];
const
Component
=
Vue
.
extend
(
DiscussionFilter
);
const
defaultValue
=
discussionFiltersMock
[
0
].
value
;
...
...
@@ -35,11 +37,15 @@ describe('DiscussionFilter component', () => {
});
it
(
'
renders the all filters
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelectorAll
(
'
.dropdown-menu li
'
).
length
).
toEqual
(
discussionFiltersMock
.
length
);
expect
(
vm
.
$el
.
querySelectorAll
(
'
.dropdown-menu li
'
).
length
).
toEqual
(
discussionFiltersMock
.
length
,
);
});
it
(
'
renders the default selected item
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
#discussion-filter-dropdown
'
).
textContent
.
trim
()).
toEqual
(
discussionFiltersMock
[
0
].
title
);
expect
(
vm
.
$el
.
querySelector
(
'
#discussion-filter-dropdown
'
).
textContent
.
trim
()).
toEqual
(
discussionFiltersMock
[
0
].
title
,
);
});
it
(
'
updates to the selected item
'
,
()
=>
{
...
...
spec/javascripts/notes/components/note_app_spec.js
View file @
7e9f44fa
...
...
@@ -97,7 +97,8 @@ describe('note_app', () => {
});
it
(
'
should render list of notes
'
,
done
=>
{
const
note
=
mockData
.
INDIVIDUAL_NOTE_RESPONSE_MAP
.
GET
[
const
note
=
mockData
.
INDIVIDUAL_NOTE_RESPONSE_MAP
.
GET
[
'
/gitlab-org/gitlab-ce/issues/26/discussions.json
'
][
0
].
notes
[
0
];
...
...
spec/javascripts/pipelines/graph/graph_component_spec.js
View file @
7e9f44fa
...
...
@@ -28,83 +28,89 @@ describe('graph component', () => {
});
});
describe
(
'
when linked pipelines are present
'
,
function
()
{
beforeEach
(
function
()
{
describe
(
'
when linked pipelines are present
'
,
function
()
{
beforeEach
(
function
()
{
component
=
mountComponent
(
GraphComponent
,
{
isLoading
:
false
,
pipeline
:
graphJSON
,
});
});
describe
(
'
rendered output
'
,
function
()
{
it
(
'
should include the pipelines graph
'
,
function
()
{
describe
(
'
rendered output
'
,
function
()
{
it
(
'
should include the pipelines graph
'
,
function
()
{
expect
(
component
.
$el
.
classList
.
contains
(
'
js-pipeline-graph
'
)).
toEqual
(
true
);
});
it
(
'
should not include the loading icon
'
,
function
()
{
it
(
'
should not include the loading icon
'
,
function
()
{
expect
(
component
.
$el
.
querySelector
(
'
.fa-spinner
'
)).
toBeNull
();
});
it
(
'
should include the stage column list
'
,
function
()
{
it
(
'
should include the stage column list
'
,
function
()
{
expect
(
component
.
$el
.
querySelector
(
'
.stage-column-list
'
)).
not
.
toBeNull
();
});
it
(
'
should include the no-margin class on the first child
'
,
function
()
{
const
firstStageColumnElement
=
component
.
$el
.
querySelector
(
'
.stage-column-list .stage-column
'
);
it
(
'
should include the no-margin class on the first child
'
,
function
()
{
const
firstStageColumnElement
=
component
.
$el
.
querySelector
(
'
.stage-column-list .stage-column
'
,
);
expect
(
firstStageColumnElement
.
classList
.
contains
(
'
no-margin
'
)).
toEqual
(
true
);
});
it
(
'
should include the has-only-one-job class on the first child
'
,
function
()
{
const
firstStageColumnElement
=
component
.
$el
.
querySelector
(
'
.stage-column-list .stage-column
'
);
it
(
'
should include the has-only-one-job class on the first child
'
,
function
()
{
const
firstStageColumnElement
=
component
.
$el
.
querySelector
(
'
.stage-column-list .stage-column
'
,
);
expect
(
firstStageColumnElement
.
classList
.
contains
(
'
has-only-one-job
'
)).
toEqual
(
true
);
});
it
(
'
should include the left-margin class on the second child
'
,
function
()
{
const
firstStageColumnElement
=
component
.
$el
.
querySelector
(
'
.stage-column-list .stage-column:last-child
'
);
it
(
'
should include the left-margin class on the second child
'
,
function
()
{
const
firstStageColumnElement
=
component
.
$el
.
querySelector
(
'
.stage-column-list .stage-column:last-child
'
,
);
expect
(
firstStageColumnElement
.
classList
.
contains
(
'
left-margin
'
)).
toEqual
(
true
);
});
it
(
'
should include the has-linked-pipelines flag
'
,
function
()
{
it
(
'
should include the has-linked-pipelines flag
'
,
function
()
{
expect
(
component
.
$el
.
querySelector
(
'
.has-linked-pipelines
'
)).
not
.
toBeNull
();
});
});
describe
(
'
computeds and methods
'
,
function
()
{
describe
(
'
capitalizeStageName
'
,
function
()
{
it
(
'
it capitalizes the stage name
'
,
function
()
{
describe
(
'
computeds and methods
'
,
function
()
{
describe
(
'
capitalizeStageName
'
,
function
()
{
it
(
'
it capitalizes the stage name
'
,
function
()
{
expect
(
component
.
capitalizeStageName
(
'
mystage
'
)).
toBe
(
'
Mystage
'
);
});
});
describe
(
'
stageConnectorClass
'
,
function
()
{
it
(
'
it returns left-margin when there is a triggerer
'
,
function
()
{
describe
(
'
stageConnectorClass
'
,
function
()
{
it
(
'
it returns left-margin when there is a triggerer
'
,
function
()
{
expect
(
component
.
stageConnectorClass
(
0
,
{
groups
:
[
'
job
'
]
})).
toBe
(
'
no-margin
'
);
});
});
});
describe
(
'
linked pipelines components
'
,
function
()
{
it
(
'
should coerce triggeredBy into a collection
'
,
function
()
{
describe
(
'
linked pipelines components
'
,
function
()
{
it
(
'
should coerce triggeredBy into a collection
'
,
function
()
{
expect
(
component
.
triggeredBy
.
length
).
toBe
(
1
);
});
it
(
'
should render an upstream pipelines column
'
,
function
()
{
it
(
'
should render an upstream pipelines column
'
,
function
()
{
expect
(
component
.
$el
.
querySelector
(
'
.linked-pipelines-column
'
)).
not
.
toBeNull
();
expect
(
component
.
$el
.
innerHTML
).
toContain
(
'
Upstream
'
);
});
it
(
'
should render a downstream pipelines column
'
,
function
()
{
it
(
'
should render a downstream pipelines column
'
,
function
()
{
expect
(
component
.
$el
.
querySelector
(
'
.linked-pipelines-column
'
)).
not
.
toBeNull
();
expect
(
component
.
$el
.
innerHTML
).
toContain
(
'
Downstream
'
);
});
});
});
describe
(
'
when linked pipelines are not present
'
,
function
()
{
beforeEach
(
function
()
{
describe
(
'
when linked pipelines are not present
'
,
function
()
{
beforeEach
(
function
()
{
const
pipeline
=
Object
.
assign
(
graphJSON
,
{
triggered
:
null
,
triggered_by
:
null
});
component
=
mountComponent
(
GraphComponent
,
{
isLoading
:
false
,
...
...
@@ -112,24 +118,24 @@ describe('graph component', () => {
});
});
describe
(
'
rendered output
'
,
function
()
{
it
(
'
should include the first column with a no margin
'
,
function
()
{
describe
(
'
rendered output
'
,
function
()
{
it
(
'
should include the first column with a no margin
'
,
function
()
{
const
firstColumn
=
component
.
$el
.
querySelector
(
'
.stage-column:first-child
'
);
expect
(
firstColumn
.
classList
.
contains
(
'
no-margin
'
)).
toEqual
(
true
);
});
it
(
'
should not render a linked pipelines column
'
,
function
()
{
it
(
'
should not render a linked pipelines column
'
,
function
()
{
expect
(
component
.
$el
.
querySelector
(
'
.linked-pipelines-column
'
)).
toBeNull
();
});
});
describe
(
'
stageConnectorClass
'
,
function
()
{
it
(
'
it returns left-margin when no triggerer and there is one job
'
,
function
()
{
describe
(
'
stageConnectorClass
'
,
function
()
{
it
(
'
it returns left-margin when no triggerer and there is one job
'
,
function
()
{
expect
(
component
.
stageConnectorClass
(
0
,
{
groups
:
[
'
job
'
]
})).
toBe
(
'
no-margin
'
);
});
it
(
'
it returns left-margin when no triggerer and not the first stage
'
,
function
()
{
it
(
'
it returns left-margin when no triggerer and not the first stage
'
,
function
()
{
expect
(
component
.
stageConnectorClass
(
99
,
{
groups
:
[
'
job
'
]
})).
toBe
(
'
left-margin
'
);
});
});
...
...
@@ -142,7 +148,9 @@ describe('graph component', () => {
pipeline
:
graphJSON
,
});
expect
(
component
.
$el
.
querySelector
(
'
.stage-column:nth-child(2) .stage-name
'
).
textContent
.
trim
()).
toEqual
(
'
Deploy <img src=x onerror=alert(document.domain)>
'
);
expect
(
component
.
$el
.
querySelector
(
'
.stage-column:nth-child(2) .stage-name
'
).
textContent
.
trim
(),
).
toEqual
(
'
Deploy <img src=x onerror=alert(document.domain)>
'
);
});
});
});
spec/javascripts/sidebar/assignees_spec.js
View file @
7e9f44fa
...
...
@@ -78,9 +78,7 @@ describe('Assignee component', () => {
component
=
new
AssigneeComponent
({
propsData
:
{
rootPath
:
'
http://localhost:3000
'
,
users
:
[
UsersMock
.
user
,
],
users
:
[
UsersMock
.
user
],
editable
:
false
,
},
}).
$mount
();
...
...
@@ -90,7 +88,9 @@ describe('Assignee component', () => {
expect
(
collapsed
.
childElementCount
).
toEqual
(
1
);
expect
(
assignee
.
querySelector
(
'
.avatar
'
).
getAttribute
(
'
src
'
)).
toEqual
(
UsersMock
.
user
.
avatar
);
expect
(
assignee
.
querySelector
(
'
.avatar
'
).
getAttribute
(
'
alt
'
)).
toEqual
(
`
${
UsersMock
.
user
.
name
}
's avatar`
);
expect
(
assignee
.
querySelector
(
'
.avatar
'
).
getAttribute
(
'
alt
'
)).
toEqual
(
`
${
UsersMock
.
user
.
name
}
's avatar`
,
);
expect
(
assignee
.
querySelector
(
'
.author
'
).
innerText
.
trim
()).
toEqual
(
UsersMock
.
user
.
name
);
});
...
...
@@ -98,34 +98,38 @@ describe('Assignee component', () => {
component
=
new
AssigneeComponent
({
propsData
:
{
rootPath
:
'
http://localhost:3000/
'
,
users
:
[
UsersMock
.
user
,
],
users
:
[
UsersMock
.
user
],
editable
:
true
,
},
}).
$mount
();
expect
(
component
.
$el
.
querySelector
(
'
.author-link
'
)).
not
.
toBeNull
();
// The image
expect
(
component
.
$el
.
querySelector
(
'
.author-link img
'
).
getAttribute
(
'
src
'
)).
toEqual
(
UsersMock
.
user
.
avatar
);
expect
(
component
.
$el
.
querySelector
(
'
.author-link img
'
).
getAttribute
(
'
src
'
)).
toEqual
(
UsersMock
.
user
.
avatar
,
);
// Author name
expect
(
component
.
$el
.
querySelector
(
'
.author-link .author
'
).
innerText
.
trim
()).
toEqual
(
UsersMock
.
user
.
name
);
expect
(
component
.
$el
.
querySelector
(
'
.author-link .author
'
).
innerText
.
trim
()).
toEqual
(
UsersMock
.
user
.
name
,
);
// Username
expect
(
component
.
$el
.
querySelector
(
'
.author-link .username
'
).
innerText
.
trim
()).
toEqual
(
`@
${
UsersMock
.
user
.
username
}
`
);
expect
(
component
.
$el
.
querySelector
(
'
.author-link .username
'
).
innerText
.
trim
()).
toEqual
(
`@
${
UsersMock
.
user
.
username
}
`
,
);
});
it
(
'
has the root url present in the assigneeUrl method
'
,
()
=>
{
component
=
new
AssigneeComponent
({
propsData
:
{
rootPath
:
'
http://localhost:3000/
'
,
users
:
[
UsersMock
.
user
,
],
users
:
[
UsersMock
.
user
],
editable
:
true
,
},
}).
$mount
();
expect
(
component
.
assigneeUrl
(
UsersMock
.
user
).
indexOf
(
'
http://localhost:3000/
'
)).
not
.
toEqual
(
-
1
);
expect
(
component
.
assigneeUrl
(
UsersMock
.
user
).
indexOf
(
'
http://localhost:3000/
'
)).
not
.
toEqual
(
-
1
,
);
});
});
...
...
@@ -147,13 +151,17 @@ describe('Assignee component', () => {
const
first
=
collapsed
.
children
[
0
];
expect
(
first
.
querySelector
(
'
.avatar
'
).
getAttribute
(
'
src
'
)).
toEqual
(
users
[
0
].
avatar
);
expect
(
first
.
querySelector
(
'
.avatar
'
).
getAttribute
(
'
alt
'
)).
toEqual
(
`
${
users
[
0
].
name
}
's avatar`
);
expect
(
first
.
querySelector
(
'
.avatar
'
).
getAttribute
(
'
alt
'
)).
toEqual
(
`
${
users
[
0
].
name
}
's avatar`
,
);
expect
(
first
.
querySelector
(
'
.author
'
).
innerText
.
trim
()).
toEqual
(
users
[
0
].
name
);
const
second
=
collapsed
.
children
[
1
];
expect
(
second
.
querySelector
(
'
.avatar
'
).
getAttribute
(
'
src
'
)).
toEqual
(
users
[
1
].
avatar
);
expect
(
second
.
querySelector
(
'
.avatar
'
).
getAttribute
(
'
alt
'
)).
toEqual
(
`
${
users
[
1
].
name
}
's avatar`
);
expect
(
second
.
querySelector
(
'
.avatar
'
).
getAttribute
(
'
alt
'
)).
toEqual
(
`
${
users
[
1
].
name
}
's avatar`
,
);
expect
(
second
.
querySelector
(
'
.author
'
).
innerText
.
trim
()).
toEqual
(
users
[
1
].
name
);
});
...
...
@@ -174,7 +182,9 @@ describe('Assignee component', () => {
const
first
=
collapsed
.
children
[
0
];
expect
(
first
.
querySelector
(
'
.avatar
'
).
getAttribute
(
'
src
'
)).
toEqual
(
users
[
0
].
avatar
);
expect
(
first
.
querySelector
(
'
.avatar
'
).
getAttribute
(
'
alt
'
)).
toEqual
(
`
${
users
[
0
].
name
}
's avatar`
);
expect
(
first
.
querySelector
(
'
.avatar
'
).
getAttribute
(
'
alt
'
)).
toEqual
(
`
${
users
[
0
].
name
}
's avatar`
,
);
expect
(
first
.
querySelector
(
'
.author
'
).
innerText
.
trim
()).
toEqual
(
users
[
0
].
name
);
const
second
=
collapsed
.
children
[
1
];
...
...
@@ -206,12 +216,10 @@ describe('Assignee component', () => {
},
}).
$mount
();
expect
(
component
.
$el
.
querySelector
(
'
.user-link
'
).
getAttribute
(
'
data-container
'
),
).
toBe
(
'
body
'
);
expect
(
component
.
$el
.
querySelector
(
'
.user-link
'
).
getAttribute
(
'
data-container
'
)).
toBe
(
'
body
'
);
});
it
(
'
Shows the "show-less" assignees label
'
,
(
done
)
=>
{
it
(
'
Shows the "show-less" assignees label
'
,
done
=>
{
const
users
=
UsersMockHelper
.
createNumberRandomUsers
(
6
);
component
=
new
AssigneeComponent
({
propsData
:
{
...
...
@@ -221,21 +229,25 @@ describe('Assignee component', () => {
},
}).
$mount
();
expect
(
component
.
$el
.
querySelectorAll
(
'
.user-item
'
).
length
).
toEqual
(
component
.
defaultRenderCount
);
expect
(
component
.
$el
.
querySelectorAll
(
'
.user-item
'
).
length
).
toEqual
(
component
.
defaultRenderCount
,
);
expect
(
component
.
$el
.
querySelector
(
'
.user-list-more
'
)).
not
.
toBe
(
null
);
const
usersLabelExpectation
=
users
.
length
-
component
.
defaultRenderCount
;
expect
(
component
.
$el
.
querySelector
(
'
.user-list-more .btn-link
'
).
innerText
.
trim
())
.
not
.
toBe
(
`+
${
usersLabelExpectation
}
more`
);
expect
(
component
.
$el
.
querySelector
(
'
.user-list-more .btn-link
'
).
innerText
.
trim
()).
not
.
toBe
(
`+
${
usersLabelExpectation
}
more`
,
);
component
.
toggleShowLess
();
Vue
.
nextTick
(()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.user-list-more .btn-link
'
).
innerText
.
trim
())
.
toBe
(
'
- show less
'
);
expect
(
component
.
$el
.
querySelector
(
'
.user-list-more .btn-link
'
).
innerText
.
trim
()).
toBe
(
'
- show less
'
,
);
done
();
});
});
it
(
'
Shows the "show-less" when "n+ more " label is clicked
'
,
(
done
)
=>
{
it
(
'
Shows the "show-less" when "n+ more " label is clicked
'
,
done
=>
{
const
users
=
UsersMockHelper
.
createNumberRandomUsers
(
6
);
component
=
new
AssigneeComponent
({
propsData
:
{
...
...
@@ -247,8 +259,9 @@ describe('Assignee component', () => {
component
.
$el
.
querySelector
(
'
.user-list-more .btn-link
'
).
click
();
Vue
.
nextTick
(()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.user-list-more .btn-link
'
).
innerText
.
trim
())
.
toBe
(
'
- show less
'
);
expect
(
component
.
$el
.
querySelector
(
'
.user-list-more .btn-link
'
).
innerText
.
trim
()).
toBe
(
'
- show less
'
,
);
done
();
});
});
...
...
@@ -279,16 +292,18 @@ describe('Assignee component', () => {
});
it
(
'
shows "+1 more" label
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.user-list-more .btn-link
'
).
innerText
.
trim
())
.
toBe
(
'
+ 1 more
'
);
expect
(
component
.
$el
.
querySelector
(
'
.user-list-more .btn-link
'
).
innerText
.
trim
()).
toBe
(
'
+ 1 more
'
,
);
});
it
(
'
shows "show less" label
'
,
(
done
)
=>
{
it
(
'
shows "show less" label
'
,
done
=>
{
component
.
toggleShowLess
();
Vue
.
nextTick
(()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.user-list-more .btn-link
'
).
innerText
.
trim
())
.
toBe
(
'
- show less
'
);
expect
(
component
.
$el
.
querySelector
(
'
.user-list-more .btn-link
'
).
innerText
.
trim
()).
toBe
(
'
- show less
'
,
);
done
();
});
});
...
...
spec/javascripts/test_bundle.js
View file @
7e9f44fa
...
...
@@ -200,8 +200,10 @@ if (process.env.BABEL_ENV === 'coverage') {
require
.
context
(
'
~
'
,
true
,
/
\.(
js|vue
)
$/
),
require
.
context
(
'
ee
'
,
true
,
/
\.(
js|vue
)
$/
),
];
const
allTestFiles
=
testContexts
.
reduce
((
accumulator
,
context
)
=>
accumulator
.
concat
(
context
.
keys
()),
[]);
const
allTestFiles
=
testContexts
.
reduce
(
(
accumulator
,
context
)
=>
accumulator
.
concat
(
context
.
keys
()),
[],
);
$
.
holdReady
(
true
);
...
...
spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
View file @
7e9f44fa
...
...
@@ -70,12 +70,12 @@ describe('MRWidgetPipeline', () => {
pipeline
:
mockData
.
pipeline
,
hasCi
:
true
,
ciStatus
:
null
,
troubleshootingDocsPath
:
'
help
'
,
troubleshootingDocsPath
:
'
help
'
,
});
expect
(
vm
.
$el
.
querySelector
(
'
.media-body
'
).
textContent
.
trim
()
,
)
.
toContain
(
'
Could not retrieve the pipeline status. For troubleshooting steps, read the <a href="help">documentation.</a>
'
)
;
expect
(
vm
.
$el
.
querySelector
(
'
.media-body
'
).
textContent
.
trim
()).
toContain
(
'
Could not retrieve the pipeline status. For troubleshooting steps, read the <a href="help">documentation.</a>
'
,
);
});
describe
(
'
with a pipeline
'
,
()
=>
{
...
...
@@ -89,34 +89,36 @@ describe('MRWidgetPipeline', () => {
});
it
(
'
should render pipeline ID
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.pipeline-id
'
).
textContent
.
trim
()
,
)
.
toEqual
(
`#
${
mockData
.
pipeline
.
id
}
`
)
;
expect
(
vm
.
$el
.
querySelector
(
'
.pipeline-id
'
).
textContent
.
trim
()).
toEqual
(
`#
${
mockData
.
pipeline
.
id
}
`
,
);
});
it
(
'
should render pipeline status and commit id
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.media-body
'
).
textContent
.
trim
()
,
)
.
toContain
(
mockData
.
pipeline
.
details
.
status
.
label
)
;
expect
(
vm
.
$el
.
querySelector
(
'
.media-body
'
).
textContent
.
trim
()).
toContain
(
mockData
.
pipeline
.
details
.
status
.
label
,
);
expect
(
vm
.
$el
.
querySelector
(
'
.js-commit-link
'
).
textContent
.
trim
()
,
)
.
toEqual
(
mockData
.
pipeline
.
commit
.
short_id
)
;
expect
(
vm
.
$el
.
querySelector
(
'
.js-commit-link
'
).
textContent
.
trim
()).
toEqual
(
mockData
.
pipeline
.
commit
.
short_id
,
);
expect
(
vm
.
$el
.
querySelector
(
'
.js-commit-link
'
).
getAttribute
(
'
href
'
)
,
)
.
toEqual
(
mockData
.
pipeline
.
commit
.
commit_path
)
;
expect
(
vm
.
$el
.
querySelector
(
'
.js-commit-link
'
).
getAttribute
(
'
href
'
)).
toEqual
(
mockData
.
pipeline
.
commit
.
commit_path
,
);
});
it
(
'
should render pipeline graph
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.mr-widget-pipeline-graph
'
)).
toBeDefined
();
expect
(
vm
.
$el
.
querySelectorAll
(
'
.stage-container
'
).
length
).
toEqual
(
mockData
.
pipeline
.
details
.
stages
.
length
);
expect
(
vm
.
$el
.
querySelectorAll
(
'
.stage-container
'
).
length
).
toEqual
(
mockData
.
pipeline
.
details
.
stages
.
length
,
);
});
it
(
'
should render coverage information
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.media-body
'
).
textContent
,
)
.
toContain
(
`Coverage
${
mockData
.
pipeline
.
coverage
}
`
)
;
expect
(
vm
.
$el
.
querySelector
(
'
.media-body
'
).
textContent
).
toContain
(
`Coverage
${
mockData
.
pipeline
.
coverage
}
`
,
);
});
});
...
...
@@ -134,30 +136,30 @@ describe('MRWidgetPipeline', () => {
});
it
(
'
should render pipeline ID
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.pipeline-id
'
).
textContent
.
trim
()
,
)
.
toEqual
(
`#
${
mockData
.
pipeline
.
id
}
`
)
;
expect
(
vm
.
$el
.
querySelector
(
'
.pipeline-id
'
).
textContent
.
trim
()).
toEqual
(
`#
${
mockData
.
pipeline
.
id
}
`
,
);
});
it
(
'
should render pipeline status
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.media-body
'
).
textContent
.
trim
()
,
)
.
toContain
(
mockData
.
pipeline
.
details
.
status
.
label
)
;
expect
(
vm
.
$el
.
querySelector
(
'
.media-body
'
).
textContent
.
trim
()).
toContain
(
mockData
.
pipeline
.
details
.
status
.
label
,
);
expect
(
vm
.
$el
.
querySelector
(
'
.js-commit-link
'
),
).
toBeNull
();
expect
(
vm
.
$el
.
querySelector
(
'
.js-commit-link
'
)).
toBeNull
();
});
it
(
'
should render pipeline graph
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.mr-widget-pipeline-graph
'
)).
toBeDefined
();
expect
(
vm
.
$el
.
querySelectorAll
(
'
.stage-container
'
).
length
).
toEqual
(
mockData
.
pipeline
.
details
.
stages
.
length
);
expect
(
vm
.
$el
.
querySelectorAll
(
'
.stage-container
'
).
length
).
toEqual
(
mockData
.
pipeline
.
details
.
stages
.
length
,
);
});
it
(
'
should render coverage information
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.media-body
'
).
textContent
,
)
.
toContain
(
`Coverage
${
mockData
.
pipeline
.
coverage
}
`
)
;
expect
(
vm
.
$el
.
querySelector
(
'
.media-body
'
).
textContent
).
toContain
(
`Coverage
${
mockData
.
pipeline
.
coverage
}
`
,
);
});
});
...
...
@@ -173,9 +175,7 @@ describe('MRWidgetPipeline', () => {
troubleshootingDocsPath
:
'
help
'
,
});
expect
(
vm
.
$el
.
querySelector
(
'
.media-body
'
).
textContent
,
).
not
.
toContain
(
'
Coverage
'
);
expect
(
vm
.
$el
.
querySelector
(
'
.media-body
'
).
textContent
).
not
.
toContain
(
'
Coverage
'
);
});
});
...
...
spec/javascripts/vue_mr_widget/mock_data.js
View file @
7e9f44fa
...
...
@@ -230,7 +230,7 @@ export default {
'
/help/user/project/merge_requests/index#interacting-with-security-reports-ultimate
'
,
merge_commit_path
:
'
http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775
'
,
troubleshooting_docs_path
:
'
help
'
troubleshooting_docs_path
:
'
help
'
,
};
// Codeclimate
export
const
headIssues
=
[
...
...
spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
View file @
7e9f44fa
...
...
@@ -454,7 +454,7 @@ describe('mrWidgetOptions', () => {
deployed_at
:
'
2017-03-22T22:44:42.258Z
'
,
deployed_at_formatted
:
'
Mar 22, 2017 10:44pm
'
,
changes
,
status
:
'
success
'
status
:
'
success
'
,
};
beforeEach
(
done
=>
{
...
...
@@ -607,33 +607,36 @@ describe('mrWidgetOptions', () => {
describe
(
'
with post merge deployments
'
,
()
=>
{
beforeEach
(
done
=>
{
vm
.
mr
.
postMergeDeployments
=
[{
id
:
15
,
name
:
'
review/diplo
'
,
url
:
'
/root/acets-review-apps/environments/15
'
,
stop_url
:
'
/root/acets-review-apps/environments/15/stop
'
,
metrics_url
:
'
/root/acets-review-apps/environments/15/deployments/1/metrics
'
,
metrics_monitoring_url
:
'
/root/acets-review-apps/environments/15/metrics
'
,
external_url
:
'
http://diplo.
'
,
external_url_formatted
:
'
diplo.
'
,
deployed_at
:
'
2017-03-22T22:44:42.258Z
'
,
deployed_at_formatted
:
'
Mar 22, 2017 10:44pm
'
,
changes
:
[
{
path
:
'
index.html
'
,
external_url
:
'
http://root-master-patch-91341.volatile-watch.surge.sh/index.html
'
,
},
{
path
:
'
imgs/gallery.html
'
,
external_url
:
'
http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html
'
,
},
{
path
:
'
about/
'
,
external_url
:
'
http://root-master-patch-91341.volatile-watch.surge.sh/about/
'
,
},
],
status
:
'
success
'
}];
vm
.
mr
.
postMergeDeployments
=
[
{
id
:
15
,
name
:
'
review/diplo
'
,
url
:
'
/root/acets-review-apps/environments/15
'
,
stop_url
:
'
/root/acets-review-apps/environments/15/stop
'
,
metrics_url
:
'
/root/acets-review-apps/environments/15/deployments/1/metrics
'
,
metrics_monitoring_url
:
'
/root/acets-review-apps/environments/15/metrics
'
,
external_url
:
'
http://diplo.
'
,
external_url_formatted
:
'
diplo.
'
,
deployed_at
:
'
2017-03-22T22:44:42.258Z
'
,
deployed_at_formatted
:
'
Mar 22, 2017 10:44pm
'
,
changes
:
[
{
path
:
'
index.html
'
,
external_url
:
'
http://root-master-patch-91341.volatile-watch.surge.sh/index.html
'
,
},
{
path
:
'
imgs/gallery.html
'
,
external_url
:
'
http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html
'
,
},
{
path
:
'
about/
'
,
external_url
:
'
http://root-master-patch-91341.volatile-watch.surge.sh/about/
'
,
},
],
status
:
'
success
'
,
},
];
vm
.
$nextTick
(
done
);
});
...
...
spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
View file @
7e9f44fa
...
...
@@ -12,7 +12,7 @@ describe('collapsedGroupedDatePicker', () => {
});
describe
(
'
toggleCollapse events
'
,
()
=>
{
beforeEach
(
(
done
)
=>
{
beforeEach
(
done
=>
{
spyOn
(
vm
,
'
toggleSidebar
'
);
vm
.
minDate
=
new
Date
(
'
07/17/2016
'
);
Vue
.
nextTick
(
done
);
...
...
@@ -26,7 +26,7 @@ describe('collapsedGroupedDatePicker', () => {
});
describe
(
'
minDate and maxDate
'
,
()
=>
{
beforeEach
(
(
done
)
=>
{
beforeEach
(
done
=>
{
vm
.
minDate
=
new
Date
(
'
07/17/2016
'
);
vm
.
maxDate
=
new
Date
(
'
07/17/2017
'
);
Vue
.
nextTick
(
done
);
...
...
@@ -42,7 +42,7 @@ describe('collapsedGroupedDatePicker', () => {
});
describe
(
'
minDate
'
,
()
=>
{
beforeEach
(
(
done
)
=>
{
beforeEach
(
done
=>
{
vm
.
minDate
=
new
Date
(
'
07/17/2016
'
);
Vue
.
nextTick
(
done
);
});
...
...
@@ -56,7 +56,7 @@ describe('collapsedGroupedDatePicker', () => {
});
describe
(
'
maxDate
'
,
()
=>
{
beforeEach
(
(
done
)
=>
{
beforeEach
(
done
=>
{
vm
.
maxDate
=
new
Date
(
'
07/17/2017
'
);
Vue
.
nextTick
(
done
);
});
...
...
spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js
View file @
7e9f44fa
...
...
@@ -32,7 +32,7 @@ describe('sidebarDatePicker', () => {
expect
(
vm
.
$el
.
querySelector
(
'
.value-content span
'
).
innerText
.
trim
()).
toEqual
(
'
None
'
);
});
it
(
'
should render date-picker when editing
'
,
(
done
)
=>
{
it
(
'
should render date-picker when editing
'
,
done
=>
{
vm
.
editing
=
true
;
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.pika-label
'
)).
toBeDefined
();
...
...
@@ -41,7 +41,7 @@ describe('sidebarDatePicker', () => {
});
describe
(
'
editable
'
,
()
=>
{
beforeEach
(
(
done
)
=>
{
beforeEach
(
done
=>
{
vm
.
editable
=
true
;
Vue
.
nextTick
(
done
);
});
...
...
@@ -50,7 +50,7 @@ describe('sidebarDatePicker', () => {
expect
(
vm
.
$el
.
querySelector
(
'
.title .btn-blank
'
).
innerText
.
trim
()).
toEqual
(
'
Edit
'
);
});
it
(
'
should enable editing when edit button is clicked
'
,
(
done
)
=>
{
it
(
'
should enable editing when edit button is clicked
'
,
done
=>
{
vm
.
isLoading
=
false
;
Vue
.
nextTick
(()
=>
{
vm
.
$el
.
querySelector
(
'
.title .btn-blank
'
).
click
();
...
...
@@ -61,7 +61,7 @@ describe('sidebarDatePicker', () => {
});
});
it
(
'
should render date if selectedDate
'
,
(
done
)
=>
{
it
(
'
should render date if selectedDate
'
,
done
=>
{
vm
.
selectedDate
=
new
Date
(
'
07/07/2017
'
);
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.value-content strong
'
).
innerText
.
trim
()).
toEqual
(
'
Jul 7, 2017
'
);
...
...
@@ -70,7 +70,7 @@ describe('sidebarDatePicker', () => {
});
describe
(
'
selectedDate and editable
'
,
()
=>
{
beforeEach
(
(
done
)
=>
{
beforeEach
(
done
=>
{
vm
.
selectedDate
=
new
Date
(
'
07/07/2017
'
);
vm
.
editable
=
true
;
Vue
.
nextTick
(
done
);
...
...
@@ -91,7 +91,7 @@ describe('sidebarDatePicker', () => {
});
describe
(
'
showToggleSidebar
'
,
()
=>
{
beforeEach
(
(
done
)
=>
{
beforeEach
(
done
=>
{
vm
.
showToggleSidebar
=
true
;
Vue
.
nextTick
(
done
);
});
...
...
spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
View file @
7e9f44fa
...
...
@@ -49,7 +49,9 @@ describe('DropdownValueCollapsedComponent', () => {
const
vmMoreLabels
=
createComponent
(
mockMoreLabels
);
expect
(
vmMoreLabels
.
labelsList
).
toBe
(
'
Foo Label, Foo Label, Foo Label, Foo Label, Foo Label, and 2 more
'
);
expect
(
vmMoreLabels
.
labelsList
).
toBe
(
'
Foo Label, Foo Label, Foo Label, Foo Label, Foo Label, and 2 more
'
,
);
vmMoreLabels
.
$destroy
();
});
...
...
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