Make folders selectable and downloadable with item count and sharing status.
Deploy Ladill Transfer / deploy (push) Successful in 35s

Folder rows match the file table layout, support zip download, and participate in bulk selection, share, and copy-link actions.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-08 22:21:39 +00:00
co-authored by Cursor
parent 449b9dc2ff
commit be3b719437
5 changed files with 278 additions and 67 deletions
+76 -20
View File
@@ -510,9 +510,12 @@ Alpine.data('transferCreateForm', (config = {}) => ({
}));
Alpine.data('filesManager', (config = {}) => ({
selected: [],
selectedFileIds: [],
selectedFolderIds: [],
fileIds: config.fileIds || [],
files: config.files || [],
folderIds: config.folderIds || [],
folders: config.folders || [],
routes: config.routes || {},
csrf: config.csrf || '',
currentFolderId: config.currentFolderId || null,
@@ -520,20 +523,38 @@ Alpine.data('filesManager', (config = {}) => ({
toast: '',
_toastTimer: null,
get selectedCount() {
return this.selectedFileIds.length + this.selectedFolderIds.length;
},
get allSelected() {
return this.fileIds.length > 0 && this.selected.length === this.fileIds.length;
const total = this.fileIds.length + this.folderIds.length;
return total > 0
&& this.selectedFileIds.length === this.fileIds.length
&& this.selectedFolderIds.length === this.folderIds.length;
},
toggleAll(event) {
this.selected = event.target.checked ? [...this.fileIds] : [];
if (event.target.checked) {
this.selectedFileIds = [...this.fileIds];
this.selectedFolderIds = [...this.folderIds];
} else {
this.selectedFileIds = [];
this.selectedFolderIds = [];
}
},
clearSelection() {
this.selected = [];
this.selectedFileIds = [];
this.selectedFolderIds = [];
},
selectedFiles() {
return this.files.filter((file) => this.selected.includes(file.id));
selectedFileRows() {
return this.files.filter((file) => this.selectedFileIds.includes(file.id));
},
selectedFolderRows() {
return this.folders.filter((folder) => this.selectedFolderIds.includes(folder.id));
},
showToast(message) {
@@ -554,7 +575,7 @@ Alpine.data('filesManager', (config = {}) => ({
csrf.value = this.csrf;
form.appendChild(csrf);
this.selected.forEach((id) => {
this.selectedFileIds.forEach((id) => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'files[]';
@@ -562,6 +583,14 @@ Alpine.data('filesManager', (config = {}) => ({
form.appendChild(input);
});
this.selectedFolderIds.forEach((id) => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'folders[]';
input.value = id;
form.appendChild(input);
});
Object.entries(extra).forEach(([key, value]) => {
const input = document.createElement('input');
input.type = 'hidden';
@@ -575,27 +604,41 @@ Alpine.data('filesManager', (config = {}) => ({
},
downloadSelected() {
if (this.selected.length === 0) return;
if (this.selected.length === 1) {
const file = this.selectedFiles()[0];
if (this.selectedCount === 0) return;
if (this.selectedFileIds.length === 1 && this.selectedFolderIds.length === 0) {
const file = this.selectedFileRows()[0];
if (file?.downloadUrl) {
window.location.href = file.downloadUrl;
}
return;
}
if (this.selectedFolderIds.length === 1 && this.selectedFileIds.length === 0) {
const folder = this.selectedFolderRows()[0];
if (folder?.downloadUrl) {
window.location.href = folder.downloadUrl;
}
return;
}
this.submitAction(this.routes.bulkDownload);
},
async deleteSelected() {
if (this.selected.length === 0) return;
if (! confirm(`Delete ${this.selected.length} file${this.selected.length === 1 ? '' : 's'}? This cannot be undone.`)) {
if (this.selectedFileIds.length === 0) return;
if (this.selectedFolderIds.length > 0) {
this.showToast('Delete files individually. Folder delete is not supported yet.');
return;
}
if (! confirm(`Delete ${this.selectedFileIds.length} file${this.selectedFileIds.length === 1 ? '' : 's'}? This cannot be undone.`)) {
return;
}
this.submitAction(this.routes.bulkDelete);
},
async shareSelected() {
if (this.selected.length === 0) return;
if (this.selectedCount === 0) return;
try {
const res = await fetch(this.routes.share, {
method: 'POST',
@@ -605,12 +648,17 @@ Alpine.data('filesManager', (config = {}) => ({
'X-CSRF-TOKEN': this.csrf,
'X-Requested-With': 'XMLHttpRequest',
},
body: JSON.stringify({ files: this.selected }),
body: JSON.stringify({
files: this.selectedFileIds,
folders: this.selectedFolderIds,
}),
});
const data = await res.json();
if (data.links?.length) {
await navigator.clipboard.writeText(data.links.join('\n'));
this.showToast(data.message || 'Share links copied.');
} else {
this.showToast(data.message || 'No share links available.');
}
} catch (e) {
this.showToast('Could not copy share links.');
@@ -618,23 +666,31 @@ Alpine.data('filesManager', (config = {}) => ({
},
async copyLinks() {
const links = [...new Set(this.selectedFiles().map((f) => f.shareUrl).filter(Boolean))];
if (links.length === 0) {
const links = [
...this.selectedFileRows().map((f) => f.shareUrl),
...this.selectedFolderRows().map((f) => f.shareUrl),
].filter(Boolean);
const uniqueLinks = [...new Set(links)];
if (uniqueLinks.length === 0) {
this.showToast('No share links available.');
return;
}
try {
await navigator.clipboard.writeText(links.join('\n'));
this.showToast(links.length === 1 ? 'Link copied.' : `${links.length} links copied.`);
await navigator.clipboard.writeText(uniqueLinks.join('\n'));
this.showToast(uniqueLinks.length === 1 ? 'Link copied.' : `${uniqueLinks.length} links copied.`);
} catch (e) {
this.showToast('Could not copy links.');
}
},
openInEmail() {
if (this.selected.length === 0) return;
if (this.selectedFileIds.length === 0) return;
if (this.selectedFolderIds.length > 0) {
this.showToast('Open in email works with files only.');
return;
}
const params = new URLSearchParams();
this.selected.forEach((id) => params.append('files[]', id));
this.selectedFileIds.forEach((id) => params.append('files[]', id));
window.location.href = `${this.routes.openInEmail}?${params.toString()}`;
},