0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-04-04 14:36:16 +02:00

modify DashboardRepoList component to show DashboardRepoGroup instead of flat repo list

This commit is contained in:
☙◦ The Tablet ❀ GamerGirlandCo ◦❧ 2025-08-16 16:47:26 -04:00
parent 8f653da71f
commit 99e9d32d93
No known key found for this signature in database
GPG Key ID: 924A5F6AF051E87C

View File

@ -1,9 +1,10 @@
<script lang="ts">
import {nextTick, defineComponent} from 'vue';
import {nextTick, defineComponent, computed, ref, type Ref} from 'vue';
import {SvgIcon} from '../svg.ts';
import {GET} from '../modules/fetch.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
import type {SvgName} from '../svg.ts';
import DashboardRepoGroup from './DashboardRepoGroup.vue';
const {appSubUrl, assetUrlPrefix, pageData} = window.config;
@ -22,17 +23,17 @@ type DashboardRepo = {
locale_latest_commit_status_state?: string,
};
type CommitStatus = 'pending' | 'success' | 'error' | 'failure' | 'warning' | 'skipped';
export type CommitStatus = 'pending' | 'success' | 'error' | 'failure' | 'warning' | 'skipped';
type CommitStatusMap = {
[status in CommitStatus]: {
export type CommitStatusMap = {
[ status in CommitStatus ]: {
name: SvgName,
color: string,
};
};
// make sure this matches templates/repo/commit_status.tmpl
const commitStatus: CommitStatusMap = {
export const commitStatus: CommitStatusMap = {
pending: {name: 'octicon-dot-fill', color: 'tw-text-yellow'},
success: {name: 'octicon-check', color: 'tw-text-green'},
error: {name: 'gitea-exclamation', color: 'tw-text-red'},
@ -40,9 +41,50 @@ const commitStatus: CommitStatusMap = {
warning: {name: 'gitea-exclamation', color: 'tw-text-yellow'},
skipped: {name: 'octicon-skip', color: 'tw-text-text-light'},
};
export type GroupMapType = {
repos: any[]
subgroups: number[]
id: number
[ k: string ]: any
}
export default defineComponent({
components: {SvgIcon},
components: {SvgIcon, DashboardRepoGroup},
provide() {
return {
expandedGroups: computed({
get: () => {
return this.expandedGroups;
},
set: (v) => {
this.expandedGroups = v;
},
}),
searchURL: this.searchURL,
groups: computed({
get: () => {
return this.groups;
},
set: (v) => {
this.groups = v;
},
}),
repos: computed(() => this.computedRepos),
loadedMap: computed({
get: () => {
return this.loadedMap;
},
set: (v) => {
this.loadedMap = v;
},
}),
orgName: this.organizationName,
};
},
setup() {
const groups = ref(new Map<number, GroupMapType>());
const loadedMap = ref(new Map<number, boolean>([[0, true]]));
return {groupsRef: groups, loadedRef: loadedMap};
},
data() {
const params = new URLSearchParams(window.location.search);
const tab = params.get('repo-search-tab') || 'repos';
@ -55,6 +97,7 @@ export default defineComponent({
return {
tab,
repos: [] as DashboardRepo[],
groupData: this.groupsRef,
reposTotalCount: null as number | null,
reposFilter,
archivedFilter,
@ -127,15 +170,15 @@ export default defineComponent({
subUrl: appSubUrl,
...pageData.dashboardRepoList,
activeIndex: -1, // don't select anything at load, first cursor down will select
expandedGroupsRaw: [],
};
},
computed: {
showMoreReposLink() {
return this.repos.length > 0 && this.repos.length < this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`];
},
searchURL() {
return `${this.subUrl}/repo/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=${this.searchQuery
return `${this.subUrl}/group/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.archivedFilter === 'archived' ? '&archived=true' : ''}${this.archivedFilter === 'unarchived' ? '&archived=false' : ''
}${this.privateFilter === 'private' ? '&is_private=true' : ''}${this.privateFilter === 'public' ? '&is_private=false' : ''
@ -156,6 +199,38 @@ export default defineComponent({
checkboxPrivateFilterProps() {
return {checked: this.privateFilter === 'private', indeterminate: this.privateFilter === 'both'};
},
expandedGroups: {
get() {
return this.expandedGroupsRaw;
},
set(val: number[]) {
this.expandedGroupsRaw = val;
},
},
groups: {
get() {
return this.groupData;
},
set(v: Map<number, GroupMapType>) {
for (const [k, val] of v) {
this.groupData.set(k, val);
}
},
},
computedRepos() {
return this.repos;
},
root() {
return [...(this.groups.get(0)?.subgroups ?? []), ...this.repos];
},
loadedMap: {
get() {
return this.loadedRef;
},
set(v: Ref<Map<number, boolean>>) {
this.loadedRef = v;
},
},
},
mounted() {
@ -185,6 +260,9 @@ export default defineComponent({
changeReposFilter(filter: string) {
this.reposFilter = filter;
this.repos = [];
this.groups = new Map();
this.loadedMap = new Map();
this.expandedGroupsRaw = [];
this.page = 1;
this.counts[`${filter}:${this.archivedFilter}:${this.privateFilter}`] = 0;
this.searchRepos();
@ -261,6 +339,8 @@ export default defineComponent({
}
this.page = 1;
this.repos = [];
this.groups = new Map();
this.loadedMap = new Map();
this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`] = 0;
this.searchRepos();
},
@ -276,6 +356,8 @@ export default defineComponent({
this.page = 1;
}
this.repos = [];
this.groups = new Map();
this.loadedMap = new Map();
this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`] = 0;
await this.searchRepos();
},
@ -284,7 +366,7 @@ export default defineComponent({
this.isLoading = true;
const searchedMode = this.repoTypes[this.reposFilter].searchMode;
const searchedURL = this.searchURL;
const searchedURL = `${this.searchURL}&group_id=-1`;
const searchedQuery = this.searchQuery;
let response, json;
@ -306,14 +388,14 @@ export default defineComponent({
response = await GET(searchedURL);
json = await response.json();
} catch {
if (searchedURL === this.searchURL) {
if (searchedURL.startsWith(this.searchURL)) {
this.isLoading = false;
}
return;
}
if (searchedURL === this.searchURL) {
this.repos = json.data.map((webSearchRepo: any) => {
if (searchedURL.startsWith(this.searchURL)) {
this.repos = json.data.repos.map((webSearchRepo: any) => {
return {
...webSearchRepo.repository,
latest_commit_status_state: webSearchRepo.latest_commit_status?.State, // if latest_commit_status is null, it means there is no commit status
@ -321,7 +403,25 @@ export default defineComponent({
locale_latest_commit_status_state: webSearchRepo.locale_latest_commit_status,
};
});
const count = Number(response.headers.get('X-Total-Count'));
this.groups.set(0, {
repos: this.repos.filter((a: any) => !a.group_id),
subgroups: json.data.subgroups.map((g: any) => {
return g.group.id;
}),
data: {},
});
for (const g of json.data.subgroups) {
this.groups.set(g.group.id, {
subgroups: g.subgroups.map((h: any) => h.group.id),
repos: g.repos,
...g.group,
latest_commit_status_state: g.latest_commit_status?.State, // if latest_commit_status is null, it means there is no commit status
latest_commit_status_state_link: g.latest_commit_status?.TargetURL,
locale_latest_commit_status_state: g.locale_latest_commit_status,
id: g.group.id,
});
}
const count = this.repos.length;
if (searchedQuery === '' && searchedMode === '' && this.archivedFilter === 'both') {
this.reposTotalCount = count;
}
@ -422,7 +522,7 @@ export default defineComponent({
<div v-else class="ui attached segment repos-search">
<div class="ui small fluid action left icon input">
<input type="search" spellcheck="false" maxlength="255" @input="changeReposFilter(reposFilter)" v-model="searchQuery" ref="search" @keydown="reposFilterKeyControl" :placeholder="textSearchRepos">
<i class="icon loading-icon-3px" :class="{'is-loading': isLoading}"><svg-icon name="octicon-search" :size="16"/></i>
<i class="icon loading-icon-3px" :class="{ 'is-loading': isLoading }"><svg-icon name="octicon-search" :size="16"/></i>
<div class="ui dropdown icon button" :title="textFilter">
<svg-icon name="octicon-filter" :size="16"/>
<div class="menu">
@ -451,23 +551,23 @@ export default defineComponent({
</div>
<overflow-menu class="ui secondary pointing tabular borderless menu repos-filter">
<div class="overflow-menu-items tw-justify-center">
<a class="item" tabindex="0" :class="{active: reposFilter === 'all'}" @click="changeReposFilter('all')">
<a class="item" tabindex="0" :class="{ active: reposFilter === 'all' }" @click="changeReposFilter('all')">
{{ textAll }}
<div v-show="reposFilter === 'all'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
</a>
<a class="item" tabindex="0" :class="{active: reposFilter === 'sources'}" @click="changeReposFilter('sources')">
<a class="item" tabindex="0" :class="{ active: reposFilter === 'sources' }" @click="changeReposFilter('sources')">
{{ textSources }}
<div v-show="reposFilter === 'sources'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
</a>
<a class="item" tabindex="0" :class="{active: reposFilter === 'forks'}" @click="changeReposFilter('forks')">
<a class="item" tabindex="0" :class="{ active: reposFilter === 'forks' }" @click="changeReposFilter('forks')">
{{ textForks }}
<div v-show="reposFilter === 'forks'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
</a>
<a class="item" tabindex="0" :class="{active: reposFilter === 'mirrors'}" @click="changeReposFilter('mirrors')" v-if="isMirrorsEnabled">
<a class="item" tabindex="0" :class="{ active: reposFilter === 'mirrors' }" @click="changeReposFilter('mirrors')" v-if="isMirrorsEnabled">
{{ textMirrors }}
<div v-show="reposFilter === 'mirrors'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
</a>
<a class="item" tabindex="0" :class="{active: reposFilter === 'collaborative'}" @click="changeReposFilter('collaborative')">
<a class="item" tabindex="0" :class="{ active: reposFilter === 'collaborative' }" @click="changeReposFilter('collaborative')">
{{ textCollaborative }}
<div v-show="reposFilter === 'collaborative'" class="ui circular mini grey label">{{ repoTypeCount }}</div>
</a>
@ -475,45 +575,31 @@ export default defineComponent({
</overflow-menu>
</div>
<div v-if="repos.length" class="ui attached table segment tw-rounded-b">
<ul class="repo-owner-name-list">
<li class="tw-flex tw-items-center tw-py-2" v-for="(repo, index) in repos" :class="{'active': index === activeIndex}" :key="repo.id">
<a class="repo-list-link muted" :href="repo.link">
<svg-icon :name="repoIcon(repo)" :size="16" class="repo-list-icon"/>
<div class="tw-inline-block tw-truncate">{{ repo.full_name }}</div>
<div v-if="repo.archived">
<svg-icon name="octicon-archive" :size="16"/>
</div>
</a>
<a class="tw-flex tw-items-center" v-if="repo.latest_commit_status_state" :href="repo.latest_commit_status_state_link || undefined" :data-tooltip-content="repo.locale_latest_commit_status_state">
<!-- the commit status icon logic is taken from templates/repo/commit_status.tmpl -->
<svg-icon :name="statusIcon(repo.latest_commit_status_state)" :class="'tw-ml-2 commit-status icon ' + statusColor(repo.latest_commit_status_state)" :size="16"/>
</a>
</li>
</ul>
<dashboard-repo-group :items="root" :depth="1" :cur-group="0" @load-changed="(nv: boolean) => (isLoading = nv)"/>
<div v-if="showMoreReposLink" class="tw-text-center">
<div class="divider tw-my-0"/>
<div class="ui borderless pagination menu narrow tw-my-2">
<a
class="item navigation tw-py-1" :class="{'disabled': page === 1}"
class="item navigation tw-py-1" :class="{ 'disabled': page === 1 }"
@click="changePage(1)" :title="textFirstPage"
>
<svg-icon name="gitea-double-chevron-left" :size="16" class="tw-mr-1"/>
</a>
<a
class="item navigation tw-py-1" :class="{'disabled': page === 1}"
class="item navigation tw-py-1" :class="{ 'disabled': page === 1 }"
@click="changePage(page - 1)" :title="textPreviousPage"
>
<svg-icon name="octicon-chevron-left" :size="16" class="tw-mr-1"/>
</a>
<a class="active item tw-py-1">{{ page }}</a>
<a
class="item navigation" :class="{'disabled': page === finalPage}"
class="item navigation" :class="{ 'disabled': page === finalPage }"
@click="changePage(page + 1)" :title="textNextPage"
>
<svg-icon name="octicon-chevron-right" :size="16" class="tw-ml-1"/>
</a>
<a
class="item navigation tw-py-1" :class="{'disabled': page === finalPage}"
class="item navigation tw-py-1" :class="{ 'disabled': page === finalPage }"
@click="changePage(finalPage)" :title="textLastPage"
>
<svg-icon name="gitea-double-chevron-right" :size="16" class="tw-ml-1"/>
@ -546,7 +632,7 @@ export default defineComponent({
<div class="tw-inline-block tw-truncate">{{ org.full_name ? `${org.full_name} (${org.name})` : org.name }}</div>
<div><!-- div to prevent underline of label on hover -->
<span class="ui tiny basic label" v-if="org.org_visibility !== 'public'">
{{ org.org_visibility === 'limited' ? textOrgVisibilityLimited: textOrgVisibilityPrivate }}
{{ org.org_visibility === 'limited' ? textOrgVisibilityLimited : textOrgVisibilityPrivate }}
</span>
</div>
</a>