1
1
mirror of https://github.com/go-gitea/gitea synced 2025-02-08 07:44:44 +00:00

Merge branch 'main' into lunny/issue_dev

This commit is contained in:
Lunny Xiao 2024-12-05 10:17:10 -08:00
commit d869d3286f
30 changed files with 1382 additions and 984 deletions

View File

@ -524,6 +524,7 @@ rules:
no-jquery/no-data: [0] no-jquery/no-data: [0]
no-jquery/no-deferred: [2] no-jquery/no-deferred: [2]
no-jquery/no-delegate: [2] no-jquery/no-delegate: [2]
no-jquery/no-done-fail: [2]
no-jquery/no-each-collection: [0] no-jquery/no-each-collection: [0]
no-jquery/no-each-util: [0] no-jquery/no-each-util: [0]
no-jquery/no-each: [0] no-jquery/no-each: [0]
@ -538,6 +539,7 @@ rules:
no-jquery/no-find-util: [2] no-jquery/no-find-util: [2]
no-jquery/no-find: [0] no-jquery/no-find: [0]
no-jquery/no-fx-interval: [2] no-jquery/no-fx-interval: [2]
no-jquery/no-fx: [2]
no-jquery/no-global-eval: [2] no-jquery/no-global-eval: [2]
no-jquery/no-global-selector: [0] no-jquery/no-global-selector: [0]
no-jquery/no-grep: [2] no-jquery/no-grep: [2]

1
.gitignore vendored
View File

@ -28,6 +28,7 @@ _testmain.go
*.exe *.exe
*.test *.test
*.prof *.prof
*.tsbuildInfo
*coverage.out *coverage.out
coverage.all coverage.all

View File

@ -114,6 +114,8 @@ const (
CommentTypePin // 36 pin Issue CommentTypePin // 36 pin Issue
CommentTypeUnpin // 37 unpin Issue CommentTypeUnpin // 37 unpin Issue
CommentTypeChangeTimeEstimate // 38 Change time estimate
) )
var commentStrings = []string{ var commentStrings = []string{
@ -155,6 +157,7 @@ var commentStrings = []string{
"pull_cancel_scheduled_merge", "pull_cancel_scheduled_merge",
"pin", "pin",
"unpin", "unpin",
"change_time_estimate",
} }
func (t CommentType) String() string { func (t CommentType) String() string {

View File

@ -147,6 +147,9 @@ type Issue struct {
// For view issue page. // For view issue page.
ShowRole RoleDescriptor `xorm:"-"` ShowRole RoleDescriptor `xorm:"-"`
// Time estimate
TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"`
} }
var ( var (
@ -934,3 +937,28 @@ func insertIssue(ctx context.Context, issue *Issue) error {
return nil return nil
} }
// ChangeIssueTimeEstimate changes the plan time of this issue, as the given user.
func ChangeIssueTimeEstimate(ctx context.Context, issue *Issue, doer *user_model.User, timeEstimate int64) error {
return db.WithTx(ctx, func(ctx context.Context) error {
if err := UpdateIssueCols(ctx, &Issue{ID: issue.ID, TimeEstimate: timeEstimate}, "time_estimate"); err != nil {
return fmt.Errorf("updateIssueCols: %w", err)
}
if err := issue.LoadRepo(ctx); err != nil {
return fmt.Errorf("loadRepo: %w", err)
}
opts := &CreateCommentOptions{
Type: CommentTypeChangeTimeEstimate,
Doer: doer,
Repo: issue.Repo,
Issue: issue,
Content: fmt.Sprintf("%d", timeEstimate),
}
if _, err := CreateComment(ctx, opts); err != nil {
return fmt.Errorf("createComment: %w", err)
}
return nil
})
}

View File

@ -368,7 +368,8 @@ func prepareMigrationTasks() []*migration {
newMigration(308, "Add index(user_id, is_deleted) for action table", v1_23.AddNewIndexForUserDashboard), newMigration(308, "Add index(user_id, is_deleted) for action table", v1_23.AddNewIndexForUserDashboard),
newMigration(309, "Improve Notification table indices", v1_23.ImproveNotificationTableIndices), newMigration(309, "Improve Notification table indices", v1_23.ImproveNotificationTableIndices),
newMigration(310, "Add Priority to ProtectedBranch", v1_23.AddPriorityToProtectedBranch), newMigration(310, "Add Priority to ProtectedBranch", v1_23.AddPriorityToProtectedBranch),
newMigration(311, "Add table issue_dev_link", v1_23.CreateTableIssueDevLink), newMigration(311, "Add TimeEstimate to Issue table", v1_23.AddTimeEstimateColumnToIssueTable),
newMigration(312, "Add table issue_dev_link", v1_23.CreateTableIssueDevLink),
} }
return preparedMigrations return preparedMigrations
} }

View File

@ -4,19 +4,13 @@
package v1_23 //nolint package v1_23 //nolint
import ( import (
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/xorm" "xorm.io/xorm"
) )
func CreateTableIssueDevLink(x *xorm.Engine) error { func AddTimeEstimateColumnToIssueTable(x *xorm.Engine) error {
type IssueDevLink struct { type Issue struct {
ID int64 `xorm:"pk autoincr"` TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"`
IssueID int64 `xorm:"INDEX"`
LinkType int
LinkedRepoID int64 `xorm:"INDEX"` // it can link to self repo or other repo
LinkIndex string // branch name, pull request number or commit sha
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
} }
return x.Sync(new(IssueDevLink))
return x.Sync(new(Issue))
} }

View File

@ -0,0 +1,22 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_23 //nolint
import (
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/xorm"
)
func CreateTableIssueDevLink(x *xorm.Engine) error {
type IssueDevLink struct {
ID int64 `xorm:"pk autoincr"`
IssueID int64 `xorm:"INDEX"`
LinkType int
LinkedRepoID int64 `xorm:"INDEX"` // it can link to self repo or other repo
LinkIndex string // branch name, pull request number or commit sha
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
}
return x.Sync(new(IssueDevLink))
}

View File

@ -70,6 +70,9 @@ func NewFuncMap() template.FuncMap {
"FileSize": base.FileSize, "FileSize": base.FileSize,
"CountFmt": base.FormatNumberSI, "CountFmt": base.FormatNumberSI,
"Sec2Time": util.SecToTime, "Sec2Time": util.SecToTime,
"TimeEstimateString": timeEstimateString,
"LoadTimes": func(startTime time.Time) string { "LoadTimes": func(startTime time.Time) string {
return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms" return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
}, },
@ -282,6 +285,14 @@ func userThemeName(user *user_model.User) string {
return setting.UI.DefaultTheme return setting.UI.DefaultTheme
} }
func timeEstimateString(timeSec any) string {
v, _ := util.ToInt64(timeSec)
if v == 0 {
return ""
}
return util.TimeEstimateString(v)
}
func panicIfDevOrTesting() { func panicIfDevOrTesting() {
if !setting.IsProd || setting.IsInTesting { if !setting.IsProd || setting.IsInTesting {
panic("legacy template functions are for backward compatibility only, do not use them in new code") panic("legacy template functions are for backward compatibility only, do not use them in new code")

85
modules/util/time_str.go Normal file
View File

@ -0,0 +1,85 @@
// Copyright 2024 Gitea. All rights reserved.
// SPDX-License-Identifier: MIT
package util
import (
"fmt"
"regexp"
"strconv"
"strings"
"sync"
)
type timeStrGlobalVarsType struct {
units []struct {
name string
num int64
}
re *regexp.Regexp
}
// When tracking working time, only hour/minute/second units are accurate and could be used.
// For other units like "day", it depends on "how many working hours in a day": 6 or 7 or 8?
// So at the moment, we only support hour/minute/second units.
// In the future, it could be some configurable options to help users
// to convert the working time to different units.
var timeStrGlobalVars = sync.OnceValue[*timeStrGlobalVarsType](func() *timeStrGlobalVarsType {
v := &timeStrGlobalVarsType{}
v.re = regexp.MustCompile(`(?i)(\d+)\s*([hms])`)
v.units = []struct {
name string
num int64
}{
{"h", 60 * 60},
{"m", 60},
{"s", 1},
}
return v
})
func TimeEstimateParse(timeStr string) (int64, error) {
if timeStr == "" {
return 0, nil
}
var total int64
matches := timeStrGlobalVars().re.FindAllStringSubmatchIndex(timeStr, -1)
if len(matches) == 0 {
return 0, fmt.Errorf("invalid time string: %s", timeStr)
}
if matches[0][0] != 0 || matches[len(matches)-1][1] != len(timeStr) {
return 0, fmt.Errorf("invalid time string: %s", timeStr)
}
for _, match := range matches {
amount, err := strconv.ParseInt(timeStr[match[2]:match[3]], 10, 64)
if err != nil {
return 0, fmt.Errorf("invalid time string: %v", err)
}
unit := timeStr[match[4]:match[5]]
found := false
for _, u := range timeStrGlobalVars().units {
if strings.ToLower(unit) == u.name {
total += amount * u.num
found = true
break
}
}
if !found {
return 0, fmt.Errorf("invalid time unit: %s", unit)
}
}
return total, nil
}
func TimeEstimateString(amount int64) string {
var timeParts []string
for _, u := range timeStrGlobalVars().units {
if amount >= u.num {
num := amount / u.num
amount %= u.num
timeParts = append(timeParts, fmt.Sprintf("%d%s", num, u.name))
}
}
return strings.Join(timeParts, " ")
}

View File

@ -0,0 +1,55 @@
// Copyright 2024 Gitea. All rights reserved.
// SPDX-License-Identifier: MIT
package util
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestTimeStr(t *testing.T) {
t.Run("Parse", func(t *testing.T) {
// Test TimeEstimateParse
tests := []struct {
input string
output int64
err bool
}{
{"1h", 3600, false},
{"1m", 60, false},
{"1s", 1, false},
{"1h 1m 1s", 3600 + 60 + 1, false},
{"1d1x", 0, true},
}
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
output, err := TimeEstimateParse(test.input)
if test.err {
assert.NotNil(t, err)
} else {
assert.Nil(t, err)
}
assert.Equal(t, test.output, output)
})
}
})
t.Run("String", func(t *testing.T) {
tests := []struct {
input int64
output string
}{
{3600, "1h"},
{60, "1m"},
{1, "1s"},
{3600 + 1, "1h 1s"},
}
for _, test := range tests {
t.Run(test.output, func(t *testing.T) {
output := TimeEstimateString(test.input)
assert.Equal(t, test.output, output)
})
}
})
}

View File

@ -1681,27 +1681,34 @@ issues.comment_on_locked = You cannot comment on a locked issue.
issues.delete = Delete issues.delete = Delete
issues.delete.title = Delete this issue? issues.delete.title = Delete this issue?
issues.delete.text = Do you really want to delete this issue? (This will permanently remove all content. Consider closing it instead, if you intend to keep it archived) issues.delete.text = Do you really want to delete this issue? (This will permanently remove all content. Consider closing it instead, if you intend to keep it archived)
issues.tracker = Time Tracker issues.tracker = Time Tracker
issues.start_tracking_short = Start Timer issues.timetracker_timer_start = Start timer
issues.start_tracking = Start Time Tracking issues.timetracker_timer_stop = Stop timer
issues.start_tracking_history = `started working %s` issues.timetracker_timer_discard = Discard timer
issues.timetracker_timer_manually_add = Add Time
issues.time_estimate_placeholder = 1h 2m
issues.time_estimate_set = Set estimated time
issues.time_estimate_display = Estimate: %s
issues.change_time_estimate_at = changed time estimate to <b>%s</b> %s
issues.remove_time_estimate_at = removed time estimate %s
issues.time_estimate_invalid = Time estimate format is invalid
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 Timer issues.stop_tracking_history = worked for <b>%s</b> %s
issues.stop_tracking_history = `stopped working %s`
issues.cancel_tracking = Discard
issues.cancel_tracking_history = `canceled time tracking %s` issues.cancel_tracking_history = `canceled time tracking %s`
issues.add_time = Manually Add Time
issues.del_time = Delete this time log issues.del_time = Delete this time log
issues.add_time_short = Add Time issues.add_time_history = added spent time <b>%s</b> %s
issues.add_time_cancel = Cancel
issues.add_time_history = `added spent time %s`
issues.del_time_history= `deleted spent time %s` issues.del_time_history= `deleted spent time %s`
issues.add_time_manually = Manually Add Time
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.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
issues.invalid_due_date_format = "Due date format must be 'yyyy-mm-dd'." issues.invalid_due_date_format = "Due date format must be 'yyyy-mm-dd'."
issues.error_modifying_due_date = "Failed to modify the due date." issues.error_modifying_due_date = "Failed to modify the due date."

1600
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,15 +12,15 @@
"@github/relative-time-element": "4.4.3", "@github/relative-time-element": "4.4.3",
"@github/text-expander-element": "2.8.0", "@github/text-expander-element": "2.8.0",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "19.12.0", "@primer/octicons": "19.13.0",
"@silverwind/vue3-calendar-heatmap": "2.0.6", "@silverwind/vue3-calendar-heatmap": "2.0.6",
"add-asset-webpack-plugin": "3.0.0", "add-asset-webpack-plugin": "3.0.0",
"ansi_up": "6.0.2", "ansi_up": "6.0.2",
"asciinema-player": "3.8.1", "asciinema-player": "3.8.1",
"chart.js": "4.4.6", "chart.js": "4.4.7",
"chartjs-adapter-dayjs-4": "1.0.4", "chartjs-adapter-dayjs-4": "1.0.4",
"chartjs-plugin-zoom": "2.0.1", "chartjs-plugin-zoom": "2.2.0",
"clippie": "4.1.3", "clippie": "4.1.4",
"cropperjs": "1.6.2", "cropperjs": "1.6.2",
"css-loader": "7.1.2", "css-loader": "7.1.2",
"dayjs": "1.11.13", "dayjs": "1.11.13",
@ -34,7 +34,7 @@
"jquery": "3.7.1", "jquery": "3.7.1",
"katex": "0.16.11", "katex": "0.16.11",
"license-checker-webpack-plugin": "0.2.1", "license-checker-webpack-plugin": "0.2.1",
"mermaid": "11.4.0", "mermaid": "11.4.1",
"mini-css-extract-plugin": "2.9.2", "mini-css-extract-plugin": "2.9.2",
"minimatch": "10.0.1", "minimatch": "10.0.1",
"monaco-editor": "0.52.0", "monaco-editor": "0.52.0",
@ -44,32 +44,32 @@
"postcss": "8.4.49", "postcss": "8.4.49",
"postcss-loader": "8.1.1", "postcss-loader": "8.1.1",
"postcss-nesting": "13.0.1", "postcss-nesting": "13.0.1",
"sortablejs": "1.15.3", "sortablejs": "1.15.6",
"swagger-ui-dist": "5.18.2", "swagger-ui-dist": "5.18.2",
"tailwindcss": "3.4.14", "tailwindcss": "3.4.16",
"throttle-debounce": "5.0.2", "throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tippy.js": "6.3.7", "tippy.js": "6.3.7",
"toastify-js": "1.12.0", "toastify-js": "1.12.0",
"tributejs": "5.1.3", "tributejs": "5.1.3",
"typescript": "5.6.3", "typescript": "5.7.2",
"uint8-to-base64": "0.2.0", "uint8-to-base64": "0.2.0",
"vanilla-colorful": "0.7.2", "vanilla-colorful": "0.7.2",
"vue": "3.5.12", "vue": "3.5.13",
"vue-bar-graph": "2.1.0", "vue-bar-graph": "2.2.0",
"vue-chartjs": "5.3.2", "vue-chartjs": "5.3.2",
"vue-loader": "17.4.2", "vue-loader": "17.4.2",
"webpack": "5.96.1", "webpack": "5.97.0",
"webpack-cli": "5.1.4", "webpack-cli": "5.1.4",
"wrap-ansi": "9.0.0" "wrap-ansi": "9.0.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "4.4.1", "@eslint-community/eslint-plugin-eslint-comments": "4.4.1",
"@playwright/test": "1.48.2", "@playwright/test": "1.49.0",
"@stoplight/spectral-cli": "6.14.0", "@stoplight/spectral-cli": "6.14.2",
"@stylistic/eslint-plugin-js": "2.10.1", "@stylistic/eslint-plugin-js": "2.11.0",
"@stylistic/stylelint-plugin": "3.1.1", "@stylistic/stylelint-plugin": "3.1.1",
"@types/dropzone": "5.7.8", "@types/dropzone": "5.7.9",
"@types/jquery": "3.5.32", "@types/jquery": "3.5.32",
"@types/katex": "0.16.7", "@types/katex": "0.16.7",
"@types/license-checker-webpack-plugin": "0.2.4", "@types/license-checker-webpack-plugin": "0.2.4",
@ -79,38 +79,38 @@
"@types/throttle-debounce": "5.0.2", "@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6", "@types/tinycolor2": "1.4.6",
"@types/toastify-js": "1.12.3", "@types/toastify-js": "1.12.3",
"@typescript-eslint/eslint-plugin": "8.14.0", "@typescript-eslint/eslint-plugin": "8.17.0",
"@typescript-eslint/parser": "8.14.0", "@typescript-eslint/parser": "8.17.0",
"@vitejs/plugin-vue": "5.1.5", "@vitejs/plugin-vue": "5.2.1",
"eslint": "8.57.0", "eslint": "8.57.0",
"eslint-import-resolver-typescript": "3.6.3", "eslint-import-resolver-typescript": "3.7.0",
"eslint-plugin-array-func": "4.0.0", "eslint-plugin-array-func": "4.0.0",
"eslint-plugin-github": "5.0.2", "eslint-plugin-github": "5.1.3",
"eslint-plugin-i": "2.29.1", "eslint-plugin-i": "2.29.1",
"eslint-plugin-no-jquery": "3.0.2", "eslint-plugin-no-jquery": "3.1.0",
"eslint-plugin-no-use-extend-native": "0.5.0", "eslint-plugin-no-use-extend-native": "0.5.0",
"eslint-plugin-playwright": "2.0.1", "eslint-plugin-playwright": "2.1.0",
"eslint-plugin-regexp": "2.6.0", "eslint-plugin-regexp": "2.7.0",
"eslint-plugin-sonarjs": "2.0.4", "eslint-plugin-sonarjs": "2.0.4",
"eslint-plugin-unicorn": "56.0.0", "eslint-plugin-unicorn": "56.0.1",
"eslint-plugin-vitest": "0.4.1", "eslint-plugin-vitest": "0.4.1",
"eslint-plugin-vitest-globals": "1.5.0", "eslint-plugin-vitest-globals": "1.5.0",
"eslint-plugin-vue": "9.31.0", "eslint-plugin-vue": "9.32.0",
"eslint-plugin-vue-scoped-css": "2.8.1", "eslint-plugin-vue-scoped-css": "2.9.0",
"eslint-plugin-wc": "2.2.0", "eslint-plugin-wc": "2.2.0",
"happy-dom": "15.11.3", "happy-dom": "15.11.7",
"markdownlint-cli": "0.42.0", "markdownlint-cli": "0.43.0",
"nolyfill": "1.0.42", "nolyfill": "1.0.42",
"postcss-html": "1.7.0", "postcss-html": "1.7.0",
"stylelint": "16.10.0", "stylelint": "16.11.0",
"stylelint-declaration-block-no-ignored-properties": "2.8.0", "stylelint-declaration-block-no-ignored-properties": "2.8.0",
"stylelint-declaration-strict-value": "1.10.6", "stylelint-declaration-strict-value": "1.10.6",
"stylelint-value-no-unknown-custom-properties": "6.0.1", "stylelint-value-no-unknown-custom-properties": "6.0.1",
"svgo": "3.3.2", "svgo": "3.3.2",
"type-fest": "4.26.1", "type-fest": "4.30.0",
"updates": "16.4.0", "updates": "16.4.0",
"vite-string-plugin": "1.3.4", "vite-string-plugin": "1.3.4",
"vitest": "2.1.4", "vitest": "2.1.8",
"vue-tsc": "2.1.10" "vue-tsc": "2.1.10"
}, },
"browserslist": [ "browserslist": [

106
poetry.lock generated
View File

@ -42,33 +42,33 @@ six = ">=1.13.0"
[[package]] [[package]]
name = "djlint" name = "djlint"
version = "1.36.1" version = "1.36.3"
description = "HTML Template Linter and Formatter" description = "HTML Template Linter and Formatter"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
files = [ files = [
{file = "djlint-1.36.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef40527fd6cd82cdd18f65a6bf5b486b767d2386f6c21f2ebd60e5d88f487fe8"}, {file = "djlint-1.36.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ae7c620b58e16d6bf003bd7de3f71376a7a3daa79dc02e77f3726d5a75243f2"},
{file = "djlint-1.36.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4712de3dea172000a098da6a0cd709d158909b4964ba0f68bee584cef18b4878"}, {file = "djlint-1.36.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e155ce0970d4a28d0a2e9f2e106733a2ad05910eee90e056b056d48049e4a97b"},
{file = "djlint-1.36.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d01c1425170b7059d68a3b01709e1c31d2cd6520a1eb0166e6670fd250518a"}, {file = "djlint-1.36.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e8bb0406e60cc696806aa6226df137618f3889c72f2dbdfa76c908c99151579"},
{file = "djlint-1.36.1-cp310-cp310-win_amd64.whl", hash = "sha256:65585a97d3a37760b4c1fbd089a3573506ad0ab2885119322a66231f911d113f"}, {file = "djlint-1.36.3-cp310-cp310-win_amd64.whl", hash = "sha256:76d32faf988ad58ef2e7a11d04046fc984b98391761bf1b61f9a6044da53d414"},
{file = "djlint-1.36.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:607437a0a230462916858c269bc5dfd15ff71b27d15dfd1ad6e96b3da9cbd8f6"}, {file = "djlint-1.36.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:32f7a5834000fff22e94d1d35f95aaf2e06f2af2cae18af0ed2a4e215d60e730"},
{file = "djlint-1.36.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ddc9ae6b83b288465f6685b24797adbde79952d6e1a5276026e5ef479bac76f"}, {file = "djlint-1.36.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3eb1b9c0be499e63e8822a051e7e55f188ff1ab8172a85d338a8ae21c872060e"},
{file = "djlint-1.36.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:001e5124b0ebab60a2044134abd11ff11dee772e7c3caaa2c8d12eb5d3b1f1dc"}, {file = "djlint-1.36.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c2e0dd1f26eb472b8c84eb70d6482877b6497a1fd031d7534864088f016d5ea"},
{file = "djlint-1.36.1-cp311-cp311-win_amd64.whl", hash = "sha256:095d62f3cabbac08683c51c1d9dacab522b54437a2a317df9e134599360f7b89"}, {file = "djlint-1.36.3-cp311-cp311-win_amd64.whl", hash = "sha256:a06b531ab9d049c46ad4d2365d1857004a1a9dd0c23c8eae94aa0d233c6ec00d"},
{file = "djlint-1.36.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:210f319c2d22489aebc0e9c1acd5015ca3892b66fa35647344511b3c03fcbe82"}, {file = "djlint-1.36.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e66361a865e5e5a4bbcb40f56af7f256fd02cbf9d48b763a40172749cc294084"},
{file = "djlint-1.36.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7aa3db13d7702c35f4e408325061d9d4e84d006c99bb3e55fddf2b2543736923"}, {file = "djlint-1.36.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:36e102b80d83e9ac2e6be9a9ded32fb925945f6dbc7a7156e4415de1b0aa0dba"},
{file = "djlint-1.36.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f907e97f4d67f4423dc71671592891cfd9cd311aeef14db25633f292dbf7048"}, {file = "djlint-1.36.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ac4b7370d80bd82281e57a470de8923ac494ffb571b89d8787cef57c738c69a"},
{file = "djlint-1.36.1-cp312-cp312-win_amd64.whl", hash = "sha256:abadf6b61dc53d81710f230542f57f2d470b7503cd3108ad8a0113271c0514dd"}, {file = "djlint-1.36.3-cp312-cp312-win_amd64.whl", hash = "sha256:107cc56bbef13d60cc0ae774a4d52881bf98e37c02412e573827a3e549217e3a"},
{file = "djlint-1.36.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7f31646435385eec1d4b03dad7bebb5e4078d9893c60d490a685535bd6303c83"}, {file = "djlint-1.36.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2a9f51971d6e63c41ea9b3831c928e1f21ae6fe57e87a3452cfe672d10232433"},
{file = "djlint-1.36.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4399477ac51f9c8147eedbef70aa8465eccba6759d875d1feec6782744aa168a"}, {file = "djlint-1.36.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:080c98714b55d8f0fef5c42beaee8247ebb2e3d46b0936473bd6c47808bb6302"},
{file = "djlint-1.36.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c217b17d3ae3c0e3b5fff57fb708029cceda6e232f5a54ff1b3aeb43a7540"}, {file = "djlint-1.36.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f65a80e0b5cb13d357ea51ca6570b34c2d9d18974c1e57142de760ea27d49ed0"},
{file = "djlint-1.36.1-cp313-cp313-win_amd64.whl", hash = "sha256:1577490802ca4697af3488ed13066c9214ef0f625a96aa20d4f297e37aa19303"}, {file = "djlint-1.36.3-cp313-cp313-win_amd64.whl", hash = "sha256:95ef6b67ef7f2b90d9434bba37d572031079001dc8524add85c00ef0386bda1e"},
{file = "djlint-1.36.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ae356faf8180c7629ca705b7b9d8c9269b2c53273a1887a438a21b8fa263588"}, {file = "djlint-1.36.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e2317a32094d525bc41cd11c8dc064bf38d1b442c99cc3f7c4a2616b5e6ce6e"},
{file = "djlint-1.36.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2237ac5cecd2524960e1684f64ce358624b0d769b7404e5aad415750ad00edc9"}, {file = "djlint-1.36.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e82266c28793cd15f97b93535d72bfbc77306eaaf6b210dd90910383a814ee6c"},
{file = "djlint-1.36.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02c22352a49c053ff6260428ed571afb783011d20afc98b44bbe1dd2fa2d5418"}, {file = "djlint-1.36.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01b2101c2d1b079e8d545e6d9d03487fcca14d2371e44cbfdedee15b0bf4567c"},
{file = "djlint-1.36.1-cp39-cp39-win_amd64.whl", hash = "sha256:99a2debeea2e931b68360306fdbf13861e3d6f96037a9d882f3d4d5e44fdc319"}, {file = "djlint-1.36.3-cp39-cp39-win_amd64.whl", hash = "sha256:15cde63ef28beb5194ff4137883025f125676ece1b574b64a3e1c6daed734639"},
{file = "djlint-1.36.1-py3-none-any.whl", hash = "sha256:950782b396dd82b74622c09d7e4c52328e56a3b03c8ac790c319708e5caa0686"}, {file = "djlint-1.36.3-py3-none-any.whl", hash = "sha256:0c05cd5b76785de2c41a2420c06ffd112800bfc0f9c0f399cc7cea7c42557f4c"},
{file = "djlint-1.36.1.tar.gz", hash = "sha256:f7260637ed72c270fa6dd4a87628e1a21c49b24a46df52e4e26f44d4934fb97c"}, {file = "djlint-1.36.3.tar.gz", hash = "sha256:d85735da34bc7ac93ad8ef9b4822cc2a23d5f0ce33f25438737b8dca1d404f78"},
] ]
[package.dependencies] [package.dependencies]
@ -109,13 +109,13 @@ six = ">=1.13.0"
[[package]] [[package]]
name = "json5" name = "json5"
version = "0.9.28" version = "0.10.0"
description = "A Python implementation of the JSON5 data format." description = "A Python implementation of the JSON5 data format."
optional = false optional = false
python-versions = ">=3.8.0" python-versions = ">=3.8.0"
files = [ files = [
{file = "json5-0.9.28-py3-none-any.whl", hash = "sha256:29c56f1accdd8bc2e037321237662034a7e07921e2b7223281a5ce2c46f0c4df"}, {file = "json5-0.10.0-py3-none-any.whl", hash = "sha256:19b23410220a7271e8377f81ba8aacba2fdd56947fbb137ee5977cbe1f5e8dfa"},
{file = "json5-0.9.28.tar.gz", hash = "sha256:1f82f36e615bc5b42f1bbd49dbc94b12563c56408c6ffa06414ea310890e9a6e"}, {file = "json5-0.10.0.tar.gz", hash = "sha256:e66941c8f0a02026943c52c2eb34ebeb2a6f819a0be05920a6f5243cd30fd559"},
] ]
[package.extras] [package.extras]
@ -299,42 +299,72 @@ files = [
[[package]] [[package]]
name = "six" name = "six"
version = "1.16.0" version = "1.17.0"
description = "Python 2 and 3 compatibility utilities" description = "Python 2 and 3 compatibility utilities"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
files = [ files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
] ]
[[package]] [[package]]
name = "tomli" name = "tomli"
version = "2.1.0" version = "2.2.1"
description = "A lil' TOML parser" description = "A lil' TOML parser"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"}, {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
{file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"},
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"},
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"},
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"},
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"},
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"},
{file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"},
{file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"},
{file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"},
{file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"},
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"},
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"},
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"},
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"},
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"},
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"},
{file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"},
{file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"},
{file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"},
{file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"},
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"},
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"},
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"},
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"},
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"},
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"},
{file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"},
{file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"},
{file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"},
{file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
] ]
[[package]] [[package]]
name = "tqdm" name = "tqdm"
version = "4.67.0" version = "4.67.1"
description = "Fast, Extensible Progress Meter" description = "Fast, Extensible Progress Meter"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "tqdm-4.67.0-py3-none-any.whl", hash = "sha256:0cd8af9d56911acab92182e88d763100d4788bdf421d251616040cc4d44863be"}, {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"},
{file = "tqdm-4.67.0.tar.gz", hash = "sha256:fe5a6f95e6fe0b9755e9469b77b9c3cf850048224ecaa8293d7d2d31f97d869a"}, {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"},
] ]
[package.dependencies] [package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""} colorama = {version = "*", markers = "platform_system == \"Windows\""}
[package.extras] [package.extras]
dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"]
discord = ["requests"] discord = ["requests"]
notebook = ["ipywidgets (>=6)"] notebook = ["ipywidgets (>=6)"]
slack = ["slack-sdk"] slack = ["slack-sdk"]
@ -361,4 +391,4 @@ dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "5cb350262cf59a02e2e4a08f10fde820cd5bf72c3c7b70ae20f6dd990380d099" content-hash = "01b1e2f910276dd20a70ebb665c83415c37531709d90874f5b7a86a5305e2369"

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-sparkles-fill" width="16" height="16" aria-hidden="true"><path d="M9.6 2.279a.426.426 0 0 1 .8 0l.407 1.112a6.39 6.39 0 0 0 3.802 3.802l1.112.407a.426.426 0 0 1 0 .8l-1.112.407a6.39 6.39 0 0 0-3.802 3.802l-.407 1.112a.426.426 0 0 1-.8 0l-.407-1.112a6.39 6.39 0 0 0-3.802-3.802L4.279 8.4a.426.426 0 0 1 0-.8l1.112-.407a6.39 6.39 0 0 0 3.802-3.802zm-4.267 8.837a.178.178 0 0 1 .334 0l.169.464a2.66 2.66 0 0 0 1.584 1.584l.464.169a.178.178 0 0 1 0 .334l-.464.169a2.66 2.66 0 0 0-1.584 1.584l-.169.464a.178.178 0 0 1-.334 0l-.169-.464a2.66 2.66 0 0 0-1.584-1.584l-.464-.169a.178.178 0 0 1 0-.334l.464-.169a2.66 2.66 0 0 0 1.584-1.584zM2.8.14a.213.213 0 0 1 .4 0l.203.556a3.2 3.2 0 0 0 1.901 1.901l.556.203a.213.213 0 0 1 0 .4l-.556.203a3.2 3.2 0 0 0-1.901 1.901L3.2 5.86a.213.213 0 0 1-.4 0l-.203-.556A3.2 3.2 0 0 0 .696 3.403L.14 3.2a.213.213 0 0 1 0-.4l.556-.203A3.2 3.2 0 0 0 2.597.696z"/></svg>

After

Width:  |  Height:  |  Size: 973 B

View File

@ -5,7 +5,7 @@ package-mode = false
python = "^3.10" python = "^3.10"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
djlint = "1.36.1" djlint = "1.36.3"
yamllint = "1.35.1" yamllint = "1.35.1"
[tool.djlint] [tool.djlint]

View File

@ -4,7 +4,6 @@
package repo package repo
import ( import (
"net/http"
"strings" "strings"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -40,8 +39,7 @@ func IssueStopwatch(c *context.Context) {
c.Flash.Success(c.Tr("repo.issues.tracker_auto_close")) c.Flash.Success(c.Tr("repo.issues.tracker_auto_close"))
} }
url := issue.Link() c.JSONRedirect("")
c.Redirect(url, http.StatusSeeOther)
} }
// CancelStopwatch cancel the stopwatch // CancelStopwatch cancel the stopwatch
@ -72,8 +70,7 @@ func CancelStopwatch(c *context.Context) {
}) })
} }
url := issue.Link() c.JSONRedirect("")
c.Redirect(url, http.StatusSeeOther)
} }
// GetActiveStopwatch is the middleware that sets .ActiveStopwatch on context // GetActiveStopwatch is the middleware that sets .ActiveStopwatch on context

View File

@ -5,6 +5,7 @@ package repo
import ( import (
"net/http" "net/http"
"strings"
"time" "time"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -13,6 +14,7 @@ import (
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
issue_service "code.gitea.io/gitea/services/issue"
) )
// AddTimeManually tracks time manually // AddTimeManually tracks time manually
@ -26,19 +28,16 @@ func AddTimeManually(c *context.Context) {
c.NotFound("CanUseTimetracker", nil) c.NotFound("CanUseTimetracker", nil)
return return
} }
url := issue.Link()
if c.HasError() { if c.HasError() {
c.Flash.Error(c.GetErrMsg()) c.JSONError(c.GetErrMsg())
c.Redirect(url)
return return
} }
total := time.Duration(form.Hours)*time.Hour + time.Duration(form.Minutes)*time.Minute total := time.Duration(form.Hours)*time.Hour + time.Duration(form.Minutes)*time.Minute
if total <= 0 { if total <= 0 {
c.Flash.Error(c.Tr("repo.issues.add_time_sum_to_small")) c.JSONError(c.Tr("repo.issues.add_time_sum_to_small"))
c.Redirect(url, http.StatusSeeOther)
return return
} }
@ -47,7 +46,7 @@ func AddTimeManually(c *context.Context) {
return return
} }
c.Redirect(url, http.StatusSeeOther) c.JSONRedirect("")
} }
// DeleteTime deletes tracked time // DeleteTime deletes tracked time
@ -83,5 +82,38 @@ func DeleteTime(c *context.Context) {
} }
c.Flash.Success(c.Tr("repo.issues.del_time_history", util.SecToTime(t.Time))) c.Flash.Success(c.Tr("repo.issues.del_time_history", util.SecToTime(t.Time)))
c.Redirect(issue.Link()) c.JSONRedirect("")
}
func UpdateIssueTimeEstimate(ctx *context.Context) {
issue := GetActionIssue(ctx)
if ctx.Written() {
return
}
if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
ctx.Error(http.StatusForbidden)
return
}
timeStr := strings.TrimSpace(ctx.FormString("time_estimate"))
total, err := util.TimeEstimateParse(timeStr)
if err != nil {
ctx.JSONError(ctx.Tr("repo.issues.time_estimate_invalid"))
return
}
// No time changed
if issue.TimeEstimate == total {
ctx.JSONRedirect("")
return
}
if err := issue_service.ChangeTimeEstimate(ctx, issue, ctx.Doer, total); err != nil {
ctx.ServerError("ChangeTimeEstimate", err)
return
}
ctx.JSONRedirect("")
} }

View File

@ -1235,6 +1235,7 @@ func registerRoutes(m *web.Router) {
m.Post("/cancel", repo.CancelStopwatch) m.Post("/cancel", repo.CancelStopwatch)
}) })
}) })
m.Post("/time_estimate", repo.UpdateIssueTimeEstimate)
m.Post("/reactions/{action}", web.Bind(forms.ReactionForm{}), repo.ChangeIssueReaction) m.Post("/reactions/{action}", web.Bind(forms.ReactionForm{}), repo.ChangeIssueReaction)
m.Post("/lock", reqRepoIssuesOrPullsWriter, web.Bind(forms.IssueLockForm{}), repo.LockIssue) m.Post("/lock", reqRepoIssuesOrPullsWriter, web.Bind(forms.IssueLockForm{}), repo.LockIssue)
m.Post("/unlock", reqRepoIssuesOrPullsWriter, repo.UnlockIssue) m.Post("/unlock", reqRepoIssuesOrPullsWriter, repo.UnlockIssue)

View File

@ -76,6 +76,11 @@ func ToTimelineComment(ctx context.Context, repo *repo_model.Repository, c *issu
// so we check for the "|" delimiter and convert new to legacy format on demand // so we check for the "|" delimiter and convert new to legacy format on demand
c.Content = util.SecToTime(c.Content[1:]) c.Content = util.SecToTime(c.Content[1:])
} }
if c.Type == issues_model.CommentTypeChangeTimeEstimate {
timeSec, _ := util.ToInt64(c.Content)
c.Content = util.TimeEstimateString(timeSec)
}
} }
comment := &api.TimelineComment{ comment := &api.TimelineComment{

View File

@ -43,6 +43,7 @@ var hiddenCommentTypeGroups = hiddenCommentTypeGroupsType{
/*14*/ issues_model.CommentTypeAddTimeManual, /*14*/ issues_model.CommentTypeAddTimeManual,
/*15*/ issues_model.CommentTypeCancelTracking, /*15*/ issues_model.CommentTypeCancelTracking,
/*26*/ issues_model.CommentTypeDeleteTimeManual, /*26*/ issues_model.CommentTypeDeleteTimeManual,
/*38*/ issues_model.CommentTypeChangeTimeEstimate,
}, },
"deadline": { "deadline": {
/*16*/ issues_model.CommentTypeAddedDeadline, /*16*/ issues_model.CommentTypeAddedDeadline,

View File

@ -105,6 +105,13 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode
return nil return nil
} }
// ChangeTimeEstimate changes the time estimate of this issue, as the given user.
func ChangeTimeEstimate(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, timeEstimate int64) (err error) {
issue.TimeEstimate = timeEstimate
return issues_model.ChangeIssueTimeEstimate(ctx, issue, doer, timeEstimate)
}
// ChangeIssueRef changes the branch of this issue, as the given user. // ChangeIssueRef changes the branch of this issue, as the given user.
func ChangeIssueRef(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, ref string) error { func ChangeIssueRef(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, ref string) error {
oldRef := issue.Ref oldRef := issue.Ref

View File

@ -1,60 +1,78 @@
{{if .Repository.IsTimetrackerEnabled ctx}} {{if .Repository.IsTimetrackerEnabled ctx}}
{{if and .CanUseTimetracker (not .Repository.IsArchived)}} {{if and .CanUseTimetracker (not .Repository.IsArchived)}}
<div class="divider"></div> <div class="divider"></div>
<div class="ui timetrack"> <div>
<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.tracker"}}</strong></span> <div class="ui dropdown jump">
<div class="tw-mt-2"> <a class="text muted">
<form method="post" action="{{.Issue.Link}}/times/stopwatch/toggle" id="toggle_stopwatch_form"> <strong>{{ctx.Locale.Tr "repo.issues.tracker"}}</strong> {{svg "octicon-gear"}}
{{$.CsrfTokenHtml}} {{if $.IsStopwatchRunning}}{{svg "octicon-stopwatch"}}{{end}}
</form> </a>
<form method="post" action="{{.Issue.Link}}/times/stopwatch/cancel" id="cancel_stopwatch_form"> <div class="menu">
{{$.CsrfTokenHtml}} <a class="item issue-set-time-estimate show-modal" data-modal="#issue-time-set-estimate-modal">
</form> {{svg "octicon-pencil"}} {{ctx.Locale.Tr "repo.issues.time_estimate_set"}}
{{if $.IsStopwatchRunning}} </a>
<button class="ui fluid button issue-stop-time"> <div class="divider"></div>
{{svg "octicon-stopwatch" 16 "tw-mr-2"}} {{if $.IsStopwatchRunning}}
{{ctx.Locale.Tr "repo.issues.stop_tracking"}} <a class="item issue-stop-time link-action" data-url="{{.Issue.Link}}/times/stopwatch/toggle">
</button> {{svg "octicon-stopwatch"}} {{ctx.Locale.Tr "repo.issues.timetracker_timer_stop"}}
<button class="ui fluid button issue-cancel-time tw-mt-2"> </a>
{{svg "octicon-trash" 16 "tw-mr-2"}} <a class="item issue-cancel-time link-action" data-url="{{.Issue.Link}}/times/stopwatch/cancel">
{{ctx.Locale.Tr "repo.issues.cancel_tracking"}} {{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.issues.timetracker_timer_discard"}}
</button> </a>
{{else}} {{else}}
{{if .HasUserStopwatch}} <a class="item issue-start-time link-action" data-url="{{.Issue.Link}}/times/stopwatch/toggle">
<div class="ui warning message"> {{svg "octicon-stopwatch"}} {{ctx.Locale.Tr "repo.issues.timetracker_timer_start"}}
{{ctx.Locale.Tr "repo.issues.tracking_already_started" .OtherStopwatchURL}} </a>
</div> <a class="item issue-add-time show-modal" data-modal="#issue-time-manually-add-modal">
{{svg "octicon-plus"}} {{ctx.Locale.Tr "repo.issues.timetracker_timer_manually_add"}}
</a>
{{end}} {{end}}
<button class="ui fluid button issue-start-time" data-tooltip-content='{{ctx.Locale.Tr "repo.issues.start_tracking"}}'> </div>
{{svg "octicon-stopwatch" 16 "tw-mr-2"}} </div>
{{ctx.Locale.Tr "repo.issues.start_tracking_short"}}
</button> {{if and (not $.IsStopwatchRunning) .HasUserStopwatch}}
<div class="ui mini modal issue-start-time-modal"> <div class="ui warning message">{{ctx.Locale.Tr "repo.issues.tracking_already_started" .OtherStopwatchURL}}</div>
<div class="header">{{ctx.Locale.Tr "repo.issues.add_time"}}</div> {{end}}
<div class="content">
<form method="post" id="add_time_manual_form" action="{{.Issue.Link}}/times/add" class="ui input fluid tw-gap-2"> {{if .Issue.TimeEstimate}}
{{$.CsrfTokenHtml}} <div class="tw-my-2">{{ctx.Locale.Tr "repo.issues.time_estimate_display" (TimeEstimateString .Issue.TimeEstimate)}}</div>
<input placeholder='{{ctx.Locale.Tr "repo.issues.add_time_hours"}}' type="number" name="hours"> {{end}}
<input placeholder='{{ctx.Locale.Tr "repo.issues.add_time_minutes"}}' type="number" name="minutes" class="ui compact">
</form> {{/* set time estimate modal */}}
</div> <div class="ui mini modal" id="issue-time-set-estimate-modal">
<div class="header">{{ctx.Locale.Tr "repo.issues.time_estimate_set"}}</div>
<form method="post" class="ui form form-fetch-action" action="{{.Issue.Link}}/time_estimate">
<div class="content">
{{$.CsrfTokenHtml}}
<input name="time_estimate" placeholder="{{ctx.Locale.Tr "repo.issues.time_estimate_placeholder"}}" value="{{TimeEstimateString .Issue.TimeEstimate}}">
<div class="actions"> <div class="actions">
<button class="ui primary approve button">{{ctx.Locale.Tr "repo.issues.add_time_short"}}</button> <button class="ui cancel button">{{ctx.Locale.Tr "cancel"}}</button>
<button class="ui cancel button">{{ctx.Locale.Tr "repo.issues.add_time_cancel"}}</button> <button class="ui primary button">{{ctx.Locale.Tr "repo.issues.save"}}</button>
</div> </div>
</div> </div>
<button class="ui fluid button issue-add-time tw-mt-2" data-tooltip-content='{{ctx.Locale.Tr "repo.issues.add_time"}}'> </form>
{{svg "octicon-plus" 16 "tw-mr-2"}} </div>
{{ctx.Locale.Tr "repo.issues.add_time_short"}}
</button> {{/* manually add time modal */}}
{{end}} <div class="ui mini modal" id="issue-time-manually-add-modal">
<div class="header">{{ctx.Locale.Tr "repo.issues.add_time_manually"}}</div>
<form method="post" class="ui form form-fetch-action" action="{{.Issue.Link}}/times/add">
<div class="content flex-text-block">
{{$.CsrfTokenHtml}}
<input placeholder='{{ctx.Locale.Tr "repo.issues.add_time_hours"}}' type="number" name="hours">:
<input placeholder='{{ctx.Locale.Tr "repo.issues.add_time_minutes"}}' type="number" name="minutes">
</div>
<div class="actions">
<button class="ui cancel button">{{ctx.Locale.Tr "cancel"}}</button>
<button class="ui primary button">{{ctx.Locale.Tr "repo.issues.timetracker_timer_manually_add"}}</button>
</div>
</form>
</div> </div>
</div> </div>
{{end}} {{end}}
{{if .WorkingUsers}} {{if .WorkingUsers}}
<div class="divider"></div> <div class="ui comments tw-mt-2">
<div class="ui comments"> {{ctx.Locale.Tr "repo.issues.time_spent_from_all_authors" ($.Issue.TotalTrackedTime | Sec2Time)}}
<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.time_spent_from_all_authors" ($.Issue.TotalTrackedTime | Sec2Time)}}</strong></span>
<div> <div>
{{range $user, $trackedtime := .WorkingUsers}} {{range $user, $trackedtime := .WorkingUsers}}
<div class="comment tw-mt-2"> <div class="comment tw-mt-2">

View File

@ -12,7 +12,8 @@
26 = DELETE_TIME_MANUAL, 27 = REVIEW_REQUEST, 28 = MERGE_PULL_REQUEST, 26 = DELETE_TIME_MANUAL, 27 = REVIEW_REQUEST, 28 = MERGE_PULL_REQUEST,
29 = PULL_PUSH_EVENT, 30 = PROJECT_CHANGED, 31 = PROJECT_BOARD_CHANGED 29 = PULL_PUSH_EVENT, 30 = PROJECT_CHANGED, 31 = PROJECT_BOARD_CHANGED
32 = DISMISSED_REVIEW, 33 = COMMENT_TYPE_CHANGE_ISSUE_REF, 34 = PR_SCHEDULE_TO_AUTO_MERGE, 32 = DISMISSED_REVIEW, 33 = COMMENT_TYPE_CHANGE_ISSUE_REF, 34 = PR_SCHEDULE_TO_AUTO_MERGE,
35 = CANCEL_SCHEDULED_AUTO_MERGE_PR, 36 = PIN_ISSUE, 37 = UNPIN_ISSUE --> 35 = CANCEL_SCHEDULED_AUTO_MERGE_PR, 36 = PIN_ISSUE, 37 = UNPIN_ISSUE,
38 = COMMENT_TYPE_CHANGE_TIME_ESTIMATE -->
{{if eq .Type 0}} {{if eq .Type 0}}
<div class="timeline-item comment" id="{{.HashTag}}"> <div class="timeline-item comment" id="{{.HashTag}}">
{{if .OriginalAuthor}} {{if .OriginalAuthor}}
@ -250,18 +251,11 @@
{{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/avatarlink" dict "user" .Poster}}
<span class="text grey muted-links"> <span class="text grey muted-links">
{{template "shared/user/authorlink" .Poster}} {{template "shared/user/authorlink" .Poster}}
{{ctx.Locale.Tr "repo.issues.stop_tracking_history" $createdStr}} {{$timeStr := .RenderedContent}} {{/* compatibility with time comments made before v1.21 */}}
{{if not $timeStr}}{{$timeStr = .Content|Sec2Time}}{{end}}
{{ctx.Locale.Tr "repo.issues.stop_tracking_history" $timeStr $createdStr}}
</span> </span>
{{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}} {{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}}
<div class="detail flex-text-block">
{{svg "octicon-clock"}}
{{if .RenderedContent}}
{{/* compatibility with time comments made before v1.21 */}}
<span class="text grey muted-links">{{.RenderedContent}}</span>
{{else}}
<span class="text grey muted-links">{{.Content|Sec2Time}}</span>
{{end}}
</div>
</div> </div>
{{else if eq .Type 14}} {{else if eq .Type 14}}
<div class="timeline-item event" id="{{.HashTag}}"> <div class="timeline-item event" id="{{.HashTag}}">
@ -269,18 +263,11 @@
{{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/avatarlink" dict "user" .Poster}}
<span class="text grey muted-links"> <span class="text grey muted-links">
{{template "shared/user/authorlink" .Poster}} {{template "shared/user/authorlink" .Poster}}
{{ctx.Locale.Tr "repo.issues.add_time_history" $createdStr}} {{$timeStr := .RenderedContent}} {{/* compatibility with time comments made before v1.21 */}}
{{if not $timeStr}}{{$timeStr = .Content|Sec2Time}}{{end}}
{{ctx.Locale.Tr "repo.issues.add_time_history" $timeStr $createdStr}}
</span> </span>
{{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}} {{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}}
<div class="detail flex-text-block">
{{svg "octicon-clock"}}
{{if .RenderedContent}}
{{/* compatibility with time comments made before v1.21 */}}
<span class="text grey muted-links">{{.RenderedContent}}</span>
{{else}}
<span class="text grey muted-links">{{.Content|Sec2Time}}</span>
{{end}}
</div>
</div> </div>
{{else if eq .Type 15}} {{else if eq .Type 15}}
<div class="timeline-item event" id="{{.HashTag}}"> <div class="timeline-item event" id="{{.HashTag}}">
@ -703,6 +690,20 @@
{{else}}{{ctx.Locale.Tr "repo.issues.unpin_comment" $createdStr}}{{end}} {{else}}{{ctx.Locale.Tr "repo.issues.unpin_comment" $createdStr}}{{end}}
</span> </span>
</div> </div>
{{else if eq .Type 38}}
<div class="timeline-item event" id="{{.HashTag}}">
<span class="badge">{{svg "octicon-clock"}}</span>
{{template "shared/user/avatarlink" dict "Context" $.Context "user" .Poster}}
<span class="text grey muted-links">
{{template "shared/user/authorlink" .Poster}}
{{$timeStr := .Content|TimeEstimateString}}
{{if $timeStr}}
{{ctx.Locale.Tr "repo.issues.change_time_estimate_at" $timeStr $createdStr}}
{{else}}
{{ctx.Locale.Tr "repo.issues.remove_time_estimate_at" $createdStr}}
{{end}}
</span>
</div>
{{end}} {{end}}
{{end}} {{end}}
{{end}} {{end}}

View File

@ -2,14 +2,10 @@
{{if (not .comment.Time.Deleted)}} {{if (not .comment.Time.Deleted)}}
{{if (or .ctxData.IsAdmin (and .ctxData.IsSigned (eq .ctxData.SignedUserID .comment.PosterID)))}} {{if (or .ctxData.IsAdmin (and .ctxData.IsSigned (eq .ctxData.SignedUserID .comment.PosterID)))}}
<span class="tw-float-right"> <span class="tw-float-right">
<div class="ui mini modal issue-delete-time-modal" data-id="{{.comment.Time.ID}}"> <button class="ui icon button compact mini link-action" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.del_time"}}"
<form method="post" class="delete-time-form" action="{{.ctxData.RepoLink}}/issues/{{.ctxData.Issue.Index}}/times/{{.comment.TimeID}}/delete"> data-url="{{.ctxData.RepoLink}}/issues/{{.ctxData.Issue.Index}}/times/{{.comment.TimeID}}/delete?id={{.comment.Time.ID}}"
{{.ctxData.CsrfTokenHtml}} data-modal-confirm="{{ctx.Locale.Tr "repo.issues.del_time"}}"
</form> >
<div class="header">{{ctx.Locale.Tr "repo.issues.del_time"}}</div>
{{template "base/modal_actions_confirm"}}
</div>
<button class="ui icon button compact mini issue-delete-time" data-id="{{.comment.Time.ID}}" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.del_time"}}">
{{svg "octicon-trash"}} {{svg "octicon-trash"}}
</button> </button>
</span> </span>

View File

@ -53,20 +53,18 @@
</div> </div>
<div class="inline field"> <div class="inline field">
<label>{{ctx.Locale.Tr "repo.fork_branch"}}</label> <label>{{ctx.Locale.Tr "repo.fork_branch"}}</label>
<div class="ui selection dropdown"> <div class="ui selection dropdown ellipsis-items-nowrap">
<input type="hidden" id="fork_single_branch" name="fork_single_branch" value="" required> <input type="hidden" id="fork_single_branch" name="fork_single_branch" value="" required>
<span class="text truncated-item-container" data-value="" title="{{ctx.Locale.Tr "repo.all_branches"}}"> <div class="text" title="{{ctx.Locale.Tr "repo.all_branches"}}">
<span class="truncated-item-name">{{ctx.Locale.Tr "repo.all_branches"}}</span> <span class="truncated-item-name">{{ctx.Locale.Tr "repo.all_branches"}}</span>
</span> </div>
{{svg "octicon-triangle-down" 14 "dropdown icon"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu"> <div class="menu">
<div class="item truncated-item-container" data-value="" title="{{ctx.Locale.Tr "repo.all_branches"}}"> <div class="item" data-value="" title="{{ctx.Locale.Tr "repo.all_branches"}}">
<span class="truncated-item-name">{{ctx.Locale.Tr "repo.all_branches"}}</span> {{ctx.Locale.Tr "repo.all_branches"}}
</div> </div>
{{range .Branches}} {{range .Branches}}
<div class="item truncated-item-container" data-value="{{.}}" title="{{.}}"> <div class="item" data-value="{{.}}" title="{{.}}">{{.}}</div>
<span class="truncated-item-name">{{.}}</span>
</div>
{{end}} {{end}}
</div> </div>
</div> </div>

View File

@ -24,15 +24,9 @@ func NewHTMLParser(t testing.TB, body *bytes.Buffer) *HTMLDoc {
return &HTMLDoc{doc: doc} return &HTMLDoc{doc: doc}
} }
// GetInputValueByID for get input value by id
func (doc *HTMLDoc) GetInputValueByID(id string) string {
text, _ := doc.doc.Find("#" + id).Attr("value")
return text
}
// GetInputValueByName for get input value by name // GetInputValueByName for get input value by name
func (doc *HTMLDoc) GetInputValueByName(name string) string { func (doc *HTMLDoc) GetInputValueByName(name string) string {
text, _ := doc.doc.Find("input[name=\"" + name + "\"]").Attr("value") text, _ := doc.doc.Find(`input[name="` + name + `"]`).Attr("value")
return text return text
} }

View File

@ -9,7 +9,6 @@ import (
"testing" "testing"
"time" "time"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -17,22 +16,24 @@ import (
func TestViewTimetrackingControls(t *testing.T) { func TestViewTimetrackingControls(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
testViewTimetrackingControls(t, session, "user2", "repo1", "1", true)
// user2/repo1
}
func TestNotViewTimetrackingControls(t *testing.T) { t.Run("Exist", func(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrintCurrentTest(t)()
session := loginUser(t, "user5") session := loginUser(t, "user2")
testViewTimetrackingControls(t, session, "user2", "repo1", "1", false) testViewTimetrackingControls(t, session, "user2", "repo1", "1", true)
// user2/repo1 })
}
func TestViewTimetrackingControlsDisabled(t *testing.T) { t.Run("Non-exist", func(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrintCurrentTest(t)()
session := loginUser(t, "user2") session := loginUser(t, "user5")
testViewTimetrackingControls(t, session, "org3", "repo3", "1", false) testViewTimetrackingControls(t, session, "user2", "repo1", "1", false)
})
t.Run("Disabled", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
session := loginUser(t, "user2")
testViewTimetrackingControls(t, session, "org3", "repo3", "1", false)
})
} }
func testViewTimetrackingControls(t *testing.T, session *TestSession, user, repo, issue string, canTrackTime bool) { func testViewTimetrackingControls(t *testing.T, session *TestSession, user, repo, issue string, canTrackTime bool) {
@ -41,40 +42,40 @@ func testViewTimetrackingControls(t *testing.T, session *TestSession, user, repo
htmlDoc := NewHTMLParser(t, resp.Body) htmlDoc := NewHTMLParser(t, resp.Body)
htmlDoc.AssertElement(t, ".timetrack .issue-start-time", canTrackTime) htmlDoc.AssertElement(t, ".issue-start-time", canTrackTime)
htmlDoc.AssertElement(t, ".timetrack .issue-add-time", canTrackTime) htmlDoc.AssertElement(t, ".issue-add-time", canTrackTime)
req = NewRequestWithValues(t, "POST", path.Join(user, repo, "issues", issue, "times", "stopwatch", "toggle"), map[string]string{ issueLink := path.Join(user, repo, "issues", issue)
req = NewRequestWithValues(t, "POST", path.Join(issueLink, "times", "stopwatch", "toggle"), map[string]string{
"_csrf": htmlDoc.GetCSRF(), "_csrf": htmlDoc.GetCSRF(),
}) })
if canTrackTime { if canTrackTime {
resp = session.MakeRequest(t, req, http.StatusSeeOther) session.MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", test.RedirectURL(resp)) req = NewRequest(t, "GET", issueLink)
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc = NewHTMLParser(t, resp.Body) htmlDoc = NewHTMLParser(t, resp.Body)
events := htmlDoc.doc.Find(".event > span.text") events := htmlDoc.doc.Find(".event > span.text")
assert.Contains(t, events.Last().Text(), "started working") assert.Contains(t, events.Last().Text(), "started working")
htmlDoc.AssertElement(t, ".timetrack .issue-stop-time", true) htmlDoc.AssertElement(t, ".issue-stop-time", true)
htmlDoc.AssertElement(t, ".timetrack .issue-cancel-time", true) htmlDoc.AssertElement(t, ".issue-cancel-time", true)
// Sleep for 1 second to not get wrong order for stopping timer // Sleep for 1 second to not get wrong order for stopping timer
time.Sleep(time.Second) time.Sleep(time.Second)
req = NewRequestWithValues(t, "POST", path.Join(user, repo, "issues", issue, "times", "stopwatch", "toggle"), map[string]string{ req = NewRequestWithValues(t, "POST", path.Join(issueLink, "times", "stopwatch", "toggle"), map[string]string{
"_csrf": htmlDoc.GetCSRF(), "_csrf": htmlDoc.GetCSRF(),
}) })
resp = session.MakeRequest(t, req, http.StatusSeeOther) session.MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", test.RedirectURL(resp)) req = NewRequest(t, "GET", issueLink)
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc = NewHTMLParser(t, resp.Body) htmlDoc = NewHTMLParser(t, resp.Body)
events = htmlDoc.doc.Find(".event > span.text") events = htmlDoc.doc.Find(".event > span.text")
assert.Contains(t, events.Last().Text(), "stopped working") assert.Contains(t, events.Last().Text(), "worked for ")
htmlDoc.AssertElement(t, ".event .detail .octicon-clock", true)
} else { } else {
session.MakeRequest(t, req, http.StatusNotFound) session.MakeRequest(t, req, http.StatusNotFound)
} }

View File

@ -11,37 +11,6 @@ import {initRepoIssueSidebar} from './repo-issue-sidebar.ts';
const {appSubUrl} = window.config; const {appSubUrl} = window.config;
export function initRepoIssueTimeTracking() {
$(document).on('click', '.issue-add-time', () => {
$('.issue-start-time-modal').modal({
duration: 200,
onApprove() {
$('#add_time_manual_form').trigger('submit');
},
}).modal('show');
$('.issue-start-time-modal input').on('keydown', (e) => {
if (e.key === 'Enter') {
$('#add_time_manual_form').trigger('submit');
}
});
});
$(document).on('click', '.issue-start-time, .issue-stop-time', () => {
$('#toggle_stopwatch_form').trigger('submit');
});
$(document).on('click', '.issue-cancel-time', () => {
$('#cancel_stopwatch_form').trigger('submit');
});
$(document).on('click', 'button.issue-delete-time', function () {
const sel = `.issue-delete-time-modal[data-id="${$(this).data('id')}"]`;
$(sel).modal({
duration: 200,
onApprove() {
$(`${sel} form`).trigger('submit');
},
}).modal('show');
});
}
/** /**
* @param {HTMLElement} item * @param {HTMLElement} item
*/ */

View File

@ -26,7 +26,6 @@ import {initPdfViewer} from './render/pdf.ts';
import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.ts'; import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.ts';
import { import {
initRepoIssueReferenceRepositorySearch, initRepoIssueReferenceRepositorySearch,
initRepoIssueTimeTracking,
initRepoIssueWipTitle, initRepoIssueWipTitle,
initRepoPullRequestMergeInstruction, initRepoPullRequestMergeInstruction,
initRepoPullRequestAllowMaintainerEdit, initRepoPullRequestAllowMaintainerEdit,
@ -184,7 +183,6 @@ onDomReady(() => {
initRepoIssueList, initRepoIssueList,
initRepoIssueSidebarList, initRepoIssueSidebarList,
initRepoIssueReferenceRepositorySearch, initRepoIssueReferenceRepositorySearch,
initRepoIssueTimeTracking,
initRepoIssueWipTitle, initRepoIssueWipTitle,
initRepoMigration, initRepoMigration,
initRepoMigrationStatusChecker, initRepoMigrationStatusChecker,