diff options
-rw-r--r-- | models/repo_list.go | 37 | ||||
-rw-r--r-- | options/locale/locale_en-US.ini | 11 | ||||
-rw-r--r-- | routers/api/v1/repo/repo.go | 13 | ||||
-rw-r--r-- | templates/swagger/v1_json.tmpl | 12 | ||||
-rw-r--r-- | templates/user/dashboard/repolist.tmpl | 63 | ||||
-rw-r--r-- | web_src/js/index.js | 251 | ||||
-rw-r--r-- | web_src/less/_base.less | 13 | ||||
-rw-r--r-- | web_src/less/_dashboard.less | 5 |
8 files changed, 371 insertions, 34 deletions
diff --git a/models/repo_list.go b/models/repo_list.go index 1632e64eb9..a676ae5c46 100644 --- a/models/repo_list.go +++ b/models/repo_list.go @@ -140,6 +140,7 @@ type SearchRepoOptions struct { PriorityOwnerID int64 OrderBy SearchOrderBy Private bool // Include private repositories in results + OnlyPrivate bool // Include only private repositories in results StarredByID int64 AllPublic bool // Include also all public repositories of users and public organisations AllLimited bool // Include also all public repositories of limited organisations @@ -159,6 +160,10 @@ type SearchRepoOptions struct { // True -> include just mirrors // False -> include just non-mirrors Mirror util.OptionalBool + // None -> include archived AND non-archived + // True -> include just archived + // False -> include just non-archived + Archived util.OptionalBool // only search topic name TopicOnly bool // include description in keyword search @@ -205,14 +210,26 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { } } else { // Not looking at private organisations - // We should be able to see all non-private repositories that either: - cond = cond.And(builder.Eq{"is_private": false}) - accessCond := builder.Or( - // A. Aren't in organisations __OR__ - builder.NotIn("owner_id", builder.Select("id").From("`user`").Where(builder.Eq{"type": UserTypeOrganization})), - // B. Isn't a private or limited organisation. - builder.NotIn("owner_id", builder.Select("id").From("`user`").Where(builder.Or(builder.Eq{"visibility": structs.VisibleTypeLimited}, builder.Eq{"visibility": structs.VisibleTypePrivate})))) - cond = cond.And(accessCond) + // We should be able to see all non-private repositories that + // isn't in a private or limited organisation. + cond = cond.And( + builder.Eq{"is_private": false}, + builder.NotIn("owner_id", builder.Select("id").From("`user`").Where( + builder.And( + builder.Eq{"type": UserTypeOrganization}, + builder.Or(builder.Eq{"visibility": structs.VisibleTypeLimited}, builder.Eq{"visibility": structs.VisibleTypePrivate}), + )))) + } + + if opts.OnlyPrivate { + cond = cond.And( + builder.Or( + builder.Eq{"is_private": true}, + builder.In("owner_id", builder.Select("id").From("`user`").Where( + builder.And( + builder.Eq{"type": UserTypeOrganization}, + builder.Or(builder.Eq{"visibility": structs.VisibleTypeLimited}, builder.Eq{"visibility": structs.VisibleTypePrivate}), + ))))) } if opts.Template != util.OptionalBoolNone { @@ -299,6 +316,10 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { cond = cond.And(accessibleRepositoryCondition(opts.Actor)) } + if opts.Archived != util.OptionalBoolNone { + cond = cond.And(builder.Eq{"is_archived": opts.Archived == util.OptionalBoolTrue}) + } + switch opts.HasMilestones { case util.OptionalBoolTrue: cond = cond.And(builder.Gt{"num_milestones": 0}) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index e9e185690a..232315122c 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -206,6 +206,17 @@ my_orgs = My Organizations my_mirrors = My Mirrors view_home = View %s search_repos = Find a repository… +filter = Other Filters + +show_archived = Archived +show_both_archived_unarchived = Showing both archived and unarchived +show_only_archived = Showing only archived +show_only_unarchived = Showing only unarchived + +show_private = Private +show_both_private_public = Showing both public and private +show_only_private = Showing only private +show_only_public = Showing only public issues.in_your_repos = In your repositories diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 5eeef9fb9d..a724ebcc37 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -78,10 +78,18 @@ func Search(ctx *context.APIContext) { // in: query // description: include private repositories this user has access to (defaults to true) // type: boolean + // - name: onlyPrivate + // in: query + // description: only include private repositories this user has access to (defaults to false) + // type: boolean // - name: template // in: query // description: include template repositories this user has access to (defaults to true) // type: boolean + // - name: archived + // in: query + // description: show only archived, non-archived or all repositories (defaults to all) + // type: boolean // - name: mode // in: query // description: type of repository to search for. Supported values are @@ -125,6 +133,7 @@ func Search(ctx *context.APIContext) { TopicOnly: ctx.QueryBool("topic"), Collaborate: util.OptionalBoolNone, Private: ctx.IsSigned && (ctx.Query("private") == "" || ctx.QueryBool("private")), + OnlyPrivate: ctx.IsSigned && ctx.QueryBool("onlyPrivate"), Template: util.OptionalBoolNone, StarredByID: ctx.QueryInt64("starredBy"), IncludeDescription: ctx.QueryBool("includeDesc"), @@ -156,6 +165,10 @@ func Search(ctx *context.APIContext) { return } + if ctx.Query("archived") != "" { + opts.Archived = util.OptionalBoolOf(ctx.QueryBool("archived")) + } + var sortMode = ctx.Query("sort") if len(sortMode) > 0 { var sortOrder = ctx.Query("order") diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 66a07419b1..a6b24ed970 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -1771,11 +1771,23 @@ }, { "type": "boolean", + "description": "only include private repositories this user has access to (defaults to false)", + "name": "onlyPrivate", + "in": "query" + }, + { + "type": "boolean", "description": "include template repositories this user has access to (defaults to true)", "name": "template", "in": "query" }, { + "type": "boolean", + "description": "show only archived, non-archived or all repositories (defaults to all)", + "name": "archived", + "in": "query" + }, + { "type": "string", "description": "type of repository to search for. Supported values are \"fork\", \"source\", \"mirror\" and \"collaborative\"", "name": "mode", diff --git a/templates/user/dashboard/repolist.tmpl b/templates/user/dashboard/repolist.tmpl index 63019e58e8..dc1507403c 100644 --- a/templates/user/dashboard/repolist.tmpl +++ b/templates/user/dashboard/repolist.tmpl @@ -35,9 +35,46 @@ {{end}} </h4> <div class="ui attached secondary segment repos-search"> - <div class="ui fluid icon input" :class="{loading: isLoading}"> + <div class="ui fluid right action left icon input" :class="{loading: isLoading}"> <input @input="searchRepos(reposFilter)" v-model="searchQuery" ref="search" placeholder="{{.i18n.Tr "home.search_repos"}}"> <i class="search icon"></i> + <div class="ui dropdown button" title="{{.i18n.Tr "home.filter"}}"> + <i class="icon filter"></i> + <div class="menu"> + <div class="item"> + <a @click="toggleArchivedFilter()"> + <div class="ui checkbox" id="archivedFilterCheckbox" title="{{.i18n.Tr "home.show_both_archived_unarchived"}}" v-if="archivedFilter === 'both'"> + <input type="checkbox"> + <label><i class="archive icon archived-icon"></i>{{.i18n.Tr "home.show_archived"}}</label> + </div> + <div class="ui checkbox" id="archivedFilterCheckbox" title="{{.i18n.Tr "home.show_only_unarchived"}}" v-if="archivedFilter === 'unarchived'"> + <input type="checkbox"> + <label><i class="archive icon archived-icon"></i>{{.i18n.Tr "home.show_archived"}}</label> + </div> + <div class="ui checkbox" id="archivedFilterCheckbox" title="{{.i18n.Tr "home.show_only_archived"}}" v-if="archivedFilter === 'archived'"> + <input type="checkbox"> + <label><i class="archive icon archived-icon"></i>{{.i18n.Tr "home.show_archived"}}</label> + </div> + </a> + </div> + <div class="item"> + <a @click="togglePrivateFilter()"> + <div class="ui checkbox" id="privateFilterCheckbox" title="{{.i18n.Tr "home.show_both_private_public"}}" v-if="privateFilter === 'both'"> + <input type="checkbox"> + <label><svg class="svg octicon-lock" width="16" height="16" aria-hidden="true"><use xlink:href="#octicon-lock" /></svg>{{.i18n.Tr "home.show_private"}}</label> + </div> + <div class="ui checkbox" id="privateFilterCheckbox" title="{{.i18n.Tr "home.show_only_public"}}" v-if="privateFilter === 'public'"> + <input type="checkbox"> + <label><svg class="svg octicon-lock" width="16" height="16" aria-hidden="true"><use xlink:href="#octicon-lock" /></svg>{{.i18n.Tr "home.show_private"}}</label> + </div> + <div class="ui checkbox" id="privateFilterCheckbox" title="{{.i18n.Tr "home.show_only_private"}}" v-if="privateFilter === 'private'"> + <input type="checkbox"> + <label><svg class="svg octicon-lock" width="16" height="16" aria-hidden="true"><use xlink:href="#octicon-lock" /></svg>{{.i18n.Tr "home.show_private"}}</label> + </div> + </a> + </div> + </div> + </div> </div> <div class="ui secondary tiny pointing borderless menu center aligned grid repos-filter"> <a class="item" :class="{active: reposFilter === 'all'}" @click="changeReposFilter('all')"> @@ -64,7 +101,7 @@ </div> <div class="ui attached table segment"> <ul class="repo-owner-name-list"> - <li v-for="repo in repos" :class="{'private': repo.private}" v-show="showRepo(repo, reposFilter)"> + <li v-for="repo in repos" :class="{'private': repo.private}" v-show="showRepo(repo)"> <a :href="suburl + '/' + repo.full_name"> <svg :class="'svg ' + repoClass(repo)" width="16" height="16" aria-hidden="true"><use :xlink:href="'#' + repoClass(repo)" /></svg> <strong class="text truncate item-name">${repo.full_name}</strong> @@ -75,7 +112,27 @@ </a> </li> <li v-if="showMoreReposLink"> - <a :href="moreReposLink">{{.i18n.Tr "home.show_more_repos"}}</a> + <div class="center"> + <div class="ui borderless pagination menu narrow"> + <a class="item navigation" :class="{'disabled': page === 1}" + @click="changePage(1)" title="{{$.i18n.Tr "admin.first_page"}}"> + <i class="angle double left icon"></i> + </a> + <a class="item navigation" :class="{'disabled': page === 1}" + @click="changePage(page - 1)" title="{{$.i18n.Tr "repo.issues.previous"}}"> + <i class="left arrow icon"></i> + </a> + <a class="active item">${page}</a> + <a class="item navigation" :class="{'disabled': page === finalPage}" + @click="changePage(page + 1)" title="{{$.i18n.Tr "repo.issues.next"}}"> + <i class="icon right arrow"></i> + </a> + <a class="item navigation" :class="{'disabled': page === finalPage}" + @click="changePage(finalPage)" title="{{$.i18n.Tr "admin.last_page"}}"> + <i class="angle double right icon"></i> + </a> + </div> + </div> </li> </ul> </div> diff --git a/web_src/js/index.js b/web_src/js/index.js index 4cb8cff443..0a7bc0c4cd 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -2662,33 +2662,70 @@ function initVueComponents() { }, 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 = 'both'; + } + + 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', + tab, repos: [], reposTotalCount: 0, - reposFilter: 'all', - searchQuery: '', + reposFilter, + archivedFilter, + privateFilter, + page, + finalPage: 1, + searchQuery, isLoading: false, staticPrefix: StaticUrlPrefix, + counts: {}, repoTypes: { all: { - count: 0, searchMode: '', }, forks: { - count: 0, searchMode: 'fork', }, mirrors: { - count: 0, searchMode: 'mirror', }, sources: { - count: 0, searchMode: 'source', }, collaborative: { - count: 0, searchMode: 'collaborative', }, } @@ -2697,21 +2734,26 @@ function initVueComponents() { computed: { showMoreReposLink() { - return this.repos.length > 0 && this.repos.length < this.repoTypes[this.reposFilter].count; + 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}&q=${this.searchQuery - }&limit=${this.searchLimit}&mode=${this.repoTypes[this.reposFilter].searchMode - }${this.reposFilter !== 'all' ? '&exclusive=1' : ''}`; + }&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' ? '&onlyPrivate=true' : ''}${this.privateFilter === 'public' ? '&private=false' : '' + }`; }, repoTypeCount() { - return this.repoTypes[this.reposFilter].count; + return this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`]; } }, mounted() { this.searchRepos(this.reposFilter); - + $(this.$el).find('.poping.up').popup(); + $(this.$el).find('.dropdown').dropdown(); + this.setCheckboxes(); const self = this; Vue.nextTick(() => { self.$refs.search.focus(); @@ -2721,17 +2763,178 @@ function initVueComponents() { 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 = 'both'; + $('#archivedFilterCheckbox').checkbox('set indeterminate'); + 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.repoTypes[filter].count = 0; - this.searchRepos(filter); + 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 === 'both') { + 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}`); + } + + window.history.replaceState({}, '', `?${params.toString()}`); + }, + + 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 = 'both'; + 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(); + }, + + showArchivedRepo(repo) { + switch (this.archivedFilter) { + case 'both': + return true; + case 'unarchived': + return !repo.archived; + case 'archived': + return repo.archived; + default: + return true; + } }, - showRepo(repo, filter) { - switch (filter) { + showPrivateRepo(repo) { + switch (this.privateFilter) { + case 'both': + return true; + case 'public': + return !repo.private; + case 'private': + return repo.private; + default: + return true; + } + }, + + showFilteredRepo(repo) { + switch (this.reposFilter) { case 'sources': return repo.owner.id === this.uid && !repo.mirror && !repo.fork; case 'forks': @@ -2745,12 +2948,16 @@ function initVueComponents() { } }, - searchRepos(reposFilter) { + showRepo(repo) { + return this.showArchivedRepo(repo) && this.showPrivateRepo(repo) && this.showFilteredRepo(repo); + }, + + searchRepos() { const self = this; this.isLoading = true; - const searchedMode = this.repoTypes[reposFilter].searchMode; + const searchedMode = this.repoTypes[this.reposFilter].searchMode; const searchedURL = this.searchURL; const searchedQuery = this.searchQuery; @@ -2758,10 +2965,12 @@ function initVueComponents() { if (searchedURL === self.searchURL) { self.repos = result.data; const count = request.getResponseHeader('X-Total-Count'); - if (searchedQuery === '' && searchedMode === '') { + if (searchedQuery === '' && searchedMode === '' && self.archivedFilter === 'both') { self.reposTotalCount = count; } - self.repoTypes[reposFilter].count = count; + Vue.set(self.counts, `${self.reposFilter}:${self.archivedFilter}:${self.privateFilter}`, count); + self.finalPage = Math.floor(count / self.searchLimit) + 1; + self.updateHistory(); } }).always(() => { if (searchedURL === self.searchURL) { diff --git a/web_src/less/_base.less b/web_src/less/_base.less index 97de03c031..de8655bd20 100644 --- a/web_src/less/_base.less +++ b/web_src/less/_base.less @@ -318,11 +318,11 @@ code, } .ui { - &.left { + &.left:not(.action) { float: left; } - &.right { + &.right:not(.action) { float: right; } @@ -727,6 +727,15 @@ code, display: none; } } + &.narrow .item { + padding-left: 8px; + padding-right: 8px; + min-width: 1em; + text-align: center; + .icon { + margin-right: 0; + } + } } } diff --git a/web_src/less/_dashboard.less b/web_src/less/_dashboard.less index 1a578d3fb0..fbc40980af 100644 --- a/web_src/less/_dashboard.less +++ b/web_src/less/_dashboard.less @@ -174,6 +174,11 @@ } } + #privateFilterCheckbox .svg { + color: #888888; + margin-right: .25rem; + } + .repo-owner-name-list { .item-name { max-width: 70%; |