feat: Gitea project linking

- Projects can now be linked to Gitea repos
- Separate sections for GitLab and Gitea
- Multi-select for both platforms
This commit is contained in:
FluxKit
2026-02-19 14:07:42 +00:00
parent 263d52141d
commit abb7a9ea81

View File

@@ -19,6 +19,13 @@ interface GitLabProjectRef {
name: string
}
interface GiteaProjectRef {
projectId: number
path: string
url: string
name: string
}
interface Project {
_id: string
name: string
@@ -26,9 +33,7 @@ interface Project {
color: string
rules?: string
gitlabProjects?: GitLabProjectRef[]
gitlabProjectId?: number
gitlabUrl?: string
gitlabPath?: string
giteaProjects?: GiteaProjectRef[]
}
interface GitLabProject {
@@ -40,11 +45,22 @@ interface GitLabProject {
webUrl: string
}
interface GiteaProject {
id: number
name: string
fullName: string
path: string
description: string | null
webUrl: string
owner: string
}
const authStore = useAuthStore()
const toast = useToast()
const projects = ref<Project[]>([])
const tasks = ref<Task[]>([])
const gitlabProjects = ref<GitLabProject[]>([])
const giteaProjects = ref<GiteaProject[]>([])
const loading = ref(true)
const showEditModal = ref(false)
@@ -56,7 +72,8 @@ const newProject = ref({
description: '',
color: '#6366f1',
rules: '',
gitlabProjects: [] as GitLabProjectRef[]
gitlabProjects: [] as GitLabProjectRef[],
giteaProjects: [] as GiteaProjectRef[]
})
async function loadData() {
@@ -83,14 +100,21 @@ async function loadGitlabProjects() {
}
}
async function loadGiteaProjects() {
try {
giteaProjects.value = await api.get('/gitea/projects')
} catch (err) {
console.error('Failed to load Gitea projects:', err)
}
}
// GitLab selection
const selectedGitlabIds = ref<number[]>([])
function initGitlabSelection() {
if (!editingProject.value) return
if (editingProject.value.gitlabProjects?.length) {
selectedGitlabIds.value = editingProject.value.gitlabProjects.map(p => p.projectId)
} else if (editingProject.value.gitlabProjectId) {
selectedGitlabIds.value = [editingProject.value.gitlabProjectId]
} else {
selectedGitlabIds.value = []
}
@@ -128,6 +152,50 @@ function isGitlabSelected(id: number): boolean {
return selectedGitlabIds.value.includes(id)
}
// Gitea selection
const selectedGiteaIds = ref<number[]>([])
function initGiteaSelection() {
if (!editingProject.value) return
if (editingProject.value.giteaProjects?.length) {
selectedGiteaIds.value = editingProject.value.giteaProjects.map(p => p.projectId)
} else {
selectedGiteaIds.value = []
}
}
function toggleGiteaProject(giteaProject: GiteaProject) {
const idx = selectedGiteaIds.value.indexOf(giteaProject.id)
if (idx >= 0) {
selectedGiteaIds.value.splice(idx, 1)
} else {
selectedGiteaIds.value.push(giteaProject.id)
}
updateGiteaProjects()
}
function updateGiteaProjects() {
const gtProjects = selectedGiteaIds.value.map(id => {
const gp = giteaProjects.value.find(p => p.id === id)
return gp ? {
projectId: gp.id,
path: gp.fullName,
url: gp.webUrl,
name: gp.name
} : null
}).filter(Boolean) as GiteaProjectRef[]
if (editingProject.value) {
editingProject.value.giteaProjects = gtProjects
} else {
newProject.value.giteaProjects = gtProjects
}
}
function isGiteaSelected(id: number): boolean {
return selectedGiteaIds.value.includes(id)
}
function getProjectTasks(projectId: string) {
return tasks.value.filter(t => t.project === projectId)
}
@@ -148,15 +216,18 @@ function openCreateModal() {
description: '',
color: '#6366f1',
rules: '',
gitlabProjects: []
gitlabProjects: [],
giteaProjects: []
}
selectedGitlabIds.value = []
selectedGiteaIds.value = []
showCreateModal.value = true
}
function editProject(project: Project) {
editingProject.value = { ...project }
initGitlabSelection()
initGiteaSelection()
showEditModal.value = true
}
@@ -172,7 +243,8 @@ async function createProject() {
description: newProject.value.description,
color: newProject.value.color,
rules: newProject.value.rules,
gitlabProjects: newProject.value.gitlabProjects
gitlabProjects: newProject.value.gitlabProjects,
giteaProjects: newProject.value.giteaProjects
})
toast.add({ severity: 'success', summary: 'Erstellt', detail: 'Projekt wurde angelegt', life: 3000 })
showCreateModal.value = false
@@ -192,9 +264,7 @@ async function saveProject() {
color: editingProject.value.color,
rules: editingProject.value.rules,
gitlabProjects: editingProject.value.gitlabProjects,
gitlabProjectId: editingProject.value.gitlabProjects?.[0]?.projectId,
gitlabUrl: editingProject.value.gitlabProjects?.[0]?.url,
gitlabPath: editingProject.value.gitlabProjects?.[0]?.path
giteaProjects: editingProject.value.giteaProjects
})
toast.add({ severity: 'success', summary: 'Gespeichert', detail: 'Projekt aktualisiert', life: 3000 })
showEditModal.value = false
@@ -219,6 +289,7 @@ async function deleteProject(id: string, name: string) {
onMounted(() => {
loadData()
loadGitlabProjects()
loadGiteaProjects()
})
</script>
@@ -272,23 +343,26 @@ onMounted(() => {
</div>
</div>
<div v-if="project.gitlabProjects?.length || project.gitlabPath" class="project-gitlab">
<h4><i class="pi pi-github"></i> GitLab</h4>
<template v-if="project.gitlabProjects?.length">
<div v-for="gp in project.gitlabProjects" :key="gp.projectId" class="gitlab-project-item">
<a :href="gp.url" target="_blank" class="gitlab-link">
<!-- GitLab Projects -->
<div v-if="project.gitlabProjects?.length" class="project-git gitlab">
<h4><i class="pi pi-gitlab"></i> GitLab</h4>
<div v-for="gp in project.gitlabProjects" :key="gp.projectId" class="git-project-item">
<a :href="gp.url" target="_blank" class="git-link">
<i class="pi pi-external-link"></i>
{{ gp.path }}
</a>
<span class="gitlab-id">ID: {{ gp.projectId }}</span>
</div>
</template>
<template v-else-if="project.gitlabPath || project.gitlabUrl">
<a v-if="project.gitlabUrl" :href="project.gitlabUrl" target="_blank" class="gitlab-link">
</div>
<!-- Gitea Projects -->
<div v-if="project.giteaProjects?.length" class="project-git gitea">
<h4><i class="pi pi-github"></i> Gitea</h4>
<div v-for="gp in project.giteaProjects" :key="gp.projectId" class="git-project-item">
<a :href="gp.url" target="_blank" class="git-link">
<i class="pi pi-external-link"></i>
{{ project.gitlabPath || project.gitlabUrl }}
{{ gp.path }}
</a>
</template>
</div>
</div>
<div v-if="project.rules" class="project-rules">
@@ -362,20 +436,41 @@ onMounted(() => {
<textarea v-model="newProject.rules" rows="3" placeholder="z.B. Code-Review erforderlich..."></textarea>
</div>
<div v-if="gitlabProjects.length > 0" class="gitlab-section">
<h4><i class="pi pi-github"></i> GitLab Projekte</h4>
<!-- Gitea Projects -->
<div v-if="giteaProjects.length > 0" class="git-section gitea">
<h4><i class="pi pi-github"></i> Gitea Projekte</h4>
<div class="form-field">
<label>Verknüpfte Projekte (optional)</label>
<div class="gitlab-multi-select">
<label>Verknüpfte Repos (optional)</label>
<div class="git-multi-select">
<div
v-for="gp in giteaProjects"
:key="gp.id"
class="git-option"
:class="{ selected: isGiteaSelected(gp.id) }"
@click="toggleGiteaProject(gp)"
>
<i :class="isGiteaSelected(gp.id) ? 'pi pi-check-square' : 'pi pi-stop'" class="checkbox-icon"></i>
<span class="git-name">{{ gp.fullName }}</span>
</div>
</div>
</div>
</div>
<!-- GitLab Projects -->
<div v-if="gitlabProjects.length > 0" class="git-section gitlab">
<h4><i class="pi pi-gitlab"></i> GitLab Projekte</h4>
<div class="form-field">
<label>Verknüpfte Repos (optional)</label>
<div class="git-multi-select">
<div
v-for="gp in gitlabProjects"
:key="gp.id"
class="gitlab-option"
class="git-option"
:class="{ selected: isGitlabSelected(gp.id) }"
@click="toggleGitlabProject(gp)"
>
<i :class="isGitlabSelected(gp.id) ? 'pi pi-check-square' : 'pi pi-stop'" class="checkbox-icon"></i>
<span class="gitlab-name">{{ gp.fullName }}</span>
<span class="git-name">{{ gp.fullName }}</span>
</div>
</div>
</div>
@@ -420,23 +515,50 @@ onMounted(() => {
<textarea v-model="editingProject.rules" rows="3"></textarea>
</div>
<div v-if="gitlabProjects.length > 0" class="gitlab-section">
<h4><i class="pi pi-github"></i> GitLab Projekte</h4>
<!-- Gitea Projects -->
<div v-if="giteaProjects.length > 0" class="git-section gitea">
<h4><i class="pi pi-github"></i> Gitea Projekte</h4>
<div class="form-field">
<label>Verknüpfte Projekte</label>
<div class="gitlab-multi-select">
<label>Verknüpfte Repos</label>
<div class="git-multi-select">
<div
v-for="gp in giteaProjects"
:key="gp.id"
class="git-option"
:class="{ selected: isGiteaSelected(gp.id) }"
@click="toggleGiteaProject(gp)"
>
<i :class="isGiteaSelected(gp.id) ? 'pi pi-check-square' : 'pi pi-stop'" class="checkbox-icon"></i>
<span class="git-name">{{ gp.fullName }}</span>
</div>
</div>
</div>
<div v-if="editingProject.giteaProjects?.length" class="git-info">
{{ editingProject.giteaProjects.length }} Gitea-Repo(s) ausgewählt
</div>
</div>
<!-- GitLab Projects -->
<div v-if="gitlabProjects.length > 0" class="git-section gitlab">
<h4><i class="pi pi-gitlab"></i> GitLab Projekte</h4>
<div class="form-field">
<label>Verknüpfte Repos</label>
<div class="git-multi-select">
<div
v-for="gp in gitlabProjects"
:key="gp.id"
class="gitlab-option"
class="git-option"
:class="{ selected: isGitlabSelected(gp.id) }"
@click="toggleGitlabProject(gp)"
>
<i :class="isGitlabSelected(gp.id) ? 'pi pi-check-square' : 'pi pi-stop'" class="checkbox-icon"></i>
<span class="gitlab-name">{{ gp.fullName }}</span>
<span class="git-name">{{ gp.fullName }}</span>
</div>
</div>
</div>
<div v-if="editingProject.gitlabProjects?.length" class="git-info">
{{ editingProject.gitlabProjects.length }} GitLab-Repo(s) ausgewählt
</div>
</div>
</div>
@@ -539,13 +661,13 @@ onMounted(() => {
.stat.progress .stat-value { color: #fbbf24; }
.stat.todo .stat-value { color: #60a5fa; }
.project-rules, .project-gitlab {
.project-rules, .project-git {
padding: 1rem 1.25rem;
background: rgba(255,255,255,0.03);
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.project-rules h4, .project-gitlab h4 {
.project-rules h4, .project-git h4 {
margin: 0 0 0.5rem;
font-size: 0.875rem;
color: rgba(255,255,255,0.7);
@@ -554,9 +676,24 @@ onMounted(() => {
gap: 0.5rem;
}
.project-gitlab h4 { color: #fb923c; }
.project-git.gitlab h4 { color: #fb923c; }
.project-git.gitea h4 { color: #22c55e; }
.project-rules p { margin: 0; font-size: 0.875rem; color: rgba(255,255,255,0.6); }
.git-project-item { padding: 0.25rem 0; }
.git-link {
display: inline-flex;
align-items: center;
gap: 0.375rem;
text-decoration: none;
font-size: 0.875rem;
font-family: monospace;
}
.project-git.gitlab .git-link { color: #fb923c; }
.project-git.gitea .git-link { color: #22c55e; }
.git-link:hover { text-decoration: underline; }
.project-tasks { padding: 1rem 1.25rem; }
.project-tasks h4 { margin: 0 0 0.75rem; font-size: 0.875rem; color: rgba(255,255,255,0.7); }
@@ -594,11 +731,6 @@ onMounted(() => {
.empty-state i { font-size: 3rem; margin-bottom: 1rem; opacity: 0.5; }
.empty-state p { margin-bottom: 1.5rem; }
.gitlab-link { display: inline-flex; align-items: center; gap: 0.375rem; color: #fb923c; text-decoration: none; font-size: 0.875rem; font-family: monospace; }
.gitlab-link:hover { text-decoration: underline; }
.gitlab-id { margin-left: 0.75rem; color: rgba(255,255,255,0.4); font-size: 0.75rem; }
.gitlab-project-item { display: flex; align-items: center; padding: 0.25rem 0; }
.modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
@@ -616,7 +748,7 @@ onMounted(() => {
border-radius: 16px;
padding: 1.25rem;
width: 100%;
max-width: 500px;
max-width: 550px;
max-height: 85vh;
display: flex;
flex-direction: column;
@@ -657,15 +789,18 @@ onMounted(() => {
.cancel-btn { padding: 0.75rem 1rem; background: transparent; border: 1px solid rgba(255,255,255,0.2); border-radius: 8px; color: rgba(255,255,255,0.7); cursor: pointer; }
.submit-btn { padding: 0.75rem 1.5rem; background: linear-gradient(135deg, #6366f1, #8b5cf6); border: none; border-radius: 8px; color: #fff; font-weight: 600; cursor: pointer; }
.gitlab-section { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid rgba(255,255,255,0.1); }
.gitlab-section h4 { margin: 0 0 1rem; color: #fb923c; font-size: 0.9rem; display: flex; align-items: center; gap: 0.5rem; }
.git-section { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid rgba(255,255,255,0.1); }
.git-section h4 { margin: 0 0 1rem; font-size: 0.9rem; display: flex; align-items: center; gap: 0.5rem; }
.git-section.gitlab h4 { color: #fb923c; }
.git-section.gitea h4 { color: #22c55e; }
.gitlab-multi-select { max-height: 150px; overflow-y: auto; border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; background: rgba(255,255,255,0.02); }
.gitlab-option { display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem 0.75rem; cursor: pointer; transition: background 0.2s; border-bottom: 1px solid rgba(255,255,255,0.05); }
.gitlab-option:last-child { border-bottom: none; }
.gitlab-option:hover { background: rgba(255,255,255,0.05); }
.gitlab-option.selected { background: rgba(99,102,241,0.2); }
.git-multi-select { max-height: 150px; overflow-y: auto; border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; background: rgba(255,255,255,0.02); }
.git-option { display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem 0.75rem; cursor: pointer; transition: background 0.2s; border-bottom: 1px solid rgba(255,255,255,0.05); }
.git-option:last-child { border-bottom: none; }
.git-option:hover { background: rgba(255,255,255,0.05); }
.git-option.selected { background: rgba(99,102,241,0.2); }
.checkbox-icon { color: rgba(255,255,255,0.4); font-size: 1rem; }
.gitlab-option.selected .checkbox-icon { color: #6366f1; }
.gitlab-name { font-size: 0.8rem; color: rgba(255,255,255,0.9); }
.git-option.selected .checkbox-icon { color: #6366f1; }
.git-name { font-size: 0.8rem; color: rgba(255,255,255,0.9); }
.git-info { margin-top: 0.5rem; padding: 0.5rem; background: rgba(99,102,241,0.1); border-radius: 8px; font-size: 0.75rem; color: #818cf8; }
</style>