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'
|
||||
|
||||
const { getAvatarUrl } = useAvatarUrl()
|
||||
const { getAvatarUrl, getInitialsAvatarUrl } = useAvatarUrl()
|
||||
</script>
|
||||
|
||||
@@ -36,7 +36,10 @@
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
|
||||
|
||||
<!-- View Settings Section - Only show on shot/project pages -->
|
||||
<SidebarColumnSwitch v-if="userRole !== 'developer' && isOnShotPage" />
|
||||
|
||||
<!-- Projects Section -->
|
||||
<!-- <SidebarGroup v-if="userRole !== 'developer'">
|
||||
<SidebarGroupLabel>Projects</SidebarGroupLabel>
|
||||
@@ -91,6 +94,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
@@ -122,14 +126,21 @@ import {
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import ProjectSwitcher from './ProjectSwitcher.vue'
|
||||
import UserMenu from './UserMenu.vue'
|
||||
import SidebarColumnSwitch from '@/components/ui/sidebar/SidebarColumnSwitch.vue'
|
||||
const authStore = useAuthStore()
|
||||
const { state } = useSidebar()
|
||||
const route = useRoute()
|
||||
const user = computed(() => authStore.user)
|
||||
const userRole = computed(() => authStore.user?.role || 'artist')
|
||||
|
||||
// Check if sidebar is 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>(), {
|
||||
collapsible: "icon",
|
||||
})
|
||||
|
||||
@@ -165,7 +165,7 @@ import {
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
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 { TaskStatus } from '@/services/shot'
|
||||
import { taskService } from '@/services/task'
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
leave-to-class="translate-x-full"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<ShotDetailPanel
|
||||
@@ -277,13 +277,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, shallowRef, markRaw, nextTick } from 'vue'
|
||||
import { ref, computed, watch, shallowRef, markRaw, nextTick, onMounted } from 'vue'
|
||||
import {
|
||||
Search, Plus, Camera, AlertCircle, RefreshCw,
|
||||
Layers, MoreHorizontal, Edit, ListTodo, Trash2
|
||||
} from 'lucide-vue-next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { useColumnVisibilityStore } from '@/stores/columnVisibility'
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
@@ -378,6 +379,7 @@ const validationError = ref<ValidationErrorDisplay | null>(null)
|
||||
|
||||
// TanStack Table state
|
||||
const sorting = ref<SortingState>([])
|
||||
const columnVisibilityStore = useColumnVisibilityStore()
|
||||
const columnVisibility = ref<VisibilityState>({})
|
||||
const rowSelection = ref<Record<string, boolean>>({})
|
||||
|
||||
@@ -385,16 +387,12 @@ const rowSelection = ref<Record<string, boolean>>({})
|
||||
const selectedCount = computed(() => {
|
||||
return Object.keys(rowSelection.value).length
|
||||
})
|
||||
|
||||
const initializeColumnVisibility = () => {
|
||||
const stored = sessionStorage.getItem('shotBrowser.columnVisibility')
|
||||
if (stored) {
|
||||
try {
|
||||
columnVisibility.value = JSON.parse(stored)
|
||||
} catch {
|
||||
// Fall back to defaults
|
||||
columnVisibility.value = {}
|
||||
}
|
||||
}
|
||||
// Initialize the global store
|
||||
columnVisibilityStore.initialize()
|
||||
// Use store's visibility as the initial value
|
||||
columnVisibility.value = columnVisibilityStore.visibility
|
||||
}
|
||||
|
||||
initializeColumnVisibility()
|
||||
@@ -538,7 +536,10 @@ let visibilityUpdateTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
const handleColumnVisibilityChange = (visibility: VisibilityState) => {
|
||||
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)
|
||||
visibilityUpdateTimeout = setTimeout(() => {
|
||||
sessionStorage.setItem('shotBrowser.columnVisibility', JSON.stringify(visibility))
|
||||
@@ -588,7 +589,7 @@ const handleTaskAssignmentUpdated = (shotId: number, taskType: string, userId: n
|
||||
if (shot && shot.task_details) {
|
||||
const taskDetail = shot.task_details.find(detail => detail.task_type === taskType)
|
||||
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([() => 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>
|
||||
@@ -83,7 +83,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { Columns } from 'lucide-vue-next'
|
||||
import {
|
||||
Select,
|
||||
@@ -95,9 +95,9 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import type { VisibilityState } from '@tanstack/vue-table'
|
||||
import { useColumnVisibilityStore } from '@/stores/columnVisibility'
|
||||
|
||||
interface Props {
|
||||
columnVisibility: VisibilityState
|
||||
allTaskTypes: string[]
|
||||
}
|
||||
|
||||
@@ -107,6 +107,26 @@ const emit = defineEmits<{
|
||||
'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 handleColumnToggle = (value: string) => {
|
||||
@@ -114,9 +134,9 @@ const handleColumnToggle = (value: string) => {
|
||||
selectedColumn.value = 'toggle'
|
||||
}
|
||||
|
||||
const isColumnVisible = (columnId: string) => {
|
||||
const isColumnVisible = (columnId: string): boolean => {
|
||||
// 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) => {
|
||||
@@ -129,9 +149,8 @@ const toggleColumn = (column: string) => {
|
||||
}
|
||||
|
||||
const updateColumn = (column: string, checked: boolean) => {
|
||||
const newVisibility = { ...props.columnVisibility }
|
||||
newVisibility[column] = checked
|
||||
emit('update:columnVisibility', newVisibility)
|
||||
// Always use global store
|
||||
columnVisibilityStore.updateColumn(column, checked)
|
||||
}
|
||||
|
||||
const formatTaskType = (taskType: string) => {
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
'selecting': isRangeSelecting
|
||||
}"
|
||||
@click="handleRowClick(row.original, $event, row)"
|
||||
@dblclick="emit('row-dblclick', row.original)"
|
||||
@mousedown="handleMouseDown"
|
||||
@mouseup="handleMouseUp"
|
||||
>
|
||||
@@ -103,6 +104,7 @@ const emit = defineEmits<{
|
||||
'update:columnVisibility': [visibility: VisibilityState]
|
||||
'update:rowSelection': [selection: Record<string, boolean>]
|
||||
'row-click': [shot: Shot, event: MouseEvent]
|
||||
'row-dblclick': [shot: Shot]
|
||||
'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>
|
||||
Reference in New Issue
Block a user