Consistent status colors, prevent native context menu leaks, fix settings 404
- Task status badges (bulk change-status popover, right-click menu) now use the real per-project TaskStatusesStore colors/labels instead of the hardcoded TaskStatus enum, matching EditableTaskStatus everywhere. - Suppress the native browser context menu from leaking through gaps in the task table (header/empty space), open popovers/dropdowns and their submenus, the status Select, and a second right-click at the same coordinates (the popover's positioning anchor now has pointer-events: none so it doesn't intercept the repeat click). - Fix 404s on /api/settings/* caused by a double-declared route prefix. - Fix 422 on bulk-applying a custom task status by relaxing BulkStatusUpdate.status from the TaskStatus enum to str, matching the existing single-task update schema. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@ from schemas.global_settings import (
|
||||
from utils.auth import get_current_user, require_admin_permission
|
||||
from models.user import User
|
||||
|
||||
router = APIRouter(prefix="/settings", tags=["settings"])
|
||||
router = APIRouter(tags=["settings"])
|
||||
|
||||
# Default upload limit in MB (1GB)
|
||||
DEFAULT_UPLOAD_LIMIT_MB = 1000
|
||||
|
||||
@@ -214,7 +214,7 @@ class ReviewResponse(ReviewBase):
|
||||
# Bulk action schemas
|
||||
class BulkStatusUpdate(BaseModel):
|
||||
task_ids: List[int] = Field(..., min_length=1)
|
||||
status: TaskStatus
|
||||
status: str # Changed from TaskStatus enum to str to support custom statuses
|
||||
|
||||
|
||||
class BulkAssignment(BaseModel):
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<!-- <TaskStatusBadge :status="currentStatusObject"/> -->
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent @contextmenu.prevent>
|
||||
<!-- System Statuses -->
|
||||
<SelectItem
|
||||
v-for="statusOption in allStatusOptions"
|
||||
|
||||
@@ -100,6 +100,7 @@ import { projectService, type ProjectMember } from '@/services/project'
|
||||
import { shotService, type Shot } from '@/services/shot'
|
||||
import { assetService, type Asset } from '@/services/asset'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useTaskStatusesStore } from '@/stores/taskStatuses'
|
||||
import { useDetailPanel } from '@/composables/useDetailPanel'
|
||||
|
||||
interface Props {
|
||||
@@ -110,6 +111,7 @@ const props = defineProps<Props>()
|
||||
|
||||
const { toast } = useToast()
|
||||
const authStore = useAuthStore()
|
||||
const taskStatusesStore = useTaskStatusesStore()
|
||||
|
||||
// Detail panel composable
|
||||
const {
|
||||
@@ -462,12 +464,28 @@ const loadColumnVisibility = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const loadTaskStatuses = async () => {
|
||||
if (!props.projectId) return
|
||||
|
||||
try {
|
||||
await taskStatusesStore.fetchProjectStatuses(props.projectId)
|
||||
} catch (err) {
|
||||
console.error('Failed to load task statuses:', err)
|
||||
toast({
|
||||
title: 'Failed to load task statuses',
|
||||
description: err instanceof Error ? err.message : 'Task status options may be unavailable',
|
||||
variant: 'destructive'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
loadColumnVisibility()
|
||||
fetchTasks()
|
||||
fetchEpisodes()
|
||||
fetchProjectMembers()
|
||||
loadTaskStatuses()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -482,6 +500,7 @@ watch(
|
||||
fetchTasks()
|
||||
fetchEpisodes()
|
||||
fetchProjectMembers()
|
||||
loadTaskStatuses()
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<Popover v-model:open="isOpen">
|
||||
<!-- pointer-events: none — this is a positioning reference only. Without it, it's a
|
||||
real (if invisible) 1x1px element sitting exactly at the original right-click
|
||||
point, so a second right-click at the same spot hits *it* directly instead of
|
||||
passing through to the row (or anything else) underneath. -->
|
||||
<PopoverAnchor
|
||||
:style="{
|
||||
position: 'fixed',
|
||||
@@ -7,6 +11,7 @@
|
||||
top: `${props.position.y}px`,
|
||||
width: '1px',
|
||||
height: '1px',
|
||||
pointerEvents: 'none',
|
||||
}"
|
||||
/>
|
||||
<PopoverContent
|
||||
@@ -15,6 +20,10 @@
|
||||
:align="'start'"
|
||||
@interact-outside="handleInteractOutside"
|
||||
>
|
||||
<!-- Plain native div, not a component prop-forward chain — guarantees the
|
||||
listener actually reaches the DOM to suppress the browser's own menu
|
||||
when right-clicking anywhere on this (already-a-right-click-triggered) menu. -->
|
||||
<div @contextmenu.prevent>
|
||||
<!-- Header showing selection count -->
|
||||
<div class="px-2 py-1.5 text-sm font-semibold text-muted-foreground border-b mb-1">
|
||||
{{ selectedCount }} {{ selectedCount === 1 ? 'task' : 'tasks' }} selected
|
||||
@@ -37,51 +46,43 @@
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent class="w-48" side="right" align="start">
|
||||
<!-- Loading state -->
|
||||
<div v-if="isLoadingStatuses" class="px-2 py-1.5 text-sm text-muted-foreground">
|
||||
Loading statuses...
|
||||
<div @contextmenu.prevent>
|
||||
<!-- Loading state -->
|
||||
<div v-if="isLoadingStatuses" class="px-2 py-1.5 text-sm text-muted-foreground">
|
||||
Loading statuses...
|
||||
</div>
|
||||
|
||||
<!-- System statuses -->
|
||||
<template v-else>
|
||||
<div v-if="systemStatuses.length > 0" class="px-2 py-1 text-xs font-semibold text-muted-foreground">
|
||||
System Statuses
|
||||
</div>
|
||||
<DropdownMenuItem
|
||||
v-for="status in systemStatuses"
|
||||
:key="status.id"
|
||||
:disabled="isProcessing"
|
||||
@click="handleStatusSelected(status.id)"
|
||||
>
|
||||
<TaskStatusBadge :status="status" compact />
|
||||
</DropdownMenuItem>
|
||||
|
||||
<!-- Divider if both system and custom statuses exist -->
|
||||
<div v-if="systemStatuses.length > 0 && customStatuses.length > 0" class="h-px bg-border my-1" />
|
||||
|
||||
<!-- Custom statuses -->
|
||||
<div v-if="customStatuses.length > 0" class="px-2 py-1 text-xs font-semibold text-muted-foreground">
|
||||
Custom Statuses
|
||||
</div>
|
||||
<DropdownMenuItem
|
||||
v-for="status in customStatuses"
|
||||
:key="status.id"
|
||||
:disabled="isProcessing"
|
||||
@click="handleStatusSelected(status.id)"
|
||||
>
|
||||
<TaskStatusBadge :status="status" compact />
|
||||
</DropdownMenuItem>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- System statuses -->
|
||||
<template v-else>
|
||||
<div v-if="systemStatuses.length > 0" class="px-2 py-1 text-xs font-semibold text-muted-foreground">
|
||||
System Statuses
|
||||
</div>
|
||||
<DropdownMenuItem
|
||||
v-for="status in systemStatuses"
|
||||
:key="status.id"
|
||||
:disabled="isProcessing"
|
||||
@click="handleStatusSelected(status.id)"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<div
|
||||
class="w-2 h-2 rounded-full flex-shrink-0"
|
||||
:style="{ backgroundColor: status.color }"
|
||||
/>
|
||||
<span>{{ status.name }}</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<!-- Divider if both system and custom statuses exist -->
|
||||
<div v-if="systemStatuses.length > 0 && customStatuses.length > 0" class="h-px bg-border my-1" />
|
||||
|
||||
<!-- Custom statuses -->
|
||||
<div v-if="customStatuses.length > 0" class="px-2 py-1 text-xs font-semibold text-muted-foreground">
|
||||
Custom Statuses
|
||||
</div>
|
||||
<DropdownMenuItem
|
||||
v-for="status in customStatuses"
|
||||
:key="status.id"
|
||||
:disabled="isProcessing"
|
||||
@click="handleStatusSelected(status.id)"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<div
|
||||
class="w-2 h-2 rounded-full flex-shrink-0"
|
||||
:style="{ backgroundColor: status.color }"
|
||||
/>
|
||||
<span>{{ status.name }}</span>
|
||||
</DropdownMenuItem>
|
||||
</template>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -89,7 +90,7 @@
|
||||
<div class="h-px bg-border my-1" />
|
||||
|
||||
<!-- Assign To submenu -->
|
||||
<DropdownMenu>
|
||||
<DropdownMenu @update:open="onAssignMenuOpenChange">
|
||||
<DropdownMenuTrigger as-child>
|
||||
<button
|
||||
:disabled="isProcessing || hasMultipleProjects"
|
||||
@@ -99,20 +100,73 @@
|
||||
<ChevronRight class="h-4 w-4" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent class="w-48 max-h-48 overflow-y-auto" side="right" align="start">
|
||||
<div v-if="projectMembers.length === 0" class="px-2 py-1.5 text-sm text-muted-foreground">
|
||||
No members available
|
||||
<DropdownMenuContent class="w-56 p-0" side="right" align="start">
|
||||
<div @contextmenu.prevent>
|
||||
<div class="p-1.5 border-b" @keydown.stop @click.stop>
|
||||
<div class="relative">
|
||||
<Search class="absolute left-2 top-1/2 -translate-y-1/2 h-3 w-3 text-muted-foreground" />
|
||||
<Input
|
||||
v-model="assigneeSearchQuery"
|
||||
placeholder="Search members..."
|
||||
class="h-7 pl-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="max-h-56 overflow-y-auto p-1">
|
||||
<div v-if="projectMembers.length === 0" class="px-2 py-1.5 text-sm text-muted-foreground">
|
||||
No members available
|
||||
</div>
|
||||
<div v-else-if="filteredMembers.length === 0" class="px-2 py-1.5 text-sm text-muted-foreground">
|
||||
No matching members
|
||||
</div>
|
||||
<template v-else>
|
||||
<div v-if="recommendedMembers.length > 0" class="px-2 py-1 text-xs font-semibold text-muted-foreground">
|
||||
Recommended
|
||||
</div>
|
||||
<DropdownMenuItem
|
||||
v-for="member in recommendedMembers"
|
||||
:key="member.user_id"
|
||||
:disabled="isProcessing"
|
||||
@click="handleAssigneeSelected(member.user_id)"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<Avatar class="h-6 w-6 flex-shrink-0">
|
||||
<AvatarImage :src="getAvatarUrl(member.user_avatar_url, member.user_first_name, member.user_last_name)" />
|
||||
<AvatarFallback class="text-[9px]">{{ getMemberInitials(member) }}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div class="flex flex-col min-w-0">
|
||||
<span class="truncate">{{ member.user_first_name }} {{ member.user_last_name }}</span>
|
||||
<span v-if="member.department_role" class="text-[10px] text-muted-foreground capitalize">{{ member.department_role }}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<div v-if="recommendedMembers.length > 0 && otherMembers.length > 0" class="h-px bg-border my-1" />
|
||||
|
||||
<div v-if="otherMembers.length > 0" class="px-2 py-1 text-xs font-semibold text-muted-foreground">
|
||||
{{ recommendedMembers.length > 0 ? 'All Members' : 'Members' }}
|
||||
</div>
|
||||
<DropdownMenuItem
|
||||
v-for="member in otherMembers"
|
||||
:key="member.user_id"
|
||||
:disabled="isProcessing"
|
||||
@click="handleAssigneeSelected(member.user_id)"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<Avatar class="h-6 w-6 flex-shrink-0">
|
||||
<AvatarImage :src="getAvatarUrl(member.user_avatar_url, member.user_first_name, member.user_last_name)" />
|
||||
<AvatarFallback class="text-[9px]">{{ getMemberInitials(member) }}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div class="flex flex-col min-w-0">
|
||||
<span class="truncate">{{ member.user_first_name }} {{ member.user_last_name }}</span>
|
||||
<span v-if="member.department_role" class="text-[10px] text-muted-foreground capitalize">{{ member.department_role }}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<DropdownMenuItem
|
||||
v-for="member in projectMembers"
|
||||
:key="member.user_id"
|
||||
:disabled="isProcessing"
|
||||
@click="handleAssigneeSelected(member.user_id)"
|
||||
>
|
||||
{{ member.user_first_name }} {{ member.user_last_name }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</template>
|
||||
@@ -130,11 +184,15 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { ChevronRight } from 'lucide-vue-next'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import TaskStatusBadge from '@/components/task/TaskStatusBadge.vue'
|
||||
import { ChevronRight, Search } from 'lucide-vue-next'
|
||||
import type { ProjectMember } from '@/services/project'
|
||||
import type { Task } from '@/services/task'
|
||||
import { customTaskStatusService, type CustomTaskStatus, type SystemTaskStatus } from '@/services/customTaskStatus'
|
||||
import { useTaskStatusesStore } from '@/stores/taskStatuses'
|
||||
import { useAvatarUrl } from '@/composables/useAvatarUrl'
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
@@ -159,10 +217,14 @@ const emit = defineEmits<Emits>()
|
||||
|
||||
// Use the shared task statuses store
|
||||
const taskStatusesStore = useTaskStatusesStore()
|
||||
const { getAvatarUrl } = useAvatarUrl()
|
||||
|
||||
// Local state for menu open/close
|
||||
const isOpen = ref(props.open)
|
||||
|
||||
// Assign To search state
|
||||
const assigneeSearchQuery = ref('')
|
||||
|
||||
// Computed properties
|
||||
const hasMultipleProjects = computed(() => {
|
||||
if (props.selectedTasks.length === 0) return false
|
||||
@@ -193,6 +255,54 @@ const customStatuses = computed(() => {
|
||||
return statuses?.statuses || []
|
||||
})
|
||||
|
||||
// Task types among the selected tasks, used to recommend project members whose
|
||||
// department matches. "composite" (department) vs. "compositing" (task type) is
|
||||
// the one known vocabulary mismatch, handled explicitly below.
|
||||
const selectedTaskTypes = computed(() => new Set(props.selectedTasks.map(task => task.task_type)))
|
||||
|
||||
const departmentMatchesSelection = (departmentRole: string | undefined | null): boolean => {
|
||||
if (!departmentRole) return false
|
||||
for (const taskType of selectedTaskTypes.value) {
|
||||
if (departmentRole === taskType) return true
|
||||
if (departmentRole === 'composite' && taskType === 'compositing') return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const filteredMembers = computed(() => {
|
||||
const query = assigneeSearchQuery.value.toLowerCase().trim()
|
||||
if (!query) return props.projectMembers
|
||||
|
||||
return props.projectMembers.filter(member => {
|
||||
const fullName = `${member.user_first_name} ${member.user_last_name}`.toLowerCase()
|
||||
const department = member.department_role?.toLowerCase() || ''
|
||||
return fullName.includes(query) || department.includes(query)
|
||||
})
|
||||
})
|
||||
|
||||
// Members whose department matches the selected task(s) are surfaced first, but
|
||||
// everyone remains selectable — department_role is often unset, and a hard filter
|
||||
// would leave the list empty for many projects.
|
||||
const recommendedMembers = computed(() =>
|
||||
filteredMembers.value.filter(member => departmentMatchesSelection(member.department_role))
|
||||
)
|
||||
|
||||
const otherMembers = computed(() =>
|
||||
filteredMembers.value.filter(member => !departmentMatchesSelection(member.department_role))
|
||||
)
|
||||
|
||||
const getMemberInitials = (member: ProjectMember): string => {
|
||||
const first = member.user_first_name?.charAt(0) || ''
|
||||
const last = member.user_last_name?.charAt(0) || ''
|
||||
return (first + last).toUpperCase()
|
||||
}
|
||||
|
||||
const onAssignMenuOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
assigneeSearchQuery.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// Methods
|
||||
const fetchStatuses = async () => {
|
||||
if (!currentProjectId.value || hasMultipleProjects.value) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="px-4 h-full flex flex-col">
|
||||
<div class="rounded-md border flex-1 min-h-0 overflow-hidden">
|
||||
<div class="rounded-md border flex-1 min-h-0 overflow-hidden" @contextmenu.prevent>
|
||||
<Table>
|
||||
<TableHeader class="sticky top-0 z-10 bg-background">
|
||||
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
|
||||
@@ -88,6 +88,7 @@ import {
|
||||
import { createColumns } from './columns'
|
||||
import { type Task } from '@/services/task'
|
||||
import { TaskStatus } from '@/services/asset'
|
||||
import { useTaskStatusesStore } from '@/stores/taskStatuses'
|
||||
|
||||
// Props interface
|
||||
interface Props {
|
||||
@@ -112,6 +113,8 @@ interface Emits {
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const taskStatusesStore = useTaskStatusesStore()
|
||||
|
||||
// Internal state
|
||||
const sorting = ref<SortingState>([{ id: 'created_at', desc: true }])
|
||||
const rowSelection = ref<RowSelectionState>({})
|
||||
@@ -141,6 +144,14 @@ const columns = createColumns({
|
||||
// row-double-click actually opens the panel, so reuse that instead.
|
||||
onViewDetails: (task: Task) => emit('row-double-click', task),
|
||||
onReassign: (task: Task) => emit('row-double-click', task),
|
||||
// Real per-project statuses (system + custom), matching EditableTaskStatus's cell
|
||||
// rendering. Scoped to the selected rows' project (falls back to props.projectId,
|
||||
// which is 0/unset for cross-project lists like My Tasks with no selection yet).
|
||||
getAllStatusOptions: () => {
|
||||
const selected = getSelectedTasks()
|
||||
const projectId = selected.length > 0 ? selected[0].project_id : props.projectId
|
||||
return projectId ? taskStatusesStore.getAllStatusOptions(projectId) : []
|
||||
},
|
||||
})
|
||||
|
||||
// TanStack Table configuration
|
||||
|
||||
@@ -15,10 +15,11 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import TaskStatusBadge from '@/components/asset/TaskStatusBadge.vue'
|
||||
import TaskStatusBadge from '@/components/task/TaskStatusBadge.vue'
|
||||
import EditableTaskStatus from '@/components/task/EditableTaskStatus.vue'
|
||||
import { type Task } from '@/services/task'
|
||||
import { TaskStatus } from '@/services/asset'
|
||||
import type { CustomTaskStatus, SystemTaskStatus } from '@/services/customTaskStatus'
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString)
|
||||
@@ -35,6 +36,10 @@ interface ColumnCallbacks {
|
||||
getSelectedCount?: () => number
|
||||
onViewDetails?: (task: Task) => void
|
||||
onReassign?: (task: Task) => void
|
||||
// Real per-project status options (system + custom), matching what EditableTaskStatus
|
||||
// shows in the cell — falls back to [] if the relevant project's statuses aren't
|
||||
// loaded yet.
|
||||
getAllStatusOptions?: () => Array<CustomTaskStatus | SystemTaskStatus>
|
||||
}
|
||||
|
||||
export const createColumns = (callbacks?: ColumnCallbacks): ColumnDef<Task>[] => {
|
||||
@@ -150,10 +155,14 @@ export const createColumns = (callbacks?: ColumnCallbacks): ColumnDef<Task>[] =>
|
||||
}),
|
||||
h(PopoverContent, { class: 'w-48 p-2', align: 'start' }, {
|
||||
default: () => {
|
||||
|
||||
return h('div', { class: 'flex flex-col gap-1' }, [
|
||||
const allStatusOptions = callbacks?.getAllStatusOptions?.() || []
|
||||
|
||||
// Plain native onContextmenu (not relying on the .prevent modifier
|
||||
// reaching through PopoverContent's attrs-forwarding) so right-clicking
|
||||
// this popover doesn't fall through to the browser's own menu.
|
||||
return h('div', { class: 'flex flex-col gap-1', onContextmenu: (e: Event) => e.preventDefault() }, [
|
||||
h('div', { class: 'px-2 py-1.5 text-sm font-semibold' }, `Change Status`),
|
||||
...Object.values(TaskStatus).map((status) =>
|
||||
...allStatusOptions.map((statusOption) =>
|
||||
h(
|
||||
Button,
|
||||
{
|
||||
@@ -161,11 +170,11 @@ export const createColumns = (callbacks?: ColumnCallbacks): ColumnDef<Task>[] =>
|
||||
size: 'sm',
|
||||
class: 'justify-start',
|
||||
onClick: () => {
|
||||
callbacks?.onBulkStatusChange?.(status)
|
||||
callbacks?.onBulkStatusChange?.(statusOption.id as TaskStatus)
|
||||
isPopoverOpen.value = false
|
||||
},
|
||||
},
|
||||
() => h(TaskStatusBadge, { status, class: 'w-full' })
|
||||
() => h(TaskStatusBadge, { status: statusOption, compact: true, class: 'w-full' })
|
||||
)
|
||||
),
|
||||
])
|
||||
@@ -352,7 +361,11 @@ export const createColumns = (callbacks?: ColumnCallbacks): ColumnDef<Task>[] =>
|
||||
DropdownMenuContent,
|
||||
{ align: 'end' },
|
||||
{
|
||||
default: () => [
|
||||
// Native div wrapper (not a prop on DropdownMenuContent itself) — that
|
||||
// component doesn't explicitly forward arbitrary attrs the way
|
||||
// PopoverContent does, so a listener placed directly on it isn't
|
||||
// guaranteed to reach the real DOM element.
|
||||
default: () => h('div', { onContextmenu: (e: Event) => e.preventDefault() }, [
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
@@ -385,7 +398,7 @@ export const createColumns = (callbacks?: ColumnCallbacks): ColumnDef<Task>[] =>
|
||||
],
|
||||
}
|
||||
),
|
||||
],
|
||||
]),
|
||||
}
|
||||
),
|
||||
],
|
||||
|
||||
@@ -125,11 +125,13 @@ import { TaskStatus } from '@/services/asset'
|
||||
import { episodeService, type Episode } from '@/services/episode'
|
||||
import { projectService, type Project, type ProjectMember } from '@/services/project'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useTaskStatusesStore } from '@/stores/taskStatuses'
|
||||
import { useDetailPanel } from '@/composables/useDetailPanel'
|
||||
import { useToast } from '@/components/ui/toast/use-toast'
|
||||
|
||||
const { toast } = useToast()
|
||||
const authStore = useAuthStore()
|
||||
const taskStatusesStore = useTaskStatusesStore()
|
||||
|
||||
// Detail panel composable
|
||||
const {
|
||||
@@ -307,6 +309,14 @@ async function fetchProjectScopedFilters() {
|
||||
episodes.value = []
|
||||
projectMembers.value = []
|
||||
}
|
||||
|
||||
// Ensures the "Change Status" table-header dropdown has real per-project status
|
||||
// options ready as soon as a project is picked, not just once a row happens to render.
|
||||
try {
|
||||
await taskStatusesStore.fetchProjectStatuses(projectId)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch task statuses:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRowClick = () => {
|
||||
|
||||
Reference in New Issue
Block a user