mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 11:28:24 +00:00 
			
		
		
		
	Frontend refactor: move Vue related code from index.js to components dir, and remove unused codes. (#17301)
				
					
				
			* frontend refactor
* Apply suggestions from code review
Co-authored-by: delvh <dev.lh@web.de>
* Update templates/base/head.tmpl
Co-authored-by: delvh <dev.lh@web.de>
* Update docs/content/doc/developers/guidelines-frontend.md
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
* fix typo
* fix typo
* refactor PageData to pageData
* Apply suggestions from code review
Co-authored-by: delvh <dev.lh@web.de>
* Simply for the visual difference.
Co-authored-by: delvh <dev.lh@web.de>
* Revert "Apply suggestions from code review"
This reverts commit 4d78ad9b0e.
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: 6543 <6543@obermui.de>
			
			
This commit is contained in:
		| @@ -3,8 +3,6 @@ reportUnusedDisableDirectives: true | |||||||
|  |  | ||||||
| ignorePatterns: | ignorePatterns: | ||||||
|   - /web_src/js/vendor |   - /web_src/js/vendor | ||||||
|   - /templates/repo/activity.tmpl |  | ||||||
|   - /templates/repo/view_file.tmpl |  | ||||||
|  |  | ||||||
| parserOptions: | parserOptions: | ||||||
|   sourceType: module |   sourceType: module | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -9,6 +9,8 @@ _test | |||||||
|  |  | ||||||
| # IntelliJ | # IntelliJ | ||||||
| .idea | .idea | ||||||
|  | # Goland's output filename can not be set manually | ||||||
|  | /go_build_* | ||||||
|  |  | ||||||
| # MS VSCode | # MS VSCode | ||||||
| .vscode | .vscode | ||||||
|   | |||||||
							
								
								
									
										51
									
								
								docs/content/doc/developers/guidelines-frontend.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								docs/content/doc/developers/guidelines-frontend.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | |||||||
|  | --- | ||||||
|  | date: "2021-10-13T16:00:00+02:00" | ||||||
|  | title: "Guidelines for Frontend Development" | ||||||
|  | slug: "guidelines-frontend" | ||||||
|  | weight: 20 | ||||||
|  | toc: false | ||||||
|  | draft: false | ||||||
|  | menu: | ||||||
|  |   sidebar: | ||||||
|  |     parent: "developers" | ||||||
|  |     name: "Guidelines for Frontend" | ||||||
|  |     weight: 20 | ||||||
|  |     identifier: "guidelines-frontend" | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | # Guidelines for Frontend Development | ||||||
|  |  | ||||||
|  | **Table of Contents** | ||||||
|  |  | ||||||
|  | {{< toc >}} | ||||||
|  |  | ||||||
|  | ## Background | ||||||
|  |  | ||||||
|  | Gitea uses [Less CSS](https://lesscss.org), [Fomantic-UI](https://fomantic-ui.com/introduction/getting-started.html) (based on [jQuery](https://api.jquery.com)) and [Vue2](https://vuejs.org/v2/guide/) for its frontend. | ||||||
|  |  | ||||||
|  | The HTML pages are rendered by [Go HTML Template](https://pkg.go.dev/html/template) | ||||||
|  |  | ||||||
|  | ## General Guidelines | ||||||
|  |  | ||||||
|  | We recommend [Google HTML/CSS Style Guide](https://google.github.io/styleguide/htmlcssguide.html) and [Google JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html) | ||||||
|  |  | ||||||
|  | ### Gitea specific guidelines: | ||||||
|  |  | ||||||
|  | 1. Every feature (Fomantic-UI/jQuery module) should be put in separate files/directories. | ||||||
|  | 2. HTML ids and classes should use kebab-case. | ||||||
|  | 3. HTML ids and classes used in JavaScript should be unique for the whole project, and should contain 2-3 feature related keywords. We recommend to use the `js-` prefix for classes that are only used in JavaScript. | ||||||
|  | 4. jQuery events across different features should use their own namespaces. | ||||||
|  | 5. CSS styling for classes provided by frameworks should not be overwritten. Always use new class-names to overwrite framework styles. We recommend to use the `us-` prefix for user defined styles.   | ||||||
|  | 6. The backend can pass complex data to the frontend by using `ctx.PageData["myModuleData"] = map[]{}` | ||||||
|  | 7. Simple pages and SEO-related pages use Go HTML Template render to generate static Fomantic-UI HTML output. Complex pages can use Vue2 (or Vue3 in future). | ||||||
|  |  | ||||||
|  | ## Legacy Problems and Solutions | ||||||
|  |  | ||||||
|  | ### Too much code in `web_src/index.js` | ||||||
|  |  | ||||||
|  | Previously, most JavaScript code was written into `web_src/index.js` directly, making the file unmaintainable. | ||||||
|  | Try to keep this file small by creating new modules instead. These modules can be put in the `web_src/js/features` directory for now. | ||||||
|  |  | ||||||
|  | ### Vue2/Vue3 and JSX | ||||||
|  |  | ||||||
|  | Gitea is using Vue2 now, we plan to upgrade to Vue3. We decided not to introduce JSX to keep the HTML and the JavaScript code separated. | ||||||
| @@ -132,7 +132,14 @@ See `make help` for all available `make` targets. Also see [`.drone.yml`](https: | |||||||
| To run and continuously rebuild when source files change: | To run and continuously rebuild when source files change: | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
|  | # for both frontend and backend | ||||||
| make watch | make watch | ||||||
|  |  | ||||||
|  | # or: watch frontend files (html/js/css) only | ||||||
|  | make watch-frontend | ||||||
|  |  | ||||||
|  | # or: watch backend files (go) only | ||||||
|  | make watch-backend | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| On macOS, watching all backend source files may hit the default open files limit which can be increased via `ulimit -n 12288` for the current shell or in your shell startup file for all future shells. | On macOS, watching all backend source files may hit the default open files limit which can be increased via `ulimit -n 12288` for the current shell or in your shell startup file for all future shells. | ||||||
| @@ -167,7 +174,9 @@ make revive vet misspell-check | |||||||
|  |  | ||||||
| ### Working on JS and CSS | ### Working on JS and CSS | ||||||
|  |  | ||||||
| Either use the `watch-frontend` target mentioned above or just build once: | Frontend development should follow [Guidelines for Frontend Development](./guidelines-frontend.md) | ||||||
|  |  | ||||||
|  | To build with frontend resources, either use the `watch-frontend` target mentioned above or just build once: | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| make build && ./gitea | make build && ./gitea | ||||||
|   | |||||||
| @@ -51,7 +51,7 @@ type Context struct { | |||||||
| 	Resp     ResponseWriter | 	Resp     ResponseWriter | ||||||
| 	Req      *http.Request | 	Req      *http.Request | ||||||
| 	Data     map[string]interface{} // data used by MVC templates | 	Data     map[string]interface{} // data used by MVC templates | ||||||
| 	PageData map[string]interface{} // data used by JavaScript modules in one page | 	PageData map[string]interface{} // data used by JavaScript modules in one page, it's `window.config.pageData` | ||||||
| 	Render   Render | 	Render   Render | ||||||
| 	translation.Locale | 	translation.Locale | ||||||
| 	Cache   cache.Cache | 	Cache   cache.Cache | ||||||
| @@ -645,9 +645,10 @@ func Contexter() func(next http.Handler) http.Handler { | |||||||
| 					"CurrentURL":    setting.AppSubURL + req.URL.RequestURI(), | 					"CurrentURL":    setting.AppSubURL + req.URL.RequestURI(), | ||||||
| 					"PageStartTime": startTime, | 					"PageStartTime": startTime, | ||||||
| 					"Link":          link, | 					"Link":          link, | ||||||
|  | 					"IsProd":        setting.IsProd(), | ||||||
| 				}, | 				}, | ||||||
| 			} | 			} | ||||||
| 			// PageData is passed by reference, and it will be rendered to `window.config.PageData` in `head.tmpl` for JavaScript modules | 			// PageData is passed by reference, and it will be rendered to `window.config.pageData` in `head.tmpl` for JavaScript modules | ||||||
| 			ctx.PageData = map[string]interface{}{} | 			ctx.PageData = map[string]interface{}{} | ||||||
| 			ctx.Data["PageData"] = ctx.PageData | 			ctx.Data["PageData"] = ctx.PageData | ||||||
|  |  | ||||||
|   | |||||||
| @@ -60,7 +60,7 @@ func Activity(ctx *context.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if ctx.Data["ActivityTopAuthors"], err = models.GetActivityStatsTopAuthors(ctx.Repo.Repository, timeFrom, 10); err != nil { | 	if ctx.PageData["repoActivityTopAuthors"], err = models.GetActivityStatsTopAuthors(ctx.Repo.Repository, timeFrom, 10); err != nil { | ||||||
| 		ctx.ServerError("GetActivityStatsTopAuthors", err) | 		ctx.ServerError("GetActivityStatsTopAuthors", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -106,7 +106,10 @@ func Dashboard(ctx *context.Context) { | |||||||
| 	ctx.Data["Title"] = ctxUser.DisplayName() + " - " + ctx.Tr("dashboard") | 	ctx.Data["Title"] = ctxUser.DisplayName() + " - " + ctx.Tr("dashboard") | ||||||
| 	ctx.Data["PageIsDashboard"] = true | 	ctx.Data["PageIsDashboard"] = true | ||||||
| 	ctx.Data["PageIsNews"] = true | 	ctx.Data["PageIsNews"] = true | ||||||
| 	ctx.Data["SearchLimit"] = setting.UI.User.RepoPagingNum |  | ||||||
|  | 	ctx.PageData["dashboardRepoList"] = map[string]interface{}{ | ||||||
|  | 		"searchLimit": setting.UI.User.RepoPagingNum, | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if setting.Service.EnableUserHeatmap { | 	if setting.Service.EnableUserHeatmap { | ||||||
| 		data, err := models.GetUserHeatmapDataByUserTeam(ctxUser, ctx.Org.Team, ctx.User) | 		data, err := models.GetUserHeatmapDataByUserTeam(ctxUser, ctx.Org.Team, ctx.User) | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| <!DOCTYPE html> | <!DOCTYPE html> | ||||||
| <html lang="{{.Lang}}" class="theme-{{.SignedUser.Theme}}"> | <html lang="{{.Lang}}" class="theme-{{.SignedUser.Theme}}"> | ||||||
| <head data-suburl="{{AppSubUrl}}"> | <head> | ||||||
| 	<meta charset="utf-8"> | 	<meta charset="utf-8"> | ||||||
| 	<meta name="viewport" content="width=device-width, initial-scale=1"> | 	<meta name="viewport" content="width=device-width, initial-scale=1"> | ||||||
| 	<title>{{if .Title}}{{.Title | RenderEmojiPlain}} - {{end}} {{if .Repository.Name}}{{.Repository.Name}} - {{end}}{{AppName}} </title> | 	<title>{{if .Title}}{{.Title | RenderEmojiPlain}} - {{end}} {{if .Repository.Name}}{{.Repository.Name}} - {{end}}{{AppName}} </title> | ||||||
| @@ -12,15 +12,6 @@ | |||||||
| 	<meta name="keywords" content="{{MetaKeywords}}"> | 	<meta name="keywords" content="{{MetaKeywords}}"> | ||||||
| 	<meta name="referrer" content="no-referrer" /> | 	<meta name="referrer" content="no-referrer" /> | ||||||
| 	<meta name="_csrf" content="{{.CsrfToken}}" /> | 	<meta name="_csrf" content="{{.CsrfToken}}" /> | ||||||
| 	{{if .IsSigned}} |  | ||||||
| 		<meta name="_uid" content="{{.SignedUser.ID}}" /> |  | ||||||
| 	{{end}} |  | ||||||
| 	{{if .ContextUser}} |  | ||||||
| 		<meta name="_context_uid" content="{{.ContextUser.ID}}" /> |  | ||||||
| 	{{end}} |  | ||||||
| 	{{if .SearchLimit}} |  | ||||||
| 		<meta name="_search_limit" content="{{.SearchLimit}}" /> |  | ||||||
| 	{{end}} |  | ||||||
| {{if .GoGetImport}} | {{if .GoGetImport}} | ||||||
| 	<meta name="go-import" content="{{.GoGetImport}} git {{.CloneLink.HTTPS}}"> | 	<meta name="go-import" content="{{.GoGetImport}} git {{.CloneLink.HTTPS}}"> | ||||||
| 	<meta name="go-source" content="{{.GoGetImport}} _ {{.GoDocDirectory}} {{.GoDocFile}}"> | 	<meta name="go-source" content="{{.GoGetImport}} _ {{.GoDocDirectory}} {{.GoDocFile}}"> | ||||||
| @@ -31,10 +22,11 @@ | |||||||
| 			AppVer: '{{AppVer}}', | 			AppVer: '{{AppVer}}', | ||||||
| 			AppSubUrl: '{{AppSubUrl}}', | 			AppSubUrl: '{{AppSubUrl}}', | ||||||
| 			AssetUrlPrefix: '{{AssetUrlPrefix}}', | 			AssetUrlPrefix: '{{AssetUrlPrefix}}', | ||||||
|  | 			IsProd: {{.IsProd}}, | ||||||
| 			CustomEmojis: {{CustomEmojis}}, | 			CustomEmojis: {{CustomEmojis}}, | ||||||
| 			UseServiceWorker: {{UseServiceWorker}}, | 			UseServiceWorker: {{UseServiceWorker}}, | ||||||
| 			csrf: '{{.CsrfToken}}', | 			csrf: '{{.CsrfToken}}', | ||||||
| 			PageData: {{ .PageData }}, | 			pageData: {{ .PageData }}, | ||||||
| 			HighlightJS: {{if .RequireHighlightJS}}true{{else}}false{{end}}, | 			HighlightJS: {{if .RequireHighlightJS}}true{{else}}false{{end}}, | ||||||
| 			SimpleMDE: {{if .RequireSimpleMDE}}true{{else}}false{{end}}, | 			SimpleMDE: {{if .RequireSimpleMDE}}true{{else}}false{{end}}, | ||||||
| 			Tribute: {{if .RequireTribute}}true{{else}}false{{end}}, | 			Tribute: {{if .RequireTribute}}true{{else}}false{{end}}, | ||||||
|   | |||||||
| @@ -108,11 +108,8 @@ | |||||||
| 						{{.i18n.Tr "repo.activity.git_stats_and_deletions" }} | 						{{.i18n.Tr "repo.activity.git_stats_and_deletions" }} | ||||||
| 						<strong class="text red">{{.i18n.Tr (TrN .i18n.Lang .Activity.Code.Deletions "repo.activity.git_stats_deletion_1" "repo.activity.git_stats_deletion_n") .Activity.Code.Deletions }}</strong>. | 						<strong class="text red">{{.i18n.Tr (TrN .i18n.Lang .Activity.Code.Deletions "repo.activity.git_stats_deletion_1" "repo.activity.git_stats_deletion_n") .Activity.Code.Deletions }}</strong>. | ||||||
| 					</div> | 					</div> | ||||||
| 					<div class="ui attached segment" id="app"> | 					<div class="ui attached segment"> | ||||||
| 						<script type="text/javascript"> | 						<div id="repo-activity-top-authors-chart"></div> | ||||||
| 						var ActivityTopAuthors = {{Json .ActivityTopAuthors | SafeJS}}; |  | ||||||
| 						</script> |  | ||||||
| 						<activity-top-authors :data="activityTopAuthors" /> |  | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
| 			{{end}} | 			{{end}} | ||||||
| @@ -126,7 +123,7 @@ | |||||||
| 			<div class="list"> | 			<div class="list"> | ||||||
| 				{{range .Activity.PublishedReleases}} | 				{{range .Activity.PublishedReleases}} | ||||||
| 					<p class="desc"> | 					<p class="desc"> | ||||||
| 						<div class="ui green label">{{$.i18n.Tr "repo.activity.published_release_label"}}</div> | 						<span class="ui green label">{{$.i18n.Tr "repo.activity.published_release_label"}}</span> | ||||||
| 						{{.TagName}} | 						{{.TagName}} | ||||||
| 						{{if not .IsTag}} | 						{{if not .IsTag}} | ||||||
| 							<a class="title" href="{{$.RepoLink}}/src/{{.TagName | EscapePound}}">{{.Title | RenderEmoji}}</a> | 							<a class="title" href="{{$.RepoLink}}/src/{{.TagName | EscapePound}}">{{.Title | RenderEmoji}}</a> | ||||||
| @@ -145,7 +142,7 @@ | |||||||
| 			<div class="list"> | 			<div class="list"> | ||||||
| 				{{range .Activity.MergedPRs}} | 				{{range .Activity.MergedPRs}} | ||||||
| 					<p class="desc"> | 					<p class="desc"> | ||||||
| 						<div class="ui purple label">{{$.i18n.Tr "repo.activity.merged_prs_label"}}</div> | 						<span class="ui purple label">{{$.i18n.Tr "repo.activity.merged_prs_label"}}</span> | ||||||
| 						#{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | RenderEmoji}}</a> | 						#{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | RenderEmoji}}</a> | ||||||
| 						{{TimeSinceUnix .MergedUnix $.Lang}} | 						{{TimeSinceUnix .MergedUnix $.Lang}} | ||||||
| 					</p> | 					</p> | ||||||
| @@ -161,7 +158,7 @@ | |||||||
| 			<div class="list"> | 			<div class="list"> | ||||||
| 				{{range .Activity.OpenedPRs}} | 				{{range .Activity.OpenedPRs}} | ||||||
| 					<p class="desc"> | 					<p class="desc"> | ||||||
| 						<div class="ui green label">{{$.i18n.Tr "repo.activity.opened_prs_label"}}</div> | 						<span class="ui green label">{{$.i18n.Tr "repo.activity.opened_prs_label"}}</span> | ||||||
| 						#{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | RenderEmoji}}</a> | 						#{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | RenderEmoji}}</a> | ||||||
| 						{{TimeSinceUnix .Issue.CreatedUnix $.Lang}} | 						{{TimeSinceUnix .Issue.CreatedUnix $.Lang}} | ||||||
| 					</p> | 					</p> | ||||||
| @@ -177,7 +174,7 @@ | |||||||
| 			<div class="list"> | 			<div class="list"> | ||||||
| 				{{range .Activity.ClosedIssues}} | 				{{range .Activity.ClosedIssues}} | ||||||
| 					<p class="desc"> | 					<p class="desc"> | ||||||
| 						<div class="ui red label">{{$.i18n.Tr "repo.activity.closed_issue_label"}}</div> | 						<span class="ui red label">{{$.i18n.Tr "repo.activity.closed_issue_label"}}</span> | ||||||
| 						#{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji}}</a> | 						#{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji}}</a> | ||||||
| 						{{TimeSinceUnix .ClosedUnix $.Lang}} | 						{{TimeSinceUnix .ClosedUnix $.Lang}} | ||||||
| 					</p> | 					</p> | ||||||
| @@ -193,7 +190,7 @@ | |||||||
| 			<div class="list"> | 			<div class="list"> | ||||||
| 				{{range .Activity.OpenedIssues}} | 				{{range .Activity.OpenedIssues}} | ||||||
| 					<p class="desc"> | 					<p class="desc"> | ||||||
| 						<div class="ui green label">{{$.i18n.Tr "repo.activity.new_issue_label"}}</div> | 						<span class="ui green label">{{$.i18n.Tr "repo.activity.new_issue_label"}}</span> | ||||||
| 						#{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji}}</a> | 						#{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji}}</a> | ||||||
| 						{{TimeSinceUnix .CreatedUnix $.Lang}} | 						{{TimeSinceUnix .CreatedUnix $.Lang}} | ||||||
| 					</p> | 					</p> | ||||||
| @@ -212,7 +209,7 @@ | |||||||
| 			<div class="list"> | 			<div class="list"> | ||||||
| 				{{range .Activity.UnresolvedIssues}} | 				{{range .Activity.UnresolvedIssues}} | ||||||
| 					<p class="desc"> | 					<p class="desc"> | ||||||
| 						<div class="ui green label">{{$.i18n.Tr "repo.activity.unresolved_conv_label"}}</div> | 						<span class="ui green label">{{$.i18n.Tr "repo.activity.unresolved_conv_label"}}</span> | ||||||
| 						#{{.Index}} | 						#{{.Index}} | ||||||
| 						{{if .IsPull}} | 						{{if .IsPull}} | ||||||
| 						<a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Title | RenderEmoji}}</a> | 						<a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Title | RenderEmoji}}</a> | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ | |||||||
| 	{{end}} | 	{{end}} | ||||||
|  |  | ||||||
| 	<!-- I know, there is probably a better way to do this (moved from sidebar.tmpl, original author: 6543 @ 2021-02-28) --> | 	<!-- I know, there is probably a better way to do this (moved from sidebar.tmpl, original author: 6543 @ 2021-02-28) --> | ||||||
| 	<!-- Agree, there should be a better way, eg: introduce window.config.PageData (original author: wxiaoguang @ 2021-09-05) --> | 	<!-- Agree, there should be a better way, eg: introduce window.config.pageData (original author: wxiaoguang @ 2021-09-05) --> | ||||||
| 	<input type="hidden" id="repolink" value="{{$.RepoRelPath}}"> | 	<input type="hidden" id="repolink" value="{{$.RepoRelPath}}"> | ||||||
| 	<input type="hidden" id="repoId" value="{{.Repository.ID}}"> | 	<input type="hidden" id="repoId" value="{{.Repository.ID}}"> | ||||||
| 	<input type="hidden" id="issueIndex" value="{{.Issue.Index}}"/> | 	<input type="hidden" id="issueIndex" value="{{.Issue.Index}}"/> | ||||||
|   | |||||||
| @@ -131,13 +131,3 @@ | |||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
| <script> |  | ||||||
| function submitDeleteForm() { |  | ||||||
| 	var message = prompt("{{.i18n.Tr "repo.delete_confirm_message"}}\n\n{{.i18n.Tr "repo.delete_commit_summary"}}", "Delete '{{.TreeName}}'"); |  | ||||||
| 	if (message != null) { |  | ||||||
| 		$("#delete-message").val(message); |  | ||||||
| 		$("#delete-file-form").submit() |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
|   | |||||||
| @@ -1,8 +1,7 @@ | |||||||
| <div id="app" class="six wide column"> | <div id="dashboard-repo-list" class="six wide column"> | ||||||
| 	<repo-search | 	<repo-search | ||||||
| 	:search-limit="searchLimit" | 	:search-limit="searchLimit" | ||||||
| 	:suburl="suburl" | 	:sub-url="subUrl" | ||||||
| 	:uid="uid" |  | ||||||
| 	{{if .Team}} | 	{{if .Team}} | ||||||
| 	:team-id="{{.Team.ID}}" | 	:team-id="{{.Team.ID}}" | ||||||
| 	{{end}} | 	{{end}} | ||||||
| @@ -31,7 +30,7 @@ | |||||||
| 					{{.i18n.Tr "home.my_repos"}} | 					{{.i18n.Tr "home.my_repos"}} | ||||||
| 					<span class="ui grey label ml-3">${reposTotalCount}</span> | 					<span class="ui grey label ml-3">${reposTotalCount}</span> | ||||||
| 				</div> | 				</div> | ||||||
| 				<a class="poping up" :href="suburl + '/repo/create'" data-content="{{.i18n.Tr "new_repo"}}" data-variation="tiny inverted" data-position="left center"> | 				<a class="poping up" :href="subUrl + '/repo/create'" data-content="{{.i18n.Tr "new_repo"}}" data-variation="tiny inverted" data-position="left center"> | ||||||
| 					{{svg "octicon-plus"}} | 					{{svg "octicon-plus"}} | ||||||
| 					<span class="sr-only">{{.i18n.Tr "new_repo"}}</span> | 					<span class="sr-only">{{.i18n.Tr "new_repo"}}</span> | ||||||
| 				</a> | 				</a> | ||||||
| @@ -122,7 +121,7 @@ | |||||||
| 			<div v-if="repos.length" class="ui attached table segment rounded-bottom"> | 			<div v-if="repos.length" class="ui attached table segment rounded-bottom"> | ||||||
| 				<ul class="repo-owner-name-list"> | 				<ul class="repo-owner-name-list"> | ||||||
| 					<li v-for="repo in repos" :class="{'private': repo.private || repo.internal}"> | 					<li v-for="repo in repos" :class="{'private': repo.private || repo.internal}"> | ||||||
| 						<a class="repo-list-link df ac sb" :href="suburl + '/' + repo.full_name"> | 						<a class="repo-list-link df ac sb" :href="subUrl + '/' + repo.full_name"> | ||||||
| 							<div class="text truncate item-name f1"> | 							<div class="text truncate item-name f1"> | ||||||
| 								<component v-bind:is="repoIcon(repo)" size="16"></component> | 								<component v-bind:is="repoIcon(repo)" size="16"></component> | ||||||
| 								<strong>${repo.full_name}</strong> | 								<strong>${repo.full_name}</strong> | ||||||
| @@ -168,7 +167,7 @@ | |||||||
| 					{{.i18n.Tr "home.my_orgs"}} | 					{{.i18n.Tr "home.my_orgs"}} | ||||||
| 					<span class="ui grey label ml-3">${organizationsTotalCount}</span> | 					<span class="ui grey label ml-3">${organizationsTotalCount}</span> | ||||||
| 				</div> | 				</div> | ||||||
| 				<a v-if="canCreateOrganization" class="poping up" :href="suburl + '/org/create'" data-content="{{.i18n.Tr "new_org"}}" data-variation="tiny inverted" data-position="left center"> | 				<a v-if="canCreateOrganization" class="poping up" :href="subUrl + '/org/create'" data-content="{{.i18n.Tr "new_org"}}" data-variation="tiny inverted" data-position="left center"> | ||||||
| 					{{svg "octicon-plus"}} | 					{{svg "octicon-plus"}} | ||||||
| 					<span class="sr-only">{{.i18n.Tr "new_org"}}</span> | 					<span class="sr-only">{{.i18n.Tr "new_org"}}</span> | ||||||
| 				</a> | 				</a> | ||||||
| @@ -176,7 +175,7 @@ | |||||||
| 			<div v-if="organizations.length" class="ui attached table segment rounded-bottom"> | 			<div v-if="organizations.length" class="ui attached table segment rounded-bottom"> | ||||||
| 				<ul class="repo-owner-name-list"> | 				<ul class="repo-owner-name-list"> | ||||||
| 					<li v-for="org in organizations"> | 					<li v-for="org in organizations"> | ||||||
| 						<a class="repo-list-link df ac sb" :href="suburl + '/' + org.name"> | 						<a class="repo-list-link df ac sb" :href="subUrl + '/' + org.name"> | ||||||
| 							<div class="text truncate item-name f1"> | 							<div class="text truncate item-name f1"> | ||||||
| 								{{svg "octicon-organization" 16 "mr-2"}} | 								{{svg "octicon-organization" 16 "mr-2"}} | ||||||
| 								<strong>${org.name}</strong> | 								<strong>${org.name}</strong> | ||||||
|   | |||||||
| @@ -70,4 +70,3 @@ export default { | |||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
| <style scoped/> |  | ||||||
|   | |||||||
							
								
								
									
										370
									
								
								web_src/js/components/DashboardRepoList.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										370
									
								
								web_src/js/components/DashboardRepoList.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,370 @@ | |||||||
|  | import Vue from 'vue'; | ||||||
|  | import {initVueSvg, vueDelimiters} from './VueComponentLoader.js'; | ||||||
|  |  | ||||||
|  | const {AppSubUrl, AssetUrlPrefix, pageData} = window.config; | ||||||
|  |  | ||||||
|  | function initVueComponents() { | ||||||
|  |   Vue.component('repo-search', { | ||||||
|  |     delimiters: vueDelimiters, | ||||||
|  |     props: { | ||||||
|  |       searchLimit: { | ||||||
|  |         type: Number, | ||||||
|  |         default: 10 | ||||||
|  |       }, | ||||||
|  |       subUrl: { | ||||||
|  |         type: String, | ||||||
|  |         required: true | ||||||
|  |       }, | ||||||
|  |       uid: { | ||||||
|  |         type: Number, | ||||||
|  |         default: 0 | ||||||
|  |       }, | ||||||
|  |       teamId: { | ||||||
|  |         type: Number, | ||||||
|  |         required: false, | ||||||
|  |         default: 0 | ||||||
|  |       }, | ||||||
|  |       organizations: { | ||||||
|  |         type: Array, | ||||||
|  |         default: () => [], | ||||||
|  |       }, | ||||||
|  |       isOrganization: { | ||||||
|  |         type: Boolean, | ||||||
|  |         default: true | ||||||
|  |       }, | ||||||
|  |       canCreateOrganization: { | ||||||
|  |         type: Boolean, | ||||||
|  |         default: false | ||||||
|  |       }, | ||||||
|  |       organizationsTotalCount: { | ||||||
|  |         type: Number, | ||||||
|  |         default: 0 | ||||||
|  |       }, | ||||||
|  |       moreReposLink: { | ||||||
|  |         type: String, | ||||||
|  |         default: '' | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     data() { | ||||||
|  |       const params = new URLSearchParams(window.location.search); | ||||||
|  |  | ||||||
|  |       let tab = params.get('repo-search-tab'); | ||||||
|  |       if (!tab) { | ||||||
|  |         tab = 'repos'; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       let reposFilter = params.get('repo-search-filter'); | ||||||
|  |       if (!reposFilter) { | ||||||
|  |         reposFilter = 'all'; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       let privateFilter = params.get('repo-search-private'); | ||||||
|  |       if (!privateFilter) { | ||||||
|  |         privateFilter = 'both'; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       let archivedFilter = params.get('repo-search-archived'); | ||||||
|  |       if (!archivedFilter) { | ||||||
|  |         archivedFilter = 'unarchived'; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       let searchQuery = params.get('repo-search-query'); | ||||||
|  |       if (!searchQuery) { | ||||||
|  |         searchQuery = ''; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       let page = 1; | ||||||
|  |       try { | ||||||
|  |         page = parseInt(params.get('repo-search-page')); | ||||||
|  |       } catch { | ||||||
|  |         // noop | ||||||
|  |       } | ||||||
|  |       if (!page) { | ||||||
|  |         page = 1; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return { | ||||||
|  |         tab, | ||||||
|  |         repos: [], | ||||||
|  |         reposTotalCount: 0, | ||||||
|  |         reposFilter, | ||||||
|  |         archivedFilter, | ||||||
|  |         privateFilter, | ||||||
|  |         page, | ||||||
|  |         finalPage: 1, | ||||||
|  |         searchQuery, | ||||||
|  |         isLoading: false, | ||||||
|  |         staticPrefix: AssetUrlPrefix, | ||||||
|  |         counts: {}, | ||||||
|  |         repoTypes: { | ||||||
|  |           all: { | ||||||
|  |             searchMode: '', | ||||||
|  |           }, | ||||||
|  |           forks: { | ||||||
|  |             searchMode: 'fork', | ||||||
|  |           }, | ||||||
|  |           mirrors: { | ||||||
|  |             searchMode: 'mirror', | ||||||
|  |           }, | ||||||
|  |           sources: { | ||||||
|  |             searchMode: 'source', | ||||||
|  |           }, | ||||||
|  |           collaborative: { | ||||||
|  |             searchMode: 'collaborative', | ||||||
|  |           }, | ||||||
|  |         } | ||||||
|  |       }; | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     computed: { | ||||||
|  |       // used in `repolist.tmpl` | ||||||
|  |       showMoreReposLink() { | ||||||
|  |         return this.repos.length > 0 && this.repos.length < this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`]; | ||||||
|  |       }, | ||||||
|  |       searchURL() { | ||||||
|  |         return `${this.subUrl}/api/v1/repos/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=${this.searchQuery | ||||||
|  |         }&page=${this.page}&limit=${this.searchLimit}&mode=${this.repoTypes[this.reposFilter].searchMode | ||||||
|  |         }${this.reposFilter !== 'all' ? '&exclusive=1' : '' | ||||||
|  |         }${this.archivedFilter === 'archived' ? '&archived=true' : ''}${this.archivedFilter === 'unarchived' ? '&archived=false' : '' | ||||||
|  |         }${this.privateFilter === 'private' ? '&is_private=true' : ''}${this.privateFilter === 'public' ? '&is_private=false' : '' | ||||||
|  |         }`; | ||||||
|  |       }, | ||||||
|  |       repoTypeCount() { | ||||||
|  |         return this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`]; | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     mounted() { | ||||||
|  |       this.changeReposFilter(this.reposFilter); | ||||||
|  |       $(this.$el).find('.poping.up').popup(); | ||||||
|  |       $(this.$el).find('.dropdown').dropdown(); | ||||||
|  |       this.setCheckboxes(); | ||||||
|  |       Vue.nextTick(() => { | ||||||
|  |         this.$refs.search.focus(); | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     methods: { | ||||||
|  |       changeTab(t) { | ||||||
|  |         this.tab = t; | ||||||
|  |         this.updateHistory(); | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       setCheckboxes() { | ||||||
|  |         switch (this.archivedFilter) { | ||||||
|  |           case 'unarchived': | ||||||
|  |             $('#archivedFilterCheckbox').checkbox('set unchecked'); | ||||||
|  |             break; | ||||||
|  |           case 'archived': | ||||||
|  |             $('#archivedFilterCheckbox').checkbox('set checked'); | ||||||
|  |             break; | ||||||
|  |           case 'both': | ||||||
|  |             $('#archivedFilterCheckbox').checkbox('set indeterminate'); | ||||||
|  |             break; | ||||||
|  |           default: | ||||||
|  |             this.archivedFilter = 'unarchived'; | ||||||
|  |             $('#archivedFilterCheckbox').checkbox('set unchecked'); | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  |         switch (this.privateFilter) { | ||||||
|  |           case 'public': | ||||||
|  |             $('#privateFilterCheckbox').checkbox('set unchecked'); | ||||||
|  |             break; | ||||||
|  |           case 'private': | ||||||
|  |             $('#privateFilterCheckbox').checkbox('set checked'); | ||||||
|  |             break; | ||||||
|  |           case 'both': | ||||||
|  |             $('#privateFilterCheckbox').checkbox('set indeterminate'); | ||||||
|  |             break; | ||||||
|  |           default: | ||||||
|  |             this.privateFilter = 'both'; | ||||||
|  |             $('#privateFilterCheckbox').checkbox('set indeterminate'); | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       changeReposFilter(filter) { | ||||||
|  |         this.reposFilter = filter; | ||||||
|  |         this.repos = []; | ||||||
|  |         this.page = 1; | ||||||
|  |         Vue.set(this.counts, `${filter}:${this.archivedFilter}:${this.privateFilter}`, 0); | ||||||
|  |         this.searchRepos(); | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       updateHistory() { | ||||||
|  |         const params = new URLSearchParams(window.location.search); | ||||||
|  |  | ||||||
|  |         if (this.tab === 'repos') { | ||||||
|  |           params.delete('repo-search-tab'); | ||||||
|  |         } else { | ||||||
|  |           params.set('repo-search-tab', this.tab); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (this.reposFilter === 'all') { | ||||||
|  |           params.delete('repo-search-filter'); | ||||||
|  |         } else { | ||||||
|  |           params.set('repo-search-filter', this.reposFilter); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (this.privateFilter === 'both') { | ||||||
|  |           params.delete('repo-search-private'); | ||||||
|  |         } else { | ||||||
|  |           params.set('repo-search-private', this.privateFilter); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (this.archivedFilter === 'unarchived') { | ||||||
|  |           params.delete('repo-search-archived'); | ||||||
|  |         } else { | ||||||
|  |           params.set('repo-search-archived', this.archivedFilter); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (this.searchQuery === '') { | ||||||
|  |           params.delete('repo-search-query'); | ||||||
|  |         } else { | ||||||
|  |           params.set('repo-search-query', this.searchQuery); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (this.page === 1) { | ||||||
|  |           params.delete('repo-search-page'); | ||||||
|  |         } else { | ||||||
|  |           params.set('repo-search-page', `${this.page}`); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const queryString = params.toString(); | ||||||
|  |         if (queryString) { | ||||||
|  |           window.history.replaceState({}, '', `?${queryString}`); | ||||||
|  |         } else { | ||||||
|  |           window.history.replaceState({}, '', window.location.pathname); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       toggleArchivedFilter() { | ||||||
|  |         switch (this.archivedFilter) { | ||||||
|  |           case 'both': | ||||||
|  |             this.archivedFilter = 'unarchived'; | ||||||
|  |             break; | ||||||
|  |           case 'unarchived': | ||||||
|  |             this.archivedFilter = 'archived'; | ||||||
|  |             break; | ||||||
|  |           case 'archived': | ||||||
|  |             this.archivedFilter = 'both'; | ||||||
|  |             break; | ||||||
|  |           default: | ||||||
|  |             this.archivedFilter = 'unarchived'; | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  |         this.page = 1; | ||||||
|  |         this.repos = []; | ||||||
|  |         this.setCheckboxes(); | ||||||
|  |         Vue.set(this.counts, `${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`, 0); | ||||||
|  |         this.searchRepos(); | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       togglePrivateFilter() { | ||||||
|  |         switch (this.privateFilter) { | ||||||
|  |           case 'both': | ||||||
|  |             this.privateFilter = 'public'; | ||||||
|  |             break; | ||||||
|  |           case 'public': | ||||||
|  |             this.privateFilter = 'private'; | ||||||
|  |             break; | ||||||
|  |           case 'private': | ||||||
|  |             this.privateFilter = 'both'; | ||||||
|  |             break; | ||||||
|  |           default: | ||||||
|  |             this.privateFilter = 'both'; | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  |         this.page = 1; | ||||||
|  |         this.repos = []; | ||||||
|  |         this.setCheckboxes(); | ||||||
|  |         Vue.set(this.counts, `${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`, 0); | ||||||
|  |         this.searchRepos(); | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |  | ||||||
|  |       changePage(page) { | ||||||
|  |         this.page = page; | ||||||
|  |         if (this.page > this.finalPage) { | ||||||
|  |           this.page = this.finalPage; | ||||||
|  |         } | ||||||
|  |         if (this.page < 1) { | ||||||
|  |           this.page = 1; | ||||||
|  |         } | ||||||
|  |         this.repos = []; | ||||||
|  |         Vue.set(this.counts, `${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`, 0); | ||||||
|  |         this.searchRepos(); | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       searchRepos() { | ||||||
|  |         this.isLoading = true; | ||||||
|  |  | ||||||
|  |         if (!this.reposTotalCount) { | ||||||
|  |           const totalCountSearchURL = `${this.subUrl}/api/v1/repos/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`; | ||||||
|  |           $.getJSON(totalCountSearchURL, (_result, _textStatus, request) => { | ||||||
|  |             this.reposTotalCount = request.getResponseHeader('X-Total-Count'); | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const searchedMode = this.repoTypes[this.reposFilter].searchMode; | ||||||
|  |         const searchedURL = this.searchURL; | ||||||
|  |         const searchedQuery = this.searchQuery; | ||||||
|  |  | ||||||
|  |         $.getJSON(searchedURL, (result, _textStatus, request) => { | ||||||
|  |           if (searchedURL === this.searchURL) { | ||||||
|  |             this.repos = result.data; | ||||||
|  |             const count = request.getResponseHeader('X-Total-Count'); | ||||||
|  |             if (searchedQuery === '' && searchedMode === '' && this.archivedFilter === 'both') { | ||||||
|  |               this.reposTotalCount = count; | ||||||
|  |             } | ||||||
|  |             Vue.set(this.counts, `${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`, count); | ||||||
|  |             this.finalPage = Math.ceil(count / this.searchLimit); | ||||||
|  |             this.updateHistory(); | ||||||
|  |           } | ||||||
|  |         }).always(() => { | ||||||
|  |           if (searchedURL === this.searchURL) { | ||||||
|  |             this.isLoading = false; | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       repoIcon(repo) { | ||||||
|  |         if (repo.fork) { | ||||||
|  |           return 'octicon-repo-forked'; | ||||||
|  |         } else if (repo.mirror) { | ||||||
|  |           return 'octicon-mirror'; | ||||||
|  |         } else if (repo.template) { | ||||||
|  |           return `octicon-repo-template`; | ||||||
|  |         } else if (repo.private) { | ||||||
|  |           return 'octicon-lock'; | ||||||
|  |         } else if (repo.internal) { | ||||||
|  |           return 'octicon-repo'; | ||||||
|  |         } | ||||||
|  |         return 'octicon-repo'; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | function initDashboardRepoList() { | ||||||
|  |   const el = document.getElementById('dashboard-repo-list'); | ||||||
|  |   const dashboardRepoListData = pageData.dashboardRepoList || null; | ||||||
|  |   if (!el || !dashboardRepoListData) return; | ||||||
|  |  | ||||||
|  |   initVueSvg(); | ||||||
|  |   initVueComponents(); | ||||||
|  |   new Vue({ | ||||||
|  |     el, | ||||||
|  |     delimiters: vueDelimiters, | ||||||
|  |     data: () => { | ||||||
|  |       return { | ||||||
|  |         searchLimit: dashboardRepoListData.searchLimit || 0, | ||||||
|  |         subUrl: AppSubUrl, | ||||||
|  |       }; | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export {initDashboardRepoList}; | ||||||
| @@ -1,9 +1,9 @@ | |||||||
| <template> | <template> | ||||||
|   <div> |   <div> | ||||||
|     <div class="activity-bar-graph" ref="style" style="width:0px;height:0px"/> |     <div class="activity-bar-graph" ref="style" style="width: 0; height: 0;"/> | ||||||
|     <div class="activity-bar-graph-alt" ref="altStyle" style="width:0px;height:0px"/> |     <div class="activity-bar-graph-alt" ref="altStyle" style="width: 0; height: 0;"/> | ||||||
|     <vue-bar-graph |     <vue-bar-graph | ||||||
|       :points="graphData" |       :points="graphPoints" | ||||||
|       :show-x-axis="true" |       :show-x-axis="true" | ||||||
|       :show-y-axis="false" |       :show-y-axis="false" | ||||||
|       :show-values="true" |       :show-values="true" | ||||||
| @@ -15,9 +15,9 @@ | |||||||
|       :label-height="20" |       :label-height="20" | ||||||
|     > |     > | ||||||
|       <template #label="opt"> |       <template #label="opt"> | ||||||
|         <g v-for="(author, idx) in authors" :key="author.position"> |         <g v-for="(author, idx) in graphAuthors" :key="author.position"> | ||||||
|           <a |           <a | ||||||
|             v-if="opt.bar.index === idx && author.home_link !== ''" |             v-if="opt.bar.index === idx && author.home_link" | ||||||
|             :href="author.home_link" |             :href="author.home_link" | ||||||
|           > |           > | ||||||
|             <image |             <image | ||||||
| @@ -39,7 +39,7 @@ | |||||||
|         </g> |         </g> | ||||||
|       </template> |       </template> | ||||||
|       <template #title="opt"> |       <template #title="opt"> | ||||||
|         <tspan v-for="(author, idx) in authors" :key="author.position"> |         <tspan v-for="(author, idx) in graphAuthors" :key="author.position"> | ||||||
|           <tspan v-if="opt.bar.index === idx"> |           <tspan v-if="opt.bar.index === idx"> | ||||||
|             {{ author.name }} |             {{ author.name }} | ||||||
|           </tspan> |           </tspan> | ||||||
| @@ -48,32 +48,39 @@ | |||||||
|     </vue-bar-graph> |     </vue-bar-graph> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  | 
 | ||||||
| <script> | <script> | ||||||
| import VueBarGraph from 'vue-bar-graph'; | import VueBarGraph from 'vue-bar-graph'; | ||||||
|  | import {initVueApp} from './VueComponentLoader.js'; | ||||||
| 
 | 
 | ||||||
| export default { | const sfc = { | ||||||
|   components: {VueBarGraph}, |   components: {VueBarGraph}, | ||||||
|   props: { |  | ||||||
|     data: {type: Array, default: () => []}, |  | ||||||
|   }, |  | ||||||
|   data: () => ({ |   data: () => ({ | ||||||
|     colors: { |     colors: { | ||||||
|       barColor: 'green', |       barColor: 'green', | ||||||
|       textColor: 'black', |       textColor: 'black', | ||||||
|       textAltColor: 'white', |       textAltColor: 'white', | ||||||
|     }, |     }, | ||||||
|  | 
 | ||||||
|  |     // possible keys: | ||||||
|  |     // * avatar_link: (...) | ||||||
|  |     // * commits: (...) | ||||||
|  |     // * home_link: (...) | ||||||
|  |     // * login: (...) | ||||||
|  |     // * name: (...) | ||||||
|  |     activityTopAuthors: window.config.pageData.repoActivityTopAuthors || [], | ||||||
|   }), |   }), | ||||||
|   computed: { |   computed: { | ||||||
|     graphData() { |     graphPoints() { | ||||||
|       return this.data.map((item) => { |       return this.activityTopAuthors.map((item) => { | ||||||
|         return { |         return { | ||||||
|           value: item.commits, |           value: item.commits, | ||||||
|           label: item.name, |           label: item.name, | ||||||
|         }; |         }; | ||||||
|       }); |       }); | ||||||
|     }, |     }, | ||||||
|     authors() { |     graphAuthors() { | ||||||
|       return this.data.map((item, idx) => { |       return this.activityTopAuthors.map((item, idx) => { | ||||||
|         return { |         return { | ||||||
|           position: idx + 1, |           position: idx + 1, | ||||||
|           ...item, |           ...item, | ||||||
| @@ -81,21 +88,23 @@ export default { | |||||||
|       }); |       }); | ||||||
|     }, |     }, | ||||||
|     graphWidth() { |     graphWidth() { | ||||||
|       return this.data.length * 40; |       return this.activityTopAuthors.length * 40; | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   mounted() { |   mounted() { | ||||||
|     const st = window.getComputedStyle(this.$refs.style); |     const refStyle = window.getComputedStyle(this.$refs.style); | ||||||
|     const stalt = window.getComputedStyle(this.$refs.altStyle); |     const refAltStyle = window.getComputedStyle(this.$refs.altStyle); | ||||||
| 
 | 
 | ||||||
|     this.colors.barColor = st.backgroundColor; |     this.colors.barColor = refStyle.backgroundColor; | ||||||
|     this.colors.textColor = st.color; |     this.colors.textColor = refStyle.color; | ||||||
|     this.colors.textAltColor = stalt.color; |     this.colors.textAltColor = refAltStyle.color; | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     hasHomeLink(i) { |  | ||||||
|       return this.graphData[i].homeLink !== '' && this.graphData[i].homeLink !== null; |  | ||||||
|     }, |  | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | function initRepoActivityTopAuthorsChart() { | ||||||
|  |   initVueApp('#repo-activity-top-authors-chart', sfc); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default sfc; | ||||||
|  | export {initRepoActivityTopAuthorsChart}; | ||||||
| </script> | </script> | ||||||
							
								
								
									
										161
									
								
								web_src/js/components/RepoBranchTagDropdown.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								web_src/js/components/RepoBranchTagDropdown.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,161 @@ | |||||||
|  | import Vue from 'vue'; | ||||||
|  |  | ||||||
|  | function initRepoBranchTagDropdown(selector) { | ||||||
|  |   $(selector).each(function () { | ||||||
|  |     const $dropdown = $(this); | ||||||
|  |     const $data = $dropdown.find('.data'); | ||||||
|  |     const data = { | ||||||
|  |       items: [], | ||||||
|  |       mode: $data.data('mode'), | ||||||
|  |       searchTerm: '', | ||||||
|  |       noResults: '', | ||||||
|  |       canCreateBranch: false, | ||||||
|  |       menuVisible: false, | ||||||
|  |       createTag: false, | ||||||
|  |       active: 0 | ||||||
|  |     }; | ||||||
|  |     $data.find('.item').each(function () { | ||||||
|  |       data.items.push({ | ||||||
|  |         name: $(this).text(), | ||||||
|  |         url: $(this).data('url'), | ||||||
|  |         branch: $(this).hasClass('branch'), | ||||||
|  |         tag: $(this).hasClass('tag'), | ||||||
|  |         selected: $(this).hasClass('selected') | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |     $data.remove(); | ||||||
|  |     new Vue({ | ||||||
|  |       el: this, | ||||||
|  |       delimiters: ['${', '}'], | ||||||
|  |       data, | ||||||
|  |       computed: { | ||||||
|  |         filteredItems() { | ||||||
|  |           const items = this.items.filter((item) => { | ||||||
|  |             return ((this.mode === 'branches' && item.branch) || (this.mode === 'tags' && item.tag)) && | ||||||
|  |               (!this.searchTerm || item.name.toLowerCase().includes(this.searchTerm.toLowerCase())); | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |           // no idea how to fix this so linting rule is disabled instead | ||||||
|  |           this.active = (items.length === 0 && this.showCreateNewBranch ? 0 : -1); // eslint-disable-line vue/no-side-effects-in-computed-properties | ||||||
|  |           return items; | ||||||
|  |         }, | ||||||
|  |         showNoResults() { | ||||||
|  |           return this.filteredItems.length === 0 && !this.showCreateNewBranch; | ||||||
|  |         }, | ||||||
|  |         showCreateNewBranch() { | ||||||
|  |           if (!this.canCreateBranch || !this.searchTerm) { | ||||||
|  |             return false; | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           return this.items.filter((item) => item.name.toLowerCase() === this.searchTerm.toLowerCase()).length === 0; | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       watch: { | ||||||
|  |         menuVisible(visible) { | ||||||
|  |           if (visible) { | ||||||
|  |             this.focusSearchField(); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       beforeMount() { | ||||||
|  |         this.noResults = this.$el.getAttribute('data-no-results'); | ||||||
|  |         this.canCreateBranch = this.$el.getAttribute('data-can-create-branch') === 'true'; | ||||||
|  |  | ||||||
|  |         document.body.addEventListener('click', (event) => { | ||||||
|  |           if (this.$el.contains(event.target)) return; | ||||||
|  |           if (this.menuVisible) { | ||||||
|  |             Vue.set(this, 'menuVisible', false); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       }, | ||||||
|  |  | ||||||
|  |       methods: { | ||||||
|  |         selectItem(item) { | ||||||
|  |           const prev = this.getSelected(); | ||||||
|  |           if (prev !== null) { | ||||||
|  |             prev.selected = false; | ||||||
|  |           } | ||||||
|  |           item.selected = true; | ||||||
|  |           window.location.href = item.url; | ||||||
|  |         }, | ||||||
|  |         createNewBranch() { | ||||||
|  |           if (!this.showCreateNewBranch) return; | ||||||
|  |           $(this.$refs.newBranchForm).trigger('submit'); | ||||||
|  |         }, | ||||||
|  |         focusSearchField() { | ||||||
|  |           Vue.nextTick(() => { | ||||||
|  |             this.$refs.searchField.focus(); | ||||||
|  |           }); | ||||||
|  |         }, | ||||||
|  |         getSelected() { | ||||||
|  |           for (let i = 0, j = this.items.length; i < j; ++i) { | ||||||
|  |             if (this.items[i].selected) return this.items[i]; | ||||||
|  |           } | ||||||
|  |           return null; | ||||||
|  |         }, | ||||||
|  |         getSelectedIndexInFiltered() { | ||||||
|  |           for (let i = 0, j = this.filteredItems.length; i < j; ++i) { | ||||||
|  |             if (this.filteredItems[i].selected) return i; | ||||||
|  |           } | ||||||
|  |           return -1; | ||||||
|  |         }, | ||||||
|  |         scrollToActive() { | ||||||
|  |           let el = this.$refs[`listItem${this.active}`]; | ||||||
|  |           if (!el || !el.length) return; | ||||||
|  |           if (Array.isArray(el)) { | ||||||
|  |             el = el[0]; | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           const cont = this.$refs.scrollContainer; | ||||||
|  |           if (el.offsetTop < cont.scrollTop) { | ||||||
|  |             cont.scrollTop = el.offsetTop; | ||||||
|  |           } else if (el.offsetTop + el.clientHeight > cont.scrollTop + cont.clientHeight) { | ||||||
|  |             cont.scrollTop = el.offsetTop + el.clientHeight - cont.clientHeight; | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         keydown(event) { | ||||||
|  |           if (event.keyCode === 40) { // arrow down | ||||||
|  |             event.preventDefault(); | ||||||
|  |  | ||||||
|  |             if (this.active === -1) { | ||||||
|  |               this.active = this.getSelectedIndexInFiltered(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (this.active + (this.showCreateNewBranch ? 0 : 1) >= this.filteredItems.length) { | ||||||
|  |               return; | ||||||
|  |             } | ||||||
|  |             this.active++; | ||||||
|  |             this.scrollToActive(); | ||||||
|  |           } else if (event.keyCode === 38) { // arrow up | ||||||
|  |             event.preventDefault(); | ||||||
|  |  | ||||||
|  |             if (this.active === -1) { | ||||||
|  |               this.active = this.getSelectedIndexInFiltered(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (this.active <= 0) { | ||||||
|  |               return; | ||||||
|  |             } | ||||||
|  |             this.active--; | ||||||
|  |             this.scrollToActive(); | ||||||
|  |           } else if (event.keyCode === 13) { // enter | ||||||
|  |             event.preventDefault(); | ||||||
|  |  | ||||||
|  |             if (this.active >= this.filteredItems.length) { | ||||||
|  |               this.createNewBranch(); | ||||||
|  |             } else if (this.active >= 0) { | ||||||
|  |               this.selectItem(this.filteredItems[this.active]); | ||||||
|  |             } | ||||||
|  |           } else if (event.keyCode === 27) { // escape | ||||||
|  |             event.preventDefault(); | ||||||
|  |             this.menuVisible = false; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export {initRepoBranchTagDropdown}; | ||||||
							
								
								
									
										52
									
								
								web_src/js/components/VueComponentLoader.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								web_src/js/components/VueComponentLoader.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | import Vue from 'vue'; | ||||||
|  | import {svgs} from '../svg.js'; | ||||||
|  |  | ||||||
|  | const vueDelimiters = ['${', '}']; | ||||||
|  |  | ||||||
|  | let vueEnvInited = false; | ||||||
|  | function initVueEnv() { | ||||||
|  |   if (vueEnvInited) return; | ||||||
|  |   vueEnvInited = true; | ||||||
|  |  | ||||||
|  |   const isProd = window.config.IsProd; | ||||||
|  |   Vue.config.productionTip = false; | ||||||
|  |   Vue.config.devtools = !isProd; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | let vueSvgInited = false; | ||||||
|  | function initVueSvg() { | ||||||
|  |   if (vueSvgInited) return; | ||||||
|  |   vueSvgInited = true; | ||||||
|  |  | ||||||
|  |   // register svg icon vue components, e.g. <octicon-repo size="16"/> | ||||||
|  |   for (const [name, htmlString] of Object.entries(svgs)) { | ||||||
|  |     const template = htmlString | ||||||
|  |       .replace(/height="[0-9]+"/, 'v-bind:height="size"') | ||||||
|  |       .replace(/width="[0-9]+"/, 'v-bind:width="size"'); | ||||||
|  |  | ||||||
|  |     Vue.component(name, { | ||||||
|  |       props: { | ||||||
|  |         size: { | ||||||
|  |           type: String, | ||||||
|  |           default: '16', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       template, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | function initVueApp(el, opts = {}) { | ||||||
|  |   if (typeof el === 'string') { | ||||||
|  |     el = document.querySelector(el); | ||||||
|  |   } | ||||||
|  |   if (!el) return null; | ||||||
|  |  | ||||||
|  |   return new Vue(Object.assign({ | ||||||
|  |     el, | ||||||
|  |     delimiters: vueDelimiters, | ||||||
|  |   }, opts)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export {vueDelimiters, initVueEnv, initVueSvg, initVueApp}; | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| export function initAdminUserListSearchForm() { | export function initAdminUserListSearchForm() { | ||||||
|   const searchForm = window.config.PageData.adminUserListSearchForm; |   const searchForm = window.config.pageData.adminUserListSearchForm; | ||||||
|   if (!searchForm) return; |   if (!searchForm) return; | ||||||
|  |  | ||||||
|   const $form = $('#user-list-search-form'); |   const $form = $('#user-list-search-form'); | ||||||
|   | |||||||
| @@ -1,10 +1,13 @@ | |||||||
| import './publicpath.js'; | import './publicpath.js'; | ||||||
|  |  | ||||||
| import Vue from 'vue'; |  | ||||||
| import {htmlEscape} from 'escape-goat'; | import {htmlEscape} from 'escape-goat'; | ||||||
| import 'jquery.are-you-sure'; | import 'jquery.are-you-sure'; | ||||||
|  |  | ||||||
| import ActivityTopAuthors from './components/ActivityTopAuthors.vue'; | import {initVueEnv} from './components/VueComponentLoader.js'; | ||||||
|  | import {initRepoActivityTopAuthorsChart} from './components/RepoActivityTopAuthors.vue'; | ||||||
|  | import {initDashboardRepoList} from './components/DashboardRepoList.js'; | ||||||
|  | import {initRepoBranchTagDropdown} from './components/RepoBranchTagDropdown.js'; | ||||||
|  |  | ||||||
| import attachTribute from './features/tribute.js'; | import attachTribute from './features/tribute.js'; | ||||||
| import createColorPicker from './features/colorpicker.js'; | import createColorPicker from './features/colorpicker.js'; | ||||||
| import createDropzone from './features/dropzone.js'; | import createDropzone from './features/dropzone.js'; | ||||||
| @@ -27,20 +30,16 @@ import {initStopwatch} from './features/stopwatch.js'; | |||||||
| import {showLineButton} from './code/linebutton.js'; | import {showLineButton} from './code/linebutton.js'; | ||||||
| import {initMarkupContent, initCommentContent} from './markup/content.js'; | import {initMarkupContent, initCommentContent} from './markup/content.js'; | ||||||
| import {stripTags, mqBinarySearch} from './utils.js'; | import {stripTags, mqBinarySearch} from './utils.js'; | ||||||
| import {svg, svgs} from './svg.js'; | import {svg} from './svg.js'; | ||||||
|  |  | ||||||
| const {AppSubUrl, AssetUrlPrefix, csrf} = window.config; | const {AppSubUrl, csrf} = window.config; | ||||||
|  |  | ||||||
| let previewFileModes; | let previewFileModes; | ||||||
| const commentMDEditors = {}; | const commentMDEditors = {}; | ||||||
|  |  | ||||||
| // Silence fomantic's error logging when tabs are used without a target content element | // Silence fomantic's error logging when tabs are used without a target content element | ||||||
| $.fn.tab.settings.silent = true; | $.fn.tab.settings.silent = true; | ||||||
|  | initVueEnv(); | ||||||
| // Silence Vue's console advertisements in dev mode |  | ||||||
| // To use the Vue browser extension, enable the devtools option temporarily |  | ||||||
| Vue.config.productionTip = false; |  | ||||||
| Vue.config.devtools = false; |  | ||||||
|  |  | ||||||
| function initCommentPreviewTab($form) { | function initCommentPreviewTab($form) { | ||||||
|   const $tabMenu = $form.find('.tabular.menu'); |   const $tabMenu = $form.find('.tabular.menu'); | ||||||
| @@ -806,7 +805,7 @@ async function initRepository() { | |||||||
|   // File list and commits |   // File list and commits | ||||||
|   if ($('.repository.file.list').length > 0 || |   if ($('.repository.file.list').length > 0 || | ||||||
|     $('.repository.commits').length > 0 || $('.repository.release').length > 0) { |     $('.repository.commits').length > 0 || $('.repository.release').length > 0) { | ||||||
|     initFilterBranchTagDropdown('.choose.reference .dropdown'); |     initRepoBranchTagDropdown('.choose.reference .dropdown'); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Wiki |   // Wiki | ||||||
| @@ -2858,7 +2857,8 @@ $(document).ready(async () => { | |||||||
|   initWebhook(); |   initWebhook(); | ||||||
|   initAdmin(); |   initAdmin(); | ||||||
|   initCodeView(); |   initCodeView(); | ||||||
|   initVueApp(); |   initRepoActivityTopAuthorsChart(); | ||||||
|  |   initDashboardRepoList(); | ||||||
|   initTeamSettings(); |   initTeamSettings(); | ||||||
|   initCtrlEnterSubmit(); |   initCtrlEnterSubmit(); | ||||||
|   initNavbarContentToggle(); |   initNavbarContentToggle(); | ||||||
| @@ -3105,369 +3105,6 @@ function linkEmailAction(e) { | |||||||
|   e.preventDefault(); |   e.preventDefault(); | ||||||
| } | } | ||||||
|  |  | ||||||
| function initVueComponents() { |  | ||||||
|   // register svg icon vue components, e.g. <octicon-repo size="16"/> |  | ||||||
|   for (const [name, htmlString] of Object.entries(svgs)) { |  | ||||||
|     const template = htmlString |  | ||||||
|       .replace(/height="[0-9]+"/, 'v-bind:height="size"') |  | ||||||
|       .replace(/width="[0-9]+"/, 'v-bind:width="size"'); |  | ||||||
|  |  | ||||||
|     Vue.component(name, { |  | ||||||
|       props: { |  | ||||||
|         size: { |  | ||||||
|           type: String, |  | ||||||
|           default: '16', |  | ||||||
|         }, |  | ||||||
|       }, |  | ||||||
|       template, |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const vueDelimeters = ['${', '}']; |  | ||||||
|  |  | ||||||
|   Vue.component('repo-search', { |  | ||||||
|     delimiters: vueDelimeters, |  | ||||||
|  |  | ||||||
|     props: { |  | ||||||
|       searchLimit: { |  | ||||||
|         type: Number, |  | ||||||
|         default: 10 |  | ||||||
|       }, |  | ||||||
|       suburl: { |  | ||||||
|         type: String, |  | ||||||
|         required: true |  | ||||||
|       }, |  | ||||||
|       uid: { |  | ||||||
|         type: Number, |  | ||||||
|         required: true |  | ||||||
|       }, |  | ||||||
|       teamId: { |  | ||||||
|         type: Number, |  | ||||||
|         required: false, |  | ||||||
|         default: 0 |  | ||||||
|       }, |  | ||||||
|       organizations: { |  | ||||||
|         type: Array, |  | ||||||
|         default: () => [], |  | ||||||
|       }, |  | ||||||
|       isOrganization: { |  | ||||||
|         type: Boolean, |  | ||||||
|         default: true |  | ||||||
|       }, |  | ||||||
|       canCreateOrganization: { |  | ||||||
|         type: Boolean, |  | ||||||
|         default: false |  | ||||||
|       }, |  | ||||||
|       organizationsTotalCount: { |  | ||||||
|         type: Number, |  | ||||||
|         default: 0 |  | ||||||
|       }, |  | ||||||
|       moreReposLink: { |  | ||||||
|         type: String, |  | ||||||
|         default: '' |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|  |  | ||||||
|     data() { |  | ||||||
|       const params = new URLSearchParams(window.location.search); |  | ||||||
|  |  | ||||||
|       let tab = params.get('repo-search-tab'); |  | ||||||
|       if (!tab) { |  | ||||||
|         tab = 'repos'; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       let reposFilter = params.get('repo-search-filter'); |  | ||||||
|       if (!reposFilter) { |  | ||||||
|         reposFilter = 'all'; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       let privateFilter = params.get('repo-search-private'); |  | ||||||
|       if (!privateFilter) { |  | ||||||
|         privateFilter = 'both'; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       let archivedFilter = params.get('repo-search-archived'); |  | ||||||
|       if (!archivedFilter) { |  | ||||||
|         archivedFilter = 'unarchived'; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       let searchQuery = params.get('repo-search-query'); |  | ||||||
|       if (!searchQuery) { |  | ||||||
|         searchQuery = ''; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       let page = 1; |  | ||||||
|       try { |  | ||||||
|         page = parseInt(params.get('repo-search-page')); |  | ||||||
|       } catch { |  | ||||||
|         // noop |  | ||||||
|       } |  | ||||||
|       if (!page) { |  | ||||||
|         page = 1; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       return { |  | ||||||
|         tab, |  | ||||||
|         repos: [], |  | ||||||
|         reposTotalCount: 0, |  | ||||||
|         reposFilter, |  | ||||||
|         archivedFilter, |  | ||||||
|         privateFilter, |  | ||||||
|         page, |  | ||||||
|         finalPage: 1, |  | ||||||
|         searchQuery, |  | ||||||
|         isLoading: false, |  | ||||||
|         staticPrefix: AssetUrlPrefix, |  | ||||||
|         counts: {}, |  | ||||||
|         repoTypes: { |  | ||||||
|           all: { |  | ||||||
|             searchMode: '', |  | ||||||
|           }, |  | ||||||
|           forks: { |  | ||||||
|             searchMode: 'fork', |  | ||||||
|           }, |  | ||||||
|           mirrors: { |  | ||||||
|             searchMode: 'mirror', |  | ||||||
|           }, |  | ||||||
|           sources: { |  | ||||||
|             searchMode: 'source', |  | ||||||
|           }, |  | ||||||
|           collaborative: { |  | ||||||
|             searchMode: 'collaborative', |  | ||||||
|           }, |  | ||||||
|         } |  | ||||||
|       }; |  | ||||||
|     }, |  | ||||||
|  |  | ||||||
|     computed: { |  | ||||||
|       showMoreReposLink() { |  | ||||||
|         return this.repos.length > 0 && this.repos.length < this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`]; |  | ||||||
|       }, |  | ||||||
|       searchURL() { |  | ||||||
|         return `${this.suburl}/api/v1/repos/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=${this.searchQuery |  | ||||||
|         }&page=${this.page}&limit=${this.searchLimit}&mode=${this.repoTypes[this.reposFilter].searchMode |  | ||||||
|         }${this.reposFilter !== 'all' ? '&exclusive=1' : '' |  | ||||||
|         }${this.archivedFilter === 'archived' ? '&archived=true' : ''}${this.archivedFilter === 'unarchived' ? '&archived=false' : '' |  | ||||||
|         }${this.privateFilter === 'private' ? '&is_private=true' : ''}${this.privateFilter === 'public' ? '&is_private=false' : '' |  | ||||||
|         }`; |  | ||||||
|       }, |  | ||||||
|       repoTypeCount() { |  | ||||||
|         return this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`]; |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|  |  | ||||||
|     mounted() { |  | ||||||
|       this.changeReposFilter(this.reposFilter); |  | ||||||
|       $(this.$el).find('.poping.up').popup(); |  | ||||||
|       $(this.$el).find('.dropdown').dropdown(); |  | ||||||
|       this.setCheckboxes(); |  | ||||||
|       Vue.nextTick(() => { |  | ||||||
|         this.$refs.search.focus(); |  | ||||||
|       }); |  | ||||||
|     }, |  | ||||||
|  |  | ||||||
|     methods: { |  | ||||||
|       changeTab(t) { |  | ||||||
|         this.tab = t; |  | ||||||
|         this.updateHistory(); |  | ||||||
|       }, |  | ||||||
|  |  | ||||||
|       setCheckboxes() { |  | ||||||
|         switch (this.archivedFilter) { |  | ||||||
|           case 'unarchived': |  | ||||||
|             $('#archivedFilterCheckbox').checkbox('set unchecked'); |  | ||||||
|             break; |  | ||||||
|           case 'archived': |  | ||||||
|             $('#archivedFilterCheckbox').checkbox('set checked'); |  | ||||||
|             break; |  | ||||||
|           case 'both': |  | ||||||
|             $('#archivedFilterCheckbox').checkbox('set indeterminate'); |  | ||||||
|             break; |  | ||||||
|           default: |  | ||||||
|             this.archivedFilter = 'unarchived'; |  | ||||||
|             $('#archivedFilterCheckbox').checkbox('set unchecked'); |  | ||||||
|             break; |  | ||||||
|         } |  | ||||||
|         switch (this.privateFilter) { |  | ||||||
|           case 'public': |  | ||||||
|             $('#privateFilterCheckbox').checkbox('set unchecked'); |  | ||||||
|             break; |  | ||||||
|           case 'private': |  | ||||||
|             $('#privateFilterCheckbox').checkbox('set checked'); |  | ||||||
|             break; |  | ||||||
|           case 'both': |  | ||||||
|             $('#privateFilterCheckbox').checkbox('set indeterminate'); |  | ||||||
|             break; |  | ||||||
|           default: |  | ||||||
|             this.privateFilter = 'both'; |  | ||||||
|             $('#privateFilterCheckbox').checkbox('set indeterminate'); |  | ||||||
|             break; |  | ||||||
|         } |  | ||||||
|       }, |  | ||||||
|  |  | ||||||
|       changeReposFilter(filter) { |  | ||||||
|         this.reposFilter = filter; |  | ||||||
|         this.repos = []; |  | ||||||
|         this.page = 1; |  | ||||||
|         Vue.set(this.counts, `${filter}:${this.archivedFilter}:${this.privateFilter}`, 0); |  | ||||||
|         this.searchRepos(); |  | ||||||
|       }, |  | ||||||
|  |  | ||||||
|       updateHistory() { |  | ||||||
|         const params = new URLSearchParams(window.location.search); |  | ||||||
|  |  | ||||||
|         if (this.tab === 'repos') { |  | ||||||
|           params.delete('repo-search-tab'); |  | ||||||
|         } else { |  | ||||||
|           params.set('repo-search-tab', this.tab); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (this.reposFilter === 'all') { |  | ||||||
|           params.delete('repo-search-filter'); |  | ||||||
|         } else { |  | ||||||
|           params.set('repo-search-filter', this.reposFilter); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (this.privateFilter === 'both') { |  | ||||||
|           params.delete('repo-search-private'); |  | ||||||
|         } else { |  | ||||||
|           params.set('repo-search-private', this.privateFilter); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (this.archivedFilter === 'unarchived') { |  | ||||||
|           params.delete('repo-search-archived'); |  | ||||||
|         } else { |  | ||||||
|           params.set('repo-search-archived', this.archivedFilter); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (this.searchQuery === '') { |  | ||||||
|           params.delete('repo-search-query'); |  | ||||||
|         } else { |  | ||||||
|           params.set('repo-search-query', this.searchQuery); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (this.page === 1) { |  | ||||||
|           params.delete('repo-search-page'); |  | ||||||
|         } else { |  | ||||||
|           params.set('repo-search-page', `${this.page}`); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const queryString = params.toString(); |  | ||||||
|         if (queryString) { |  | ||||||
|           window.history.replaceState({}, '', `?${queryString}`); |  | ||||||
|         } else { |  | ||||||
|           window.history.replaceState({}, '', window.location.pathname); |  | ||||||
|         } |  | ||||||
|       }, |  | ||||||
|  |  | ||||||
|       toggleArchivedFilter() { |  | ||||||
|         switch (this.archivedFilter) { |  | ||||||
|           case 'both': |  | ||||||
|             this.archivedFilter = 'unarchived'; |  | ||||||
|             break; |  | ||||||
|           case 'unarchived': |  | ||||||
|             this.archivedFilter = 'archived'; |  | ||||||
|             break; |  | ||||||
|           case 'archived': |  | ||||||
|             this.archivedFilter = 'both'; |  | ||||||
|             break; |  | ||||||
|           default: |  | ||||||
|             this.archivedFilter = 'unarchived'; |  | ||||||
|             break; |  | ||||||
|         } |  | ||||||
|         this.page = 1; |  | ||||||
|         this.repos = []; |  | ||||||
|         this.setCheckboxes(); |  | ||||||
|         Vue.set(this.counts, `${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`, 0); |  | ||||||
|         this.searchRepos(); |  | ||||||
|       }, |  | ||||||
|  |  | ||||||
|       togglePrivateFilter() { |  | ||||||
|         switch (this.privateFilter) { |  | ||||||
|           case 'both': |  | ||||||
|             this.privateFilter = 'public'; |  | ||||||
|             break; |  | ||||||
|           case 'public': |  | ||||||
|             this.privateFilter = 'private'; |  | ||||||
|             break; |  | ||||||
|           case 'private': |  | ||||||
|             this.privateFilter = 'both'; |  | ||||||
|             break; |  | ||||||
|           default: |  | ||||||
|             this.privateFilter = 'both'; |  | ||||||
|             break; |  | ||||||
|         } |  | ||||||
|         this.page = 1; |  | ||||||
|         this.repos = []; |  | ||||||
|         this.setCheckboxes(); |  | ||||||
|         Vue.set(this.counts, `${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`, 0); |  | ||||||
|         this.searchRepos(); |  | ||||||
|       }, |  | ||||||
|  |  | ||||||
|  |  | ||||||
|       changePage(page) { |  | ||||||
|         this.page = page; |  | ||||||
|         if (this.page > this.finalPage) { |  | ||||||
|           this.page = this.finalPage; |  | ||||||
|         } |  | ||||||
|         if (this.page < 1) { |  | ||||||
|           this.page = 1; |  | ||||||
|         } |  | ||||||
|         this.repos = []; |  | ||||||
|         Vue.set(this.counts, `${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`, 0); |  | ||||||
|         this.searchRepos(); |  | ||||||
|       }, |  | ||||||
|  |  | ||||||
|       searchRepos() { |  | ||||||
|         this.isLoading = true; |  | ||||||
|  |  | ||||||
|         if (!this.reposTotalCount) { |  | ||||||
|           const totalCountSearchURL = `${this.suburl}/api/v1/repos/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`; |  | ||||||
|           $.getJSON(totalCountSearchURL, (_result, _textStatus, request) => { |  | ||||||
|             this.reposTotalCount = request.getResponseHeader('X-Total-Count'); |  | ||||||
|           }); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const searchedMode = this.repoTypes[this.reposFilter].searchMode; |  | ||||||
|         const searchedURL = this.searchURL; |  | ||||||
|         const searchedQuery = this.searchQuery; |  | ||||||
|  |  | ||||||
|         $.getJSON(searchedURL, (result, _textStatus, request) => { |  | ||||||
|           if (searchedURL === this.searchURL) { |  | ||||||
|             this.repos = result.data; |  | ||||||
|             const count = request.getResponseHeader('X-Total-Count'); |  | ||||||
|             if (searchedQuery === '' && searchedMode === '' && this.archivedFilter === 'both') { |  | ||||||
|               this.reposTotalCount = count; |  | ||||||
|             } |  | ||||||
|             Vue.set(this.counts, `${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`, count); |  | ||||||
|             this.finalPage = Math.ceil(count / this.searchLimit); |  | ||||||
|             this.updateHistory(); |  | ||||||
|           } |  | ||||||
|         }).always(() => { |  | ||||||
|           if (searchedURL === this.searchURL) { |  | ||||||
|             this.isLoading = false; |  | ||||||
|           } |  | ||||||
|         }); |  | ||||||
|       }, |  | ||||||
|  |  | ||||||
|       repoIcon(repo) { |  | ||||||
|         if (repo.fork) { |  | ||||||
|           return 'octicon-repo-forked'; |  | ||||||
|         } else if (repo.mirror) { |  | ||||||
|           return 'octicon-mirror'; |  | ||||||
|         } else if (repo.template) { |  | ||||||
|           return `octicon-repo-template`; |  | ||||||
|         } else if (repo.private) { |  | ||||||
|           return 'octicon-lock'; |  | ||||||
|         } else if (repo.internal) { |  | ||||||
|           return 'octicon-repo'; |  | ||||||
|         } |  | ||||||
|         return 'octicon-repo'; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function initCtrlEnterSubmit() { | function initCtrlEnterSubmit() { | ||||||
|   $('.js-quick-submit').on('keydown', function (e) { |   $('.js-quick-submit').on('keydown', function (e) { | ||||||
|     if (((e.ctrlKey && !e.altKey) || e.metaKey) && (e.keyCode === 13 || e.keyCode === 10)) { |     if (((e.ctrlKey && !e.altKey) || e.metaKey) && (e.keyCode === 13 || e.keyCode === 10)) { | ||||||
| @@ -3476,31 +3113,6 @@ function initCtrlEnterSubmit() { | |||||||
|   }); |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
| function initVueApp() { |  | ||||||
|   const el = document.getElementById('app'); |  | ||||||
|   if (!el) { |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   initVueComponents(); |  | ||||||
|  |  | ||||||
|   new Vue({ |  | ||||||
|     el, |  | ||||||
|     delimiters: ['${', '}'], |  | ||||||
|     components: { |  | ||||||
|       ActivityTopAuthors, |  | ||||||
|     }, |  | ||||||
|     data: () => { |  | ||||||
|       return { |  | ||||||
|         searchLimit: Number((document.querySelector('meta[name=_search_limit]') || {}).content), |  | ||||||
|         suburl: AppSubUrl, |  | ||||||
|         uid: Number((document.querySelector('meta[name=_context_uid]') || {}).content), |  | ||||||
|         activityTopAuthors: window.ActivityTopAuthors || [], |  | ||||||
|       }; |  | ||||||
|     }, |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function initIssueTimetracking() { | function initIssueTimetracking() { | ||||||
|   $(document).on('click', '.issue-add-time', () => { |   $(document).on('click', '.issue-add-time', () => { | ||||||
|     $('.issue-start-time-modal').modal({ |     $('.issue-start-time-modal').modal({ | ||||||
| @@ -3543,163 +3155,6 @@ function initBranchOrTagDropdown(selector) { | |||||||
|   }); |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
| function initFilterBranchTagDropdown(selector) { |  | ||||||
|   $(selector).each(function () { |  | ||||||
|     const $dropdown = $(this); |  | ||||||
|     const $data = $dropdown.find('.data'); |  | ||||||
|     const data = { |  | ||||||
|       items: [], |  | ||||||
|       mode: $data.data('mode'), |  | ||||||
|       searchTerm: '', |  | ||||||
|       noResults: '', |  | ||||||
|       canCreateBranch: false, |  | ||||||
|       menuVisible: false, |  | ||||||
|       createTag: false, |  | ||||||
|       active: 0 |  | ||||||
|     }; |  | ||||||
|     $data.find('.item').each(function () { |  | ||||||
|       data.items.push({ |  | ||||||
|         name: $(this).text(), |  | ||||||
|         url: $(this).data('url'), |  | ||||||
|         branch: $(this).hasClass('branch'), |  | ||||||
|         tag: $(this).hasClass('tag'), |  | ||||||
|         selected: $(this).hasClass('selected') |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|     $data.remove(); |  | ||||||
|     new Vue({ |  | ||||||
|       el: this, |  | ||||||
|       delimiters: ['${', '}'], |  | ||||||
|       data, |  | ||||||
|       computed: { |  | ||||||
|         filteredItems() { |  | ||||||
|           const items = this.items.filter((item) => { |  | ||||||
|             return ((this.mode === 'branches' && item.branch) || (this.mode === 'tags' && item.tag)) && |  | ||||||
|               (!this.searchTerm || item.name.toLowerCase().includes(this.searchTerm.toLowerCase())); |  | ||||||
|           }); |  | ||||||
|  |  | ||||||
|           // no idea how to fix this so linting rule is disabled instead |  | ||||||
|           this.active = (items.length === 0 && this.showCreateNewBranch ? 0 : -1); // eslint-disable-line vue/no-side-effects-in-computed-properties |  | ||||||
|           return items; |  | ||||||
|         }, |  | ||||||
|         showNoResults() { |  | ||||||
|           return this.filteredItems.length === 0 && !this.showCreateNewBranch; |  | ||||||
|         }, |  | ||||||
|         showCreateNewBranch() { |  | ||||||
|           if (!this.canCreateBranch || !this.searchTerm) { |  | ||||||
|             return false; |  | ||||||
|           } |  | ||||||
|  |  | ||||||
|           return this.items.filter((item) => item.name.toLowerCase() === this.searchTerm.toLowerCase()).length === 0; |  | ||||||
|         } |  | ||||||
|       }, |  | ||||||
|  |  | ||||||
|       watch: { |  | ||||||
|         menuVisible(visible) { |  | ||||||
|           if (visible) { |  | ||||||
|             this.focusSearchField(); |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       }, |  | ||||||
|  |  | ||||||
|       beforeMount() { |  | ||||||
|         this.noResults = this.$el.getAttribute('data-no-results'); |  | ||||||
|         this.canCreateBranch = this.$el.getAttribute('data-can-create-branch') === 'true'; |  | ||||||
|  |  | ||||||
|         document.body.addEventListener('click', (event) => { |  | ||||||
|           if (this.$el.contains(event.target)) return; |  | ||||||
|           if (this.menuVisible) { |  | ||||||
|             Vue.set(this, 'menuVisible', false); |  | ||||||
|           } |  | ||||||
|         }); |  | ||||||
|       }, |  | ||||||
|  |  | ||||||
|       methods: { |  | ||||||
|         selectItem(item) { |  | ||||||
|           const prev = this.getSelected(); |  | ||||||
|           if (prev !== null) { |  | ||||||
|             prev.selected = false; |  | ||||||
|           } |  | ||||||
|           item.selected = true; |  | ||||||
|           window.location.href = item.url; |  | ||||||
|         }, |  | ||||||
|         createNewBranch() { |  | ||||||
|           if (!this.showCreateNewBranch) return; |  | ||||||
|           $(this.$refs.newBranchForm).trigger('submit'); |  | ||||||
|         }, |  | ||||||
|         focusSearchField() { |  | ||||||
|           Vue.nextTick(() => { |  | ||||||
|             this.$refs.searchField.focus(); |  | ||||||
|           }); |  | ||||||
|         }, |  | ||||||
|         getSelected() { |  | ||||||
|           for (let i = 0, j = this.items.length; i < j; ++i) { |  | ||||||
|             if (this.items[i].selected) return this.items[i]; |  | ||||||
|           } |  | ||||||
|           return null; |  | ||||||
|         }, |  | ||||||
|         getSelectedIndexInFiltered() { |  | ||||||
|           for (let i = 0, j = this.filteredItems.length; i < j; ++i) { |  | ||||||
|             if (this.filteredItems[i].selected) return i; |  | ||||||
|           } |  | ||||||
|           return -1; |  | ||||||
|         }, |  | ||||||
|         scrollToActive() { |  | ||||||
|           let el = this.$refs[`listItem${this.active}`]; |  | ||||||
|           if (!el || !el.length) return; |  | ||||||
|           if (Array.isArray(el)) { |  | ||||||
|             el = el[0]; |  | ||||||
|           } |  | ||||||
|  |  | ||||||
|           const cont = this.$refs.scrollContainer; |  | ||||||
|           if (el.offsetTop < cont.scrollTop) { |  | ||||||
|             cont.scrollTop = el.offsetTop; |  | ||||||
|           } else if (el.offsetTop + el.clientHeight > cont.scrollTop + cont.clientHeight) { |  | ||||||
|             cont.scrollTop = el.offsetTop + el.clientHeight - cont.clientHeight; |  | ||||||
|           } |  | ||||||
|         }, |  | ||||||
|         keydown(event) { |  | ||||||
|           if (event.keyCode === 40) { // arrow down |  | ||||||
|             event.preventDefault(); |  | ||||||
|  |  | ||||||
|             if (this.active === -1) { |  | ||||||
|               this.active = this.getSelectedIndexInFiltered(); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if (this.active + (this.showCreateNewBranch ? 0 : 1) >= this.filteredItems.length) { |  | ||||||
|               return; |  | ||||||
|             } |  | ||||||
|             this.active++; |  | ||||||
|             this.scrollToActive(); |  | ||||||
|           } else if (event.keyCode === 38) { // arrow up |  | ||||||
|             event.preventDefault(); |  | ||||||
|  |  | ||||||
|             if (this.active === -1) { |  | ||||||
|               this.active = this.getSelectedIndexInFiltered(); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if (this.active <= 0) { |  | ||||||
|               return; |  | ||||||
|             } |  | ||||||
|             this.active--; |  | ||||||
|             this.scrollToActive(); |  | ||||||
|           } else if (event.keyCode === 13) { // enter |  | ||||||
|             event.preventDefault(); |  | ||||||
|  |  | ||||||
|             if (this.active >= this.filteredItems.length) { |  | ||||||
|               this.createNewBranch(); |  | ||||||
|             } else if (this.active >= 0) { |  | ||||||
|               this.selectItem(this.filteredItems[this.active]); |  | ||||||
|             } |  | ||||||
|           } else if (event.keyCode === 27) { // escape |  | ||||||
|             event.preventDefault(); |  | ||||||
|             this.menuVisible = false; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| $('.commit-button').on('click', function (e) { | $('.commit-button').on('click', function (e) { | ||||||
|   e.preventDefault(); |   e.preventDefault(); | ||||||
|   | |||||||
| @@ -415,10 +415,6 @@ | |||||||
|             opacity: var(--opacity-disabled); |             opacity: var(--opacity-disabled); | ||||||
|             cursor: default; |             cursor: default; | ||||||
|           } |           } | ||||||
|  |  | ||||||
|           #delete-file-form { |  | ||||||
|             display: inline-block; |  | ||||||
|           } |  | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user