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 && (