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:
2026-02-08 16:43:05 +01:00
parent 46bd3f5917
commit 1ce14eca13

View 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>