refactor: session-based incremental TFE upload via HTMX, drop SortableJS

Replace the client-side FileArray + Sortable drag-to-reorder with a
server-side session-based upload flow:

- New endpoints: /partage/upload-tfe-file, /partage/remove-tfe-file
  (and /admin/ variants) — single-file incremental upload via HTMX
  multipart/form-data with progress bar support
- Session storage: uploaded files go to STORAGE_ROOT/uploads/{session_id}/
  with metadata in $_SESSION['tfe_uploads']
- file-upload-queue.js reduced to single-file previews only (couverture,
  note_intention, annexes thumbnails)
- ThesisFileHandler gains handleTfeFilesFromSession + writeTfeFileFromSrc
  + cleanupSessionUploads for final commit from session temp
- Sortable.min.js removed from all script tags; drag handles and ghost
  CSS removed
- No file_orders[]/file_labels[] hidden field injection needed
- Upload queue survives page refresh (server-owned list)

This eliminates the SortableJS dependency entirely while keeping the
same UX: pick files, see them in a queue, remove individual files.
This commit is contained in:
Pontoporeia
2026-05-10 16:19:46 +02:00
parent e06a317499
commit ca7707cd47
20 changed files with 606 additions and 255 deletions

View File

@@ -303,6 +303,102 @@ trait ThesisFileHandler
return $num;
}
/**
* Process TFE file uploads from session-stored temp paths.
*
* Used when files are uploaded incrementally via HTMX fragments (upload-tfe-file.php)
* rather than submitted in a single multipart form.
*
* @param int $thesisId
* @param array $uploads Array of ['orig_name', 'size', 'mime', 'tmp_path']
* @param string $folderPath
* @param string $filePrefix
* @param int $startNum
*/
protected function handleTfeFilesFromSession(int $thesisId, array $uploads, string $folderPath, string $filePrefix, int $startNum = 1): int
{
if (empty($uploads)) {
return $startNum;
}
$dir = STORAGE_ROOT . '/' . $folderPath;
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
$files = [];
foreach ($uploads as $f) {
$mimeType = $f['mime'];
$absPath = STORAGE_ROOT . '/' . $f['tmp_path'];
if (!file_exists($absPath)) {
error_log("ThesisFileHandler: session temp file missing {$f['tmp_path']}, skipping");
continue;
}
$ext = strtolower(pathinfo($f['orig_name'], PATHINFO_EXTENSION));
if ($mimeType === 'text/plain' && $ext === 'vtt') {
$mimeType = 'text/vtt';
}
$files[] = [
'mimeType' => $mimeType,
'ext' => $ext,
'size' => $f['size'],
'origName' => $f['orig_name'],
'label' => '',
'sortOrder' => null,
'hierarchy' => $this->tfeHierarchyRank($mimeType, $ext),
'fileType' => $this->detectFileType($mimeType, $ext),
// Pass the absolute path so writeTfeFile knows where to copy from
'srcPath' => $absPath,
];
}
// Sort by hierarchy rank
usort($files, fn($a, $b) => $a['hierarchy'] - $b['hierarchy']);
$videoCount = 0;
$vttQueue = [];
foreach ($files as $f) {
if ($f['fileType'] === 'video') {
$videoCount++;
}
}
$num = $startNum;
foreach ($files as $f) {
if ($f['fileType'] === 'caption') {
$vttQueue[] = $f;
continue;
}
if ($f['fileType'] === 'video') {
$this->writeTfeFileFromSrc($f, $thesisId, $dir, $folderPath, $filePrefix, $num);
$num++;
if (!empty($vttQueue)) {
$vtt = array_shift($vttQueue);
$this->writeTfeFileFromSrc($vtt, $thesisId, $dir, $folderPath, $filePrefix, $num);
$num++;
}
} else {
$this->writeTfeFileFromSrc($f, $thesisId, $dir, $folderPath, $filePrefix, $num);
$num++;
}
}
foreach ($vttQueue as $vtt) {
$this->writeTfeFileFromSrc($vtt, $thesisId, $dir, $folderPath, $filePrefix, $num);
$num++;
}
return $num;
}
/**
* Process annex file uploads.
*
@@ -421,6 +517,61 @@ trait ThesisFileHandler
error_log("ThesisFileHandler: TFE uploaded → $targetName ({$f['fileType']})");
}
/**
* Write a single TFE file from a source path (session temp) to the thesis folder.
* Used by handleTfeFilesFromSession instead of move_uploaded_file.
*/
protected function writeTfeFileFromSrc(array $f, int $thesisId, string $dir, string $folderPath, string $filePrefix, int $num): void
{
$padded = sprintf('%02d', $num);
$targetName = $filePrefix . '_TFE_' . $padded . '.' . $f['ext'];
$targetPath = $dir . $targetName;
if (!rename($f['srcPath'], $targetPath)) {
// Fallback: copy + unlink
if (!copy($f['srcPath'], $targetPath)) {
error_log("ThesisFileHandler: failed to move session TFE {$f['origName']}");
return;
}
unlink($f['srcPath']);
}
chmod($targetPath, 0644);
$relPath = $folderPath . $targetName;
$this->db->insertThesisFile(
$thesisId, $f['fileType'],
$relPath,
basename($f['origName']),
$f['size'],
$f['mimeType'],
$f['label'] !== '' ? $f['label'] : null,
$f['sortOrder']
);
error_log("ThesisFileHandler: TFE (session) moved → $targetName ({$f['fileType']})");
}
/**
* Clean up session upload temp files and clear the session entry.
* Call after successful commit of TFE files.
*/
protected function cleanupSessionUploads(): void
{
$sessionId = session_id();
$tempDir = STORAGE_ROOT . '/uploads/' . $sessionId;
// Remove any remaining files in the temp dir
if (is_dir($tempDir)) {
$files = glob($tempDir . '/*');
foreach ($files as $file) {
@unlink($file);
}
@rmdir($tempDir);
}
unset($_SESSION['tfe_uploads']);
}
/**
* Assign a hierarchy rank for sorting TFE files.
* Lower = earlier in the sequence.