From 6567d71ce82d790b1a10a5f6f3261dbd5e724995 Mon Sep 17 00:00:00 2001 From: End Nightshade Date: Tue, 10 Feb 2026 13:52:42 -0700 Subject: [PATCH] plz no lookie shit --- app/components/uploads/_row.rb | 25 +++----- app/components/uploads/index.rb | 23 ++++---- app/frontend/controllers/batch_select.js | 72 +++++++++++++++++++----- 3 files changed, 77 insertions(+), 43 deletions(-) diff --git a/app/components/uploads/_row.rb b/app/components/uploads/_row.rb index 35e1be2..da54aa9 100644 --- a/app/components/uploads/_row.rb +++ b/app/components/uploads/_row.rb @@ -26,7 +26,7 @@ class Components::Uploads::Row < Components::Base def compact_content div(style: "flex: 1; min-width: 0;") do - div(style: "font-size: 14px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;") do + div(style: "font-size: 14px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer;", data: { batch_select_toggle: upload.id }) do render Primer::Beta::Octicon.new(icon: file_icon_for(upload.content_type), size: :small, mr: 1) plain upload.filename.to_s end @@ -50,7 +50,7 @@ class Components::Uploads::Row < Components::Base div(style: "flex: 1; min-width: 0;") do div(style: "display: flex; align-items: center; gap: 8px; margin-bottom: 8px; min-width: 0;") do render Primer::Beta::Octicon.new(icon: file_icon_for(upload.content_type), size: :small) - div(style: "flex: 1; min-width: 0; font-size: 14px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;") do + div(style: "flex: 1; min-width: 0; font-size: 14px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer;", data: { batch_select_toggle: upload.id }) do plain upload.filename.to_s end render(Primer::Beta::Label.new(scheme: :secondary)) { plain upload.provenance.titleize } @@ -99,20 +99,13 @@ class Components::Uploads::Row < Components::Base def file_icon_for(content_type) case content_type - when /image/ - :image - when /video/ - :video - when /audio/ - :unmute - when /pdf/ - :file - when /zip|rar|tar|gz/ - :"file-zip" - when /text|json|xml/ - :code - else - :file + when /image/ then :image + when /video/ then :video + when /audio/ then :unmute + when /pdf/ then :file + when /zip|rar|tar|gz/ then :"file-zip" + when /text|json|xml/ then :code + else :file end end end diff --git a/app/components/uploads/index.rb b/app/components/uploads/index.rb index 1b56d57..820d9ea 100644 --- a/app/components/uploads/index.rb +++ b/app/components/uploads/index.rb @@ -66,7 +66,6 @@ class Components::Uploads::Index < Components::Base def uploads_list if uploads.any? - # Select all toggle div(style: "display: flex; align-items: center; gap: 8px; margin-bottom: 8px; padding: 4px 0;") do input( type: "checkbox", @@ -88,8 +87,7 @@ class Components::Uploads::Index < Components::Base name: "ids[]", value: upload.id, form: "batch-delete-form", - class: "batch-select-checkbox", - data: { batch_select_item: true }, + data: { batch_select_item: true, upload_id: upload.id }, style: "margin-top: 6px; cursor: pointer;" ) div(style: "flex: 1; min-width: 0;") do @@ -133,16 +131,15 @@ class Components::Uploads::Index < Components::Base end def batch_delete_bar - div(id: "batch-delete-bar", data: { batch_bar: true }, style: "display: none; position: fixed; bottom: 0; left: 0; right: 0; background: var(--bgColor-danger-muted, #FFEBE9); border-top: 1px solid var(--borderColor-danger-muted, #ffcecb); padding: 12px 24px; z-index: 100;") do - div(style: "max-width: 1200px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center;") do - span(data: { batch_count: true }, style: "font-size: 14px; font-weight: 500;") { "0 files selected" } - div(style: "display: flex; gap: 8px;") do - button(type: "button", data: { batch_deselect: true }, class: "btn btn-sm") { "Deselect all" } - form_with url: destroy_batch_uploads_path, method: :delete, id: "batch-delete-form", data: { turbo_confirm: "Are you sure you want to delete the selected files? This cannot be undone." } do - button(type: "submit", class: "btn btn-sm btn-danger") do - render Primer::Beta::Octicon.new(icon: :trash, mr: 1) - plain "Delete selected" - end + div(id: "batch-delete-bar", data: { batch_bar: true }, style: "display: none; position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); background: var(--bgColor-default, #fff); border: 1px solid var(--borderColor-default, #d0d7de); border-radius: 12px; padding: 12px 20px; z-index: 100; box-shadow: 0 8px 24px rgba(0,0,0,0.12); min-width: 320px; max-width: 600px;") do + div(style: "display: flex; align-items: center; gap: 16px;") do + span(data: { batch_count: true }, style: "font-size: 14px; font-weight: 600; white-space: nowrap;") { "0 selected" } + div(style: "flex: 1;") + button(type: "button", data: { batch_deselect: true }, class: "btn btn-sm", style: "white-space: nowrap;") { "Deselect" } + form_with url: destroy_batch_uploads_path, method: :delete, id: "batch-delete-form", data: { batch_delete_form: true } do + button(type: "submit", class: "btn btn-sm btn-danger", style: "white-space: nowrap;") do + render Primer::Beta::Octicon.new(icon: :trash, mr: 1) + plain "Delete" end end end diff --git a/app/frontend/controllers/batch_select.js b/app/frontend/controllers/batch_select.js index 88f73e2..4953c8b 100644 --- a/app/frontend/controllers/batch_select.js +++ b/app/frontend/controllers/batch_select.js @@ -4,6 +4,7 @@ const bar = document.querySelector("[data-batch-bar]"); const countLabel = document.querySelector("[data-batch-count]"); const deselectBtn = document.querySelector("[data-batch-deselect]"); + const deleteForm = document.querySelector("[data-batch-delete-form]"); if (!bar) return; @@ -11,20 +12,19 @@ return document.querySelectorAll("[data-batch-select-item]"); } + function getChecked() { + return Array.from(getCheckboxes()).filter((cb) => cb.checked); + } + function updateBar() { const checkboxes = getCheckboxes(); - const checked = Array.from(checkboxes).filter((cb) => cb.checked); + const checked = getChecked(); const count = checked.length; - if (count > 0) { - bar.style.display = "block"; - countLabel.textContent = - count + (count === 1 ? " file selected" : " files selected"); - } else { - bar.style.display = "none"; - } + bar.style.display = count > 0 ? "block" : "none"; + countLabel.textContent = + count + (count === 1 ? " file selected" : " files selected"); - // Sync "select all" checkbox if (selectAll) { selectAll.checked = checkboxes.length > 0 && checked.length === checkboxes.length; @@ -33,14 +33,29 @@ } } - // Listen for individual checkbox changes + // Individual checkbox changes document.addEventListener("change", (e) => { if (e.target.matches("[data-batch-select-item]")) { updateBar(); } }); - // Select all / deselect all toggle + // Click filename to toggle its checkbox + document.addEventListener("click", (e) => { + const toggle = e.target.closest("[data-batch-select-toggle]"); + if (!toggle) return; + + const uploadId = toggle.dataset.batchSelectToggle; + const checkbox = document.querySelector( + '[data-batch-select-item][data-upload-id="' + uploadId + '"]', + ); + if (checkbox) { + checkbox.checked = !checkbox.checked; + updateBar(); + } + }); + + // Select all toggle if (selectAll) { selectAll.addEventListener("change", () => { const checkboxes = getCheckboxes(); @@ -51,17 +66,46 @@ }); } - // Deselect all button in the bar + // Deselect all button if (deselectBtn) { deselectBtn.addEventListener("click", () => { - const checkboxes = getCheckboxes(); - checkboxes.forEach((cb) => { + getCheckboxes().forEach((cb) => { cb.checked = false; }); if (selectAll) selectAll.checked = false; updateBar(); }); } + + // Confirmation before batch delete + if (deleteForm) { + deleteForm.addEventListener("submit", (e) => { + const checked = getChecked(); + if (checked.length === 0) { + e.preventDefault(); + return; + } + + // Build a list of filenames from the row next to each checkbox + const names = checked.map((cb) => { + const row = cb.closest("[class*='BorderBox']") || cb.parentElement; + const nameEl = row.querySelector("[data-batch-select-toggle]"); + return nameEl ? nameEl.textContent.trim() : cb.value; + }); + + const message = + "Delete " + + checked.length + + (checked.length === 1 ? " file" : " files") + + "?\n\n" + + names.map((n) => " • " + n).join("\n") + + "\n\nThis cannot be undone."; + + if (!confirm(message)) { + e.preventDefault(); + } + }); + } } if (document.readyState === "loading") {