diff --git a/app/Http/Controllers/Transfer/FilesController.php b/app/Http/Controllers/Transfer/FilesController.php index af7fd96..d75ccd8 100644 --- a/app/Http/Controllers/Transfer/FilesController.php +++ b/app/Http/Controllers/Transfer/FilesController.php @@ -317,9 +317,15 @@ class FilesController extends Controller 'filenames' => 'required|array|min:1', 'filenames.*' => 'required|string|max:255', 'folder' => 'nullable|integer|exists:transfers,id', + 'upload_folder_name' => 'nullable|string|max:120', ]); $account = ladill_account(); + + if (! empty($data['upload_folder_name'])) { + return response()->json(['conflicts' => []]); + } + $folder = $this->resolveUploadFolder($account, $data['folder'] ?? null); $conflicts = $this->storage->findConflicts( @@ -337,15 +343,23 @@ class FilesController extends Controller 'files' => 'required|array|min:1', 'files.*' => 'required|file', 'folder' => 'nullable|integer|exists:transfers,id', + 'upload_folder_name' => 'nullable|string|max:120', 'resolutions' => 'nullable|array', 'resolutions.*' => 'in:replace,keep_both', ]); $account = ladill_account(); - $folder = $this->resolveUploadFolder($account, $data['folder'] ?? null); $files = array_values($request->file('files', []) ?? []); + $createdFolder = null; try { + if (! empty($data['upload_folder_name'])) { + $createdFolder = $this->storage->createFolderForUpload($account, $data['upload_folder_name']); + $folder = $createdFolder; + } else { + $folder = $this->resolveUploadFolder($account, $data['folder'] ?? null); + } + $uploaded = $this->storage->uploadFiles( $account, $folder, @@ -362,9 +376,22 @@ class FilesController extends Controller $count = count($uploaded); $message = $count.' file'.($count === 1 ? '' : 's').' uploaded.'; + if ($createdFolder) { + $message = $count.' file'.($count === 1 ? '' : 's').' uploaded to '.$createdFolder->title.'.'; + } if ($request->expectsJson()) { - return response()->json(['message' => $message, 'count' => $count]); + return response()->json([ + 'message' => $message, + 'count' => $count, + 'folder_id' => $createdFolder?->id, + ]); + } + + if ($createdFolder) { + return redirect() + ->route('transfer.files.index', ['folder' => $createdFolder->id]) + ->with('success', $message); } return back()->with('success', $message); diff --git a/app/Services/Transfer/FileStorageService.php b/app/Services/Transfer/FileStorageService.php index 48dccba..1830d66 100644 --- a/app/Services/Transfer/FileStorageService.php +++ b/app/Services/Transfer/FileStorageService.php @@ -59,6 +59,39 @@ class FileStorageService ]); } + public function createFolderForUpload(User $user, string $name): Transfer + { + $title = trim($name); + if ($title === '') { + throw new RuntimeException('Enter a folder name.'); + } + + return $this->createFolder($user, $this->uniqueFolderTitle($user, $title)); + } + + public function uniqueFolderTitle(User $user, string $title): string + { + $candidate = $title; + $counter = 1; + + while ($this->folderTitleExists($user, $candidate)) { + $candidate = $title.' ('.$counter.')'; + $counter++; + } + + return $candidate; + } + + private function folderTitleExists(User $user, string $title): bool + { + return Transfer::query() + ->where('user_id', $user->id) + ->where('is_folder', true) + ->where('status', '!=', Transfer::STATUS_DELETED) + ->where('title', $title) + ->exists(); + } + public function uploadDestination(User $user, ?Transfer $folder): Transfer { if ($folder !== null) { diff --git a/resources/js/app.js b/resources/js/app.js index 7321dec..85a3559 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -647,6 +647,7 @@ Alpine.data('filesManager', (config = {}) => ({ pendingUploads: null, pendingResolutions: {}, duplicateQueue: [], + pendingUploadFolderName: null, uploadConfirmOpen: false, pendingConfirmFiles: null, uploadConfirmFolderName: '', @@ -781,9 +782,10 @@ Alpine.data('filesManager', (config = {}) => ({ confirmUpload() { const files = this.pendingConfirmFiles; + const uploadFolderName = this.uploadConfirmIsFolder ? this.uploadConfirmFolderName : null; this.cancelUploadConfirm(); if (files?.length) { - this.processUploadQueue(files); + this.processUploadQueue(files, { uploadFolderName }); } }, @@ -804,8 +806,10 @@ Alpine.data('filesManager', (config = {}) => ({ return formatUploadBytes(bytes); }, - async processUploadQueue(files) { + async processUploadQueue(files, options = {}) { this.uploading = true; + this.pendingUploadFolderName = options.uploadFolderName || null; + try { const checkRes = await fetch(this.routes.uploadCheck, { method: 'POST', @@ -817,7 +821,8 @@ Alpine.data('filesManager', (config = {}) => ({ }, body: JSON.stringify({ filenames: files.map((f) => this.fileBasename(f)), - folder: this.currentFolderId || null, + folder: this.pendingUploadFolderName ? null : (this.currentFolderId || null), + upload_folder_name: this.pendingUploadFolderName, }), }); const checkData = await checkRes.json(); @@ -847,6 +852,7 @@ Alpine.data('filesManager', (config = {}) => ({ this.uploading = false; this.pendingUploads = null; this.pendingResolutions = {}; + this.pendingUploadFolderName = null; }); return; } @@ -869,9 +875,12 @@ Alpine.data('filesManager', (config = {}) => ({ const name = this.fileBasename(file); formData.append('files[]', file, name); }); - if (this.currentFolderId) { + if (this.currentFolderId && !this.pendingUploadFolderName) { formData.append('folder', this.currentFolderId); } + if (this.pendingUploadFolderName) { + formData.append('upload_folder_name', this.pendingUploadFolderName); + } Object.entries(resolutions).forEach(([name, action]) => { formData.append(`resolutions[${name}]`, action); }); @@ -893,6 +902,12 @@ Alpine.data('filesManager', (config = {}) => ({ } this.showToast(data.message || 'Upload complete.'); + + if (data.folder_id && this.routes.filesIndex) { + window.location.href = `${this.routes.filesIndex}?folder=${data.folder_id}`; + return; + } + window.location.reload(); }, })); diff --git a/resources/views/transfer/files/index.blade.php b/resources/views/transfer/files/index.blade.php index bd9d6c6..8a4bfa7 100644 --- a/resources/views/transfer/files/index.blade.php +++ b/resources/views/transfer/files/index.blade.php @@ -33,6 +33,7 @@ 'openInEmail' => route('transfer.files.open-in-email'), 'uploadCheck' => route('transfer.files.upload.check'), 'upload' => route('transfer.files.upload'), + 'filesIndex' => route('transfer.files.index'), 'createFolder' => route('transfer.files.folders.store'), ], 'moveTargets' => $moveTargets->map(fn ($t) => [ @@ -259,12 +260,12 @@

- - files from + A folder named '' - will be uploaded individually to - . - No new folder will be created. + will be created with + + + inside it. Selected files will be added to diff --git a/tests/Feature/TransferFilesTest.php b/tests/Feature/TransferFilesTest.php index a479202..135c822 100644 --- a/tests/Feature/TransferFilesTest.php +++ b/tests/Feature/TransferFilesTest.php @@ -188,4 +188,41 @@ class TransferFilesTest extends TestCase $this->assertDatabaseHas('transfer_files', ['original_name' => 'icon.svg']); $this->assertDatabaseMissing('transfer_files', ['original_name' => 'red-logo/icon.svg']); } + + public function test_folder_upload_creates_folder_with_files(): void + { + Storage::fake('qr'); + $this->fakeTransferBillingApi(); + + $user = $this->createUser(); + + $this->actingAs($user) + ->postJson(route('transfer.files.upload'), [ + 'upload_folder_name' => 'Brand assets', + 'files' => [ + UploadedFile::fake()->create('logo.svg', 10, 'image/svg+xml'), + UploadedFile::fake()->create('cover.png', 10, 'image/png'), + ], + ]) + ->assertOk() + ->assertJsonPath('count', 2) + ->assertJsonPath('folder_id', fn ($id) => is_int($id)); + + $folder = Transfer::query() + ->where('user_id', $user->id) + ->where('is_folder', true) + ->where('title', 'Brand assets') + ->first(); + + $this->assertNotNull($folder); + $this->assertSame(2, $folder->files()->count()); + $this->assertDatabaseHas('transfer_files', [ + 'transfer_id' => $folder->id, + 'original_name' => 'logo.svg', + ]); + $this->assertDatabaseHas('transfer_files', [ + 'transfer_id' => $folder->id, + 'original_name' => 'cover.png', + ]); + } }