Sticky table headers, layout parity, and reuse My Tasks on the shared table/toolbar

Shot/Asset/Task data tables now keep their header row pinned while scrolling
(both the locked two-pane and single-table modes), matching a common data-
table expectation that was missing everywhere.

Brings Asset and Task browsers to full structural parity with Shot's bounded-
height layout (fixed toolbar, internally-scrolling table) instead of their
previous page-scroll model with a sticky-positioned toolbar. AssetsDataTable
gained the synced frozen/movable-pane scrolling it was missing entirely;
TasksDataTable and both browsers/parent views got the same container model.

The global My Tasks page (/tasks) now reuses TaskTableToolbar and
TasksDataTable instead of its own ad hoc filter selects and TaskList,
including the detail panel, bulk status/assignment context menu, and sticky
header for free. Since it spans multiple projects, TaskTableToolbar gained
an optional Project filter (only rendered when a project list is passed in,
so the project-scoped Tasks page is unaffected); episode/assignee filters
populate once a specific project is chosen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 10:15:04 +08:00
parent 442abc3586
commit e628c99498
9 changed files with 525 additions and 163 deletions
@@ -92,6 +92,49 @@
</PopoverContent>
</Popover>
<!-- Project Filter (only shown when a cross-project task list is passed in, e.g. My Tasks) -->
<Popover v-if="projects && projects.length > 0">
<PopoverTrigger as-child>
<Button variant="outline" size="sm" class="h-8 border-dashed">
<FolderOpen class="mr-2 h-4 w-4" />
Project
<Badge
v-if="projectFilter !== 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 project..." />
<CommandList>
<CommandEmpty>No project found.</CommandEmpty>
<CommandGroup>
<CheckableCommandItem
value="all"
:model-value="projectFilter === null"
@update:model-value="$emit('update:project-filter', null)"
>
<span>All Projects</span>
</CheckableCommandItem>
<CheckableCommandItem
v-for="project in projects"
:key="project.id"
:value="project.id.toString()"
:model-value="projectFilter === project.id"
@update:model-value="$emit('update:project-filter', project.id)"
>
<span>{{ project.name }}</span>
</CheckableCommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<!-- Episode Filter -->
<Popover v-if="episodes.length > 0">
<PopoverTrigger as-child>
@@ -205,7 +248,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import { Search, ListFilter, Tag, Film, Package, User } from 'lucide-vue-next'
import { Search, ListFilter, Tag, Film, Package, User, FolderOpen } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
@@ -229,6 +272,7 @@ import ClearFiltersButton from '@/components/shared/ClearFiltersButton.vue'
import { useDebouncedSearch } from '@/composables/useDebouncedSearch'
import type { VisibilityState } from '@tanstack/vue-table'
import type { Episode } from '@/services/episode'
import type { Project } from '@/services/project'
interface Props {
statusFilter: string[]
@@ -244,6 +288,10 @@ interface Props {
myTasksFilter: boolean
currentUserId: number | null
isDetailPanelEnabled: boolean
// Cross-project task lists (e.g. My Tasks) pass a project list + filter; the
// project-scoped Tasks page omits these and the filter stays hidden.
projects?: Project[]
projectFilter?: number | null
}
const props = defineProps<Props>()
@@ -257,6 +305,7 @@ const emit = defineEmits<{
'update:search': [value: string]
'update:column-visibility': [value: VisibilityState]
'update:my-tasks-filter': [value: boolean]
'update:project-filter': [value: number | null]
'toggle-detail-panel': []
}>()
@@ -298,7 +347,8 @@ const hasFilters = computed(() => {
props.assigneeFilter.length > 0 ||
props.contextFilter !== 'all' ||
props.search !== '' ||
props.myTasksFilter
props.myTasksFilter ||
(props.projectFilter ?? null) !== null
)
})
@@ -348,5 +398,8 @@ const clearFilters = () => {
emit('update:context-filter', 'all')
emit('update:search', '')
emit('update:my-tasks-filter', false)
if (props.projects) {
emit('update:project-filter', null)
}
}
</script>