mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	Prettify number of issues (#17760)
* Prettify number of issues - Use the PrettyNumber function to add commas in large amount of issues. * Use client-side formatting * prettify on both server and client * remove unused i18n entries * handle more cases, support other int types in PrettyNumber * specify locale to avoid issues with node default locale * remove superfluos argument * introduce template helper, octicon tweaks, js refactor * Update modules/templates/helper.go * Apply some suggestions. * Add comment * Update templates/user/dashboard/issues.tmpl Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
		| @@ -24,6 +24,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
|  | ||||
| 	"github.com/dustin/go-humanize" | ||||
| ) | ||||
| @@ -143,9 +144,9 @@ func FileSize(s int64) string { | ||||
| } | ||||
|  | ||||
| // PrettyNumber produces a string form of the given number in base 10 with | ||||
| // commas after every three orders of magnitud | ||||
| func PrettyNumber(v int64) string { | ||||
| 	return humanize.Comma(v) | ||||
| // commas after every three orders of magnitude | ||||
| func PrettyNumber(i interface{}) string { | ||||
| 	return humanize.Comma(util.NumberIntoInt64(i)) | ||||
| } | ||||
|  | ||||
| // Subtract deals with subtraction of all types of number. | ||||
|   | ||||
| @@ -117,6 +117,7 @@ func TestFileSize(t *testing.T) { | ||||
|  | ||||
| func TestPrettyNumber(t *testing.T) { | ||||
| 	assert.Equal(t, "23,342,432", PrettyNumber(23342432)) | ||||
| 	assert.Equal(t, "23,342,432", PrettyNumber(int32(23342432))) | ||||
| 	assert.Equal(t, "0", PrettyNumber(0)) | ||||
| 	assert.Equal(t, "-100,000", PrettyNumber(-100000)) | ||||
| } | ||||
|   | ||||
| @@ -98,18 +98,19 @@ func NewFuncMap() []template.FuncMap { | ||||
| 		"CustomEmojis": func() map[string]string { | ||||
| 			return setting.UI.CustomEmojisMap | ||||
| 		}, | ||||
| 		"Safe":          Safe, | ||||
| 		"SafeJS":        SafeJS, | ||||
| 		"JSEscape":      JSEscape, | ||||
| 		"Str2html":      Str2html, | ||||
| 		"TimeSince":     timeutil.TimeSince, | ||||
| 		"TimeSinceUnix": timeutil.TimeSinceUnix, | ||||
| 		"RawTimeSince":  timeutil.RawTimeSince, | ||||
| 		"FileSize":      base.FileSize, | ||||
| 		"PrettyNumber":  base.PrettyNumber, | ||||
| 		"Subtract":      base.Subtract, | ||||
| 		"EntryIcon":     base.EntryIcon, | ||||
| 		"MigrationIcon": MigrationIcon, | ||||
| 		"Safe":           Safe, | ||||
| 		"SafeJS":         SafeJS, | ||||
| 		"JSEscape":       JSEscape, | ||||
| 		"Str2html":       Str2html, | ||||
| 		"TimeSince":      timeutil.TimeSince, | ||||
| 		"TimeSinceUnix":  timeutil.TimeSinceUnix, | ||||
| 		"RawTimeSince":   timeutil.RawTimeSince, | ||||
| 		"FileSize":       base.FileSize, | ||||
| 		"PrettyNumber":   base.PrettyNumber, | ||||
| 		"JsPrettyNumber": JsPrettyNumber, | ||||
| 		"Subtract":       base.Subtract, | ||||
| 		"EntryIcon":      base.EntryIcon, | ||||
| 		"MigrationIcon":  MigrationIcon, | ||||
| 		"Add": func(a ...int) int { | ||||
| 			sum := 0 | ||||
| 			for _, val := range a { | ||||
| @@ -1005,3 +1006,11 @@ func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteNa | ||||
|  | ||||
| 	return a | ||||
| } | ||||
|  | ||||
| // JsPrettyNumber renders a number using english decimal separators, e.g. 1,200 and subsequent | ||||
| // JS will replace the number with locale-specific separators, based on the user's selected language | ||||
| func JsPrettyNumber(i interface{}) template.HTML { | ||||
| 	num := util.NumberIntoInt64(i) | ||||
|  | ||||
| 	return template.HTML(`<span class="js-pretty-number" data-value="` + strconv.FormatInt(num, 10) + `">` + base.PrettyNumber(num) + `</span>`) | ||||
| } | ||||
|   | ||||
| @@ -224,3 +224,21 @@ func Dedent(s string) string { | ||||
| 	} | ||||
| 	return strings.TrimSpace(s) | ||||
| } | ||||
|  | ||||
| // NumberIntoInt64 transform a given int into int64. | ||||
| func NumberIntoInt64(number interface{}) int64 { | ||||
| 	var value int64 | ||||
| 	switch v := number.(type) { | ||||
| 	case int: | ||||
| 		value = int64(v) | ||||
| 	case int8: | ||||
| 		value = int64(v) | ||||
| 	case int16: | ||||
| 		value = int64(v) | ||||
| 	case int32: | ||||
| 		value = int64(v) | ||||
| 	case int64: | ||||
| 		value = v | ||||
| 	} | ||||
| 	return value | ||||
| } | ||||
|   | ||||
| @@ -1259,8 +1259,6 @@ issues.change_ref_at = `changed reference from <b><strike>%s</strike></b> to <b> | ||||
| issues.remove_ref_at = `removed reference <b>%s</b> %s` | ||||
| issues.add_ref_at = `added reference <b>%s</b> %s` | ||||
| issues.delete_branch_at = `deleted branch <b>%s</b> %s` | ||||
| issues.open_tab = %d Open | ||||
| issues.close_tab = %d Closed | ||||
| issues.filter_label = Label | ||||
| issues.filter_label_exclude = `Use <code>alt</code> + <code>click/enter</code> to exclude labels` | ||||
| issues.filter_label_no_select = All labels | ||||
| @@ -1613,8 +1611,6 @@ pulls.auto_merge_newly_scheduled_comment = `scheduled this pull request to auto | ||||
| pulls.auto_merge_canceled_schedule_comment = `canceled auto merging this pull request when all checks succeed %[1]s` | ||||
|  | ||||
| milestones.new = New Milestone | ||||
| milestones.open_tab = %d Open | ||||
| milestones.close_tab = %d Closed | ||||
| milestones.closed = Closed %s | ||||
| milestones.update_ago = Updated %s ago | ||||
| milestones.no_due_date = No due date | ||||
|   | ||||
| @@ -18,11 +18,11 @@ | ||||
| 				<div class="ui compact tiny menu"> | ||||
| 					<a class="item{{if not .IsShowClosed}} active{{end}}" href="{{.RepoLink}}/milestones?state=open&q={{$.Keyword}}"> | ||||
| 						{{svg "octicon-milestone" 16 "mr-3"}} | ||||
| 						{{.i18n.Tr "repo.milestones.open_tab" .OpenCount}} | ||||
| 						{{JsPrettyNumber .OpenCount}} {{.i18n.Tr "repo.issues.open_title"}} | ||||
| 					</a> | ||||
| 					<a class="item{{if .IsShowClosed}} active{{end}}" href="{{.RepoLink}}/milestones?state=closed&q={{$.Keyword}}"> | ||||
| 						{{svg "octicon-milestone" 16 "mr-3"}} | ||||
| 						{{.i18n.Tr "repo.milestones.close_tab" .ClosedCount}} | ||||
| 						{{svg "octicon-check" 16 "mr-3"}} | ||||
| 						{{JsPrettyNumber .ClosedCount}} {{.i18n.Tr "repo.issues.closed_title"}} | ||||
| 					</a> | ||||
| 				</div> | ||||
| 			</div> | ||||
| @@ -83,8 +83,10 @@ | ||||
| 							{{end}} | ||||
| 						{{end}} | ||||
| 						<span class="issue-stats"> | ||||
| 							{{svg "octicon-issue-opened"}} {{$.i18n.Tr "repo.issues.open_tab" .NumOpenIssues}} | ||||
| 							{{svg "octicon-issue-closed"}} {{$.i18n.Tr "repo.issues.close_tab" .NumClosedIssues}} | ||||
| 							{{svg "octicon-issue-opened" 16 "mr-3"}} | ||||
| 							{{JsPrettyNumber .NumOpenIssues}} {{$.i18n.Tr "repo.issues.open_title"}} | ||||
| 							{{svg "octicon-check" 16 "mr-3"}} | ||||
| 							{{JsPrettyNumber .NumClosedIssues}} {{$.i18n.Tr "repo.issues.closed_title"}} | ||||
| 							{{if .TotalTrackedTime}}{{svg "octicon-clock"}} {{.TotalTrackedTime|Sec2Time}}{{end}} | ||||
| 							{{if .UpdatedUnix}}{{svg "octicon-clock"}} {{$.i18n.Tr "repo.milestones.update_ago" (.TimeSinceUpdate|Sec2Time)}}{{end}} | ||||
| 						</span> | ||||
|   | ||||
| @@ -1,10 +1,14 @@ | ||||
| <div class="ui compact tiny menu"> | ||||
| 	<a class="{{if not .IsShowClosed}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state=open&labels={{.SelectLabels}}&milestone={{.MilestoneID}}&assignee={{.AssigneeID}}"> | ||||
| 		{{svg "octicon-issue-opened" 16 "mr-3"}} | ||||
| 		{{.i18n.Tr "repo.issues.open_tab" .IssueStats.OpenCount}} | ||||
| 		{{if .PageIsPullList}} | ||||
| 			{{svg "octicon-git-pull-request" 16 "mr-3"}} | ||||
| 		{{else}} | ||||
| 			{{svg "octicon-issue-opened" 16 "mr-3"}} | ||||
| 		{{end}} | ||||
| 		{{JsPrettyNumber .IssueStats.OpenCount}} {{.i18n.Tr "repo.issues.open_title"}} | ||||
| 	</a> | ||||
| 	<a class="{{if .IsShowClosed}}active{{end}} item" href="{{$.Link}}?q={{$.Keyword}}&type={{.ViewType}}&sort={{$.SortType}}&state=closed&labels={{.SelectLabels}}&milestone={{.MilestoneID}}&assignee={{.AssigneeID}}"> | ||||
| 		{{svg "octicon-issue-closed" 16 "mr-3"}} | ||||
| 		{{.i18n.Tr "repo.issues.close_tab" .IssueStats.ClosedCount}} | ||||
| 		{{svg "octicon-check" 16 "mr-3"}} | ||||
| 		{{JsPrettyNumber .IssueStats.ClosedCount}} {{.i18n.Tr "repo.issues.closed_title"}} | ||||
| 	</a> | ||||
| </div> | ||||
|   | ||||
| @@ -14,12 +14,12 @@ | ||||
| 		{{template "base/alert" .}} | ||||
| 		<div class="ui compact tiny menu"> | ||||
| 			<a class="item{{if not .IsShowClosed}} active{{end}}" href="{{.RepoLink}}/projects?state=open"> | ||||
| 				{{svg "octicon-project" 16 "mr-2"}} | ||||
| 				{{.i18n.Tr "repo.issues.open_tab" .OpenCount}} | ||||
| 				{{svg "octicon-project" 16 "mr-3"}} | ||||
| 				{{JsPrettyNumber .OpenCount}} {{.i18n.Tr "repo.issues.open_title"}} | ||||
| 			</a> | ||||
| 			<a class="item{{if .IsShowClosed}} active{{end}}" href="{{.RepoLink}}/projects?state=closed"> | ||||
| 				{{svg "octicon-check" 16 "mr-2"}} | ||||
| 				{{.i18n.Tr "repo.milestones.close_tab" .ClosedCount}} | ||||
| 				{{svg "octicon-check" 16 "mr-3"}} | ||||
| 				{{JsPrettyNumber .ClosedCount}} {{.i18n.Tr "repo.issues.closed_title"}} | ||||
| 			</a> | ||||
| 		</div> | ||||
|  | ||||
| @@ -47,8 +47,10 @@ | ||||
| 							{{svg "octicon-clock"}} {{$.i18n.Tr "repo.milestones.closed" $closedDate|Str2html}} | ||||
| 						{{end}} | ||||
| 						<span class="issue-stats"> | ||||
| 							{{svg "octicon-issue-opened"}} {{$.i18n.Tr "repo.issues.open_tab" .NumOpenIssues}} | ||||
| 							{{svg "octicon-issue-closed"}} {{$.i18n.Tr "repo.issues.close_tab" .NumClosedIssues}} | ||||
| 							{{svg "octicon-issue-opened" 16 "mr-3"}} | ||||
| 							{{JsPrettyNumber .NumOpenIssues}} {{$.i18n.Tr "repo.issues.open_title"}} | ||||
| 							{{svg "octicon-check" 16 "mr-3"}} | ||||
| 							{{JsPrettyNumber .NumClosedIssues}} {{$.i18n.Tr "repo.issues.closed_title"}} | ||||
| 						</span> | ||||
| 					</div> | ||||
| 					{{if and (or $.CanWriteIssues $.CanWritePulls) (not $.Repository.IsArchived)}} | ||||
|   | ||||
| @@ -61,11 +61,11 @@ | ||||
| 						<div class="ui compact tiny menu"> | ||||
| 							<a class="item{{if not .IsShowClosed}} active{{end}}" href="{{.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=open&q={{$.Keyword}}"> | ||||
| 								{{svg "octicon-issue-opened" 16 "mr-3"}} | ||||
| 								{{.i18n.Tr "repo.issues.open_tab" .IssueStats.OpenCount}} | ||||
| 								{{JsPrettyNumber .ShownIssueStats.OpenCount}} {{.i18n.Tr "repo.issues.open_title"}} | ||||
| 							</a> | ||||
| 							<a class="item{{if .IsShowClosed}} active{{end}}" href="{{.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=closed&q={{$.Keyword}}"> | ||||
| 								{{svg "octicon-issue-closed" 16 "mr-3"}} | ||||
| 								{{.i18n.Tr "repo.issues.close_tab" .IssueStats.ClosedCount}} | ||||
| 								{{JsPrettyNumber .ShownIssueStats.ClosedCount}} {{.i18n.Tr "repo.issues.closed_title"}} | ||||
| 							</a> | ||||
| 						</div> | ||||
| 					</div> | ||||
|   | ||||
| @@ -38,12 +38,12 @@ | ||||
| 					<div class="column"> | ||||
| 						<div class="ui compact tiny menu"> | ||||
| 							<a class="item{{if not .IsShowClosed}} active{{end}}" href="{{.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=open&q={{$.Keyword}}"> | ||||
| 								{{svg "octicon-issue-opened" 16 "mr-3"}} | ||||
| 								{{.i18n.Tr "repo.milestones.open_tab" .MilestoneStats.OpenCount}} | ||||
| 								{{svg "octicon-milestone" 16 "mr-3"}} | ||||
| 								{{JsPrettyNumber .MilestoneStats.OpenCount}} {{.i18n.Tr "repo.issues.open_title"}} | ||||
| 							</a> | ||||
| 							<a class="item{{if .IsShowClosed}} active{{end}}" href="{{.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=closed&q={{$.Keyword}}"> | ||||
| 								{{svg "octicon-issue-closed" 16 "mr-3"}} | ||||
| 								{{.i18n.Tr "repo.milestones.close_tab" .MilestoneStats.ClosedCount}} | ||||
| 								{{svg "octicon-check" 16 "mr-3"}} | ||||
| 								{{JsPrettyNumber .MilestoneStats.ClosedCount}} {{.i18n.Tr "repo.issues.closed_title"}} | ||||
| 							</a> | ||||
| 						</div> | ||||
| 					</div> | ||||
| @@ -103,9 +103,13 @@ | ||||
| 									{{end}} | ||||
| 								{{end}} | ||||
| 								<span class="issue-stats"> | ||||
| 									{{svg "octicon-issue-opened"}} {{$.i18n.Tr "repo.milestones.open_tab" .NumOpenIssues}} | ||||
| 									{{svg "octicon-issue-closed"}} {{$.i18n.Tr "repo.milestones.close_tab" .NumClosedIssues}} | ||||
| 									{{if .TotalTrackedTime}}{{svg "octicon-clock"}} {{.TotalTrackedTime|Sec2Time}}{{end}} | ||||
| 									{{svg "octicon-issue-opened" 16 "mr-3"}} | ||||
| 									{{JsPrettyNumber .NumOpenIssues}} {{$.i18n.Tr "repo.issues.open_title"}} | ||||
| 									{{svg "octicon-check" 16 "mr-3"}} | ||||
| 									{{JsPrettyNumber .NumClosedIssues}} {{$.i18n.Tr "repo.issues.closed_title"}} | ||||
| 									{{if .TotalTrackedTime}} | ||||
| 										{{svg "octicon-clock"}} {{.TotalTrackedTime|Sec2Time}} | ||||
| 									{{end}} | ||||
| 								</span> | ||||
| 							</div> | ||||
| 							{{if and (or $.CanWriteIssues $.CanWritePulls) (not $.Repository.IsArchived)}} | ||||
|   | ||||
							
								
								
									
										14
									
								
								web_src/js/features/formatting.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								web_src/js/features/formatting.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| import {prettyNumber} from '../utils.js'; | ||||
|  | ||||
| const {lang} = document.documentElement; | ||||
|  | ||||
| export function initFormattingReplacements() { | ||||
|   // replace english formatted numbers with locale-specific separators | ||||
|   for (const el of document.getElementsByClassName('js-pretty-number')) { | ||||
|     const num = Number(el.getAttribute('data-value')); | ||||
|     const formatted = prettyNumber(num, lang); | ||||
|     if (formatted && formatted !== el.textContent) { | ||||
|       el.textContent = formatted; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -84,6 +84,11 @@ import {initRepoBranchButton} from './features/repo-branch.js'; | ||||
| import {initCommonOrganization} from './features/common-organization.js'; | ||||
| import {initRepoWikiForm} from './features/repo-wiki.js'; | ||||
| import {initRepoCommentForm, initRepository} from './features/repo-legacy.js'; | ||||
| import {initFormattingReplacements} from './features/formatting.js'; | ||||
|  | ||||
| // Run time-critical code as soon as possible. This is safe to do because this | ||||
| // script appears at the end of <body> and rendered HTML is accessible at that point. | ||||
| initFormattingReplacements(); | ||||
|  | ||||
| // Silence fomantic's error logging when tabs are used without a target content element | ||||
| $.fn.tab.settings.silent = true; | ||||
|   | ||||
| @@ -90,3 +90,10 @@ export function strSubMatch(full, sub) { | ||||
|   } | ||||
|   return res; | ||||
| } | ||||
|  | ||||
| // pretty-print a number using locale-specific separators, e.g. 1200 -> 1,200 | ||||
| export function prettyNumber(num, locale = 'en-US') { | ||||
|   if (typeof num !== 'number') return ''; | ||||
|   const {format} = new Intl.NumberFormat(locale); | ||||
|   return format(num); | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { | ||||
|   basename, extname, isObject, uniq, stripTags, joinPaths, parseIssueHref, strSubMatch, | ||||
|   basename, extname, isObject, uniq, stripTags, joinPaths, parseIssueHref, strSubMatch, prettyNumber, | ||||
| } from './utils.js'; | ||||
|  | ||||
| test('basename', () => { | ||||
| @@ -85,7 +85,6 @@ test('parseIssueHref', () => { | ||||
|   expect(parseIssueHref('')).toEqual({owner: undefined, repo: undefined, type: undefined, index: undefined}); | ||||
| }); | ||||
|  | ||||
|  | ||||
| test('strSubMatch', () => { | ||||
|   expect(strSubMatch('abc', '')).toEqual(['abc']); | ||||
|   expect(strSubMatch('abc', 'a')).toEqual(['', 'a', 'bc']); | ||||
| @@ -98,3 +97,14 @@ test('strSubMatch', () => { | ||||
|   expect(strSubMatch('aabbcc', 'abc')).toEqual(['', 'a', 'a', 'b', 'b', 'c', 'c']); | ||||
|   expect(strSubMatch('the/directory', 'hedir')).toEqual(['t', 'he', '/', 'dir', 'ectory']); | ||||
| }); | ||||
|  | ||||
| test('prettyNumber', () => { | ||||
|   expect(prettyNumber()).toEqual(''); | ||||
|   expect(prettyNumber(null)).toEqual(''); | ||||
|   expect(prettyNumber(undefined)).toEqual(''); | ||||
|   expect(prettyNumber('1200')).toEqual(''); | ||||
|   expect(prettyNumber(12345678, 'en-US')).toEqual('12,345,678'); | ||||
|   expect(prettyNumber(12345678, 'de-DE')).toEqual('12.345.678'); | ||||
|   expect(prettyNumber(12345678, 'be-BE')).toEqual('12 345 678'); | ||||
|   expect(prettyNumber(12345678, 'hi-IN')).toEqual('1,23,45,678'); | ||||
| }); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user