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:
parent
96ff3e310f
commit
56362043d3
20 changed files with 718 additions and 634 deletions
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue