summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorwxiaoguang <wxiaoguang@gmail.com>2021-10-15 10:35:26 +0800
committerGitHub <noreply@github.com>2021-10-15 10:35:26 +0800
commit56362043d35d2542c6fe4ac7c0ac5aabb833a9ed (patch)
treed3dd12505e7dd1b2d854c61384e1088f6901bab6
parent96ff3e310f0ba1e94f4e8206758b583719a9b46c (diff)
downloadgitea-56362043d35d2542c6fe4ac7c0ac5aabb833a9ed.tar.gz
gitea-56362043d35d2542c6fe4ac7c0ac5aabb833a9ed.zip
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 4d78ad9b0e96ca180e0823de17659a2e0814c099. Co-authored-by: delvh <dev.lh@web.de> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: 6543 <6543@obermui.de>
-rw-r--r--.eslintrc2
-rw-r--r--.gitignore2
-rw-r--r--docs/content/doc/developers/guidelines-frontend.md51
-rw-r--r--docs/content/doc/developers/hacking-on-gitea.en-us.md11
-rw-r--r--modules/context/context.go5
-rw-r--r--routers/web/repo/activity.go2
-rw-r--r--routers/web/user/home.go5
-rw-r--r--templates/base/head.tmpl14
-rw-r--r--templates/repo/activity.tmpl19
-rw-r--r--templates/repo/issue/view_content.tmpl2
-rw-r--r--templates/repo/view_file.tmpl10
-rw-r--r--templates/user/dashboard/repolist.tmpl13
-rw-r--r--web_src/js/components/ActivityHeatmap.vue1
-rw-r--r--web_src/js/components/DashboardRepoList.js370
-rw-r--r--web_src/js/components/RepoActivityTopAuthors.vue (renamed from web_src/js/components/ActivityTopAuthors.vue)59
-rw-r--r--web_src/js/components/RepoBranchTagDropdown.js161
-rw-r--r--web_src/js/components/VueComponentLoader.js52
-rw-r--r--web_src/js/features/admin-users.js2
-rw-r--r--web_src/js/index.js567
-rw-r--r--web_src/less/_repository.less4
20 files changed, 718 insertions, 634 deletions
diff --git a/.eslintrc b/.eslintrc
index bab34478cf..4419e16a35 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -3,8 +3,6 @@ reportUnusedDisableDirectives: true
ignorePatterns:
- /web_src/js/vendor
- - /templates/repo/activity.tmpl
- - /templates/repo/view_file.tmpl
parserOptions:
sourceType: module
diff --git a/.gitignore b/.gitignore
index 5bf71be65d..10d9574f33 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,8 @@ _test
# IntelliJ
.idea
+# Goland's output filename can not be set manually
+/go_build_*
# MS VSCode
.vscode
diff --git a/docs/content/doc/developers/guidelines-frontend.md b/docs/content/doc/developers/guidelines-frontend.md
new file mode 100644
index 0000000000..86286127aa
--- /dev/null
+++ b/docs/content/doc/developers/guidelines-frontend.md
@@ -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.
diff --git a/docs/content/doc/developers/hacking-on-gitea.en-us.md b/docs/content/doc/developers/hacking-on-gitea.en-us.md
index 23e3b37680..d91d80e626 100644
--- a/docs/content/doc/developers/hacking-on-gitea.en-us.md
+++ b/docs/content/doc/developers/hacking-on-gitea.en-us.md
@@ -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:
```bash
+# for both frontend and backend
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.
@@ -167,7 +174,9 @@ make revive vet misspell-check
### 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
make build && ./gitea
diff --git a/modules/context/context.go b/modules/context/context.go
index 29f52c53cd..6bd934928e 100644
--- a/modules/context/context.go
+++ b/modules/context/context.go
@@ -51,7 +51,7 @@ type Context struct {
Resp ResponseWriter
Req *http.Request
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
translation.Locale
Cache cache.Cache
@@ -645,9 +645,10 @@ func Contexter() func(next http.Handler) http.Handler {
"CurrentURL": setting.AppSubURL + req.URL.RequestURI(),
"PageStartTime": startTime,
"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.Data["PageData"] = ctx.PageData
diff --git a/routers/web/repo/activity.go b/routers/web/repo/activity.go
index dcb7bf57cd..f9d248b06e 100644
--- a/routers/web/repo/activity.go
+++ b/routers/web/repo/activity.go
@@ -60,7 +60,7 @@ func Activity(ctx *context.Context) {
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)
return
}
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index 2f1fca4527..d2b67e6e59 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -106,7 +106,10 @@ func Dashboard(ctx *context.Context) {
ctx.Data["Title"] = ctxUser.DisplayName() + " - " + ctx.Tr("dashboard")
ctx.Data["PageIsDashboard"] = 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 {
data, err := models.GetUserHeatmapDataByUserTeam(ctxUser, ctx.Org.Team, ctx.User)
diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl
index 817bdae288..80bb121c6b 100644
--- a/templates/base/head.tmpl
+++ b/templates/base/head.tmpl
@@ -1,6 +1,6 @@
<!DOCTYPE html>
<html lang="{{.Lang}}" class="theme-{{.SignedUser.Theme}}">
-<head data-suburl="{{AppSubUrl}}">
+<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<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="referrer" content="no-referrer" />
<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}}
<meta name="go-import" content="{{.GoGetImport}} git {{.CloneLink.HTTPS}}">
<meta name="go-source" content="{{.GoGetImport}} _ {{.GoDocDirectory}} {{.GoDocFile}}">
@@ -31,10 +22,11 @@
AppVer: '{{AppVer}}',
AppSubUrl: '{{AppSubUrl}}',
AssetUrlPrefix: '{{AssetUrlPrefix}}',
+ IsProd: {{.IsProd}},
CustomEmojis: {{CustomEmojis}},
UseServiceWorker: {{UseServiceWorker}},
csrf: '{{.CsrfToken}}',
- PageData: {{ .PageData }},
+ pageData: {{ .PageData }},
HighlightJS: {{if .RequireHighlightJS}}true{{else}}false{{end}},
SimpleMDE: {{if .RequireSimpleMDE}}true{{else}}false{{end}},
Tribute: {{if .RequireTribute}}true{{else}}false{{end}},
diff --git a/templates/repo/activity.tmpl b/templates/repo/activity.tmpl
index 08e2a31115..d4cff880e5 100644
--- a/templates/repo/activity.tmpl
+++ b/templates/repo/activity.tmpl
@@ -108,11 +108,8 @@
{{.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>.
</div>
- <div class="ui attached segment" id="app">
- <script type="text/javascript">
- var ActivityTopAuthors = {{Json .ActivityTopAuthors | SafeJS}};
- </script>
- <activity-top-authors :data="activityTopAuthors" />
+ <div class="ui attached segment">
+ <div id="repo-activity-top-authors-chart"></div>
</div>
</div>
{{end}}
@@ -126,7 +123,7 @@
<div class="list">
{{range .Activity.PublishedReleases}}
<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}}
{{if not .IsTag}}
<a class="title" href="{{$.RepoLink}}/src/{{.TagName | EscapePound}}">{{.Title | RenderEmoji}}</a>
@@ -145,7 +142,7 @@
<div class="list">
{{range .Activity.MergedPRs}}
<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>
{{TimeSinceUnix .MergedUnix $.Lang}}
</p>
@@ -161,7 +158,7 @@
<div class="list">
{{range .Activity.OpenedPRs}}
<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>
{{TimeSinceUnix .Issue.CreatedUnix $.Lang}}
</p>
@@ -177,7 +174,7 @@
<div class="list">
{{range .Activity.ClosedIssues}}
<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>
{{TimeSinceUnix .ClosedUnix $.Lang}}
</p>
@@ -193,7 +190,7 @@
<div class="list">
{{range .Activity.OpenedIssues}}
<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>
{{TimeSinceUnix .CreatedUnix $.Lang}}
</p>
@@ -212,7 +209,7 @@
<div class="list">
{{range .Activity.UnresolvedIssues}}
<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}}
{{if .IsPull}}
<a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Title | RenderEmoji}}</a>
diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl
index 95c9296174..9ad6bee651 100644
--- a/templates/repo/issue/view_content.tmpl
+++ b/templates/repo/issue/view_content.tmpl
@@ -9,7 +9,7 @@
{{end}}
<!-- 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="repoId" value="{{.Repository.ID}}">
<input type="hidden" id="issueIndex" value="{{.Issue.Index}}"/>
diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl
index 8e75bcb4ca..0c8990a4f5 100644
--- a/templates/repo/view_file.tmpl
+++ b/templates/repo/view_file.tmpl
@@ -131,13 +131,3 @@
</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>
diff --git a/templates/user/dashboard/repolist.tmpl b/templates/user/dashboard/repolist.tmpl
index f39d3711d4..e2cfa76e88 100644
--- a/templates/user/dashboard/repolist.tmpl
+++ b/templates/user/dashboard/repolist.tmpl
@@ -1,8 +1,7 @@
-<div id="app" class="six wide column">
+<div id="dashboard-repo-list" class="six wide column">
<repo-search
:search-limit="searchLimit"
- :suburl="suburl"
- :uid="uid"
+ :sub-url="subUrl"
{{if .Team}}
:team-id="{{.Team.ID}}"
{{end}}
@@ -31,7 +30,7 @@
{{.i18n.Tr "home.my_repos"}}
<span class="ui grey label ml-3">${reposTotalCount}</span>
</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"}}
<span class="sr-only">{{.i18n.Tr "new_repo"}}</span>
</a>
@@ -122,7 +121,7 @@
<div v-if="repos.length" class="ui attached table segment rounded-bottom">
<ul class="repo-owner-name-list">
<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">
<component v-bind:is="repoIcon(repo)" size="16"></component>
<strong>${repo.full_name}</strong>
@@ -168,7 +167,7 @@
{{.i18n.Tr "home.my_orgs"}}
<span class="ui grey label ml-3">${organizationsTotalCount}</span>
</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"}}
<span class="sr-only">{{.i18n.Tr "new_org"}}</span>
</a>
@@ -176,7 +175,7 @@
<div v-if="organizations.length" class="ui attached table segment rounded-bottom">
<ul class="repo-owner-name-list">
<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">
{{svg "octicon-organization" 16 "mr-2"}}
<strong>${org.name}</strong>
diff --git a/web_src/js/components/ActivityHeatmap.vue b/web_src/js/components/ActivityHeatmap.vue
index 63e38ea69e..fa2c43b5b4 100644
--- a/web_src/js/components/ActivityHeatmap.vue
+++ b/web_src/js/components/ActivityHeatmap.vue
@@ -70,4 +70,3 @@ export default {
},
};
</script>
-<style scoped/>
diff --git a/web_src/js/components/DashboardRepoList.js b/web_src/js/components/DashboardRepoList.js
new file mode 100644
index 0000000000..7ae62af0a0
--- /dev/null
+++ b/web_src/js/components/DashboardRepoList.js
@@ -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};
diff --git a/web_src/js/components/ActivityTopAuthors.vue b/web_src/js/components/RepoActivityTopAuthors.vue
index a9ee0e56d5..d510695b1d 100644
--- a/web_src/js/components/ActivityTopAuthors.vue
+++ b/web_src/js/components/RepoActivityTopAuthors.vue
@@ -1,9 +1,9 @@
<template>
<div>
- <div class="activity-bar-graph" ref="style" style="width:0px;height:0px"/>
- <div class="activity-bar-graph-alt" ref="altStyle" 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: 0; height: 0;"/>
<vue-bar-graph
- :points="graphData"
+ :points="graphPoints"
:show-x-axis="true"
:show-y-axis="false"
:show-values="true"
@@ -15,9 +15,9 @@
:label-height="20"
>
<template #label="opt">
- <g v-for="(author, idx) in authors" :key="author.position">
+ <g v-for="(author, idx) in graphAuthors" :key="author.position">
<a
- v-if="opt.bar.index === idx && author.home_link !== ''"
+ v-if="opt.bar.index === idx && author.home_link"
:href="author.home_link"
>
<image
@@ -39,7 +39,7 @@
</g>
</template>
<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">
{{ author.name }}
</tspan>
@@ -48,32 +48,39 @@
</vue-bar-graph>
</div>
</template>
+
<script>
import VueBarGraph from 'vue-bar-graph';
+import {initVueApp} from './VueComponentLoader.js';
-export default {
+const sfc = {
components: {VueBarGraph},
- props: {
- data: {type: Array, default: () => []},
- },
data: () => ({
colors: {
barColor: 'green',
textColor: 'black',
textAltColor: 'white',
},
+
+ // possible keys:
+ // * avatar_link: (...)
+ // * commits: (...)
+ // * home_link: (...)
+ // * login: (...)
+ // * name: (...)
+ activityTopAuthors: window.config.pageData.repoActivityTopAuthors || [],
}),
computed: {
- graphData() {
- return this.data.map((item) => {
+ graphPoints() {
+ return this.activityTopAuthors.map((item) => {
return {
value: item.commits,
label: item.name,
};
});
},
- authors() {
- return this.data.map((item, idx) => {
+ graphAuthors() {
+ return this.activityTopAuthors.map((item, idx) => {
return {
position: idx + 1,
...item,
@@ -81,21 +88,23 @@ export default {
});
},
graphWidth() {
- return this.data.length * 40;
+ return this.activityTopAuthors.length * 40;
},
},
mounted() {
- const st = window.getComputedStyle(this.$refs.style);
- const stalt = window.getComputedStyle(this.$refs.altStyle);
+ const refStyle = window.getComputedStyle(this.$refs.style);
+ const refAltStyle = window.getComputedStyle(this.$refs.altStyle);
- this.colors.barColor = st.backgroundColor;
- this.colors.textColor = st.color;
- this.colors.textAltColor = stalt.color;
- },
- methods: {
- hasHomeLink(i) {
- return this.graphData[i].homeLink !== '' && this.graphData[i].homeLink !== null;
- },
+ this.colors.barColor = refStyle.backgroundColor;
+ this.colors.textColor = refStyle.color;
+ this.colors.textAltColor = refAltStyle.color;
}
};
+
+function initRepoActivityTopAuthorsChart() {
+ initVueApp('#repo-activity-top-authors-chart', sfc);
+}
+
+export default sfc;
+export {initRepoActivityTopAuthorsChart};
</script>
diff --git a/web_src/js/components/RepoBranchTagDropdown.js b/web_src/js/components/RepoBranchTagDropdown.js
new file mode 100644
index 0000000000..a0be57ab3d
--- /dev/null
+++ b/web_src/js/components/RepoBranchTagDropdown.js
@@ -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};
diff --git a/web_src/js/components/VueComponentLoader.js b/web_src/js/components/VueComponentLoader.js
new file mode 100644
index 0000000000..6b2a2cbd58
--- /dev/null
+++ b/web_src/js/components/VueComponentLoader.js
@@ -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};
diff --git a/web_src/js/features/admin-users.js b/web_src/js/features/admin-users.js
index b01c66afe2..5863c5480f 100644
--- a/web_src/js/features/admin-users.js
+++ b/web_src/js/features/admin-users.js
@@ -1,5 +1,5 @@
export function initAdminUserListSearchForm() {
- const searchForm = window.config.PageData.adminUserListSearchForm;
+ const searchForm = window.config.pageData.adminUserListSearchForm;
if (!searchForm) return;
const $form = $('#user-list-search-form');
diff --git a/web_src/js/index.js b/web_src/js/index.js
index bc9725da95..e6269c8abf 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -1,10 +1,13 @@
import './publicpath.js';
-import Vue from 'vue';
import {htmlEscape} from 'escape-goat';
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 createColorPicker from './features/colorpicker.js';
import createDropzone from './features/dropzone.js';
@@ -27,20 +30,16 @@ import {initStopwatch} from './features/stopwatch.js';
import {showLineButton} from './code/linebutton.js';
import {initMarkupContent, initCommentContent} from './markup/content.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;
const commentMDEditors = {};
// Silence fomantic's error logging when tabs are used without a target content element
$.fn.tab.settings.silent = true;
-
-// 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;
+initVueEnv();
function initCommentPreviewTab($form) {
const $tabMenu = $form.find('.tabular.menu');
@@ -806,7 +805,7 @@ async function initRepository() {
// File list and commits
if ($('.repository.file.list').length > 0 ||
$('.repository.commits').length > 0 || $('.repository.release').length > 0) {
- initFilterBranchTagDropdown('.choose.reference .dropdown');
+ initRepoBranchTagDropdown('.choose.reference .dropdown');
}
// Wiki
@@ -2858,7 +2857,8 @@ $(document).ready(async () => {
initWebhook();
initAdmin();
initCodeView();
- initVueApp();
+ initRepoActivityTopAuthorsChart();
+ initDashboardRepoList();
initTeamSettings();
initCtrlEnterSubmit();
initNavbarContentToggle();
@@ -3105,369 +3105,6 @@ function linkEmailAction(e) {
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() {
$('.js-quick-submit').on('keydown', function (e) {
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() {
$(document).on('click', '.issue-add-time', () => {
$('.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) {
e.preventDefault();
diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less
index 1e572ffa7e..3d7d40ffa3 100644
--- a/web_src/less/_repository.less
+++ b/web_src/less/_repository.less
@@ -415,10 +415,6 @@
opacity: var(--opacity-disabled);
cursor: default;
}
-
- #delete-file-form {
- display: inline-block;
- }
}
}