Phase 2: asset/shot feature parity, task actions, member management
Phase 2 of frontend_tasks.md - close feature gaps between domains: - Asset user-assignment popover, ported from shot's EditableTaskStatus, wired through columns.ts and AssetBrowser.vue. - Asset column-locking toggle: two-pane frozen-column layout ported into AssetsDataTable.vue (adapted for asset's page-scroll layout, which has no bounded-height container like shot's). - Task table row-actions menu (View Details + Reassign only - no Delete/Edit, since no backend support exists for either). - Real select-task/create-task behavior on asset and shot detail panels: clicking a task swaps in the actual TaskDetailPanel in place; "Add Task" opens a task-type picker that creates real tasks via the existing createAssetTask/createShotTask services. - create-note/upload-reference/publish-version implemented via a task-picker that deep-links into TaskDetailPanel's Notes/ Attachments/Submissions tabs (new initialTab prop), reusing the already-working task-level components instead of building three new bespoke forms. Also fixed shot's pre-existing dead "Add Note"/ "Upload Reference" buttons the same way. - Consolidated ProjectMembersManager.vue and ProjectMemberManagement.vue into one component, combining remove-confirmation and approved-user filtering with toast feedback and the shared Select/Dialog UI kit. - Wired ProjectDetailView's "Manage Members" to navigate to the project's Settings > Team tab. Also fixed a bug in this session's own new code: TaskBrowser.vue's row-click handler is a no-op by design, so the new row-actions menu needed to emit row-double-click (which actually opens the panel) instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -117,8 +117,111 @@
|
||||
|
||||
<!-- Task Status & Assignees -->
|
||||
<div class="space-y-3">
|
||||
<h3 class="text-sm font-semibold">Tasks</h3>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold">Tasks</h3>
|
||||
<div class="flex items-center gap-1">
|
||||
<!-- Add Task -->
|
||||
<Popover>
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="ghost" size="sm" class="h-7 w-7 p-0" title="Add Task" :disabled="availableTaskTypes.length === 0">
|
||||
<Plus class="h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-48 p-2" align="end">
|
||||
<div class="px-2 py-1.5 text-sm font-semibold">Add Task</div>
|
||||
<div v-if="availableTaskTypes.length === 0" class="px-2 py-2 text-xs text-muted-foreground">
|
||||
All task types already added
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-1 max-h-48 overflow-y-auto">
|
||||
<Button
|
||||
v-for="type in availableTaskTypes"
|
||||
:key="type"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="justify-start"
|
||||
:disabled="isCreatingTask"
|
||||
@click="handleAddTask(type)"
|
||||
>
|
||||
{{ formatTaskType(type) }}
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<!-- Add Note (picks a task, opens its Notes tab) -->
|
||||
<Popover>
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="ghost" size="sm" class="h-7 w-7 p-0" title="Add Note" :disabled="tasks.length === 0">
|
||||
<MessageSquarePlus class="h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-48 p-2" align="end">
|
||||
<div class="px-2 py-1.5 text-sm font-semibold">Add note to task</div>
|
||||
<div class="flex flex-col gap-1 max-h-48 overflow-y-auto">
|
||||
<Button
|
||||
v-for="task in tasks"
|
||||
:key="task.id"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="justify-start"
|
||||
@click="$emit('select-task', task, 'notes')"
|
||||
>
|
||||
{{ task.name }}
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<!-- Upload Reference (picks a task, opens its Attachments tab) -->
|
||||
<Popover>
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="ghost" size="sm" class="h-7 w-7 p-0" title="Upload Reference" :disabled="tasks.length === 0">
|
||||
<Paperclip class="h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-48 p-2" align="end">
|
||||
<div class="px-2 py-1.5 text-sm font-semibold">Upload reference to task</div>
|
||||
<div class="flex flex-col gap-1 max-h-48 overflow-y-auto">
|
||||
<Button
|
||||
v-for="task in tasks"
|
||||
:key="task.id"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="justify-start"
|
||||
@click="$emit('select-task', task, 'attachments')"
|
||||
>
|
||||
{{ task.name }}
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<!-- Publish Version (picks a task, opens its Submissions tab) -->
|
||||
<Popover>
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="ghost" size="sm" class="h-7 w-7 p-0" title="Publish Version" :disabled="tasks.length === 0">
|
||||
<Send class="h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-48 p-2" align="end">
|
||||
<div class="px-2 py-1.5 text-sm font-semibold">Publish version for task</div>
|
||||
<div class="flex flex-col gap-1 max-h-48 overflow-y-auto">
|
||||
<Button
|
||||
v-for="task in tasks"
|
||||
:key="task.id"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="justify-start"
|
||||
@click="$emit('select-task', task, 'submissions')"
|
||||
>
|
||||
{{ task.name }}
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading Tasks -->
|
||||
<div v-if="isLoading" class="flex items-center justify-center py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -182,17 +285,19 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import {
|
||||
AlertCircle, RefreshCw, X
|
||||
import {
|
||||
AlertCircle, RefreshCw, X, Plus, MessageSquarePlus, Paperclip, Send
|
||||
} from 'lucide-vue-next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import AssetNotes from './AssetNotes.vue'
|
||||
import AssetReferences from './AssetReferences.vue'
|
||||
|
||||
import { assetService, AssetStatus, type Asset, type TaskStatusInfo } from '@/services/asset'
|
||||
import { taskService } from '@/services/task'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
@@ -208,16 +313,13 @@ interface Task {
|
||||
interface Props {
|
||||
projectId: number
|
||||
assetId: number
|
||||
allTaskTypes: string[]
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'edit', asset: Asset): void
|
||||
(e: 'delete', asset: Asset): void
|
||||
(e: 'create-task'): void
|
||||
(e: 'select-task', task: Task): void
|
||||
(e: 'create-note'): void
|
||||
(e: 'upload-reference'): void
|
||||
(e: 'publish-version'): void
|
||||
(e: 'select-task', task: Task, tab?: string): void
|
||||
(e: 'close'): void
|
||||
}
|
||||
|
||||
@@ -233,6 +335,7 @@ const notes = ref<any[]>([])
|
||||
const references = ref<any[]>([])
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const isCreatingTask = ref(false)
|
||||
|
||||
// Computed properties
|
||||
const tasks = computed(() => {
|
||||
@@ -266,6 +369,11 @@ const progressPercentage = computed(() => {
|
||||
return Math.round((completedTasksCount.value / tasks.value.length) * 100)
|
||||
})
|
||||
|
||||
const availableTaskTypes = computed(() => {
|
||||
const existingTypes = new Set(tasks.value.map(task => task.task_type))
|
||||
return props.allTaskTypes.filter(type => !existingTypes.has(type))
|
||||
})
|
||||
|
||||
const taskStatusCounts = computed(() => {
|
||||
const counts = {
|
||||
not_started: 0,
|
||||
@@ -365,6 +473,18 @@ const loadReferences = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddTask = async (taskType: string) => {
|
||||
isCreatingTask.value = true
|
||||
try {
|
||||
await taskService.createAssetTask(props.assetId, taskType)
|
||||
await loadAssetDetails()
|
||||
} catch (err) {
|
||||
console.error('Failed to create task:', err)
|
||||
} finally {
|
||||
isCreatingTask.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const formatCategory = (category: string) => {
|
||||
return category.charAt(0).toUpperCase() + category.slice(1)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user