feat: bulk delete, team dispatch, calendar views, timeline colors
All checks were successful
Deploy / deploy (push) Successful in 11s
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user