Display current stopwatch in navbar (#14122)
* add notification about running stopwatch to header * serialize seconds, duration in stopwatches api * ajax update stopwatch i should get my testenv working locally... * new variant: hover dialog * noscript compatibility * js: live-update stopwatch time * js live update robustness
This commit is contained in:
parent
56a8929605
commit
b5570d3e68
15 changed files with 226 additions and 15 deletions
|
@ -7,7 +7,6 @@ package integrations
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
@ -31,14 +30,11 @@ func TestAPIListStopWatches(t *testing.T) {
|
||||||
issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: stopwatch.IssueID}).(*models.Issue)
|
issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: stopwatch.IssueID}).(*models.Issue)
|
||||||
if assert.Len(t, apiWatches, 1) {
|
if assert.Len(t, apiWatches, 1) {
|
||||||
assert.EqualValues(t, stopwatch.CreatedUnix.AsTime().Unix(), apiWatches[0].Created.Unix())
|
assert.EqualValues(t, stopwatch.CreatedUnix.AsTime().Unix(), apiWatches[0].Created.Unix())
|
||||||
apiWatches[0].Created = time.Time{}
|
assert.EqualValues(t, issue.Index, apiWatches[0].IssueIndex)
|
||||||
assert.EqualValues(t, api.StopWatch{
|
assert.EqualValues(t, issue.Title, apiWatches[0].IssueTitle)
|
||||||
Created: time.Time{},
|
assert.EqualValues(t, repo.Name, apiWatches[0].RepoName)
|
||||||
IssueIndex: issue.Index,
|
assert.EqualValues(t, repo.OwnerName, apiWatches[0].RepoOwnerName)
|
||||||
IssueTitle: issue.Title,
|
assert.Greater(t, int64(apiWatches[0].Seconds), int64(0))
|
||||||
RepoName: repo.Name,
|
|
||||||
RepoOwnerName: repo.OwnerName,
|
|
||||||
}, *apiWatches[0])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -72,7 +72,7 @@ func TestCreateIssueAttachment(t *testing.T) {
|
||||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
|
|
||||||
link, exists := htmlDoc.doc.Find("form").Attr("action")
|
link, exists := htmlDoc.doc.Find("form#new-issue").Attr("action")
|
||||||
assert.True(t, exists, "The template has changed")
|
assert.True(t, exists, "The template has changed")
|
||||||
|
|
||||||
postData := map[string]string{
|
postData := map[string]string{
|
||||||
|
|
|
@ -19,6 +19,16 @@ type Stopwatch struct {
|
||||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Seconds returns the amount of time passed since creation, based on local server time
|
||||||
|
func (s Stopwatch) Seconds() int64 {
|
||||||
|
return int64(timeutil.TimeStampNow() - s.CreatedUnix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duration returns a human-readable duration string based on local server time
|
||||||
|
func (s Stopwatch) Duration() string {
|
||||||
|
return SecToTime(s.Seconds())
|
||||||
|
}
|
||||||
|
|
||||||
func getStopwatch(e Engine, userID, issueID int64) (sw *Stopwatch, exists bool, err error) {
|
func getStopwatch(e Engine, userID, issueID int64) (sw *Stopwatch, exists bool, err error) {
|
||||||
sw = new(Stopwatch)
|
sw = new(Stopwatch)
|
||||||
exists, err = e.
|
exists, err = e.
|
||||||
|
|
|
@ -147,6 +147,8 @@ func ToStopWatches(sws []*models.Stopwatch) (api.StopWatches, error) {
|
||||||
|
|
||||||
result = append(result, api.StopWatch{
|
result = append(result, api.StopWatch{
|
||||||
Created: sw.CreatedUnix.AsTime(),
|
Created: sw.CreatedUnix.AsTime(),
|
||||||
|
Seconds: sw.Seconds(),
|
||||||
|
Duration: sw.Duration(),
|
||||||
IssueIndex: issue.Index,
|
IssueIndex: issue.Index,
|
||||||
IssueTitle: issue.Title,
|
IssueTitle: issue.Title,
|
||||||
RepoOwnerName: repo.OwnerName,
|
RepoOwnerName: repo.OwnerName,
|
||||||
|
|
|
@ -12,6 +12,8 @@ import (
|
||||||
type StopWatch struct {
|
type StopWatch struct {
|
||||||
// swagger:strfmt date-time
|
// swagger:strfmt date-time
|
||||||
Created time.Time `json:"created"`
|
Created time.Time `json:"created"`
|
||||||
|
Seconds int64 `json:"seconds"`
|
||||||
|
Duration string `json:"duration"`
|
||||||
IssueIndex int64 `json:"issue_index"`
|
IssueIndex int64 `json:"issue_index"`
|
||||||
IssueTitle string `json:"issue_title"`
|
IssueTitle string `json:"issue_title"`
|
||||||
RepoOwnerName string `json:"repo_owner_name"`
|
RepoOwnerName string `json:"repo_owner_name"`
|
||||||
|
|
|
@ -15,6 +15,7 @@ page = Page
|
||||||
template = Template
|
template = Template
|
||||||
language = Language
|
language = Language
|
||||||
notifications = Notifications
|
notifications = Notifications
|
||||||
|
active_stopwatch = Active Time Tracker
|
||||||
create_new = Create…
|
create_new = Create…
|
||||||
user_profile_and_more = Profile and Settings…
|
user_profile_and_more = Profile and Settings…
|
||||||
signed_in_as = Signed in as
|
signed_in_as = Signed in as
|
||||||
|
@ -1139,13 +1140,15 @@ issues.lock.title = Lock conversation on this issue.
|
||||||
issues.unlock.title = Unlock conversation on this issue.
|
issues.unlock.title = Unlock conversation on this issue.
|
||||||
issues.comment_on_locked = You cannot comment on a locked issue.
|
issues.comment_on_locked = You cannot comment on a locked issue.
|
||||||
issues.tracker = Time Tracker
|
issues.tracker = Time Tracker
|
||||||
issues.start_tracking_short = Start
|
issues.start_tracking_short = Start Timer
|
||||||
issues.start_tracking = Start Time Tracking
|
issues.start_tracking = Start Time Tracking
|
||||||
issues.start_tracking_history = `started working %s`
|
issues.start_tracking_history = `started working %s`
|
||||||
issues.tracker_auto_close = Timer will be stopped automatically when this issue gets closed
|
issues.tracker_auto_close = Timer will be stopped automatically when this issue gets closed
|
||||||
issues.tracking_already_started = `You have already started time tracking on <a href="%s">another issue</a>!`
|
issues.tracking_already_started = `You have already started time tracking on <a href="%s">another issue</a>!`
|
||||||
issues.stop_tracking = Stop
|
issues.stop_tracking = Stop Timer
|
||||||
issues.stop_tracking_history = `stopped working %s`
|
issues.stop_tracking_history = `stopped working %s`
|
||||||
|
issues.cancel_tracking = Discard
|
||||||
|
issues.cancel_tracking_history = `cancelled time tracking %s`
|
||||||
issues.add_time = Manually Add Time
|
issues.add_time = Manually Add Time
|
||||||
issues.add_time_short = Add Time
|
issues.add_time_short = Add Time
|
||||||
issues.add_time_cancel = Cancel
|
issues.add_time_cancel = Cancel
|
||||||
|
@ -1154,8 +1157,6 @@ issues.del_time_history= `deleted spent time %s`
|
||||||
issues.add_time_hours = Hours
|
issues.add_time_hours = Hours
|
||||||
issues.add_time_minutes = Minutes
|
issues.add_time_minutes = Minutes
|
||||||
issues.add_time_sum_to_small = No time was entered.
|
issues.add_time_sum_to_small = No time was entered.
|
||||||
issues.cancel_tracking = Cancel
|
|
||||||
issues.cancel_tracking_history = `cancelled time tracking %s`
|
|
||||||
issues.time_spent_total = Total Time Spent
|
issues.time_spent_total = Total Time Spent
|
||||||
issues.time_spent_from_all_authors = `Total Time Spent: %s`
|
issues.time_spent_from_all_authors = `Total Time Spent: %s`
|
||||||
issues.due_date = Due Date
|
issues.due_date = Due Date
|
||||||
|
|
13
package-lock.json
generated
13
package-lock.json
generated
|
@ -5293,6 +5293,11 @@
|
||||||
"json-parse-better-errors": "^1.0.1"
|
"json-parse-better-errors": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"parse-ms": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA=="
|
||||||
|
},
|
||||||
"parse-node-version": {
|
"parse-node-version": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz",
|
||||||
|
@ -6702,6 +6707,14 @@
|
||||||
"integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==",
|
"integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"pretty-ms": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==",
|
||||||
|
"requires": {
|
||||||
|
"parse-ms": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
"monaco-editor": "0.21.2",
|
"monaco-editor": "0.21.2",
|
||||||
"monaco-editor-webpack-plugin": "2.1.0",
|
"monaco-editor-webpack-plugin": "2.1.0",
|
||||||
"postcss": "8.2.1",
|
"postcss": "8.2.1",
|
||||||
|
"pretty-ms": "7.0.1",
|
||||||
"raw-loader": "4.0.2",
|
"raw-loader": "4.0.2",
|
||||||
"sortablejs": "1.12.0",
|
"sortablejs": "1.12.0",
|
||||||
"swagger-ui-dist": "3.38.0",
|
"swagger-ui-dist": "3.38.0",
|
||||||
|
|
|
@ -6,6 +6,7 @@ package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
|
@ -61,3 +62,47 @@ func CancelStopwatch(c *context.Context) {
|
||||||
url := issue.HTMLURL()
|
url := issue.HTMLURL()
|
||||||
c.Redirect(url, http.StatusSeeOther)
|
c.Redirect(url, http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetActiveStopwatch is the middleware that sets .ActiveStopwatch on context
|
||||||
|
func GetActiveStopwatch(c *context.Context) {
|
||||||
|
if strings.HasPrefix(c.Req.URL.Path, "/api") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.IsSigned {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, sw, err := models.HasUserStopwatch(c.User.ID)
|
||||||
|
if err != nil {
|
||||||
|
c.ServerError("HasUserStopwatch", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if sw == nil || sw.ID == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
issue, err := models.GetIssueByID(sw.IssueID)
|
||||||
|
if err != nil || issue == nil {
|
||||||
|
c.ServerError("GetIssueByID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = issue.LoadRepo(); err != nil {
|
||||||
|
c.ServerError("LoadRepo", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Data["ActiveStopwatch"] = StopwatchTmplInfo{
|
||||||
|
issue.Repo.FullName(),
|
||||||
|
issue.Index,
|
||||||
|
sw.Seconds() + 1, // ensure time is never zero in ui
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopwatchTmplInfo is a view on a stopwatch specifically for template rendering
|
||||||
|
type StopwatchTmplInfo struct {
|
||||||
|
RepoSlug string
|
||||||
|
IssueIndex int64
|
||||||
|
Seconds int64
|
||||||
|
}
|
||||||
|
|
|
@ -176,6 +176,7 @@ func RegisterMacaronRoutes(m *macaron.Macaron) {
|
||||||
}
|
}
|
||||||
|
|
||||||
m.Use(user.GetNotificationCount)
|
m.Use(user.GetNotificationCount)
|
||||||
|
m.Use(repo.GetActiveStopwatch)
|
||||||
m.Use(func(ctx *context.Context) {
|
m.Use(func(ctx *context.Context) {
|
||||||
ctx.Data["UnitWikiGlobalDisabled"] = models.UnitTypeWiki.UnitGlobalDisabled()
|
ctx.Data["UnitWikiGlobalDisabled"] = models.UnitTypeWiki.UnitGlobalDisabled()
|
||||||
ctx.Data["UnitIssuesGlobalDisabled"] = models.UnitTypeIssues.UnitGlobalDisabled()
|
ctx.Data["UnitIssuesGlobalDisabled"] = models.UnitTypeIssues.UnitGlobalDisabled()
|
||||||
|
|
|
@ -67,6 +67,44 @@
|
||||||
</div>
|
</div>
|
||||||
{{else if .IsSigned}}
|
{{else if .IsSigned}}
|
||||||
<div class="right stackable menu">
|
<div class="right stackable menu">
|
||||||
|
{{$issueURL := Printf "%s/%s/issues/%d" AppSubUrl .ActiveStopwatch.RepoSlug .ActiveStopwatch.IssueIndex}}
|
||||||
|
<a class="active-stopwatch-trigger item ui label {{if not .ActiveStopwatch}}hidden{{end}}" href="{{$issueURL}}">
|
||||||
|
<span class="text">
|
||||||
|
<span class="fitted item">
|
||||||
|
{{svg "octicon-stopwatch"}}
|
||||||
|
<span class="red" style="position:absolute; right:-0.6em; top:-0.6em;">{{svg "octicon-dot-fill"}}</span>
|
||||||
|
</span>
|
||||||
|
<span class="sr-mobile-only">{{.i18n.Tr "active_stopwatch"}}</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<div class="ui popup very wide">
|
||||||
|
<div class="df ac">
|
||||||
|
<a class="stopwatch-link df ac" href="{{$issueURL}}">
|
||||||
|
{{svg "octicon-issue-opened"}}
|
||||||
|
<span class="stopwatch-issue">{{.ActiveStopwatch.RepoSlug}}#{{.ActiveStopwatch.IssueIndex}}</span>
|
||||||
|
<span class="ui label blue stopwatch-time my-0 mx-4" data-seconds="{{.ActiveStopwatch.Seconds}}">
|
||||||
|
{{if .ActiveStopwatch}}{{Sec2Time .ActiveStopwatch.Seconds}}{{end}}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<form class="stopwatch-commit" method="POST" action="{{$issueURL}}/times/stopwatch/toggle">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<button
|
||||||
|
class="ui button mini compact basic icon fitted poping up"
|
||||||
|
data-content="{{.i18n.Tr "repo.issues.stop_tracking"}}"
|
||||||
|
data-position="top right" data-variation="small inverted"
|
||||||
|
>{{svg "octicon-square-fill"}}</button>
|
||||||
|
</form>
|
||||||
|
<form class="stopwatch-cancel" method="POST" action="{{$issueURL}}/times/stopwatch/cancel">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<button
|
||||||
|
class="ui button mini compact basic icon fitted poping up"
|
||||||
|
data-content="{{.i18n.Tr "repo.issues.cancel_tracking"}}"
|
||||||
|
data-position="top right" data-variation="small inverted"
|
||||||
|
>{{svg "octicon-trashcan"}}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<a href="{{AppSubUrl}}/notifications" class="item poping up" data-content='{{.i18n.Tr "notifications"}}' data-variation="tiny inverted">
|
<a href="{{AppSubUrl}}/notifications" class="item poping up" data-content='{{.i18n.Tr "notifications"}}' data-variation="tiny inverted">
|
||||||
<span class="text">
|
<span class="text">
|
||||||
<span class="fitted">{{svg "octicon-bell"}}</span>
|
<span class="fitted">{{svg "octicon-bell"}}</span>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<form class="ui comment form stackable grid" action="{{.Link}}" method="post">
|
<form class="ui comment form stackable grid" id="new-issue" action="{{.Link}}" method="post">
|
||||||
{{.CsrfTokenHtml}}
|
{{.CsrfTokenHtml}}
|
||||||
{{if .Flash}}
|
{{if .Flash}}
|
||||||
<div class="sixteen wide column">
|
<div class="sixteen wide column">
|
||||||
|
|
|
@ -15473,6 +15473,10 @@
|
||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
"x-go-name": "Created"
|
"x-go-name": "Created"
|
||||||
},
|
},
|
||||||
|
"duration": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Duration"
|
||||||
|
},
|
||||||
"issue_index": {
|
"issue_index": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int64",
|
"format": "int64",
|
||||||
|
@ -15489,6 +15493,11 @@
|
||||||
"repo_owner_name": {
|
"repo_owner_name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "RepoOwnerName"
|
"x-go-name": "RepoOwnerName"
|
||||||
|
},
|
||||||
|
"seconds": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"x-go-name": "Seconds"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
|
|
91
web_src/js/features/stopwatch.js
Normal file
91
web_src/js/features/stopwatch.js
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import prettyMilliseconds from 'pretty-ms';
|
||||||
|
const {AppSubUrl, csrf, NotificationSettings} = window.config;
|
||||||
|
|
||||||
|
let updateTimeInterval = null; // holds setInterval id when active
|
||||||
|
|
||||||
|
export async function initStopwatch() {
|
||||||
|
const stopwatchEl = $('.active-stopwatch-trigger');
|
||||||
|
|
||||||
|
stopwatchEl.removeAttr('href'); // intended for noscript mode only
|
||||||
|
stopwatchEl.popup({
|
||||||
|
position: 'bottom right',
|
||||||
|
hoverable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// form handlers
|
||||||
|
$('form > button', stopwatchEl).on('click', function () {
|
||||||
|
$(this).parent().trigger('submit');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!stopwatchEl || NotificationSettings.MinTimeout <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn = (timeout) => {
|
||||||
|
setTimeout(async () => {
|
||||||
|
await updateStopwatchWithCallback(fn, timeout);
|
||||||
|
}, timeout);
|
||||||
|
};
|
||||||
|
|
||||||
|
fn(NotificationSettings.MinTimeout);
|
||||||
|
|
||||||
|
const currSeconds = $('.stopwatch-time').data('seconds');
|
||||||
|
if (currSeconds) {
|
||||||
|
updateTimeInterval = updateStopwatchTime(currSeconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateStopwatchWithCallback(callback, timeout) {
|
||||||
|
const isSet = await updateStopwatch();
|
||||||
|
|
||||||
|
if (!isSet) {
|
||||||
|
timeout = NotificationSettings.MinTimeout;
|
||||||
|
} else if (timeout < NotificationSettings.MaxTimeout) {
|
||||||
|
timeout += NotificationSettings.TimeoutStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateStopwatch() {
|
||||||
|
const data = await $.ajax({
|
||||||
|
type: 'GET',
|
||||||
|
url: `${AppSubUrl}/api/v1/user/stopwatches`,
|
||||||
|
headers: {'X-Csrf-Token': csrf},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (updateTimeInterval) {
|
||||||
|
clearInterval(updateTimeInterval);
|
||||||
|
updateTimeInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const watch = data[0];
|
||||||
|
const btnEl = $('.active-stopwatch-trigger');
|
||||||
|
if (!watch) {
|
||||||
|
btnEl.addClass('hidden');
|
||||||
|
} else {
|
||||||
|
const {repo_owner_name, repo_name, issue_index, seconds} = watch;
|
||||||
|
const issueUrl = `${AppSubUrl}/${repo_owner_name}/${repo_name}/issues/${issue_index}`;
|
||||||
|
$('.stopwatch-link').attr('href', issueUrl);
|
||||||
|
$('.stopwatch-commit').attr('action', `${issueUrl}/times/stopwatch/toggle`);
|
||||||
|
$('.stopwatch-cancel').attr('action', `${issueUrl}/times/stopwatch/cancel`);
|
||||||
|
$('.stopwatch-issue').text(`${repo_owner_name}/${repo_name}#${issue_index}`);
|
||||||
|
$('.stopwatch-time').text(prettyMilliseconds(seconds * 1000));
|
||||||
|
updateStopwatchTime(seconds);
|
||||||
|
btnEl.removeClass('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateStopwatchTime(seconds) {
|
||||||
|
const secs = parseInt(seconds);
|
||||||
|
if (!Number.isFinite(secs)) return;
|
||||||
|
|
||||||
|
const start = Date.now();
|
||||||
|
updateTimeInterval = setInterval(() => {
|
||||||
|
const delta = Date.now() - start;
|
||||||
|
const dur = prettyMilliseconds(secs * 1000 + delta, {compact: true});
|
||||||
|
$('.stopwatch-time').text(dur);
|
||||||
|
}, 1000);
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ import createDropzone from './features/dropzone.js';
|
||||||
import initTableSort from './features/tablesort.js';
|
import initTableSort from './features/tablesort.js';
|
||||||
import ActivityTopAuthors from './components/ActivityTopAuthors.vue';
|
import ActivityTopAuthors from './components/ActivityTopAuthors.vue';
|
||||||
import {initNotificationsTable, initNotificationCount} from './features/notification.js';
|
import {initNotificationsTable, initNotificationCount} from './features/notification.js';
|
||||||
|
import {initStopwatch} from './features/stopwatch.js';
|
||||||
import {createCodeEditor, createMonaco} from './features/codeeditor.js';
|
import {createCodeEditor, createMonaco} from './features/codeeditor.js';
|
||||||
import {svg, svgs} from './svg.js';
|
import {svg, svgs} from './svg.js';
|
||||||
import {stripTags} from './utils.js';
|
import {stripTags} from './utils.js';
|
||||||
|
@ -2626,6 +2627,7 @@ $(document).ready(async () => {
|
||||||
initProject(),
|
initProject(),
|
||||||
initServiceWorker(),
|
initServiceWorker(),
|
||||||
initNotificationCount(),
|
initNotificationCount(),
|
||||||
|
initStopwatch(),
|
||||||
renderMarkdownContent(),
|
renderMarkdownContent(),
|
||||||
initGithook(),
|
initGithook(),
|
||||||
]);
|
]);
|
||||||
|
|
Reference in a new issue