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 name: string
} }
interface GiteaProjectRef {
projectId: number
path: string
url: string
name: string
}
interface Project { interface Project {
_id: string _id: string
name: string name: string
@@ -26,9 +33,7 @@ interface Project {
color: string color: string
rules?: string rules?: string
gitlabProjects?: GitLabProjectRef[] gitlabProjects?: GitLabProjectRef[]
gitlabProjectId?: number giteaProjects?: GiteaProjectRef[]
gitlabUrl?: string
gitlabPath?: string
} }
interface GitLabProject { interface GitLabProject {
@@ -40,11 +45,22 @@ interface GitLabProject {
webUrl: string webUrl: string
} }
interface GiteaProject {
id: number
name: string
fullName: string
path: string
description: string | null
webUrl: string
owner: string
}
const authStore = useAuthStore() const authStore = useAuthStore()
const toast = useToast() const toast = useToast()
const projects = ref<Project[]>([]) const projects = ref<Project[]>([])
const tasks = ref<Task[]>([]) const tasks = ref<Task[]>([])
const gitlabProjects = ref<GitLabProject[]>([]) const gitlabProjects = ref<GitLabProject[]>([])
const giteaProjects = ref<GiteaProject[]>([])
const loading = ref(true) const loading = ref(true)
const showEditModal = ref(false) const showEditModal = ref(false)
@@ -56,7 +72,8 @@ const newProject = ref({
description: '', description: '',
color: '#6366f1', color: '#6366f1',
rules: '', rules: '',
gitlabProjects: [] as GitLabProjectRef[] gitlabProjects: [] as GitLabProjectRef[],
giteaProjects: [] as GiteaProjectRef[]
}) })
async function loadData() { 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[]>([]) const selectedGitlabIds = ref<number[]>([])
function initGitlabSelection() { function initGitlabSelection() {
if (!editingProject.value) return if (!editingProject.value) return
if (editingProject.value.gitlabProjects?.length) { if (editingProject.value.gitlabProjects?.length) {
selectedGitlabIds.value = editingProject.value.gitlabProjects.map(p => p.projectId) selectedGitlabIds.value = editingProject.value.gitlabProjects.map(p => p.projectId)
} else if (editingProject.value.gitlabProjectId) {
selectedGitlabIds.value = [editingProject.value.gitlabProjectId]
} else { } else {
selectedGitlabIds.value = [] selectedGitlabIds.value = []
} }
@@ -128,6 +152,50 @@ function isGitlabSelected(id: number): boolean {
return selectedGitlabIds.value.includes(id) 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) { function getProjectTasks(projectId: string) {
return tasks.value.filter(t => t.project === projectId) return tasks.value.filter(t => t.project === projectId)
} }
@@ -148,15 +216,18 @@ function openCreateModal() {
description: '', description: '',
color: '#6366f1', color: '#6366f1',
rules: '', rules: '',
gitlabProjects: [] gitlabProjects: [],
giteaProjects: []
} }
selectedGitlabIds.value = [] selectedGitlabIds.value = []
selectedGiteaIds.value = []
showCreateModal.value = true showCreateModal.value = true
} }
function editProject(project: Project) { function editProject(project: Project) {
editingProject.value = { ...project } editingProject.value = { ...project }
initGitlabSelection() initGitlabSelection()
initGiteaSelection()
showEditModal.value = true showEditModal.value = true
} }
@@ -172,7 +243,8 @@ async function createProject() {
description: newProject.value.description, description: newProject.value.description,
color: newProject.value.color, color: newProject.value.color,
rules: newProject.value.rules, 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 }) toast.add({ severity: 'success', summary: 'Erstellt', detail: 'Projekt wurde angelegt', life: 3000 })
showCreateModal.value = false showCreateModal.value = false
@@ -192,9 +264,7 @@ async function saveProject() {
color: editingProject.value.color, color: editingProject.value.color,
rules: editingProject.value.rules, rules: editingProject.value.rules,
gitlabProjects: editingProject.value.gitlabProjects, gitlabProjects: editingProject.value.gitlabProjects,
gitlabProjectId: editingProject.value.gitlabProjects?.[0]?.projectId, giteaProjects: editingProject.value.giteaProjects
gitlabUrl: editingProject.value.gitlabProjects?.[0]?.url,
gitlabPath: editingProject.value.gitlabProjects?.[0]?.path
}) })
toast.add({ severity: 'success', summary: 'Gespeichert', detail: 'Projekt aktualisiert', life: 3000 }) toast.add({ severity: 'success', summary: 'Gespeichert', detail: 'Projekt aktualisiert', life: 3000 })
showEditModal.value = false showEditModal.value = false
@@ -219,6 +289,7 @@ async function deleteProject(id: string, name: string) {
onMounted(() => { onMounted(() => {
loadData() loadData()
loadGitlabProjects() loadGitlabProjects()
loadGiteaProjects()
}) })
</script> </script>
@@ -272,23 +343,26 @@ onMounted(() => {
</div> </div>
</div> </div>
<div v-if="project.gitlabProjects?.length || project.gitlabPath" class="project-gitlab"> <!-- GitLab Projects -->
<h4><i class="pi pi-github"></i> GitLab</h4> <div v-if="project.gitlabProjects?.length" class="project-git gitlab">
<template v-if="project.gitlabProjects?.length"> <h4><i class="pi pi-gitlab"></i> GitLab</h4>
<div v-for="gp in project.gitlabProjects" :key="gp.projectId" class="gitlab-project-item"> <div v-for="gp in project.gitlabProjects" :key="gp.projectId" class="git-project-item">
<a :href="gp.url" target="_blank" class="gitlab-link"> <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">
<i class="pi pi-external-link"></i> <i class="pi pi-external-link"></i>
{{ project.gitlabPath || project.gitlabUrl }} {{ gp.path }}
</a> </a>
</template> </div>
</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>
{{ gp.path }}
</a>
</div>
</div> </div>
<div v-if="project.rules" class="project-rules"> <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> <textarea v-model="newProject.rules" rows="3" placeholder="z.B. Code-Review erforderlich..."></textarea>
</div> </div>
<div v-if="gitlabProjects.length > 0" class="gitlab-section"> <!-- Gitea Projects -->
<h4><i class="pi pi-github"></i> GitLab Projekte</h4> <div v-if="giteaProjects.length > 0" class="git-section gitea">
<h4><i class="pi pi-github"></i> Gitea Projekte</h4>
<div class="form-field"> <div class="form-field">
<label>Verknüpfte Projekte (optional)</label> <label>Verknüpfte Repos (optional)</label>
<div class="gitlab-multi-select"> <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 <div
v-for="gp in gitlabProjects" v-for="gp in gitlabProjects"
:key="gp.id" :key="gp.id"
class="gitlab-option" class="git-option"
:class="{ selected: isGitlabSelected(gp.id) }" :class="{ selected: isGitlabSelected(gp.id) }"
@click="toggleGitlabProject(gp)" @click="toggleGitlabProject(gp)"
> >
<i :class="isGitlabSelected(gp.id) ? 'pi pi-check-square' : 'pi pi-stop'" class="checkbox-icon"></i> <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>
</div> </div>
@@ -420,23 +515,50 @@ onMounted(() => {
<textarea v-model="editingProject.rules" rows="3"></textarea> <textarea v-model="editingProject.rules" rows="3"></textarea>
</div> </div>
<div v-if="gitlabProjects.length > 0" class="gitlab-section"> <!-- Gitea Projects -->
<h4><i class="pi pi-github"></i> GitLab Projekte</h4> <div v-if="giteaProjects.length > 0" class="git-section gitea">
<h4><i class="pi pi-github"></i> Gitea Projekte</h4>
<div class="form-field"> <div class="form-field">
<label>Verknüpfte Projekte</label> <label>Verknüpfte Repos</label>
<div class="gitlab-multi-select"> <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 <div
v-for="gp in gitlabProjects" v-for="gp in gitlabProjects"
:key="gp.id" :key="gp.id"
class="gitlab-option" class="git-option"
:class="{ selected: isGitlabSelected(gp.id) }" :class="{ selected: isGitlabSelected(gp.id) }"
@click="toggleGitlabProject(gp)" @click="toggleGitlabProject(gp)"
> >
<i :class="isGitlabSelected(gp.id) ? 'pi pi-check-square' : 'pi pi-stop'" class="checkbox-icon"></i> <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>
</div> </div>
<div v-if="editingProject.gitlabProjects?.length" class="git-info">
{{ editingProject.gitlabProjects.length }} GitLab-Repo(s) ausgewählt
</div>
</div> </div>
</div> </div>
@@ -539,13 +661,13 @@ onMounted(() => {
.stat.progress .stat-value { color: #fbbf24; } .stat.progress .stat-value { color: #fbbf24; }
.stat.todo .stat-value { color: #60a5fa; } .stat.todo .stat-value { color: #60a5fa; }
.project-rules, .project-gitlab { .project-rules, .project-git {
padding: 1rem 1.25rem; padding: 1rem 1.25rem;
background: rgba(255,255,255,0.03); background: rgba(255,255,255,0.03);
border-bottom: 1px solid rgba(255,255,255,0.05); 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; margin: 0 0 0.5rem;
font-size: 0.875rem; font-size: 0.875rem;
color: rgba(255,255,255,0.7); color: rgba(255,255,255,0.7);
@@ -554,9 +676,24 @@ onMounted(() => {
gap: 0.5rem; 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); } .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 { padding: 1rem 1.25rem; }
.project-tasks h4 { margin: 0 0 0.75rem; font-size: 0.875rem; color: rgba(255,255,255,0.7); } .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 i { font-size: 3rem; margin-bottom: 1rem; opacity: 0.5; }
.empty-state p { margin-bottom: 1.5rem; } .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 { .modal-overlay {
position: fixed; position: fixed;
top: 0; left: 0; right: 0; bottom: 0; top: 0; left: 0; right: 0; bottom: 0;
@@ -616,7 +748,7 @@ onMounted(() => {
border-radius: 16px; border-radius: 16px;
padding: 1.25rem; padding: 1.25rem;
width: 100%; width: 100%;
max-width: 500px; max-width: 550px;
max-height: 85vh; max-height: 85vh;
display: flex; display: flex;
flex-direction: column; 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; } .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; } .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); } .git-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 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); } .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); }
.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); } .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); }
.gitlab-option:last-child { border-bottom: none; } .git-option:last-child { border-bottom: none; }
.gitlab-option:hover { background: rgba(255,255,255,0.05); } .git-option:hover { background: rgba(255,255,255,0.05); }
.gitlab-option.selected { background: rgba(99,102,241,0.2); } .git-option.selected { background: rgba(99,102,241,0.2); }
.checkbox-icon { color: rgba(255,255,255,0.4); font-size: 1rem; } .checkbox-icon { color: rgba(255,255,255,0.4); font-size: 1rem; }
.gitlab-option.selected .checkbox-icon { color: #6366f1; } .git-option.selected .checkbox-icon { color: #6366f1; }
.gitlab-name { font-size: 0.8rem; color: rgba(255,255,255,0.9); } .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> </style>