diff options
author | wxiaoguang <wxiaoguang@gmail.com> | 2021-10-15 10:35:26 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-10-15 10:35:26 +0800 |
commit | 56362043d35d2542c6fe4ac7c0ac5aabb833a9ed (patch) | |
tree | d3dd12505e7dd1b2d854c61384e1088f6901bab6 | |
parent | 96ff3e310f0ba1e94f4e8206758b583719a9b46c (diff) | |
download | gitea-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-- | .eslintrc | 2 | ||||
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | docs/content/doc/developers/guidelines-frontend.md | 51 | ||||
-rw-r--r-- | docs/content/doc/developers/hacking-on-gitea.en-us.md | 11 | ||||
-rw-r--r-- | modules/context/context.go | 5 | ||||
-rw-r--r-- | routers/web/repo/activity.go | 2 | ||||
-rw-r--r-- | routers/web/user/home.go | 5 | ||||
-rw-r--r-- | templates/base/head.tmpl | 14 | ||||
-rw-r--r-- | templates/repo/activity.tmpl | 19 | ||||
-rw-r--r-- | templates/repo/issue/view_content.tmpl | 2 | ||||
-rw-r--r-- | templates/repo/view_file.tmpl | 10 | ||||
-rw-r--r-- | templates/user/dashboard/repolist.tmpl | 13 | ||||
-rw-r--r-- | web_src/js/components/ActivityHeatmap.vue | 1 | ||||
-rw-r--r-- | web_src/js/components/DashboardRepoList.js | 370 | ||||
-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.js | 161 | ||||
-rw-r--r-- | web_src/js/components/VueComponentLoader.js | 52 | ||||
-rw-r--r-- | web_src/js/features/admin-users.js | 2 | ||||
-rw-r--r-- | web_src/js/index.js | 567 | ||||
-rw-r--r-- | web_src/less/_repository.less | 4 |
20 files changed, 718 insertions, 634 deletions
@@ -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; - } } } |