feat: bulk delete, team dispatch, calendar views, timeline colors
All checks were successful
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:
fahed
2026-03-01 14:55:36 +03:00
parent 20d76dea8b
commit 42a5f17d0b
40 changed files with 3050 additions and 1625 deletions

View File

@@ -10,6 +10,8 @@ import PostDetailPanel from '../components/PostDetailPanel'
import DatePresetPicker from '../components/DatePresetPicker'
import { SkeletonKanbanBoard, SkeletonTable } from '../components/SkeletonLoader'
import EmptyState from '../components/EmptyState'
import BulkSelectBar from '../components/BulkSelectBar'
import Modal from '../components/Modal'
import { useToast } from '../components/ToastContainer'
const EMPTY_POST = {
@@ -32,16 +34,18 @@ export default function PostProduction() {
const [searchTerm, setSearchTerm] = useState('')
const [activePreset, setActivePreset] = useState('')
const [moveError, setMoveError] = useState('')
const [selectedIds, setSelectedIds] = useState(new Set())
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false)
useEffect(() => {
loadPosts()
api.get('/campaigns').then(r => setCampaigns(Array.isArray(r) ? r : (r.data || []))).catch(() => {})
api.get('/campaigns').then(r => setCampaigns(Array.isArray(r) ? r : [])).catch(() => {})
}, [])
const loadPosts = async () => {
try {
const res = await api.get('/posts')
setPosts(res.data || res || [])
setPosts(Array.isArray(res) ? res : [])
} catch (err) {
console.error('Failed to load posts:', err)
} finally {
@@ -88,9 +92,36 @@ export default function PostProduction() {
}
}
const handleBulkDelete = async () => {
try {
await api.post('/posts/bulk-delete', { ids: [...selectedIds] })
toast.success(t('posts.deleted'))
setSelectedIds(new Set())
setShowBulkDeleteConfirm(false)
loadPosts()
} 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 === filteredPosts.length) setSelectedIds(new Set())
else setSelectedIds(new Set(filteredPosts.map(p => p._id || p.id || p.Id)))
}
const openEdit = (post) => {
if (!canEditResource('post', post)) {
alert('You can only edit your own posts')
toast.error(t('posts.canOnlyEditOwn'))
return
}
setPanelPost(post)
@@ -244,9 +275,18 @@ export default function PostProduction() {
}}
/>
) : (
<>
{selectedIds.size > 0 && (
<div className="px-4 pt-3">
<BulkSelectBar selectedCount={selectedIds.size} onDelete={() => setShowBulkDeleteConfirm(true)} onClear={() => setSelectedIds(new Set())} />
</div>
)}
<table className="w-full">
<thead>
<tr className="border-b border-border bg-surface-secondary">
<th className="w-10 px-3 py-3" onClick={e => e.stopPropagation()}>
<input type="checkbox" checked={selectedIds.size === filteredPosts.length && filteredPosts.length > 0} onChange={toggleSelectAll} className="rounded border-border" />
</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.postTitle')}</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.brand')}</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('posts.status')}</th>
@@ -256,15 +296,37 @@ export default function PostProduction() {
</tr>
</thead>
<tbody className="divide-y divide-border-light">
{filteredPosts.map(post => (
<PostCard key={post._id} post={post} onClick={() => openEdit(post)} />
))}
{filteredPosts.map(post => {
const postId = post._id || post.id || post.Id
return (
<PostCard
key={postId}
post={post}
onClick={() => openEdit(post)}
checkboxSlot={<input type="checkbox" checked={selectedIds.has(postId)} onChange={() => toggleSelect(postId)} className="rounded border-border" />}
/>
)
})}
</tbody>
</table>
</>
)}
</div>
)}
{/* Bulk Delete Confirmation */}
<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>
{/* Post Detail Panel */}
{panelPost && (
<PostDetailPanel