Add function for sidebar shot column switch

This commit is contained in:
2026-03-07 09:33:44 +08:00
parent 1f229bff6c
commit 19bc4e8a23
17 changed files with 632 additions and 26 deletions
@@ -277,5 +277,5 @@ function navigateToProject(projectId: number) {
import { useAvatarUrl } from '@/composables/useAvatarUrl'
const { getAvatarUrl } = useAvatarUrl()
const { getAvatarUrl, getInitialsAvatarUrl } = useAvatarUrl()
</script>
+12 -1
View File
@@ -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'
+22 -13
View File
@@ -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>