]> source.dussan.org Git - gitea.git/commitdiff
Frontend refactor: move Vue related code from `index.js` to `components` dir, and...
authorwxiaoguang <wxiaoguang@gmail.com>
Fri, 15 Oct 2021 02:35:26 +0000 (10:35 +0800)
committerGitHub <noreply@github.com>
Fri, 15 Oct 2021 02:35:26 +0000 (10:35 +0800)
* 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>
21 files changed:
.eslintrc
.gitignore
docs/content/doc/developers/guidelines-frontend.md [new file with mode: 0644]
docs/content/doc/developers/hacking-on-gitea.en-us.md
modules/context/context.go
routers/web/repo/activity.go
routers/web/user/home.go
templates/base/head.tmpl
templates/repo/activity.tmpl
templates/repo/issue/view_content.tmpl
templates/repo/view_file.tmpl
templates/user/dashboard/repolist.tmpl
web_src/js/components/ActivityHeatmap.vue
web_src/js/components/ActivityTopAuthors.vue [deleted file]
web_src/js/components/DashboardRepoList.js [new file with mode: 0644]
web_src/js/components/RepoActivityTopAuthors.vue [new file with mode: 0644]
web_src/js/components/RepoBranchTagDropdown.js [new file with mode: 0644]
web_src/js/components/VueComponentLoader.js [new file with mode: 0644]
web_src/js/features/admin-users.js
web_src/js/index.js
web_src/less/_repository.less

index bab34478cf46412894c1dd1d7014590b8bf0f59f..4419e16a35b3eb497025fdf21659ed407ead7b9e 100644 (file)
--- 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
index 5bf71be65d56082504bdd73edc4c0f631b76cd49..10d9574f33d9f3b73fdca37531dfd48e5932a27a 100644 (file)
@@ -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 (file)
index 0000000..8628612
--- /dev/null
@@ -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.
index 23e3b37680914d7df156429ee0eaa11040d627d4..d91d80e626c6d9bb926773449d118e1bf23e4cc3 100644 (file)
@@ -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
index 29f52c53cd1a48e67e9174f812dac0ec9a46db29..6bd934928e1977b20fb42e89d3d2088ec557fb5b 100644 (file)
@@ -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
 
index dcb7bf57cd45f7fcbf2166bc5d947a5469a62996..f9d248b06ea8a20c449998d760a664bbeefed993 100644 (file)
@@ -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
        }
index 2f1fca45271155090cf77c33b097b7bdb88fcc50..d2b67e6e5921a88eb67ea72025779a12441d576d 100644 (file)
@@ -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)
index 817bdae288c04b60b3bd570cd0014794b2de1743..80bb121c6bcea3d2922e59834c3c6c0d0e9ebd87 100644 (file)
@@ -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>
        <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}}">
                        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}},
index 08e2a31115a35146016de5df7faa2a245cbfe473..d4cff880e58294cbaf91ad53903bac1cca967b3a 100644 (file)
                                                {{.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}}
                        <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>
                        <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>
                        <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>
                        <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>
                        <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>
                        <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>
index 95c929617442cc3d79c1ad11c2b0269dbcb9d4eb..9ad6bee6515ad541facc047567b59b801c7c2fa1 100644 (file)
@@ -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}}"/>
index 8e75bcb4ca495f32d01cbb27ff22fbf6db43bd4c..0c8990a4f5c8d8121e8d0ac7aec783625761c59e 100644 (file)
                </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>
index f39d3711d473fa38919576f46b0c79e084a8d1f0..e2cfa76e88328776e7ef001aab8fb9f343b2b8b0 100644 (file)
@@ -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>
                        <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>
                                        {{.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>
                        <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>
index 63e38ea69e1e5fc70da2a50513c740146d42e3f5..fa2c43b5b4922d502ec425e8814a73769b869042 100644 (file)
@@ -70,4 +70,3 @@ export default {
   },
 };
 </script>
-<style scoped/>
diff --git a/web_src/js/components/ActivityTopAuthors.vue b/web_src/js/components/ActivityTopAuthors.vue
deleted file mode 100644 (file)
index a9ee0e5..0000000
+++ /dev/null
@@ -1,101 +0,0 @@
-<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"/>
-    <vue-bar-graph
-      :points="graphData"
-      :show-x-axis="true"
-      :show-y-axis="false"
-      :show-values="true"
-      :width="graphWidth"
-      :bar-color="colors.barColor"
-      :text-color="colors.textColor"
-      :text-alt-color="colors.textAltColor"
-      :height="100"
-      :label-height="20"
-    >
-      <template #label="opt">
-        <g v-for="(author, idx) in authors" :key="author.position">
-          <a
-            v-if="opt.bar.index === idx && author.home_link !== ''"
-            :href="author.home_link"
-          >
-            <image
-              :x="`${opt.bar.midPoint - 10}px`"
-              :y="`${opt.bar.yLabel}px`"
-              height="20"
-              width="20"
-              :href="author.avatar_link"
-            />
-          </a>
-          <image
-            v-else-if="opt.bar.index === idx"
-            :x="`${opt.bar.midPoint - 10}px`"
-            :y="`${opt.bar.yLabel}px`"
-            height="20"
-            width="20"
-            :href="author.avatar_link"
-          />
-        </g>
-      </template>
-      <template #title="opt">
-        <tspan v-for="(author, idx) in authors" :key="author.position">
-          <tspan v-if="opt.bar.index === idx">
-            {{ author.name }}
-          </tspan>
-        </tspan>
-      </template>
-    </vue-bar-graph>
-  </div>
-</template>
-<script>
-import VueBarGraph from 'vue-bar-graph';
-
-export default {
-  components: {VueBarGraph},
-  props: {
-    data: {type: Array, default: () => []},
-  },
-  data: () => ({
-    colors: {
-      barColor: 'green',
-      textColor: 'black',
-      textAltColor: 'white',
-    },
-  }),
-  computed: {
-    graphData() {
-      return this.data.map((item) => {
-        return {
-          value: item.commits,
-          label: item.name,
-        };
-      });
-    },
-    authors() {
-      return this.data.map((item, idx) => {
-        return {
-          position: idx + 1,
-          ...item,
-        };
-      });
-    },
-    graphWidth() {
-      return this.data.length * 40;
-    },
-  },
-  mounted() {
-    const st = window.getComputedStyle(this.$refs.style);
-    const stalt = 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;
-    },
-  }
-};
-</script>
diff --git a/web_src/js/components/DashboardRepoList.js b/web_src/js/components/DashboardRepoList.js
new file mode 100644 (file)
index 0000000..7ae62af
--- /dev/null
@@ -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/RepoActivityTopAuthors.vue b/web_src/js/components/RepoActivityTopAuthors.vue
new file mode 100644 (file)
index 0000000..d510695
--- /dev/null
@@ -0,0 +1,110 @@
+<template>
+  <div>
+    <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="graphPoints"
+      :show-x-axis="true"
+      :show-y-axis="false"
+      :show-values="true"
+      :width="graphWidth"
+      :bar-color="colors.barColor"
+      :text-color="colors.textColor"
+      :text-alt-color="colors.textAltColor"
+      :height="100"
+      :label-height="20"
+    >
+      <template #label="opt">
+        <g v-for="(author, idx) in graphAuthors" :key="author.position">
+          <a
+            v-if="opt.bar.index === idx && author.home_link"
+            :href="author.home_link"
+          >
+            <image
+              :x="`${opt.bar.midPoint - 10}px`"
+              :y="`${opt.bar.yLabel}px`"
+              height="20"
+              width="20"
+              :href="author.avatar_link"
+            />
+          </a>
+          <image
+            v-else-if="opt.bar.index === idx"
+            :x="`${opt.bar.midPoint - 10}px`"
+            :y="`${opt.bar.yLabel}px`"
+            height="20"
+            width="20"
+            :href="author.avatar_link"
+          />
+        </g>
+      </template>
+      <template #title="opt">
+        <tspan v-for="(author, idx) in graphAuthors" :key="author.position">
+          <tspan v-if="opt.bar.index === idx">
+            {{ author.name }}
+          </tspan>
+        </tspan>
+      </template>
+    </vue-bar-graph>
+  </div>
+</template>
+
+<script>
+import VueBarGraph from 'vue-bar-graph';
+import {initVueApp} from './VueComponentLoader.js';
+
+const sfc = {
+  components: {VueBarGraph},
+  data: () => ({
+    colors: {
+      barColor: 'green',
+      textColor: 'black',
+      textAltColor: 'white',
+    },
+
+    // possible keys:
+    // * avatar_link: (...)
+    // * commits: (...)
+    // * home_link: (...)
+    // * login: (...)
+    // * name: (...)
+    activityTopAuthors: window.config.pageData.repoActivityTopAuthors || [],
+  }),
+  computed: {
+    graphPoints() {
+      return this.activityTopAuthors.map((item) => {
+        return {
+          value: item.commits,
+          label: item.name,
+        };
+      });
+    },
+    graphAuthors() {
+      return this.activityTopAuthors.map((item, idx) => {
+        return {
+          position: idx + 1,
+          ...item,
+        };
+      });
+    },
+    graphWidth() {
+      return this.activityTopAuthors.length * 40;
+    },
+  },
+  mounted() {
+    const refStyle = window.getComputedStyle(this.$refs.style);
+    const refAltStyle = window.getComputedStyle(this.$refs.altStyle);
+
+    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 (file)
index 0000000..a0be57a
--- /dev/null
@@ -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 (file)
index 0000000..6b2a2cb
--- /dev/null
@@ -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};
index b01c66afe265ebe080d90930fd182e89c316db65..5863c5480f8fc0b9534fccf991cf6e09f36e0f20 100644 (file)
@@ -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');
index bc9725da9545fe56a8b0cdb10bad4b41b6757a1c..e6269c8abfe6381d1afa0b0064a78a2ea9b7ecb8 100644 (file)
@@ -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();
index 1e572ffa7ed7d05c2006569750defa37fb5e17d8..3d7d40ffa3427eebd66c5dfd63ba122df8dbc9bf 100644 (file)
             opacity: var(--opacity-disabled);
             cursor: default;
           }
-
-          #delete-file-form {
-            display: inline-block;
-          }
         }
       }