Init Repo
This commit is contained in:
@@ -0,0 +1,372 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<!-- Main Toolbar Row -->
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<!-- Left Side - Filters -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<!-- View Toggle -->
|
||||
<div class="flex items-center border rounded-md h-8 p-0.5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:class="{ 'bg-muted': viewMode === 'grid' }"
|
||||
@click="$emit('update:view-mode', 'grid')"
|
||||
class="h-7 px-2"
|
||||
>
|
||||
<LayoutGrid class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:class="{ 'bg-muted': viewMode === 'list' }"
|
||||
@click="$emit('update:view-mode', 'list')"
|
||||
class="h-7 px-2"
|
||||
>
|
||||
<List class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:class="{ 'bg-muted': viewMode === 'table' }"
|
||||
@click="$emit('update:view-mode', 'table')"
|
||||
class="h-7 px-2"
|
||||
>
|
||||
<Table2 class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Episode Filter -->
|
||||
<Popover v-if="episodes.length > 0">
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="outline" size="sm" class="h-8 border-dashed">
|
||||
<Film class="mr-2 h-4 w-4" />
|
||||
Episode
|
||||
<Badge
|
||||
v-if="episodeFilter !== null"
|
||||
variant="secondary"
|
||||
class="ml-2 rounded-sm px-1 font-normal"
|
||||
>
|
||||
1
|
||||
</Badge>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-[200px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search episode..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No episode found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
value="all"
|
||||
@select="$emit('update:episode-filter', null)"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
||||
episodeFilter === null
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'opacity-50 [&_svg]:invisible'
|
||||
]"
|
||||
>
|
||||
<Check class="h-4 w-4" />
|
||||
</div>
|
||||
<span>All Episodes</span>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
v-for="episode in episodes"
|
||||
:key="episode.id"
|
||||
:value="episode.id.toString()"
|
||||
@select="$emit('update:episode-filter', episode.id)"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
||||
episodeFilter === episode.id
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'opacity-50 [&_svg]:invisible'
|
||||
]"
|
||||
>
|
||||
<Check class="h-4 w-4" />
|
||||
</div>
|
||||
<span>{{ episode.name }}</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<!-- Task Status Filter (only for table view) -->
|
||||
<ShotTaskStatusFilter
|
||||
v-if="viewMode === 'table'"
|
||||
:all-task-types="allTaskTypes"
|
||||
:project-id="projectId"
|
||||
@filter-changed="$emit('task-status-filter-changed', $event)"
|
||||
/>
|
||||
|
||||
<!-- Column Visibility Control (only for table view) -->
|
||||
<Popover v-if="viewMode === 'table'">
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="outline" size="sm" class="h-8 border-dashed">
|
||||
<Settings2 class="mr-2 h-4 w-4" />
|
||||
View
|
||||
<Badge
|
||||
v-if="hiddenColumnsCount > 0"
|
||||
variant="secondary"
|
||||
class="ml-2 rounded-sm px-1 font-normal"
|
||||
>
|
||||
{{ hiddenColumnsCount }}
|
||||
</Badge>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-[200px] p-0" align="end">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search columns..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No columns found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandSeparator />
|
||||
<template v-for="column in allColumns">
|
||||
<CommandItem
|
||||
v-if="column.type == 'default'"
|
||||
:key="column.id"
|
||||
:value="column.id"
|
||||
@select="toggleColumn(column.id, !(columnVisibility[column.id] !== false))"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
||||
columnVisibility[column.id] !== false
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'opacity-50 [&_svg]:invisible'
|
||||
]"
|
||||
>
|
||||
<Check class="h-4 w-4" />
|
||||
</div>
|
||||
<span>{{ column.label }}</span>
|
||||
</CommandItem>
|
||||
</template>
|
||||
</CommandGroup>
|
||||
<CommandGroup>
|
||||
|
||||
<div class="px-2 py-1.5 text-xs font-medium text-muted-foreground">
|
||||
All Task Types
|
||||
</div>
|
||||
<CommandSeparator />
|
||||
<template v-for="column in allColumns">
|
||||
<CommandItem
|
||||
v-if="column.type == 'task'"
|
||||
:key="column.id"
|
||||
:value="column.id"
|
||||
@select="toggleColumn(column.id, !(columnVisibility[column.id] !== false))"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
||||
columnVisibility[column.id] !== false
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'opacity-50 [&_svg]:invisible'
|
||||
]"
|
||||
>
|
||||
<Check class="h-4 w-4" />
|
||||
</div>
|
||||
<span>{{ column.label }}</span>
|
||||
</CommandItem>
|
||||
</template>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<!-- Task Columns Toggle Button (only for table view) -->
|
||||
<Button
|
||||
v-if="viewMode === 'table'"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="toggleAllTaskColumns"
|
||||
class="h-8"
|
||||
>
|
||||
<ListTodo v-if="!allTaskColumnsVisible" class="h-4 w-4 mr-2" />
|
||||
<ListX v-else class="h-4 w-4 mr-2" />
|
||||
{{ allTaskColumnsVisible ? 'Hide' : 'Show' }} Tasks
|
||||
</Button>
|
||||
|
||||
<!-- Detail Panel Enable/Disable Toggle Button (only for table view) -->
|
||||
<Button
|
||||
v-if="viewMode === 'table'"
|
||||
@click="$emit('toggle-detail-panel')"
|
||||
:variant="isDetailPanelEnabled ? 'default' : 'outline'"
|
||||
size="sm"
|
||||
:class="[
|
||||
'h-8 w-8 p-0',
|
||||
isDetailPanelEnabled ? 'bg-primary text-primary-foreground hover:bg-primary/90' : ''
|
||||
]"
|
||||
:title="isDetailPanelEnabled ? 'Disable Auto Detail Panel' : 'Enable Auto Detail Panel'"
|
||||
>
|
||||
<PanelRightClose v-if="isDetailPanelEnabled" class="h-4 w-4" />
|
||||
<PanelRightOpen v-else class="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<!-- Clear Filters -->
|
||||
<Button
|
||||
v-if="hasFilters"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-8 px-2 lg:px-3"
|
||||
@click="clearFilters"
|
||||
>
|
||||
Reset
|
||||
<X class="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Right Side - Search and Actions -->
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<!-- Search -->
|
||||
<div class="relative w-64">
|
||||
<Search class="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
:model-value="search"
|
||||
@update:model-value="debouncedSearch"
|
||||
placeholder="Search shots..."
|
||||
class="pl-9 h-8"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<Button @click="$emit('bulk-create')" variant="outline" size="sm" class="h-8 w-8 p-0">
|
||||
<Layers class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button @click="$emit('create-shot')" size="sm" class="h-8 w-8 p-0">
|
||||
<Plus class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import {
|
||||
LayoutGrid, List, Table2, Search, Film, Plus, Layers,
|
||||
PanelRightClose, PanelRightOpen, Check, X, Settings2, ListTodo, ListX
|
||||
} from 'lucide-vue-next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from '@/components/ui/command'
|
||||
import ShotTaskStatusFilter from './ShotTaskStatusFilter.vue'
|
||||
import type { VisibilityState } from '@tanstack/vue-table'
|
||||
import type { Episode } from '@/services/episode'
|
||||
import type { Shot } from '@/services/shot'
|
||||
|
||||
interface Props {
|
||||
viewMode: 'grid' | 'list' | 'table'
|
||||
episodeFilter: number | null
|
||||
search: string
|
||||
columnVisibility: VisibilityState
|
||||
episodes: Episode[]
|
||||
allTaskTypes: string[]
|
||||
projectId: number
|
||||
selectedShot: Shot | null
|
||||
isDetailPanelEnabled: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:view-mode': [value: 'grid' | 'list' | 'table']
|
||||
'update:episode-filter': [value: number | null]
|
||||
'update:search': [value: string]
|
||||
'update:column-visibility': [value: VisibilityState]
|
||||
'task-status-filter-changed': [value: string]
|
||||
'toggle-detail-panel': []
|
||||
'toggle-task-columns': []
|
||||
'bulk-create': []
|
||||
'create-shot': []
|
||||
}>()
|
||||
|
||||
// Column definitions - computed to be reactive to allTaskTypes changes
|
||||
const allColumns = computed(() => [
|
||||
{ id: 'thumbnail', label: 'Thumbnail', type: 'default' },
|
||||
{ id: 'name', label: 'Shot Name', type: 'default' },
|
||||
{ id: 'episode', label: 'Episode', type: 'default' },
|
||||
{ id: 'frameRange', label: 'Frame Range', type: 'default' },
|
||||
{ id: 'frames', label: 'Frames', type: 'default' },
|
||||
{ id: 'status', label: 'Status', type: 'default' },
|
||||
{ id: 'description', label: 'Description', type: 'default' },
|
||||
...props.allTaskTypes.map(taskType => ({
|
||||
id: taskType,
|
||||
label: taskType.charAt(0).toUpperCase() + taskType.slice(1),
|
||||
type: 'task'
|
||||
})),
|
||||
|
||||
])
|
||||
|
||||
// Computed
|
||||
const hasFilters = computed(() => {
|
||||
return (
|
||||
props.episodeFilter !== null ||
|
||||
props.search !== ''
|
||||
)
|
||||
})
|
||||
|
||||
const hiddenColumnsCount = computed(() => {
|
||||
return allColumns.value.filter(col => props.columnVisibility[col.id] === false).length
|
||||
})
|
||||
|
||||
// Check if all task columns are visible
|
||||
const allTaskColumnsVisible = computed(() => {
|
||||
const taskColumns = allColumns.value.filter(col => col.type === 'task')
|
||||
return taskColumns.length > 0 && taskColumns.every(col => props.columnVisibility[col.id] !== false)
|
||||
})
|
||||
|
||||
// Debounced search
|
||||
let searchTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
const debouncedSearch = (value: string | number) => {
|
||||
const searchValue = typeof value === 'string' ? value : String(value)
|
||||
if (searchTimeout) clearTimeout(searchTimeout)
|
||||
searchTimeout = setTimeout(() => {
|
||||
emit('update:search', searchValue)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// Methods
|
||||
const toggleColumn = (columnId: string, value: any) => {
|
||||
const newVisibility = { ...props.columnVisibility, [columnId]: value as boolean }
|
||||
emit('update:column-visibility', newVisibility)
|
||||
}
|
||||
|
||||
const toggleAllTaskColumns = () => {
|
||||
const newVisibility = { ...props.columnVisibility }
|
||||
const taskColumns = allColumns.value.filter(col => col.type === 'task')
|
||||
|
||||
// If all task columns are visible, hide them; otherwise show them
|
||||
const shouldHide = allTaskColumnsVisible.value
|
||||
|
||||
taskColumns.forEach(col => {
|
||||
newVisibility[col.id] = !shouldHide
|
||||
})
|
||||
|
||||
emit('update:column-visibility', newVisibility)
|
||||
}
|
||||
|
||||
const clearFilters = () => {
|
||||
emit('update:episode-filter', null)
|
||||
emit('update:search', '')
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user