From f24e50d9fd84beee8e36949f3a0eb3a4b79d17a7 Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Mon, 27 Jul 2026 10:29:34 +0200 Subject: [PATCH] file manager popup rework 2 --- apps/api/app/admin/files.rb | 20 ++ .../app/assets/javascripts/active_admin.js | 200 +++++++++++++++--- .../app/assets/stylesheets/active_admin.scss | 15 +- 3 files changed, 202 insertions(+), 33 deletions(-) diff --git a/apps/api/app/admin/files.rb b/apps/api/app/admin/files.rb index a3fef2b..622661c 100644 --- a/apps/api/app/admin/files.rb +++ b/apps/api/app/admin/files.rb @@ -152,6 +152,26 @@ ActiveAdmin.register_page "Files" do end end + # JSON API for AJAX file picker + page_action :list, method: :get do + service = FileManagerService.new + dir = params[:dir].to_s.presence || "" + begin + entries = service.list(dir) + render json: { entries: entries, dir: dir } + rescue ArgumentError => e + render json: { entries: [], dir: dir, error: e.message } + end + end + + page_action :ajax_upload, method: :post do + service = FileManagerService.new + path = service.upload(params[:dir].to_s, params[:file]) + render json: { ok: true, path: path } + rescue => e + render json: { ok: false, error: e.message }, status: 422 + end + page_action :upload, method: :post do service = FileManagerService.new service.upload(params[:dir].to_s, params[:file]) diff --git a/apps/api/app/assets/javascripts/active_admin.js b/apps/api/app/assets/javascripts/active_admin.js index d60f0cc..bbe544d 100644 --- a/apps/api/app/assets/javascripts/active_admin.js +++ b/apps/api/app/assets/javascripts/active_admin.js @@ -2,6 +2,7 @@ (function() { var PATH_FIELDS = ['html_folder_path', 'cartridge_path', 'source_path', 'docs_folder_path']; + var _pickerTarget = null; function addBrowseButtons() { PATH_FIELDS.forEach(function(fieldName) { @@ -14,66 +15,207 @@ btn.className = 'fm-browse-btn'; btn.addEventListener('click', function(e) { e.preventDefault(); - openFilePicker(input.id); + _pickerTarget = input; + openPicker(''); }); input.parentNode.insertBefore(btn, input.nextSibling); }); }); } - function openFilePicker(fieldId) { - // Backdrop + function openPicker(dir) { + closePicker(); var overlay = document.createElement('div'); overlay.className = 'fm-modal-overlay'; + overlay.id = 'fm-picker-overlay'; overlay.addEventListener('click', function(e) { - if (e.target === overlay) closeFilePicker(); + if (e.target === overlay) closePicker(); }); - // Modal container var modal = document.createElement('div'); modal.className = 'fm-modal'; - // Close button + var header = document.createElement('div'); + header.className = 'fm-modal-header'; + header.innerHTML = 'Select file or folder'; var closeBtn = document.createElement('button'); closeBtn.className = 'fm-modal-close'; closeBtn.innerHTML = '×'; - closeBtn.addEventListener('click', closeFilePicker); - modal.appendChild(closeBtn); + closeBtn.addEventListener('click', closePicker); + header.appendChild(closeBtn); + modal.appendChild(header); - // Iframe - var iframe = document.createElement('iframe'); - iframe.src = '/admin/files?picker=1&field=' + encodeURIComponent(fieldId); - iframe.className = 'fm-modal-iframe'; - modal.appendChild(iframe); + var body = document.createElement('div'); + body.className = 'fm-modal-body'; + body.id = 'fm-picker-body'; + body.innerHTML = '
Loading...
'; + modal.appendChild(body); overlay.appendChild(modal); document.body.appendChild(overlay); document.body.style.overflow = 'hidden'; - window._fmOverlay = overlay; + loadDir(dir); } - function closeFilePicker() { - if (window._fmOverlay) { - window._fmOverlay.remove(); - window._fmOverlay = null; - document.body.style.overflow = ''; + function closePicker() { + var ov = document.getElementById('fm-picker-overlay'); + if (ov) { ov.remove(); } + document.body.style.overflow = ''; + } + + function selectPath(absPath) { + if (_pickerTarget) { + _pickerTarget.value = absPath; + _pickerTarget.dispatchEvent(new Event('change', { bubbles: true })); + } + closePicker(); + } + + function loadDir(dir) { + var body = document.getElementById('fm-picker-body'); + if (!body) return; + body.innerHTML = '
Loading...
'; + + fetch('/admin/files/list?dir=' + encodeURIComponent(dir)) + .then(function(r) { return r.json(); }) + .then(function(data) { renderDir(data.entries || [], data.dir || ''); }) + .catch(function(e) { + body.innerHTML = '
Error: ' + e.message + '
'; + }); + } + + function renderDir(entries, dir) { + var body = document.getElementById('fm-picker-body'); + if (!body) return; + + var html = ''; + + // Breadcrumb + var parts = dir ? dir.split('/').filter(function(p){return p;}) : []; + html += '
softwares/'; + var acc = ''; + for (var i = 0; i < parts.length; i++) { + acc = acc ? acc + '/' + parts[i] : parts[i]; + html += '' + esc(parts[i]) + '/'; + } + html += '
'; + + // Upload bar + html += '
'; + html += ''; + html += ''; + html += '
'; + + // Table + html += ''; + html += ''; + html += ''; + + // Parent dir + if (dir) { + var parentDir = dir.indexOf('/') >= 0 ? dir.substring(0, dir.lastIndexOf('/')) : ''; + html += ''; + html += ''; + html += ''; + } + + for (var j = 0; j < entries.length; j++) { + var e = entries[j]; + var absPath = '/softwares/' + e.path; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + } + + html += '
TypeNameSize
DIR..
' + (e.type === 'directory' ? 'DIR' : 'FILE') + ''; + if (e.type === 'directory') { + html += '' + esc(e.name) + ''; + } else { + html += esc(e.name); + } + html += '' + (e.size ? humanSize(e.size) : '') + 'Select
'; + body.innerHTML = html; + + // Event delegation + body.addEventListener('click', handlePickerClick); + } + + function handlePickerClick(e) { + var t = e.target; + + // Navigate directory + if (t.tagName === 'A' && t.hasAttribute('data-dir')) { + e.preventDefault(); + loadDir(t.getAttribute('data-dir')); + return; + } + + // Select file/folder + if (t.hasAttribute('data-select')) { + e.preventDefault(); + selectPath(t.getAttribute('data-select')); + return; + } + + // Upload button + if (t.id === 'fm-picker-upload-btn') { + e.preventDefault(); + var fileInput = document.getElementById('fm-picker-file'); + if (!fileInput || !fileInput.files.length) { alert('Select a file first'); return; } + + var breadcrumbs = document.querySelector('#fm-picker-body .fm-breadcrumbs'); + var lastLink = breadcrumbs ? breadcrumbs.querySelectorAll('a') : []; + var currentDir = lastLink.length ? lastLink[lastLink.length - 1].getAttribute('data-dir') || '' : ''; + + var csrfToken = document.querySelector('meta[name="csrf-token"]'); + var fd = new FormData(); + fd.append('file', fileInput.files[0]); + fd.append('dir', currentDir); + + t.textContent = 'Uploading...'; + t.disabled = true; + + fetch('/admin/files/ajax_upload', { + method: 'POST', + headers: csrfToken ? { 'X-CSRF-Token': csrfToken.getAttribute('content') } : {}, + body: fd + }) + .then(function(r) { return r.json(); }) + .then(function(data) { + if (data.ok) { + loadDir(currentDir); + } else { + alert('Upload failed: ' + (data.error || 'unknown error')); + t.textContent = 'Upload'; + t.disabled = false; + } + }) + .catch(function(err) { + alert('Upload error: ' + err.message); + t.textContent = 'Upload'; + t.disabled = false; + }); } } - // Global callback for iframe to call directly (same-origin) - window.filePickerSelect = function(fieldId, path) { - var input = document.getElementById(fieldId); - if (input) { - input.value = path; - input.dispatchEvent(new Event('change', { bubbles: true })); - } - closeFilePicker(); - }; + function esc(s) { + var d = document.createElement('div'); + d.textContent = s; + return d.innerHTML; + } + + function humanSize(bytes) { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; + return (bytes / 1048576).toFixed(1) + ' MB'; + } document.addEventListener('DOMContentLoaded', addBrowseButtons); - // ActiveAdmin has_many dynamic rows if (typeof jQuery !== 'undefined') { jQuery(document).on('has_many_add:after', function() { setTimeout(addBrowseButtons, 50); diff --git a/apps/api/app/assets/stylesheets/active_admin.scss b/apps/api/app/assets/stylesheets/active_admin.scss index 3f100cf..6393299 100644 --- a/apps/api/app/assets/stylesheets/active_admin.scss +++ b/apps/api/app/assets/stylesheets/active_admin.scss @@ -39,12 +39,19 @@ position: fixed; inset: 0; z-index: 9999; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; } .fm-modal { - position: relative; width: 90vw; max-width: 1000px; height: 80vh; background: #fff; border-radius: 8px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); overflow: hidden; + position: relative; width: 90vw; max-width: 1000px; height: 80vh; background: #fff; border-radius: 8px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); display: flex; flex-direction: column; overflow: hidden; } +.fm-modal-header { + display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid #e0e0e0; background: #f8f8f8; flex-shrink: 0; +} +.fm-modal-title { font-weight: bold; font-size: 15px; color: #333; } .fm-modal-close { - position: absolute; top: 8px; right: 12px; z-index: 10; background: none; border: none; font-size: 28px; color: #666; cursor: pointer; line-height: 1; + background: none; border: none; font-size: 24px; color: #666; cursor: pointer; line-height: 1; padding: 0 4px; &:hover { color: #000; } } -.fm-modal-iframe { - width: 100%; height: 100%; border: none; +.fm-modal-body { + flex: 1; overflow-y: auto; padding: 16px; + .fm-breadcrumbs { margin-bottom: 12px; } + .fm-actions { margin-bottom: 12px; } + .fm-table { font-size: 13px; } }