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
95e04916
Commit
95e04916
authored
Apr 27, 2017
by
Clement Ho
Committed by
Jacob Schatz
Apr 27, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add multiple assignee support to sidebar
parent
8cce7368
Changes
46
Hide whitespace changes
Inline
Side-by-side
Showing
46 changed files
with
1404 additions
and
651 deletions
+1404
-651
app/assets/javascripts/gl_dropdown.js
app/assets/javascripts/gl_dropdown.js
+23
-8
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/issue_status_select.js
app/assets/javascripts/issue_status_select.js
+2
-2
app/assets/javascripts/labels_select.js
app/assets/javascripts/labels_select.js
+4
-1
app/assets/javascripts/main.js
app/assets/javascripts/main.js
+0
-1
app/assets/javascripts/milestone_select.js
app/assets/javascripts/milestone_select.js
+4
-1
app/assets/javascripts/sidebar/components/assignees/assignee_title.js
...avascripts/sidebar/components/assignees/assignee_title.js
+41
-0
app/assets/javascripts/sidebar/components/assignees/assignees.js
...ets/javascripts/sidebar/components/assignees/assignees.js
+217
-0
app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js
...scripts/sidebar/components/assignees/sidebar_assignees.js
+83
-0
app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js
...ripts/sidebar/components/time_tracking/collapsed_state.js
+97
-0
app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js
...ripts/sidebar/components/time_tracking/comparison_pane.js
+98
-0
app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.js
...ts/sidebar/components/time_tracking/estimate_only_pane.js
+17
-0
app/assets/javascripts/sidebar/components/time_tracking/help_state.js
...avascripts/sidebar/components/time_tracking/help_state.js
+44
-0
app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js
...ipts/sidebar/components/time_tracking/no_tracking_pane.js
+10
-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
+163
-0
app/assets/javascripts/sidebar/event_hub.js
app/assets/javascripts/sidebar/event_hub.js
+3
-0
app/assets/javascripts/sidebar/services/sidebar_service.js
app/assets/javascripts/sidebar/services/sidebar_service.js
+28
-0
app/assets/javascripts/sidebar/sidebar_bundle.js
app/assets/javascripts/sidebar/sidebar_bundle.js
+21
-0
app/assets/javascripts/sidebar/sidebar_mediator.js
app/assets/javascripts/sidebar/sidebar_mediator.js
+38
-0
app/assets/javascripts/sidebar/stores/sidebar_store.js
app/assets/javascripts/sidebar/stores/sidebar_store.js
+52
-0
app/assets/javascripts/subbable_resource.js
app/assets/javascripts/subbable_resource.js
+0
-51
app/assets/javascripts/subscription_select.js
app/assets/javascripts/subscription_select.js
+2
-2
app/assets/javascripts/users_select.js
app/assets/javascripts/users_select.js
+212
-54
app/assets/javascripts/weight_select.js
app/assets/javascripts/weight_select.js
+4
-1
app/assets/stylesheets/framework/avatar.scss
app/assets/stylesheets/framework/avatar.scss
+1
-1
app/assets/stylesheets/framework/dropdowns.scss
app/assets/stylesheets/framework/dropdowns.scss
+7
-5
app/assets/stylesheets/framework/variables.scss
app/assets/stylesheets/framework/variables.scss
+0
-1
app/assets/stylesheets/pages/diff.scss
app/assets/stylesheets/pages/diff.scss
+1
-8
app/assets/stylesheets/pages/issuable.scss
app/assets/stylesheets/pages/issuable.scss
+103
-9
app/controllers/projects/issues_controller.rb
app/controllers/projects/issues_controller.rb
+1
-1
app/serializers/issue_entity.rb
app/serializers/issue_entity.rb
+1
-1
app/views/shared/issuable/_participants.html.haml
app/views/shared/issuable/_participants.html.haml
+4
-4
app/views/shared/issuable/_sidebar.html.haml
app/views/shared/issuable/_sidebar.html.haml
+60
-10
config/webpack.config.js
config/webpack.config.js
+2
-2
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
No files found.
app/assets/javascripts/gl_dropdown.js
View file @
95e04916
...
...
@@ -255,7 +255,8 @@ GitLabDropdown = (function() {
}
};
// Remote data
})(
this
)
})(
this
),
instance
:
this
,
});
}
}
...
...
@@ -269,6 +270,7 @@ GitLabDropdown = (function() {
remote
:
this
.
options
.
filterRemote
,
query
:
this
.
options
.
data
,
keys
:
searchFields
,
instance
:
this
,
elements
:
(
function
(
_this
)
{
return
function
()
{
selector
=
'
.dropdown-content li:not(
'
+
NON_SELECTABLE_CLASSES
+
'
)
'
;
...
...
@@ -343,21 +345,26 @@ GitLabDropdown = (function() {
}
this
.
dropdown
.
on
(
"
click
"
,
selector
,
function
(
e
)
{
var
$el
,
selected
,
selectedObj
,
isMarking
;
$el
=
$
(
this
);
$el
=
$
(
e
.
currentTarget
);
selected
=
self
.
rowClicked
(
$el
);
selectedObj
=
selected
?
selected
[
0
]
:
null
;
isMarking
=
selected
?
selected
[
1
]
:
null
;
if
(
self
.
options
.
clicked
)
{
self
.
options
.
clicked
(
selectedObj
,
$el
,
e
,
isMarking
);
if
(
this
.
options
.
clicked
)
{
this
.
options
.
clicked
.
call
(
this
,
{
selectedObj
,
$el
,
e
,
isMarking
,
});
}
// Update label right after all modifications in dropdown has been done
if
(
self
.
options
.
toggleLabel
)
{
self
.
updateLabel
(
selectedObj
,
$el
,
self
);
if
(
this
.
options
.
toggleLabel
)
{
this
.
updateLabel
(
selectedObj
,
$el
,
this
);
}
$el
.
trigger
(
'
blur
'
);
});
}
.
bind
(
this
)
);
}
}
...
...
@@ -724,6 +731,7 @@ GitLabDropdown = (function() {
if
(
this
.
options
.
inputId
!=
null
)
{
$input
.
attr
(
'
id
'
,
this
.
options
.
inputId
);
}
return
this
.
dropdown
.
before
(
$input
);
};
...
...
@@ -844,7 +852,14 @@ GitLabDropdown = (function() {
if
(
instance
==
null
)
{
instance
=
null
;
}
return
$
(
this
.
el
).
find
(
"
.dropdown-toggle-text
"
).
text
(
this
.
options
.
toggleLabel
(
selected
,
el
,
instance
));
let
toggleText
=
this
.
options
.
toggleLabel
(
selected
,
el
,
instance
);
if
(
this
.
options
.
updateLabel
)
{
// Option to override the dropdown label text
toggleText
=
this
.
options
.
updateLabel
;
}
return
$
(
this
.
el
).
find
(
"
.dropdown-toggle-text
"
).
text
(
toggleText
);
};
GitLabDropdown
.
prototype
.
clearField
=
function
(
field
,
isInput
)
{
...
...
app/assets/javascripts/issuable/issuable_bundle.js
deleted
100644 → 0
View file @
8cce7368
require
(
'
./time_tracking/time_tracking_bundle
'
);
app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js
deleted
100644 → 0
View file @
8cce7368
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 @
8cce7368
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 @
8cce7368
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 @
8cce7368
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 @
8cce7368
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 @
8cce7368
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 @
8cce7368
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 @
8cce7368
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/issue_status_select.js
View file @
95e04916
...
...
@@ -19,8 +19,8 @@
return
label
;
};
})(
this
),
clicked
:
function
(
item
,
$el
,
e
)
{
return
e
.
preventDefault
();
clicked
:
function
(
options
)
{
return
options
.
e
.
preventDefault
();
},
id
:
function
(
obj
,
el
)
{
return
$
(
el
).
data
(
"
id
"
);
...
...
app/assets/javascripts/labels_select.js
View file @
95e04916
...
...
@@ -330,7 +330,10 @@
},
multiSelect
:
$dropdown
.
hasClass
(
'
js-multiselect
'
),
vue
:
$dropdown
.
hasClass
(
'
js-issue-board-sidebar
'
),
clicked
:
function
(
label
,
$el
,
e
,
isMarking
)
{
clicked
:
function
(
options
)
{
const
{
$el
,
e
,
isMarking
}
=
options
;
const
label
=
options
.
selectedObj
;
var
isIssueIndex
,
isMRIndex
,
page
,
boardsModel
;
page
=
$
(
'
body
'
).
data
(
'
page
'
);
...
...
app/assets/javascripts/main.js
View file @
95e04916
...
...
@@ -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/milestone_select.js
View file @
95e04916
...
...
@@ -144,7 +144,10 @@ import Vue from 'vue';
return
true
;
},
clicked
:
function
(
selected
,
$el
,
e
)
{
clicked
:
function
(
options
)
{
const
{
$el
,
e
}
=
options
;
let
selected
=
options
.
selectedObj
;
var
data
,
isIssueIndex
,
isMRIndex
,
page
,
boardsStore
;
if
(
!
selected
)
return
;
page
=
$
(
'
body
'
).
data
(
'
page
'
);
...
...
app/assets/javascripts/sidebar/components/assignees/assignee_title.js
0 → 100644
View file @
95e04916
export
default
{
name
:
'
AssigneeTitle
'
,
props
:
{
loading
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
numberOfAssignees
:
{
type
:
Number
,
required
:
true
,
},
editable
:
{
type
:
Boolean
,
required
:
true
,
},
},
computed
:
{
assigneeTitle
()
{
const
assignees
=
this
.
numberOfAssignees
;
return
assignees
>
1
?
`
${
assignees
}
Assignees`
:
'
Assignee
'
;
},
},
template
:
`
<div class="title hide-collapsed">
{{assigneeTitle}}
<i
v-if="loading"
aria-hidden="true"
class="fa fa-spinner fa-spin block-loading"
/>
<a
v-if="editable"
class="edit-link pull-right"
href="#"
>
Edit
</a>
</div>
`
,
};
app/assets/javascripts/sidebar/components/assignees/assignees.js
0 → 100644
View file @
95e04916
export
default
{
name
:
'
Assignees
'
,
data
()
{
return
{
defaultRenderCount
:
5
,
defaultMaxCounter
:
99
,
showLess
:
true
,
};
},
props
:
{
rootPath
:
{
type
:
String
,
required
:
true
,
},
users
:
{
type
:
Array
,
required
:
true
,
},
},
computed
:
{
firstUser
()
{
return
this
.
users
[
0
];
},
hasMoreThanTwoAssignees
()
{
return
this
.
users
.
length
>
2
;
},
hasMoreThanOneAssignee
()
{
return
this
.
users
.
length
>
1
;
},
hasAssignees
()
{
return
this
.
users
.
length
>
0
;
},
hasNoUsers
()
{
return
!
this
.
users
.
length
;
},
hasOneUser
()
{
return
this
.
users
.
length
===
1
;
},
renderShowMoreSection
()
{
return
this
.
users
.
length
>
this
.
defaultRenderCount
;
},
numberOfHiddenAssignees
()
{
return
this
.
users
.
length
-
this
.
defaultRenderCount
;
},
isHiddenAssignees
()
{
return
this
.
numberOfHiddenAssignees
>
0
;
},
hiddenAssigneesLabel
()
{
return
`+
${
this
.
numberOfHiddenAssignees
}
more`
;
},
collapsedTooltipTitle
()
{
const
maxRender
=
Math
.
min
(
this
.
defaultRenderCount
,
this
.
users
.
length
);
const
renderUsers
=
this
.
users
.
slice
(
0
,
maxRender
);
const
names
=
renderUsers
.
map
(
u
=>
u
.
name
);
if
(
this
.
users
.
length
>
maxRender
)
{
names
.
push
(
`+
${
this
.
users
.
length
-
maxRender
}
more`
);
}
return
names
.
join
(
'
,
'
);
},
sidebarAvatarCounter
()
{
let
counter
=
`+
${
this
.
users
.
length
-
1
}
`
;
if
(
this
.
users
.
length
>
this
.
defaultMaxCounter
)
{
counter
=
`
${
this
.
defaultMaxCounter
}
+`
;
}
return
counter
;
},
},
methods
:
{
assignSelf
()
{
this
.
$emit
(
'
assign-self
'
);
},
toggleShowLess
()
{
this
.
showLess
=
!
this
.
showLess
;
},
renderAssignee
(
index
)
{
return
!
this
.
showLess
||
(
index
<
this
.
defaultRenderCount
&&
this
.
showLess
);
},
avatarUrl
(
user
)
{
return
user
.
avatarUrl
||
user
.
avatar_url
;
},
assigneeUrl
(
user
)
{
return
`
${
this
.
rootPath
}${
user
.
username
}
`
;
},
assigneeAlt
(
user
)
{
return
`
${
user
.
name
}
's avatar`
;
},
assigneeUsername
(
user
)
{
return
`@
${
user
.
username
}
`
;
},
shouldRenderCollapsedAssignee
(
index
)
{
const
firstTwo
=
this
.
users
.
length
<=
2
&&
index
<=
2
;
return
index
===
0
||
firstTwo
;
},
},
template
:
`
<div>
<div
class="sidebar-collapsed-icon sidebar-collapsed-user"
:class="{ 'multiple-users': hasMoreThanOneAssignee, 'has-tooltip': hasAssignees }"
data-container="body"
data-placement="left"
:title="collapsedTooltipTitle"
>
<i
v-if="hasNoUsers"
aria-label="No Assignee"
class="fa fa-user"
/>
<button
type="button"
class="btn-link"
v-for="(user, index) in users"
v-if="shouldRenderCollapsedAssignee(index)"
>
<img
width="24"
class="avatar avatar-inline s24"
:alt="assigneeAlt(user)"
:src="avatarUrl(user)"
/>
<span class="author">
{{ user.name }}
</span>
</button>
<button
v-if="hasMoreThanTwoAssignees"
class="btn-link"
type="button"
>
<span
class="avatar-counter sidebar-avatar-counter"
>
{{ sidebarAvatarCounter }}
</span>
</button>
</div>
<div class="value hide-collapsed">
<template v-if="hasNoUsers">
<span class="assign-yourself no-value">
No assignee -
<button
type="button"
class="btn-link"
@click="assignSelf"
>
assign yourself
</button>
</span>
</template>
<template v-else-if="hasOneUser">
<a
class="author_link bold"
:href="assigneeUrl(firstUser)"
>
<img
width="32"
class="avatar avatar-inline s32"
:alt="assigneeAlt(firstUser)"
:src="avatarUrl(firstUser)"
/>
<span class="author">
{{ firstUser.name }}
</span>
<span class="username">
{{ assigneeUsername(firstUser) }}
</span>
</a>
</template>
<template v-else>
<div class="user-list">
<div
class="user-item"
v-for="(user, index) in users"
v-if="renderAssignee(index)"
>
<a
class="user-link has-tooltip"
data-placement="bottom"
:href="assigneeUrl(user)"
:data-title="user.name"
>
<img
width="32"
class="avatar avatar-inline s32"
:alt="assigneeAlt(user)"
:src="avatarUrl(user)"
/>
</a>
</div>
</div>
<div
v-if="renderShowMoreSection"
class="user-list-more"
>
<button
type="button"
class="btn-link"
@click="toggleShowLess"
>
<template v-if="showLess">
{{ hiddenAssigneesLabel }}
</template>
<template v-else>
- show less
</template>
</button>
</div>
</template>
</div>
</div>
`
,
};
app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js
0 → 100644
View file @
95e04916
/* global Flash */
import
AssigneeTitle
from
'
./assignee_title
'
;
import
Assignees
from
'
./assignees
'
;
import
Store
from
'
../../stores/sidebar_store
'
;
import
Mediator
from
'
../../sidebar_mediator
'
;
import
eventHub
from
'
../../event_hub
'
;
export
default
{
name
:
'
SidebarAssignees
'
,
data
()
{
return
{
mediator
:
new
Mediator
(),
store
:
new
Store
(),
loading
:
false
,
field
:
''
,
};
},
components
:
{
'
assignee-title
'
:
AssigneeTitle
,
assignees
:
Assignees
,
},
methods
:
{
assignSelf
()
{
// Notify gl dropdown that we are now assigning to current user
this
.
$el
.
parentElement
.
dispatchEvent
(
new
Event
(
'
assignYourself
'
));
this
.
mediator
.
assignYourself
();
this
.
saveAssignees
();
},
saveAssignees
()
{
this
.
loading
=
true
;
function
setLoadingFalse
()
{
this
.
loading
=
false
;
}
this
.
mediator
.
saveAssignees
(
this
.
field
)
.
then
(
setLoadingFalse
.
bind
(
this
))
.
catch
(()
=>
{
setLoadingFalse
();
return
new
Flash
(
'
Error occurred when saving assignees
'
);
});
},
},
created
()
{
this
.
removeAssignee
=
this
.
store
.
removeAssignee
.
bind
(
this
.
store
);
this
.
addAssignee
=
this
.
store
.
addAssignee
.
bind
(
this
.
store
);
this
.
removeAllAssignees
=
this
.
store
.
removeAllAssignees
.
bind
(
this
.
store
);
// Get events from glDropdown
eventHub
.
$on
(
'
sidebar.removeAssignee
'
,
this
.
removeAssignee
);
eventHub
.
$on
(
'
sidebar.addAssignee
'
,
this
.
addAssignee
);
eventHub
.
$on
(
'
sidebar.removeAllAssignees
'
,
this
.
removeAllAssignees
);
eventHub
.
$on
(
'
sidebar.saveAssignees
'
,
this
.
saveAssignees
);
},
beforeDestroy
()
{
eventHub
.
$off
(
'
sidebar.removeAssignee
'
,
this
.
removeAssignee
);
eventHub
.
$off
(
'
sidebar.addAssignee
'
,
this
.
addAssignee
);
eventHub
.
$off
(
'
sidebar.removeAllAssignees
'
,
this
.
removeAllAssignees
);
eventHub
.
$off
(
'
sidebar.saveAssignees
'
,
this
.
saveAssignees
);
},
beforeMount
()
{
this
.
field
=
this
.
$el
.
dataset
.
field
;
},
template
:
`
<div>
<assignee-title
:number-of-assignees="store.assignees.length"
:loading="loading"
:editable="store.editable"
/>
<assignees
class="value"
:root-path="store.rootPath"
:users="store.assignees"
@assign-self="assignSelf"
/>
</div>
`
,
};
app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js
0 → 100644
View file @
95e04916
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
,
default
:
''
,
},
timeEstimateHumanReadable
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
},
computed
:
{
timeSpent
()
{
return
this
.
abbreviateTime
(
this
.
timeSpentHumanReadable
);
},
timeEstimate
()
{
return
this
.
abbreviateTime
(
this
.
timeEstimateHumanReadable
);
},
divClass
()
{
if
(
this
.
showComparisonState
)
{
return
'
compare
'
;
}
else
if
(
this
.
showEstimateOnlyState
)
{
return
'
estimate-only
'
;
}
else
if
(
this
.
showSpentOnlyState
)
{
return
'
spend-only
'
;
}
else
if
(
this
.
showNoTimeTrackingState
)
{
return
'
no-tracking
'
;
}
return
''
;
},
spanClass
()
{
if
(
this
.
showComparisonState
)
{
return
''
;
}
else
if
(
this
.
showEstimateOnlyState
||
this
.
showSpentOnlyState
)
{
return
'
bold
'
;
}
else
if
(
this
.
showNoTimeTrackingState
)
{
return
'
no-value
'
;
}
return
''
;
},
text
()
{
if
(
this
.
showComparisonState
)
{
return
`
${
this
.
timeSpent
}
/
${
this
.
timeEstimate
}
`
;
}
else
if
(
this
.
showEstimateOnlyState
)
{
return
`-- /
${
this
.
timeEstimate
}
`
;
}
else
if
(
this
.
showSpentOnlyState
)
{
return
`
${
this
.
timeSpent
}
/ --`
;
}
else
if
(
this
.
showNoTimeTrackingState
)
{
return
'
None
'
;
}
return
''
;
},
},
methods
:
{
abbreviateTime
(
timeStr
)
{
return
gl
.
utils
.
prettyTime
.
abbreviateTime
(
timeStr
);
},
},
template
:
`
<div class="sidebar-collapsed-icon">
${
stopwatchSvg
}
<div class="time-tracking-collapsed-summary">
<div :class="divClass">
<span :class="spanClass">
{{ text }}
</span>
</div>
</div>
</div>
`
,
};
app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js
0 → 100644
View file @
95e04916
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 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 @
95e04916
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 @
95e04916
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 @
95e04916
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 @
95e04916
import
'
~/smart_interval
'
;
import
timeTracker
from
'
./time_tracker
'
;
import
Store
from
'
../../stores/sidebar_store
'
;
import
Mediator
from
'
../../sidebar_mediator
'
;
export
default
{
data
()
{
return
{
mediator
:
new
Mediator
(),
store
:
new
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
)
{
this
.
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 @
95e04916
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 @
95e04916
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
,
},
time_spent
:
{
type
:
Number
,
required
:
true
,
},
human_time_estimate
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
human_time_spent
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
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/event_hub.js
0 → 100644
View file @
95e04916
import
Vue
from
'
vue
'
;
export
default
new
Vue
();
app/assets/javascripts/sidebar/services/sidebar_service.js
0 → 100644
View file @
95e04916
import
Vue
from
'
vue
'
;
import
VueResource
from
'
vue-resource
'
;
Vue
.
use
(
VueResource
);
export
default
class
SidebarService
{
constructor
(
endpoint
)
{
if
(
!
SidebarService
.
singleton
)
{
this
.
endpoint
=
endpoint
;
SidebarService
.
singleton
=
this
;
}
return
SidebarService
.
singleton
;
}
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 @
95e04916
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
'
,
()
=>
{
const
mediator
=
new
Mediator
(
gl
.
sidebarOptions
);
mediator
.
fetch
();
const
sidebarAssigneesEl
=
document
.
querySelector
(
'
#js-vue-sidebar-assignees
'
);
// Only create the sidebarAssignees vue app if it is found in the DOM
// We currently do not use sidebarAssignees for the MR page
if
(
sidebarAssigneesEl
)
{
new
Vue
(
sidebarAssignees
).
$mount
(
sidebarAssigneesEl
);
}
new
Vue
(
sidebarTimeTracking
).
$mount
(
'
#issuable-time-tracker
'
);
});
app/assets/javascripts/sidebar/sidebar_mediator.js
0 → 100644
View file @
95e04916
/* global Flash */
import
Service
from
'
./services/sidebar_service
'
;
import
Store
from
'
./stores/sidebar_store
'
;
export
default
class
SidebarMediator
{
constructor
(
options
)
{
if
(
!
SidebarMediator
.
singleton
)
{
this
.
store
=
new
Store
(
options
);
this
.
service
=
new
Service
(
options
.
endpoint
);
SidebarMediator
.
singleton
=
this
;
}
return
SidebarMediator
.
singleton
;
}
assignYourself
()
{
this
.
store
.
addAssignee
(
this
.
store
.
currentUser
);
}
saveAssignees
(
field
)
{
const
selected
=
this
.
store
.
assignees
.
map
((
u
)
=>
u
.
id
);
// If there are no ids, that means we have to unassign (which is id = 0)
// And it only accepts an array, hence [0]
return
this
.
service
.
update
(
field
,
selected
.
length
===
0
?
[
0
]
:
selected
);
}
fetch
()
{
this
.
service
.
get
()
.
then
((
response
)
=>
{
const
data
=
response
.
json
();
this
.
store
.
processAssigneeData
(
data
);
this
.
store
.
processTimeTrackingData
(
data
);
})
.
catch
(()
=>
new
Flash
(
'
Error occured when fetching sidebar data
'
));
}
}
app/assets/javascripts/sidebar/stores/sidebar_store.js
0 → 100644
View file @
95e04916
export
default
class
SidebarStore
{
constructor
(
store
)
{
if
(
!
SidebarStore
.
singleton
)
{
const
{
currentUser
,
rootPath
,
editable
}
=
store
;
this
.
currentUser
=
currentUser
;
this
.
rootPath
=
rootPath
;
this
.
editable
=
editable
;
this
.
timeEstimate
=
0
;
this
.
totalTimeSpent
=
0
;
this
.
humanTimeEstimate
=
''
;
this
.
humanTimeSpent
=
''
;
this
.
assignees
=
[];
SidebarStore
.
singleton
=
this
;
}
return
SidebarStore
.
singleton
;
}
processAssigneeData
(
data
)
{
if
(
data
.
assignees
)
{
this
.
assignees
=
data
.
assignees
;
}
}
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
;
}
addAssignee
(
assignee
)
{
if
(
!
this
.
findAssignee
(
assignee
))
{
this
.
assignees
.
push
(
assignee
);
}
}
findAssignee
(
findAssignee
)
{
return
this
.
assignees
.
filter
(
assignee
=>
assignee
.
id
===
findAssignee
.
id
)[
0
];
}
removeAssignee
(
removeAssignee
)
{
if
(
removeAssignee
)
{
this
.
assignees
=
this
.
assignees
.
filter
(
assignee
=>
assignee
.
id
!==
removeAssignee
.
id
);
}
}
removeAllAssignees
()
{
this
.
assignees
=
[];
}
}
app/assets/javascripts/subbable_resource.js
deleted
100644 → 0
View file @
8cce7368
(()
=>
{
/*
* 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/subscription_select.js
View file @
95e04916
...
...
@@ -19,8 +19,8 @@
return
label
;
};
})(
this
),
clicked
:
function
(
item
,
$el
,
e
)
{
return
e
.
preventDefault
();
clicked
:
function
(
options
)
{
return
options
.
e
.
preventDefault
();
},
id
:
function
(
obj
,
el
)
{
return
$
(
el
).
data
(
"
id
"
);
...
...
app/assets/javascripts/users_select.js
View file @
95e04916
...
...
@@ -3,6 +3,7 @@
/* global ListUser */
import
Vue
from
'
vue
'
;
import
eventHub
from
'
./sidebar/event_hub
'
;
(
function
()
{
var
bind
=
function
(
fn
,
me
)
{
return
function
()
{
return
fn
.
apply
(
me
,
arguments
);
};
},
...
...
@@ -53,6 +54,33 @@ import Vue from 'vue';
$collapsedSidebar
=
$block
.
find
(
'
.sidebar-collapsed-user
'
);
$loading
=
$block
.
find
(
'
.block-loading
'
).
fadeOut
();
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
();
}
// 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
);
});
}
var
getSelected
=
function
()
{
return
$selectbox
.
find
(
`input[name="
${
$dropdown
.
data
(
'
field-name
'
)}
"]`
)
.
map
((
index
,
input
)
=>
parseInt
(
input
.
value
,
10
))
.
get
();
};
var
updateIssueBoardsIssue
=
function
()
{
$loading
.
removeClass
(
'
hidden
'
).
fadeIn
();
gl
.
issueBoards
.
BoardsStore
.
detail
.
issue
.
update
(
$dropdown
.
attr
(
'
data-issue-update
'
))
...
...
@@ -93,6 +121,7 @@ import Vue from 'vue';
data
[
abilityName
].
assignee_id
=
selected
!=
null
?
selected
:
null
;
$loading
.
removeClass
(
'
hidden
'
).
fadeIn
();
$dropdown
.
trigger
(
'
loading.gl.dropdown
'
);
return
$
.
ajax
({
type
:
'
PUT
'
,
dataType
:
'
json
'
,
...
...
@@ -102,7 +131,6 @@ import Vue from 'vue';
var
user
;
$dropdown
.
trigger
(
'
loaded.gl.dropdown
'
);
$loading
.
fadeOut
();
$selectbox
.
hide
();
if
(
data
.
assignee
)
{
user
=
{
name
:
data
.
assignee
.
name
,
...
...
@@ -129,51 +157,90 @@ import Vue from 'vue';
var
isAuthorFilter
;
isAuthorFilter
=
$
(
'
.js-author-search
'
);
return
_this
.
users
(
term
,
options
,
function
(
users
)
{
var
anyUser
,
index
,
j
,
len
,
name
,
obj
,
showDivider
;
if
(
term
.
length
===
0
)
{
showDivider
=
0
;
if
(
firstUser
)
{
// Move current user to the front of the list
for
(
index
=
j
=
0
,
len
=
users
.
length
;
j
<
len
;
index
=
(
j
+=
1
))
{
obj
=
users
[
index
];
if
(
obj
.
username
===
firstUser
)
{
users
.
splice
(
index
,
1
);
users
.
unshift
(
obj
);
break
;
}
// GitLabDropdownFilter returns this.instance
// GitLabDropdownRemote returns this.options.instance
const
glDropdown
=
this
.
instance
||
this
.
options
.
instance
;
glDropdown
.
options
.
processData
(
term
,
users
,
callback
);
}.
bind
(
this
));
},
processData
:
function
(
term
,
users
,
callback
)
{
let
anyUser
;
let
index
;
let
j
;
let
len
;
let
name
;
let
obj
;
let
showDivider
;
if
(
term
.
length
===
0
)
{
showDivider
=
0
;
if
(
firstUser
)
{
// Move current user to the front of the list
for
(
index
=
j
=
0
,
len
=
users
.
length
;
j
<
len
;
index
=
(
j
+=
1
))
{
obj
=
users
[
index
];
if
(
obj
.
username
===
firstUser
)
{
users
.
splice
(
index
,
1
);
users
.
unshift
(
obj
);
break
;
}
}
if
(
showNullUser
)
{
showDivider
+=
1
;
users
.
unshift
({
beforeDivider
:
true
,
name
:
'
Unassigned
'
,
id
:
0
});
}
if
(
showAnyUser
)
{
showDivider
+=
1
;
name
=
showAnyUser
;
if
(
name
===
true
)
{
name
=
'
Any User
'
;
}
anyUser
=
{
beforeDivider
:
true
,
name
:
name
,
id
:
null
};
users
.
unshift
(
anyUser
);
}
if
(
showNullUser
)
{
showDivider
+=
1
;
users
.
unshift
({
beforeDivider
:
true
,
name
:
'
Unassigned
'
,
id
:
0
});
}
if
(
showAnyUser
)
{
showDivider
+=
1
;
name
=
showAnyUser
;
if
(
name
===
true
)
{
name
=
'
Any User
'
;
}
anyUser
=
{
beforeDivider
:
true
,
name
:
name
,
id
:
null
};
users
.
unshift
(
anyUser
);
}
if
(
showDivider
)
{
users
.
splice
(
showDivider
,
0
,
"
divider
"
);
users
.
splice
(
showDivider
,
0
,
'
divider
'
);
}
callback
(
users
);
if
(
showMenuAbove
)
{
$dropdown
.
data
(
'
glDropdown
'
).
positionMenuAbove
();
if
(
$dropdown
.
hasClass
(
'
js-multiselect
'
))
{
const
selected
=
getSelected
().
filter
(
i
=>
i
!==
0
);
if
(
selected
.
length
>
0
)
{
if
(
$dropdown
.
data
(
'
dropdown-header
'
))
{
showDivider
+=
1
;
users
.
splice
(
showDivider
,
0
,
{
header
:
$dropdown
.
data
(
'
dropdown-header
'
),
});
}
const
selectedUsers
=
users
.
filter
(
u
=>
selected
.
indexOf
(
u
.
id
)
!==
-
1
)
.
sort
((
a
,
b
)
=>
a
.
name
>
b
.
name
);
users
=
users
.
filter
(
u
=>
selected
.
indexOf
(
u
.
id
)
===
-
1
);
selectedUsers
.
forEach
((
selectedUser
)
=>
{
showDivider
+=
1
;
users
.
splice
(
showDivider
,
0
,
selectedUser
);
});
users
.
splice
(
showDivider
+
1
,
0
,
'
divider
'
);
}
}
});
}
callback
(
users
);
if
(
showMenuAbove
)
{
$dropdown
.
data
(
'
glDropdown
'
).
positionMenuAbove
();
}
},
filterable
:
true
,
filterRemote
:
true
,
...
...
@@ -182,7 +249,21 @@ import Vue from 'vue';
},
selectable
:
true
,
fieldName
:
$dropdown
.
data
(
'
field-name
'
),
toggleLabel
:
function
(
selected
,
el
)
{
toggleLabel
:
function
(
selected
,
el
,
glDropdown
)
{
const
inputValue
=
glDropdown
.
filterInput
.
val
();
if
(
this
.
multiSelect
&&
inputValue
===
''
)
{
// Remove non-users from the fullData array
const
users
=
glDropdown
.
fullData
.
filter
(
r
=>
typeof
r
===
'
object
'
&&
!
Object
.
prototype
.
hasOwnProperty
.
call
(
r
,
'
beforeDivider
'
)
&&
!
Object
.
prototype
.
hasOwnProperty
.
call
(
r
,
'
header
'
)
);
const
callback
=
glDropdown
.
parseData
.
bind
(
glDropdown
);
// Update the data model
this
.
processData
(
inputValue
,
users
,
callback
);
}
if
(
selected
&&
'
id
'
in
selected
&&
$
(
el
).
hasClass
(
'
is-active
'
))
{
if
(
selected
.
text
)
{
return
selected
.
text
;
...
...
@@ -194,14 +275,77 @@ import Vue from 'vue';
}
},
defaultLabel
:
defaultLabel
,
inputId
:
'
issue_assignee_id
'
,
hidden
:
function
(
e
)
{
if
(
$dropdown
.
hasClass
(
'
js-multiselect
'
))
{
eventHub
.
$emit
(
'
sidebar.saveAssignees
'
);
}
$selectbox
.
hide
();
// Recalculate where .value is because vue might have changed it
$block
=
$selectbox
.
closest
(
'
.block
'
);
$value
=
$block
.
find
(
'
.value
'
);
// display:block overrides the hide-collapse rule
return
$value
.
css
(
'
display
'
,
''
);
},
multiSelect
:
$dropdown
.
hasClass
(
'
js-multiselect
'
),
vue
:
$dropdown
.
hasClass
(
'
js-issue-board-sidebar
'
),
clicked
:
function
(
user
,
$el
,
e
)
{
clicked
:
function
(
options
)
{
const
{
$el
,
e
,
isMarking
}
=
options
;
const
user
=
options
.
selectedObj
;
if
(
$dropdown
.
hasClass
(
'
js-multiselect
'
))
{
const
isActive
=
$el
.
hasClass
(
'
is-active
'
);
const
previouslySelected
=
$dropdown
.
closest
(
'
.selectbox
'
)
.
find
(
"
input[name='
"
+
(
$dropdown
.
data
(
'
field-name
'
))
+
"
'][value!=0]
"
);
// Enables support for limiting the number of users selected
// Automatically removes the first on the list if more users are selected
const
maxSelect
=
$dropdown
.
data
(
'
max-select
'
);
if
(
maxSelect
)
{
const
selected
=
getSelected
();
if
(
selected
.
length
>
maxSelect
)
{
const
firstSelectedId
=
selected
[
0
];
const
firstSelected
=
$dropdown
.
closest
(
'
.selectbox
'
)
.
find
(
`input[name='
${
$dropdown
.
data
(
'
field-name
'
)}
'][value=
${
firstSelectedId
}
]`
);
firstSelected
.
remove
();
eventHub
.
$emit
(
'
sidebar.removeAssignee
'
,
{
id
:
firstSelectedId
,
});
}
}
if
(
user
.
beforeDivider
&&
user
.
name
.
toLowerCase
()
===
'
unassigned
'
)
{
// Unassigned selected
previouslySelected
.
each
((
index
,
element
)
=>
{
const
id
=
parseInt
(
element
.
value
,
10
);
element
.
remove
();
});
eventHub
.
$emit
(
'
sidebar.removeAllAssignees
'
);
}
else
if
(
isActive
)
{
// user selected
eventHub
.
$emit
(
'
sidebar.addAssignee
'
,
user
);
// Remove unassigned selection (if it was previously selected)
const
unassignedSelected
=
$dropdown
.
closest
(
'
.selectbox
'
)
.
find
(
"
input[name='
"
+
(
$dropdown
.
data
(
'
field-name
'
))
+
"
'][value=0]
"
);
if
(
unassignedSelected
)
{
unassignedSelected
.
remove
();
}
}
else
{
if
(
previouslySelected
.
length
===
0
)
{
// Select unassigned because there is no more selected users
this
.
addInput
(
$dropdown
.
data
(
'
field-name
'
),
0
,
{});
}
// User unselected
eventHub
.
$emit
(
'
sidebar.removeAssignee
'
,
user
);
}
}
var
isIssueIndex
,
isMRIndex
,
page
,
selected
;
page
=
$
(
'
body
'
).
data
(
'
page
'
);
isIssueIndex
=
page
===
'
projects:issues:index
'
;
...
...
@@ -236,7 +380,7 @@ import Vue from 'vue';
}
updateIssueBoardsIssue
();
}
else
{
}
else
if
(
!
$dropdown
.
hasClass
(
'
js-multiselect
'
))
{
selected
=
$dropdown
.
closest
(
'
.selectbox
'
).
find
(
"
input[name='
"
+
(
$dropdown
.
data
(
'
field-name
'
))
+
"
']
"
).
val
();
return
assignTo
(
selected
);
}
...
...
@@ -247,29 +391,43 @@ import Vue from 'vue';
opened
:
function
(
e
)
{
const
$el
=
$
(
e
.
currentTarget
);
$el
.
find
(
'
.is-active
'
).
removeClass
(
'
is-active
'
);
$el
.
find
(
`li[data-user-id="
${
selectedId
}
"] .dropdown-menu-user-link`
).
addClass
(
'
is-active
'
);
const
initialSelected
=
getSelected
().
forEach
((
selectedId
)
=>
{
$el
.
find
(
`li[data-user-id="
${
selectedId
}
"] .dropdown-menu-user-link`
).
addClass
(
'
is-active
'
);
});
},
updateLabel
:
$dropdown
.
data
(
'
dropdown-title
'
),
renderRow
:
function
(
user
)
{
var
avatar
,
img
,
listClosingTags
,
listWithName
,
listWithUserName
,
selected
,
username
;
username
=
user
.
username
?
"
@
"
+
user
.
username
:
""
;
avatar
=
user
.
avatar_url
?
user
.
avatar_url
:
false
;
selected
=
user
.
id
===
parseInt
(
selectedId
,
10
)
?
"
is-active
"
:
""
;
const
fieldName
=
this
.
fieldName
;
const
field
=
$dropdown
.
closest
(
'
.selectbox
'
).
find
(
"
input[name='
"
+
fieldName
+
"
'][value='
"
+
user
.
id
+
"
']
"
);
if
(
field
.
length
)
{
selected
=
true
;
}
img
=
""
;
if
(
user
.
beforeDivider
!=
null
)
{
"
<li> <a href='#' class='
"
+
selected
+
"
'>
"
+
user
.
name
+
"
</a> </li>
"
;
`<li><a href='#' class='
${
selected
===
true
?
'
is-active
'
:
''
}
'>
${
user
.
name
}
</a></li>`
;
}
else
{
if
(
avatar
)
{
img
=
"
<img src='
"
+
avatar
+
"
' class='avatar avatar-inline' width='3
0
' />
"
;
img
=
"
<img src='
"
+
avatar
+
"
' class='avatar avatar-inline' width='3
2
' />
"
;
}
}
// split into three parts so we can remove the username section if nessesary
listWithName
=
"
<li data-user-id=
"
+
user
.
id
+
"
> <a href='#' class='dropdown-menu-user-link
"
+
selected
+
"
'>
"
+
img
+
"
<strong class='dropdown-menu-user-full-name'>
"
+
user
.
name
+
"
</strong>
"
;
listWithUserName
=
"
<span class='dropdown-menu-user-username'>
"
+
username
+
"
</span>
"
;
listClosingTags
=
"
</a> </li>
"
;
if
(
username
===
''
)
{
listWithUserName
=
''
;
}
return
listWithName
+
listWithUserName
+
listClosingTags
;
return
`
<li data-user-id=
${
user
.
id
}
>
<a href='#' class='dropdown-menu-user-link
${
selected
===
true
?
'
is-active
'
:
''
}
'>
${
img
}
<strong class='dropdown-menu-user-full-name'>
${
user
.
name
}
</strong>
${
username
?
`<span class='dropdown-menu-user-username'>
${
username
}
</span>`
:
''
}
</a>
</li>
`
;
}
});
};
...
...
app/assets/javascripts/weight_select.js
View file @
95e04916
...
...
@@ -54,7 +54,10 @@
return
''
;
}
},
clicked
:
function
(
selected
,
$el
,
e
)
{
clicked
:
function
(
options
)
{
const
e
=
options
.
e
;
let
selected
=
options
.
selectedObj
;
if
(
$
(
dropdown
).
is
(
"
.js-filter-submit
"
))
{
return
$
(
dropdown
).
parents
(
'
form
'
).
submit
();
}
else
if
(
$dropdown
.
is
(
'
.js-issuable-form-weight
'
))
{
...
...
app/assets/stylesheets/framework/avatar.scss
View file @
95e04916
...
...
@@ -97,7 +97,7 @@
.avatar-counter
{
background-color
:
$gray-darkest
;
color
:
$white-light
;
border
:
1px
solid
$
avatar-counter-
border-color
;
border
:
1px
solid
$border-color
;
border-radius
:
1em
;
font-family
:
$regular_font
;
font-size
:
9px
;
...
...
app/assets/stylesheets/framework/dropdowns.scss
View file @
95e04916
...
...
@@ -222,11 +222,9 @@
}
.dropdown-header
{
color
:
$gl-text-color
;
color
:
$gl-text-color
-secondary
;
font-size
:
13px
;
font-weight
:
600
;
line-height
:
22px
;
text-transform
:
capitalize
;
padding
:
0
16px
;
}
...
...
@@ -291,8 +289,8 @@
.dropdown-menu-user
{
.avatar
{
float
:
left
;
width
:
30px
;
height
:
30px
;
width
:
2
*
$gl-padding
;
height
:
2
*
$gl-padding
;
margin
:
0
10px
0
0
;
}
}
...
...
@@ -331,6 +329,7 @@
.dropdown-menu-selectable
{
a
{
padding-left
:
26px
;
position
:
relative
;
&
.is-indeterminate
,
&
.is-active
{
...
...
@@ -355,6 +354,9 @@
&.
is-active
:
:
before
{
content
:
"\f00c"
;
position
:
absolute
;
top
:
50%
;
transform
:
translateY
(
-50%
);
}
}
}
...
...
app/assets/stylesheets/framework/variables.scss
View file @
95e04916
...
...
@@ -439,7 +439,6 @@ $help-shortcut-header-color: #333;
$issues-today-bg
:
#f3fff2
;
$issues-today-border
:
#e1e8d5
;
$compare-display-color
:
#888
;
$avatar-counter-border-color
:
#e6e6e6
;
/*
* jQuery UI
...
...
app/assets/stylesheets/pages/diff.scss
View file @
95e04916
...
...
@@ -557,14 +557,7 @@
.diff-comments-more-count
,
.diff-notes-collapse
{
background-color
:
$gray-darkest
;
color
:
$white-light
;
border
:
1px
solid
$white-light
;
border-radius
:
1em
;
font-family
:
$regular_font
;
font-size
:
9px
;
line-height
:
17px
;
text-align
:
center
;
@extend
.avatar-counter
;
}
.diff-notes-collapse
{
...
...
app/assets/stylesheets/pages/issuable.scss
View file @
95e04916
...
...
@@ -90,10 +90,15 @@
}
.right-sidebar
{
a
{
a
,
.btn-link
{
color
:
inherit
;
}
.btn-link
{
outline
:
none
;
}
.issuable-header-text
{
margin-top
:
7px
;
}
...
...
@@ -209,6 +214,10 @@
}
}
.assign-yourself
.btn-link
{
padding-left
:
0
;
}
.bold
{
font-weight
:
600
;
}
...
...
@@ -237,6 +246,10 @@
margin-left
:
0
;
}
.assignee
.user-list
.avatar
{
margin
:
0
;
}
.username
{
display
:
block
;
margin-top
:
4px
;
...
...
@@ -294,14 +307,23 @@
margin-top
:
0
;
}
.sidebar-avatar-counter
{
padding-top
:
2px
;
}
.author
{
display
:
none
;
}
.avatar
:hover
{
.avatar
:hover
,
.avatar-counter
:hover
{
border-color
:
$issuable-sidebar-color
;
}
.avatar-counter
:hover
{
color
:
$issuable-sidebar-color
;
}
.btn-clipboard
{
border
:
none
;
color
:
$issuable-sidebar-color
;
...
...
@@ -311,6 +333,17 @@
color
:
$gl-text-color
;
}
}
&
.multiple-users
{
display
:
flex
;
justify-content
:
center
;
}
}
.sidebar-avatar-counter
{
width
:
24px
;
height
:
24px
;
border-radius
:
12px
;
}
.sidebar-collapsed-user
{
...
...
@@ -321,6 +354,37 @@
.issuable-header-btn
{
display
:
none
;
}
.multiple-users
{
height
:
24px
;
margin-bottom
:
17px
;
margin-top
:
4px
;
padding-bottom
:
4px
;
.btn-link
{
padding
:
0
;
border
:
0
;
.avatar
{
margin
:
0
;
}
}
.btn-link
:first-child
{
position
:
absolute
;
left
:
10px
;
z-index
:
1
;
}
.btn-link
:last-child
{
position
:
absolute
;
right
:
10px
;
&
:hover
{
text-decoration
:
none
;
}
}
}
}
a
{
...
...
@@ -365,17 +429,21 @@
}
.participants-list
{
display
:
flex
;
flex-wrap
:
wrap
;
justify-content
:
space-between
;
margin
:
-5px
;
}
.user-list
{
display
:
flex
;
flex-wrap
:
wrap
;
}
.participants-author
{
display
:
inline-block
;
flex-basis
:
14%
;
padding
:
5px
;
&
:nth-of-type
(
7n
)
{
padding-right
:
0
;
}
.author_link
{
display
:
block
;
}
...
...
@@ -385,13 +453,39 @@
}
}
.participants-more
{
.user-item
{
display
:
inline-block
;
padding
:
5px
;
flex-basis
:
20%
;
.user-link
{
display
:
inline-block
;
}
}
.participants-more
,
.user-list-more
{
margin-top
:
5px
;
margin-left
:
5px
;
a
{
a
,
.btn-link
{
color
:
$gl-text-color-secondary
;
}
.btn-link
{
outline
:
none
;
padding
:
0
;
}
.btn-link
:hover
{
@extend
a
:hover
;
text-decoration
:
none
;
}
.btn-link
:focus
{
text-decoration
:
none
;
}
}
.issuable-form-padding-top
{
...
...
app/controllers/projects/issues_controller.rb
View file @
95e04916
...
...
@@ -157,7 +157,7 @@ class Projects::IssuesController < Projects::ApplicationController
if
@issue
.
valid?
render
json:
@issue
.
to_json
(
methods:
[
:task_status
,
:task_status_short
],
include:
{
milestone:
{},
assignees:
{
only:
[
:name
,
:username
],
methods:
[
:avatar_url
]
},
assignees:
{
only:
[
:
id
,
:
name
,
:username
],
methods:
[
:avatar_url
]
},
labels:
{
methods: :text_color
}
})
else
render
json:
{
errors:
@issue
.
errors
.
full_messages
},
status: :unprocessable_entity
...
...
app/serializers/issue_entity.rb
View file @
95e04916
class
IssueEntity
<
IssuableEntity
expose
:branch_name
expose
:confidential
expose
:assignee
_ids
expose
:assignee
s
,
using:
API
::
Entities
::
UserBasic
expose
:due_date
expose
:moved_to_id
expose
:project_id
...
...
app/views/shared/issuable/_participants.html.haml
View file @
95e04916
...
...
@@ -12,9 +12,9 @@
-
participants
.
each
do
|
participant
|
.participants-author.js-participants-author
=
link_to_member
(
@project
,
participant
,
name:
false
,
size:
24
)
-
if
participants_extra
>
0
.participants-more
%a
.js-participants-more
{
href:
"#"
,
data:
{
original_text:
"+ #{participants_size - 7} more"
,
less_text:
"- show less"
}
}
+
#{
participants_extra
}
more
-
if
participants_extra
>
0
.hide-collapsed
.participants-more
%a
.js-participants-more
{
href:
"#"
,
data:
{
original_text:
"+ #{participants_size - 7} more"
,
less_text:
"- show less"
}
}
+
#{
participants_extra
}
more
:javascript
IssuableContext
.
prototype
.
PARTICIPANTS_ROW_COUNT
=
#{
participants_row
}
;
app/views/shared/issuable/_sidebar.html.haml
View file @
95e04916
-
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
...
...
@@ -22,9 +22,55 @@
=
icon
(
'spin spinner'
,
class:
'hidden js-issuable-todo-loading'
,
'aria-hidden'
:
'true'
)
=
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:
{
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
=
link_to_member
(
@project
,
issuable
.
assignee
,
size:
24
)
-
else
=
icon
(
'user'
,
'aria-hidden'
:
'true'
)
.title.hide-collapsed
Assignee
=
icon
(
'spinner spin'
,
class:
'hidden block-loading'
,
'aria-hidden'
:
'true'
)
-
if
can_edit_issuable
=
link_to
'Edit'
,
'#'
,
class:
'edit-link pull-right'
.value.hide-collapsed
-
if
issuable
.
assignee
=
link_to_member
(
@project
,
issuable
.
assignee
,
size:
32
,
extra_class:
'bold'
)
do
-
if
issuable
.
instance_of?
(
MergeRequest
)
&&
!
issuable
.
can_be_merged_by?
(
issuable
.
assignee
)
%span
.pull-right.cannot-be-merged
{
data:
{
toggle:
'tooltip'
,
placement:
'left'
},
title:
'Not allowed to merge'
}
=
icon
(
'exclamation-triangle'
,
'aria-hidden'
:
'true'
)
%span
.username
=
issuable
.
assignee
.
to_reference
-
else
%span
.assign-yourself.no-value
No assignee
-
if
can_edit_issuable
\-
%a
.js-assign-yourself
{
href:
'#'
}
assign yourself
=
render
"shared/issuable/form/
#{
issuable
.
model_name
.
singular
}
_assignee"
,
can_edit_issuable:
can_edit_issuable
,
issuable:
issuable
,
f:
f
.selectbox.hide-collapsed
-
issuable
.
assignees
.
each
do
|
assignee
|
=
hidden_field_tag
"
#{
issuable
.
to_ability_name
}
[assignee_ids][]"
,
assignee
.
id
,
id:
nil
-
options
=
{
toggle_class:
'js-user-search js-author-search'
,
title:
'Assign to'
,
filter:
true
,
dropdown_class:
'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author'
,
placeholder:
'Search users'
,
data:
{
first_user:
(
current_user
.
username
if
current_user
),
current_user:
true
,
project_id:
(
@project
.
id
if
@project
),
author_id:
issuable
.
author_id
,
field_name:
"
#{
issuable
.
to_ability_name
}
[assignee_id]"
,
issue_update:
issuable_json_path
(
issuable
),
ability_name:
issuable
.
to_ability_name
,
null_user:
true
}
}
-
if
issuable
.
instance_of?
(
Issue
)
-
if
issuable
.
assignees
.
length
==
0
=
hidden_field_tag
"
#{
issuable
.
to_ability_name
}
[assignee_ids][]"
,
0
,
id:
nil
-
title
=
'Select assignee(s)'
-
options
[
:toggle_class
]
+=
' js-multiselect js-save-user-data'
-
options
[
:data
][
:field_name
]
=
"
#{
issuable
.
to_ability_name
}
[assignee_ids][]"
-
options
[
:data
][
:multi_select
]
=
true
-
options
[
:data
][
'dropdown-title'
]
=
title
-
options
[
:data
][
'dropdown-header'
]
=
'Assignee(s)'
-
else
-
title
=
'Select assignee'
=
dropdown_tag
(
title
,
options:
options
)
.block.milestone
.sidebar-collapsed-icon
=
icon
(
'clock-o'
,
'aria-hidden'
:
'true'
)
...
...
@@ -50,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
...
...
@@ -171,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
}
,
currentUser
:
#{
current_user
.
to_json
(
only:
[
:username
,
:id
,
:name
],
methods: :avatar_url
)
}
,
rootPath
:
"
#{
root_path
}
"
};
new
MilestoneSelect
(
'
{"full_path":"
#{
@project
.
full_path
}
"}
'
);
new
LabelsSelect
();
new
WeightSelect
();
...
...
config/webpack.config.js
View file @
95e04916
...
...
@@ -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
'
,
...
...
@@ -105,7 +105,7 @@ var config = {
'
diff_notes
'
,
'
environments
'
,
'
environments_folder
'
,
'
issuable
'
,
'
sidebar
'
,
'
merge_conflicts
'
,
'
mr_widget_ee
'
,
'
vue_pipelines
'
,
...
...
spec/javascripts/issuable_time_tracker_spec.js
View file @
95e04916
...
...
@@ -2,7 +2,7 @@
import
Vue
from
'
vue
'
;
require
(
'
~/issuable/time_tracking/components/time_tracker
'
)
;
import
'
~/sidebar/components/time_tracking/time_tracker
'
;
function
initTimeTrackingComponent
(
opts
)
{
setFixtures
(
`
...
...
spec/javascripts/subbable_resource_spec.js
deleted
100644 → 0
View file @
8cce7368
/* 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
=
{}));
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