Projekte: Projektabschnitt am Ticket als Abschnitt, Art-Frage als Dialog
Der Abschnitt nutzt Card, Button und Dialog statt Altklassen und Icons. TicketProjects laedt nur noch bei einer anderen Ticket-ID neu (load haengt an ticket.$id, der neueste Ticketstand kommt ueber ein ref), damit "Suche neue Repos ..." bei Status-, Zustaendig- oder Dringend-Aenderung nicht erneut blinkt. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
47cf82eb91
commit
f1aac2ada9
1 file changed
+43
-39
@@ -1,5 +1,4 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { FaPlus, FaLink } from 'react-icons/fa6'
|
||||
import { Fragment, useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { useWebsiteProjects } from '../hooks/useWebsiteProjects'
|
||||
import { createProjectFromTemplate, importNewReposThrottled } from '../lib/projectAdminApi'
|
||||
import { isOutsideOrg, assignSummary, assignableProjects } from '../lib/projectAssign'
|
||||
@@ -7,6 +6,7 @@ import { slugify } from '../lib/subdomain'
|
||||
import AssignedProjectCard from './AssignedProjectCard'
|
||||
import { fetchServices, completeAssignment } from '../lib/projectKindsApi'
|
||||
import { kindLabelFor } from '../lib/assignFlow'
|
||||
import { Card, Button, Dialog } from './ui'
|
||||
|
||||
// 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.
|
||||
@@ -29,12 +29,18 @@ export default function TicketProjects({ ticket }) {
|
||||
fetchServices().then((s) => setKinds(s.kinds || [])).catch(() => {})
|
||||
}, [])
|
||||
|
||||
// Neu laden (samt "Suche neue Repos ...") nur bei einem anderen Ticket, nicht bei Status, Zustaendig oder Dringend:
|
||||
// load haengt an der Ticket-ID, den jeweils neuesten Ticketstand (Kunde) liest es ueber ticketRef
|
||||
const ticketRef = useRef(ticket)
|
||||
ticketRef.current = ticket
|
||||
const ticketId = ticket?.$id
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!ticket?.$id) return
|
||||
const [all, mine] = await Promise.all([fetchAllProjects(), fetchByTicketId(ticket.$id)])
|
||||
if (!ticketId) return
|
||||
const [all, mine] = await Promise.all([fetchAllProjects(), fetchByTicketId(ticketId)])
|
||||
setAssigned(mine)
|
||||
setAvailable(assignableProjects(all, ticket, new Set(mine.map((p) => p.$id))))
|
||||
}, [ticket, fetchAllProjects, fetchByTicketId])
|
||||
setAvailable(assignableProjects(all, ticketRef.current, new Set(mine.map((p) => p.$id))))
|
||||
}, [ticketId, fetchAllProjects, fetchByTicketId])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
@@ -156,16 +162,15 @@ export default function TicketProjects({ ticket }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ui-card pad ticket-projects">
|
||||
<h5 style={{ fontWeight: 700, marginBottom: 12 }}>Projekte ({assigned.length})</h5>
|
||||
{importing && <p className="muted" style={{ marginTop: 0 }}>Suche neue Repos …</p>}
|
||||
{importHint && <div className="text-red" style={{ marginBottom: 8 }}>{importHint}</div>}
|
||||
{error && <div className="text-red" style={{ marginBottom: 8 }}>{error}</div>}
|
||||
<Card title={`Projekte (${assigned.length})`}>
|
||||
{importing && <p className="muted">Suche neue Repos …</p>}
|
||||
{importHint && <p className="msg err" role="alert">{importHint}</p>}
|
||||
{error && <p className="msg err" role="alert">{error}</p>}
|
||||
{assigned.length === 0
|
||||
? <p className="muted" style={{ marginTop: 0 }}>Noch kein Projekt an diesem Ticket.</p>
|
||||
? <p className="muted">Noch kein Projekt an diesem Ticket.</p>
|
||||
: assigned.map((p) => (
|
||||
<div key={p.$id}>
|
||||
<span className="muted" style={{ fontSize: 12 }} data-testid="project-kind">{kindLabelFor(kinds, p.kind)}</span>
|
||||
<span className="muted xs" data-testid="project-kind">{kindLabelFor(kinds, p.kind)}</span>
|
||||
<AssignedProjectCard
|
||||
project={p}
|
||||
showActions
|
||||
@@ -177,7 +182,7 @@ export default function TicketProjects({ ticket }) {
|
||||
))}
|
||||
|
||||
<div className="ticket-projects-assign">
|
||||
<select className="form-control" aria-label="Projekt zuweisen" value={selected} onChange={(e) => setSelected(e.target.value)}>
|
||||
<select className="select" 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}>
|
||||
@@ -186,44 +191,43 @@ export default function TicketProjects({ ticket }) {
|
||||
</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>
|
||||
<Button type="button" disabled={!selected || busy} onClick={assign}>Zuweisen</Button>
|
||||
<Button variant="quiet" type="button" onClick={() => setShowCreate((s) => !s)}>Neues Projekt</Button>
|
||||
</div>
|
||||
|
||||
{assignResult && (
|
||||
<div style={{ marginTop: 8, fontSize: 13 }}>
|
||||
<div>{assignResult.text}</div>
|
||||
<p className="msg">
|
||||
{assignResult.text}
|
||||
{assignResult.warnings.map((w, i) => (
|
||||
<div key={i} className="muted" style={{ marginLeft: 12 }}>{w}</div>
|
||||
<Fragment key={i}>
|
||||
<br />
|
||||
<span className="muted">{w}</span>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{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 className="ticket-projects-assign" onSubmit={create}>
|
||||
<input className="input" aria-label="Anzeigename" placeholder="Anzeigename" value={createForm.displayName} onChange={(e) => setCreateForm((f) => ({ ...f, displayName: e.target.value }))} required />
|
||||
<input className="input" aria-label="Subdomain" placeholder="Subdomain" value={createForm.subdomain} onChange={(e) => setCreateForm((f) => ({ ...f, subdomain: e.target.value }))} required />
|
||||
<Button type="submit" variant="primary" disabled={busy}>{busy ? 'Wird angelegt …' : 'Anlegen & zuweisen'}</Button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{kindDialog && (
|
||||
<div className="overlay">
|
||||
<span className="overlay-close" onClick={() => setKindDialog(false)}>×</span>
|
||||
<div className="overlay-content">
|
||||
<h2 className="mb-2">Was ist das?</h2>
|
||||
<p className="muted">Bitte wähle die Art des Projekts, bevor es zugewiesen wird.</p>
|
||||
{kinds.length === 0 && (
|
||||
<div className="text-red" style={{ marginBottom: 8 }}>Arten konnten nicht geladen werden – bitte neu laden.</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{kinds.map((k) => (
|
||||
<button key={k.key} type="button" className="ui-btn" onClick={() => assignWithKind(k.key)}>{k.label}</button>
|
||||
))}
|
||||
</div>
|
||||
<Dialog title="Was ist das?" onClose={() => setKindDialog(false)}>
|
||||
<p className="muted">Bitte wähle die Art des Projekts, bevor es zugewiesen wird.</p>
|
||||
{kinds.length === 0 && (
|
||||
<p className="msg err">Arten konnten nicht geladen werden – bitte neu laden.</p>
|
||||
)}
|
||||
<div className="row-buttons">
|
||||
{kinds.map((k) => (
|
||||
<Button key={k.key} type="button" onClick={() => assignWithKind(k.key)}>{k.label}</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user