Init Repo
This commit is contained in:
@@ -0,0 +1,404 @@
|
||||
import type { ColumnDef } from '@tanstack/vue-table'
|
||||
import { h, ref } from 'vue'
|
||||
import { Camera, MoreHorizontal, Edit, ListTodo, Trash2, ArrowUpDown, ArrowUp, ArrowDown, ChevronDown } from 'lucide-vue-next'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
|
||||
import EditableTaskStatus from './EditableTaskStatus.vue'
|
||||
import TaskStatusBadge from '@/components/task/TaskStatusBadge.vue'
|
||||
import { type Shot, ShotStatus, TaskStatus } from '@/services/shot'
|
||||
|
||||
// Helper function to get the appropriate sort icon
|
||||
const getSortIcon = (sortDirection: false | 'asc' | 'desc') => {
|
||||
if (sortDirection === 'asc') {
|
||||
return h(ArrowDown, { class: 'ml-2 h-4 w-4' })
|
||||
} else if (sortDirection === 'desc') {
|
||||
return h(ArrowUp, { class: 'ml-2 h-4 w-4' })
|
||||
} else {
|
||||
return h(ArrowUpDown, { class: 'ml-2 h-4 w-4' })
|
||||
}
|
||||
}
|
||||
|
||||
export interface ShotColumnMeta {
|
||||
projectId: number
|
||||
episodes: Array<{ id: number; name: string }>
|
||||
onEdit: (shot: Shot) => void
|
||||
onDelete: (shot: Shot) => void
|
||||
onViewTasks: (shot: Shot) => void
|
||||
onTaskStatusUpdated: (shotId: number, taskType: string, newStatus: TaskStatus) => void
|
||||
onTaskAssignmentUpdated?: (shotId: number, taskType: string, userId: number | null) => void
|
||||
onBulkTaskStatusChange?: (taskType: string, status: TaskStatus) => void
|
||||
getSelectedCount?: () => number
|
||||
getAllStatusOptions?: () => Array<{ id: string; name: string; color?: string; is_system?: boolean }>
|
||||
}
|
||||
|
||||
export const createShotColumns = (
|
||||
allTaskTypes: string[],
|
||||
meta: ShotColumnMeta
|
||||
): ColumnDef<Shot>[] => {
|
||||
const columns: ColumnDef<Shot>[] = [
|
||||
// Select column
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) =>
|
||||
h(Checkbox, {
|
||||
modelValue: table.getIsAllPageRowsSelected(),
|
||||
'onUpdate:modelValue': (value: boolean | 'indeterminate') => table.toggleAllPageRowsSelected(value === true),
|
||||
ariaLabel: 'Select all',
|
||||
}),
|
||||
cell: ({ row }) =>
|
||||
h(Checkbox, {
|
||||
modelValue: row.getIsSelected(),
|
||||
'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(value === true),
|
||||
ariaLabel: 'Select row',
|
||||
onClick: (e: Event) => e.stopPropagation(),
|
||||
}),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
// Thumbnail column
|
||||
{
|
||||
id: 'thumbnail',
|
||||
header: 'Thumbnail',
|
||||
cell: () => {
|
||||
return h('div', { class: 'w-20 h-11 bg-muted flex items-center justify-center' }, [
|
||||
h(Camera, { class: 'h-6 w-6 text-muted-foreground' }),
|
||||
])
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
// Shot Name column
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: ({ column }) => {
|
||||
return h(
|
||||
// Button,
|
||||
// {
|
||||
// variant: 'ghost',
|
||||
// onClick: () => column.toggleSorting(column.getIsSorted() === 'desc'),
|
||||
// },
|
||||
// () => ['Shot Name', getSortIcon(column.getIsSorted())]
|
||||
'div', {class:'flex items-center justify-center'}, ['Shot Name', getSortIcon(column.getIsSorted())]
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const shot = row.original
|
||||
return h('div', { class: 'flex items-center gap-2' }, [
|
||||
// h(Camera, { class: 'h-4 w-4 text-muted-foreground flex-shrink-0' }),
|
||||
h('span', { class: 'font-medium' }, shot.name),
|
||||
])
|
||||
},
|
||||
},
|
||||
// Episode column
|
||||
{
|
||||
id: 'episode',
|
||||
accessorFn: (row) => {
|
||||
const episode = meta.episodes.find((e) => e.id === row.episode_id)
|
||||
return episode ? episode.name : `Episode ${row.episode_id}`
|
||||
},
|
||||
header: ({ column }) => {
|
||||
return h(
|
||||
'div', {class:'flex items-center justify-center'}, ['Episode', getSortIcon(column.getIsSorted())]
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const shot = row.original
|
||||
const episode = meta.episodes.find((e) => e.id === shot.episode_id)
|
||||
const episodeName = episode ? episode.name : `Episode ${shot.episode_id}`
|
||||
return h(Badge, { variant: 'outline', class: 'text-xs' }, () => episodeName)
|
||||
},
|
||||
},
|
||||
// Frame Range column (not sortable)
|
||||
{
|
||||
id: 'frameRange',
|
||||
header: 'Frame Range',
|
||||
cell: ({ row }) => {
|
||||
const shot = row.original
|
||||
return h('span', { class: 'text-sm' }, `${shot.frame_start}-${shot.frame_end}`)
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
// Frames column (frame count)
|
||||
{
|
||||
id: 'frames',
|
||||
accessorFn: (row) => row.frame_end - row.frame_start + 1,
|
||||
header: ({ column }) => {
|
||||
return h(
|
||||
'div', {class:'flex items-center justify-center'}, ['Frames', getSortIcon(column.getIsSorted())]
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const shot = row.original
|
||||
const frameCount = shot.frame_end - shot.frame_start + 1
|
||||
return h('span', { class: 'text-sm font-medium' }, frameCount.toString())
|
||||
},
|
||||
},
|
||||
// Status column
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: ({ column }) => {
|
||||
return h(
|
||||
'div', {class:'flex items-center justify-center'}, ['Status', getSortIcon(column.getIsSorted())]
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const shot = row.original
|
||||
const variant = getStatusVariant(shot.status)
|
||||
const label = formatStatus(shot.status)
|
||||
return h(Badge, { variant, class: 'text-xs' }, () => label)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// Add task status columns dynamically
|
||||
allTaskTypes.forEach((taskType) => {
|
||||
// Create ref for popover state for each task type
|
||||
const isPopoverOpen = ref(false)
|
||||
|
||||
columns.push({
|
||||
accessorKey: `task_status.${taskType}`,
|
||||
id: taskType,
|
||||
header: ({ column }) => {
|
||||
const selectedCount = meta.getSelectedCount?.() || 0
|
||||
|
||||
if (selectedCount > 0) {
|
||||
return h('div', { class: 'flex items-center gap-2' }, [
|
||||
h(
|
||||
'div',
|
||||
{ class: 'flex items-center justify-center' },
|
||||
[formatTaskType(taskType), getSortIcon(column.getIsSorted())]
|
||||
),
|
||||
h('div', { onClick: (e: Event) => e.stopPropagation() }, [
|
||||
h(Popover, {
|
||||
open: isPopoverOpen.value,
|
||||
'onUpdate:open': (value: boolean) => { isPopoverOpen.value = value }
|
||||
}, {
|
||||
default: () => [
|
||||
h(PopoverTrigger, {}, {
|
||||
default: () => h(
|
||||
Button,
|
||||
{
|
||||
variant: 'outline',
|
||||
size: 'sm',
|
||||
class: 'h-6 w-6 p-0',
|
||||
},
|
||||
() => h(ChevronDown, { class: 'h-3 w-3' })
|
||||
),
|
||||
}),
|
||||
h(PopoverContent, { class: 'w-48 p-2', align: 'start' }, {
|
||||
default: () => {
|
||||
// Get task statuses from meta
|
||||
const allStatusOptions = meta.getAllStatusOptions?.() || []
|
||||
|
||||
return h('div', { class: 'flex flex-col gap-1' }, [
|
||||
h('div', { class: 'px-2 py-1.5 text-sm font-semibold' }, `Change ${formatTaskType(taskType)} Status`),
|
||||
...allStatusOptions.map((statusOption) =>
|
||||
h(
|
||||
Button,
|
||||
{
|
||||
variant: 'ghost',
|
||||
size: 'sm',
|
||||
class: 'justify-start',
|
||||
onClick: () => {
|
||||
meta.onBulkTaskStatusChange?.(taskType, statusOption.id as TaskStatus)
|
||||
isPopoverOpen.value = false
|
||||
},
|
||||
},
|
||||
() => h(TaskStatusBadge, { status: statusOption, compact: true })
|
||||
)
|
||||
),
|
||||
])
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
]),
|
||||
])
|
||||
}
|
||||
|
||||
return h(
|
||||
'div',
|
||||
{ class: 'flex items-center justify-center' },
|
||||
[formatTaskType(taskType), getSortIcon(column.getIsSorted())]
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const shot = row.original
|
||||
const status = shot.task_status?.[taskType] || TaskStatus.NOT_STARTED
|
||||
const taskId = shot.task_ids?.[taskType]
|
||||
|
||||
// Get assigned user ID from task_details
|
||||
const taskDetail = shot.task_details?.find(detail => detail.task_type === taskType)
|
||||
const assignedUserId = taskDetail?.assigned_user_id || null
|
||||
|
||||
return h(EditableTaskStatus, {
|
||||
key: `${shot.id}-${taskType}`, // Add stable key to prevent unnecessary re-renders
|
||||
shotId: shot.id,
|
||||
taskType,
|
||||
status,
|
||||
taskId,
|
||||
projectId: meta.projectId,
|
||||
assignedUserId,
|
||||
onStatusUpdated: (shotId: number, taskType: string, newStatus: TaskStatus) => {
|
||||
meta.onTaskStatusUpdated(shotId, taskType, newStatus)
|
||||
},
|
||||
onAssignmentUpdated: (shotId: number, taskType: string, userId: number | null) => {
|
||||
meta.onTaskAssignmentUpdated?.(shotId, taskType, userId)
|
||||
},
|
||||
})
|
||||
},
|
||||
enableSorting: true,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
|
||||
// Actions column
|
||||
columns.push({
|
||||
id: 'actions',
|
||||
cell: ({ row }) => {
|
||||
const shot = row.original
|
||||
return h(
|
||||
DropdownMenu,
|
||||
{},
|
||||
{
|
||||
default: () => [
|
||||
h(
|
||||
DropdownMenuTrigger,
|
||||
{
|
||||
asChild: true
|
||||
},
|
||||
{
|
||||
default: () =>
|
||||
h(
|
||||
Button,
|
||||
{
|
||||
variant: 'ghost',
|
||||
size: 'sm',
|
||||
class: 'h-8 w-8 p-0',
|
||||
onMouseDown: (e: Event) => {
|
||||
e.stopPropagation()
|
||||
},
|
||||
onClick: (e: Event) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}
|
||||
},
|
||||
{
|
||||
default: () => h(MoreHorizontal, { class: 'h-4 w-4' }),
|
||||
}
|
||||
),
|
||||
}
|
||||
),
|
||||
h(
|
||||
DropdownMenuContent,
|
||||
{
|
||||
align: 'end'
|
||||
},
|
||||
{
|
||||
default: () => [
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
onClick: (e: Event) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
meta.onEdit(shot)
|
||||
}
|
||||
},
|
||||
{
|
||||
default: () => [
|
||||
h(Edit, { class: 'h-4 w-4 mr-2' }),
|
||||
'Edit Shot',
|
||||
],
|
||||
}
|
||||
),
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
onClick: (e: Event) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
meta.onViewTasks(shot)
|
||||
}
|
||||
},
|
||||
{
|
||||
default: () => [
|
||||
h(ListTodo, { class: 'h-4 w-4 mr-2' }),
|
||||
'View Tasks',
|
||||
],
|
||||
}
|
||||
),
|
||||
h(DropdownMenuSeparator),
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
onClick: (e: Event) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
meta.onDelete(shot)
|
||||
},
|
||||
class: 'text-destructive focus:text-destructive',
|
||||
},
|
||||
{
|
||||
default: () => [
|
||||
h(Trash2, { class: 'h-4 w-4 mr-2' }),
|
||||
'Delete Shot',
|
||||
],
|
||||
}
|
||||
),
|
||||
],
|
||||
}
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
||||
},
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
})
|
||||
|
||||
return columns
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function formatTaskType(taskType: string): string {
|
||||
return taskType.charAt(0).toUpperCase() + taskType.slice(1)
|
||||
}
|
||||
|
||||
function formatStatus(status: ShotStatus): string {
|
||||
return status
|
||||
.split('_')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
function getStatusVariant(status: ShotStatus): 'default' | 'secondary' | 'outline' {
|
||||
switch (status) {
|
||||
case ShotStatus.NOT_STARTED:
|
||||
return 'secondary'
|
||||
case ShotStatus.IN_PROGRESS:
|
||||
return 'default'
|
||||
case ShotStatus.ON_HOLD:
|
||||
return 'outline'
|
||||
case ShotStatus.COMPLETED:
|
||||
return 'default'
|
||||
case ShotStatus.APPROVED:
|
||||
return 'default'
|
||||
default:
|
||||
return 'secondary'
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user