feat: bulk delete, team dispatch, calendar views, timeline colors
Deploy / deploy (push) Successful in 11s
Deploy / deploy (push) Successful in 11s
- Multi-select bulk delete in all 5 list views (Artefacts, Posts, Tasks, Issues, Assets) with cascade deletes and confirmation modals - Team-based issue dispatch: team picker on public issue form, team filter on Issues page, copy public link from Team page and Issues header, team assignment in IssueDetailPanel - Month/Week toggle on PostCalendar and TaskCalendarView - Month/Week/Day zoom on project and campaign timelines (InteractiveTimeline) and ProjectDetail GanttView, with Month as default - Custom timeline bar colors: clickable color dot with 12-color palette popover on project, campaign, and task timeline bars - Artefacts default view changed to list - BulkSelectBar reusable component - i18n keys for all new features (en + ar) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,8 @@ import { useLanguage } from '../i18n/LanguageContext'
|
||||
import { api, PRIORITY_CONFIG, getBrandColor } from '../utils/api'
|
||||
import TaskCard from '../components/TaskCard'
|
||||
import TaskDetailPanel from '../components/TaskDetailPanel'
|
||||
import BulkSelectBar from '../components/BulkSelectBar'
|
||||
import Modal from '../components/Modal'
|
||||
import TaskCalendarView from '../components/TaskCalendarView'
|
||||
import DatePresetPicker from '../components/DatePresetPicker'
|
||||
import EmptyState from '../components/EmptyState'
|
||||
@@ -45,6 +47,8 @@ export default function Tasks() {
|
||||
const [filterOverdue, setFilterOverdue] = useState(false)
|
||||
const [activePreset, setActivePreset] = useState('')
|
||||
const [showFilters, setShowFilters] = useState(false)
|
||||
const [selectedIds, setSelectedIds] = useState(new Set())
|
||||
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false)
|
||||
|
||||
// Assignable users & team
|
||||
const [assignableUsers, setAssignableUsers] = useState([])
|
||||
@@ -54,17 +58,17 @@ export default function Tasks() {
|
||||
|
||||
useEffect(() => { loadTasks() }, [currentUser])
|
||||
useEffect(() => {
|
||||
api.get('/users/assignable').then(res => setAssignableUsers(res.data || res || [])).catch(() => {})
|
||||
api.get('/projects').then(res => setProjects(res.data || res || [])).catch(() => {})
|
||||
api.get('/users/assignable').then(res => setAssignableUsers(Array.isArray(res) ? res : [])).catch(() => {})
|
||||
api.get('/projects').then(res => setProjects(Array.isArray(res) ? res : [])).catch(() => {})
|
||||
if (isSuperadmin) {
|
||||
api.get('/team').then(res => setUsers(res.data || res || [])).catch(() => {})
|
||||
api.get('/team').then(res => setUsers(Array.isArray(res) ? res : [])).catch(() => {})
|
||||
}
|
||||
}, [isSuperadmin])
|
||||
|
||||
const loadTasks = async () => {
|
||||
try {
|
||||
const res = await api.get('/tasks')
|
||||
setTasks(res.data || res || [])
|
||||
setTasks(Array.isArray(res) ? res : [])
|
||||
} catch (err) {
|
||||
console.error('Failed to load tasks:', err)
|
||||
} finally {
|
||||
@@ -177,6 +181,33 @@ export default function Tasks() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleBulkDelete = async () => {
|
||||
try {
|
||||
await api.post('/tasks/bulk-delete', { ids: [...selectedIds] })
|
||||
toast.success(t('tasks.deleted'))
|
||||
setSelectedIds(new Set())
|
||||
setShowBulkDeleteConfirm(false)
|
||||
loadTasks()
|
||||
} catch (err) {
|
||||
console.error('Bulk delete failed:', err)
|
||||
toast.error(t('common.deleteFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
const toggleSelect = (id) => {
|
||||
setSelectedIds(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(id)) next.delete(id)
|
||||
else next.add(id)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const toggleSelectAll = () => {
|
||||
if (selectedIds.size === sortedListTasks.length) setSelectedIds(new Set())
|
||||
else setSelectedIds(new Set(sortedListTasks.map(t => t._id || t.id)))
|
||||
}
|
||||
|
||||
const handleMove = async (taskId, newStatus) => {
|
||||
try {
|
||||
await api.patch(`/tasks/${taskId}`, { status: newStatus })
|
||||
@@ -594,10 +625,27 @@ export default function Tasks() {
|
||||
|
||||
{/* ─── List View ───────────────────────── */}
|
||||
{viewMode === 'list' && (
|
||||
<>
|
||||
{selectedIds.size > 0 && (
|
||||
<BulkSelectBar
|
||||
selectedCount={selectedIds.size}
|
||||
onClear={() => setSelectedIds(new Set())}
|
||||
onDelete={() => setShowBulkDeleteConfirm(true)}
|
||||
/>
|
||||
)}
|
||||
<div className="bg-white rounded-xl border border-border overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border bg-surface-secondary/50">
|
||||
<th className="w-8 px-3 py-2.5">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={sortedListTasks.length > 0 && selectedIds.size === sortedListTasks.length}
|
||||
onChange={toggleSelectAll}
|
||||
className="w-3.5 h-3.5 rounded border-border text-brand-primary cursor-pointer"
|
||||
onClick={e => e.stopPropagation()}
|
||||
/>
|
||||
</th>
|
||||
<th className="w-8 px-3 py-2.5"></th>
|
||||
<th
|
||||
className="text-left px-3 py-2.5 text-xs font-semibold text-text-tertiary uppercase cursor-pointer hover:text-text-primary"
|
||||
@@ -645,6 +693,14 @@ export default function Tasks() {
|
||||
onClick={() => openTask(task)}
|
||||
className="border-b border-border-light hover:bg-surface-secondary/30 cursor-pointer transition-colors group"
|
||||
>
|
||||
<td className="px-3 py-2.5" onClick={e => e.stopPropagation()}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedIds.has(task._id || task.id)}
|
||||
onChange={() => toggleSelect(task._id || task.id)}
|
||||
className="w-3.5 h-3.5 rounded border-border text-brand-primary cursor-pointer"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-3 py-2.5">
|
||||
<div className={`w-2.5 h-2.5 rounded-full ${priority.color}`} title={priority.label} />
|
||||
</td>
|
||||
@@ -686,6 +742,7 @@ export default function Tasks() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ─── Calendar View ───────────────────── */}
|
||||
@@ -695,6 +752,19 @@ export default function Tasks() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ─── Bulk Delete Confirmation Modal ─────── */}
|
||||
<Modal
|
||||
isOpen={showBulkDeleteConfirm}
|
||||
onClose={() => setShowBulkDeleteConfirm(false)}
|
||||
title={t('common.bulkDeleteConfirm').replace('{count}', selectedIds.size)}
|
||||
isConfirm
|
||||
danger
|
||||
confirmText={t('common.deleteSelected')}
|
||||
onConfirm={handleBulkDelete}
|
||||
>
|
||||
{t('common.bulkDeleteDesc')}
|
||||
</Modal>
|
||||
|
||||
{/* ─── Task Detail Side Panel ──────────────── */}
|
||||
{selectedTask && (
|
||||
<TaskDetailPanel
|
||||
|
||||
Reference in New Issue
Block a user