11<script lang="ts">
2- import {nextTick , defineComponent } from ' vue' ;
2+ import {nextTick , defineComponent , computed , ref , type Ref } from ' vue' ;
33import {SvgIcon } from ' ../svg.ts' ;
44import {GET } from ' ../modules/fetch.ts' ;
55import {fomanticQuery } from ' ../modules/fomantic/base.ts' ;
6+ import DashboardRepoGroup from ' ./DashboardRepoGroup.vue' ;
67
78const {appSubUrl, assetUrlPrefix, pageData} = window .config ;
89
9- type CommitStatus = ' pending' | ' success' | ' error' | ' failure' | ' warning' | ' skipped' ;
10+ export type CommitStatus = ' pending' | ' success' | ' error' | ' failure' | ' warning' | ' skipped' ;
1011
11- type CommitStatusMap = {
12- [status in CommitStatus ]: {
12+ export type CommitStatusMap = {
13+ [ status in CommitStatus ]: {
1314 name: string ,
1415 color: string ,
1516 };
1617};
1718
1819// make sure this matches templates/repo/commit_status.tmpl
19- const commitStatus: CommitStatusMap = {
20+ export const commitStatus: CommitStatusMap = {
2021 pending: {name: ' octicon-dot-fill' , color: ' yellow' },
2122 success: {name: ' octicon-check' , color: ' green' },
2223 error: {name: ' gitea-exclamation' , color: ' red' },
2324 failure: {name: ' octicon-x' , color: ' red' },
2425 warning: {name: ' gitea-exclamation' , color: ' yellow' },
2526 skipped: {name: ' octicon-skip' , color: ' grey' },
2627};
27-
28+ export type GroupMapType = {
29+ repos: any []
30+ subgroups: number []
31+ id: number
32+ [ k : string ]: any
33+ }
2834export default defineComponent ({
29- components: {SvgIcon },
35+ components: {SvgIcon , DashboardRepoGroup },
36+ provide() {
37+ return {
38+ expandedGroups: computed ({
39+ get : () => {
40+ return this .expandedGroups ;
41+ },
42+ set : (v ) => {
43+ this .expandedGroups = v ;
44+ },
45+ }),
46+ searchURL: this .searchURL ,
47+ groups: computed ({
48+ get : () => {
49+ return this .groups ;
50+ },
51+ set : (v ) => {
52+ this .groups = v ;
53+ },
54+ }),
55+ repos: computed (() => this .computedRepos ),
56+ loadedMap: computed ({
57+ get : () => {
58+ return this .loadedMap ;
59+ },
60+ set : (v ) => {
61+ this .loadedMap = v ;
62+ },
63+ }),
64+ orgName: this .organizationName ,
65+ };
66+ },
67+ setup() {
68+ const groups = ref (new Map <number , GroupMapType >());
69+ const loadedMap = ref (new Map <number , boolean >([[0 , true ]]));
70+ return {groupsRef: groups , loadedRef: loadedMap };
71+ },
3072 data() {
3173 const params = new URLSearchParams (window .location .search );
3274 const tab = params .get (' repo-search-tab' ) || ' repos' ;
@@ -39,6 +81,7 @@ export default defineComponent({
3981 return {
4082 tab ,
4183 repos: [],
84+ groupData: this .groupsRef ,
4285 reposTotalCount: null ,
4386 reposFilter ,
4487 archivedFilter ,
@@ -78,15 +121,15 @@ export default defineComponent({
78121 subUrl: appSubUrl ,
79122 ... pageData .dashboardRepoList ,
80123 activeIndex: - 1 , // don't select anything at load, first cursor down will select
124+ expandedGroupsRaw: [],
81125 };
82126 },
83-
84127 computed: {
85128 showMoreReposLink() {
86129 return this .repos .length > 0 && this .repos .length < this .counts [` ${this .reposFilter }:${this .archivedFilter }:${this .privateFilter } ` ];
87130 },
88131 searchURL() {
89- return ` ${this .subUrl }/repo /search?sort=updated&order=desc&uid=${this .uid }&team_id=${this .teamId }&q=${this .searchQuery
132+ return ` ${this .subUrl }/group /search?sort=updated&order=desc&uid=${this .uid }&team_id=${this .teamId }&q=${this .searchQuery
90133 }&page=${this .page }&limit=${this .searchLimit }&mode=${this .repoTypes [this .reposFilter ].searchMode
91134 }${this .archivedFilter === ' archived' ? ' &archived=true' : ' ' }${this .archivedFilter === ' unarchived' ? ' &archived=false' : ' '
92135 }${this .privateFilter === ' private' ? ' &is_private=true' : ' ' }${this .privateFilter === ' public' ? ' &is_private=false' : ' '
@@ -107,6 +150,38 @@ export default defineComponent({
107150 checkboxPrivateFilterProps() {
108151 return {checked: this .privateFilter === ' private' , indeterminate: this .privateFilter === ' both' };
109152 },
153+ expandedGroups: {
154+ get() {
155+ return this .expandedGroupsRaw ;
156+ },
157+ set(val : number []) {
158+ this .expandedGroupsRaw = val ;
159+ },
160+ },
161+ groups: {
162+ get() {
163+ return this .groupData ;
164+ },
165+ set(v : Map <number , GroupMapType >) {
166+ for (const [k, val] of v ) {
167+ this .groupData .set (k , val );
168+ }
169+ },
170+ },
171+ computedRepos() {
172+ return this .repos ;
173+ },
174+ root() {
175+ return [... (this .groups .get (0 )?.subgroups ?? []), ... this .repos ];
176+ },
177+ loadedMap: {
178+ get() {
179+ return this .loadedRef ;
180+ },
181+ set(v : Ref <Map <number , boolean >>) {
182+ this .loadedRef = v ;
183+ },
184+ },
110185 },
111186
112187 mounted() {
@@ -136,6 +211,9 @@ export default defineComponent({
136211 changeReposFilter(filter : string ) {
137212 this .reposFilter = filter ;
138213 this .repos = [];
214+ this .groups = new Map ();
215+ this .loadedMap = new Map ();
216+ this .expandedGroupsRaw = [];
139217 this .page = 1 ;
140218 this .counts [` ${filter }:${this .archivedFilter }:${this .privateFilter } ` ] = 0 ;
141219 this .searchRepos ();
@@ -212,6 +290,8 @@ export default defineComponent({
212290 }
213291 this .page = 1 ;
214292 this .repos = [];
293+ this .groups = new Map ();
294+ this .loadedMap = new Map ();
215295 this .counts [` ${this .reposFilter }:${this .archivedFilter }:${this .privateFilter } ` ] = 0 ;
216296 this .searchRepos ();
217297 },
@@ -227,6 +307,8 @@ export default defineComponent({
227307 this .page = 1 ;
228308 }
229309 this .repos = [];
310+ this .groups = new Map ();
311+ this .loadedMap = new Map ();
230312 this .counts [` ${this .reposFilter }:${this .archivedFilter }:${this .privateFilter } ` ] = 0 ;
231313 await this .searchRepos ();
232314 },
@@ -235,7 +317,7 @@ export default defineComponent({
235317 this .isLoading = true ;
236318
237319 const searchedMode = this .repoTypes [this .reposFilter ].searchMode ;
238- const searchedURL = this .searchURL ;
320+ const searchedURL = ` ${ this .searchURL }&group_id=-1 ` ;
239321 const searchedQuery = this .searchQuery ;
240322
241323 let response, json;
@@ -257,22 +339,40 @@ export default defineComponent({
257339 response = await GET (searchedURL );
258340 json = await response .json ();
259341 } catch {
260- if (searchedURL === this .searchURL ) {
342+ if (searchedURL . startsWith ( this .searchURL ) ) {
261343 this .isLoading = false ;
262344 }
263345 return ;
264346 }
265347
266- if (searchedURL === this .searchURL ) {
267- this .repos = json .data .map ((webSearchRepo : any ) => {
348+ if (searchedURL . startsWith ( this .searchURL ) ) {
349+ this .repos = json .data .repos . map ((webSearchRepo : any ) => {
268350 return {
269351 ... webSearchRepo .repository ,
270352 latest_commit_status_state: webSearchRepo .latest_commit_status ?.State , // if latest_commit_status is null, it means there is no commit status
271353 latest_commit_status_state_link: webSearchRepo .latest_commit_status ?.TargetURL ,
272354 locale_latest_commit_status_state: webSearchRepo .locale_latest_commit_status ,
273355 };
274356 });
275- const count = Number (response .headers .get (' X-Total-Count' ));
357+ this .groups .set (0 , {
358+ repos: this .repos .filter ((a : any ) => ! a .group_id ),
359+ subgroups: json .data .subgroups .map ((g : any ) => {
360+ return g .group .id ;
361+ }),
362+ data: {},
363+ });
364+ for (const g of json .data .subgroups ) {
365+ this .groups .set (g .group .id , {
366+ subgroups: g .subgroups .map ((h : any ) => h .group .id ),
367+ repos: g .repos ,
368+ ... g .group ,
369+ latest_commit_status_state: g .latest_commit_status ?.State , // if latest_commit_status is null, it means there is no commit status
370+ latest_commit_status_state_link: g .latest_commit_status ?.TargetURL ,
371+ locale_latest_commit_status_state: g .locale_latest_commit_status ,
372+ id: g .group .id ,
373+ });
374+ }
375+ const count = this .repos .length ;
276376 if (searchedQuery === ' ' && searchedMode === ' ' && this .archivedFilter === ' both' ) {
277377 this .reposTotalCount = count ;
278378 }
@@ -372,7 +472,7 @@ export default defineComponent({
372472 <div v-else class =" ui attached segment repos-search" >
373473 <div class =" ui small fluid action left icon input" >
374474 <input type =" search" spellcheck =" false" maxlength =" 255" @input =" changeReposFilter(reposFilter)" v-model =" searchQuery" ref =" search" @keydown =" reposFilterKeyControl" :placeholder =" textSearchRepos" >
375- <i class =" icon loading-icon-3px" :class =" {'is-loading': isLoading}" ><svg-icon name =" octicon-search" :size =" 16" /></i >
475+ <i class =" icon loading-icon-3px" :class =" { 'is-loading': isLoading }" ><svg-icon name =" octicon-search" :size =" 16" /></i >
376476 <div class =" ui dropdown icon button" :title =" textFilter" >
377477 <svg-icon name =" octicon-filter" :size =" 16" />
378478 <div class =" menu" >
@@ -401,69 +501,55 @@ export default defineComponent({
401501 </div >
402502 <overflow-menu class =" ui secondary pointing tabular borderless menu repos-filter" >
403503 <div class =" overflow-menu-items tw-justify-center" >
404- <a class =" item" tabindex =" 0" :class =" {active: reposFilter === 'all'}" @click =" changeReposFilter('all')" >
504+ <a class =" item" tabindex =" 0" :class =" { active: reposFilter === 'all' }" @click =" changeReposFilter('all')" >
405505 {{ textAll }}
406506 <div v-show =" reposFilter === 'all'" class =" ui circular mini grey label" >{{ repoTypeCount }}</div >
407507 </a >
408- <a class =" item" tabindex =" 0" :class =" {active: reposFilter === 'sources'}" @click =" changeReposFilter('sources')" >
508+ <a class =" item" tabindex =" 0" :class =" { active: reposFilter === 'sources' }" @click =" changeReposFilter('sources')" >
409509 {{ textSources }}
410510 <div v-show =" reposFilter === 'sources'" class =" ui circular mini grey label" >{{ repoTypeCount }}</div >
411511 </a >
412- <a class =" item" tabindex =" 0" :class =" {active: reposFilter === 'forks'}" @click =" changeReposFilter('forks')" >
512+ <a class =" item" tabindex =" 0" :class =" { active: reposFilter === 'forks' }" @click =" changeReposFilter('forks')" >
413513 {{ textForks }}
414514 <div v-show =" reposFilter === 'forks'" class =" ui circular mini grey label" >{{ repoTypeCount }}</div >
415515 </a >
416- <a class =" item" tabindex =" 0" :class =" {active: reposFilter === 'mirrors'}" @click =" changeReposFilter('mirrors')" v-if =" isMirrorsEnabled" >
516+ <a class =" item" tabindex =" 0" :class =" { active: reposFilter === 'mirrors' }" @click =" changeReposFilter('mirrors')" v-if =" isMirrorsEnabled" >
417517 {{ textMirrors }}
418518 <div v-show =" reposFilter === 'mirrors'" class =" ui circular mini grey label" >{{ repoTypeCount }}</div >
419519 </a >
420- <a class =" item" tabindex =" 0" :class =" {active: reposFilter === 'collaborative'}" @click =" changeReposFilter('collaborative')" >
520+ <a class =" item" tabindex =" 0" :class =" { active: reposFilter === 'collaborative' }" @click =" changeReposFilter('collaborative')" >
421521 {{ textCollaborative }}
422522 <div v-show =" reposFilter === 'collaborative'" class =" ui circular mini grey label" >{{ repoTypeCount }}</div >
423523 </a >
424524 </div >
425525 </overflow-menu >
426526 </div >
427527 <div v-if =" repos.length" class =" ui attached table segment tw-rounded-b" >
428- <ul class =" repo-owner-name-list" >
429- <li class =" tw-flex tw-items-center tw-py-2" v-for =" (repo, index) in repos" :class =" {'active': index === activeIndex}" :key =" repo.id" >
430- <a class =" repo-list-link muted" :href =" repo.link" >
431- <svg-icon :name =" repoIcon(repo)" :size =" 16" class =" repo-list-icon" />
432- <div class =" text truncate" >{{ repo.full_name }}</div >
433- <div v-if =" repo.archived" >
434- <svg-icon name =" octicon-archive" :size =" 16" />
435- </div >
436- </a >
437- <a class =" tw-flex tw-items-center" v-if =" repo.latest_commit_status_state" :href =" repo.latest_commit_status_state_link || null" :data-tooltip-content =" repo.locale_latest_commit_status_state" >
438- <!-- the commit status icon logic is taken from templates/repo/commit_status.tmpl -->
439- <svg-icon :name =" statusIcon(repo.latest_commit_status_state)" :class =" 'tw-ml-2 commit-status icon text ' + statusColor(repo.latest_commit_status_state)" :size =" 16" />
440- </a >
441- </li >
442- </ul >
528+ <dashboard-repo-group :items =" root" :depth =" 1" :cur-group =" 0" @load-changed =" (nv: boolean) => (isLoading = nv)" />
443529 <div v-if =" showMoreReposLink" class =" tw-text-center" >
444530 <div class =" divider tw-my-0" />
445531 <div class =" ui borderless pagination menu narrow tw-my-2" >
446532 <a
447- class =" item navigation tw-py-1" :class =" {'disabled': page === 1}"
533+ class =" item navigation tw-py-1" :class =" { 'disabled': page === 1 }"
448534 @click =" changePage(1)" :title =" textFirstPage"
449535 >
450536 <svg-icon name =" gitea-double-chevron-left" :size =" 16" class =" tw-mr-1" />
451537 </a >
452538 <a
453- class =" item navigation tw-py-1" :class =" {'disabled': page === 1}"
539+ class =" item navigation tw-py-1" :class =" { 'disabled': page === 1 }"
454540 @click =" changePage(page - 1)" :title =" textPreviousPage"
455541 >
456542 <svg-icon name =" octicon-chevron-left" :size =" 16" class =" tw-mr-1" />
457543 </a >
458544 <a class =" active item tw-py-1" >{{ page }}</a >
459545 <a
460- class =" item navigation" :class =" {'disabled': page === finalPage}"
546+ class =" item navigation" :class =" { 'disabled': page === finalPage }"
461547 @click =" changePage(page + 1)" :title =" textNextPage"
462548 >
463549 <svg-icon name =" octicon-chevron-right" :size =" 16" class =" tw-ml-1" />
464550 </a >
465551 <a
466- class =" item navigation tw-py-1" :class =" {'disabled': page === finalPage}"
552+ class =" item navigation tw-py-1" :class =" { 'disabled': page === finalPage }"
467553 @click =" changePage(finalPage)" :title =" textLastPage"
468554 >
469555 <svg-icon name =" gitea-double-chevron-right" :size =" 16" class =" tw-ml-1" />
@@ -496,7 +582,7 @@ export default defineComponent({
496582 <div class =" text truncate" >{{ org.full_name ? `${org.full_name} (${org.name})` : org.name }}</div >
497583 <div ><!-- div to prevent underline of label on hover -->
498584 <span class =" ui tiny basic label" v-if =" org.org_visibility !== 'public'" >
499- {{ org.org_visibility === 'limited' ? textOrgVisibilityLimited: textOrgVisibilityPrivate }}
585+ {{ org.org_visibility === 'limited' ? textOrgVisibilityLimited : textOrgVisibilityPrivate }}
500586 </span >
501587 </div >
502588 </a >
0 commit comments