Projekte im Ticket als eine Liste (Paket E)
- Im Details-Reiter steht eine Projektliste mit Zuweisen, Lösen und "Neues Projekt"; der Knopf "Projekte zuweisen / bearbeiten", der Dialog und die zweite Projektliste sind weg. - Zuweisen bietet nur passende Projekte an: gleicher Kunde oder ohne Kunde, nicht archiviert, keine Vorlage. - Lösen nimmt ein Projekt nur noch vom Ticket. Bisher wurde dabei auch die Kundenzuordnung gelöscht und der Kunde sah seine Vorschau im Portal nicht mehr. - Projektkarten ohne README-Vorschau (kostete je Karte einen Abruf) und ohne nie befüllte Felder (Live-Domain, Commit-Box, "Preview: aktiv"); react-markdown entfällt damit (Lockfile mit Node 20 erzeugt). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
92fdfdc961
commit
50d237a932
11 files changed
+137
-1569
No files matched your search
@@ -0,0 +1,112 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { FaPlus, FaLink } from 'react-icons/fa6'
|
||||
import { useWebsiteProjects } from '../hooks/useWebsiteProjects'
|
||||
import { createProjectFromTemplate } from '../lib/projectAdminApi'
|
||||
import AssignedProjectCard from './AssignedProjectCard'
|
||||
|
||||
function slugify(v) {
|
||||
return String(v || '').toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 100)
|
||||
}
|
||||
|
||||
// Zuordnen lassen sich echte Repos des gleichen Kunden oder ohne Kunden, die nicht
|
||||
// archiviert, keine Vorlage und noch nicht an diesem Ticket sind
|
||||
function assignableProjects(all, ticket, assignedIds) {
|
||||
return (all || []).filter((p) =>
|
||||
p.repoFullName &&
|
||||
!assignedIds.has(p.$id) &&
|
||||
p.status !== 'archived' &&
|
||||
!p.isTemplate &&
|
||||
(!p.customerId || p.customerId === ticket.customerId)
|
||||
)
|
||||
}
|
||||
|
||||
// Projekte am Ticket: Liste, Zuweisen, Loesen und neues Projekt an einer Stelle.
|
||||
// Loesen nimmt das Projekt nur vom Ticket; die Kundenzuordnung (Sichtbarkeit im Portal) bleibt.
|
||||
export default function TicketProjects({ ticket }) {
|
||||
const { fetchAllProjects, fetchByTicketId, assignProjects, unassignProject } = useWebsiteProjects()
|
||||
const [assigned, setAssigned] = useState([])
|
||||
const [available, setAvailable] = useState([])
|
||||
const [selected, setSelected] = useState('')
|
||||
const [showCreate, setShowCreate] = useState(false)
|
||||
const [createForm, setCreateForm] = useState({ displayName: '', subdomain: '' })
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!ticket?.$id) return
|
||||
const [all, mine] = await Promise.all([fetchAllProjects(), fetchByTicketId(ticket.$id)])
|
||||
setAssigned(mine)
|
||||
setAvailable(assignableProjects(all, ticket, new Set(mine.map((p) => p.$id))))
|
||||
}, [ticket, fetchAllProjects, fetchByTicketId])
|
||||
|
||||
useEffect(() => { load() }, [load])
|
||||
|
||||
const run = async (action) => {
|
||||
setBusy(true)
|
||||
setError('')
|
||||
try {
|
||||
await action()
|
||||
await load()
|
||||
} catch (err) {
|
||||
setError(err.message || 'Aktion fehlgeschlagen')
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
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 unassign = (projectId) => run(async () => {
|
||||
const r = await unassignProject(projectId)
|
||||
if (!r.success) throw new Error(r.error)
|
||||
})
|
||||
|
||||
const create = (e) => {
|
||||
e.preventDefault()
|
||||
run(async () => {
|
||||
const subdomain = slugify(createForm.subdomain)
|
||||
await createProjectFromTemplate({
|
||||
repoName: subdomain,
|
||||
displayName: createForm.displayName,
|
||||
subdomain,
|
||||
customerId: ticket.customerId || '',
|
||||
ticketId: ticket.$id,
|
||||
})
|
||||
setCreateForm({ displayName: '', subdomain: '' })
|
||||
setShowCreate(false)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ui-card pad ticket-projects">
|
||||
<h5 style={{ fontWeight: 700, marginBottom: 12 }}>Projekte ({assigned.length})</h5>
|
||||
{error && <div className="text-red" style={{ marginBottom: 8 }}>{error}</div>}
|
||||
{assigned.length === 0
|
||||
? <p className="muted" style={{ marginTop: 0 }}>Noch kein Projekt an diesem Ticket.</p>
|
||||
: assigned.map((p) => <AssignedProjectCard key={p.$id} project={p} showActions onUnassign={unassign} />)}
|
||||
|
||||
<div className="ticket-projects-assign">
|
||||
<select className="form-control" aria-label="Projekt zuweisen" value={selected} onChange={(e) => setSelected(e.target.value)}>
|
||||
<option value="">Projekt wählen …</option>
|
||||
{available.map((p) => (
|
||||
<option key={p.$id} value={p.$id}>{p.projectName} ({p.subdomain || p.repoFullName})</option>
|
||||
))}
|
||||
</select>
|
||||
<button type="button" className="ui-btn" disabled={!selected || busy} onClick={assign}><FaLink /> Zuweisen</button>
|
||||
<button type="button" className="ui-btn ui-btn-ghost" onClick={() => setShowCreate((s) => !s)}><FaPlus /> Neues Projekt</button>
|
||||
</div>
|
||||
|
||||
{showCreate && (
|
||||
<form className="ticket-projects-assign" onSubmit={create} style={{ marginTop: 8 }}>
|
||||
<input className="form-control" placeholder="Anzeigename" value={createForm.displayName} onChange={(e) => setCreateForm((f) => ({ ...f, displayName: e.target.value }))} required />
|
||||
<input className="form-control" placeholder="Subdomain" value={createForm.subdomain} onChange={(e) => setCreateForm((f) => ({ ...f, subdomain: e.target.value }))} required />
|
||||
<button type="submit" className="ui-btn ui-btn-primary" disabled={busy}>{busy ? 'Wird angelegt …' : 'Anlegen & zuweisen'}</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user