]> source.dussan.org Git - gitea.git/commitdiff
frontend refactor
authorwxiaoguang <wxiaoguang@gmail.com>
Wed, 13 Oct 2021 17:40:37 +0000 (01:40 +0800)
committerwxiaoguang <wxiaoguang@gmail.com>
Wed, 13 Oct 2021 17:53:11 +0000 (01:53 +0800)
19 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/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/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..6bb8d12
--- /dev/null
@@ -0,0 +1,53 @@
+---
+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 Text Template](https://pkg.go.dev/text/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)
+
+Guidelines specialized for Gitea:
+
+1. Every feature (Fomantic-UI/jQuery module) should be put in separated files/directories.
+2. HTML id/css-class-name should use kebab-case.
+3. HTML id/css-class-name used by JavaScript top-level selector should be unique in whole project,
+   and should contain 2-3 feature related keywords. Recommend to use `js-` prefix for CSS names for JavaScript usage only.
+4. jQuery events across different features should use their own namespaces.
+5. CSS styles provided by frameworks should not be overwritten by framework's selectors globally.
+   Always use new defined CSS names to overwrite framework styles. Recommend to use `us-` prefix for user defined styles.  
+6. Backend can pass data to frontend (JavaScript) by `ctx.PageData["myModuleData"] = map{}`
+7. Simple pages and SEO-related pages use Go Text Template render to generate static Fomantic-UI HTML output. Complex pages can use Vue2 (or Vue3 in future).
+
+## Legacy Problems and Solutions
+
+### Too many codes in `web_src/index.js`
+
+In history, many JavaScript codes are written into `web_src/index.js` directly, which becomes too big to maintain.
+We should split this file into small modules, the separated files can be put in `web_src/js/features` for the first step.
+
+### Vue2/Vue3 and JSX
+
+Gitea is using Vue2 now, we have plan to upgrade to Vue3. We decide not to introduce JSX now to make sure the HTML and JavaScript codes are not mixed together.
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 2076ef82ab8e2a05a155e16e4ff7b31229cb692b..b733eadad0e290f5c6f348ef12f8af22826b0ab7 100644 (file)
@@ -645,6 +645,7 @@ 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
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..f11952fb12e1f140d06da5b809df7a1163322c8f 100644 (file)
@@ -1,6 +1,5 @@
 <!DOCTYPE html>
 <html lang="{{.Lang}}" class="theme-{{.SignedUser.Theme}}">
-<head data-suburl="{{AppSubUrl}}">
        <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}}">
@@ -31,6 +21,7 @@
                        AppVer: '{{AppVer}}',
                        AppSubUrl: '{{AppSubUrl}}',
                        AssetUrlPrefix: '{{AssetUrlPrefix}}',
+                       IsProd: {{.IsProd}},
                        CustomEmojis: {{CustomEmojis}},
                        UseServiceWorker: {{UseServiceWorker}},
                        csrf: '{{.CsrfToken}}',
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 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..559fa1d
--- /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..a5975f4
--- /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 71e5691179a2037d888c51862c1b98cf497ad995..cdfdc72afb9030cea975a843e610e1c84f51457d 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
@@ -2852,7 +2851,8 @@ $(document).ready(async () => {
   initWebhook();
   initAdmin();
   initCodeView();
-  initVueApp();
+  initRepoActivityTopAuthorsChart();
+  initDashboardRepoList();
   initTeamSettings();
   initCtrlEnterSubmit();
   initNavbarContentToggle();
@@ -3099,369 +3099,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)) {
@@ -3470,31 +3107,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({
@@ -3537,163 +3149,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;
-          }
         }
       }