Add admin issue deletion workflow
This commit is contained in:
@@ -8,6 +8,7 @@ type PortalPermissions = {
|
||||
can_edit?: boolean
|
||||
can_comment?: boolean
|
||||
can_moderate?: boolean
|
||||
can_delete?: boolean
|
||||
can_confirm_resolution?: boolean
|
||||
}
|
||||
|
||||
@@ -433,6 +434,8 @@ export default function PortalClient({ workspace }: PortalClientProps) {
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [commenting, setCommenting] = useState(false)
|
||||
const [respondingResolution, setRespondingResolution] = useState(false)
|
||||
const [deleteConfirming, setDeleteConfirming] = useState(false)
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [status, setStatus] = useState<string | null>(null)
|
||||
const [totalItems, setTotalItems] = useState(0)
|
||||
@@ -1382,14 +1385,49 @@ export default function PortalClient({ workspace }: PortalClientProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const deleteIssue = async () => {
|
||||
if (selectedItem?.kind !== 'issue' || !selectedItem.permissions?.can_delete) return
|
||||
setDeleting(true)
|
||||
setError(null)
|
||||
setStatus(null)
|
||||
const issueId = selectedItem.id
|
||||
try {
|
||||
const response = await authFetch(`${getApiBase()}/portal/items/${issueId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
clearToken()
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
const payload = await response.json().catch(() => null)
|
||||
throw new Error(payload?.detail || `Could not delete issue (${response.status})`)
|
||||
}
|
||||
closeIssueModal()
|
||||
setStatus(`Issue #${issueId} was deleted. The linked media request was not changed.`)
|
||||
await Promise.all([loadItems(), loadOverview()])
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setError(err instanceof Error ? err.message : 'Could not delete the issue.')
|
||||
} finally {
|
||||
setDeleting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const closeIssueModal = () => {
|
||||
setSelectedItemId(null)
|
||||
setSelectedItem(null)
|
||||
setComments([])
|
||||
setActivity([])
|
||||
setCommentText('')
|
||||
setDeleteConfirming(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setDeleteConfirming(false)
|
||||
}, [selectedItemId])
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace !== 'issue' || selectedItemId == null) return
|
||||
const previousOverflow = document.body.style.overflow
|
||||
@@ -2098,9 +2136,21 @@ export default function PortalClient({ workspace }: PortalClientProps) {
|
||||
{selectedItem ? `Issue #${selectedItem.id}` : 'Issue details'}
|
||||
</strong>
|
||||
</div>
|
||||
<button type="button" className="ghost-button" onClick={closeIssueModal}>
|
||||
Close
|
||||
</button>
|
||||
<div className="issue-modal-toolbar-actions">
|
||||
{selectedItem?.permissions?.can_delete ? (
|
||||
<button
|
||||
type="button"
|
||||
className="danger-button"
|
||||
disabled={deleting}
|
||||
onClick={() => setDeleteConfirming(true)}
|
||||
>
|
||||
Delete issue
|
||||
</button>
|
||||
) : null}
|
||||
<button type="button" className="ghost-button" disabled={deleting} onClick={closeIssueModal}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{!selectedItemId ? (
|
||||
@@ -2145,6 +2195,31 @@ export default function PortalClient({ workspace }: PortalClientProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedItem.kind === 'issue' && deleteConfirming ? (
|
||||
<section className="issue-delete-confirmation" aria-live="polite">
|
||||
<div>
|
||||
<span className="section-kicker">Permanent deletion</span>
|
||||
<h3>Delete issue #{selectedItem.id}?</h3>
|
||||
<p>
|
||||
This removes the issue, its comments, and its activity history. The linked media request and collected content will not be changed.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-button"
|
||||
disabled={deleting}
|
||||
onClick={() => setDeleteConfirming(false)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" className="danger-button" disabled={deleting} onClick={() => void deleteIssue()}>
|
||||
{deleting ? 'Deleting…' : 'Delete permanently'}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{selectedItem.kind === 'issue' ? <IssuePipeline item={selectedItem} /> : null}
|
||||
|
||||
{selectedItem.kind === 'issue' && selectedItem.external_ref?.startsWith('/requests/') ? (
|
||||
|
||||
Reference in New Issue
Block a user