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 += 'Upload ';
+ html += '
';
+
+ // Table
+ html += '';
+ html += 'Type Name Size ';
+ html += ' ';
+
+ // Parent dir
+ if (dir) {
+ var parentDir = dir.indexOf('/') >= 0 ? dir.substring(0, dir.lastIndexOf('/')) : '';
+ html += 'DIR ';
+ html += '.. ';
+ html += ' ';
+ }
+
+ for (var j = 0; j < entries.length; j++) {
+ var e = entries[j];
+ var absPath = '/softwares/' + e.path;
+ html += '';
+ html += '' + (e.type === 'directory' ? 'DIR' : 'FILE') + ' ';
+ html += '';
+ if (e.type === 'directory') {
+ html += '' + esc(e.name) + ' ';
+ } else {
+ html += esc(e.name);
+ }
+ html += ' ';
+ html += '' + (e.size ? humanSize(e.size) : '') + ' ';
+ html += 'Select ';
+ html += ' ';
+ }
+
+ html += '
';
+ 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; }
}