import { useEffect, useMemo, useState } from 'react' import { apiFetchJson, getItem, createLink } from '../../lib/workbench-data' import { Button } from '@core/components/ui/button' import { Input } from '@core/components/ui/input' import { Card, CardContent, CardHeader, CardTitle } from '@core/components/ui/card' import { Badge } from '@core/components/ui/badge' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@core/components/ui/select' const ADMIN_DATA = '/api/admin-data' const LINK_TYPE_OPTIONS = [ { value: 'workbench_parent', label: 'Parent' }, { value: 'workbench_depends_on', label: 'Depends on' }, { value: 'workbench_blocks', label: 'Blocks' }, { value: 'workbench_blocked_by', label: 'Blocked by' }, { value: 'workbench_related', label: 'Related' }, ] const LINK_LABELS: Record = Object.fromEntries(LINK_TYPE_OPTIONS.map(o => [o.value, o.label])) function normalizeList(json: any): any[] { return Array.isArray(json?.data) ? json.data : Array.isArray(json) ? json : [] } function LinkRow({ id, title, status, typeLabel }: { id?: string; title?: string; status?: string; typeLabel?: string }) { if (!id) return null return (
{typeLabel && {typeLabel}} {title || id} {status && {status}}
) } export function WorkItemLinks({ itemId }: { itemId: string }) { const [links, setLinks] = useState>({}) const [children, setChildren] = useState([]) const [relatedReverse, setRelatedReverse] = useState([]) const [records, setRecords] = useState>({}) const [newType, setNewType] = useState('workbench_parent') const [newTarget, setNewTarget] = useState('') const [search, setSearch] = useState('') const [searchResults, setSearchResults] = useState([]) const [searching, setSearching] = useState(false) const [adding, setAdding] = useState(false) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const allIds = useMemo(() => { const ids = new Set() Object.values(links).flat().forEach((l: any) => l?.target_id && ids.add(l.target_id)) children.forEach((l: any) => l?.source_id && ids.add(l.source_id)) relatedReverse.forEach((l: any) => l?.source_id && ids.add(l.source_id)) return Array.from(ids) }, [links, children, relatedReverse]) const loadRecord = async (id: string) => { if (records[id]) return try { const rec = await getItem(id) setRecords(prev => ({ ...prev, [id]: rec })) } catch { // ignore missing record } } const loadLinks = async () => { setLoading(true) setError(null) try { const [forward, reverseParent, reverseRelated] = await Promise.all([ apiFetchJson(`${ADMIN_DATA}?entity=links&source_id=${itemId}&source_type=items&limit=200`), apiFetchJson(`${ADMIN_DATA}?entity=links&target_id=${itemId}&target_type=items&link_type=workbench_parent&limit=100`), apiFetchJson(`${ADMIN_DATA}?entity=links&target_id=${itemId}&target_type=items&link_type=workbench_related&limit=100`), ]) const forwardLinks = normalizeList(forward) const grouped: Record = {} for (const l of forwardLinks) { const t = l.link_type || 'unknown' if (!grouped[t]) grouped[t] = [] grouped[t].push(l) } setLinks(grouped) setChildren(normalizeList(reverseParent)) setRelatedReverse(normalizeList(reverseRelated)) } catch (err: any) { setError(err.message || 'Failed to load links') } finally { setLoading(false) } } useEffect(() => { loadLinks() }, [itemId]) useEffect(() => { for (const id of allIds) loadRecord(id) }, [allIds]) useEffect(() => { if (!search.trim() || search.length < 2) { setSearchResults([]) return } const timeout = setTimeout(() => { setSearching(true) apiFetchJson(`${ADMIN_DATA}?entity=items&search=${encodeURIComponent(search)}&limit=20`) .then(json => { const results = normalizeList(json).filter((r: any) => r.id !== itemId) setSearchResults(results) }) .catch(() => setSearchResults([])) .finally(() => setSearching(false)) }, 300) return () => clearTimeout(timeout) }, [search, itemId]) const handleAdd = async (e: React.FormEvent) => { e.preventDefault() if (!newTarget.trim()) return setAdding(true) setError(null) try { await createLink(itemId, newTarget.trim(), newType) setNewTarget('') setSearch('') setSearchResults([]) await loadLinks() } catch (err: any) { setError(err.message || 'Failed to add link') } finally { setAdding(false) } } const renderSection = (label: string, linkType: string, reverse?: boolean) => { const list = reverse ? (linkType === 'workbench_parent' ? children : relatedReverse) : (links[linkType] || []) if (!list.length) return null return (
{label}
{list.map((l: any, idx: number) => { const id = reverse ? l.source_id : l.target_id const rec = records[id] return })}
) } return ( Links {loading &&
Loading links…
} {error &&
{error}
} {renderSection('Parent', 'workbench_parent')} {renderSection('Children', 'workbench_parent', true)} {renderSection('Depends on', 'workbench_depends_on')} {renderSection('Blocks', 'workbench_blocks')} {renderSection('Blocked by', 'workbench_blocked_by')} {renderSection('Related', 'workbench_related')} {renderSection('Related to', 'workbench_related', true)}
Add link
{ setSearch(e.target.value) setNewTarget('') }} placeholder="Search by title" className="h-8 text-xs" /> {searching &&
Searching…
} {!newTarget && searchResults.length > 0 && (
{searchResults.map((r: any) => ( ))}
)}
) }