file manager popup rework 2

This commit is contained in:
2026-07-27 10:29:34 +02:00
parent 89c3712a39
commit f24e50d9fd
3 changed files with 202 additions and 33 deletions
+20
View File
@@ -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])
+171 -29
View File
@@ -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 = '<span class="fm-modal-title">Select file or folder</span>';
var closeBtn = document.createElement('button');
closeBtn.className = 'fm-modal-close';
closeBtn.innerHTML = '&times;';
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 = '<div style="text-align:center;padding:40px;color:#999;">Loading...</div>';
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 = '<div style="text-align:center;padding:40px;color:#999;">Loading...</div>';
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 = '<div style="color:red;padding:20px;">Error: ' + e.message + '</div>';
});
}
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 += '<div class="fm-breadcrumbs"><a href="#" data-dir="">softwares/</a>';
var acc = '';
for (var i = 0; i < parts.length; i++) {
acc = acc ? acc + '/' + parts[i] : parts[i];
html += '<a href="#" data-dir="' + esc(acc) + '">' + esc(parts[i]) + '/</a>';
}
html += '</div>';
// Upload bar
html += '<div class="fm-actions">';
html += '<input type="file" id="fm-picker-file" />';
html += '<button class="fm-btn" id="fm-picker-upload-btn">Upload</button>';
html += '</div>';
// Table
html += '<table class="fm-table"><thead><tr>';
html += '<th style="width:50px">Type</th><th>Name</th><th style="width:90px">Size</th>';
html += '<th style="width:80px"></th></tr></thead><tbody>';
// Parent dir
if (dir) {
var parentDir = dir.indexOf('/') >= 0 ? dir.substring(0, dir.lastIndexOf('/')) : '';
html += '<tr><td class="fm-type">DIR</td>';
html += '<td><a href="#" data-dir="' + esc(parentDir) + '">..</a></td>';
html += '<td></td><td></td></tr>';
}
for (var j = 0; j < entries.length; j++) {
var e = entries[j];
var absPath = '/softwares/' + e.path;
html += '<tr>';
html += '<td class="fm-type">' + (e.type === 'directory' ? 'DIR' : 'FILE') + '</td>';
html += '<td>';
if (e.type === 'directory') {
html += '<a href="#" data-dir="' + esc(e.path) + '">' + esc(e.name) + '</a>';
} else {
html += esc(e.name);
}
html += '</td>';
html += '<td>' + (e.size ? humanSize(e.size) : '') + '</td>';
html += '<td><a href="#" class="fm-btn fm-btn-select" data-select="' + esc(absPath) + '">Select</a></td>';
html += '</tr>';
}
html += '</tbody></table>';
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);
@@ -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; }
}