Session 2026-05-31: tasks table overhaul, FileBrowser redesign, folder path fix

- Tasks table: added R#, Assigned To (avatar), Priority (HOT/NO) columns; removed Status column; sortable columns; adjustable widths
- FileBrowser: full visual rewrite to match FileSharing page (table layout, colored ext badges, SVG folder icon, sortable columns, multi-select, context menu, drag-drop)
- TaskDetail folder tab: fix path with safeName(), rootPath set to project folder level
- filebrowserFolders: export safeName for reuse

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Krao Hasanee
2026-05-31 11:02:40 -04:00
parent 0b4705311b
commit 5b3060e190
12 changed files with 724 additions and 2393 deletions
+66 -21
View File
@@ -210,6 +210,8 @@ export default function RequestsPage() {
const [addProjectForm, setAddProjectForm] = useState({ name: '', companyId: '' });
const [companyFilterMenuOpen, setCompanyFilterMenuOpen] = useState(false);
const companyFilterMenuRef = useRef(null);
const [projectFilterMenuOpen, setProjectFilterMenuOpen] = useState(false);
const projectFilterMenuRef = useRef(null);
useEffect(() => {
if (!companyFilterMenuOpen) return;
@@ -220,6 +222,15 @@ export default function RequestsPage() {
return () => document.removeEventListener('mousedown', onDocClick);
}, [companyFilterMenuOpen]);
useEffect(() => {
if (!projectFilterMenuOpen) return;
function onDocClick(e) {
if (projectFilterMenuRef.current && !projectFilterMenuRef.current.contains(e.target)) setProjectFilterMenuOpen(false);
}
document.addEventListener('mousedown', onDocClick);
return () => document.removeEventListener('mousedown', onDocClick);
}, [projectFilterMenuOpen]);
useEffect(() => {
let cancelled = false;
async function loadTasksPage() {
@@ -260,7 +271,7 @@ export default function RequestsPage() {
}
const tasksQ = supabase.from('tasks')
.select('id, title, status, current_version, project_id, assigned_name, assigned_to, invoiced, completed_at, submitted_at')
.select('id, title, status, current_version, project_id, assigned_name, assigned_to, invoiced, completed_at, submitted_at, assignee:profiles!assigned_to(avatar_url)')
.order('submitted_at', { ascending: false });
if (!isTeam) {
if ((scopedProjectIds || []).length > 0) tasksQ.in('project_id', scopedProjectIds);
@@ -430,6 +441,7 @@ export default function RequestsPage() {
id: task.id, title: task.title, status: task.status, projectId: task.project_id,
serviceType: sub?.service_type || '—', deadline: sub?.deadline || null,
version: task.current_version || 0, isHot: false,
assignedName: task.assigned_name || null, assigneeAvatar: task.assignee?.avatar_url || null,
submittedAt: task.submitted_at ? new Date(task.submitted_at).getTime() : 0,
};
});
@@ -448,6 +460,7 @@ export default function RequestsPage() {
serviceType, deadline: deadlineSource.deadline,
version: deadlineSource.version_number ?? 0,
isHot: deadlineSource.is_hot || false,
assignedName: task.assigned_name || null, assigneeAvatar: task.assignee?.avatar_url || null,
submittedAt: latestGroup.length > 0
? Math.max(...latestGroup.map(s => new Date(s.submitted_at).getTime()))
: (task.submitted_at ? new Date(task.submitted_at).getTime() : 0),
@@ -489,6 +502,8 @@ export default function RequestsPage() {
if (key === 'deadline') return row.deadline || '';
if (key === 'status') return TASK_STATUS_SORT_RANK[row.status] ?? 99;
if (key === 'submitted_at') return row.submittedAt || 0;
if (key === 'assigned') return row.assignedName || '';
if (key === 'priority') return row.isHot ? 0 : 1;
return '';
});
@@ -509,26 +524,36 @@ export default function RequestsPage() {
const renderRow = (row) => {
const project = projects.find(p => p.id === row.projectId);
const initials = row.assignedName ? row.assignedName.split(' ').map(n => n[0]).join('').slice(0, 2).toUpperCase() : null;
return (
<tr key={row.id}>
<td style={{ ...TASK_TABLE_TD_BASE, fontSize: 12, color: 'var(--text-primary)', fontVariantNumeric: 'tabular-nums' }}>
<Link to={`/tasks/${row.id}`} className="table-link">{`R${String(row.version).padStart(2, '0')}`}</Link>
</td>
<td style={{ ...TASK_TABLE_TD_BASE, fontSize: 13, color: 'var(--text-primary)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
<Link to={`/requests/${row.id}/v2`} className="table-link">{row.title || '—'}</Link>
<span style={{ color: 'var(--text-muted)' }}>{`R${String(row.version).padStart(2, '0')}`}</span>
{row.isHot && <span className="badge badge-needs_revision">HOT</span>}
</div>
<Link to={`/tasks/${row.id}`} className="table-link">{row.title || '—'}</Link>
</td>
<td style={{ ...TASK_TABLE_TD_BASE, fontSize: 13, color: 'var(--text-primary)' }}>
{project ? <Link to={`/projects/${project.id}`} className="table-link">{project.name}</Link> : '—'}
</td>
<td style={{ ...TASK_TABLE_TD_BASE, fontSize: 12, color: 'var(--text-primary)', textAlign: 'center' }}>
<Link to={`/requests/${row.id}/v2`} className="table-link">{row.serviceType}</Link>
</td>
<td style={{ ...TASK_TABLE_TD_BASE, fontSize: 12, color: 'var(--text-primary)', textAlign: 'center' }}>
<Link to={`/requests/${row.id}/v2`} className="table-link">{fmtShortDate(row.deadline, 'Not specified')}</Link>
</td>
<td style={{ ...TASK_TABLE_TD_BASE, textAlign: 'center' }}>
<Link to={`/requests/${row.id}/v2`} className="table-link"><StatusBadge status={row.status || 'not_started'} /></Link>
{(() => {
const avatarStyle = { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 26, height: 26, borderRadius: '50%', overflow: 'hidden', verticalAlign: 'middle', flexShrink: 0 };
if (row.assignedName && row.assigneeAvatar)
return <div title={row.assignedName} style={avatarStyle}><img src={row.assigneeAvatar} alt={row.assignedName} style={{ width: '100%', height: '100%', objectFit: 'cover' }} /></div>;
if (row.assignedName)
return <div title={row.assignedName} style={{ ...avatarStyle, background: 'var(--accent)' }}><span style={{ fontSize: 10, fontWeight: 600, color: '#000', lineHeight: 1 }}>{initials}</span></div>;
return <div title="Unassigned" style={{ ...avatarStyle, background: 'rgba(255,255,255,0.08)' }}><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.3)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/></svg></div>;
})()}
</td>
<td style={{ ...TASK_TABLE_TD_BASE, fontSize: 12, textAlign: 'center' }}>
<Link to={`/tasks/${row.id}`} className="table-link" style={{ color: row.isHot ? '#ef4444' : 'var(--text-primary)', textTransform: 'uppercase', fontSize: 11, letterSpacing: 0.5 }}>{row.isHot ? 'HOT' : 'NO'}</Link>
</td>
<td style={{ ...TASK_TABLE_TD_BASE, fontSize: 12, color: 'var(--text-primary)', textAlign: 'center' }}>
<Link to={`/tasks/${row.id}`} className="table-link">{row.serviceType}</Link>
</td>
<td style={{ ...TASK_TABLE_TD_BASE, fontSize: 12, color: 'var(--text-primary)', textAlign: 'center' }}>
<Link to={`/tasks/${row.id}`} className="table-link">{fmtShortDate(row.deadline, 'Not specified')}</Link>
</td>
</tr>
);
@@ -611,10 +636,26 @@ export default function RequestsPage() {
))}
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 8 }} ref={companyFilterMenuRef}>
{isExternal && projectOptions.length > 0 && (
<select className="filter-select" style={{ width: 120 }} value={filterProject} onChange={e => setFilterProject(e.target.value)}>
<option value="">All Projects</option>
{projectOptions.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
<div style={{ position: 'relative' }} ref={projectFilterMenuRef}>
<button
className="btn btn-outline"
aria-label="Filter projects" title="Filter projects"
onClick={() => setProjectFilterMenuOpen(o => !o)}
style={{ width: 30, minWidth: 30, padding: 0, display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}
>
<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M2 4h12M4 8h8M6 12h4" />
</svg>
</button>
{projectFilterMenuOpen && (
<div className="site-header-avatar-menu" style={{ position: 'absolute', top: 'calc(100% + 4px)', right: 0, zIndex: 220, minWidth: 170 }}>
<button onClick={() => { setFilterProject(''); setProjectFilterMenuOpen(false); }} className="site-header-avatar-item" style={{ background: !filterProject ? 'rgba(245,165,35,0.08)' : 'transparent', color: !filterProject ? 'var(--accent)' : 'var(--text-primary)' }}>All Projects</button>
{projectOptions.map(p => (
<button key={p.id} onClick={() => { setFilterProject(p.id); setProjectFilterMenuOpen(false); }} className="site-header-avatar-item" style={{ background: filterProject === p.id ? 'rgba(245,165,35,0.08)' : 'transparent', color: filterProject === p.id ? 'var(--accent)' : 'var(--text-primary)' }}>{p.name}</button>
))}
</div>
)}
</div>
)}
{(isTeam || isClient) && (
<div style={{ position: 'relative' }}>
@@ -663,19 +704,23 @@ export default function RequestsPage() {
<div className="scrollbar-thin-theme table-scroll-fade table-scroll-hidden" style={{ flex: 1, minHeight: 0, overflowY: 'auto' }}>
<table className="table-sticky-head" style={{ tableLayout: 'fixed', borderCollapse: 'collapse', width: '100%' }}>
<colgroup>
<col style={{ width: '28%' }} />
<col style={{ width: '5%' }} />
<col style={{ width: '25%' }} />
<col style={{ width: '18%' }} />
<col style={{ width: '15%' }} />
<col style={{ width: '7%' }} />
<col style={{ width: '15%' }} />
<col style={{ width: '15%' }} />
<col style={{ width: '12%' }} />
<col style={{ width: '20%' }} />
</colgroup>
<thead>
<tr>
<SortTh col="revision" sortKey={sortKey} sortDir={sortDir} onSort={toggle} style={TASK_TABLE_TH_STYLE}>R#</SortTh>
<SortTh col="title" sortKey={sortKey} sortDir={sortDir} onSort={toggle} style={TASK_TABLE_TH_STYLE}>Name</SortTh>
<SortTh col="project" sortKey={sortKey} sortDir={sortDir} onSort={toggle} style={TASK_TABLE_TH_STYLE}>Project</SortTh>
<SortTh col="assigned" sortKey={sortKey} sortDir={sortDir} onSort={toggle} style={{ ...TASK_TABLE_TH_STYLE, textAlign: 'center' }}>Assigned</SortTh>
<SortTh col="priority" sortKey={sortKey} sortDir={sortDir} onSort={toggle} style={{ ...TASK_TABLE_TH_STYLE, textAlign: 'center' }}>Priority</SortTh>
<SortTh col="serviceType" sortKey={sortKey} sortDir={sortDir} onSort={toggle} style={{ ...TASK_TABLE_TH_STYLE, textAlign: 'center' }}>Task Type</SortTh>
<SortTh col="deadline" sortKey={sortKey} sortDir={sortDir} onSort={toggle} style={{ ...TASK_TABLE_TH_STYLE, textAlign: 'center' }}>Deadline</SortTh>
<SortTh col="status" sortKey={sortKey} sortDir={sortDir} onSort={toggle} style={{ ...TASK_TABLE_TH_STYLE, textAlign: 'center' }}>Status</SortTh>
</tr>
</thead>
<tbody>{sortedRows.map(renderRow)}</tbody>