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
eb2f29be
Commit
eb2f29be
authored
Oct 05, 2020
by
Olena Horal-Koretska
Committed by
Peter Leitzen
Oct 05, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add incidents comments timeline view
parent
28ab1364
Changes
29
Hide whitespace changes
Inline
Side-by-side
Showing
29 changed files
with
370 additions
and
34 deletions
+370
-34
app/assets/javascripts/notes/components/discussion_filter.vue
...assets/javascripts/notes/components/discussion_filter.vue
+12
-2
app/assets/javascripts/notes/components/notes_app.vue
app/assets/javascripts/notes/components/notes_app.vue
+3
-2
app/assets/javascripts/notes/components/sort_discussion.vue
app/assets/javascripts/notes/components/sort_discussion.vue
+4
-3
app/assets/javascripts/notes/components/timeline_toggle.vue
app/assets/javascripts/notes/components/timeline_toggle.vue
+55
-0
app/assets/javascripts/notes/constants.js
app/assets/javascripts/notes/constants.js
+2
-1
app/assets/javascripts/notes/index.js
app/assets/javascripts/notes/index.js
+2
-0
app/assets/javascripts/notes/stores/actions.js
app/assets/javascripts/notes/stores/actions.js
+6
-2
app/assets/javascripts/notes/stores/getters.js
app/assets/javascripts/notes/stores/getters.js
+21
-0
app/assets/javascripts/notes/stores/modules/index.js
app/assets/javascripts/notes/stores/modules/index.js
+2
-0
app/assets/javascripts/notes/stores/mutation_types.js
app/assets/javascripts/notes/stores/mutation_types.js
+1
-0
app/assets/javascripts/notes/stores/mutations.js
app/assets/javascripts/notes/stores/mutations.js
+7
-2
app/assets/javascripts/notes/timeline.js
app/assets/javascripts/notes/timeline.js
+16
-0
app/assets/javascripts/vue_shared/components/local_storage_sync.vue
.../javascripts/vue_shared/components/local_storage_sync.vue
+7
-0
app/views/projects/issues/show.html.haml
app/views/projects/issues/show.html.haml
+1
-0
changelogs/unreleased/227836-incidents-comments-timeline-view.yml
...gs/unreleased/227836-incidents-comments-timeline-view.yml
+5
-0
ee/app/helpers/ee/issues_helper.rb
ee/app/helpers/ee/issues_helper.rb
+4
-0
ee/app/models/license.rb
ee/app/models/license.rb
+1
-0
ee/app/views/projects/issues/_timeline_toggle.html.haml
ee/app/views/projects/issues/_timeline_toggle.html.haml
+2
-0
ee/spec/helpers/ee/issues_helper_spec.rb
ee/spec/helpers/ee/issues_helper_spec.rb
+20
-0
ee/spec/views/projects/issues/show.html.haml_spec.rb
ee/spec/views/projects/issues/show.html.haml_spec.rb
+12
-0
locale/gitlab.pot
locale/gitlab.pot
+6
-0
spec/frontend/notes/components/discussion_filter_spec.js
spec/frontend/notes/components/discussion_filter_spec.js
+14
-16
spec/frontend/notes/components/notes_app_spec.js
spec/frontend/notes/components/notes_app_spec.js
+17
-0
spec/frontend/notes/components/sort_discussion_spec.js
spec/frontend/notes/components/sort_discussion_spec.js
+7
-3
spec/frontend/notes/components/timeline_toggle_spec.js
spec/frontend/notes/components/timeline_toggle_spec.js
+93
-0
spec/frontend/notes/stores/actions_spec.js
spec/frontend/notes/stores/actions_spec.js
+7
-2
spec/frontend/notes/stores/getters_spec.js
spec/frontend/notes/stores/getters_spec.js
+13
-0
spec/frontend/notes/stores/mutation_spec.js
spec/frontend/notes/stores/mutation_spec.js
+2
-1
spec/frontend/vue_shared/components/local_storage_sync_spec.js
...frontend/vue_shared/components/local_storage_sync_spec.js
+28
-0
No files found.
app/assets/javascripts/notes/components/discussion_filter.vue
View file @
eb2f29be
...
@@ -5,6 +5,7 @@ import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility
...
@@ -5,6 +5,7 @@ import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility
import
{
import
{
DISCUSSION_FILTERS_DEFAULT_VALUE
,
DISCUSSION_FILTERS_DEFAULT_VALUE
,
HISTORY_ONLY_FILTER_VALUE
,
HISTORY_ONLY_FILTER_VALUE
,
COMMENTS_ONLY_FILTER_VALUE
,
DISCUSSION_TAB_LABEL
,
DISCUSSION_TAB_LABEL
,
DISCUSSION_FILTER_TYPES
,
DISCUSSION_FILTER_TYPES
,
NOTE_UNDERSCORE
,
NOTE_UNDERSCORE
,
...
@@ -38,7 +39,7 @@ export default {
...
@@ -38,7 +39,7 @@ export default {
};
};
},
},
computed
:
{
computed
:
{
...
mapGetters
([
'
getNotesDataByProp
'
]),
...
mapGetters
([
'
getNotesDataByProp
'
,
'
timelineEnabled
'
]),
currentFilter
()
{
currentFilter
()
{
if
(
!
this
.
currentValue
)
return
this
.
filters
[
0
];
if
(
!
this
.
currentValue
)
return
this
.
filters
[
0
];
return
this
.
filters
.
find
(
filter
=>
filter
.
value
===
this
.
currentValue
);
return
this
.
filters
.
find
(
filter
=>
filter
.
value
===
this
.
currentValue
);
...
@@ -63,11 +64,20 @@ export default {
...
@@ -63,11 +64,20 @@ export default {
window
.
removeEventListener
(
'
hashchange
'
,
this
.
handleLocationHash
);
window
.
removeEventListener
(
'
hashchange
'
,
this
.
handleLocationHash
);
},
},
methods
:
{
methods
:
{
...
mapActions
([
'
filterDiscussion
'
,
'
setCommentsDisabled
'
,
'
setTargetNoteHash
'
]),
...
mapActions
([
'
filterDiscussion
'
,
'
setCommentsDisabled
'
,
'
setTargetNoteHash
'
,
'
setTimelineView
'
,
]),
selectFilter
(
value
,
persistFilter
=
true
)
{
selectFilter
(
value
,
persistFilter
=
true
)
{
const
filter
=
parseInt
(
value
,
10
);
const
filter
=
parseInt
(
value
,
10
);
if
(
filter
===
this
.
currentValue
)
return
;
if
(
filter
===
this
.
currentValue
)
return
;
if
(
this
.
timelineEnabled
&&
filter
!==
COMMENTS_ONLY_FILTER_VALUE
)
{
this
.
setTimelineView
(
false
);
}
this
.
currentValue
=
filter
;
this
.
currentValue
=
filter
;
this
.
filterDiscussion
({
this
.
filterDiscussion
({
path
:
this
.
getNotesDataByProp
(
'
discussionsPath
'
),
path
:
this
.
getNotesDataByProp
(
'
discussionsPath
'
),
...
...
app/assets/javascripts/notes/components/notes_app.vue
View file @
eb2f29be
...
@@ -73,6 +73,7 @@ export default {
...
@@ -73,6 +73,7 @@ export default {
'
userCanReply
'
,
'
userCanReply
'
,
'
discussionTabCounter
'
,
'
discussionTabCounter
'
,
'
sortDirection
'
,
'
sortDirection
'
,
'
timelineEnabled
'
,
]),
]),
sortDirDesc
()
{
sortDirDesc
()
{
return
this
.
sortDirection
===
constants
.
DESC
;
return
this
.
sortDirection
===
constants
.
DESC
;
...
@@ -95,7 +96,7 @@ export default {
...
@@ -95,7 +96,7 @@ export default {
return
this
.
discussions
;
return
this
.
discussions
;
},
},
canReply
()
{
canReply
()
{
return
this
.
userCanReply
&&
!
this
.
commentsDisabled
;
return
this
.
userCanReply
&&
!
this
.
commentsDisabled
&&
!
this
.
timelineEnabled
;
},
},
slotKeys
()
{
slotKeys
()
{
return
this
.
sortDirDesc
?
[
'
form
'
,
'
comments
'
]
:
[
'
comments
'
,
'
form
'
];
return
this
.
sortDirDesc
?
[
'
form
'
,
'
comments
'
]
:
[
'
comments
'
,
'
form
'
];
...
@@ -252,7 +253,7 @@ export default {
...
@@ -252,7 +253,7 @@ export default {
<ordered-layout
:slot-keys=
"slotKeys"
>
<ordered-layout
:slot-keys=
"slotKeys"
>
<template
#form
>
<template
#form
>
<comment-form
<comment-form
v-if=
"!
commentsDisabled
"
v-if=
"!
(commentsDisabled || timelineEnabled)
"
class=
"js-comment-form"
class=
"js-comment-form"
:noteable-type=
"noteableType"
:noteable-type=
"noteableType"
/>
/>
...
...
app/assets/javascripts/notes/components/sort_discussion.vue
View file @
eb2f29be
...
@@ -20,7 +20,7 @@ export default {
...
@@ -20,7 +20,7 @@ export default {
},
},
mixins
:
[
Tracking
.
mixin
()],
mixins
:
[
Tracking
.
mixin
()],
computed
:
{
computed
:
{
...
mapGetters
([
'
sortDirection
'
,
'
noteableType
'
]),
...
mapGetters
([
'
sortDirection
'
,
'
persistSortOrder
'
,
'
noteableType
'
]),
selectedOption
()
{
selectedOption
()
{
return
SORT_OPTIONS
.
find
(({
key
})
=>
this
.
sortDirection
===
key
);
return
SORT_OPTIONS
.
find
(({
key
})
=>
this
.
sortDirection
===
key
);
},
},
...
@@ -38,7 +38,7 @@ export default {
...
@@ -38,7 +38,7 @@ export default {
return
;
return
;
}
}
this
.
setDiscussionSortDirection
(
direction
);
this
.
setDiscussionSortDirection
(
{
direction
}
);
this
.
track
(
'
change_discussion_sort_direction
'
,
{
property
:
direction
});
this
.
track
(
'
change_discussion_sort_direction
'
,
{
property
:
direction
});
},
},
isDropdownItemActive
(
sortDir
)
{
isDropdownItemActive
(
sortDir
)
{
...
@@ -53,7 +53,8 @@ export default {
...
@@ -53,7 +53,8 @@ export default {
<local-storage-sync
<local-storage-sync
:value=
"sortDirection"
:value=
"sortDirection"
:storage-key=
"storageKey"
:storage-key=
"storageKey"
@
input=
"setDiscussionSortDirection"
:persist=
"persistSortOrder"
@
input=
"setDiscussionSortDirection(
{ direction: $event })"
/>
/>
<gl-dropdown
<gl-dropdown
:text=
"dropdownText"
:text=
"dropdownText"
...
...
app/assets/javascripts/notes/components/timeline_toggle.vue
0 → 100644
View file @
eb2f29be
<
script
>
import
{
GlButton
,
GlTooltipDirective
}
from
'
@gitlab/ui
'
;
import
{
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
{
s__
}
from
'
~/locale
'
;
import
{
COMMENTS_ONLY_FILTER_VALUE
,
DESC
}
from
'
../constants
'
;
import
notesEventHub
from
'
../event_hub
'
;
export
const
timelineEnabledTooltip
=
s__
(
'
Timeline|Turn timeline view off
'
);
export
const
timelineDisabledTooltip
=
s__
(
'
Timeline|Turn timeline view on
'
);
export
default
{
components
:
{
GlButton
,
},
directives
:
{
GlTooltip
:
GlTooltipDirective
,
},
computed
:
{
...
mapGetters
([
'
timelineEnabled
'
,
'
sortDirection
'
]),
tooltip
()
{
return
this
.
timelineEnabled
?
timelineEnabledTooltip
:
timelineDisabledTooltip
;
},
},
methods
:
{
...
mapActions
([
'
setTimelineView
'
,
'
setDiscussionSortDirection
'
]),
setSort
()
{
if
(
this
.
timelineEnabled
&&
this
.
sortDirection
!==
DESC
)
{
this
.
setDiscussionSortDirection
({
direction
:
DESC
,
persist
:
false
});
}
},
setFilter
()
{
notesEventHub
.
$emit
(
'
dropdownSelect
'
,
COMMENTS_ONLY_FILTER_VALUE
,
false
);
},
toggleTimeline
(
event
)
{
event
.
currentTarget
.
blur
();
this
.
setTimelineView
(
!
this
.
timelineEnabled
);
this
.
setSort
();
this
.
setFilter
();
},
},
};
</
script
>
<
template
>
<gl-button
v-gl-tooltip
icon=
"comments"
size=
"small"
:selected=
"timelineEnabled"
:title=
"tooltip"
:aria-label=
"tooltip"
class=
"gl-mr-3"
@
click=
"toggleTimeline"
/>
</
template
>
app/assets/javascripts/notes/constants.js
View file @
eb2f29be
...
@@ -14,8 +14,9 @@ export const MERGE_REQUEST_NOTEABLE_TYPE = 'MergeRequest';
...
@@ -14,8 +14,9 @@ export const MERGE_REQUEST_NOTEABLE_TYPE = 'MergeRequest';
export
const
UNRESOLVE_NOTE_METHOD_NAME
=
'
delete
'
;
export
const
UNRESOLVE_NOTE_METHOD_NAME
=
'
delete
'
;
export
const
RESOLVE_NOTE_METHOD_NAME
=
'
post
'
;
export
const
RESOLVE_NOTE_METHOD_NAME
=
'
post
'
;
export
const
DESCRIPTION_TYPE
=
'
changed the description
'
;
export
const
DESCRIPTION_TYPE
=
'
changed the description
'
;
export
const
HISTORY_ONLY_FILTER_VALUE
=
2
;
export
const
DISCUSSION_FILTERS_DEFAULT_VALUE
=
0
;
export
const
DISCUSSION_FILTERS_DEFAULT_VALUE
=
0
;
export
const
COMMENTS_ONLY_FILTER_VALUE
=
1
;
export
const
HISTORY_ONLY_FILTER_VALUE
=
2
;
export
const
DISCUSSION_TAB_LABEL
=
'
show
'
;
export
const
DISCUSSION_TAB_LABEL
=
'
show
'
;
export
const
NOTE_UNDERSCORE
=
'
note_
'
;
export
const
NOTE_UNDERSCORE
=
'
note_
'
;
export
const
TIME_DIFFERENCE_VALUE
=
10
;
export
const
TIME_DIFFERENCE_VALUE
=
10
;
...
...
app/assets/javascripts/notes/index.js
View file @
eb2f29be
...
@@ -2,6 +2,7 @@ import Vue from 'vue';
...
@@ -2,6 +2,7 @@ import Vue from 'vue';
import
notesApp
from
'
./components/notes_app.vue
'
;
import
notesApp
from
'
./components/notes_app.vue
'
;
import
initDiscussionFilters
from
'
./discussion_filters
'
;
import
initDiscussionFilters
from
'
./discussion_filters
'
;
import
initSortDiscussions
from
'
./sort_discussions
'
;
import
initSortDiscussions
from
'
./sort_discussions
'
;
import
initTimelineToggle
from
'
./timeline
'
;
import
{
store
}
from
'
./stores
'
;
import
{
store
}
from
'
./stores
'
;
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
{
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
{
...
@@ -59,4 +60,5 @@ document.addEventListener('DOMContentLoaded', () => {
...
@@ -59,4 +60,5 @@ document.addEventListener('DOMContentLoaded', () => {
initDiscussionFilters
(
store
);
initDiscussionFilters
(
store
);
initSortDiscussions
(
store
);
initSortDiscussions
(
store
);
initTimelineToggle
(
store
);
});
});
app/assets/javascripts/notes/stores/actions.js
View file @
eb2f29be
...
@@ -99,8 +99,12 @@ export const updateDiscussion = ({ commit, state }, discussion) => {
...
@@ -99,8 +99,12 @@ export const updateDiscussion = ({ commit, state }, discussion) => {
return
utils
.
findNoteObjectById
(
state
.
discussions
,
discussion
.
id
);
return
utils
.
findNoteObjectById
(
state
.
discussions
,
discussion
.
id
);
};
};
export
const
setDiscussionSortDirection
=
({
commit
},
direction
)
=>
{
export
const
setDiscussionSortDirection
=
({
commit
},
{
direction
,
persist
=
true
})
=>
{
commit
(
types
.
SET_DISCUSSIONS_SORT
,
direction
);
commit
(
types
.
SET_DISCUSSIONS_SORT
,
{
direction
,
persist
});
};
export
const
setTimelineView
=
({
commit
},
enabled
)
=>
{
commit
(
types
.
SET_TIMELINE_VIEW
,
enabled
);
};
};
export
const
setSelectedCommentPosition
=
({
commit
},
position
)
=>
{
export
const
setSelectedCommentPosition
=
({
commit
},
position
)
=>
{
...
...
app/assets/javascripts/notes/stores/getters.js
View file @
eb2f29be
...
@@ -5,6 +5,23 @@ import { collapseSystemNotes } from './collapse_utils';
...
@@ -5,6 +5,23 @@ import { collapseSystemNotes } from './collapse_utils';
export
const
discussions
=
state
=>
{
export
const
discussions
=
state
=>
{
let
discussionsInState
=
clone
(
state
.
discussions
);
let
discussionsInState
=
clone
(
state
.
discussions
);
// NOTE: not testing bc will be removed when backend is finished.
// NOTE: not testing bc will be removed when backend is finished.
if
(
state
.
isTimelineEnabled
)
{
discussionsInState
=
discussionsInState
.
reduce
((
acc
,
discussion
)
=>
{
const
transformedToIndividualNotes
=
discussion
.
notes
.
map
(
note
=>
({
...
discussion
,
id
:
note
.
id
,
created_at
:
note
.
created_at
,
individual_note
:
true
,
notes
:
[
note
],
}));
return
acc
.
concat
(
transformedToIndividualNotes
);
},
[])
.
sort
((
a
,
b
)
=>
new
Date
(
a
.
created_at
)
-
new
Date
(
b
.
created_at
));
}
if
(
state
.
discussionSortOrder
===
constants
.
DESC
)
{
if
(
state
.
discussionSortOrder
===
constants
.
DESC
)
{
discussionsInState
=
discussionsInState
.
reverse
();
discussionsInState
=
discussionsInState
.
reverse
();
}
}
...
@@ -27,6 +44,10 @@ export const isNotesFetched = state => state.isNotesFetched;
...
@@ -27,6 +44,10 @@ export const isNotesFetched = state => state.isNotesFetched;
export
const
sortDirection
=
state
=>
state
.
discussionSortOrder
;
export
const
sortDirection
=
state
=>
state
.
discussionSortOrder
;
export
const
persistSortOrder
=
state
=>
state
.
persistSortOrder
;
export
const
timelineEnabled
=
state
=>
state
.
isTimelineEnabled
;
export
const
isLoading
=
state
=>
state
.
isLoading
;
export
const
isLoading
=
state
=>
state
.
isLoading
;
export
const
getNotesDataByProp
=
state
=>
prop
=>
state
.
notesData
[
prop
];
export
const
getNotesDataByProp
=
state
=>
prop
=>
state
.
notesData
[
prop
];
...
...
app/assets/javascripts/notes/stores/modules/index.js
View file @
eb2f29be
...
@@ -7,6 +7,7 @@ export default () => ({
...
@@ -7,6 +7,7 @@ export default () => ({
state
:
{
state
:
{
discussions
:
[],
discussions
:
[],
discussionSortOrder
:
ASC
,
discussionSortOrder
:
ASC
,
persistSortOrder
:
true
,
convertedDisscussionIds
:
[],
convertedDisscussionIds
:
[],
targetNoteHash
:
null
,
targetNoteHash
:
null
,
lastFetchedAt
:
null
,
lastFetchedAt
:
null
,
...
@@ -45,6 +46,7 @@ export default () => ({
...
@@ -45,6 +46,7 @@ export default () => ({
resolvableDiscussionsCount
:
0
,
resolvableDiscussionsCount
:
0
,
unresolvedDiscussionsCount
:
0
,
unresolvedDiscussionsCount
:
0
,
descriptionVersions
:
{},
descriptionVersions
:
{},
isTimelineEnabled
:
false
,
},
},
actions
,
actions
,
getters
,
getters
,
...
...
app/assets/javascripts/notes/stores/mutation_types.js
View file @
eb2f29be
...
@@ -34,6 +34,7 @@ export const SET_EXPAND_DISCUSSIONS = 'SET_EXPAND_DISCUSSIONS';
...
@@ -34,6 +34,7 @@ export const SET_EXPAND_DISCUSSIONS = 'SET_EXPAND_DISCUSSIONS';
export
const
UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS
=
'
UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS
'
;
export
const
UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS
=
'
UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS
'
;
export
const
SET_CURRENT_DISCUSSION_ID
=
'
SET_CURRENT_DISCUSSION_ID
'
;
export
const
SET_CURRENT_DISCUSSION_ID
=
'
SET_CURRENT_DISCUSSION_ID
'
;
export
const
SET_DISCUSSIONS_SORT
=
'
SET_DISCUSSIONS_SORT
'
;
export
const
SET_DISCUSSIONS_SORT
=
'
SET_DISCUSSIONS_SORT
'
;
export
const
SET_TIMELINE_VIEW
=
'
SET_TIMELINE_VIEW
'
;
export
const
SET_SELECTED_COMMENT_POSITION
=
'
SET_SELECTED_COMMENT_POSITION
'
;
export
const
SET_SELECTED_COMMENT_POSITION
=
'
SET_SELECTED_COMMENT_POSITION
'
;
export
const
SET_SELECTED_COMMENT_POSITION_HOVER
=
'
SET_SELECTED_COMMENT_POSITION_HOVER
'
;
export
const
SET_SELECTED_COMMENT_POSITION_HOVER
=
'
SET_SELECTED_COMMENT_POSITION_HOVER
'
;
export
const
SET_FETCHING_DISCUSSIONS
=
'
SET_FETCHING_DISCUSSIONS
'
;
export
const
SET_FETCHING_DISCUSSIONS
=
'
SET_FETCHING_DISCUSSIONS
'
;
...
...
app/assets/javascripts/notes/stores/mutations.js
View file @
eb2f29be
...
@@ -313,8 +313,13 @@ export default {
...
@@ -313,8 +313,13 @@ export default {
discussion
.
truncated_diff_lines
=
utils
.
prepareDiffLines
(
diffLines
);
discussion
.
truncated_diff_lines
=
utils
.
prepareDiffLines
(
diffLines
);
},
},
[
types
.
SET_DISCUSSIONS_SORT
](
state
,
sort
)
{
[
types
.
SET_DISCUSSIONS_SORT
](
state
,
{
direction
,
persist
})
{
state
.
discussionSortOrder
=
sort
;
state
.
discussionSortOrder
=
direction
;
state
.
persistSortOrder
=
persist
;
},
[
types
.
SET_TIMELINE_VIEW
](
state
,
value
)
{
state
.
isTimelineEnabled
=
value
;
},
},
[
types
.
SET_SELECTED_COMMENT_POSITION
](
state
,
position
)
{
[
types
.
SET_SELECTED_COMMENT_POSITION
](
state
,
position
)
{
...
...
app/assets/javascripts/notes/timeline.js
0 → 100644
View file @
eb2f29be
import
Vue
from
'
vue
'
;
import
TimelineToggle
from
'
./components/timeline_toggle.vue
'
;
export
default
function
initTimelineToggle
(
store
)
{
const
el
=
document
.
getElementById
(
'
js-incidents-timeline-toggle
'
);
if
(
!
el
)
return
null
;
return
new
Vue
({
el
,
store
,
render
(
createElement
)
{
return
createElement
(
TimelineToggle
);
},
});
}
app/assets/javascripts/vue_shared/components/local_storage_sync.vue
View file @
eb2f29be
...
@@ -17,6 +17,11 @@ export default {
...
@@ -17,6 +17,11 @@ export default {
required
:
false
,
required
:
false
,
default
:
false
,
default
:
false
,
},
},
persist
:
{
type
:
Boolean
,
required
:
false
,
default
:
true
,
},
},
},
watch
:
{
watch
:
{
value
(
newVal
)
{
value
(
newVal
)
{
...
@@ -52,6 +57,8 @@ export default {
...
@@ -52,6 +57,8 @@ export default {
}
}
},
},
saveValue
(
val
)
{
saveValue
(
val
)
{
if
(
!
this
.
persist
)
return
;
localStorage
.
setItem
(
this
.
storageKey
,
val
);
localStorage
.
setItem
(
this
.
storageKey
,
val
);
},
},
serialize
(
val
)
{
serialize
(
val
)
{
...
...
app/views/projects/issues/show.html.haml
View file @
eb2f29be
...
@@ -91,6 +91,7 @@
...
@@ -91,6 +91,7 @@
.js-noteable-awards
.js-noteable-awards
=
render
'award_emoji/awards_block'
,
awardable:
@issue
,
inline:
true
=
render
'award_emoji/awards_block'
,
awardable:
@issue
,
inline:
true
.new-branch-col
.new-branch-col
=
render_if_exists
"projects/issues/timeline_toggle"
,
issue:
@issue
#js-vue-sort-issue-discussions
#js-vue-sort-issue-discussions
#js-vue-discussion-filter
{
data:
{
default_filter:
current_user
&
.
notes_filter_for
(
@issue
),
notes_filters:
UserPreference
.
notes_filters
.
to_json
}
}
#js-vue-discussion-filter
{
data:
{
default_filter:
current_user
&
.
notes_filter_for
(
@issue
),
notes_filters:
UserPreference
.
notes_filters
.
to_json
}
}
=
render
'new_branch'
if
show_new_branch_button?
=
render
'new_branch'
if
show_new_branch_button?
...
...
changelogs/unreleased/227836-incidents-comments-timeline-view.yml
0 → 100644
View file @
eb2f29be
---
title
:
Add timeline toggle button for incidents comments
merge_request
:
43302
author
:
type
:
added
ee/app/helpers/ee/issues_helper.rb
View file @
eb2f29be
...
@@ -50,5 +50,9 @@ module EE
...
@@ -50,5 +50,9 @@ module EE
# than the filter epic id on params
# than the filter epic id on params
epic_id
.
to_i
!=
issue
.
epic_issue
.
epic_id
epic_id
.
to_i
!=
issue
.
epic_issue
.
epic_id
end
end
def
show_timeline_view_toggle?
(
issue
)
issue
.
incident?
&&
issue
.
project
.
feature_available?
(
:incident_timeline_view
)
end
end
end
end
end
ee/app/models/license.rb
View file @
eb2f29be
...
@@ -116,6 +116,7 @@ class License < ApplicationRecord
...
@@ -116,6 +116,7 @@ class License < ApplicationRecord
minimal_access_role
minimal_access_role
unprotection_restrictions
unprotection_restrictions
ci_project_subscriptions
ci_project_subscriptions
incident_timeline_view
]
]
EEP_FEATURES
.
freeze
EEP_FEATURES
.
freeze
...
...
ee/app/views/projects/issues/_timeline_toggle.html.haml
0 → 100644
View file @
eb2f29be
-
if
show_timeline_view_toggle?
(
issue
)
#js-incidents-timeline-toggle
ee/spec/helpers/ee/issues_helper_spec.rb
View file @
eb2f29be
...
@@ -69,4 +69,24 @@ RSpec.describe EE::IssuesHelper do
...
@@ -69,4 +69,24 @@ RSpec.describe EE::IssuesHelper do
expect
(
helper
.
issue_in_subepic?
(
issue
,
'subepic_id'
)).
to
be_truthy
expect
(
helper
.
issue_in_subepic?
(
issue
,
'subepic_id'
)).
to
be_truthy
end
end
end
end
describe
'#show_timeline_view_toggle?'
do
subject
{
helper
.
show_timeline_view_toggle?
(
issue
)
}
it
{
is_expected
.
to
be_falsy
}
context
'issue is an incident'
do
let
(
:issue
)
{
build_stubbed
(
:incident
)
}
it
{
is_expected
.
to
be_falsy
}
context
'with license'
do
before
do
stub_licensed_features
(
incident_timeline_view:
true
)
end
it
{
is_expected
.
to
be_truthy
}
end
end
end
end
end
ee/spec/views/projects/issues/show.html.haml_spec.rb
View file @
eb2f29be
...
@@ -20,4 +20,16 @@ RSpec.describe 'projects/issues/show' do
...
@@ -20,4 +20,16 @@ RSpec.describe 'projects/issues/show' do
expect
(
rendered
).
to
have_selector
(
'[aria-label="GitLab Team Member"]'
)
expect
(
rendered
).
to
have_selector
(
'[aria-label="GitLab Team Member"]'
)
end
end
end
end
context
'for applicable incidents'
do
before
do
allow
(
view
).
to
receive
(
:show_timeline_view_toggle?
).
and_return
(
true
)
end
it
'renders a timeline toggle'
do
render
expect
(
rendered
).
to
have_selector
(
'#js-incidents-timeline-toggle'
)
end
end
end
end
locale/gitlab.pot
View file @
eb2f29be
...
@@ -26854,6 +26854,12 @@ msgstr ""
...
@@ -26854,6 +26854,12 @@ msgstr ""
msgid "Timeago|right now"
msgid "Timeago|right now"
msgstr ""
msgstr ""
msgid "Timeline|Turn timeline view off"
msgstr ""
msgid "Timeline|Turn timeline view on"
msgstr ""
msgid "Timeout"
msgid "Timeout"
msgstr ""
msgstr ""
...
...
spec/frontend/notes/components/discussion_filter_spec.js
View file @
eb2f29be
...
@@ -25,6 +25,8 @@ describe('DiscussionFilter component', () => {
...
@@ -25,6 +25,8 @@ describe('DiscussionFilter component', () => {
const
filterDiscussion
=
jest
.
fn
();
const
filterDiscussion
=
jest
.
fn
();
const
findFilter
=
filterType
=>
wrapper
.
find
(
`.dropdown-item[data-filter-type="
${
filterType
}
"]`
);
const
mountComponent
=
()
=>
{
const
mountComponent
=
()
=>
{
const
discussions
=
[
const
discussions
=
[
{
{
...
@@ -89,9 +91,7 @@ describe('DiscussionFilter component', () => {
...
@@ -89,9 +91,7 @@ describe('DiscussionFilter component', () => {
});
});
it
(
'
updates to the selected item
'
,
()
=>
{
it
(
'
updates to the selected item
'
,
()
=>
{
const
filterItem
=
wrapper
.
find
(
const
filterItem
=
findFilter
(
DISCUSSION_FILTER_TYPES
.
ALL
);
`.discussion-filter-container .dropdown-item[data-filter-type="
${
DISCUSSION_FILTER_TYPES
.
HISTORY
}
"]`
,
);
filterItem
.
trigger
(
'
click
'
);
filterItem
.
trigger
(
'
click
'
);
...
@@ -99,29 +99,27 @@ describe('DiscussionFilter component', () => {
...
@@ -99,29 +99,27 @@ describe('DiscussionFilter component', () => {
});
});
it
(
'
only updates when selected filter changes
'
,
()
=>
{
it
(
'
only updates when selected filter changes
'
,
()
=>
{
wrapper
findFilter
(
DISCUSSION_FILTER_TYPES
.
ALL
).
trigger
(
'
click
'
);
.
find
(
`.discussion-filter-container .dropdown-item[data-filter-type="
${
DISCUSSION_FILTER_TYPES
.
ALL
}
"]`
,
)
.
trigger
(
'
click
'
);
expect
(
filterDiscussion
).
not
.
toHaveBeenCalled
();
expect
(
filterDiscussion
).
not
.
toHaveBeenCalled
();
});
});
it
(
'
disables timeline view if it was enabled
'
,
()
=>
{
store
.
state
.
isTimelineEnabled
=
true
;
findFilter
(
DISCUSSION_FILTER_TYPES
.
HISTORY
).
trigger
(
'
click
'
);
expect
(
wrapper
.
vm
.
$store
.
state
.
isTimelineEnabled
).
toBe
(
false
);
});
it
(
'
disables commenting when "Show history only" filter is applied
'
,
()
=>
{
it
(
'
disables commenting when "Show history only" filter is applied
'
,
()
=>
{
const
filterItem
=
wrapper
.
find
(
findFilter
(
DISCUSSION_FILTER_TYPES
.
HISTORY
).
trigger
(
'
click
'
);
`.discussion-filter-container .dropdown-item[data-filter-type="
${
DISCUSSION_FILTER_TYPES
.
HISTORY
}
"]`
,
);
filterItem
.
trigger
(
'
click
'
);
expect
(
wrapper
.
vm
.
$store
.
state
.
commentsDisabled
).
toBe
(
true
);
expect
(
wrapper
.
vm
.
$store
.
state
.
commentsDisabled
).
toBe
(
true
);
});
});
it
(
'
enables commenting when "Show history only" filter is not applied
'
,
()
=>
{
it
(
'
enables commenting when "Show history only" filter is not applied
'
,
()
=>
{
const
filterItem
=
wrapper
.
find
(
findFilter
(
DISCUSSION_FILTER_TYPES
.
ALL
).
trigger
(
'
click
'
);
`.discussion-filter-container .dropdown-item[data-filter-type="
${
DISCUSSION_FILTER_TYPES
.
ALL
}
"]`
,
);
filterItem
.
trigger
(
'
click
'
);
expect
(
wrapper
.
vm
.
$store
.
state
.
commentsDisabled
).
toBe
(
false
);
expect
(
wrapper
.
vm
.
$store
.
state
.
commentsDisabled
).
toBe
(
false
);
});
});
...
...
spec/frontend/notes/components/notes_app_spec.js
View file @
eb2f29be
...
@@ -174,6 +174,23 @@ describe('note_app', () => {
...
@@ -174,6 +174,23 @@ describe('note_app', () => {
});
});
});
});
describe
(
'
timeline view
'
,
()
=>
{
beforeEach
(()
=>
{
setFixtures
(
'
<div class="js-discussions-count"></div>
'
);
axiosMock
.
onAny
().
reply
(
mockData
.
getIndividualNoteResponse
);
store
.
state
.
commentsDisabled
=
false
;
store
.
state
.
isTimelineEnabled
=
true
;
wrapper
=
mountComponent
();
return
waitForDiscussionsRequest
();
});
it
(
'
should not render comments form
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
.js-main-target-form
'
).
exists
()).
toBe
(
false
);
});
});
describe
(
'
while fetching data
'
,
()
=>
{
describe
(
'
while fetching data
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
setFixtures
(
'
<div class="js-discussions-count"></div>
'
);
setFixtures
(
'
<div class="js-discussions-count"></div>
'
);
...
...
spec/frontend/notes/components/sort_discussion_spec.js
View file @
eb2f29be
...
@@ -46,7 +46,7 @@ describe('Sort Discussion component', () => {
...
@@ -46,7 +46,7 @@ describe('Sort Discussion component', () => {
it
(
'
calls setDiscussionSortDirection when update is emitted
'
,
()
=>
{
it
(
'
calls setDiscussionSortDirection when update is emitted
'
,
()
=>
{
findLocalStorageSync
().
vm
.
$emit
(
'
input
'
,
ASC
);
findLocalStorageSync
().
vm
.
$emit
(
'
input
'
,
ASC
);
expect
(
store
.
dispatch
).
toHaveBeenCalledWith
(
'
setDiscussionSortDirection
'
,
ASC
);
expect
(
store
.
dispatch
).
toHaveBeenCalledWith
(
'
setDiscussionSortDirection
'
,
{
direction
:
ASC
}
);
});
});
});
});
...
@@ -57,7 +57,9 @@ describe('Sort Discussion component', () => {
...
@@ -57,7 +57,9 @@ describe('Sort Discussion component', () => {
wrapper
.
find
(
'
.js-newest-first
'
).
vm
.
$emit
(
'
click
'
);
wrapper
.
find
(
'
.js-newest-first
'
).
vm
.
$emit
(
'
click
'
);
expect
(
store
.
dispatch
).
toHaveBeenCalledWith
(
'
setDiscussionSortDirection
'
,
DESC
);
expect
(
store
.
dispatch
).
toHaveBeenCalledWith
(
'
setDiscussionSortDirection
'
,
{
direction
:
DESC
,
});
expect
(
Tracking
.
event
).
toHaveBeenCalledWith
(
undefined
,
'
change_discussion_sort_direction
'
,
{
expect
(
Tracking
.
event
).
toHaveBeenCalledWith
(
undefined
,
'
change_discussion_sort_direction
'
,
{
property
:
DESC
,
property
:
DESC
,
});
});
...
@@ -81,7 +83,9 @@ describe('Sort Discussion component', () => {
...
@@ -81,7 +83,9 @@ describe('Sort Discussion component', () => {
it
(
'
calls the right actions
'
,
()
=>
{
it
(
'
calls the right actions
'
,
()
=>
{
wrapper
.
find
(
'
.js-oldest-first
'
).
vm
.
$emit
(
'
click
'
);
wrapper
.
find
(
'
.js-oldest-first
'
).
vm
.
$emit
(
'
click
'
);
expect
(
store
.
dispatch
).
toHaveBeenCalledWith
(
'
setDiscussionSortDirection
'
,
ASC
);
expect
(
store
.
dispatch
).
toHaveBeenCalledWith
(
'
setDiscussionSortDirection
'
,
{
direction
:
ASC
,
});
expect
(
Tracking
.
event
).
toHaveBeenCalledWith
(
undefined
,
'
change_discussion_sort_direction
'
,
{
expect
(
Tracking
.
event
).
toHaveBeenCalledWith
(
undefined
,
'
change_discussion_sort_direction
'
,
{
property
:
ASC
,
property
:
ASC
,
});
});
...
...
spec/frontend/notes/components/timeline_toggle_spec.js
0 → 100644
View file @
eb2f29be
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
{
GlButton
}
from
'
@gitlab/ui
'
;
import
Vuex
from
'
vuex
'
;
import
TimelineToggle
,
{
timelineEnabledTooltip
,
timelineDisabledTooltip
,
}
from
'
~/notes/components/timeline_toggle.vue
'
;
import
createStore
from
'
~/notes/stores
'
;
import
{
ASC
,
DESC
}
from
'
~/notes/constants
'
;
const
localVue
=
createLocalVue
();
localVue
.
use
(
Vuex
);
describe
(
'
Timeline toggle
'
,
()
=>
{
let
wrapper
;
let
store
;
const
mockEvent
=
{
currentTarget
:
{
blur
:
jest
.
fn
()
}
};
const
createComponent
=
()
=>
{
jest
.
spyOn
(
store
,
'
dispatch
'
).
mockImplementation
();
wrapper
=
shallowMount
(
TimelineToggle
,
{
localVue
,
store
,
});
};
const
findGlButton
=
()
=>
wrapper
.
find
(
GlButton
);
beforeEach
(()
=>
{
store
=
createStore
();
createComponent
();
});
afterEach
(()
=>
{
if
(
wrapper
)
{
wrapper
.
destroy
();
wrapper
=
null
;
}
store
.
dispatch
.
mockReset
();
mockEvent
.
currentTarget
.
blur
.
mockReset
();
});
describe
(
'
ON state
'
,
()
=>
{
it
(
'
should update timeline flag in the store
'
,
()
=>
{
store
.
state
.
isTimelineEnabled
=
false
;
findGlButton
().
vm
.
$emit
(
'
click
'
,
mockEvent
);
expect
(
store
.
dispatch
).
toHaveBeenCalledWith
(
'
setTimelineView
'
,
true
);
});
it
(
'
should set sort direction to DESC if not set
'
,
()
=>
{
store
.
state
.
isTimelineEnabled
=
true
;
store
.
state
.
sortDirection
=
ASC
;
findGlButton
().
vm
.
$emit
(
'
click
'
,
mockEvent
);
expect
(
store
.
dispatch
).
toHaveBeenCalledWith
(
'
setDiscussionSortDirection
'
,
{
direction
:
DESC
,
persist
:
false
,
});
});
it
(
'
should set correct UI state
'
,
async
()
=>
{
store
.
state
.
isTimelineEnabled
=
true
;
findGlButton
().
vm
.
$emit
(
'
click
'
,
mockEvent
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
findGlButton
().
attributes
(
'
title
'
)).
toBe
(
timelineEnabledTooltip
);
expect
(
findGlButton
().
attributes
(
'
selected
'
)).
toBe
(
'
true
'
);
expect
(
mockEvent
.
currentTarget
.
blur
).
toHaveBeenCalled
();
});
});
describe
(
'
OFF state
'
,
()
=>
{
it
(
'
should update timeline flag in the store
'
,
()
=>
{
store
.
state
.
isTimelineEnabled
=
true
;
findGlButton
().
vm
.
$emit
(
'
click
'
,
mockEvent
);
expect
(
store
.
dispatch
).
toHaveBeenCalledWith
(
'
setTimelineView
'
,
false
);
});
it
(
'
should NOT update sort direction
'
,
()
=>
{
store
.
state
.
isTimelineEnabled
=
false
;
findGlButton
().
vm
.
$emit
(
'
click
'
,
mockEvent
);
expect
(
store
.
dispatch
).
not
.
toHaveBeenCalledWith
();
});
it
(
'
should set correct UI state
'
,
async
()
=>
{
store
.
state
.
isTimelineEnabled
=
false
;
findGlButton
().
vm
.
$emit
(
'
click
'
,
mockEvent
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
findGlButton
().
attributes
(
'
title
'
)).
toBe
(
timelineDisabledTooltip
);
expect
(
findGlButton
().
attributes
(
'
selected
'
)).
toBe
(
undefined
);
expect
(
mockEvent
.
currentTarget
.
blur
).
toHaveBeenCalled
();
});
});
});
spec/frontend/notes/stores/actions_spec.js
View file @
eb2f29be
...
@@ -1144,9 +1144,14 @@ describe('Actions Notes Store', () => {
...
@@ -1144,9 +1144,14 @@ describe('Actions Notes Store', () => {
it
(
'
calls the correct mutation with the correct args
'
,
done
=>
{
it
(
'
calls the correct mutation with the correct args
'
,
done
=>
{
testAction
(
testAction
(
actions
.
setDiscussionSortDirection
,
actions
.
setDiscussionSortDirection
,
notesConstants
.
DESC
,
{
direction
:
notesConstants
.
DESC
,
persist
:
false
}
,
{},
{},
[{
type
:
mutationTypes
.
SET_DISCUSSIONS_SORT
,
payload
:
notesConstants
.
DESC
}],
[
{
type
:
mutationTypes
.
SET_DISCUSSIONS_SORT
,
payload
:
{
direction
:
notesConstants
.
DESC
,
persist
:
false
},
},
],
[],
[],
done
,
done
,
);
);
...
...
spec/frontend/notes/stores/getters_spec.js
View file @
eb2f29be
...
@@ -6,6 +6,7 @@ import {
...
@@ -6,6 +6,7 @@ import {
noteableDataMock
,
noteableDataMock
,
individualNote
,
individualNote
,
collapseNotesMock
,
collapseNotesMock
,
discussionMock
,
discussion1
,
discussion1
,
discussion2
,
discussion2
,
discussion3
,
discussion3
,
...
@@ -65,6 +66,18 @@ describe('Getters Notes Store', () => {
...
@@ -65,6 +66,18 @@ describe('Getters Notes Store', () => {
it
(
'
should return all discussions in the store
'
,
()
=>
{
it
(
'
should return all discussions in the store
'
,
()
=>
{
expect
(
getters
.
discussions
(
state
)).
toEqual
([
individualNote
]);
expect
(
getters
.
discussions
(
state
)).
toEqual
([
individualNote
]);
});
});
it
(
'
should transform discussion to individual notes in timeline view
'
,
()
=>
{
state
.
discussions
=
[
discussionMock
];
state
.
isTimelineEnabled
=
true
;
expect
(
getters
.
discussions
(
state
).
length
).
toEqual
(
discussionMock
.
notes
.
length
);
getters
.
discussions
(
state
).
forEach
(
discussion
=>
{
expect
(
discussion
.
individual_note
).
toBe
(
true
);
expect
(
discussion
.
id
).
toBe
(
discussion
.
notes
[
0
].
id
);
expect
(
discussion
.
created_at
).
toBe
(
discussion
.
notes
[
0
].
created_at
);
});
});
});
});
describe
(
'
resolvedDiscussionsById
'
,
()
=>
{
describe
(
'
resolvedDiscussionsById
'
,
()
=>
{
...
...
spec/frontend/notes/stores/mutation_spec.js
View file @
eb2f29be
...
@@ -680,9 +680,10 @@ describe('Notes Store mutations', () => {
...
@@ -680,9 +680,10 @@ describe('Notes Store mutations', () => {
});
});
it
(
'
sets sort order
'
,
()
=>
{
it
(
'
sets sort order
'
,
()
=>
{
mutations
.
SET_DISCUSSIONS_SORT
(
state
,
DESC
);
mutations
.
SET_DISCUSSIONS_SORT
(
state
,
{
direction
:
DESC
,
persist
:
false
}
);
expect
(
state
.
discussionSortOrder
).
toBe
(
DESC
);
expect
(
state
.
discussionSortOrder
).
toBe
(
DESC
);
expect
(
state
.
persistSortOrder
).
toBe
(
false
);
});
});
});
});
...
...
spec/frontend/vue_shared/components/local_storage_sync_spec.js
View file @
eb2f29be
...
@@ -126,6 +126,34 @@ describe('Local Storage Sync', () => {
...
@@ -126,6 +126,34 @@ describe('Local Storage Sync', () => {
expect
(
localStorage
.
getItem
(
storageKey
)).
toBe
(
newValue
);
expect
(
localStorage
.
getItem
(
storageKey
)).
toBe
(
newValue
);
});
});
});
});
it
(
'
persists the value by default
'
,
async
()
=>
{
const
persistedValue
=
'
persisted
'
;
createComponent
({
props
:
{
storageKey
,
},
});
wrapper
.
setProps
({
value
:
persistedValue
});
await
wrapper
.
vm
.
$nextTick
();
expect
(
localStorage
.
getItem
(
storageKey
)).
toBe
(
persistedValue
);
});
it
(
'
does not save a value if persist is set to false
'
,
async
()
=>
{
const
notPersistedValue
=
'
notPersisted
'
;
createComponent
({
props
:
{
storageKey
,
},
});
wrapper
.
setProps
({
persist
:
false
,
value
:
notPersistedValue
});
await
wrapper
.
vm
.
$nextTick
();
expect
(
localStorage
.
getItem
(
storageKey
)).
not
.
toBe
(
notPersistedValue
);
});
});
});
describe
(
'
with "asJson" prop set to "true"
'
,
()
=>
{
describe
(
'
with "asJson" prop set to "true"
'
,
()
=>
{
...
...
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