feat(Editor): Refactor et Renommage avancé des médias
- Algorithme de renommage en 2 passes (TMP -> Final) pour éviter les verrous fichiers - Gestion propre de la suppression des questions et médias associés - Délégation de l'affichage de la liste à SessionQuestionsList
This commit is contained in:
162
VApp/src/components/SessionDetails.vue
Normal file
162
VApp/src/components/SessionDetails.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-alert v-if="error" type="error" closable class="mb-4">{{ error }}</v-alert>
|
||||
<v-alert rounded="xl" v-if="success" type="success" closable class="ma-15">{{ success }}</v-alert>
|
||||
|
||||
<v-card class="ma-15 pa-5" rounded="xl">
|
||||
<v-card-title class="pb-10">Configuration Générale</v-card-title>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field color="primary" v-model="config.PackId" label="ID du Pack" readonly variant="outlined" rounded="xl"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field color="primary" v-model="config.PackTitle" label="Titre du Pack" variant="outlined" rounded="xl"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card rounded="xl" class="pa-5 ma-15">
|
||||
<!-- List container without v-list wrapper for clean transition -->
|
||||
<SessionQuestionsList
|
||||
:questions="config.Questions"
|
||||
:session-id="sessionId"
|
||||
:api-url="apiUrl"
|
||||
@move="(index, dir) => moveQuestion(index, dir)"
|
||||
@update="(q, i) => updateQuestion(q, i)"
|
||||
@delete="(i) => deleteQuestion(i)"
|
||||
@delete-media="(path) => $emit('delete-media', path)"
|
||||
/>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SessionQuestionsList from '@/components/SessionQuestionsList.vue';
|
||||
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
sessionId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
apiUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
success: String,
|
||||
error: String
|
||||
});
|
||||
|
||||
const emit = defineEmits(['delete-media', 'rename-media']);
|
||||
const cleanUrl = (url) => url ? url.split('?')[0] : url;
|
||||
|
||||
function deleteQuestion(index) {
|
||||
if (confirm('Êtes-vous sûr de vouloir supprimer cette question ? (Le média sera aussi supprimé)')) {
|
||||
const question = props.config.Questions[index];
|
||||
if (question.MediaUrl) {
|
||||
emit('delete-media', cleanUrl(question.MediaUrl));
|
||||
}
|
||||
props.config.Questions.splice(index, 1);
|
||||
reindexQuestions();
|
||||
}
|
||||
}
|
||||
|
||||
function reindexQuestions() {
|
||||
// Step 1: Rename conflict candidates to temporary names
|
||||
console.log("reindexQuestions: Starting Step 1 (Rename to TMP)");
|
||||
props.config.Questions.forEach((q, i) => {
|
||||
const idNumber = (i + 1).toString().padStart(3, '0');
|
||||
const newId = `Q-${idNumber}`;
|
||||
|
||||
if (q.QuestionId !== newId && q.MediaUrl) {
|
||||
// It will be renamed. Rename to TEMP first.
|
||||
const lastDotIndex = q.MediaUrl.lastIndexOf('.');
|
||||
if (lastDotIndex !== -1) {
|
||||
const ext = q.MediaUrl.substring(lastDotIndex);
|
||||
const lastSlashIndex = q.MediaUrl.lastIndexOf('/');
|
||||
const folderPath = q.MediaUrl.substring(0, lastSlashIndex + 1);
|
||||
|
||||
const tempId = `TMP-${q.QuestionId}`;
|
||||
const tempPath = folderPath + tempId + ext;
|
||||
|
||||
emit('rename-media', {
|
||||
oldPath: cleanUrl(q.MediaUrl),
|
||||
newName: tempId
|
||||
});
|
||||
console.log(`Step 1: Renaming ${q.MediaUrl} to ${tempId}`);
|
||||
|
||||
// Store temp path but DO NOT update MediaUrl yet to avoid browser locking the file
|
||||
q._temp_path = tempPath;
|
||||
|
||||
// Mark for second pass
|
||||
q._temp_renamed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Step 2: Rename all to final names (Delayed to allow FS to settle)
|
||||
setTimeout(() => {
|
||||
console.log("reindexQuestions: Starting Step 2 (Rename to Final)");
|
||||
props.config.Questions.forEach((q, i) => {
|
||||
const idNumber = (i + 1).toString().padStart(3, '0');
|
||||
const newId = `Q-${idNumber}`;
|
||||
|
||||
if (q._temp_renamed && q._temp_path) {
|
||||
// Rename TMP -> Final
|
||||
const lastDotIndex = q.MediaUrl.lastIndexOf('.');
|
||||
const ext = q.MediaUrl.substring(lastDotIndex);
|
||||
const lastSlashIndex = q.MediaUrl.lastIndexOf('/');
|
||||
const folderPath = q.MediaUrl.substring(0, lastSlashIndex + 1);
|
||||
|
||||
console.log(`Step 2: Renaming ${q._temp_path} to ${newId}`);
|
||||
emit('rename-media', {
|
||||
oldPath: cleanUrl(q._temp_path), // Use the stored temp path
|
||||
newName: newId
|
||||
});
|
||||
|
||||
// Update with timestamp to force refresh
|
||||
q.MediaUrl = folderPath + newId + ext + `?t=${Date.now()}`;
|
||||
|
||||
delete q._temp_renamed;
|
||||
delete q._temp_path;
|
||||
}
|
||||
|
||||
// Note: QuestionId update is done here inside the timeout
|
||||
// This might cause a slight reactive delay but ensures alignment
|
||||
q.QuestionId = newId;
|
||||
});
|
||||
}, 200); // 200ms delay
|
||||
}
|
||||
|
||||
function updateQuestion(questionData, index) {
|
||||
if (index > -1) {
|
||||
Object.assign(props.config.Questions[index], questionData);
|
||||
} else {
|
||||
// Add new
|
||||
if (!questionData._ui_key) {
|
||||
Object.defineProperty(questionData, '_ui_key', {
|
||||
value: `q-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
props.config.Questions.push(questionData);
|
||||
reindexQuestions();
|
||||
}
|
||||
}
|
||||
|
||||
function moveQuestion(index, direction) {
|
||||
const newIndex = index + direction;
|
||||
if (newIndex >= 0 && newIndex < props.config.Questions.length) {
|
||||
const item = props.config.Questions.splice(index, 1)[0];
|
||||
props.config.Questions.splice(newIndex, 0, item);
|
||||
reindexQuestions();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user