From bd267403909541579633542d467dc184d504a2fd Mon Sep 17 00:00:00 2001 From: ignacionelson Date: Fri, 25 Sep 2026 02:44:54 -0300 Subject: [PATCH] Upload into the folder you are in, on the staff Files page Inside a folder, Upload opened the upload page with no folder, so every file landed at the top of the library and had to be moved. The client portal already carried the folder; the staff page now does the same. The upload page takes ?folder=, says "Uploading into ", passes it to the upload, and sends a multi-file upload back to that folder. The same two checks as the portal's upload page: a folder outside the staff member's library is a 404, so the page never confirms it exists, and one they may not upload into is a 403. ChunkedUploadsController still checks the destination again when the upload starts. While searching, the button keeps uploading to the top, since results span folders. Reported by @lolgufdHD (#1801) --- .../Http/Controllers/FilesController.php | 15 +++++++ resources/js/pages/files/create.tsx | 12 ++++- resources/js/pages/files/index.tsx | 5 ++- tests/Feature/Files/UploadIntoFolderTest.php | 44 +++++++++++++++++++ 4 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 tests/Feature/Files/UploadIntoFolderTest.php diff --git a/app/Modules/Files/Http/Controllers/FilesController.php b/app/Modules/Files/Http/Controllers/FilesController.php index 108aed0c..3c5e13e7 100644 --- a/app/Modules/Files/Http/Controllers/FilesController.php +++ b/app/Modules/Files/Http/Controllers/FilesController.php @@ -62,7 +62,22 @@ class FilesController extends Controller $user = $request->user(); assert($user !== null); + // Opened from inside a folder, the upload goes into it (#1801). + // The same two checks the portal's upload page makes, with the + // staff library in place of the client's: a folder this person + // cannot see is a 404, one they may not upload into is a 403. + // ChunkedUploadsController checks the destination again when the + // upload starts, so this decides what the page offers, not what + // is allowed. + $folder = null; + if ($request->integer('folder') > 0) { + $folder = Folder::query()->find($request->integer('folder')); + abort_if($folder === null || ! app(StaffLibraryScope::class)->allowsFolder($user, $folder), 404); + abort_unless(Folder::uploadableBy($user, $folder), 403); + } + return Inertia::render('files/create', [ + 'folder' => $folder === null ? null : ['id' => $folder->id, 'name' => $folder->name], 'max_file_size_mb' => app(Settings::class)->get(Setting::MaxFileSizeMb), 'part_size_mb' => (int) config('projectsend.upload_part_size_mb'), 'allowed_extensions' => app(UploadExtensionPolicy::class)->hintFor($user), diff --git a/resources/js/pages/files/create.tsx b/resources/js/pages/files/create.tsx index cc2444cf..4f41c631 100644 --- a/resources/js/pages/files/create.tsx +++ b/resources/js/pages/files/create.tsx @@ -7,12 +7,14 @@ import { useTranslation } from '@/hooks/use-translation'; import AppLayout from '@/layouts/app-layout'; interface FilesCreateProps { + /** The folder the upload page was opened from, which uploads go into. */ + folder: { id: number; name: string } | null; max_file_size_mb: number; part_size_mb: number; allowed_extensions: string[] | null; } -export default function FilesCreate({ max_file_size_mb, part_size_mb, allowed_extensions }: FilesCreateProps) { +export default function FilesCreate({ folder, max_file_size_mb, part_size_mb, allowed_extensions }: FilesCreateProps) { const { t } = useTranslation(); const breadcrumbs: BreadcrumbItem[] = [ @@ -24,7 +26,8 @@ export default function FilesCreate({ max_file_size_mb, part_size_mb, allowed_ex if (fileIds.length === 1) { router.visit(route('files.edit', fileIds[0])); } else if (fileIds.length > 1) { - router.visit(route('files.index')); + // Back to where the upload started, so the new files are in view. + router.visit(route('files.index', folder !== null ? { folder: folder.id } : {})); } }; @@ -44,7 +47,12 @@ export default function FilesCreate({ max_file_size_mb, part_size_mb, allowed_ex } /> + {folder !== null && ( +

{t('Uploading into :folder', { folder: folder.name })}

+ )} + - {t('Upload')} + {/* Into the folder on screen, not the top of the + library (#1801). Not while searching: the + results span folders, so there is no "here". */} + {t('Upload')} )} diff --git a/tests/Feature/Files/UploadIntoFolderTest.php b/tests/Feature/Files/UploadIntoFolderTest.php new file mode 100644 index 00000000..2ab7ccd3 --- /dev/null +++ b/tests/Feature/Files/UploadIntoFolderTest.php @@ -0,0 +1,44 @@ +admin = User::factory()->create(); +}); + +test('the upload page opened from a folder uploads into that folder', function () { + $folder = Folder::query()->create(['name' => 'Wedding']); + + $this->actingAs($this->admin)->get(route('files.create', ['folder' => $folder->id])) + ->assertInertia(fn ($page) => $page + ->where('folder.id', $folder->id) + ->where('folder.name', 'Wedding')); +}); + +test('without a folder it still uploads to the top of the library', function () { + $this->actingAs($this->admin)->get(route('files.create')) + ->assertInertia(fn ($page) => $page->where('folder', null)); +}); + +test('a folder that does not exist is a 404, not a quiet upload to the top', function () { + $this->actingAs($this->admin)->get(route('files.create', ['folder' => 999999]))->assertNotFound(); +}); + +test('a folder outside a client-scoped staff member\'s library is a 404', function () { + // Nobody is assigned to this manager, so the folder is outside what + // they can see. The page must not confirm it exists by naming it. + $manager = User::factory()->role(SystemRole::ClientManager)->create(); + $folder = Folder::query()->create(['name' => 'Somebody else\'s']); + + $this->actingAs($manager)->get(route('files.create', ['folder' => $folder->id]))->assertNotFound(); +});