Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display current stopwatch in navbar #14122

Merged
merged 19 commits into from
Jan 21, 2021
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 5 additions & 9 deletions integrations/api_issue_stopwatch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ package integrations
import (
"net/http"
"testing"
"time"

"code.gitea.io/gitea/models"
api "code.gitea.io/gitea/modules/structs"
Expand All @@ -31,14 +30,11 @@ func TestAPIListStopWatches(t *testing.T) {
issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: stopwatch.IssueID}).(*models.Issue)
if assert.Len(t, apiWatches, 1) {
assert.EqualValues(t, stopwatch.CreatedUnix.AsTime().Unix(), apiWatches[0].Created.Unix())
apiWatches[0].Created = time.Time{}
assert.EqualValues(t, api.StopWatch{
Created: time.Time{},
IssueIndex: issue.Index,
IssueTitle: issue.Title,
RepoName: repo.Name,
RepoOwnerName: repo.OwnerName,
}, *apiWatches[0])
assert.EqualValues(t, issue.Index, apiWatches[0].IssueIndex)
assert.EqualValues(t, issue.Title, apiWatches[0].IssueTitle)
assert.EqualValues(t, repo.Name, apiWatches[0].RepoName)
assert.EqualValues(t, repo.OwnerName, apiWatches[0].RepoOwnerName)
assert.Greater(t, int64(apiWatches[0].Seconds), int64(0))
}
}

Expand Down
2 changes: 1 addition & 1 deletion integrations/attachment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func TestCreateIssueAttachment(t *testing.T) {
resp := session.MakeRequest(t, req, http.StatusOK)
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")

postData := map[string]string{
Expand Down
10 changes: 10 additions & 0 deletions models/issue_stopwatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ type Stopwatch struct {
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) {
sw = new(Stopwatch)
exists, err = e.
Expand Down
2 changes: 2 additions & 0 deletions modules/convert/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ func ToStopWatches(sws []*models.Stopwatch) (api.StopWatches, error) {

result = append(result, api.StopWatch{
Created: sw.CreatedUnix.AsTime(),
Seconds: sw.Seconds(),
Duration: sw.Duration(),
IssueIndex: issue.Index,
IssueTitle: issue.Title,
RepoOwnerName: repo.OwnerName,
Expand Down
2 changes: 2 additions & 0 deletions modules/structs/issue_stopwatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
type StopWatch struct {
// swagger:strfmt date-time
Created time.Time `json:"created"`
Seconds int64 `json:"seconds"`
Duration string `json:"duration"`
IssueIndex int64 `json:"issue_index"`
IssueTitle string `json:"issue_title"`
RepoOwnerName string `json:"repo_owner_name"`
Expand Down
9 changes: 5 additions & 4 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ page = Page
template = Template
language = Language
notifications = Notifications
active_stopwatch = Active Time Tracker
create_new = Create…
user_profile_and_more = Profile and Settings…
signed_in_as = Signed in as
Expand Down Expand Up @@ -1138,13 +1139,15 @@ issues.lock.title = Lock conversation on this issue.
issues.unlock.title = Unlock conversation on this issue.
issues.comment_on_locked = You cannot comment on a locked issue.
issues.tracker = Time Tracker
issues.start_tracking_short = Start
issues.start_tracking_short = Start Timer
issues.start_tracking = Start Time Tracking
issues.start_tracking_history = `started working %s`
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.stop_tracking = Stop
issues.stop_tracking = Stop Timer
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_short = Add Time
issues.add_time_cancel = Cancel
Expand All @@ -1153,8 +1156,6 @@ issues.del_time_history= `deleted spent time %s`
issues.add_time_hours = Hours
issues.add_time_minutes = Minutes
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_from_all_authors = `Total Time Spent: %s`
issues.due_date = Due Date
Expand Down
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"monaco-editor": "0.21.2",
"monaco-editor-webpack-plugin": "2.1.0",
"postcss": "8.2.1",
"pretty-ms": "7.0.1",
"raw-loader": "4.0.2",
"sortablejs": "1.12.0",
"swagger-ui-dist": "3.38.0",
Expand Down
45 changes: 45 additions & 0 deletions routers/repo/issue_stopwatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package repo

import (
"net/http"
"strings"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
Expand Down Expand Up @@ -61,3 +62,47 @@ func CancelStopwatch(c *context.Context) {
url := issue.HTMLURL()
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
}
1 change: 1 addition & 0 deletions routers/routes/macaron.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ func RegisterMacaronRoutes(m *macaron.Macaron) {
}

m.Use(user.GetNotificationCount)
m.Use(repo.GetActiveStopwatch)
m.Use(func(ctx *context.Context) {
ctx.Data["UnitWikiGlobalDisabled"] = models.UnitTypeWiki.UnitGlobalDisabled()
ctx.Data["UnitIssuesGlobalDisabled"] = models.UnitTypeIssues.UnitGlobalDisabled()
Expand Down
38 changes: 38 additions & 0 deletions templates/base/head_navbar.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,44 @@
</div>
{{else if .IsSigned}}
<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">
<span class="text">
<span class="fitted">{{svg "octicon-bell"}}</span>
Expand Down
2 changes: 1 addition & 1 deletion templates/repo/issue/new_form.tmpl
Original file line number Diff line number Diff line change
@@ -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}}
{{if .Flash}}
<div class="sixteen wide column">
Expand Down
9 changes: 9 additions & 0 deletions templates/swagger/v1_json.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -15473,6 +15473,10 @@
"format": "date-time",
"x-go-name": "Created"
},
"duration": {
"type": "string",
"x-go-name": "Duration"
},
"issue_index": {
"type": "integer",
"format": "int64",
Expand All @@ -15489,6 +15493,11 @@
"repo_owner_name": {
"type": "string",
"x-go-name": "RepoOwnerName"
},
"seconds": {
"type": "integer",
"format": "int64",
"x-go-name": "Seconds"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
Expand Down
91 changes: 91 additions & 0 deletions web_src/js/features/stopwatch.js
Original file line number Diff line number Diff line change
@@ -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);
}
2 changes: 2 additions & 0 deletions web_src/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import createDropzone from './features/dropzone.js';
import initTableSort from './features/tablesort.js';
import ActivityTopAuthors from './components/ActivityTopAuthors.vue';
import {initNotificationsTable, initNotificationCount} from './features/notification.js';
import {initStopwatch} from './features/stopwatch.js';
import {createCodeEditor, createMonaco} from './features/codeeditor.js';
import {svg, svgs} from './svg.js';
import {stripTags} from './utils.js';
Expand Down Expand Up @@ -2579,6 +2580,7 @@ $(document).ready(async () => {
initProject(),
initServiceWorker(),
initNotificationCount(),
initStopwatch(),
renderMarkdownContent(),
initGithook(),
]);
Expand Down