Files
LinkDesk/frontend/src/components/project/SubmissionConfigManager.vue
T
indigo 981808b901 Extend submission config: .mxf/codec/frame-rate checks, regex naming, shot/asset tabs
- Movie spec checks now also cover codec (h264/h265/mjpeg/dnxhd/dnxhr/
  prores/uncompressed/avid/cineform) and frame rate (23.976/24/30/48/60),
  detected in-browser via mediainfo.js container parsing - works even
  for formats the native <video> element can't decode (MXF, ProRes,
  DNxHD). Added .mxf to the supported/allowed format lists.
- Naming pattern supports a raw regex mode (validated by compiling
  server-side at save time) as an alternative to the token pattern,
  and gained three more tokens: {task_name}, {project_name},
  {project_code} (fetched lazily only when referenced).
- Submission Configuration settings page now splits Shot Tasks /
  Asset Tasks into separate tabs instead of one merged list; the
  per-task-type card was extracted into SubmissionTypeConfigCard.vue
  to avoid duplicating it across both tabs.
2026-07-19 19:15:05 +08:00

176 lines
5.9 KiB
Vue

<template>
<div class="space-y-6">
<div class="flex items-center justify-between">
<div>
<h3 class="text-lg font-semibold">Submission Configuration</h3>
<p class="text-sm text-muted-foreground">Configure accepted file types and naming conventions artists must follow when submitting work, per task type</p>
</div>
</div>
<div v-if="isLoading" class="text-center py-8 text-sm text-muted-foreground">
Loading submission configuration...
</div>
<Tabs v-else default-value="shot" class="w-full">
<TabsList class="grid w-full grid-cols-2">
<TabsTrigger value="shot">Shot Tasks</TabsTrigger>
<TabsTrigger value="asset">Asset Tasks</TabsTrigger>
</TabsList>
<TabsContent value="shot" class="space-y-4 mt-4">
<SubmissionTypeConfigCard
v-for="taskType in shotTaskTypes"
:key="taskType"
:task-type="taskType"
:config="getConfig(taskType)"
/>
<div v-if="shotTaskTypes.length === 0" class="text-center py-8 text-sm text-muted-foreground">
No shot task types configured for this project yet.
</div>
</TabsContent>
<TabsContent value="asset" class="space-y-4 mt-4">
<SubmissionTypeConfigCard
v-for="taskType in assetTaskTypes"
:key="taskType"
:task-type="taskType"
:config="getConfig(taskType)"
/>
<div v-if="assetTaskTypes.length === 0" class="text-center py-8 text-sm text-muted-foreground">
No asset task types configured for this project yet.
</div>
</TabsContent>
</Tabs>
<div class="flex justify-end">
<Button :disabled="isLoading || isSaving" @click="onSave">
<div v-if="isSaving" class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Save Submission Configuration
</Button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Button } from '@/components/ui/button'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { useToast } from '@/components/ui/toast/use-toast'
import { projectService, type SubmissionTypeConfig } from '@/services/project'
import { customTaskTypeService } from '@/services/customTaskType'
import SubmissionTypeConfigCard from './SubmissionTypeConfigCard.vue'
interface Props {
projectId: number
}
const props = defineProps<Props>()
const { toast } = useToast()
const isLoading = ref(false)
const isSaving = ref(false)
const shotTaskTypes = ref<string[]>([])
const assetTaskTypes = ref<string[]>([])
const formState = reactive<Record<string, SubmissionTypeConfig>>({})
const getConfig = (taskType: string): SubmissionTypeConfig => {
if (!formState[taskType]) {
formState[taskType] = {
allowed_extensions: [],
naming_pattern_is_regex: false,
naming_pattern: '',
check_naming: true,
required: false,
check_movie_spec: false,
movie_resolution: '',
movie_format: '',
movie_codec: '',
movie_frame_rate: null
}
}
return formState[taskType]
}
const load = async () => {
try {
isLoading.value = true
const [allTypes, config] = await Promise.all([
customTaskTypeService.getAllTaskTypes(props.projectId),
projectService.getProjectSubmissionConfig(props.projectId)
])
shotTaskTypes.value = allTypes.shot_task_types
assetTaskTypes.value = allTypes.asset_task_types
for (const [taskType, cfg] of Object.entries(config.config_by_task_type)) {
formState[taskType] = {
allowed_extensions: [...cfg.allowed_extensions],
naming_pattern_is_regex: cfg.naming_pattern_is_regex,
naming_pattern: cfg.naming_pattern || '',
check_naming: cfg.check_naming,
required: cfg.required,
check_movie_spec: cfg.check_movie_spec,
movie_resolution: cfg.movie_resolution || '',
movie_format: cfg.movie_format || '',
movie_codec: cfg.movie_codec || '',
movie_frame_rate: cfg.movie_frame_rate ?? null
}
}
} catch (error) {
console.error('Failed to load submission configuration:', error)
toast({
title: 'Error',
description: 'Failed to load submission configuration',
variant: 'destructive'
})
} finally {
isLoading.value = false
}
}
const onSave = async () => {
try {
isSaving.value = true
const configByTaskType: Record<string, SubmissionTypeConfig> = {}
for (const taskType of [...shotTaskTypes.value, ...assetTaskTypes.value]) {
const config = formState[taskType]
if (!config) continue
const hasConfig = config.allowed_extensions.length > 0 || !!config.naming_pattern
|| config.required || config.check_movie_spec
if (hasConfig) {
configByTaskType[taskType] = {
allowed_extensions: config.allowed_extensions,
naming_pattern_is_regex: config.naming_pattern_is_regex,
naming_pattern: config.naming_pattern || null,
check_naming: config.check_naming,
required: config.required,
check_movie_spec: config.check_movie_spec,
movie_resolution: config.movie_resolution || null,
movie_format: config.movie_format || null,
movie_codec: config.movie_codec || null,
movie_frame_rate: config.movie_frame_rate || null
}
}
}
await projectService.updateProjectSubmissionConfig(props.projectId, { config_by_task_type: configByTaskType })
toast({
title: 'Submission Configuration Updated',
description: 'Submission rules have been saved successfully'
})
} catch (error: any) {
console.error('Failed to save submission configuration:', error)
toast({
title: 'Error',
description: error.response?.data?.detail || 'Failed to save submission configuration',
variant: 'destructive'
})
} finally {
isSaving.value = false
}
}
onMounted(load)
</script>