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>
+6
View File
@@ -60,6 +60,12 @@ const routes: RouteRecordRaw[] = [
component: () => import('@/views/project/ProjectShotsView.vue'),
meta: { tab: 'shots', tabLabel: 'Shots', hasEpisode: true }
},
{
path: 'shots/:shotId',
name: 'ShotDetail',
component: () => import('@/views/project/ShotDetailView.vue'),
meta: { requiresAuth: true }
},
{
path: 'assets',
name: 'ProjectAssets',
+111
View File
@@ -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,
}
})
+22
View File
@@ -108,4 +108,26 @@
body {
@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;
}
}
+1
View File
@@ -20,6 +20,7 @@ export interface UserInfo {
first_name: string
last_name: string
email: string
avatar_url: string
}
export interface Activity {
+7 -3
View File
@@ -1,6 +1,10 @@
// 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" {
import Vue from "vue";
export default Vue;
}
const component: DefineComponent<{}, {}, any>
export default component
}