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:
2026-07-17 08:45:06 +08:00
parent e8b26487db
commit cd3628a255
15 changed files with 1034 additions and 292 deletions
+65 -35
View File
@@ -16,6 +16,7 @@
:selected-asset="selectedAsset"
:is-detail-panel-enabled="isDetailPanelEnabled"
:show-thumbnails="showThumbnails"
:is-columns-locked="lockColumns"
@update:view-mode="viewMode = $event"
@update:category-filter="handleCategoryFilterChange"
@update:search="searchQuery = $event"
@@ -23,6 +24,7 @@
@update:show-thumbnails="handleThumbnailToggle"
@task-status-filter-changed="handleTaskStatusFilter"
@toggle-detail-panel="toggleDetailPanelEnabled"
@toggle-column-lock="toggleColumnLock"
@create-asset="showCreateDialog = true"
/>
<div v-else class="flex items-center justify-center py-4">
@@ -109,6 +111,7 @@
:sorting="sorting"
:column-visibility="columnVisibility"
:all-task-types="allTaskTypes"
:lock-columns="lockColumns"
@update:sorting="sorting = $event"
@update:column-visibility="handleColumnVisibilityChange"
@update:rowSelection="handleRowSelectionChange"
@@ -179,21 +182,27 @@
leave-from-class="translate-x-0"
leave-to-class="translate-x-full"
>
<div
v-if="showPanel && selectedAsset"
<div
v-if="showPanel && selectedAsset"
class="fixed right-0 top-[113px] bottom-0 w-96 bg-background border-l shadow-lg z-50 hidden lg:block overflow-y-auto"
>
<TaskDetailPanel
v-if="selectedTaskId"
:key="selectedTaskId"
:task-id="selectedTaskId"
:initial-tab="selectedTaskTab"
@close="selectedTaskId = null"
@task-updated="loadAssets"
/>
<AssetDetailPanel
v-else
:project-id="projectId"
:asset-id="selectedAsset.id"
:all-task-types="allTaskTypes"
@close="closeDetailPanel"
@edit="editAsset"
@delete="deleteAsset"
@create-task="handleCreateTask"
@select-task="handleSelectTask"
@create-note="handleCreateNote"
@upload-reference="handleUploadReference"
@publish-version="handlePublishVersion"
/>
</div>
</Transition>
@@ -201,18 +210,23 @@
<!-- Asset Detail Panel (Mobile) -->
<Sheet v-model:open="showMobileDetail">
<SheetContent side="right" class="w-full sm:max-w-md p-0">
<TaskDetailPanel
v-if="selectedTaskId"
:key="selectedTaskId"
:task-id="selectedTaskId"
:initial-tab="selectedTaskTab"
@close="selectedTaskId = null"
@task-updated="loadAssets"
/>
<AssetDetailPanel
v-if="selectedAsset"
v-else-if="selectedAsset"
:project-id="projectId"
:asset-id="selectedAsset.id"
:all-task-types="allTaskTypes"
@close="closeDetailPanel"
@edit="editAsset"
@delete="deleteAsset"
@create-task="handleCreateTask"
@select-task="handleSelectTask"
@create-note="handleCreateNote"
@upload-reference="handleUploadReference"
@publish-version="handlePublishVersion"
/>
</SheetContent>
</Sheet>
@@ -244,6 +258,7 @@ import AssetCard from "./AssetCard.vue";
import AssetForm from "./AssetForm.vue";
import AssetDetailPanel from "./AssetDetailPanel.vue";
import AssetDeleteConfirmDialog from "./AssetDeleteConfirmDialog.vue";
import TaskDetailPanel from "@/components/task/TaskDetailPanel.vue";
import AssetsDataTable from "./AssetsDataTable.vue";
import AssetTableToolbar from "./AssetTableToolbar.vue";
import { createAssetColumns, type AssetColumnMeta } from "./columns";
@@ -376,6 +391,13 @@ const showThumbnails = ref(
sessionStorage.getItem('assetBrowser.showThumbnails') === 'true'
);
// Lock (freeze) the first columns (select/thumbnail/name) for horizontal scroll
const lockColumns = ref(localStorage.getItem('asset-columns-locked') === 'true');
const toggleColumnLock = () => {
lockColumns.value = !lockColumns.value;
localStorage.setItem('asset-columns-locked', String(lockColumns.value));
};
// Computed properties
const assets = computed(() => assetsStore.assets);
const isLoading = computed(() => assetsStore.isLoading);
@@ -435,6 +457,7 @@ const assetColumns = computed(() => {
onDelete: deleteAsset,
onViewTasks: viewAssetTasks,
onTaskStatusUpdated: handleTaskStatusUpdate,
onTaskAssignmentUpdated: handleTaskAssignmentUpdated,
onBulkTaskStatusChange: handleBulkTaskStatusChange,
getSelectedCount: () => selectedCount.value,
getAllStatusOptions: () => taskStatusesStore.getAllStatusOptions(props.projectId)
@@ -564,8 +587,8 @@ const deleteAsset = async (asset: Asset) => {
};
const viewAssetTasks = (asset: Asset) => {
// TODO: Navigate to asset tasks view
console.log("View tasks for asset:", asset.name);
// Open the asset's own detail panel (Infos tab, already the default, lists its tasks)
selectAsset(asset);
};
const handleCreateAsset = async (assetData: AssetCreate | AssetUpdate) => {
@@ -661,6 +684,24 @@ const handleTaskStatusUpdate = async (
}
};
const handleTaskAssignmentUpdated = (assetId: number, taskType: string, userId: number | null) => {
// Update local state instead of reloading all assets
const asset = assetsStore.assets.find((a) => a.id === assetId);
if (asset && asset.task_details) {
const taskDetail = asset.task_details.find((detail) => detail.task_type === taskType);
if (taskDetail) {
taskDetail.assigned_user_id = userId ?? undefined;
}
}
toast({
title: "Task assignment updated",
description: userId
? `${formatTaskType(taskType)} task assigned successfully.`
: `${formatTaskType(taskType)} task unassigned successfully.`,
});
};
const clearFilters = () => {
selectedCategory.value = "all";
searchQuery.value = "";
@@ -678,30 +719,19 @@ const formatTaskType = (taskType: string) => {
};
// Detail panel event handlers
const handleCreateTask = () => {
// TODO: Navigate to task creation for this asset
console.log('Create task for asset:', selectedAsset.value?.name);
const selectedTaskId = ref<number | null>(null);
const selectedTaskTab = ref<string>('infos');
const handleSelectTask = (task: { id: number }, tab?: string) => {
selectedTaskId.value = task.id;
selectedTaskTab.value = tab || 'infos';
};
const handleSelectTask = (task: any) => {
// TODO: Open task detail panel
console.log('Select task:', task);
};
const handleCreateNote = () => {
// TODO: Open note creation dialog
console.log('Create note for asset:', selectedAsset.value?.name);
};
const handleUploadReference = () => {
// TODO: Open reference upload dialog
console.log('Upload reference for asset:', selectedAsset.value?.name);
};
const handlePublishVersion = () => {
// TODO: Open version publish dialog
console.log('Publish version for asset:', selectedAsset.value?.name);
};
// Reset the task sub-panel whenever the asset selection changes (including close)
watch(selectedAsset, () => {
selectedTaskId.value = null;
selectedTaskTab.value = 'infos';
});
// Load custom task types from project
const loadCustomTaskTypes = async () => {