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
47f60167
Commit
47f60167
authored
Apr 18, 2017
by
Clement Ho
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
[skip ci] Introduce mediator
parent
7b935cc8
Changes
45
Hide whitespace changes
Inline
Side-by-side
Showing
45 changed files
with
640 additions
and
1618 deletions
+640
-1618
app/assets/javascripts/issuable/issuable_bundle.js
app/assets/javascripts/issuable/issuable_bundle.js
+0
-1
app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js
...ipts/issuable/time_tracking/components/collapsed_state.js
+0
-60
app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js
...ipts/issuable/time_tracking/components/comparison_pane.js
+0
-82
app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js
...s/issuable/time_tracking/components/estimate_only_pane.js
+0
-19
app/assets/javascripts/issuable/time_tracking/components/help_state.js
...vascripts/issuable/time_tracking/components/help_state.js
+0
-30
app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js
...pts/issuable/time_tracking/components/no_tracking_pane.js
+0
-12
app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js
...ipts/issuable/time_tracking/components/spent_only_pane.js
+0
-19
app/assets/javascripts/issuable/time_tracking/components/time_tracker.js
...scripts/issuable/time_tracking/components/time_tracker.js
+0
-135
app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js
...avascripts/issuable/time_tracking/time_tracking_bundle.js
+0
-66
app/assets/javascripts/main.js
app/assets/javascripts/main.js
+0
-1
app/assets/javascripts/sidebar/components/assignees/assignee_title.js
...avascripts/sidebar/components/assignees/assignee_title.js
+0
-0
app/assets/javascripts/sidebar/components/assignees/assignees.js
...ets/javascripts/sidebar/components/assignees/assignees.js
+4
-6
app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js
...scripts/sidebar/components/assignees/sidebar_assignees.js
+67
-0
app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js
...ripts/sidebar/components/time_tracking/collapsed_state.js
+57
-0
app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js
...ripts/sidebar/components/time_tracking/comparison_pane.js
+78
-0
app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.js
...ts/sidebar/components/time_tracking/estimate_only_pane.js
+15
-0
app/assets/javascripts/sidebar/components/time_tracking/help_state.js
...avascripts/sidebar/components/time_tracking/help_state.js
+31
-0
app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js
...ipts/sidebar/components/time_tracking/no_tracking_pane.js
+8
-0
app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js
...sidebar/components/time_tracking/sidebar_time_tracking.js
+45
-0
app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.js
...ripts/sidebar/components/time_tracking/spent_only_pane.js
+15
-0
app/assets/javascripts/sidebar/components/time_tracking/time_tracker.js
...ascripts/sidebar/components/time_tracking/time_tracker.js
+159
-0
app/assets/javascripts/sidebar/event_hub.js
app/assets/javascripts/sidebar/event_hub.js
+0
-0
app/assets/javascripts/sidebar/services/sidebar_service.js
app/assets/javascripts/sidebar/services/sidebar_service.js
+23
-0
app/assets/javascripts/sidebar/sidebar_bundle.js
app/assets/javascripts/sidebar/sidebar_bundle.js
+14
-0
app/assets/javascripts/sidebar/sidebar_mediator.js
app/assets/javascripts/sidebar/sidebar_mediator.js
+44
-0
app/assets/javascripts/sidebar/stores/sidebar_store.js
app/assets/javascripts/sidebar/stores/sidebar_store.js
+41
-0
app/assets/javascripts/sidebar_assignees/index.js
app/assets/javascripts/sidebar_assignees/index.js
+0
-4
app/assets/javascripts/subbable_resource.js
app/assets/javascripts/subbable_resource.js
+0
-51
app/assets/javascripts/users_select.js
app/assets/javascripts/users_select.js
+21
-19
app/serializers/issue_entity.rb
app/serializers/issue_entity.rb
+1
-1
app/views/shared/issuable/_sidebar.html.haml
app/views/shared/issuable/_sidebar.html.haml
+14
-22
config/webpack.config.js
config/webpack.config.js
+2
-3
spec/javascripts/helpers/vue_spec_helper.js
spec/javascripts/helpers/vue_spec_helper.js
+0
-12
spec/javascripts/helpers/vue_spec_helper_spec.js
spec/javascripts/helpers/vue_spec_helper_spec.js
+0
-37
spec/javascripts/issuable_time_tracker_spec.js
spec/javascripts/issuable_time_tracker_spec.js
+1
-1
spec/javascripts/subbable_resource_spec.js
spec/javascripts/subbable_resource_spec.js
+0
-63
spec/javascripts/vue_sidebar_assignees/assignee_title_spec.js
.../javascripts/vue_sidebar_assignees/assignee_title_spec.js
+0
-90
spec/javascripts/vue_sidebar_assignees/collapsed/assignees_spec.js
...scripts/vue_sidebar_assignees/collapsed/assignees_spec.js
+0
-255
spec/javascripts/vue_sidebar_assignees/collapsed/avatar_spec.js
...avascripts/vue_sidebar_assignees/collapsed/avatar_spec.js
+0
-43
spec/javascripts/vue_sidebar_assignees/expanded/multiple_assignees_spec.js
...vue_sidebar_assignees/expanded/multiple_assignees_spec.js
+0
-245
spec/javascripts/vue_sidebar_assignees/expanded/no_assignee_spec.js
...cripts/vue_sidebar_assignees/expanded/no_assignee_spec.js
+0
-42
spec/javascripts/vue_sidebar_assignees/expanded/single_assignee_spec.js
...ts/vue_sidebar_assignees/expanded/single_assignee_spec.js
+0
-96
spec/javascripts/vue_sidebar_assignees/mock_data.js
spec/javascripts/vue_sidebar_assignees/mock_data.js
+0
-26
spec/javascripts/vue_sidebar_assignees/services/sidebar_assignees_service_spec.js
...ebar_assignees/services/sidebar_assignees_service_spec.js
+0
-32
spec/javascripts/vue_sidebar_assignees/stores/sidebar_assignees_store_spec.js
..._sidebar_assignees/stores/sidebar_assignees_store_spec.js
+0
-145
No files found.
app/assets/javascripts/issuable/issuable_bundle.js
deleted
100644 → 0
View file @
7b935cc8
require
(
'
./time_tracking/time_tracking_bundle
'
);
app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js
deleted
100644 → 0
View file @
7b935cc8
import
Vue
from
'
vue
'
;
import
stopwatchSvg
from
'
icons/_icon_stopwatch.svg
'
;
require
(
'
../../../lib/utils/pretty_time
'
);
(()
=>
{
Vue
.
component
(
'
time-tracking-collapsed-state
'
,
{
name
:
'
time-tracking-collapsed-state
'
,
props
:
{
showComparisonState
:
{
type
:
Boolean
,
required
:
true
,
},
showSpentOnlyState
:
{
type
:
Boolean
,
required
:
true
,
},
showEstimateOnlyState
:
{
type
:
Boolean
,
required
:
true
,
},
showNoTimeTrackingState
:
{
type
:
Boolean
,
required
:
true
,
},
timeSpentHumanReadable
:
{
type
:
String
,
required
:
false
,
},
timeEstimateHumanReadable
:
{
type
:
String
,
required
:
false
,
},
},
methods
:
{
abbreviateTime
(
timeStr
)
{
return
gl
.
utils
.
prettyTime
.
abbreviateTime
(
timeStr
);
},
},
template
:
`
<div class='sidebar-collapsed-icon'>
${
stopwatchSvg
}
<div class='time-tracking-collapsed-summary'>
<div class='compare' v-if='showComparisonState'>
<span>{{ abbreviateTime(timeSpentHumanReadable) }} / {{ abbreviateTime(timeEstimateHumanReadable) }}</span>
</div>
<div class='estimate-only' v-if='showEstimateOnlyState'>
<span class='bold'>-- / {{ abbreviateTime(timeEstimateHumanReadable) }}</span>
</div>
<div class='spend-only' v-if='showSpentOnlyState'>
<span class='bold'>{{ abbreviateTime(timeSpentHumanReadable) }} / --</span>
</div>
<div class='no-tracking' v-if='showNoTimeTrackingState'>
<span class='no-value'>None</span>
</div>
</div>
</div>
`
,
});
})();
app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js
deleted
100644 → 0
View file @
7b935cc8
import
Vue
from
'
vue
'
;
require
(
'
../../../lib/utils/pretty_time
'
);
(()
=>
{
const
prettyTime
=
gl
.
utils
.
prettyTime
;
Vue
.
component
(
'
time-tracking-comparison-pane
'
,
{
name
:
'
time-tracking-comparison-pane
'
,
props
:
{
timeSpent
:
{
type
:
Number
,
required
:
true
,
},
timeEstimate
:
{
type
:
Number
,
required
:
true
,
},
timeSpentHumanReadable
:
{
type
:
String
,
required
:
true
,
},
timeEstimateHumanReadable
:
{
type
:
String
,
required
:
true
,
},
},
computed
:
{
parsedRemaining
()
{
const
diffSeconds
=
this
.
timeEstimate
-
this
.
timeSpent
;
return
prettyTime
.
parseSeconds
(
diffSeconds
);
},
timeRemainingHumanReadable
()
{
return
prettyTime
.
stringifyTime
(
this
.
parsedRemaining
);
},
timeRemainingTooltip
()
{
const
prefix
=
this
.
timeRemainingMinutes
<
0
?
'
Over by
'
:
'
Time remaining:
'
;
return
`
${
prefix
}
${
this
.
timeRemainingHumanReadable
}
`
;
},
/* Diff values for comparison meter */
timeRemainingMinutes
()
{
return
this
.
timeEstimate
-
this
.
timeSpent
;
},
timeRemainingPercent
()
{
return
`
${
Math
.
floor
((
this
.
timeSpent
/
this
.
timeEstimate
)
*
100
)}
%`
;
},
timeRemainingStatusClass
()
{
return
this
.
timeEstimate
>=
this
.
timeSpent
?
'
within_estimate
'
:
'
over_estimate
'
;
},
/* Parsed time values */
parsedEstimate
()
{
return
prettyTime
.
parseSeconds
(
this
.
timeEstimate
);
},
parsedSpent
()
{
return
prettyTime
.
parseSeconds
(
this
.
timeSpent
);
},
},
template
:
`
<div class='time-tracking-comparison-pane'>
<div class='compare-meter' data-toggle='tooltip' data-placement='top' role='timeRemainingDisplay'
:aria-valuenow='timeRemainingTooltip'
:title='timeRemainingTooltip'
:data-original-title='timeRemainingTooltip'
:class='timeRemainingStatusClass'>
<div class='meter-container' role='timeSpentPercent' :aria-valuenow='timeRemainingPercent'>
<div :style='{ width: timeRemainingPercent }' class='meter-fill'></div>
</div>
<div class='compare-display-container'>
<div class='compare-display pull-left'>
<span class='compare-label'>Spent</span>
<span class='compare-value spent'>{{ timeSpentHumanReadable }}</span>
</div>
<div class='compare-display estimated pull-right'>
<span class='compare-label'>Est</span>
<span class='compare-value'>{{ timeEstimateHumanReadable }}</span>
</div>
</div>
</div>
</div>
`
,
});
})();
app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js
deleted
100644 → 0
View file @
7b935cc8
import
Vue
from
'
vue
'
;
(()
=>
{
Vue
.
component
(
'
time-tracking-estimate-only-pane
'
,
{
name
:
'
time-tracking-estimate-only-pane
'
,
props
:
{
timeEstimateHumanReadable
:
{
type
:
String
,
required
:
true
,
},
},
template
:
`
<div class='time-tracking-estimate-only-pane'>
<span class='bold'>Estimated:</span>
{{ timeEstimateHumanReadable }}
</div>
`
,
});
})();
app/assets/javascripts/issuable/time_tracking/components/help_state.js
deleted
100644 → 0
View file @
7b935cc8
import
Vue
from
'
vue
'
;
(()
=>
{
Vue
.
component
(
'
time-tracking-help-state
'
,
{
name
:
'
time-tracking-help-state
'
,
props
:
{
docsUrl
:
{
type
:
String
,
required
:
true
,
},
},
template
:
`
<div class='time-tracking-help-state'>
<div class='time-tracking-info'>
<h4>Track time with slash commands</h4>
<p>Slash commands can be used in the issues description and comment boxes.</p>
<p>
<code>/estimate</code>
will update the estimated time with the latest command.
</p>
<p>
<code>/spend</code>
will update the sum of the time spent.
</p>
<a class='btn btn-default learn-more-button' :href='docsUrl'>Learn more</a>
</div>
</div>
`
,
});
})();
app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js
deleted
100644 → 0
View file @
7b935cc8
import
Vue
from
'
vue
'
;
(()
=>
{
Vue
.
component
(
'
time-tracking-no-tracking-pane
'
,
{
name
:
'
time-tracking-no-tracking-pane
'
,
template
:
`
<div class='time-tracking-no-tracking-pane'>
<span class='no-value'>No estimate or time spent</span>
</div>
`
,
});
})();
app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js
deleted
100644 → 0
View file @
7b935cc8
import
Vue
from
'
vue
'
;
(()
=>
{
Vue
.
component
(
'
time-tracking-spent-only-pane
'
,
{
name
:
'
time-tracking-spent-only-pane
'
,
props
:
{
timeSpentHumanReadable
:
{
type
:
String
,
required
:
true
,
},
},
template
:
`
<div class='time-tracking-spend-only-pane'>
<span class='bold'>Spent:</span>
{{ timeSpentHumanReadable }}
</div>
`
,
});
})();
app/assets/javascripts/issuable/time_tracking/components/time_tracker.js
deleted
100644 → 0
View file @
7b935cc8
import
Vue
from
'
vue
'
;
require
(
'
./help_state
'
);
require
(
'
./collapsed_state
'
);
require
(
'
./spent_only_pane
'
);
require
(
'
./no_tracking_pane
'
);
require
(
'
./estimate_only_pane
'
);
require
(
'
./comparison_pane
'
);
(()
=>
{
Vue
.
component
(
'
issuable-time-tracker
'
,
{
name
:
'
issuable-time-tracker
'
,
props
:
{
time_estimate
:
{
type
:
Number
,
required
:
true
,
default
:
0
,
},
time_spent
:
{
type
:
Number
,
required
:
true
,
default
:
0
,
},
human_time_estimate
:
{
type
:
String
,
required
:
false
,
},
human_time_spent
:
{
type
:
String
,
required
:
false
,
},
docsUrl
:
{
type
:
String
,
required
:
true
,
},
},
data
()
{
return
{
showHelp
:
false
,
};
},
computed
:
{
timeSpent
()
{
return
this
.
time_spent
;
},
timeEstimate
()
{
return
this
.
time_estimate
;
},
timeEstimateHumanReadable
()
{
return
this
.
human_time_estimate
;
},
timeSpentHumanReadable
()
{
return
this
.
human_time_spent
;
},
hasTimeSpent
()
{
return
!!
this
.
timeSpent
;
},
hasTimeEstimate
()
{
return
!!
this
.
timeEstimate
;
},
showComparisonState
()
{
return
this
.
hasTimeEstimate
&&
this
.
hasTimeSpent
;
},
showEstimateOnlyState
()
{
return
this
.
hasTimeEstimate
&&
!
this
.
hasTimeSpent
;
},
showSpentOnlyState
()
{
return
this
.
hasTimeSpent
&&
!
this
.
hasTimeEstimate
;
},
showNoTimeTrackingState
()
{
return
!
this
.
hasTimeEstimate
&&
!
this
.
hasTimeSpent
;
},
showHelpState
()
{
return
!!
this
.
showHelp
;
},
},
methods
:
{
toggleHelpState
(
show
)
{
this
.
showHelp
=
show
;
},
},
template
:
`
<div class='time_tracker time-tracking-component-wrap' v-cloak>
<time-tracking-collapsed-state
:show-comparison-state='showComparisonState'
:show-no-time-tracking-state='showNoTimeTrackingState'
:show-help-state='showHelpState'
:show-spent-only-state='showSpentOnlyState'
:show-estimate-only-state='showEstimateOnlyState'
:time-spent-human-readable='timeSpentHumanReadable'
:time-estimate-human-readable='timeEstimateHumanReadable'>
</time-tracking-collapsed-state>
<div class='title hide-collapsed'>
Time tracking
<div class='help-button pull-right'
v-if='!showHelpState'
@click='toggleHelpState(true)'>
<i class='fa fa-question-circle' aria-hidden='true'></i>
</div>
<div class='close-help-button pull-right'
v-if='showHelpState'
@click='toggleHelpState(false)'>
<i class='fa fa-close' aria-hidden='true'></i>
</div>
</div>
<div class='time-tracking-content hide-collapsed'>
<time-tracking-estimate-only-pane
v-if='showEstimateOnlyState'
:time-estimate-human-readable='timeEstimateHumanReadable'>
</time-tracking-estimate-only-pane>
<time-tracking-spent-only-pane
v-if='showSpentOnlyState'
:time-spent-human-readable='timeSpentHumanReadable'>
</time-tracking-spent-only-pane>
<time-tracking-no-tracking-pane
v-if='showNoTimeTrackingState'>
</time-tracking-no-tracking-pane>
<time-tracking-comparison-pane
v-if='showComparisonState'
:time-estimate='timeEstimate'
:time-spent='timeSpent'
:time-spent-human-readable='timeSpentHumanReadable'
:time-estimate-human-readable='timeEstimateHumanReadable'>
</time-tracking-comparison-pane>
<transition name='help-state-toggle'>
<time-tracking-help-state
v-if='showHelpState'
:docs-url='docsUrl'>
</time-tracking-help-state>
</transition>
</div>
</div>
`
,
});
})();
app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js
deleted
100644 → 0
View file @
7b935cc8
import
Vue
from
'
vue
'
;
import
VueResource
from
'
vue-resource
'
;
require
(
'
./components/time_tracker
'
);
require
(
'
../../smart_interval
'
);
require
(
'
../../subbable_resource
'
);
Vue
.
use
(
VueResource
);
(()
=>
{
/* This Vue instance represents what will become the parent instance for the
* sidebar. It will be responsible for managing `issuable` state and propagating
* changes to sidebar components. We will want to create a separate service to
* interface with the server at that point.
*/
class
IssuableTimeTracking
{
constructor
(
issuableJSON
)
{
const
parsedIssuable
=
JSON
.
parse
(
issuableJSON
);
return
this
.
initComponent
(
parsedIssuable
);
}
initComponent
(
parsedIssuable
)
{
this
.
parentInstance
=
new
Vue
({
el
:
'
#issuable-time-tracker
'
,
data
:
{
issuable
:
parsedIssuable
,
},
methods
:
{
fetchIssuable
()
{
return
gl
.
IssuableResource
.
get
.
call
(
gl
.
IssuableResource
,
{
type
:
'
GET
'
,
url
:
gl
.
IssuableResource
.
endpoint
,
});
},
updateState
(
data
)
{
this
.
issuable
=
data
;
},
subscribeToUpdates
()
{
gl
.
IssuableResource
.
subscribe
(
data
=>
this
.
updateState
(
data
));
},
listenForSlashCommands
()
{
$
(
document
).
on
(
'
ajax:success
'
,
'
.gfm-form
'
,
(
e
,
data
)
=>
{
const
subscribedCommands
=
[
'
spend_time
'
,
'
time_estimate
'
];
const
changedCommands
=
data
.
commands_changes
?
Object
.
keys
(
data
.
commands_changes
)
:
[];
if
(
changedCommands
&&
_
.
intersection
(
subscribedCommands
,
changedCommands
).
length
)
{
this
.
fetchIssuable
();
}
});
},
},
created
()
{
this
.
fetchIssuable
();
},
mounted
()
{
this
.
subscribeToUpdates
();
this
.
listenForSlashCommands
();
},
});
}
}
gl
.
IssuableTimeTracking
=
IssuableTimeTracking
;
})(
window
.
gl
||
(
window
.
gl
=
{}));
app/assets/javascripts/main.js
View file @
47f60167
...
...
@@ -171,7 +171,6 @@ import './single_file_diff';
import
'
./smart_interval
'
;
import
'
./snippets_list
'
;
import
'
./star
'
;
import
'
./subbable_resource
'
;
import
'
./subscription
'
;
import
'
./subscription_select
'
;
import
'
./syntax_highlight
'
;
...
...
app/assets/javascripts/sidebar
_assignees/component
s/assignee_title.js
→
app/assets/javascripts/sidebar
/components/assignee
s/assignee_title.js
View file @
47f60167
File moved
app/assets/javascripts/sidebar
_assignees/component
s/assignees.js
→
app/assets/javascripts/sidebar
/components/assignee
s/assignees.js
View file @
47f60167
import
eventHub
from
'
../event_hub
'
;
export
default
{
name
:
'
Assignees
'
,
data
()
{
...
...
@@ -52,7 +50,7 @@ export default {
},
methods
:
{
assignSelf
()
{
eventHub
.
$emit
(
'
addCurrentUser
'
);
this
.
$emit
(
'
assignSelf
'
);
},
toggleShowLess
()
{
this
.
showLess
=
!
this
.
showLess
;
...
...
@@ -91,7 +89,7 @@ export default {
width="24"
class="avatar avatar-inline s24"
:alt="assigneeAlt(user)"
:src="user.avatar
U
rl"
:src="user.avatar
_u
rl"
>
<span class="author">{{user.name}}</span>
</button>
...
...
@@ -129,7 +127,7 @@ export default {
width="32"
class="avatar avatar-inline s32"
:alt="assigneeAlt(users[0])"
:src="users[0].avatar
U
rl"
:src="users[0].avatar
_u
rl"
>
<span class="author">{{users[0].name}}</span>
<span class="username">@{{users[0].username}}</span>
...
...
@@ -152,7 +150,7 @@ export default {
width="32"
class="avatar avatar-inline s32"
:alt="assigneeAlt(user)"
:src="user.avatar
U
rl"
:src="user.avatar
_u
rl"
/>
</a>
</div>
...
...
app/assets/javascripts/sidebar
_assignees/sidebar_assignees_option
s.js
→
app/assets/javascripts/sidebar
/components/assignees/sidebar_assignee
s.js
View file @
47f60167
/* global Flash */
import
AssigneeTitle
from
'
./assignee_title
'
;
import
Assignees
from
'
./assignees
'
;
import
eventHub
from
'
./event_hub
'
;
import
store
from
'
../../stores/sidebar_store
'
;
import
mediator
from
'
../../sidebar_mediator
'
;
import
AssigneeTitle
from
'
./components/assignee_title
'
;
import
Assignees
from
'
./components/assignees
'
;
import
SidebarAssigneesService
from
'
./services/sidebar_assignees_service
'
;
import
SidebarAssigneesStore
from
'
./stores/sidebar_assignees_store
'
;
import
eventHub
from
'
../../event_hub
'
;
export
default
{
el
:
'
#js-vue-sidebar-assignees
'
,
name
:
'
SidebarAssignees
'
,
data
()
{
const
selector
=
this
.
$options
.
el
;
const
element
=
document
.
querySelector
(
selector
);
// Get data from data attributes passed from haml
const
rootPath
=
element
.
dataset
.
rootPath
;
const
path
=
element
.
dataset
.
path
;
const
field
=
element
.
dataset
.
field
;
const
editable
=
element
.
hasAttribute
(
'
data-editable
'
);
const
currentUserId
=
parseInt
(
element
.
dataset
.
userId
,
10
);
const
service
=
new
SidebarAssigneesService
(
path
,
field
);
const
store
=
new
SidebarAssigneesStore
({
currentUserId
,
rootPath
,
editable
,
assignees
:
gl
.
sidebarAssigneesData
,
});
return
{
loading
:
false
,
store
,
service
,
loading
:
false
,
field
:
''
,
};
},
components
:
{
'
assignee-title
'
:
AssigneeTitle
,
'
assignees
'
:
Assignees
,
},
computed
:
{
numberOfAssignees
()
{
return
this
.
store
.
user
s
.
length
;
return
this
.
store
.
selectedUserId
s
.
length
;
},
},
created
()
{
eventHub
.
$on
(
'
addCurrentUser
'
,
this
.
addCurrentUser
);
eventHub
.
$on
(
'
addUser
'
,
this
.
store
.
addUserId
.
bind
(
this
.
store
));
eventHub
.
$on
(
'
removeUser
'
,
this
.
store
.
removeUserId
.
bind
(
this
.
store
));
eventHub
.
$on
(
'
removeAllUsers
'
,
this
.
store
.
removeAllUserIds
.
bind
(
this
.
store
));
eventHub
.
$on
(
'
saveUsers
'
,
this
.
saveUsers
);
},
methods
:
{
addCurrentUser
()
{
this
.
store
.
addCurrentUserId
();
assignSelf
()
{
// Notify gl dropdown that we are now assigning to current user
this
.
$el
.
parentElement
.
dispatchEvent
(
new
Event
(
'
assignYourself
'
));
mediator
.
assignYourself
();
this
.
saveUsers
();
},
saveUsers
()
{
this
.
loading
=
true
;
this
.
service
.
update
(
this
.
store
.
getUserIds
())
.
then
((
response
)
=>
{
this
.
loading
=
false
;
this
.
store
.
setUsers
(
response
.
data
.
assignees
);
})
.
catch
(()
=>
{
this
.
loading
=
false
;
return
new
Flash
(
'
An error occured while saving assignees
'
);
});
},
mediator
.
saveSelectedUsers
(
this
.
field
).
then
(()
=>
this
.
loading
=
false
);
}
},
components
:
{
'
assignee-title
'
:
AssigneeTitle
,
'
assignees
'
:
Assignees
,
created
()
{
// Get events from glDropdown
eventHub
.
$on
(
'
sidebar:removeUser
'
,
this
.
store
.
removeUserId
.
bind
(
this
.
store
));
eventHub
.
$on
(
'
sidebar:addUser
'
,
this
.
store
.
addUserId
.
bind
(
this
.
store
));
eventHub
.
$on
(
'
sidebar:removeAllUsers
'
,
this
.
store
.
removeAllUserIds
.
bind
(
this
.
store
));
eventHub
.
$on
(
'
sidebar:saveUsers
'
,
this
.
saveUsers
);
},
beforeMount
()
{
const
element
=
this
.
$el
;
this
.
field
=
element
.
dataset
.
field
;
},
template
:
`
<div>
...
...
@@ -83,9 +57,10 @@ export default {
/>
<assignees
class="value"
v-if="!
store.
loading"
v-if="!loading"
:rootPath="store.rootPath"
:users="store.renderedUsers"
@assignSelf="assignSelf"
/>
</div>
`
,
...
...
app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js
0 → 100644
View file @
47f60167
import
stopwatchSvg
from
'
icons/_icon_stopwatch.svg
'
;
import
'
../../../lib/utils/pretty_time
'
;
export
default
{
name
:
'
time-tracking-collapsed-state
'
,
props
:
{
showComparisonState
:
{
type
:
Boolean
,
required
:
true
,
},
showSpentOnlyState
:
{
type
:
Boolean
,
required
:
true
,
},
showEstimateOnlyState
:
{
type
:
Boolean
,
required
:
true
,
},
showNoTimeTrackingState
:
{
type
:
Boolean
,
required
:
true
,
},
timeSpentHumanReadable
:
{
type
:
String
,
required
:
false
,
},
timeEstimateHumanReadable
:
{
type
:
String
,
required
:
false
,
},
},
methods
:
{
abbreviateTime
(
timeStr
)
{
return
gl
.
utils
.
prettyTime
.
abbreviateTime
(
timeStr
);
},
},
template
:
`
<div class='sidebar-collapsed-icon'>
${
stopwatchSvg
}
<div class='time-tracking-collapsed-summary'>
<div class='compare' v-if='showComparisonState'>
<span>{{ abbreviateTime(timeSpentHumanReadable) }} / {{ abbreviateTime(timeEstimateHumanReadable) }}</span>
</div>
<div class='estimate-only' v-if='showEstimateOnlyState'>
<span class='bold'>-- / {{ abbreviateTime(timeEstimateHumanReadable) }}</span>
</div>
<div class='spend-only' v-if='showSpentOnlyState'>
<span class='bold'>{{ abbreviateTime(timeSpentHumanReadable) }} / --</span>
</div>
<div class='no-tracking' v-if='showNoTimeTrackingState'>
<span class='no-value'>None</span>
</div>
</div>
</div>
`
,
};
app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js
0 → 100644
View file @
47f60167
import
'
../../../lib/utils/pretty_time
'
;
const
prettyTime
=
gl
.
utils
.
prettyTime
;
export
default
{
name
:
'
time-tracking-comparison-pane
'
,
props
:
{
timeSpent
:
{
type
:
Number
,
required
:
true
,
},
timeEstimate
:
{
type
:
Number
,
required
:
true
,
},
timeSpentHumanReadable
:
{
type
:
String
,
required
:
true
,
},
timeEstimateHumanReadable
:
{
type
:
String
,
required
:
true
,
},
},
computed
:
{
parsedRemaining
()
{
const
diffSeconds
=
this
.
timeEstimate
-
this
.
timeSpent
;
return
prettyTime
.
parseSeconds
(
diffSeconds
);
},
timeRemainingHumanReadable
()
{
return
prettyTime
.
stringifyTime
(
this
.
parsedRemaining
);
},
timeRemainingTooltip
()
{
const
prefix
=
this
.
timeRemainingMinutes
<
0
?
'
Over by
'
:
'
Time remaining:
'
;
return
`
${
prefix
}
${
this
.
timeRemainingHumanReadable
}
`
;
},
/* Diff values for comparison meter */
timeRemainingMinutes
()
{
return
this
.
timeEstimate
-
this
.
timeSpent
;
},
timeRemainingPercent
()
{
return
`
${
Math
.
floor
((
this
.
timeSpent
/
this
.
timeEstimate
)
*
100
)}
%`
;
},
timeRemainingStatusClass
()
{
return
this
.
timeEstimate
>=
this
.
timeSpent
?
'
within_estimate
'
:
'
over_estimate
'
;
},
/* Parsed time values */
parsedEstimate
()
{
return
prettyTime
.
parseSeconds
(
this
.
timeEstimate
);
},
parsedSpent
()
{
return
prettyTime
.
parseSeconds
(
this
.
timeSpent
);
},
},
template
:
`
<div class='time-tracking-comparison-pane'>
<div class='compare-meter' data-toggle='tooltip' data-placement='top' role='timeRemainingDisplay'
:aria-valuenow='timeRemainingTooltip'
:title='timeRemainingTooltip'
:data-original-title='timeRemainingTooltip'
:class='timeRemainingStatusClass'>
<div class='meter-container' role='timeSpentPercent' :aria-valuenow='timeRemainingPercent'>
<div :style='{ width: timeRemainingPercent }' class='meter-fill'></div>
</div>
<div class='compare-display-container'>
<div class='compare-display pull-left'>
<span class='compare-label'>Spent</span>
<span class='compare-value spent'>{{ timeSpentHumanReadable }}</span>
</div>
<div class='compare-display estimated pull-right'>
<span class='compare-label'>Est</span>
<span class='compare-value'>{{ timeEstimateHumanReadable }}</span>
</div>
</div>
</div>
</div>
`
,
};
app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.js
0 → 100644
View file @
47f60167
export
default
{
name
:
'
time-tracking-estimate-only-pane
'
,
props
:
{
timeEstimateHumanReadable
:
{
type
:
String
,
required
:
true
,
},
},
template
:
`
<div class='time-tracking-estimate-only-pane'>
<span class='bold'>Estimated:</span>
{{ timeEstimateHumanReadable }}
</div>
`
,
};
app/assets/javascripts/sidebar/components/time_tracking/help_state.js
0 → 100644
View file @
47f60167
export
default
{
name
:
'
time-tracking-help-state
'
,
props
:
{
rootPath
:
{
type
:
String
,
required
:
true
,
},
},
computed
:
{
href
()
{
return
`
${
this
.
rootPath
}
help/workflow/time_tracking.md`
;
},
},
template
:
`
<div class='time-tracking-help-state'>
<div class='time-tracking-info'>
<h4>Track time with slash commands</h4>
<p>Slash commands can be used in the issues description and comment boxes.</p>
<p>
<code>/estimate</code>
will update the estimated time with the latest command.
</p>
<p>
<code>/spend</code>
will update the sum of the time spent.
</p>
<a class='btn btn-default learn-more-button' :href="href">Learn more</a>
</div>
</div>
`
,
};
app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js
0 → 100644
View file @
47f60167
export
default
{
name
:
'
time-tracking-no-tracking-pane
'
,
template
:
`
<div class='time-tracking-no-tracking-pane'>
<span class='no-value'>No estimate or time spent</span>
</div>
`
,
};
app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js
0 → 100644
View file @
47f60167
import
'
~/smart_interval
'
;
import
timeTracker
from
'
./time_tracker
'
;
import
eventHub
from
'
../../event_hub
'
;
import
store
from
'
../../stores/sidebar_store
'
;
import
mediator
from
'
../../sidebar_mediator
'
;
export
default
{
data
()
{
return
{
store
,
};
},
components
:
{
'
issuable-time-tracker
'
:
timeTracker
,
},
methods
:
{
listenForSlashCommands
()
{
$
(
document
).
on
(
'
ajax:success
'
,
'
.gfm-form
'
,
(
e
,
data
)
=>
{
const
subscribedCommands
=
[
'
spend_time
'
,
'
time_estimate
'
];
const
changedCommands
=
data
.
commands_changes
?
Object
.
keys
(
data
.
commands_changes
)
:
[];
if
(
changedCommands
&&
_
.
intersection
(
subscribedCommands
,
changedCommands
).
length
)
{
mediator
.
fetch
();
}
});
},
},
mounted
()
{
this
.
listenForSlashCommands
();
},
template
:
`
<div class="block">
<issuable-time-tracker
:time_estimate="store.timeEstimate"
:time_spent="store.totalTimeSpent"
:human_time_estimate="store.humanTimeEstimate"
:human_time_spent="store.humanTotalTimeSpent"
:rootPath="store.rootPath"
/>
</div>
`
,
};
app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.js
0 → 100644
View file @
47f60167
export
default
{
name
:
'
time-tracking-spent-only-pane
'
,
props
:
{
timeSpentHumanReadable
:
{
type
:
String
,
required
:
true
,
},
},
template
:
`
<div class='time-tracking-spend-only-pane'>
<span class='bold'>Spent:</span>
{{ timeSpentHumanReadable }}
</div>
`
,
};
app/assets/javascripts/sidebar/components/time_tracking/time_tracker.js
0 → 100644
View file @
47f60167
import
timeTrackingHelpState
from
'
./help_state
'
;
import
timeTrackingCollapsedState
from
'
./collapsed_state
'
;
import
timeTrackingSpentOnlyPane
from
'
./spent_only_pane
'
;
import
timeTrackingNoTrackingPane
from
'
./no_tracking_pane
'
;
import
timeTrackingEstimateOnlyPane
from
'
./estimate_only_pane
'
;
import
timeTrackingComparisonPane
from
'
./comparison_pane
'
;
import
eventHub
from
'
../../event_hub
'
;
export
default
{
name
:
'
issuable-time-tracker
'
,
props
:
{
time_estimate
:
{
type
:
Number
,
required
:
true
,
default
:
0
,
},
time_spent
:
{
type
:
Number
,
required
:
true
,
default
:
0
,
},
human_time_estimate
:
{
type
:
String
,
required
:
false
,
},
human_time_spent
:
{
type
:
String
,
required
:
false
,
},
rootPath
:
{
type
:
String
,
required
:
true
,
},
},
data
()
{
return
{
showHelp
:
false
,
};
},
components
:
{
'
time-tracking-collapsed-state
'
:
timeTrackingCollapsedState
,
'
time-tracking-estimate-only-pane
'
:
timeTrackingEstimateOnlyPane
,
'
time-tracking-spent-only-pane
'
:
timeTrackingSpentOnlyPane
,
'
time-tracking-no-tracking-pane
'
:
timeTrackingNoTrackingPane
,
'
time-tracking-comparison-pane
'
:
timeTrackingComparisonPane
,
'
time-tracking-help-state
'
:
timeTrackingHelpState
,
},
computed
:
{
timeSpent
()
{
return
this
.
time_spent
;
},
timeEstimate
()
{
return
this
.
time_estimate
;
},
timeEstimateHumanReadable
()
{
return
this
.
human_time_estimate
;
},
timeSpentHumanReadable
()
{
return
this
.
human_time_spent
;
},
hasTimeSpent
()
{
return
!!
this
.
timeSpent
;
},
hasTimeEstimate
()
{
return
!!
this
.
timeEstimate
;
},
showComparisonState
()
{
return
this
.
hasTimeEstimate
&&
this
.
hasTimeSpent
;
},
showEstimateOnlyState
()
{
return
this
.
hasTimeEstimate
&&
!
this
.
hasTimeSpent
;
},
showSpentOnlyState
()
{
return
this
.
hasTimeSpent
&&
!
this
.
hasTimeEstimate
;
},
showNoTimeTrackingState
()
{
return
!
this
.
hasTimeEstimate
&&
!
this
.
hasTimeSpent
;
},
showHelpState
()
{
return
!!
this
.
showHelp
;
},
},
methods
:
{
toggleHelpState
(
show
)
{
this
.
showHelp
=
show
;
},
update
(
data
)
{
this
.
time_estimate
=
data
.
time_estimate
;
this
.
time_spent
=
data
.
time_spent
;
this
.
human_time_estimate
=
data
.
human_time_estimate
;
this
.
human_time_spent
=
data
.
human_time_spent
;
},
},
created
()
{
eventHub
.
$on
(
'
timeTracker:updateData
'
,
this
.
update
);
},
template
:
`
<div class='time_tracker time-tracking-component-wrap' v-cloak>
<time-tracking-collapsed-state
:show-comparison-state='showComparisonState'
:show-no-time-tracking-state='showNoTimeTrackingState'
:show-help-state='showHelpState'
:show-spent-only-state='showSpentOnlyState'
:show-estimate-only-state='showEstimateOnlyState'
:time-spent-human-readable='timeSpentHumanReadable'
:time-estimate-human-readable='timeEstimateHumanReadable'
/>
<div class='title hide-collapsed'>
Time tracking
<div
class='help-button pull-right'
v-if='!showHelpState'
@click='toggleHelpState(true)'
>
<i
class='fa fa-question-circle'
aria-hidden='true'
/>
</div>
<div
class='close-help-button pull-right'
v-if='showHelpState'
@click='toggleHelpState(false)'>
<i
class='fa fa-close'
aria-hidden='true'
/>
</div>
</div>
<div class='time-tracking-content hide-collapsed'>
<time-tracking-estimate-only-pane
v-if='showEstimateOnlyState'
:time-estimate-human-readable='timeEstimateHumanReadable'
/>
<time-tracking-spent-only-pane
v-if='showSpentOnlyState'
:time-spent-human-readable='timeSpentHumanReadable'
/>
<time-tracking-no-tracking-pane
v-if='showNoTimeTrackingState'
/>
<time-tracking-comparison-pane
v-if='showComparisonState'
:time-estimate='timeEstimate'
:time-spent='timeSpent'
:time-spent-human-readable='timeSpentHumanReadable'
:time-estimate-human-readable='timeEstimateHumanReadable'
/>
<transition name='help-state-toggle'>
<time-tracking-help-state
v-if='showHelpState'
:rootPath='rootPath'
/>
</transition>
</div>
</div>
`
,
};
app/assets/javascripts/sidebar
_assignees
/event_hub.js
→
app/assets/javascripts/sidebar/event_hub.js
View file @
47f60167
File moved
app/assets/javascripts/sidebar
_assignees/services/sidebar_assignees
_service.js
→
app/assets/javascripts/sidebar
/services/sidebar
_service.js
View file @
47f60167
...
...
@@ -4,15 +4,18 @@ import '../../vue_shared/vue_resource_interceptor';
Vue
.
use
(
VueResource
);
export
default
class
SidebarAssigneesService
{
constructor
(
path
,
field
)
{
this
.
field
=
field
;
this
.
path
=
path
;
export
default
class
SidebarService
{
constructor
(
endpoint
)
{
this
.
endpoint
=
endpoint
;
}
update
(
userIds
)
{
return
Vue
.
http
.
put
(
this
.
path
,
{
[
this
.
field
]:
userIds
,
get
()
{
return
Vue
.
http
.
get
(
this
.
endpoint
);
}
update
(
key
,
data
)
{
return
Vue
.
http
.
put
(
this
.
endpoint
,
{
[
key
]:
data
,
},
{
emulateJSON
:
true
,
});
...
...
app/assets/javascripts/sidebar/sidebar_bundle.js
0 → 100644
View file @
47f60167
import
Vue
from
'
vue
'
;
import
sidebarTimeTracking
from
'
./components/time_tracking/sidebar_time_tracking
'
;
import
sidebarAssignees
from
'
./components/assignees/sidebar_assignees
'
;
import
mediator
from
'
./sidebar_mediator
'
;
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
{
mediator
.
init
(
gl
.
sidebarOptions
);
mediator
.
fetch
();
new
Vue
(
sidebarAssignees
).
$mount
(
'
#js-vue-sidebar-assignees
'
);
new
Vue
(
sidebarTimeTracking
).
$mount
(
'
#issuable-time-tracker
'
);
});
app/assets/javascripts/sidebar/sidebar_mediator.js
0 → 100644
View file @
47f60167
import
Service
from
'
./services/sidebar_service
'
;
import
store
from
'
./stores/sidebar_store
'
;
export
default
{
init
(
options
)
{
store
.
init
(
options
);
this
.
service
=
new
Service
(
options
.
endpoint
);
},
assignYourself
(
field
)
{
store
.
addUserId
(
store
.
currentUserId
);
},
saveSelectedUsers
(
field
)
{
return
new
Promise
((
resolve
,
reject
)
=>
{
const
selected
=
store
.
selectedUserIds
;
// If there are no ids, that means we have to unassign (which is id = 0)
// And it only accepts an array, hence [0]
this
.
service
.
update
(
field
,
selected
.
length
===
0
?
[
0
]
:
selected
)
.
then
((
response
)
=>
{
store
.
processUserData
(
response
.
data
);
resolve
();
})
.
catch
(()
=>
{
reject
();
return
new
Flash
(
'
Error occurred when saving users
'
);
});
});
},
fetch
()
{
return
new
Promise
((
resolve
,
reject
)
=>
{
this
.
service
.
get
()
.
then
((
response
)
=>
{
this
.
fetching
=
false
;
store
.
processUserData
(
response
.
data
);
store
.
processTimeTrackingData
(
response
.
data
);
return
resolve
();
})
.
catch
(()
=>
{
reject
();
return
new
Flash
(
'
Error occured when fetching sidebar data
'
);
});
});
},
}
app/assets/javascripts/sidebar
_assignees/stores/sidebar_assignees
_store.js
→
app/assets/javascripts/sidebar
/stores/sidebar
_store.js
View file @
47f60167
export
default
class
SidebarAssigneesStore
{
constructor
(
store
)
{
const
{
currentUserId
,
assignees
,
rootPath
,
editable
}
=
store
;
export
default
{
timeEstimate
:
0
,
totalTimeSpent
:
0
,
humanTimeEstimate
:
''
,
humanTimeSpent
:
''
,
selectedUserIds
:
[],
renderedUsers
:
[],
init
(
store
)
{
const
{
currentUserId
,
rootPath
,
editable
}
=
store
;
this
.
currentUserId
=
currentUserId
;
this
.
rootPath
=
rootPath
;
this
.
selectedUserIds
=
[];
this
.
renderedUsers
=
[];
this
.
loading
=
false
;
this
.
editable
=
editable
;
},
this
.
setUsers
(
assignees
);
}
processUserData
(
data
)
{
this
.
renderedUsers
=
data
.
assignees
;
addCurrentUserId
()
{
this
.
addUserId
(
this
.
currentUserId
);
}
this
.
removeAllUserIds
();
this
.
renderedUsers
.
map
(
u
=>
this
.
addUserId
(
u
.
id
));
},
processTimeTrackingData
(
data
)
{
this
.
timeEstimate
=
data
.
time_estimate
;
this
.
totalTimeSpent
=
data
.
total_time_spent
;
this
.
humanTimeEstimate
=
data
.
human_time_estimate
;
this
.
humanTimeSpent
=
data
.
human_time_spent
;
},
addUserId
(
id
)
{
// Prevent duplicate user id's from being added
if
(
this
.
selectedUserIds
.
indexOf
(
id
)
===
-
1
)
{
this
.
selectedUserIds
.
push
(
id
);
}
}
},
removeUserId
(
id
)
{
this
.
selectedUserIds
=
this
.
selectedUserIds
.
filter
(
uid
=>
uid
!==
id
);
}
},
removeAllUserIds
()
{
this
.
selectedUserIds
=
[];
}
getUserIds
()
{
// If there are no ids, that means we have to unassign (which is id = 0)
return
this
.
selectedUserIds
.
length
>
0
?
this
.
selectedUserIds
:
[
0
];
}
setUsers
(
users
)
{
this
.
renderedUsers
=
users
.
map
((
u
)
=>
({
id
:
u
.
id
,
name
:
u
.
name
,
username
:
u
.
username
,
avatarUrl
:
u
.
avatar_url
,
}));
this
.
selectedUserIds
=
users
.
map
(
u
=>
u
.
id
);
}
}
};
app/assets/javascripts/sidebar_assignees/index.js
deleted
100644 → 0
View file @
7b935cc8
import
Vue
from
'
vue
'
;
import
sidebarAssigneesOptions
from
'
./sidebar_assignees_options
'
;
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
new
Vue
(
sidebarAssigneesOptions
));
app/assets/javascripts/subbable_resource.js
deleted
100644 → 0
View file @
7b935cc8
(()
=>
{
/*
* SubbableResource can be extended to provide a pubsub-style service for one-off REST
* calls. Subscribe by passing a callback or render method you will use to handle responses.
*
* */
class
SubbableResource
{
constructor
(
resourcePath
)
{
this
.
endpoint
=
resourcePath
;
// TODO: Switch to axios.create
this
.
resource
=
$
.
ajax
;
this
.
subscribers
=
[];
}
subscribe
(
callback
)
{
this
.
subscribers
.
push
(
callback
);
}
publish
(
newResponse
)
{
const
responseCopy
=
_
.
extend
({},
newResponse
);
this
.
subscribers
.
forEach
((
fn
)
=>
{
fn
(
responseCopy
);
});
return
newResponse
;
}
get
(
payload
)
{
return
this
.
resource
(
payload
)
.
then
(
data
=>
this
.
publish
(
data
));
}
post
(
payload
)
{
return
this
.
resource
(
payload
)
.
then
(
data
=>
this
.
publish
(
data
));
}
put
(
payload
)
{
return
this
.
resource
(
payload
)
.
then
(
data
=>
this
.
publish
(
data
));
}
delete
(
payload
)
{
return
this
.
resource
(
payload
)
.
then
(
data
=>
this
.
publish
(
data
));
}
}
gl
.
SubbableResource
=
SubbableResource
;
})(
window
.
gl
||
(
window
.
gl
=
{}));
app/assets/javascripts/users_select.js
View file @
47f60167
...
...
@@ -3,7 +3,7 @@
/* global ListUser */
import
Vue
from
'
vue
'
;
import
eventHub
from
'
./sidebar
_assignees
/event_hub
'
;
import
eventHub
from
'
./sidebar/event_hub
'
;
(
function
()
{
var
bind
=
function
(
fn
,
me
)
{
return
function
()
{
return
fn
.
apply
(
me
,
arguments
);
};
},
...
...
@@ -54,23 +54,25 @@ import eventHub from './sidebar_assignees/event_hub';
$collapsedSidebar
=
$block
.
find
(
'
.sidebar-collapsed-user
'
);
$loading
=
$block
.
find
(
'
.block-loading
'
).
fadeOut
();
$block
[
0
].
addEventListener
(
'
assignYourself
'
,
()
=>
{
// Remove unassigned selected from the DOM
const
unassignedSelected
=
$dropdown
.
closest
(
'
.selectbox
'
)
.
find
(
"
input[name='
"
+
(
$dropdown
.
data
(
'
field-name
'
))
+
"
'][value=0]
"
);
if
(
$block
[
0
])
{
$block
[
0
].
addEventListener
(
'
assignYourself
'
,
()
=>
{
// Remove unassigned selected from the DOM
const
unassignedSelected
=
$dropdown
.
closest
(
'
.selectbox
'
)
.
find
(
"
input[name='
"
+
(
$dropdown
.
data
(
'
field-name
'
))
+
"
'][value=0]
"
);
if
(
unassignedSelected
)
{
unassignedSelected
.
remove
();
}
if
(
unassignedSelected
)
{
unassignedSelected
.
remove
();
}
// Save current selected user to the DOM
const
input
=
document
.
createElement
(
'
input
'
);
input
.
type
=
'
hidden
'
;
input
.
name
=
$dropdown
.
data
(
'
field-name
'
);
input
.
value
=
_this
.
currentUser
.
id
;
// Save current selected user to the DOM
const
input
=
document
.
createElement
(
'
input
'
);
input
.
type
=
'
hidden
'
;
input
.
name
=
$dropdown
.
data
(
'
field-name
'
);
input
.
value
=
_this
.
currentUser
.
id
;
$dropdown
.
before
(
input
);
});
$dropdown
.
before
(
input
);
});
}
var
getSelected
=
function
()
{
return
$selectbox
...
...
@@ -269,7 +271,7 @@ import eventHub from './sidebar_assignees/event_hub';
defaultLabel
:
defaultLabel
,
hidden
:
function
(
e
)
{
if
(
$dropdown
.
hasClass
(
'
js-multiselect
'
))
{
eventHub
.
$emit
(
'
saveUsers
'
);
eventHub
.
$emit
(
'
s
idebar:s
aveUsers
'
);
}
$selectbox
.
hide
();
...
...
@@ -294,10 +296,10 @@ import eventHub from './sidebar_assignees/event_hub';
const
id
=
parseInt
(
element
.
value
,
10
);
element
.
remove
();
});
eventHub
.
$emit
(
'
removeAllUsers
'
);
eventHub
.
$emit
(
'
sidebar:
removeAllUsers
'
);
}
else
if
(
isActive
)
{
// user selected
eventHub
.
$emit
(
'
addUser
'
,
user
.
id
);
eventHub
.
$emit
(
'
sidebar:
addUser
'
,
user
.
id
);
// Remove unassigned selection (if it was previously selected)
const
unassignedSelected
=
$dropdown
.
closest
(
'
.selectbox
'
)
...
...
@@ -313,7 +315,7 @@ import eventHub from './sidebar_assignees/event_hub';
}
// User unselected
eventHub
.
$emit
(
'
removeUser
'
,
user
.
id
);
eventHub
.
$emit
(
'
sidebar:
removeUser
'
,
user
.
id
);
}
}
...
...
app/serializers/issue_entity.rb
View file @
47f60167
class
IssueEntity
<
IssuableEntity
expose
:branch_name
expose
:confidential
expose
:assignee
_id
s
expose
:assignees
expose
:due_date
expose
:moved_to_id
expose
:project_id
...
...
app/views/shared/issuable/_sidebar.html.haml
View file @
47f60167
-
todo
=
issuable_todo
(
issuable
)
-
content_for
:page_specific_javascripts
do
=
page_specific_javascript_bundle_tag
(
'common_vue'
)
=
page_specific_javascript_bundle_tag
(
'
issuable
'
)
=
page_specific_javascript_bundle_tag
(
'
sidebar
'
)
%aside
.right-sidebar.js-right-sidebar
{
data:
{
"offset-top"
=>
"101"
,
"spy"
=>
"affix"
},
class:
sidebar_gutter_collapsed_class
,
'aria-live'
=>
'polite'
}
.issuable-sidebar
.issuable-sidebar
{
data:
{
endpoint:
"#{issuable_json_path(issuable)}"
}
}
-
can_edit_issuable
=
can?
(
current_user
,
:"admin_
#{
issuable
.
to_ability_name
}
"
,
@project
)
.block.issuable-sidebar-header
-
if
current_user
...
...
@@ -24,19 +24,7 @@
=
form_for
[
@project
.
namespace
.
becomes
(
Namespace
),
@project
,
issuable
],
remote:
true
,
format: :json
,
html:
{
class:
'issuable-context-form inline-update js-issuable-update'
}
do
|
f
|
.block.assignee
-
if
issuable
.
instance_of?
(
Issue
)
#js-vue-sidebar-assignees
{
data:
{
path:
issuable_json_path
(
issuable
),
field:
"#{issuable.to_ability_name}[assignee_ids]"
,
'editable'
=>
can_edit_issuable
?
true
:
false
,
user:
{
id:
current_user
.
id
},
root:
{
path:
root_path
}
}
}
-
content_for
:page_specific_javascripts
do
=
page_specific_javascript_bundle_tag
(
'sidebar_assignees'
)
:javascript
gl
.
sidebarAssigneesData
=
[];
-
issuable
.
assignees
.
each
do
|
assignee
|
:javascript
gl
.
sidebarAssigneesData
.
push
({
id
:
#{
assignee
.
id
}
,
name
:
"
#{
assignee
.
name
}
"
,
username
:
"
#{
assignee
.
username
}
"
,
avatar_url
:
"
#{
assignee
.
avatar_url
}
"
})
#js-vue-sidebar-assignees
{
data:
{
field:
"#{issuable.to_ability_name}[assignee_ids]"
}
}
-
else
.sidebar-collapsed-icon.sidebar-collapsed-user
{
data:
{
toggle:
"tooltip"
,
placement:
"left"
,
container:
"body"
},
title:
(
issuable
.
assignee
.
name
if
issuable
.
assignee
)
}
-
if
issuable
.
assignee
...
...
@@ -108,11 +96,10 @@
=
dropdown_tag
(
'Milestone'
,
options:
{
title:
'Assign milestone'
,
toggle_class:
'js-milestone-select js-extra-options'
,
filter:
true
,
dropdown_class:
'dropdown-menu-selectable'
,
placeholder:
'Search milestones'
,
data:
{
show_no:
true
,
field_name:
"
#{
issuable
.
to_ability_name
}
[milestone_id]"
,
project_id:
@project
.
id
,
issuable_id:
issuable
.
id
,
milestones:
namespace_project_milestones_path
(
@project
.
namespace
,
@project
,
:json
),
ability_name:
issuable
.
to_ability_name
,
issue_update:
issuable_json_path
(
issuable
),
use_id:
true
}})
-
if
issuable
.
has_attribute?
(
:time_estimate
)
#issuable-time-tracker
.block
%issuable-time-tracker
{
':time_estimate'
=>
'issuable.time_estimate'
,
':time_spent'
=>
'issuable.total_time_spent'
,
':human_time_estimate'
=>
'issuable.human_time_estimate'
,
':human_time_spent'
=>
'issuable.human_total_time_spent'
,
'docs-url'
=>
help_page_path
(
'workflow/time_tracking.md'
)
}
// Fallback while content is loading
.title.hide-collapsed
Time tracking
=
icon
(
'spinner spin'
,
'aria-hidden'
:
'true'
)
// Fallback while content is loading
.title.hide-collapsed
Time tracking
=
icon
(
'spinner spin'
,
'aria-hidden'
:
'true'
)
-
if
issuable
.
has_attribute?
(
:due_date
)
.block.due_date
.sidebar-collapsed-icon
...
...
@@ -229,8 +216,13 @@
=
clipboard_button
(
clipboard_text:
project_ref
,
title:
"Copy reference to clipboard"
,
placement:
"left"
)
:javascript
gl
.
IssuableResource
=
new
gl
.
SubbableResource
(
'
#{
issuable_json_path
(
issuable
)
}
'
);
new
gl
.
IssuableTimeTracking
(
"
#{
escape_javascript
(
serialize_issuable
(
issuable
))
}
"
);
gl
.
sidebarOptions
=
{
endpoint
:
"
#{
issuable_json_path
(
issuable
)
}
"
,
editable
:
#{
can_edit_issuable
?
true
:
false
}
,
currentUserId
:
#{
current_user
.
id
}
,
rootPath
:
"
#{
root_path
}
"
};
new
MilestoneSelect
(
'
{"full_path":"
#{
@project
.
full_path
}
"}
'
);
new
LabelsSelect
();
new
WeightSelect
();
...
...
config/webpack.config.js
View file @
47f60167
...
...
@@ -33,7 +33,7 @@ var config = {
graphs
:
'
./graphs/graphs_bundle.js
'
,
groups_list
:
'
./groups_list.js
'
,
issues
:
'
./issues/issues_bundle.js
'
,
issuable
:
'
./issuable/issuable
_bundle.js
'
,
sidebar
:
'
./sidebar/sidebar
_bundle.js
'
,
merge_conflicts
:
'
./merge_conflicts/merge_conflicts_bundle.js
'
,
merge_request_widget
:
'
./merge_request_widget/ci_bundle.js
'
,
mr_widget_ee
:
'
./merge_request_widget/widget_bundle.js
'
,
...
...
@@ -46,7 +46,6 @@ var config = {
u2f
:
[
'
vendor/u2f
'
],
users
:
'
./users/users_bundle.js
'
,
vue_pipelines
:
'
./vue_pipelines_index/index.js
'
,
sidebar_assignees
:
'
./sidebar_assignees/index.js
'
,
},
output
:
{
...
...
@@ -106,7 +105,7 @@ var config = {
'
diff_notes
'
,
'
environments
'
,
'
environments_folder
'
,
'
issuable
'
,
'
sidebar
'
,
'
merge_conflicts
'
,
'
mr_widget_ee
'
,
'
vue_pipelines
'
,
...
...
spec/javascripts/helpers/vue_spec_helper.js
deleted
100644 → 0
View file @
7b935cc8
class
VueSpecHelper
{
static
createComponent
(
Vue
,
componentName
,
propsData
)
{
const
Component
=
Vue
.
extend
.
call
(
Vue
,
componentName
);
return
new
Component
({
el
:
document
.
createElement
(
'
div
'
),
propsData
,
});
}
}
module
.
exports
=
VueSpecHelper
;
spec/javascripts/helpers/vue_spec_helper_spec.js
deleted
100644 → 0
View file @
7b935cc8
import
Vue
from
'
vue
'
;
import
VueSpecHelper
from
'
./vue_spec_helper
'
;
import
ClassSpecHelper
from
'
./class_spec_helper
'
;
describe
(
'
VueSpecHelper
'
,
()
=>
{
describe
(
'
createComponent
'
,
()
=>
{
const
sample
=
{
name
:
'
Sample
'
,
props
:
{
content
:
{
type
:
String
,
required
:
false
,
},
},
template
:
`
<div>{{content}}</div>
`
,
};
it
(
'
should be a static method
'
,
()
=>
{
expect
(
ClassSpecHelper
.
itShouldBeAStaticMethod
(
VueSpecHelper
,
'
createComponent
'
).
status
()).
toBe
(
'
passed
'
);
});
it
(
'
should call Vue.extend
'
,
()
=>
{
spyOn
(
Vue
,
'
extend
'
).
and
.
callThrough
();
VueSpecHelper
.
createComponent
(
Vue
,
sample
,
{});
expect
(
Vue
.
extend
).
toHaveBeenCalled
();
});
it
(
'
should return view model
'
,
()
=>
{
const
vm
=
VueSpecHelper
.
createComponent
(
Vue
,
sample
,
{
content
:
'
content
'
,
});
expect
(
vm
.
$el
.
textContent
).
toEqual
(
'
content
'
);
});
});
});
spec/javascripts/issuable_time_tracker_spec.js
View file @
47f60167
...
...
@@ -2,7 +2,7 @@
import
Vue
from
'
vue
'
;
require
(
'
~/issuable/time_tracking/components/time_tracker
'
)
;
import
'
~/issuable/components/time_tracking/time_tracker
'
;
function
initTimeTrackingComponent
(
opts
)
{
setFixtures
(
`
...
...
spec/javascripts/subbable_resource_spec.js
deleted
100644 → 0
View file @
7b935cc8
/* eslint-disable max-len, arrow-parens, comma-dangle */
require
(
'
~/subbable_resource
'
);
/*
* Test that each rest verb calls the publish and subscribe function and passes the correct value back
*
*
* */
((
global
)
=>
{
describe
(
'
Subbable Resource
'
,
function
()
{
describe
(
'
PubSub
'
,
function
()
{
beforeEach
(
function
()
{
this
.
MockResource
=
new
global
.
SubbableResource
(
'
https://example.com
'
);
});
it
(
'
should successfully add a single subscriber
'
,
function
()
{
const
callback
=
()
=>
{};
this
.
MockResource
.
subscribe
(
callback
);
expect
(
this
.
MockResource
.
subscribers
.
length
).
toBe
(
1
);
expect
(
this
.
MockResource
.
subscribers
[
0
]).
toBe
(
callback
);
});
it
(
'
should successfully add multiple subscribers
'
,
function
()
{
const
callbackOne
=
()
=>
{};
const
callbackTwo
=
()
=>
{};
const
callbackThree
=
()
=>
{};
this
.
MockResource
.
subscribe
(
callbackOne
);
this
.
MockResource
.
subscribe
(
callbackTwo
);
this
.
MockResource
.
subscribe
(
callbackThree
);
expect
(
this
.
MockResource
.
subscribers
.
length
).
toBe
(
3
);
});
it
(
'
should successfully publish an update to a single subscriber
'
,
function
()
{
const
state
=
{
myprop
:
1
};
const
callbacks
=
{
one
:
(
data
)
=>
expect
(
data
.
myprop
).
toBe
(
2
),
two
:
(
data
)
=>
expect
(
data
.
myprop
).
toBe
(
2
),
three
:
(
data
)
=>
expect
(
data
.
myprop
).
toBe
(
2
)
};
const
spyOne
=
spyOn
(
callbacks
,
'
one
'
);
const
spyTwo
=
spyOn
(
callbacks
,
'
two
'
);
const
spyThree
=
spyOn
(
callbacks
,
'
three
'
);
this
.
MockResource
.
subscribe
(
callbacks
.
one
);
this
.
MockResource
.
subscribe
(
callbacks
.
two
);
this
.
MockResource
.
subscribe
(
callbacks
.
three
);
state
.
myprop
+=
1
;
this
.
MockResource
.
publish
(
state
);
expect
(
spyOne
).
toHaveBeenCalled
();
expect
(
spyTwo
).
toHaveBeenCalled
();
expect
(
spyThree
).
toHaveBeenCalled
();
});
});
});
})(
window
.
gl
||
(
window
.
gl
=
{}));
spec/javascripts/vue_sidebar_assignees/assignee_title_spec.js
deleted
100644 → 0
View file @
7b935cc8
import
Vue
from
'
vue
'
;
import
assigneeTitleComponent
from
'
~/vue_sidebar_assignees/components/assignee_title
'
;
import
VueSpecHelper
from
'
../helpers/vue_spec_helper
'
;
describe
(
'
AssigneeTitle
'
,
()
=>
{
const
createComponent
=
props
=>
VueSpecHelper
.
createComponent
(
Vue
,
assigneeTitleComponent
,
props
);
describe
(
'
computed
'
,
()
=>
{
describe
(
'
assigneeTitle
'
,
()
=>
{
it
(
'
returns "Assignee" when there is only one assignee
'
,
()
=>
{
const
vm
=
createComponent
({
numberOfAssignees
:
1
,
editable
:
true
,
});
expect
(
vm
.
assigneeTitle
).
toEqual
(
'
Assignee
'
);
});
it
(
'
returns "Assignee" when there is only no assignee
'
,
()
=>
{
const
vm
=
createComponent
({
numberOfAssignees
:
0
,
editable
:
true
,
});
expect
(
vm
.
assigneeTitle
).
toEqual
(
'
Assignee
'
);
});
it
(
'
returns "2 Assignees" when there is two assignee
'
,
()
=>
{
const
vm
=
createComponent
({
numberOfAssignees
:
2
,
editable
:
false
,
});
expect
(
vm
.
assigneeTitle
).
toEqual
(
'
2 Assignees
'
);
});
});
});
describe
(
'
template
'
,
()
=>
{
it
(
'
should render assigneeTitle
'
,
()
=>
{
const
vm
=
createComponent
({
numberOfAssignees
:
100
,
editable
:
false
,
});
const
el
=
vm
.
$el
;
expect
(
el
.
tagName
).
toEqual
(
'
DIV
'
);
expect
(
el
.
textContent
.
trim
()).
toEqual
(
vm
.
assigneeTitle
);
});
it
(
'
should display spinner when loading
'
,
()
=>
{
const
el
=
createComponent
({
numberOfAssignees
:
0
,
loading
:
true
,
editable
:
false
,
}).
$el
;
const
i
=
el
.
querySelector
(
'
i
'
);
expect
(
i
).
toBeDefined
();
});
it
(
'
should not display spinner when not loading
'
,
()
=>
{
const
el
=
createComponent
({
numberOfAssignees
:
0
,
editable
:
true
,
}).
$el
;
const
i
=
el
.
querySelector
(
'
i
'
);
expect
(
i
).
toBeNull
();
});
it
(
'
should display edit link when editable
'
,
()
=>
{
const
el
=
createComponent
({
numberOfAssignees
:
0
,
editable
:
true
,
}).
$el
;
const
editLink
=
el
.
querySelector
(
'
.edit-link
'
);
expect
(
editLink
).
toBeDefined
();
});
it
(
'
should display edit link when not editable
'
,
()
=>
{
const
el
=
createComponent
({
numberOfAssignees
:
0
,
editable
:
false
,
}).
$el
;
const
editLink
=
el
.
querySelector
(
'
.edit-link
'
);
expect
(
editLink
).
toBeNull
();
});
});
});
spec/javascripts/vue_sidebar_assignees/collapsed/assignees_spec.js
deleted
100644 → 0
View file @
7b935cc8
import
Vue
from
'
vue
'
;
import
assigneesComponent
from
'
~/vue_sidebar_assignees/components/collapsed/assignees
'
;
import
avatarComponent
from
'
~/vue_sidebar_assignees/components/collapsed/avatar
'
;
import
VueSpecHelper
from
'
../../helpers/vue_spec_helper
'
;
import
{
mockUser
,
mockUser2
,
mockUser3
}
from
'
../mock_data
'
;
describe
(
'
CollapsedAssignees
'
,
()
=>
{
const
mockUsers
=
[
mockUser
,
mockUser2
];
const
createAssigneesComponent
=
props
=>
VueSpecHelper
.
createComponent
(
Vue
,
assigneesComponent
,
props
);
const
createAvatarComponent
=
props
=>
VueSpecHelper
.
createComponent
(
Vue
,
avatarComponent
,
props
);
describe
(
'
computed
'
,
()
=>
{
describe
(
'
title
'
,
()
=>
{
it
(
'
returns one name when there is one assignee
'
,
()
=>
{
const
users
=
Object
.
assign
([],
mockUsers
);
users
.
pop
();
const
vm
=
createAssigneesComponent
({
users
,
});
expect
(
vm
.
title
).
toEqual
(
'
Clark Kent
'
);
});
it
(
'
returns two names when there are two assignees
'
,
()
=>
{
const
vm
=
createAssigneesComponent
({
users
:
mockUsers
,
});
expect
(
vm
.
title
).
toEqual
(
'
Clark Kent, Bruce Wayne
'
);
});
it
(
'
returns more text when there are more than defaultRenderCount assignees
'
,
()
=>
{
const
vm
=
createAssigneesComponent
({
users
:
mockUsers
,
defaultRenderCount
:
1
,
});
expect
(
vm
.
title
).
toEqual
(
'
Clark Kent, + 1 more
'
);
});
});
describe
(
'
counter
'
,
()
=>
{
it
(
'
should return one less than users.length
'
,
()
=>
{
const
vm
=
createAssigneesComponent
({
users
:
mockUsers
,
});
expect
(
vm
.
counter
).
toEqual
(
'
+1
'
);
});
it
(
'
should return defaultMaxCounter+ when users.length is greater than defaultMaxCounter
'
,
()
=>
{
const
vm
=
createAssigneesComponent
({
users
:
mockUsers
,
defaultMaxCounter
:
1
,
});
expect
(
vm
.
counter
).
toEqual
(
'
1+
'
);
});
});
describe
(
'
hasNoAssignees
'
,
()
=>
{
it
(
'
returns true when there are no assignees
'
,
()
=>
{
const
vm
=
createAssigneesComponent
({
users
:
[],
});
expect
(
vm
.
hasNoAssignees
).
toEqual
(
true
);
});
it
(
'
returns false when there are assignees
'
,
()
=>
{
const
vm
=
createAssigneesComponent
({
users
:
mockUsers
,
});
expect
(
vm
.
hasNoAssignees
).
toEqual
(
false
);
});
});
describe
(
'
hasTwoAssignees
'
,
()
=>
{
it
(
'
returns true when there are two assignees
'
,
()
=>
{
const
vm
=
createAssigneesComponent
({
users
:
mockUsers
,
});
expect
(
vm
.
hasTwoAssignees
).
toEqual
(
true
);
});
it
(
'
returns false when there is no assignes
'
,
()
=>
{
const
vm
=
createAssigneesComponent
({
users
:
[],
});
expect
(
vm
.
hasTwoAssignees
).
toEqual
(
false
);
});
});
describe
(
'
moreThanOneAssignees
'
,
()
=>
{
it
(
'
returns true when there are more than one assignee
'
,
()
=>
{
const
vm
=
createAssigneesComponent
({
users
:
mockUsers
,
});
expect
(
vm
.
moreThanOneAssignees
).
toEqual
(
true
);
});
it
(
'
returns false when there is one assignee
'
,
()
=>
{
const
users
=
Object
.
assign
([],
mockUsers
);
users
.
pop
();
const
vm
=
createAssigneesComponent
({
users
,
});
expect
(
vm
.
moreThanOneAssignees
).
toEqual
(
false
);
});
});
describe
(
'
moreThanTwoAssignees
'
,
()
=>
{
it
(
'
returns true when there are more than two assignees
'
,
()
=>
{
const
users
=
Object
.
assign
([],
mockUsers
);
users
.
push
(
mockUser3
);
const
vm
=
createAssigneesComponent
({
users
,
});
expect
(
vm
.
moreThanTwoAssignees
).
toEqual
(
true
);
});
it
(
'
returns false when there are two assignees
'
,
()
=>
{
const
vm
=
createAssigneesComponent
({
users
:
mockUsers
,
});
expect
(
vm
.
moreThanTwoAssignees
).
toEqual
(
false
);
});
});
});
describe
(
'
components
'
,
()
=>
{
it
(
'
should have components added
'
,
()
=>
{
expect
(
assigneesComponent
.
components
[
'
collapsed-avatar
'
]).
toBeDefined
();
});
});
describe
(
'
template
'
,
()
=>
{
function
avatarProp
(
user
)
{
return
{
name
:
user
.
name
,
avatarUrl
:
user
.
avatarUrl
,
};
}
it
(
'
should render fa-user if there are no assignees
'
,
()
=>
{
const
el
=
createAssigneesComponent
({
users
:
[],
}).
$el
;
const
sidebarCollapsedIcons
=
el
.
querySelectorAll
(
'
.sidebar-collapsed-icon
'
);
expect
(
sidebarCollapsedIcons
.
length
).
toEqual
(
1
);
const
userIcon
=
sidebarCollapsedIcons
[
0
].
querySelector
(
'
.fa-user
'
);
expect
(
userIcon
).
toBeDefined
();
});
it
(
'
should not render fa-user if there are assignees
'
,
()
=>
{
const
el
=
createAssigneesComponent
({
users
:
mockUsers
,
}).
$el
;
const
sidebarCollapsedIcons
=
el
.
querySelectorAll
(
'
.sidebar-collapsed-icon
'
);
expect
(
sidebarCollapsedIcons
.
length
).
toEqual
(
1
);
const
userIcon
=
sidebarCollapsedIcons
[
0
].
querySelector
(
'
.fa-user
'
);
expect
(
userIcon
).
toBeNull
();
});
it
(
'
should render one assignee if there is one assignee
'
,
()
=>
{
const
users
=
Object
.
assign
([],
mockUsers
);
users
.
pop
();
const
vm
=
createAssigneesComponent
({
users
,
});
const
el
=
vm
.
$el
;
const
sidebarCollapsedIcons
=
el
.
querySelectorAll
(
'
.sidebar-collapsed-icon
'
);
expect
(
sidebarCollapsedIcons
.
length
).
toEqual
(
1
);
const
div
=
sidebarCollapsedIcons
[
0
];
expect
(
div
.
getAttribute
(
'
data-original-title
'
)).
toEqual
(
vm
.
title
);
expect
(
div
.
classList
.
contains
(
'
multiple-users
'
)).
toEqual
(
false
);
expect
(
div
.
querySelector
(
'
.avatar-counter
'
)).
toBeNull
();
const
avatarEl
=
createAvatarComponent
(
avatarProp
(
users
[
0
])).
$el
;
const
divWithoutComments
=
div
.
innerHTML
.
replace
(
/<!---->/g
,
''
).
trim
();
expect
(
divWithoutComments
).
toEqual
(
avatarEl
.
outerHTML
);
});
it
(
'
should render two assignees if there are two assignees
'
,
()
=>
{
const
vm
=
createAssigneesComponent
({
users
:
mockUsers
,
});
const
el
=
vm
.
$el
;
const
sidebarCollapsedIcons
=
el
.
querySelectorAll
(
'
.sidebar-collapsed-icon
'
);
expect
(
sidebarCollapsedIcons
.
length
).
toEqual
(
1
);
const
div
=
sidebarCollapsedIcons
[
0
];
expect
(
div
.
getAttribute
(
'
data-original-title
'
)).
toEqual
(
vm
.
title
);
expect
(
div
.
classList
.
contains
(
'
multiple-users
'
)).
toEqual
(
true
);
expect
(
div
.
querySelector
(
'
.avatar-counter
'
)).
toBeNull
();
const
avatarEl
=
[
createAvatarComponent
(
avatarProp
(
mockUsers
[
0
])).
$el
,
createAvatarComponent
(
avatarProp
(
mockUsers
[
1
])).
$el
,
];
const
divWithoutComments
=
div
.
innerHTML
.
replace
(
/<!---->/g
,
''
).
trim
();
expect
(
divWithoutComments
).
toEqual
(
`
${
avatarEl
[
0
].
outerHTML
}
${
avatarEl
[
1
].
outerHTML
}
`
);
});
it
(
'
should render counter if there are more than two assignees
'
,
()
=>
{
const
users
=
Object
.
assign
([],
mockUsers
);
users
.
push
(
mockUser3
);
const
vm
=
createAssigneesComponent
({
users
,
});
const
el
=
vm
.
$el
;
const
sidebarCollapsedIcons
=
el
.
querySelectorAll
(
'
.sidebar-collapsed-icon
'
);
expect
(
sidebarCollapsedIcons
.
length
).
toEqual
(
1
);
const
div
=
sidebarCollapsedIcons
[
0
];
expect
(
div
.
getAttribute
(
'
data-original-title
'
)).
toEqual
(
vm
.
title
);
expect
(
div
.
classList
.
contains
(
'
multiple-users
'
)).
toEqual
(
true
);
const
avatarCounter
=
div
.
querySelector
(
'
.avatar-counter
'
);
expect
(
avatarCounter
).
toBeDefined
();
expect
(
avatarCounter
.
textContent
).
toEqual
(
vm
.
counter
);
const
avatarEl
=
createAvatarComponent
(
avatarProp
(
users
[
0
])).
$el
;
expect
(
div
.
innerHTML
.
indexOf
(
avatarEl
.
outerHTML
)
!==
-
1
).
toEqual
(
true
);
});
});
});
spec/javascripts/vue_sidebar_assignees/collapsed/avatar_spec.js
deleted
100644 → 0
View file @
7b935cc8
import
Vue
from
'
vue
'
;
import
avatarComponent
from
'
~/vue_sidebar_assignees/components/collapsed/avatar
'
;
import
VueSpecHelper
from
'
../../helpers/vue_spec_helper
'
;
import
{
mockUser
}
from
'
../mock_data
'
;
describe
(
'
CollapsedAvatar
'
,
()
=>
{
const
createComponent
=
props
=>
VueSpecHelper
.
createComponent
(
Vue
,
avatarComponent
,
props
);
describe
(
'
computed
'
,
()
=>
{
describe
(
'
alt
'
,
()
=>
{
it
(
'
returns avatar alt text
'
,
()
=>
{
const
vm
=
createComponent
(
mockUser
);
expect
(
vm
.
alt
).
toEqual
(
`
${
mockUser
.
name
}
's avatar`
);
});
});
});
describe
(
'
template
'
,
()
=>
{
it
(
'
should render alt text
'
,
()
=>
{
const
vm
=
createComponent
(
mockUser
);
const
el
=
vm
.
$el
;
const
avatar
=
el
.
querySelector
(
'
.avatar
'
);
expect
(
avatar
.
getAttribute
(
'
alt
'
)).
toEqual
(
vm
.
alt
);
});
it
(
'
should render avatar src url
'
,
()
=>
{
const
el
=
createComponent
(
mockUser
).
$el
;
const
avatar
=
el
.
querySelector
(
'
.avatar
'
);
expect
(
avatar
.
getAttribute
(
'
src
'
)).
toEqual
(
mockUser
.
avatarUrl
);
});
it
(
'
should render name
'
,
()
=>
{
const
el
=
createComponent
(
mockUser
).
$el
;
const
span
=
el
.
querySelector
(
'
.author
'
);
expect
(
span
.
textContent
).
toEqual
(
mockUser
.
name
);
});
});
});
spec/javascripts/vue_sidebar_assignees/expanded/multiple_assignees_spec.js
deleted
100644 → 0
View file @
7b935cc8
import
Vue
from
'
vue
'
;
import
multipleAssigneesComponent
from
'
~/vue_sidebar_assignees/components/expanded/multiple_assignees
'
;
import
VueSpecHelper
from
'
../../helpers/vue_spec_helper
'
;
import
{
mockUser
,
mockUser2
,
mockUser3
}
from
'
../mock_data
'
;
describe
(
'
MultipleAssignees
'
,
()
=>
{
const
mockStore
=
{
users
:
[
mockUser
,
mockUser2
],
defaultRenderCount
:
1
,
rootPath
:
'
rootPath
'
,
};
const
createComponent
=
props
=>
VueSpecHelper
.
createComponent
(
Vue
,
multipleAssigneesComponent
,
props
);
describe
(
'
computed
'
,
()
=>
{
describe
(
'
renderShowMoreSection
'
,
()
=>
{
it
(
'
should return true when users.length is greater than defaultRenderCount
'
,
()
=>
{
const
vm
=
createComponent
({
store
:
mockStore
,
});
expect
(
vm
.
renderShowMoreSection
).
toEqual
(
true
);
});
it
(
'
should return false when users.length is not greater than defaultRenderCount
'
,
()
=>
{
const
newStore
=
Object
.
assign
({},
mockStore
);
newStore
.
defaultRenderCount
=
5
;
const
vm
=
createComponent
({
store
:
newStore
,
});
expect
(
vm
.
renderShowMoreSection
).
toEqual
(
false
);
});
});
describe
(
'
numberOfHiddenAssignees
'
,
()
=>
{
it
(
'
should return number of assignees that are not rendered
'
,
()
=>
{
const
vm
=
createComponent
({
store
:
mockStore
,
});
expect
(
vm
.
numberOfHiddenAssignees
).
toEqual
(
1
);
});
});
describe
(
'
isHiddenAssignees
'
,
()
=>
{
it
(
'
should return true when numberOfHiddenAssignees is greater than zero
'
,
()
=>
{
const
vm
=
createComponent
({
store
:
mockStore
,
});
expect
(
vm
.
isHiddenAssignees
).
toEqual
(
true
);
});
it
(
'
should return false when numberOfHiddenAssignees is zero
'
,
()
=>
{
const
newStore
=
Object
.
assign
({},
mockStore
);
newStore
.
defaultRenderCount
=
2
;
const
vm
=
createComponent
({
store
:
newStore
,
});
expect
(
vm
.
isHiddenAssignees
).
toEqual
(
false
);
});
});
});
describe
(
'
methods
'
,
()
=>
{
describe
(
'
toggleShowLess
'
,
()
=>
{
it
(
'
should toggle showLess
'
,
()
=>
{
const
vm
=
createComponent
({
store
:
mockStore
,
});
expect
(
vm
.
showLess
).
toEqual
(
true
);
vm
.
toggleShowLess
();
expect
(
vm
.
showLess
).
toEqual
(
false
);
});
});
describe
(
'
renderAssignee
'
,
()
=>
{
it
(
'
should return true if showLess is false
'
,
()
=>
{
const
vm
=
createComponent
({
store
:
mockStore
,
});
vm
.
showLess
=
false
;
expect
(
vm
.
renderAssignee
(
0
)).
toEqual
(
true
);
});
it
(
'
should return true if showLess is true and index is less than defaultRenderCount
'
,
()
=>
{
const
vm
=
createComponent
({
store
:
mockStore
,
});
vm
.
showLess
=
true
;
expect
(
vm
.
renderAssignee
(
0
)).
toEqual
(
true
);
});
it
(
'
should return false if showLess is true and index is greater than defaultRenderCount
'
,
()
=>
{
const
vm
=
createComponent
({
store
:
mockStore
,
});
vm
.
showLess
=
true
;
expect
(
vm
.
renderAssignee
(
10
)).
toEqual
(
false
);
});
});
describe
(
'
assigneeUrl
'
,
()
=>
{
it
(
'
should return url
'
,
()
=>
{
const
vm
=
createComponent
({
store
:
mockStore
,
});
const
username
=
'
username
'
;
expect
(
vm
.
assigneeUrl
(
username
)).
toEqual
(
`
${
mockStore
.
rootPath
}${
username
}
`
);
});
});
describe
(
'
assigneeAlt
'
,
()
=>
{
it
(
'
should return alt
'
,
()
=>
{
const
vm
=
createComponent
({
store
:
mockStore
,
});
const
name
=
'
name
'
;
expect
(
vm
.
assigneeAlt
(
name
)).
toEqual
(
`
${
name
}
's avatar`
);
});
});
});
describe
(
'
template
'
,
()
=>
{
let
vm
;
let
el
;
describe
(
'
userItem
'
,
()
=>
{
let
userItems
;
beforeEach
(()
=>
{
const
newStore
=
Object
.
assign
({},
mockStore
);
newStore
.
defaultRenderCount
=
2
;
// Create a new copy to prevent mutating `mockStore.users`
const
users
=
Object
.
assign
([],
mockStore
.
users
);
users
.
push
(
mockUser3
);
newStore
.
users
=
users
;
vm
=
createComponent
({
store
:
newStore
,
});
el
=
vm
.
$el
;
userItems
=
el
.
querySelectorAll
(
'
.user-item
'
);
});
it
(
'
should render multiple user-item
'
,
()
=>
{
expect
(
userItems
.
length
).
toEqual
(
2
);
});
it
(
'
should render href
'
,
()
=>
{
[].
forEach
.
call
(
userItems
,
(
item
,
index
)
=>
{
const
user
=
vm
.
store
.
users
[
index
];
const
a
=
item
.
querySelector
(
'
a
'
);
expect
(
a
.
getAttribute
(
'
href
'
)).
toEqual
(
vm
.
assigneeUrl
(
user
.
username
));
});
});
it
(
'
should render anchor title
'
,
()
=>
{
[].
forEach
.
call
(
userItems
,
(
item
,
index
)
=>
{
const
user
=
vm
.
store
.
users
[
index
];
const
a
=
item
.
querySelector
(
'
a
'
);
expect
(
a
.
getAttribute
(
'
data-title
'
)).
toEqual
(
user
.
name
);
});
});
it
(
'
should render image alt
'
,
()
=>
{
[].
forEach
.
call
(
userItems
,
(
item
,
index
)
=>
{
const
user
=
vm
.
store
.
users
[
index
];
const
img
=
item
.
querySelector
(
'
img
'
);
expect
(
img
.
getAttribute
(
'
alt
'
)).
toEqual
(
vm
.
assigneeAlt
(
user
.
name
));
});
});
it
(
'
should render image
'
,
()
=>
{
[].
forEach
.
call
(
userItems
,
(
item
,
index
)
=>
{
const
user
=
vm
.
store
.
users
[
index
];
const
img
=
item
.
querySelector
(
'
img
'
);
expect
(
img
.
getAttribute
(
'
src
'
)).
toEqual
(
user
.
avatarUrl
);
});
});
});
describe
(
'
userListMore
'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
createComponent
({
store
:
mockStore
,
});
el
=
vm
.
$el
;
});
it
(
'
should render user-list-more
'
,
()
=>
{
const
userListMore
=
el
.
querySelector
(
'
.user-list-more
'
);
expect
(
userListMore
).
toBeDefined
();
});
it
(
'
should toggle user-list-more
'
,
(
done
)
=>
{
const
button
=
el
.
querySelector
(
'
button
'
);
const
buttonContent
=
button
.
textContent
;
button
.
click
();
Vue
.
nextTick
(()
=>
{
expect
(
button
.
textContent
.
trim
()).
not
.
toEqual
(
buttonContent
);
done
();
});
});
it
(
'
should render show less
'
,
()
=>
{
const
button
=
el
.
querySelector
(
'
button
'
);
expect
(
button
.
textContent
.
trim
()).
toEqual
(
'
+ 1 more
'
);
});
describe
(
'
show more
'
,
()
=>
{
let
button
;
beforeEach
(()
=>
{
button
=
el
.
querySelector
(
'
button
'
);
});
it
(
'
should render show more
'
,
(
done
)
=>
{
button
.
click
();
Vue
.
nextTick
(()
=>
{
expect
(
button
.
textContent
.
trim
()).
toEqual
(
'
- show less
'
);
done
();
});
});
it
(
'
should render number of hidden assignees
'
,
(
done
)
=>
{
const
count
=
el
.
querySelectorAll
(
'
.user-item
'
).
length
;
button
.
click
();
Vue
.
nextTick
(()
=>
{
expect
(
el
.
querySelectorAll
(
'
.user-item
'
).
length
>
count
).
toEqual
(
true
);
done
();
});
});
});
});
});
});
spec/javascripts/vue_sidebar_assignees/expanded/no_assignee_spec.js
deleted
100644 → 0
View file @
7b935cc8
import
Vue
from
'
vue
'
;
import
noAssigneeComponent
from
'
~/vue_sidebar_assignees/components/expanded/no_assignee
'
;
import
VueSpecHelper
from
'
../../helpers/vue_spec_helper
'
;
describe
(
'
NoAssignee
'
,
()
=>
{
const
mockStore
=
{
addCurrentUser
:
()
=>
{},
};
const
createComponent
=
props
=>
VueSpecHelper
.
createComponent
(
Vue
,
noAssigneeComponent
,
props
);
describe
(
'
methods
'
,
()
=>
{
describe
(
'
assignSelf
'
,
()
=>
{
it
(
'
should call addCurrentUser in store
'
,
()
=>
{
spyOn
(
mockStore
,
'
addCurrentUser
'
).
and
.
callThrough
();
const
vm
=
createComponent
({
store
:
mockStore
,
});
vm
.
assignSelf
();
expect
(
mockStore
.
addCurrentUser
).
toHaveBeenCalled
();
});
});
});
describe
(
'
template
'
,
()
=>
{
it
(
'
should call addCurrentUser when button is clicked
'
,
()
=>
{
spyOn
(
mockStore
,
'
addCurrentUser
'
).
and
.
callThrough
();
const
vm
=
createComponent
({
store
:
mockStore
,
});
const
el
=
vm
.
$el
;
const
button
=
el
.
querySelector
(
'
button
'
);
button
.
click
();
expect
(
mockStore
.
addCurrentUser
).
toHaveBeenCalled
();
});
});
});
spec/javascripts/vue_sidebar_assignees/expanded/single_assignee_spec.js
deleted
100644 → 0
View file @
7b935cc8
import
Vue
from
'
vue
'
;
import
singleAssigneeComponent
from
'
~/vue_sidebar_assignees/components/expanded/single_assignee
'
;
import
VueSpecHelper
from
'
../../helpers/vue_spec_helper
'
;
import
{
mockUser
,
mockUser2
}
from
'
../mock_data
'
;
describe
(
'
SingleAssignee
'
,
()
=>
{
const
mockStore
=
{
users
:
[
mockUser
],
rootPath
:
'
rootPath
'
,
};
const
createComponent
=
props
=>
VueSpecHelper
.
createComponent
(
Vue
,
singleAssigneeComponent
,
props
);
describe
(
'
computed
'
,
()
=>
{
describe
(
'
user
'
,
()
=>
{
it
(
'
should return first user
'
,
()
=>
{
const
newMockStore
=
Object
.
assign
({},
mockStore
);
newMockStore
.
users
.
push
(
mockUser2
);
const
vm
=
createComponent
({
store
:
newMockStore
,
});
expect
(
vm
.
user
).
toEqual
(
newMockStore
.
users
[
0
]);
});
});
describe
(
'
userUrl
'
,
()
=>
{
it
(
'
should return url
'
,
()
=>
{
const
vm
=
createComponent
({
store
:
mockStore
,
});
expect
(
vm
.
userUrl
).
toEqual
(
`
${
mockStore
.
rootPath
}${
mockStore
.
users
[
0
].
username
}
`
);
});
});
describe
(
'
username
'
,
()
=>
{
it
(
'
should return username
'
,
()
=>
{
const
vm
=
createComponent
({
store
:
mockStore
,
});
expect
(
vm
.
username
).
toEqual
(
`@
${
mockStore
.
users
[
0
].
username
}
`
);
});
});
describe
(
'
avatarAlt
'
,
()
=>
{
it
(
'
should return alt text
'
,
()
=>
{
const
vm
=
createComponent
({
store
:
mockStore
,
});
expect
(
vm
.
avatarAlt
).
toEqual
(
`
${
mockStore
.
users
[
0
].
name
}
's avatar`
);
});
});
});
describe
(
'
template
'
,
()
=>
{
let
vm
;
let
el
;
beforeEach
(()
=>
{
vm
=
createComponent
({
store
:
mockStore
,
});
el
=
vm
.
$el
;
});
it
(
'
should load the userUrl in href
'
,
()
=>
{
const
link
=
el
.
querySelector
(
'
a
'
);
expect
(
link
.
href
).
toEqual
(
`
${
window
.
location
.
origin
}
/
${
vm
.
userUrl
}
`
);
});
it
(
'
should load the avatarAlt
'
,
()
=>
{
const
img
=
el
.
querySelector
(
'
img
'
);
expect
(
img
.
alt
).
toEqual
(
vm
.
avatarAlt
);
});
it
(
'
should load the avatar image
'
,
()
=>
{
const
img
=
el
.
querySelector
(
'
img
'
);
expect
(
img
.
src
).
toEqual
(
vm
.
user
.
avatarUrl
);
});
it
(
'
should load the user
\'
s name
'
,
()
=>
{
const
name
=
el
.
querySelector
(
'
.author
'
);
expect
(
name
.
textContent
).
toEqual
(
vm
.
user
.
name
);
});
it
(
'
should load the user
\'
s username
'
,
()
=>
{
const
username
=
el
.
querySelector
(
'
.username
'
);
expect
(
username
.
textContent
).
toEqual
(
vm
.
username
);
});
});
});
spec/javascripts/vue_sidebar_assignees/mock_data.js
deleted
100644 → 0
View file @
7b935cc8
const
mockUser
=
{
id
:
1
,
name
:
'
Clark Kent
'
,
username
:
'
superman
'
,
avatarUrl
:
'
https://superman.com/avatar.png
'
,
};
const
mockUser2
=
{
id
:
2
,
name
:
'
Bruce Wayne
'
,
username
:
'
batman
'
,
avatarUrl
:
'
https://batman.com/avatar.png
'
,
};
const
mockUser3
=
{
id
:
3
,
name
:
'
Tony Stark
'
,
username
:
'
ironman
'
,
avatarUrl
:
'
https://ironman.com/avatar.png
'
,
};
module
.
exports
=
{
mockUser
,
mockUser2
,
mockUser3
,
};
spec/javascripts/vue_sidebar_assignees/services/sidebar_assignees_service_spec.js
deleted
100644 → 0
View file @
7b935cc8
import
SidebarAssigneesService
from
'
~/vue_sidebar_assignees/services/sidebar_assignees_service
'
;
describe
(
'
SidebarAssigneesService
'
,
()
=>
{
let
service
;
beforeEach
(()
=>
{
service
=
new
SidebarAssigneesService
(
''
,
'
field
'
);
});
describe
(
'
constructor
'
,
()
=>
{
it
(
'
should save field
'
,
()
=>
{
expect
(
service
.
field
).
toEqual
(
'
field
'
);
});
it
(
'
should save sidebarAssigneeResource
'
,
()
=>
{
expect
(
service
.
sidebarAssigneeResource
).
toBeDefined
();
});
});
describe
(
'
update
'
,
()
=>
{
it
(
'
should call vue resource update
'
,
(
done
)
=>
{
const
userIds
=
[
1
,
2
,
3
];
spyOn
(
service
.
sidebarAssigneeResource
,
'
update
'
).
and
.
callFake
((
o
)
=>
{
expect
(
o
.
field
).
toEqual
(
userIds
);
done
();
});
service
.
update
(
userIds
);
});
});
});
spec/javascripts/vue_sidebar_assignees/stores/sidebar_assignees_store_spec.js
deleted
100644 → 0
View file @
7b935cc8
import
'
~/flash
'
;
import
SidebarAssigneesStore
from
'
~/vue_sidebar_assignees/stores/sidebar_assignees_store
'
;
import
{
mockUser
}
from
'
../mock_data
'
;
describe
(
'
SidebarAssigneesStore
'
,
()
=>
{
let
params
;
beforeEach
(()
=>
{
params
=
{
currentUserId
:
1
,
service
:
{
update
:
()
=>
{},
},
rootPath
:
'
rootPath
'
,
editable
:
true
,
};
});
const
getStore
=
p
=>
new
SidebarAssigneesStore
(
p
);
it
(
'
should store information
'
,
()
=>
{
const
store
=
getStore
(
params
);
Object
.
keys
(
params
).
forEach
((
k
)
=>
{
expect
(
store
[
k
]).
toEqual
(
params
[
k
]);
});
});
describe
(
'
addUser
'
,
()
=>
{
let
store
;
beforeEach
(()
=>
{
store
=
getStore
(
params
);
});
it
(
'
should add user to users array
'
,
()
=>
{
expect
(
store
.
users
.
length
).
toEqual
(
0
);
store
.
addUser
(
mockUser
);
expect
(
store
.
users
.
length
).
toEqual
(
1
);
expect
(
store
.
users
[
0
]).
toEqual
(
mockUser
);
expect
(
store
.
saved
).
toEqual
(
false
);
});
it
(
'
should set saved flag to true if second param is true
'
,
()
=>
{
store
.
addUser
(
mockUser
,
true
);
expect
(
store
.
saved
).
toEqual
(
true
);
});
});
describe
(
'
addCurrentUser
'
,
()
=>
{
let
store
;
beforeEach
(()
=>
{
store
=
getStore
(
params
);
spyOn
(
store
,
'
saveUsers
'
).
and
.
callFake
(()
=>
{});
});
it
(
'
should add current user to users array
'
,
()
=>
{
spyOn
(
store
,
'
addUser
'
).
and
.
callThrough
();
store
.
addCurrentUser
();
expect
(
store
.
addUser
).
toHaveBeenCalledWith
({
id
:
1
,
});
});
it
(
'
should call saveUsers
'
,
()
=>
{
store
.
addCurrentUser
();
expect
(
store
.
saveUsers
).
toHaveBeenCalled
();
});
});
describe
(
'
removeUser
'
,
()
=>
{
let
store
;
beforeEach
(()
=>
{
store
=
getStore
(
params
);
store
.
addUser
(
mockUser
,
true
);
});
it
(
'
should remove user from users array
'
,
()
=>
{
expect
(
store
.
users
.
length
).
toEqual
(
1
);
store
.
removeUser
(
mockUser
.
id
);
expect
(
store
.
users
.
length
).
toEqual
(
0
);
});
it
(
'
should set saved flag to false
'
,
()
=>
{
expect
(
store
.
saved
).
toEqual
(
true
);
store
.
removeUser
(
mockUser
.
id
);
expect
(
store
.
saved
).
toEqual
(
false
);
});
});
describe
(
'
saveUsers
'
,
()
=>
{
it
(
'
should save unassigned user when there are no users2
'
,
()
=>
{
const
spyParams
=
Object
.
assign
({},
params
);
const
store
=
getStore
(
spyParams
);
spyOn
(
spyParams
.
service
,
'
update
'
).
and
.
callFake
(()
=>
new
Promise
(
resolve
=>
resolve
({
data
:
{
assignees
:
[],
},
}),
),
);
store
.
saveUsers
();
expect
(
spyParams
.
service
.
update
).
toHaveBeenCalledWith
([
0
]);
expect
(
store
.
users
.
length
).
toEqual
(
0
);
});
it
(
'
should catch error
'
,
()
=>
{
const
spyParams
=
Object
.
assign
({},
params
);
const
store
=
getStore
(
spyParams
);
spyOn
(
window
,
'
Flash
'
).
and
.
callThrough
();
spyOn
(
spyParams
.
service
,
'
update
'
).
and
.
callFake
(()
=>
new
Promise
((
resolve
,
reject
)
=>
reject
()),
);
store
.
saveUsers
();
setTimeout
(()
=>
{
expect
(
window
.
Flash
).
toHaveBeenCalled
();
});
});
it
(
'
should save unassigned user when there are no users
'
,
()
=>
{
const
spyParams
=
Object
.
assign
({},
params
);
const
store
=
getStore
(
spyParams
);
spyOn
(
spyParams
.
service
,
'
update
'
).
and
.
callFake
(()
=>
new
Promise
(
resolve
=>
resolve
({
data
:
{
assignees
:
[],
},
}),
),
);
store
.
saveUsers
();
expect
(
spyParams
.
service
.
update
).
toHaveBeenCalledWith
([
0
]);
expect
(
store
.
users
.
length
).
toEqual
(
0
);
});
});
});
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