From 7bf92e2d2cc16ad0034047990e69df3c04edfcb7 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Mon, 8 Jun 2026 22:03:03 +0000 Subject: [PATCH] Create a Ladill folder when uploading a local folder and place files inside it. Folder upload now names and creates the destination folder from the picked directory, uploads all files into it, and redirects to the new folder view. Co-authored-by: Cursor --- .../Controllers/Transfer/FilesController.php | 31 +++++++++++++++- app/Services/Transfer/FileStorageService.php | 33 +++++++++++++++++ resources/js/app.js | 23 ++++++++++-- .../views/transfer/files/index.blade.php | 11 +++--- tests/Feature/TransferFilesTest.php | 37 +++++++++++++++++++ 5 files changed, 124 insertions(+), 11 deletions(-) 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', + ]); + } }