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(); +});