diff --git a/src/components/AssignedProjectCard.jsx b/src/components/AssignedProjectCard.jsx index 80a2193..0288088 100644 --- a/src/components/AssignedProjectCard.jsx +++ b/src/components/AssignedProjectCard.jsx @@ -1,7 +1,11 @@ +import { useState } from 'react' import { FaCodeBranch } from 'react-icons/fa' import { FaLinkSlash } from 'react-icons/fa6' import GiteaLinkButton from './GiteaLinkButton' import PreviewLinkButton from './PreviewLinkButton' +import { suggestSubdomain } from '../lib/subdomain' +import { needsPreviewButton, assignSummary } from '../lib/projectAssign' +import { updateWebsiteProject, backfillProjectPushes } from '../lib/projectAdminApi' const STATUS_COLORS = { ready: '#10b981', @@ -28,7 +32,49 @@ function StatusBadge({ label, value }) { ) } -export default function AssignedProjectCard({ project, onUnassign, showActions = false }) { +export default function AssignedProjectCard({ project, onUnassign, showActions = false, customerName = '', onChanged = () => {} }) { + const [showPreviewInput, setShowPreviewInput] = useState(false) + const [subdomain, setSubdomain] = useState('') + const [previewBusy, setPreviewBusy] = useState(false) + const [previewMessage, setPreviewMessage] = useState('') + const [pushBusy, setPushBusy] = useState(false) + const [pushMessage, setPushMessage] = useState('') + + const openPreviewInput = () => { + setSubdomain(suggestSubdomain(customerName, project.projectName)) + setPreviewMessage('') + setShowPreviewInput(true) + } + + const handleEnablePreview = async () => { + setPreviewBusy(true) + setPreviewMessage('') + try { + const r = await updateWebsiteProject(project.$id, { projectType: 'preview', subdomain }) + setPreviewMessage(`Preview läuft: ${r.previewUrl}`) + setShowPreviewInput(false) + onChanged() + } catch (err) { + setPreviewMessage(err.message) + } finally { + setPreviewBusy(false) + } + } + + const handleBackfill = async () => { + setPushBusy(true) + setPushMessage('') + try { + const r = await backfillProjectPushes(project.$id) + setPushMessage(assignSummary(project.projectName, r).text) + onChanged() + } catch (err) { + setPushMessage(err.message) + } finally { + setPushBusy(false) + } + } + return (
@@ -63,6 +109,46 @@ export default function AssignedProjectCard({ project, onUnassign, showActions =
+ + {showActions && ( +
+
+ {needsPreviewButton(project) && !showPreviewInput && ( + + )} + {showPreviewInput && ( + <> + setSubdomain(e.target.value)} + /> + + + )} + +
+ {(previewMessage || pushMessage) && ( +
+ {previewMessage &&
{previewMessage}
} + {pushMessage &&
{pushMessage}
} +
+ )} +
+ )}
) } diff --git a/src/components/TicketProjects.jsx b/src/components/TicketProjects.jsx index 9a61583..6965f26 100644 --- a/src/components/TicketProjects.jsx +++ b/src/components/TicketProjects.jsx @@ -1,7 +1,8 @@ import { useState, useEffect, useCallback } from 'react' import { FaPlus, FaLink } from 'react-icons/fa6' import { useWebsiteProjects } from '../hooks/useWebsiteProjects' -import { createProjectFromTemplate } from '../lib/projectAdminApi' +import { createProjectFromTemplate, importNewRepos } from '../lib/projectAdminApi' +import { isOutsideOrg, assignSummary } from '../lib/projectAssign' import AssignedProjectCard from './AssignedProjectCard' function slugify(v) { @@ -31,6 +32,9 @@ export default function TicketProjects({ ticket }) { const [createForm, setCreateForm] = useState({ displayName: '', subdomain: '' }) const [busy, setBusy] = useState(false) const [error, setError] = useState('') + const [importing, setImporting] = useState(false) + const [importHint, setImportHint] = useState('') + const [assignResult, setAssignResult] = useState(null) const load = useCallback(async () => { if (!ticket?.$id) return @@ -39,7 +43,22 @@ export default function TicketProjects({ ticket }) { setAvailable(assignableProjects(all, ticket, new Set(mine.map((p) => p.$id)))) }, [ticket, fetchAllProjects, fetchByTicketId]) - useEffect(() => { load() }, [load]) + useEffect(() => { + let cancelled = false + ;(async () => { + setImporting(true) + setImportHint('') + try { + await importNewRepos() + } catch (err) { + if (!cancelled) setImportHint(`Neue Repos konnten nicht geladen werden (${err.message}).`) + } finally { + if (!cancelled) setImporting(false) + } + await load() + })() + return () => { cancelled = true } + }, [load]) const run = async (action) => { setBusy(true) @@ -54,11 +73,23 @@ export default function TicketProjects({ ticket }) { } } - const assign = () => run(async () => { - const r = await assignProjects([selected], { customerId: ticket.customerId || '', ticketId: ticket.$id }) - if (!r.success) throw new Error(r.error) - setSelected('') - }) + const assign = async () => { + setBusy(true) + setError('') + setAssignResult(null) + try { + const projectName = available.find((p) => p.$id === selected)?.projectName || '' + const r = await assignProjects([selected], { customerId: ticket.customerId || '', ticketId: ticket.$id }) + const one = r.results[0] + setAssignResult(assignSummary(projectName, one.ok ? one.result : { error: one.error })) + if (r.success) setSelected('') + await load() + } catch (err) { + setError(err.message || 'Aktion fehlgeschlagen') + } finally { + setBusy(false) + } + } const unassign = (projectId) => run(async () => { const r = await unassignProject(projectId) @@ -84,22 +115,45 @@ export default function TicketProjects({ ticket }) { return (
Projekte ({assigned.length})
+ {importing &&

Suche neue Repos …

} + {importHint &&
{importHint}
} {error &&
{error}
} {assigned.length === 0 ?

Noch kein Projekt an diesem Ticket.

- : assigned.map((p) => )} + : assigned.map((p) => ( + + ))}
+ {assignResult && ( +
+
{assignResult.text}
+ {assignResult.warnings.map((w, i) => ( +
{w}
+ ))} +
+ )} + {showCreate && (
setCreateForm((f) => ({ ...f, displayName: e.target.value }))} required /> diff --git a/src/pages/ProjectsPage.jsx b/src/pages/ProjectsPage.jsx index 866aef1..d39149b 100644 --- a/src/pages/ProjectsPage.jsx +++ b/src/pages/ProjectsPage.jsx @@ -23,6 +23,7 @@ import { archiveWebsiteProject, unarchiveWebsiteProject, } from '../lib/projectAdminApi' +import { assignSummary } from '../lib/projectAssign' import PreviewLinkButton from '../components/PreviewLinkButton' import GiteaLinkButton from '../components/GiteaLinkButton' @@ -246,10 +247,15 @@ export default function ProjectsPage() { const handleAssignCustomer = async (project, customerId) => { setAssignBusyId(project.$id) setNotice('') - const r = customerId - ? await assignProjects([project.$id], { customerId, ticketId: project.ticketId || '' }) - : await unassignProject(project.$id) - if (!r.success) setNotice('Zuordnung fehlgeschlagen: ' + (r.error || 'unbekannt')) + if (customerId) { + const r = await assignProjects([project.$id], { customerId, ticketId: project.ticketId || '' }) + const one = r.results[0] + const summary = assignSummary(project.projectName, one.ok ? one.result : { error: one.error }) + setNotice(summary.warnings.length ? `${summary.text} – ${summary.warnings.join(' – ')}` : summary.text) + } else { + const r = await unassignProject(project.$id) + if (!r.success) setNotice('Zuordnung fehlgeschlagen: ' + (r.error || 'unbekannt')) + } await fetchAllProjects() setAssignBusyId('') }