Init Repo
This commit is contained in:
@@ -0,0 +1,801 @@
|
||||
<template>
|
||||
<div class="relative h-full">
|
||||
<!-- Main Content (Full Width) -->
|
||||
<div class="space-y-4">
|
||||
<!-- Toolbar - Sticky -->
|
||||
<div class="sticky top-0 z-10 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b pb-4 px-4 sm:px-6 pt-4 sm:pt-6 mb-4">
|
||||
<AssetTableToolbar
|
||||
v-if="customTaskTypesLoaded"
|
||||
:view-mode="viewMode"
|
||||
:category-filter="selectedCategory"
|
||||
:search="searchQuery"
|
||||
:column-visibility="columnVisibility"
|
||||
:categories="categories"
|
||||
:all-task-types="allTaskTypes"
|
||||
:project-id="projectId"
|
||||
:selected-asset="selectedAsset"
|
||||
:is-detail-panel-enabled="isDetailPanelEnabled"
|
||||
:show-thumbnails="showThumbnails"
|
||||
@update:view-mode="viewMode = $event"
|
||||
@update:category-filter="handleCategoryFilterChange"
|
||||
@update:search="searchQuery = $event"
|
||||
@update:column-visibility="handleColumnVisibilityChange"
|
||||
@update:show-thumbnails="handleThumbnailToggle"
|
||||
@task-status-filter-changed="handleTaskStatusFilter"
|
||||
@toggle-detail-panel="toggleDetailPanelEnabled"
|
||||
@create-asset="showCreateDialog = true"
|
||||
/>
|
||||
<div v-else class="flex items-center justify-center py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-primary"></div>
|
||||
<span class="text-sm text-muted-foreground">Loading toolbar...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="isLoading" class="flex items-center justify-center py-12">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"
|
||||
></div>
|
||||
<span class="text-muted-foreground">Loading assets...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div v-else-if="error" class="text-center py-12">
|
||||
<AlertCircle class="h-12 w-12 mx-auto text-destructive mb-4" />
|
||||
<h3 class="text-lg font-semibold mb-2">Failed to load assets</h3>
|
||||
<p class="text-muted-foreground mb-4">{{ error }}</p>
|
||||
<Button @click="loadAssets" variant="outline">
|
||||
<RefreshCw class="h-4 w-4 mr-2" />
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div
|
||||
v-else-if="
|
||||
filteredAssets.length === 0 && !searchQuery && !selectedCategory
|
||||
"
|
||||
class="text-center py-12"
|
||||
>
|
||||
<Package class="h-12 w-12 mx-auto text-muted-foreground mb-4" />
|
||||
<h3 class="text-lg font-semibold mb-2">No assets yet</h3>
|
||||
<p class="text-muted-foreground mb-4">
|
||||
Create your first asset to get started
|
||||
</p>
|
||||
<Button @click="showCreateDialog = true">
|
||||
<Plus class="h-4 w-4 mr-2" />
|
||||
Create Asset
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- No Results State -->
|
||||
<div v-else-if="filteredAssets.length === 0" class="text-center py-12">
|
||||
<Search class="h-12 w-12 mx-auto text-muted-foreground mb-4" />
|
||||
<h3 class="text-lg font-semibold mb-2">No assets found</h3>
|
||||
<p class="text-muted-foreground mb-4">
|
||||
Try adjusting your search or filter criteria
|
||||
</p>
|
||||
<Button @click="clearFilters" variant="outline"> Clear Filters </Button>
|
||||
</div>
|
||||
|
||||
<!-- Assets Grid/List -->
|
||||
<div v-else>
|
||||
<!-- Grid View -->
|
||||
<div
|
||||
v-if="viewMode === 'grid'"
|
||||
class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-3"
|
||||
>
|
||||
<AssetCard
|
||||
v-for="asset in filteredAssets"
|
||||
:key="asset.id"
|
||||
:asset="asset"
|
||||
:show-thumbnail="showThumbnails"
|
||||
@select="selectAsset"
|
||||
@edit="editAsset"
|
||||
@delete="deleteAsset"
|
||||
@view-tasks="viewAssetTasks"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Table View -->
|
||||
<AssetsDataTable
|
||||
v-else-if="customTaskTypesLoaded"
|
||||
:columns="assetColumns"
|
||||
:data="filteredAssets"
|
||||
:sorting="sorting"
|
||||
:column-visibility="columnVisibility"
|
||||
:all-task-types="allTaskTypes"
|
||||
@update:sorting="sorting = $event"
|
||||
@update:column-visibility="handleColumnVisibilityChange"
|
||||
@update:rowSelection="handleRowSelectionChange"
|
||||
@row-click="handleRowClick"
|
||||
/>
|
||||
<div v-else class="flex items-center justify-center py-12">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
|
||||
<span class="text-muted-foreground">Loading table...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Asset Dialog -->
|
||||
<Dialog v-model:open="showCreateDialog">
|
||||
<DialogContent class="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Asset</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add a new asset to the project. Assets can be characters, props,
|
||||
sets, or vehicles.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<AssetForm
|
||||
:project-id="projectId"
|
||||
:is-loading="isCreating"
|
||||
@submit="handleCreateAsset"
|
||||
@cancel="showCreateDialog = false"
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<!-- Edit Asset Dialog -->
|
||||
<Dialog v-model:open="showEditDialog">
|
||||
<DialogContent class="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Asset</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update the asset information and settings.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<AssetForm
|
||||
:project-id="projectId"
|
||||
:asset="selectedAsset || undefined"
|
||||
:is-loading="isUpdating"
|
||||
@submit="handleUpdateAsset"
|
||||
@cancel="showEditDialog = false"
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<!-- Delete Confirmation Dialog -->
|
||||
<AlertDialog v-model:open="showDeleteDialog">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Asset</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete "{{ selectedAsset?.name }}"? This
|
||||
action cannot be undone and will remove all associated tasks.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
@click="handleDeleteAsset"
|
||||
class="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Delete Asset
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
<!-- Asset Detail Panel (Desktop) with slide animation -->
|
||||
<Transition
|
||||
enter-active-class="transition-transform duration-300 ease-out"
|
||||
enter-from-class="translate-x-full"
|
||||
enter-to-class="translate-x-0"
|
||||
leave-active-class="transition-transform duration-300 ease-in"
|
||||
leave-from-class="translate-x-0"
|
||||
leave-to-class="translate-x-full"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<AssetDetailPanel
|
||||
:project-id="projectId"
|
||||
:asset-id="selectedAsset.id"
|
||||
@close="closeDetailPanel"
|
||||
@edit="editAsset"
|
||||
@delete="deleteAsset"
|
||||
@create-task="handleCreateTask"
|
||||
@select-task="handleSelectTask"
|
||||
@create-note="handleCreateNote"
|
||||
@upload-reference="handleUploadReference"
|
||||
@publish-version="handlePublishVersion"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- Asset Detail Panel (Mobile) -->
|
||||
<Sheet v-model:open="showMobileDetail">
|
||||
<SheetContent side="right" class="w-full sm:max-w-md p-0">
|
||||
<AssetDetailPanel
|
||||
v-if="selectedAsset"
|
||||
:project-id="projectId"
|
||||
:asset-id="selectedAsset.id"
|
||||
@close="closeDetailPanel"
|
||||
@edit="editAsset"
|
||||
@delete="deleteAsset"
|
||||
@create-task="handleCreateTask"
|
||||
@select-task="handleSelectTask"
|
||||
@create-note="handleCreateNote"
|
||||
@upload-reference="handleUploadReference"
|
||||
@publish-version="handlePublishVersion"
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from "vue";
|
||||
import {
|
||||
Search,
|
||||
Plus,
|
||||
Package,
|
||||
AlertCircle,
|
||||
RefreshCw,
|
||||
Users,
|
||||
Building,
|
||||
Car,
|
||||
} from "lucide-vue-next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
||||
import AssetCard from "./AssetCard.vue";
|
||||
import AssetForm from "./AssetForm.vue";
|
||||
import AssetDetailPanel from "./AssetDetailPanel.vue";
|
||||
import AssetsDataTable from "./AssetsDataTable.vue";
|
||||
import AssetTableToolbar from "./AssetTableToolbar.vue";
|
||||
import { createAssetColumns, type AssetColumnMeta } from "./columns";
|
||||
import { useAssetsStore } from "@/stores/assets";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useTaskStatusesStore } from "@/stores/taskStatuses";
|
||||
import { useDetailPanel } from "@/composables/useDetailPanel";
|
||||
import {
|
||||
AssetCategory,
|
||||
TaskStatus,
|
||||
type Asset,
|
||||
type AssetCreate,
|
||||
type AssetUpdate,
|
||||
} from "@/services/asset";
|
||||
import { useToast } from "@/components/ui/toast/use-toast";
|
||||
import type { SortingState, VisibilityState } from '@tanstack/vue-table';
|
||||
|
||||
interface Props {
|
||||
projectId: number;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
// Stores and composables
|
||||
const assetsStore = useAssetsStore();
|
||||
const authStore = useAuthStore();
|
||||
const taskStatusesStore = useTaskStatusesStore();
|
||||
const { toast } = useToast();
|
||||
|
||||
// TanStack Table state
|
||||
const sorting = ref<SortingState>([])
|
||||
const columnVisibility = ref<VisibilityState>({})
|
||||
const rowSelection = ref<Record<string, boolean>>({})
|
||||
|
||||
// Computed for selected count
|
||||
const selectedCount = computed(() => {
|
||||
return Object.keys(rowSelection.value).length
|
||||
})
|
||||
|
||||
const initializeColumnVisibility = () => {
|
||||
const stored = sessionStorage.getItem('assetBrowser.columnVisibility')
|
||||
if (stored) {
|
||||
try {
|
||||
const parsedVisibility = JSON.parse(stored)
|
||||
|
||||
// Ensure custom task types are included in stored visibility
|
||||
const updatedVisibility = { ...parsedVisibility }
|
||||
let hasChanges = false
|
||||
|
||||
for (const customType of customTaskTypes.value) {
|
||||
if (!(customType in updatedVisibility)) {
|
||||
updatedVisibility[customType] = true
|
||||
hasChanges = true
|
||||
}
|
||||
}
|
||||
|
||||
columnVisibility.value = updatedVisibility
|
||||
|
||||
if (hasChanges) {
|
||||
sessionStorage.setItem('assetBrowser.columnVisibility', JSON.stringify(updatedVisibility))
|
||||
}
|
||||
} catch {
|
||||
// Fall back to defaults
|
||||
setDefaultColumnVisibility()
|
||||
}
|
||||
} else {
|
||||
// Default visible columns
|
||||
setDefaultColumnVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
const setDefaultColumnVisibility = () => {
|
||||
const defaultVisibility: Record<string, boolean> = {
|
||||
name: true,
|
||||
category: true,
|
||||
status: true,
|
||||
thumbnail: false,
|
||||
modeling: true,
|
||||
surfacing: true,
|
||||
rigging: true,
|
||||
description: true,
|
||||
updatedAt: true
|
||||
}
|
||||
|
||||
// Add custom task types to default visibility if they exist
|
||||
for (const customType of customTaskTypes.value) {
|
||||
defaultVisibility[customType] = true
|
||||
}
|
||||
|
||||
columnVisibility.value = defaultVisibility
|
||||
}
|
||||
|
||||
// Don't initialize immediately - wait for custom task types to load
|
||||
// initializeColumnVisibility()
|
||||
|
||||
// Detail panel composable
|
||||
const {
|
||||
isDetailPanelEnabled,
|
||||
selectedEntity: selectedAsset,
|
||||
showMobileDetail,
|
||||
showPanel,
|
||||
toggleDetailPanelEnabled,
|
||||
closeDetailPanel,
|
||||
selectEntity: selectAsset,
|
||||
handleRowClick: handleRowClickComposable
|
||||
} = useDetailPanel<Asset>({
|
||||
isDialogOpen: () => showCreateDialog.value || showEditDialog.value || showDeleteDialog.value,
|
||||
sessionStorageKey: 'assetBrowser.detailPanelEnabled'
|
||||
})
|
||||
|
||||
// Reactive state - default to list view to show task status
|
||||
const viewMode = ref<"grid" | "list">("list");
|
||||
const selectedCategory = ref<AssetCategory | "all">("all");
|
||||
const searchQuery = ref("");
|
||||
const showCreateDialog = ref(false);
|
||||
const showEditDialog = ref(false);
|
||||
const showDeleteDialog = ref(false);
|
||||
const isCreating = ref(false);
|
||||
const isUpdating = ref(false);
|
||||
|
||||
const taskStatusFilter = ref('')
|
||||
|
||||
// Thumbnail display state - with session storage
|
||||
const showThumbnails = ref(
|
||||
sessionStorage.getItem('assetBrowser.showThumbnails') === 'true'
|
||||
);
|
||||
|
||||
// Computed properties
|
||||
const assets = computed(() => assetsStore.assets);
|
||||
const isLoading = computed(() => assetsStore.isLoading);
|
||||
const error = computed(() => assetsStore.error);
|
||||
|
||||
const categories = [
|
||||
{ value: AssetCategory.CHARACTERS, label: "Characters", icon: Users },
|
||||
{ value: AssetCategory.PROPS, label: "Props", icon: Package },
|
||||
{ value: AssetCategory.SETS, label: "Sets", icon: Building },
|
||||
{ value: AssetCategory.VEHICLES, label: "Vehicles", icon: Car },
|
||||
];
|
||||
|
||||
const filteredAssets = computed(() => {
|
||||
let filtered = [...(assets.value || [])];
|
||||
|
||||
// Filter out soft deleted assets unless user is admin
|
||||
if (!authStore.isAdmin) {
|
||||
filtered = filtered.filter(asset => !asset.deleted_at);
|
||||
}
|
||||
|
||||
// Filter by category
|
||||
if (selectedCategory.value && selectedCategory.value !== "all") {
|
||||
filtered = filtered.filter(
|
||||
(asset) => asset.category === selectedCategory.value
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by search query
|
||||
if (searchQuery.value.trim()) {
|
||||
const query = searchQuery.value.toLowerCase().trim();
|
||||
filtered = filtered.filter(
|
||||
(asset) =>
|
||||
asset.name.toLowerCase().includes(query) ||
|
||||
asset.description?.toLowerCase().includes(query) ||
|
||||
asset.category.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
});
|
||||
|
||||
// Custom task types from project
|
||||
const customTaskTypes = ref<string[]>([]);
|
||||
const customTaskTypesLoaded = ref(false);
|
||||
|
||||
// All available task types for assets (standard + custom)
|
||||
const allTaskTypes = computed(() => {
|
||||
return ["modeling", "surfacing", "rigging", ...customTaskTypes.value];
|
||||
});
|
||||
|
||||
// Asset columns for TanStack Table
|
||||
const assetColumns = computed(() => {
|
||||
const meta: AssetColumnMeta = {
|
||||
projectId: props.projectId,
|
||||
categories: categories,
|
||||
onEdit: editAsset,
|
||||
onDelete: deleteAsset,
|
||||
onViewTasks: viewAssetTasks,
|
||||
onTaskStatusUpdated: handleTaskStatusUpdate,
|
||||
onBulkTaskStatusChange: handleBulkTaskStatusChange,
|
||||
getSelectedCount: () => selectedCount.value,
|
||||
getAllStatusOptions: () => taskStatusesStore.getAllStatusOptions(props.projectId)
|
||||
}
|
||||
|
||||
return createAssetColumns(allTaskTypes.value, meta);
|
||||
})
|
||||
|
||||
const loadAssets = async () => {
|
||||
console.log("AssetBrowser - Loading assets for project:", props.projectId);
|
||||
console.log(
|
||||
"AssetBrowser - Current assets count before fetch:",
|
||||
assetsStore.assets.length
|
||||
);
|
||||
try {
|
||||
const options: any = {};
|
||||
|
||||
if (selectedCategory.value && selectedCategory.value !== 'all') {
|
||||
options.category = selectedCategory.value;
|
||||
}
|
||||
|
||||
if (taskStatusFilter.value) {
|
||||
options.taskStatusFilter = taskStatusFilter.value;
|
||||
}
|
||||
|
||||
// Convert TanStack Table sorting to API format
|
||||
if (sorting.value.length > 0) {
|
||||
const sort = sorting.value[0]
|
||||
options.sortBy = sort.id;
|
||||
options.sortDirection = sort.desc ? 'desc' : 'asc';
|
||||
}
|
||||
|
||||
await assetsStore.fetchAssets(props.projectId, options);
|
||||
console.log(
|
||||
"AssetBrowser - Assets loaded successfully:",
|
||||
assetsStore.assets.length
|
||||
);
|
||||
console.log("AssetBrowser - Assets data:", assetsStore.assets);
|
||||
console.log("AssetBrowser - View mode:", viewMode.value);
|
||||
console.log("AssetBrowser - Visible columns:", columnVisibility.value);
|
||||
if (assetsStore.assets.length > 0) {
|
||||
console.log("AssetBrowser - First asset task status:", assetsStore.assets[0].task_status);
|
||||
console.log("AssetBrowser - First asset task details:", assetsStore.assets[0].task_details);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("AssetBrowser - Failed to load assets:", err);
|
||||
console.error("AssetBrowser - Error details:", err);
|
||||
}
|
||||
};
|
||||
|
||||
// TanStack Table event handlers
|
||||
const handleColumnVisibilityChange = (visibility: VisibilityState) => {
|
||||
columnVisibility.value = visibility
|
||||
sessionStorage.setItem('assetBrowser.columnVisibility', JSON.stringify(visibility))
|
||||
}
|
||||
|
||||
const handleRowSelectionChange = (selection: Record<string, boolean>) => {
|
||||
rowSelection.value = selection
|
||||
}
|
||||
|
||||
const handleRowClick = (asset: Asset, event: MouseEvent) => {
|
||||
// Use the detail panel composable for row click handling
|
||||
handleRowClickComposable(asset, event)
|
||||
}
|
||||
|
||||
const handleCategoryFilterChange = (category: AssetCategory | 'all') => {
|
||||
selectedCategory.value = category
|
||||
}
|
||||
|
||||
const handleThumbnailToggle = (show: boolean) => {
|
||||
showThumbnails.value = show
|
||||
// Sync with column visibility
|
||||
columnVisibility.value = {
|
||||
...columnVisibility.value,
|
||||
thumbnail: show
|
||||
}
|
||||
}
|
||||
|
||||
const handleBulkTaskStatusChange = async (taskType: string, newStatus: TaskStatus) => {
|
||||
const selectedAssetIds = Object.keys(rowSelection.value).map(id => parseInt(id))
|
||||
|
||||
if (selectedAssetIds.length === 0) {
|
||||
toast({
|
||||
title: "No assets selected",
|
||||
description: "Please select assets to update their task status.",
|
||||
variant: "destructive",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Use the new bulk update method from the store
|
||||
await assetsStore.bulkUpdateTaskStatus(selectedAssetIds, taskType, newStatus)
|
||||
|
||||
toast({
|
||||
title: "Task status updated",
|
||||
description: `Updated ${formatTaskType(taskType)} status for ${selectedAssetIds.length} asset(s).`,
|
||||
})
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: "Failed to update task status",
|
||||
description: err instanceof Error ? err.message : "An error occurred",
|
||||
variant: "destructive",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const editAsset = (asset: Asset) => {
|
||||
selectedAsset.value = asset;
|
||||
showEditDialog.value = true;
|
||||
};
|
||||
|
||||
const deleteAsset = (asset: Asset) => {
|
||||
selectedAsset.value = asset;
|
||||
showDeleteDialog.value = true;
|
||||
};
|
||||
|
||||
const viewAssetTasks = (asset: Asset) => {
|
||||
// TODO: Navigate to asset tasks view
|
||||
console.log("View tasks for asset:", asset.name);
|
||||
};
|
||||
|
||||
const handleCreateAsset = async (assetData: AssetCreate | AssetUpdate) => {
|
||||
try {
|
||||
isCreating.value = true;
|
||||
// When creating, we know it's AssetCreate, but TypeScript needs the union type
|
||||
await assetsStore.createAsset(props.projectId, assetData as AssetCreate);
|
||||
showCreateDialog.value = false;
|
||||
toast({
|
||||
title: "Asset created",
|
||||
description: `${(assetData as AssetCreate).name} has been created successfully.`,
|
||||
});
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: "Failed to create asset",
|
||||
description: err instanceof Error ? err.message : "An error occurred",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
isCreating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateAsset = async (assetData: AssetCreate | AssetUpdate) => {
|
||||
if (!selectedAsset.value) return;
|
||||
|
||||
try {
|
||||
isUpdating.value = true;
|
||||
// When updating, we know it's AssetUpdate, but TypeScript needs the union type
|
||||
await assetsStore.updateAsset(selectedAsset.value.id, assetData as AssetUpdate);
|
||||
showEditDialog.value = false;
|
||||
selectedAsset.value = null;
|
||||
toast({
|
||||
title: "Asset updated",
|
||||
description: "Asset has been updated successfully.",
|
||||
});
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: "Failed to update asset",
|
||||
description: err instanceof Error ? err.message : "An error occurred",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
isUpdating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAsset = async () => {
|
||||
if (!selectedAsset.value) return;
|
||||
|
||||
try {
|
||||
await assetsStore.deleteAsset(selectedAsset.value.id);
|
||||
showDeleteDialog.value = false;
|
||||
selectedAsset.value = null;
|
||||
toast({
|
||||
title: "Asset deleted",
|
||||
description: "Asset has been deleted successfully.",
|
||||
});
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: "Failed to delete asset",
|
||||
description: err instanceof Error ? err.message : "An error occurred",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleTaskStatusUpdate = async (
|
||||
assetId: number,
|
||||
taskType: string,
|
||||
newStatus: string
|
||||
) => {
|
||||
try {
|
||||
// Update the asset in the store
|
||||
await assetsStore.updateTaskStatus(assetId, taskType, newStatus as TaskStatus);
|
||||
|
||||
toast({
|
||||
title: "Task status updated",
|
||||
description: `${formatTaskType(taskType)} status updated successfully.`,
|
||||
});
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: "Failed to update task status",
|
||||
description: err instanceof Error ? err.message : "An error occurred",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
selectedCategory.value = "all";
|
||||
searchQuery.value = "";
|
||||
taskStatusFilter.value = "";
|
||||
loadAssets();
|
||||
};
|
||||
|
||||
const handleTaskStatusFilter = (filter: string) => {
|
||||
taskStatusFilter.value = filter;
|
||||
loadAssets();
|
||||
};
|
||||
|
||||
const formatTaskType = (taskType: string) => {
|
||||
return taskType.charAt(0).toUpperCase() + taskType.slice(1);
|
||||
};
|
||||
|
||||
// Detail panel event handlers
|
||||
const handleCreateTask = () => {
|
||||
// TODO: Navigate to task creation for this asset
|
||||
console.log('Create task for asset:', selectedAsset.value?.name);
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
// Load custom task types from project
|
||||
const loadCustomTaskTypes = async () => {
|
||||
if (!props.projectId) return;
|
||||
|
||||
try {
|
||||
const { projectService } = await import('@/services/project');
|
||||
const project = await projectService.getProject(props.projectId);
|
||||
|
||||
customTaskTypes.value = project.custom_asset_task_types || [];
|
||||
customTaskTypesLoaded.value = true;
|
||||
|
||||
// Initialize column visibility AFTER custom task types are loaded
|
||||
initializeColumnVisibility();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load custom task types:', error);
|
||||
customTaskTypesLoaded.value = true; // Mark as loaded even if failed
|
||||
// Initialize with defaults even if loading fails
|
||||
initializeColumnVisibility();
|
||||
}
|
||||
};
|
||||
|
||||
// Watchers
|
||||
watch(
|
||||
() => props.projectId,
|
||||
async (newProjectId) => {
|
||||
if (newProjectId) {
|
||||
// Reset loaded flag when project changes
|
||||
customTaskTypesLoaded.value = false;
|
||||
|
||||
// Load custom task types first, then assets
|
||||
try {
|
||||
await loadCustomTaskTypes();
|
||||
} catch (err) {
|
||||
console.warn('Could not load custom task types for new project:', err);
|
||||
}
|
||||
loadAssets();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// Watch for custom task types changes and reinitialize column visibility
|
||||
watch(customTaskTypes, (newCustomTypes) => {
|
||||
if (newCustomTypes.length > 0) {
|
||||
// Reinitialize column visibility to include new custom types
|
||||
initializeColumnVisibility();
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
// Clear selections when filters change
|
||||
watch([selectedCategory, searchQuery], () => {
|
||||
rowSelection.value = {};
|
||||
});
|
||||
|
||||
// Save thumbnail toggle preference to session storage
|
||||
watch(showThumbnails, (newValue) => {
|
||||
sessionStorage.setItem('assetBrowser.showThumbnails', newValue.toString());
|
||||
// Sync with column visibility
|
||||
columnVisibility.value = {
|
||||
...columnVisibility.value,
|
||||
thumbnail: newValue
|
||||
};
|
||||
});
|
||||
|
||||
// Sync column visibility with thumbnail toggle
|
||||
watch(() => columnVisibility.value.thumbnail, (newValue) => {
|
||||
showThumbnails.value = newValue;
|
||||
});
|
||||
|
||||
// Watch for filter changes and reload assets
|
||||
watch([selectedCategory, taskStatusFilter], () => {
|
||||
loadAssets();
|
||||
});
|
||||
|
||||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
if (props.projectId) {
|
||||
// Load task statuses for bulk operations
|
||||
taskStatusesStore.fetchProjectStatuses(props.projectId).catch(err => {
|
||||
console.warn('Could not load task statuses:', err);
|
||||
});
|
||||
|
||||
// Load custom task types FIRST (blocking) to ensure columns are created correctly
|
||||
try {
|
||||
await loadCustomTaskTypes();
|
||||
} catch (err) {
|
||||
console.warn('Could not load custom task types:', err);
|
||||
// Continue without custom task types
|
||||
customTaskTypesLoaded.value = true;
|
||||
}
|
||||
|
||||
// Then load assets
|
||||
loadAssets();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user