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
@@ -1769,12 +1769,24 @@
"name": "private",
"in": "query"
},
+ {
+ "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\"",
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}}
-
+
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();
},
- showRepo(repo, filter) {
- switch (filter) {
+ 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;
+ }
+ },
+
+ 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%;