Phase 3: Unify controls across shot/asset/task toolbars and detail panels
Extracts duplication that built up as shot/asset/task features reached parity: CheckableCommandItem/ColumnToggleList replace 14+ hand-rolled checkbox-list blocks, shared toolbar pieces (debounced search, detail-panel toggle, clear-filters, segmented view/context toggle) replace copy-pasted markup in the three table toolbars, icon-only buttons standardize on the icon-sm size, TaskBulkActionsMenu's Assign To submenu matches Set Status, and a shared DetailPanelOverlay/Header/Loading/Error shell backs all three detail panels (adding a previously-missing error state to the task panel).
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { Settings2 } from 'lucide-vue-next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
CheckableCommandItem,
|
||||
} from '@/components/ui/command'
|
||||
import type { VisibilityState } from '@tanstack/vue-table'
|
||||
|
||||
export interface ColumnOption {
|
||||
id: string
|
||||
label: string
|
||||
/** Optional section label; columns without one are grouped first, unlabeled. */
|
||||
group?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
columns: ColumnOption[]
|
||||
modelValue: VisibilityState
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: VisibilityState]
|
||||
}>()
|
||||
|
||||
const isVisible = (columnId: string) => props.modelValue[columnId] !== false
|
||||
|
||||
const toggle = (columnId: string, value: boolean) => {
|
||||
emit('update:modelValue', { ...props.modelValue, [columnId]: value })
|
||||
}
|
||||
|
||||
const hiddenCount = computed(() => props.columns.filter(col => !isVisible(col.id)).length)
|
||||
|
||||
const groupedColumns = computed(() => {
|
||||
const groups = new Map<string | undefined, ColumnOption[]>()
|
||||
for (const column of props.columns) {
|
||||
const key = column.group
|
||||
if (!groups.has(key)) groups.set(key, [])
|
||||
groups.get(key)!.push(column)
|
||||
}
|
||||
return Array.from(groups.entries()).map(([label, columns]) => ({ label, columns }))
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Popover>
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="outline" size="sm" class="h-8 border-dashed">
|
||||
<Settings2 class="mr-2 h-4 w-4" />
|
||||
View
|
||||
<Badge
|
||||
v-if="hiddenCount > 0"
|
||||
variant="secondary"
|
||||
class="ml-2 rounded-sm px-1 font-normal"
|
||||
>
|
||||
{{ hiddenCount }}
|
||||
</Badge>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-[200px] p-0" align="end">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search columns..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No columns found.</CommandEmpty>
|
||||
<template v-for="group in groupedColumns" :key="group.label ?? '__default__'">
|
||||
<CommandGroup>
|
||||
<div v-if="group.label" class="px-2 py-1.5 text-xs font-medium text-muted-foreground">
|
||||
{{ group.label }}
|
||||
</div>
|
||||
<CommandSeparator v-if="group.label" />
|
||||
<CheckableCommandItem
|
||||
v-for="column in group.columns"
|
||||
:key="column.id"
|
||||
:value="column.id"
|
||||
:model-value="isVisible(column.id)"
|
||||
@update:model-value="(value) => toggle(column.id, value)"
|
||||
>
|
||||
<span>{{ column.label }}</span>
|
||||
</CheckableCommandItem>
|
||||
</CommandGroup>
|
||||
</template>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</template>
|
||||
Reference in New Issue
Block a user