mirror of
https://github.com/System-End/cdn.git
synced 2026-04-19 19:45:07 +00:00
batchy yay
This commit is contained in:
parent
53f80ef244
commit
cec596e620
6 changed files with 165 additions and 15 deletions
|
|
@ -19,6 +19,7 @@ class Components::Uploads::Index < Components::Base
|
|||
uploads_list
|
||||
pagination_section if uploads.respond_to?(:total_pages) && uploads.total_pages > 1
|
||||
end
|
||||
batch_delete_bar
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -65,10 +66,36 @@ 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",
|
||||
id: "select-all-uploads",
|
||||
data: { batch_select_all: true },
|
||||
style: "cursor: pointer;"
|
||||
)
|
||||
label(for: "select-all-uploads", style: "font-size: 13px; color: var(--fgColor-muted, #656d76); cursor: pointer;") do
|
||||
plain "Select all"
|
||||
end
|
||||
end
|
||||
|
||||
render Primer::Beta::BorderBox.new do |box|
|
||||
uploads.each do |upload|
|
||||
box.with_row do
|
||||
render Components::Uploads::Row.new(upload: upload, compact: false)
|
||||
div(style: "display: flex; align-items: flex-start; gap: 12px;") do
|
||||
input(
|
||||
type: "checkbox",
|
||||
name: "ids[]",
|
||||
value: upload.id,
|
||||
form: "batch-delete-form",
|
||||
class: "batch-select-checkbox",
|
||||
data: { batch_select_item: true },
|
||||
style: "margin-top: 6px; cursor: pointer;"
|
||||
)
|
||||
div(style: "flex: 1; min-width: 0;") do
|
||||
render Components::Uploads::Row.new(upload: upload, compact: false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -104,4 +131,21 @@ class Components::Uploads::Index < Components::Base
|
|||
input(type: "file", name: "files[]", id: "dropzone-file-input", multiple: true, data: { dropzone_input: true }, style: "display: none;")
|
||||
end
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -108,6 +108,28 @@ module API
|
|||
render json: { error: "Rename failed: #{e.message}" }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
# DELETE /api/v4/uploads/batch
|
||||
def destroy_batch
|
||||
ids = Array(params[:ids]).reject(&:blank?)
|
||||
|
||||
if ids.empty?
|
||||
render json: { error: "Missing ids[] parameter" }, status: :bad_request
|
||||
return
|
||||
end
|
||||
|
||||
uploads = current_user.uploads.where(id: ids).includes(:blob)
|
||||
found_ids = uploads.map(&:id)
|
||||
not_found_ids = ids - found_ids
|
||||
|
||||
deleted = uploads.map { |u| { id: u.id, filename: u.filename.to_s } }
|
||||
uploads.destroy_all
|
||||
|
||||
response = { deleted: deleted }
|
||||
response[:not_found] = not_found_ids if not_found_ids.any?
|
||||
|
||||
render json: response, status: :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_quota
|
||||
|
|
|
|||
|
|
@ -65,6 +65,23 @@ class UploadsController < ApplicationController
|
|||
redirect_back fallback_location: uploads_path, alert: "You are not authorized to delete this upload."
|
||||
end
|
||||
|
||||
def destroy_batch
|
||||
ids = Array(params[:ids]).reject(&:blank?)
|
||||
|
||||
if ids.empty?
|
||||
redirect_to uploads_path, alert: "No files selected."
|
||||
return
|
||||
end
|
||||
|
||||
uploads = current_user.uploads.where(id: ids)
|
||||
count = uploads.size
|
||||
filenames = uploads.includes(:blob).map { |u| u.filename.to_s }
|
||||
|
||||
uploads.destroy_all
|
||||
|
||||
redirect_to uploads_path, notice: "Deleted #{count} #{'file'.pluralize(count)}: #{filenames.join(', ')}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def extract_uploaded_files
|
||||
|
|
|
|||
74
app/frontend/controllers/batch_select.js
Normal file
74
app/frontend/controllers/batch_select.js
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
(function () {
|
||||
function init() {
|
||||
const selectAll = document.querySelector("[data-batch-select-all]");
|
||||
const bar = document.querySelector("[data-batch-bar]");
|
||||
const countLabel = document.querySelector("[data-batch-count]");
|
||||
const deselectBtn = document.querySelector("[data-batch-deselect]");
|
||||
|
||||
if (!bar) return;
|
||||
|
||||
function getCheckboxes() {
|
||||
return document.querySelectorAll("[data-batch-select-item]");
|
||||
}
|
||||
|
||||
function updateBar() {
|
||||
const checkboxes = getCheckboxes();
|
||||
const checked = Array.from(checkboxes).filter((cb) => cb.checked);
|
||||
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";
|
||||
}
|
||||
|
||||
// Sync "select all" checkbox
|
||||
if (selectAll) {
|
||||
selectAll.checked =
|
||||
checkboxes.length > 0 && checked.length === checkboxes.length;
|
||||
selectAll.indeterminate =
|
||||
checked.length > 0 && checked.length < checkboxes.length;
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for individual checkbox changes
|
||||
document.addEventListener("change", (e) => {
|
||||
if (e.target.matches("[data-batch-select-item]")) {
|
||||
updateBar();
|
||||
}
|
||||
});
|
||||
|
||||
// Select all / deselect all toggle
|
||||
if (selectAll) {
|
||||
selectAll.addEventListener("change", () => {
|
||||
const checkboxes = getCheckboxes();
|
||||
checkboxes.forEach((cb) => {
|
||||
cb.checked = selectAll.checked;
|
||||
});
|
||||
updateBar();
|
||||
});
|
||||
}
|
||||
|
||||
// Deselect all button in the bar
|
||||
if (deselectBtn) {
|
||||
deselectBtn.addEventListener("click", () => {
|
||||
const checkboxes = getCheckboxes();
|
||||
checkboxes.forEach((cb) => {
|
||||
cb.checked = false;
|
||||
});
|
||||
if (selectAll) selectAll.checked = false;
|
||||
updateBar();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
document.addEventListener("turbo:load", init);
|
||||
})();
|
||||
|
|
@ -3,3 +3,4 @@ Rails.start();
|
|||
import "@primer/view-components";
|
||||
|
||||
import "../controllers/upload_dropzone.js";
|
||||
import "../controllers/batch_select.js";
|
||||
|
|
|
|||
|
|
@ -13,7 +13,11 @@ Rails.application.routes.draw do
|
|||
get "/auth/hack_club/callback", to: "sessions#create"
|
||||
get "/auth/failure", to: "sessions#failure"
|
||||
|
||||
resources :uploads, only: [ :index, :create, :update, :destroy ]
|
||||
resources :uploads, only: [ :index, :create, :update, :destroy ] do
|
||||
collection do
|
||||
delete :destroy_batch
|
||||
end
|
||||
end
|
||||
|
||||
resources :api_keys, only: [ :index, :create, :destroy ]
|
||||
|
||||
|
|
@ -24,33 +28,21 @@ Rails.application.routes.draw do
|
|||
post "uploads", to: "uploads#create_batch"
|
||||
post "upload_from_url", to: "uploads#create_from_url"
|
||||
patch "uploads/:id/rename", to: "uploads#rename", as: :upload_rename
|
||||
delete "uploads/batch", to: "uploads#destroy_batch", as: :uploads_batch_delete
|
||||
post "revoke", to: "api_keys#revoke"
|
||||
end
|
||||
end
|
||||
|
||||
get "/docs", to: redirect("/docs/getting-started")
|
||||
get "/docs/:id", to: "docs#show", as: :doc
|
||||
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
|
||||
|
||||
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
|
||||
# Can be used by load balancers and uptime monitors to verify that the app is live.
|
||||
get "up" => "rails/health#show", as: :rails_health_check
|
||||
|
||||
# Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb)
|
||||
# get "manifest" => "rails/pwa#manifest", as: :pwa_manifest
|
||||
# get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker
|
||||
|
||||
# Defines the root path route ("/")
|
||||
# root "posts#index"
|
||||
|
||||
# Rescue endpoint to find uploads by original URL
|
||||
get "/rescue", to: "external_uploads#rescue", as: :rescue_upload
|
||||
|
||||
# Slack events webhook
|
||||
namespace :slack do
|
||||
post "events", to: "events#create"
|
||||
end
|
||||
|
||||
# External upload redirects (must be last to avoid conflicts)
|
||||
get "/:id/*filename", to: "external_uploads#show", constraints: { id: /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/ }, as: :external_upload
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue