Create a Ladill folder when uploading a local folder and place files inside it.
Deploy Ladill Transfer / deploy (push) Successful in 27s
Deploy Ladill Transfer / deploy (push) Successful in 27s
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 <cursoragent@cursor.com>
This commit is contained in:
@@ -317,9 +317,15 @@ class FilesController extends Controller
|
|||||||
'filenames' => 'required|array|min:1',
|
'filenames' => 'required|array|min:1',
|
||||||
'filenames.*' => 'required|string|max:255',
|
'filenames.*' => 'required|string|max:255',
|
||||||
'folder' => 'nullable|integer|exists:transfers,id',
|
'folder' => 'nullable|integer|exists:transfers,id',
|
||||||
|
'upload_folder_name' => 'nullable|string|max:120',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$account = ladill_account();
|
$account = ladill_account();
|
||||||
|
|
||||||
|
if (! empty($data['upload_folder_name'])) {
|
||||||
|
return response()->json(['conflicts' => []]);
|
||||||
|
}
|
||||||
|
|
||||||
$folder = $this->resolveUploadFolder($account, $data['folder'] ?? null);
|
$folder = $this->resolveUploadFolder($account, $data['folder'] ?? null);
|
||||||
|
|
||||||
$conflicts = $this->storage->findConflicts(
|
$conflicts = $this->storage->findConflicts(
|
||||||
@@ -337,15 +343,23 @@ class FilesController extends Controller
|
|||||||
'files' => 'required|array|min:1',
|
'files' => 'required|array|min:1',
|
||||||
'files.*' => 'required|file',
|
'files.*' => 'required|file',
|
||||||
'folder' => 'nullable|integer|exists:transfers,id',
|
'folder' => 'nullable|integer|exists:transfers,id',
|
||||||
|
'upload_folder_name' => 'nullable|string|max:120',
|
||||||
'resolutions' => 'nullable|array',
|
'resolutions' => 'nullable|array',
|
||||||
'resolutions.*' => 'in:replace,keep_both',
|
'resolutions.*' => 'in:replace,keep_both',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$account = ladill_account();
|
$account = ladill_account();
|
||||||
$folder = $this->resolveUploadFolder($account, $data['folder'] ?? null);
|
|
||||||
$files = array_values($request->file('files', []) ?? []);
|
$files = array_values($request->file('files', []) ?? []);
|
||||||
|
$createdFolder = null;
|
||||||
|
|
||||||
try {
|
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(
|
$uploaded = $this->storage->uploadFiles(
|
||||||
$account,
|
$account,
|
||||||
$folder,
|
$folder,
|
||||||
@@ -362,9 +376,22 @@ class FilesController extends Controller
|
|||||||
|
|
||||||
$count = count($uploaded);
|
$count = count($uploaded);
|
||||||
$message = $count.' file'.($count === 1 ? '' : 's').' uploaded.';
|
$message = $count.' file'.($count === 1 ? '' : 's').' uploaded.';
|
||||||
|
if ($createdFolder) {
|
||||||
|
$message = $count.' file'.($count === 1 ? '' : 's').' uploaded to '.$createdFolder->title.'.';
|
||||||
|
}
|
||||||
|
|
||||||
if ($request->expectsJson()) {
|
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);
|
return back()->with('success', $message);
|
||||||
|
|||||||
@@ -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
|
public function uploadDestination(User $user, ?Transfer $folder): Transfer
|
||||||
{
|
{
|
||||||
if ($folder !== null) {
|
if ($folder !== null) {
|
||||||
|
|||||||
+19
-4
@@ -647,6 +647,7 @@ Alpine.data('filesManager', (config = {}) => ({
|
|||||||
pendingUploads: null,
|
pendingUploads: null,
|
||||||
pendingResolutions: {},
|
pendingResolutions: {},
|
||||||
duplicateQueue: [],
|
duplicateQueue: [],
|
||||||
|
pendingUploadFolderName: null,
|
||||||
uploadConfirmOpen: false,
|
uploadConfirmOpen: false,
|
||||||
pendingConfirmFiles: null,
|
pendingConfirmFiles: null,
|
||||||
uploadConfirmFolderName: '',
|
uploadConfirmFolderName: '',
|
||||||
@@ -781,9 +782,10 @@ Alpine.data('filesManager', (config = {}) => ({
|
|||||||
|
|
||||||
confirmUpload() {
|
confirmUpload() {
|
||||||
const files = this.pendingConfirmFiles;
|
const files = this.pendingConfirmFiles;
|
||||||
|
const uploadFolderName = this.uploadConfirmIsFolder ? this.uploadConfirmFolderName : null;
|
||||||
this.cancelUploadConfirm();
|
this.cancelUploadConfirm();
|
||||||
if (files?.length) {
|
if (files?.length) {
|
||||||
this.processUploadQueue(files);
|
this.processUploadQueue(files, { uploadFolderName });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -804,8 +806,10 @@ Alpine.data('filesManager', (config = {}) => ({
|
|||||||
return formatUploadBytes(bytes);
|
return formatUploadBytes(bytes);
|
||||||
},
|
},
|
||||||
|
|
||||||
async processUploadQueue(files) {
|
async processUploadQueue(files, options = {}) {
|
||||||
this.uploading = true;
|
this.uploading = true;
|
||||||
|
this.pendingUploadFolderName = options.uploadFolderName || null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const checkRes = await fetch(this.routes.uploadCheck, {
|
const checkRes = await fetch(this.routes.uploadCheck, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -817,7 +821,8 @@ Alpine.data('filesManager', (config = {}) => ({
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
filenames: files.map((f) => this.fileBasename(f)),
|
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();
|
const checkData = await checkRes.json();
|
||||||
@@ -847,6 +852,7 @@ Alpine.data('filesManager', (config = {}) => ({
|
|||||||
this.uploading = false;
|
this.uploading = false;
|
||||||
this.pendingUploads = null;
|
this.pendingUploads = null;
|
||||||
this.pendingResolutions = {};
|
this.pendingResolutions = {};
|
||||||
|
this.pendingUploadFolderName = null;
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -869,9 +875,12 @@ Alpine.data('filesManager', (config = {}) => ({
|
|||||||
const name = this.fileBasename(file);
|
const name = this.fileBasename(file);
|
||||||
formData.append('files[]', file, name);
|
formData.append('files[]', file, name);
|
||||||
});
|
});
|
||||||
if (this.currentFolderId) {
|
if (this.currentFolderId && !this.pendingUploadFolderName) {
|
||||||
formData.append('folder', this.currentFolderId);
|
formData.append('folder', this.currentFolderId);
|
||||||
}
|
}
|
||||||
|
if (this.pendingUploadFolderName) {
|
||||||
|
formData.append('upload_folder_name', this.pendingUploadFolderName);
|
||||||
|
}
|
||||||
Object.entries(resolutions).forEach(([name, action]) => {
|
Object.entries(resolutions).forEach(([name, action]) => {
|
||||||
formData.append(`resolutions[${name}]`, action);
|
formData.append(`resolutions[${name}]`, action);
|
||||||
});
|
});
|
||||||
@@ -893,6 +902,12 @@ Alpine.data('filesManager', (config = {}) => ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.showToast(data.message || 'Upload complete.');
|
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();
|
window.location.reload();
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
'openInEmail' => route('transfer.files.open-in-email'),
|
'openInEmail' => route('transfer.files.open-in-email'),
|
||||||
'uploadCheck' => route('transfer.files.upload.check'),
|
'uploadCheck' => route('transfer.files.upload.check'),
|
||||||
'upload' => route('transfer.files.upload'),
|
'upload' => route('transfer.files.upload'),
|
||||||
|
'filesIndex' => route('transfer.files.index'),
|
||||||
'createFolder' => route('transfer.files.folders.store'),
|
'createFolder' => route('transfer.files.folders.store'),
|
||||||
],
|
],
|
||||||
'moveTargets' => $moveTargets->map(fn ($t) => [
|
'moveTargets' => $moveTargets->map(fn ($t) => [
|
||||||
@@ -259,12 +260,12 @@
|
|||||||
</h3>
|
</h3>
|
||||||
<p class="mt-2 text-sm text-slate-600">
|
<p class="mt-2 text-sm text-slate-600">
|
||||||
<span x-show="uploadConfirmIsFolder">
|
<span x-show="uploadConfirmIsFolder">
|
||||||
<span x-text="pendingConfirmFiles?.length || 0"></span>
|
A folder named
|
||||||
files from
|
|
||||||
<span class="font-medium text-slate-800">'<span x-text="uploadConfirmFolderName"></span>'</span>
|
<span class="font-medium text-slate-800">'<span x-text="uploadConfirmFolderName"></span>'</span>
|
||||||
will be uploaded individually to
|
will be created with
|
||||||
<span class="font-medium text-slate-800" x-text="currentFolderId ? 'this folder' : 'My files'"></span>.
|
<span x-text="pendingConfirmFiles?.length || 0"></span>
|
||||||
No new folder will be created.
|
<span x-text="(pendingConfirmFiles?.length || 0) === 1 ? 'file' : 'files'"></span>
|
||||||
|
inside it.
|
||||||
</span>
|
</span>
|
||||||
<span x-show="!uploadConfirmIsFolder">
|
<span x-show="!uploadConfirmIsFolder">
|
||||||
Selected files will be added to
|
Selected files will be added to
|
||||||
|
|||||||
@@ -188,4 +188,41 @@ class TransferFilesTest extends TestCase
|
|||||||
$this->assertDatabaseHas('transfer_files', ['original_name' => 'icon.svg']);
|
$this->assertDatabaseHas('transfer_files', ['original_name' => 'icon.svg']);
|
||||||
$this->assertDatabaseMissing('transfer_files', ['original_name' => 'red-logo/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',
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user