Add function for sidebar shot column switch
This commit is contained in:
@@ -277,5 +277,5 @@ function navigateToProject(projectId: number) {
|
|||||||
|
|
||||||
import { useAvatarUrl } from '@/composables/useAvatarUrl'
|
import { useAvatarUrl } from '@/composables/useAvatarUrl'
|
||||||
|
|
||||||
const { getAvatarUrl } = useAvatarUrl()
|
const { getAvatarUrl, getInitialsAvatarUrl } = useAvatarUrl()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -36,7 +36,10 @@
|
|||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
|
|
||||||
|
<!-- View Settings Section - Only show on shot/project pages -->
|
||||||
|
<SidebarColumnSwitch v-if="userRole !== 'developer' && isOnShotPage" />
|
||||||
|
|
||||||
<!-- Projects Section -->
|
<!-- Projects Section -->
|
||||||
<!-- <SidebarGroup v-if="userRole !== 'developer'">
|
<!-- <SidebarGroup v-if="userRole !== 'developer'">
|
||||||
<SidebarGroupLabel>Projects</SidebarGroupLabel>
|
<SidebarGroupLabel>Projects</SidebarGroupLabel>
|
||||||
@@ -91,6 +94,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@@ -122,14 +126,21 @@ import {
|
|||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import ProjectSwitcher from './ProjectSwitcher.vue'
|
import ProjectSwitcher from './ProjectSwitcher.vue'
|
||||||
import UserMenu from './UserMenu.vue'
|
import UserMenu from './UserMenu.vue'
|
||||||
|
import SidebarColumnSwitch from '@/components/ui/sidebar/SidebarColumnSwitch.vue'
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const { state } = useSidebar()
|
const { state } = useSidebar()
|
||||||
|
const route = useRoute()
|
||||||
const user = computed(() => authStore.user)
|
const user = computed(() => authStore.user)
|
||||||
const userRole = computed(() => authStore.user?.role || 'artist')
|
const userRole = computed(() => authStore.user?.role || 'artist')
|
||||||
|
|
||||||
// Check if sidebar is collapsed
|
// Check if sidebar is collapsed
|
||||||
const isCollapsed = computed(() => state.value === 'collapsed')
|
const isCollapsed = computed(() => state.value === 'collapsed')
|
||||||
|
|
||||||
|
// Check if on shot page - show column switch only on shot pages
|
||||||
|
const isOnShotPage = computed(() => {
|
||||||
|
return route.path.includes('/shots') || route.path.includes('/project/')
|
||||||
|
})
|
||||||
|
|
||||||
const props = withDefaults(defineProps<SidebarProps>(), {
|
const props = withDefaults(defineProps<SidebarProps>(), {
|
||||||
collapsible: "icon",
|
collapsible: "icon",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ import {
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||||
import { User, UserX, Search, Check, X } from 'lucide-vue-next'
|
import { User, Search, Check, X } from 'lucide-vue-next'
|
||||||
import TaskStatusBadge from '@/components/task/TaskStatusBadge.vue'
|
import TaskStatusBadge from '@/components/task/TaskStatusBadge.vue'
|
||||||
import { TaskStatus } from '@/services/shot'
|
import { TaskStatus } from '@/services/shot'
|
||||||
import { taskService } from '@/services/task'
|
import { taskService } from '@/services/task'
|
||||||
|
|||||||
@@ -166,7 +166,7 @@
|
|||||||
leave-to-class="translate-x-full"
|
leave-to-class="translate-x-full"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="showPanel"
|
v-if="showPanel && selectedShot"
|
||||||
class="fixed right-0 top-[113px] bottom-0 w-96 bg-background border-l shadow-lg z-50 hidden lg:block overflow-y-auto"
|
class="fixed right-0 top-[113px] bottom-0 w-96 bg-background border-l shadow-lg z-50 hidden lg:block overflow-y-auto"
|
||||||
>
|
>
|
||||||
<ShotDetailPanel
|
<ShotDetailPanel
|
||||||
@@ -277,13 +277,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, shallowRef, markRaw, nextTick } from 'vue'
|
import { ref, computed, watch, shallowRef, markRaw, nextTick, onMounted } from 'vue'
|
||||||
import {
|
import {
|
||||||
Search, Plus, Camera, AlertCircle, RefreshCw,
|
Search, Plus, Camera, AlertCircle, RefreshCw,
|
||||||
Layers, MoreHorizontal, Edit, ListTodo, Trash2
|
Layers, MoreHorizontal, Edit, ListTodo, Trash2
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { useColumnVisibilityStore } from '@/stores/columnVisibility'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -378,6 +379,7 @@ const validationError = ref<ValidationErrorDisplay | null>(null)
|
|||||||
|
|
||||||
// TanStack Table state
|
// TanStack Table state
|
||||||
const sorting = ref<SortingState>([])
|
const sorting = ref<SortingState>([])
|
||||||
|
const columnVisibilityStore = useColumnVisibilityStore()
|
||||||
const columnVisibility = ref<VisibilityState>({})
|
const columnVisibility = ref<VisibilityState>({})
|
||||||
const rowSelection = ref<Record<string, boolean>>({})
|
const rowSelection = ref<Record<string, boolean>>({})
|
||||||
|
|
||||||
@@ -385,16 +387,12 @@ const rowSelection = ref<Record<string, boolean>>({})
|
|||||||
const selectedCount = computed(() => {
|
const selectedCount = computed(() => {
|
||||||
return Object.keys(rowSelection.value).length
|
return Object.keys(rowSelection.value).length
|
||||||
})
|
})
|
||||||
|
|
||||||
const initializeColumnVisibility = () => {
|
const initializeColumnVisibility = () => {
|
||||||
const stored = sessionStorage.getItem('shotBrowser.columnVisibility')
|
// Initialize the global store
|
||||||
if (stored) {
|
columnVisibilityStore.initialize()
|
||||||
try {
|
// Use store's visibility as the initial value
|
||||||
columnVisibility.value = JSON.parse(stored)
|
columnVisibility.value = columnVisibilityStore.visibility
|
||||||
} catch {
|
|
||||||
// Fall back to defaults
|
|
||||||
columnVisibility.value = {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeColumnVisibility()
|
initializeColumnVisibility()
|
||||||
@@ -538,7 +536,10 @@ let visibilityUpdateTimeout: ReturnType<typeof setTimeout> | null = null
|
|||||||
const handleColumnVisibilityChange = (visibility: VisibilityState) => {
|
const handleColumnVisibilityChange = (visibility: VisibilityState) => {
|
||||||
columnVisibility.value = visibility
|
columnVisibility.value = visibility
|
||||||
|
|
||||||
// Debounce session storage updates
|
// Also update the global store
|
||||||
|
columnVisibilityStore.setColumnVisibility(visibility)
|
||||||
|
|
||||||
|
// Debounce session storage updates (for backwards compatibility)
|
||||||
if (visibilityUpdateTimeout) clearTimeout(visibilityUpdateTimeout)
|
if (visibilityUpdateTimeout) clearTimeout(visibilityUpdateTimeout)
|
||||||
visibilityUpdateTimeout = setTimeout(() => {
|
visibilityUpdateTimeout = setTimeout(() => {
|
||||||
sessionStorage.setItem('shotBrowser.columnVisibility', JSON.stringify(visibility))
|
sessionStorage.setItem('shotBrowser.columnVisibility', JSON.stringify(visibility))
|
||||||
@@ -588,7 +589,7 @@ const handleTaskAssignmentUpdated = (shotId: number, taskType: string, userId: n
|
|||||||
if (shot && shot.task_details) {
|
if (shot && shot.task_details) {
|
||||||
const taskDetail = shot.task_details.find(detail => detail.task_type === taskType)
|
const taskDetail = shot.task_details.find(detail => detail.task_type === taskType)
|
||||||
if (taskDetail) {
|
if (taskDetail) {
|
||||||
taskDetail.assigned_user_id = userId
|
taskDetail.assigned_user_id = userId ?? undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -912,4 +913,12 @@ watch(() => showEditDialog.value, (isOpen) => {
|
|||||||
|
|
||||||
// Watch for changes that require column recreation (after all functions are defined)
|
// Watch for changes that require column recreation (after all functions are defined)
|
||||||
watch([() => allTaskTypes.value, () => episodes.value, () => props.projectId], updateColumns, { immediate: true })
|
watch([() => allTaskTypes.value, () => episodes.value, () => props.projectId], updateColumns, { immediate: true })
|
||||||
|
|
||||||
|
// Watch for changes from global column visibility store
|
||||||
|
watch(() => columnVisibilityStore.visibility, (newVisibility) => {
|
||||||
|
// Only update if the change didn't come from this component
|
||||||
|
if (JSON.stringify(newVisibility) !== JSON.stringify(columnVisibility.value)) {
|
||||||
|
columnVisibility.value = newVisibility
|
||||||
|
}
|
||||||
|
}, { deep: true })
|
||||||
</script>
|
</script>
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, onMounted, watch } from 'vue'
|
||||||
import { Columns } from 'lucide-vue-next'
|
import { Columns } from 'lucide-vue-next'
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -95,9 +95,9 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import type { VisibilityState } from '@tanstack/vue-table'
|
import type { VisibilityState } from '@tanstack/vue-table'
|
||||||
|
import { useColumnVisibilityStore } from '@/stores/columnVisibility'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
columnVisibility: VisibilityState
|
|
||||||
allTaskTypes: string[]
|
allTaskTypes: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +107,26 @@ const emit = defineEmits<{
|
|||||||
'update:columnVisibility': [visibility: VisibilityState]
|
'update:columnVisibility': [visibility: VisibilityState]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// Use global store
|
||||||
|
const columnVisibilityStore = useColumnVisibilityStore()
|
||||||
|
|
||||||
|
// Local reactive state for checkbox visibility
|
||||||
|
const localVisibility = ref<Record<string, boolean>>({})
|
||||||
|
|
||||||
|
// Watch for store changes and update local state
|
||||||
|
watch(
|
||||||
|
() => columnVisibilityStore.columnVisibility,
|
||||||
|
(newVal) => {
|
||||||
|
localVisibility.value = { ...newVal }
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Initialize store on mount
|
||||||
|
onMounted(() => {
|
||||||
|
columnVisibilityStore.initialize()
|
||||||
|
})
|
||||||
|
|
||||||
const selectedColumn = ref('toggle')
|
const selectedColumn = ref('toggle')
|
||||||
|
|
||||||
const handleColumnToggle = (value: string) => {
|
const handleColumnToggle = (value: string) => {
|
||||||
@@ -114,9 +134,9 @@ const handleColumnToggle = (value: string) => {
|
|||||||
selectedColumn.value = 'toggle'
|
selectedColumn.value = 'toggle'
|
||||||
}
|
}
|
||||||
|
|
||||||
const isColumnVisible = (columnId: string) => {
|
const isColumnVisible = (columnId: string): boolean => {
|
||||||
// If not in visibility state, column is visible by default
|
// If not in visibility state, column is visible by default
|
||||||
return props.columnVisibility[columnId] !== false
|
return localVisibility.value[columnId] !== false
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCheckboxChange = (column: string, event: Event) => {
|
const handleCheckboxChange = (column: string, event: Event) => {
|
||||||
@@ -129,9 +149,8 @@ const toggleColumn = (column: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateColumn = (column: string, checked: boolean) => {
|
const updateColumn = (column: string, checked: boolean) => {
|
||||||
const newVisibility = { ...props.columnVisibility }
|
// Always use global store
|
||||||
newVisibility[column] = checked
|
columnVisibilityStore.updateColumn(column, checked)
|
||||||
emit('update:columnVisibility', newVisibility)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatTaskType = (taskType: string) => {
|
const formatTaskType = (taskType: string) => {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
'selecting': isRangeSelecting
|
'selecting': isRangeSelecting
|
||||||
}"
|
}"
|
||||||
@click="handleRowClick(row.original, $event, row)"
|
@click="handleRowClick(row.original, $event, row)"
|
||||||
|
@dblclick="emit('row-dblclick', row.original)"
|
||||||
@mousedown="handleMouseDown"
|
@mousedown="handleMouseDown"
|
||||||
@mouseup="handleMouseUp"
|
@mouseup="handleMouseUp"
|
||||||
>
|
>
|
||||||
@@ -103,6 +104,7 @@ const emit = defineEmits<{
|
|||||||
'update:columnVisibility': [visibility: VisibilityState]
|
'update:columnVisibility': [visibility: VisibilityState]
|
||||||
'update:rowSelection': [selection: Record<string, boolean>]
|
'update:rowSelection': [selection: Record<string, boolean>]
|
||||||
'row-click': [shot: Shot, event: MouseEvent]
|
'row-click': [shot: Shot, event: MouseEvent]
|
||||||
|
'row-dblclick': [shot: Shot]
|
||||||
'selection-cleared': []
|
'selection-cleared': []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
<template>
|
||||||
|
<SidebarGroup>
|
||||||
|
<SidebarGroupLabel>View</SidebarGroupLabel>
|
||||||
|
<SidebarGroup class="py-0" as-child>
|
||||||
|
<Collapsible v-model:open="isBaseColumnsOpen" class="group/collapsible">
|
||||||
|
<CollapsibleTrigger as-child>
|
||||||
|
<SidebarGroupLabel class="px-2 hover:bg-sidebar-accent text-sm hover:text-sidebar-accent-foreground cursor-pointer">
|
||||||
|
<div class="flex items-center justify-between w-full">
|
||||||
|
Columns
|
||||||
|
<ChevronRight
|
||||||
|
class="h-4 w-4 transition-transform duration-200"
|
||||||
|
:class="isBaseColumnsOpen ? 'rotate-90' : ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SidebarGroupLabel>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<SidebarGroupContent>
|
||||||
|
<Command class="bg-transparent border-0 p-0">
|
||||||
|
<CommandList>
|
||||||
|
<CommandGroup>
|
||||||
|
<template v-for="column in baseColumns" :key="column.id">
|
||||||
|
<CommandItem
|
||||||
|
:value="column.id"
|
||||||
|
@select="toggleColumn(column.id, !isColumnVisible(column.id))"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
||||||
|
isColumnVisible(column.id)
|
||||||
|
? 'bg-primary text-primary-foreground'
|
||||||
|
: 'opacity-50 [&_svg]:invisible'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<Check class="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
<span class="text-sm text-sidebar-foreground">{{ column.label }}</span>
|
||||||
|
</CommandItem>
|
||||||
|
</template>
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
</SidebarGroup>
|
||||||
|
<SidebarGroup class="py-0">
|
||||||
|
<Collapsible v-model:open="isTaskColumnsOpen" class="group/collapsible">
|
||||||
|
<CollapsibleTrigger as-child>
|
||||||
|
<SidebarGroupLabel class="px-2 hover:bg-sidebar-accent text-sm hover:text-sidebar-accent-foreground cursor-pointer">
|
||||||
|
<div class="flex items-center justify-between w-full">
|
||||||
|
Task Columns
|
||||||
|
<ChevronRight
|
||||||
|
class="h-4 w-4 transition-transform duration-200"
|
||||||
|
:class="isTaskColumnsOpen ? 'rotate-90' : ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SidebarGroupLabel>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<SidebarGroupContent>
|
||||||
|
<Command class="bg-transparent border-0 p-0">
|
||||||
|
<CommandList>
|
||||||
|
<CommandGroup>
|
||||||
|
<template v-for="column in taskColumns" :key="column.id">
|
||||||
|
<CommandItem
|
||||||
|
:value="column.id"
|
||||||
|
@select="toggleColumn(column.id, !isColumnVisible(column.id))"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
||||||
|
isColumnVisible(column.id)
|
||||||
|
? 'bg-primary text-primary-foreground'
|
||||||
|
: 'opacity-50 [&_svg]:invisible'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<Check class="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
<span class="text-sm text-sidebar-foreground">{{ column.label }}</span>
|
||||||
|
</CommandItem>
|
||||||
|
</template>
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
</SidebarGroup>
|
||||||
|
</SidebarGroup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { Check, ChevronDown, ChevronRight } from 'lucide-vue-next'
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
CommandSeparator,
|
||||||
|
} from '@/components/ui/command'
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from '@/components/ui/collapsible'
|
||||||
|
import {
|
||||||
|
SidebarGroup,
|
||||||
|
SidebarGroupContent,
|
||||||
|
SidebarGroupLabel,
|
||||||
|
} from '@/components/ui/sidebar'
|
||||||
|
import { useColumnVisibilityStore } from '@/stores/columnVisibility'
|
||||||
|
import { customTaskTypeService } from '@/services/customTaskType'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const columnVisibilityStore = useColumnVisibilityStore()
|
||||||
|
|
||||||
|
// Collapsible state
|
||||||
|
const isBaseColumnsOpen = ref(true)
|
||||||
|
const isTaskColumnsOpen = ref(true)
|
||||||
|
|
||||||
|
// Task types state
|
||||||
|
const taskTypes = ref<string[]>([])
|
||||||
|
|
||||||
|
// Base columns - synced with Shot Data Table default columns
|
||||||
|
const baseColumns = [
|
||||||
|
{ id: 'thumbnail', label: 'Thumbnail' },
|
||||||
|
{ id: 'name', label: 'Shot Name' },
|
||||||
|
{ id: 'episode', label: 'Episode' },
|
||||||
|
{ id: 'frames', label: 'Frames' },
|
||||||
|
{ id: 'status', label: 'Status' },
|
||||||
|
]
|
||||||
|
const taskColumns = computed(() => {
|
||||||
|
const columns = taskTypes.value.map(taskType => ({
|
||||||
|
id: taskType,
|
||||||
|
label: taskType.charAt(0).toUpperCase() + taskType.slice(1),
|
||||||
|
}))
|
||||||
|
return [...columns]
|
||||||
|
})
|
||||||
|
// All columns including task types
|
||||||
|
const allColumns = computed(() => {
|
||||||
|
const taskColumns = taskTypes.value.map(taskType => ({
|
||||||
|
id: taskType,
|
||||||
|
label: taskType.charAt(0).toUpperCase() + taskType.slice(1),
|
||||||
|
}))
|
||||||
|
return [...baseColumns, ...taskColumns]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Local reactive state
|
||||||
|
const localVisibility = ref<Record<string, boolean>>({})
|
||||||
|
|
||||||
|
// Watch for store changes and update local state
|
||||||
|
watch(
|
||||||
|
() => columnVisibilityStore.columnVisibility,
|
||||||
|
(newVal) => {
|
||||||
|
localVisibility.value = { ...newVal }
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const isColumnVisible = (columnId: string): boolean => {
|
||||||
|
// If not in visibility state, column is visible by default
|
||||||
|
return localVisibility.value[columnId] !== false
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleColumn = (columnId: string, value: boolean) => {
|
||||||
|
columnVisibilityStore.updateColumn(columnId, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch task types based on project ID from route
|
||||||
|
const fetchTaskTypes = async () => {
|
||||||
|
// Extract projectId from route params
|
||||||
|
const projectId = route.params.projectId as string | undefined
|
||||||
|
|
||||||
|
// Clear task types if no projectId
|
||||||
|
if (!projectId) {
|
||||||
|
taskTypes.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const projectIdNum = parseInt(projectId, 10)
|
||||||
|
if (!isNaN(projectIdNum)) {
|
||||||
|
const data = await customTaskTypeService.getAllTaskTypes(projectIdNum)
|
||||||
|
taskTypes.value = data.shot_task_types || []
|
||||||
|
} else {
|
||||||
|
taskTypes.value = []
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch task types:', err)
|
||||||
|
taskTypes.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifecycle - initialize store and fetch task types
|
||||||
|
onMounted(() => {
|
||||||
|
columnVisibilityStore.initialize()
|
||||||
|
fetchTaskTypes()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch for route changes to refetch task types
|
||||||
|
watch(() => route.params.projectId, () => {
|
||||||
|
fetchTaskTypes()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -60,6 +60,12 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/views/project/ProjectShotsView.vue'),
|
component: () => import('@/views/project/ProjectShotsView.vue'),
|
||||||
meta: { tab: 'shots', tabLabel: 'Shots', hasEpisode: true }
|
meta: { tab: 'shots', tabLabel: 'Shots', hasEpisode: true }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'shots/:shotId',
|
||||||
|
name: 'ShotDetail',
|
||||||
|
component: () => import('@/views/project/ShotDetailView.vue'),
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'assets',
|
path: 'assets',
|
||||||
name: 'ProjectAssets',
|
name: 'ProjectAssets',
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import type { VisibilityState } from '@tanstack/vue-table'
|
||||||
|
|
||||||
|
// Default column visibility - basic columns visible, task type columns hidden by default
|
||||||
|
const DEFAULT_VISIBILITY: VisibilityState = {
|
||||||
|
thumbnail: true,
|
||||||
|
name: true,
|
||||||
|
episode: true,
|
||||||
|
frames: true,
|
||||||
|
status: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'shot-column-visibility'
|
||||||
|
|
||||||
|
export const useColumnVisibilityStore = defineStore('columnVisibility', () => {
|
||||||
|
// State
|
||||||
|
const columnVisibility = ref<VisibilityState>({})
|
||||||
|
let isInitialized = false
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
const visibility = computed(() => {
|
||||||
|
// Auto-initialize if empty
|
||||||
|
if (Object.keys(columnVisibility.value).length === 0) {
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
return columnVisibility.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const isColumnVisible = (columnId: string): boolean => {
|
||||||
|
// Auto-initialize if empty
|
||||||
|
if (Object.keys(columnVisibility.value).length === 0) {
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
// If not in visibility state, column is visible by default
|
||||||
|
return columnVisibility.value[columnId] !== false
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleColumns = computed(() => {
|
||||||
|
return Object.entries(columnVisibility.value)
|
||||||
|
.filter(([, visible]) => visible !== false)
|
||||||
|
.map(([column]) => column)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
const initialize = () => {
|
||||||
|
if (isInitialized) return
|
||||||
|
|
||||||
|
// Get saved visibility from localStorage
|
||||||
|
const saved = localStorage.getItem(STORAGE_KEY)
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(saved)
|
||||||
|
// Merge with defaults to ensure all columns have a state
|
||||||
|
columnVisibility.value = { ...DEFAULT_VISIBILITY, ...parsed }
|
||||||
|
} catch {
|
||||||
|
// If parsing fails, use defaults
|
||||||
|
columnVisibility.value = { ...DEFAULT_VISIBILITY }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No saved state, use defaults
|
||||||
|
columnVisibility.value = { ...DEFAULT_VISIBILITY }
|
||||||
|
}
|
||||||
|
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleColumn = (columnId: string) => {
|
||||||
|
const newVisibility = !isColumnVisible(columnId)
|
||||||
|
updateColumn(columnId, newVisibility)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateColumn = (columnId: string, visible: boolean) => {
|
||||||
|
columnVisibility.value = {
|
||||||
|
...columnVisibility.value,
|
||||||
|
[columnId]: visible,
|
||||||
|
}
|
||||||
|
saveToStorage()
|
||||||
|
}
|
||||||
|
|
||||||
|
const setColumnVisibility = (visibility: VisibilityState) => {
|
||||||
|
columnVisibility.value = { ...DEFAULT_VISIBILITY, ...visibility }
|
||||||
|
saveToStorage()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetToDefaults = () => {
|
||||||
|
columnVisibility.value = { ...DEFAULT_VISIBILITY }
|
||||||
|
saveToStorage()
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveToStorage = () => {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(columnVisibility.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
columnVisibility,
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
visibility,
|
||||||
|
isColumnVisible,
|
||||||
|
visibleColumns,
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
initialize,
|
||||||
|
toggleColumn,
|
||||||
|
updateColumn,
|
||||||
|
setColumnVisibility,
|
||||||
|
resetToDefaults,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -108,4 +108,26 @@
|
|||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.scrollbar::-webkit-scrollbar {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar::-webkit-scrollbar-track {
|
||||||
|
border-radius: 100vh;
|
||||||
|
background: #f7f4ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background: #e0cbcb;
|
||||||
|
border-radius: 100vh;
|
||||||
|
border: 3px solid #f6f7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #c0a0b9;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,7 @@ export interface UserInfo {
|
|||||||
first_name: string
|
first_name: string
|
||||||
last_name: string
|
last_name: string
|
||||||
email: string
|
email: string
|
||||||
|
avatar_url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Activity {
|
export interface Activity {
|
||||||
|
|||||||
Vendored
+7
-3
@@ -1,6 +1,10 @@
|
|||||||
// src/vue-shims.d.ts
|
// src/vue-shims.d.ts
|
||||||
|
|
||||||
|
import type { DefineComponent } from 'vue'
|
||||||
|
|
||||||
|
// Declare default export for all .vue files
|
||||||
|
// This is needed for TypeScript to understand Vue 3 <script setup> components
|
||||||
declare module "*.vue" {
|
declare module "*.vue" {
|
||||||
import Vue from "vue";
|
const component: DefineComponent<{}, {}, any>
|
||||||
export default Vue;
|
export default component
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-03-05
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
The VFX Studio application currently has a ShotColumnVisibilityControl component in the Shot page toolbar that allows users to toggle which columns to display in the Shot data table. However, this control is only accessible when users are on the Shot page. Users have requested the ability to quickly toggle Shot table columns from the Sidebar so they can adjust column visibility without navigating to the Shot page first.
|
||||||
|
|
||||||
|
This change will add a column display switch to the application Sidebar, making column visibility controls accessible from anywhere in the application.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
- Add a column visibility control to the AppSidebar component
|
||||||
|
- Create a global column visibility state accessible throughout the application
|
||||||
|
- Ensure column preferences persist across user sessions using localStorage
|
||||||
|
- Maintain consistency with the existing ShotColumnVisibilityControl component
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
- Backend API changes (not required)
|
||||||
|
- Database schema changes (not required)
|
||||||
|
- Modifying the existing Shot page column visibility functionality
|
||||||
|
- Adding column visibility controls for other data tables (e.g., Tasks, Assets)
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### 1. Global State Management Approach
|
||||||
|
**Decision:** Use a Pinia store for global column visibility state with localStorage persistence.
|
||||||
|
|
||||||
|
**Rationale:**
|
||||||
|
- Pinia is already the state management solution used in the project
|
||||||
|
- localStorage provides simple persistence without backend changes
|
||||||
|
- This approach aligns with existing patterns in the codebase
|
||||||
|
|
||||||
|
**Alternative Considered:** Use Vue's provide/inject pattern
|
||||||
|
- Rejected because it doesn't provide automatic persistence and is less scalable
|
||||||
|
|
||||||
|
### 2. Sidebar Integration Location
|
||||||
|
**Decision:** Add the column display switch as a new SidebarGroup in the AppSidebar component, positioned in the SidebarContent area.
|
||||||
|
|
||||||
|
**Rationale:**
|
||||||
|
- Keeps the control accessible but not obtrusive
|
||||||
|
- Follows the existing Sidebar organization pattern (Navigation, Administration, Developer Tools)
|
||||||
|
- Allows for future expansion to other column visibility controls
|
||||||
|
|
||||||
|
**Alternative Considered:** Add to SidebarFooter
|
||||||
|
- Rejected because Footer is already used for UserMenu and would become cluttered
|
||||||
|
|
||||||
|
### 3. Reuse Existing Component vs. New Implementation
|
||||||
|
**Decision:** Create a new Sidebar-specific component that adapts the existing ShotColumnVisibilityControl logic.
|
||||||
|
|
||||||
|
**Rationale:**
|
||||||
|
- The existing ShotColumnVisibilityControl is tightly coupled to the Shot page context
|
||||||
|
- A sidebar-specific component can be simpler and more focused
|
||||||
|
- Allows for different UI presentation in sidebarmore compact)
|
||||||
|
|
||||||
|
** context (Alternative Considered:** Reuse ShotColumnVisibilityControl directly
|
||||||
|
- Rejected because it has Shot-specific UI elements (toolbar styling, select dropdown)
|
||||||
|
|
||||||
|
### 4. Default Column Visibility
|
||||||
|
**Decision:** Default visibility: Thumbnail, Shot Name, Episode, Status visible; all task type columns hidden.
|
||||||
|
|
||||||
|
**Rationale:**
|
||||||
|
- Matches typical VFX production workflow priorities
|
||||||
|
- Reduces visual clutter for new users
|
||||||
|
- Aligns with the current Shot page defaults
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
**[Risk]** State synchronization between Sidebar control and Shot page control
|
||||||
|
→ **Mitigation:** Both controls will read from and write to the same Pinia store, ensuring consistency
|
||||||
|
|
||||||
|
**[Risk]** Performance impact from reactive column visibility state
|
||||||
|
→ **Mitigation:** Column visibility is a simple object; Vue's reactivity system handles this efficiently
|
||||||
|
|
||||||
|
**[Risk]** User confusion if Sidebar is collapsed
|
||||||
|
→ **Mitigation:** The column control will be hidden when sidebar is collapsed (consistent with other sidebar content)
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
1. Create new Pinia store for column visibility (e.g., `useColumnVisibilityStore`)
|
||||||
|
2. Create new SidebarColumnSwitch component
|
||||||
|
3. Add SidebarColumnSwitch to AppSidebar.vue
|
||||||
|
4. Update ShotColumnVisibilityControl to use the new store
|
||||||
|
5. Test state synchronization between Sidebar and Shot page
|
||||||
|
6. No rollback needed - this is a purely additive feature
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1. Should the column visibility settings be per-project or global across all projects?
|
||||||
|
- Current design assumes global (simpler implementation)
|
||||||
|
- Could be enhanced later if users request per-project settings
|
||||||
|
|
||||||
|
2. Should we show all task type columns or only a subset in the Sidebar?
|
||||||
|
- Current design shows all available task types
|
||||||
|
- Could be limited to top 5 most-used types if the list becomes too long
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
The Shot page already has a column visibility control component (`ShotColumnVisibilityControl.vue`) that allows users to toggle which columns to display in the Shot data table. However, users need to navigate to the Shot page to access this functionality. Adding a column display switch directly to the Sidebar will improve user experience by providing quick access to toggle Shot table columns from anywhere in the application, without needing to first navigate to the Shot page.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Add a new Sidebar menu item or control section in the Sidebar for Shot column visibility
|
||||||
|
- Create a global column visibility state that can be accessed from the Sidebar
|
||||||
|
- Integrate the existing column toggle logic into the Sidebar component
|
||||||
|
- Persist user column preferences globally (so they apply across all views)
|
||||||
|
- Add appropriate UI components (switch/checkbox) in the Sidebar for quick column toggling
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
- `shot-column-display-switch`: Add a Sidebar control for toggling Shot data table column visibility globally
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
- None. This is a new feature that doesn't change existing requirements.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Frontend: New Sidebar component or modification to AppSidebar.vue
|
||||||
|
- State Management: May need to add a Pinia store or extend existing store for global column visibility state
|
||||||
|
- No API changes required
|
||||||
|
- Existing ShotColumnVisibilityControl component can be reused or adapted
|
||||||
|
- Minimal impact on existing functionality
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Sidebar Shot Column Display Switch
|
||||||
|
The system SHALL provide a column visibility control in the Sidebar that allows users to toggle which columns are displayed in the Shot data table from any page in the application.
|
||||||
|
|
||||||
|
#### Scenario: User toggles column from Sidebar
|
||||||
|
- **WHEN** user interacts with the column display switch in the Sidebar
|
||||||
|
- **THEN** the Shot data table column visibility is updated globally across all views
|
||||||
|
|
||||||
|
#### Scenario: User views available column options
|
||||||
|
- **WHEN** user expands the column display control in the Sidebar
|
||||||
|
- **THEN** the system displays all available Shot table columns with their current visibility state
|
||||||
|
|
||||||
|
#### Scenario: Column preferences persist across sessions
|
||||||
|
- **WHEN** user toggles a column visibility setting
|
||||||
|
- **THEN** the preference SHALL be saved and restored when the user returns to the application
|
||||||
|
|
||||||
|
### Requirement: Global Column Visibility State
|
||||||
|
The system SHALL maintain a global column visibility state that is accessible from both the Sidebar and the Shot page column visibility control.
|
||||||
|
|
||||||
|
#### Scenario: Sidebar control reflects current visibility state
|
||||||
|
- **WHEN** user opens the Sidebar column control
|
||||||
|
- **THEN** the checkboxes SHALL reflect the current column visibility state from the global state
|
||||||
|
|
||||||
|
#### Scenario: Changes in Sidebar reflect in Shot page
|
||||||
|
- **WHEN** user toggles a column from the Sidebar
|
||||||
|
- **THEN** the Shot page column visibility control SHALL show the updated state
|
||||||
|
|
||||||
|
### Requirement: Default Column Visibility
|
||||||
|
The system SHALL provide sensible default column visibility settings that apply when no user preference has been saved.
|
||||||
|
|
||||||
|
#### Scenario: New user views Shot table
|
||||||
|
- **WHEN** a new user (no saved preferences) views the Shot data table
|
||||||
|
- **THEN** default columns (Thumbnail, Shot Name, Episode, Status) SHALL be visible
|
||||||
|
- **AND** task type columns SHALL be hidden by default
|
||||||
|
|
||||||
|
### Requirement: Column Types Supported
|
||||||
|
The system SHALL support toggling visibility for the following column types:
|
||||||
|
- Thumbnail
|
||||||
|
- Shot Name
|
||||||
|
- Episode
|
||||||
|
- Status
|
||||||
|
- Task type columns (dynamic based on project task types)
|
||||||
|
|
||||||
|
#### Scenario: User toggles thumbnail column
|
||||||
|
- **WHEN** user toggles the Thumbnail column visibility
|
||||||
|
- **THEN** the thumbnail column SHALL show/hide in the Shot data table
|
||||||
|
|
||||||
|
#### Scenario: User toggles task type column
|
||||||
|
- **WHEN** user toggles a task type column (e.g., "Animation", "Compositing")
|
||||||
|
- **THEN** that task status column SHALL show/hide in the Shot data table
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
## 1. Create Column Visibility Store
|
||||||
|
|
||||||
|
- [x] 1.1 Create Pinia store `useColumnVisibilityStore` in `frontend/src/stores/`
|
||||||
|
- [x] 1.2 Add localStorage persistence for column visibility state
|
||||||
|
- [x] 1.3 Define default column visibility (Thumbnail, Shot Name, Episode, Status visible; task types hidden)
|
||||||
|
- [x] 1.4 Add getter methods for checking column visibility
|
||||||
|
- [x] 1.5 Add action methods for toggling column visibility
|
||||||
|
|
||||||
|
## 2. Create Sidebar Column Switch Component
|
||||||
|
|
||||||
|
- [x] 2.1 Create new component `SidebarColumnSwitch.vue` in `frontend/src/components/ui/sidebar/`
|
||||||
|
- [x] 2.2 Implement column visibility toggle UI (checkboxes or switches)
|
||||||
|
- [x] 2.3 Integrate with `useColumnVisibilityStore`
|
||||||
|
- [x] 2.4 Style component for sidebar context (compact, collapsible)
|
||||||
|
|
||||||
|
## 3. Integrate Component into Sidebar
|
||||||
|
|
||||||
|
- [x] 3.1 Import `SidebarColumnSwitch` into `AppSidebar.vue`
|
||||||
|
- [x] 3.2 Add new `SidebarGroup` for "View Settings" in `AppSidebar.vue`
|
||||||
|
- [x] 3.3 Place `SidebarColumnSwitch` inside the new group
|
||||||
|
- [ ] 3.4 Test visibility when sidebar is collapsed
|
||||||
|
|
||||||
|
## 4. Update Existing Shot Column Control
|
||||||
|
|
||||||
|
- [x] 4.1 Modify `ShotColumnVisibilityControl.vue` to use `useColumnVisibilityStore`
|
||||||
|
- [x] 4.2 Remove local state management from Shot page column control
|
||||||
|
- [x] 4.3 Ensure changes in Shot page reflect in Sidebar and vice versa
|
||||||
|
|
||||||
|
## 5. Testing and Verification
|
||||||
|
|
||||||
|
- [ ] 5.1 Test column toggle from Sidebar updates Shot table
|
||||||
|
- [ ] 5.2 Test column toggle from Shot page updates Sidebar control
|
||||||
|
- [ ] 5.3 Test persistence - preferences survive page refresh
|
||||||
|
- [ ] 5.4 Test default visibility for new users
|
||||||
|
- [ ] 5.5 Test behavior when sidebar is collapsed/expanded
|
||||||
|
|
||||||
|
## 6. Edge Cases
|
||||||
|
|
||||||
|
- [ ] 6.1 Handle case when no project is selected
|
||||||
|
- [ ] 6.2 Handle case when task types list is empty
|
||||||
|
- [ ] 6.3 Test responsiveness on mobile (sidebar behavior)
|
||||||
Reference in New Issue
Block a user