Add file editing functionality to dashboard
Implement API endpoints for retrieving and saving file content, allowing users to edit supported file types directly from the dashboard. Introduce a modal editor interface with syntax highlighting for various file formats. Update the HTML template to include the editor overlay and associated JavaScript for handling file operations, enhancing user experience and interactivity in managing portal files.
This commit is contained in:
@@ -47,6 +47,15 @@
|
||||
.desc-input { width: 100%; max-width: 280px; font-size: 0.8rem; background: var(--bg-tertiary); border: 1px solid var(--border); color: var(--text); padding: 0.3rem 0.5rem; border-radius: 4px; }
|
||||
.folder-name { font-weight: 500; }
|
||||
input[type="text"] { background: var(--bg-tertiary); border: 1px solid var(--border); color: var(--text); padding: 0.35rem 0.5rem; border-radius: 4px; font: inherit; }
|
||||
/* Editor modal */
|
||||
.editor-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 1000; display: none; align-items: center; justify-content: center; padding: 1rem; }
|
||||
.editor-overlay.visible { display: flex; }
|
||||
.editor-modal { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); width: 100%; max-width: 900px; max-height: 90vh; display: flex; flex-direction: column; box-shadow: 0 8px 32px rgba(0,0,0,0.4); }
|
||||
.editor-header { padding: 0.75rem 1rem; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }
|
||||
.editor-title { font-family: 'JetBrains Mono', monospace; font-size: 0.9rem; color: var(--text); }
|
||||
.editor-actions { display: flex; gap: 0.5rem; }
|
||||
.editor-body { flex: 1; min-height: 400px; overflow: hidden; }
|
||||
#editorHost { width: 100%; height: 100%; min-height: 420px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -80,8 +89,24 @@
|
||||
</table>
|
||||
<p id="portalEmpty" class="empty-msg" style="display:none;">No files or folders. Create a folder or upload a file.</p>
|
||||
</div>
|
||||
|
||||
<div id="editorOverlay" class="editor-overlay">
|
||||
<div class="editor-modal">
|
||||
<div class="editor-header">
|
||||
<span id="editorTitle" class="editor-title">—</span>
|
||||
<div class="editor-actions">
|
||||
<button type="button" class="btn btn-primary btn-sm" id="editorSaveBtn">Save</button>
|
||||
<button type="button" class="btn btn-outline btn-sm" id="editorCancelBtn">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-body">
|
||||
<div id="editorHost"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.0/ace.min.js" crossorigin="anonymous"></script>
|
||||
<script>
|
||||
function authFetch(url, opts) {
|
||||
opts = opts || {};
|
||||
@@ -142,7 +167,7 @@
|
||||
if (it.type === 'folder') {
|
||||
actions = '<button type="button" class="btn btn-outline btn-sm open-folder" data-path="' + escapeHtml(it.path) + '">Open</button> <button type="button" class="btn btn-danger btn-sm delete-item" data-path="' + escapeHtml(it.path) + '" data-type="folder">Delete</button>';
|
||||
} else {
|
||||
actions = '<a href="' + escapeHtml(baseUrl + it.path) + '" target="_blank" rel="noopener" class="btn btn-outline btn-sm">Open</a> <button type="button" class="btn btn-danger btn-sm delete-item" data-path="' + escapeHtml(it.path) + '" data-type="file">Delete</button>';
|
||||
actions = '<button type="button" class="btn btn-outline btn-sm edit-file" data-path="' + escapeHtml(it.path) + '">Edit</button> <a href="' + escapeHtml(baseUrl + it.path) + '" target="_blank" rel="noopener" class="btn btn-outline btn-sm">Open</a> <button type="button" class="btn btn-danger btn-sm delete-item" data-path="' + escapeHtml(it.path) + '" data-type="file">Delete</button>';
|
||||
}
|
||||
tr.innerHTML = '<td class="' + (it.type === 'folder' ? 'folder-name' : '') + '">' + escapeHtml(it.name) + (it.type === 'folder' ? ' /' : '') + '</td><td>' + typeLabel + '</td>' + sizeCell + descCell + '<td class="actions-cell">' + actions + '</td>';
|
||||
tbody.appendChild(tr);
|
||||
@@ -154,6 +179,9 @@
|
||||
tbody.querySelectorAll('.open-folder').forEach(function(btn) {
|
||||
btn.onclick = function() { currentPath = btn.getAttribute('data-path'); fetchPortal(); };
|
||||
});
|
||||
tbody.querySelectorAll('.edit-file').forEach(function(btn) {
|
||||
btn.onclick = function() { openEditor(btn.getAttribute('data-path')); };
|
||||
});
|
||||
tbody.querySelectorAll('.delete-item').forEach(function(btn) {
|
||||
btn.onclick = function() {
|
||||
var path = btn.getAttribute('data-path');
|
||||
@@ -209,6 +237,50 @@
|
||||
this.value = '';
|
||||
};
|
||||
|
||||
var editorInstance = null;
|
||||
var editorCurrentPath = null;
|
||||
var aceModeMap = { sh: 'sh', json: 'json', python: 'python', yaml: 'yaml', markdown: 'markdown', ini: 'ini', plain_text: 'plain_text' };
|
||||
|
||||
function initAce() {
|
||||
if (editorInstance) return editorInstance;
|
||||
var host = document.getElementById('editorHost');
|
||||
editorInstance = ace.edit(host);
|
||||
editorInstance.setTheme('ace/theme/tomorrow_night_eighties');
|
||||
editorInstance.setOptions({ fontSize: '13px', showPrintMargin: false, wrap: true, useSoftTabs: true, tabSize: 2 });
|
||||
editorInstance.session.setUseWorker(false);
|
||||
return editorInstance;
|
||||
}
|
||||
|
||||
function openEditor(path) {
|
||||
editorCurrentPath = path;
|
||||
document.getElementById('editorTitle').textContent = path;
|
||||
document.getElementById('editorOverlay').classList.add('visible');
|
||||
authFetch('/api/portal-files/content?path=' + encodeURIComponent(path)).then(function(r) { return r.json(); }).then(function(d) {
|
||||
if (!d.ok) { alert(d.error || 'Could not load file'); closeEditor(); return; }
|
||||
var ace = initAce();
|
||||
ace.session.setValue(d.content || '');
|
||||
var mode = aceModeMap[d.language] || 'plain_text';
|
||||
ace.session.setMode('ace/mode/' + mode);
|
||||
ace.focus();
|
||||
}).catch(function() { alert('Could not load file'); closeEditor(); });
|
||||
}
|
||||
|
||||
function closeEditor() {
|
||||
editorCurrentPath = null;
|
||||
document.getElementById('editorOverlay').classList.remove('visible');
|
||||
}
|
||||
|
||||
document.getElementById('editorSaveBtn').onclick = function() {
|
||||
if (!editorCurrentPath) return;
|
||||
var content = editorInstance.getSession().getValue();
|
||||
authFetch('/api/portal-files/content', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: editorCurrentPath, content: content }) })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) { if (d.ok) { closeEditor(); fetchPortal(); } else alert(d.error || 'Save failed'); })
|
||||
.catch(function() { alert('Save failed'); });
|
||||
};
|
||||
document.getElementById('editorCancelBtn').onclick = closeEditor;
|
||||
document.getElementById('editorOverlay').onclick = function(e) { if (e.target === document.getElementById('editorOverlay')) closeEditor(); };
|
||||
|
||||
fetchPortal();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user