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 <folder>", 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)
This commit is contained in:
ignacionelson
2026-09-25 02:44:54 -03:00
parent 37c9cb839f
commit bd26740390
4 changed files with 73 additions and 3 deletions
@@ -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),
+10 -2
View File
@@ -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 && (
<p className="text-muted-foreground mb-4 text-sm">{t('Uploading into :folder', { folder: folder.name })}</p>
)}
<ChunkedUploadDashboard
folderId={folder?.id}
maxFileSizeMb={max_file_size_mb}
partSizeMb={part_size_mb}
allowedExtensions={allowed_extensions}
+4 -1
View File
@@ -397,7 +397,10 @@ export default function FilesIndex({
)}
{can_upload && (
<Button asChild>
<Link href={route('files.create')}>{t('Upload')}</Link>
{/* Into the folder on screen, not the top of the
library (#1801). Not while searching: the
results span folders, so there is no "here". */}
<Link href={route('files.create', folder !== null && !searching ? { folder: folder.id } : {})}>{t('Upload')}</Link>
</Button>
)}
</div>
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
use App\Models\User;
use App\Modules\Files\Models\Folder;
use App\Modules\Identity\Permissions\SystemRole;
/*
* Upload from inside a folder on the staff Files page opened the upload
* page with no folder, so everything landed at the top of the library
* (#1801). The portal already carried the folder; this is the staff twin.
*/
beforeEach(function () {
$this->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();
});