using dialogs in file browser now
This commit is contained in:
@@ -317,7 +317,7 @@ function renderFileManagerPage(title, body) {
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>${title}</title>
|
<title>${title}</title>
|
||||||
<style>
|
<style>
|
||||||
:root { --ink:#0b0f19; --muted:#5b6470; --line:#d8dde4; --bg:#eef1f5; --card:#ffffff; --accent:#0f766e; --accent-strong:#0a5b55; }
|
:root { --ink:#0b0f19; --muted:#5b6470; --line:#d8dde4; --bg:#eef1f5; --card:#ffffff; --accent:#0f766e; --accent-strong:#0a5b55; --accent-soft:#e6f4f2; }
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
body { margin: 0; font-family: "IBM Plex Sans", "Noto Sans", sans-serif; background: var(--bg); color: var(--ink); }
|
body { margin: 0; font-family: "IBM Plex Sans", "Noto Sans", sans-serif; background: var(--bg); color: var(--ink); }
|
||||||
main { max-width: 1280px; margin: 0 auto; padding: 26px 18px 70px; }
|
main { max-width: 1280px; margin: 0 auto; padding: 26px 18px 70px; }
|
||||||
@@ -327,8 +327,14 @@ function renderFileManagerPage(title, body) {
|
|||||||
.muted { color: var(--muted); font-size: 0.95rem; }
|
.muted { color: var(--muted); font-size: 0.95rem; }
|
||||||
.card { margin-top: 18px; padding: 16px; background: var(--card); border-radius: 16px; border: 1px solid var(--line); box-shadow: 0 8px 26px rgba(12, 18, 28, 0.08); }
|
.card { margin-top: 18px; padding: 16px; background: var(--card); border-radius: 16px; border: 1px solid var(--line); box-shadow: 0 8px 26px rgba(12, 18, 28, 0.08); }
|
||||||
.toolbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
|
.toolbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
|
||||||
.tag { display: inline-flex; align-items: center; gap: 8px; padding: 4px 12px; border-radius: 999px; background: #f1f5f9; border: 1px solid var(--line); color: var(--ink); text-decoration: none; font-weight: 600; }
|
.tag { display: inline-flex; align-items: center; gap: 8px; padding: 6px 14px; border-radius: 999px; background: #f1f5f9; border: 1px solid var(--line); color: var(--ink); text-decoration: none; font-weight: 600; }
|
||||||
|
.tag.primary { background: var(--accent-soft); border-color: #bfe6e0; color: var(--accent-strong); }
|
||||||
.tag span { color: var(--muted); font-weight: 500; }
|
.tag span { color: var(--muted); font-weight: 500; }
|
||||||
|
.browser-shell { display: grid; gap: 16px; }
|
||||||
|
.browser-bar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
|
||||||
|
.crumbs { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
|
||||||
|
.crumbs a { color: var(--accent-strong); text-decoration: none; font-weight: 600; }
|
||||||
|
.crumbs span { color: var(--muted); }
|
||||||
.grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
|
.grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
|
||||||
form { display: grid; gap: 10px; }
|
form { display: grid; gap: 10px; }
|
||||||
label { display: grid; gap: 6px; font-weight: 600; }
|
label { display: grid; gap: 6px; font-weight: 600; }
|
||||||
@@ -339,12 +345,17 @@ function renderFileManagerPage(title, body) {
|
|||||||
button.secondary { background: transparent; color: var(--accent); }
|
button.secondary { background: transparent; color: var(--accent); }
|
||||||
table { width: 100%; border-collapse: collapse; font-size: 0.95rem; }
|
table { width: 100%; border-collapse: collapse; font-size: 0.95rem; }
|
||||||
th, td { text-align: left; padding: 10px 8px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
th, td { text-align: left; padding: 10px 8px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
||||||
tbody tr:nth-child(even) { background: #f9fbfc; }
|
tbody tr:hover { background: #f2f8f7; }
|
||||||
.name { display: inline-flex; align-items: center; gap: 10px; }
|
.name { display: inline-flex; align-items: center; gap: 10px; }
|
||||||
.name strong { font-weight: 700; }
|
.name strong { font-weight: 700; }
|
||||||
.folder { font-weight: 700; color: var(--accent-strong); text-decoration: none; }
|
.folder { font-weight: 700; color: var(--accent-strong); text-decoration: none; }
|
||||||
.actions { display: grid; gap: 6px; }
|
.actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
||||||
.actions input, .actions button { font-size: 0.9rem; padding: 7px 10px; }
|
.actions button { font-size: 0.9rem; padding: 7px 10px; }
|
||||||
|
dialog { border: none; border-radius: 16px; padding: 0; width: min(480px, 92vw); }
|
||||||
|
dialog::backdrop { background: rgba(15, 23, 42, 0.4); }
|
||||||
|
.dialog-card { padding: 18px; display: grid; gap: 12px; }
|
||||||
|
.dialog-actions { display: flex; gap: 10px; justify-content: flex-end; }
|
||||||
|
.dialog-actions .secondary { border-color: var(--line); color: var(--accent-strong); }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -743,8 +754,8 @@ app.get(`${basePath}/admin/dashboard`, requireAdminPage, async (req, res) => {
|
|||||||
<div class="muted">Systemstatistiken und Logs</div>
|
<div class="muted">Systemstatistiken und Logs</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<a class="tag" href="${baseUrl('/admin/files')}">Dateimanager</a>
|
<a class="tag primary" href="${baseUrl('/admin/files')}">Dateimanager</a>
|
||||||
<a class="tag" href="${baseUrl('/admin/users')}">Benutzer verwalten</a>
|
<a class="tag primary" href="${baseUrl('/admin/users')}">Benutzer verwalten</a>
|
||||||
<form method="post" action="${baseUrl('/admin/logout')}">
|
<form method="post" action="${baseUrl('/admin/logout')}">
|
||||||
${csrfField(res.locals.csrfToken)}
|
${csrfField(res.locals.csrfToken)}
|
||||||
<button type="submit" class="secondary">Abmelden</button>
|
<button type="submit" class="secondary">Abmelden</button>
|
||||||
@@ -937,11 +948,31 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => {
|
|||||||
|
|
||||||
const parentPath = relativePath ? relativePath.split('/').slice(0, -1).join('/') : '';
|
const parentPath = relativePath ? relativePath.split('/').slice(0, -1).join('/') : '';
|
||||||
|
|
||||||
const dirs = entries.filter((entry) => entry.isDirectory() && entry.name !== '_share');
|
const filtered = entries.filter((entry) => entry.name !== '_share');
|
||||||
const files = entries.filter((entry) => !entry.isDirectory() && entry.name !== '_share');
|
const details = await Promise.all(
|
||||||
|
filtered.map(async (entry) => {
|
||||||
|
const childPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
||||||
|
let stat = null;
|
||||||
|
try {
|
||||||
|
stat = await fs.promises.stat(path.join(resolved, entry.name));
|
||||||
|
} catch (err) {
|
||||||
|
stat = null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
entry,
|
||||||
|
childPath,
|
||||||
|
isDir: entry.isDirectory(),
|
||||||
|
size: stat && stat.isFile() ? stat.size : null,
|
||||||
|
modifiedAt: stat ? stat.mtimeMs : null,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const rowForEntry = (entry, isDir) => {
|
const dirs = details.filter((item) => item.isDir);
|
||||||
const childPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
const files = details.filter((item) => !item.isDir);
|
||||||
|
|
||||||
|
const rowForEntry = (item) => {
|
||||||
|
const { entry, childPath, isDir, size, modifiedAt } = item;
|
||||||
const href = baseUrl(`/admin/files?path=${encodeURIComponent(childPath)}`);
|
const href = baseUrl(`/admin/files?path=${encodeURIComponent(childPath)}`);
|
||||||
const escapedName = escapeHtml(entry.name);
|
const escapedName = escapeHtml(entry.name);
|
||||||
const escapedPath = escapeHtml(childPath);
|
const escapedPath = escapeHtml(childPath);
|
||||||
@@ -954,26 +985,19 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>${isDir ? 'Ordner' : 'Datei'}</td>
|
<td>${isDir ? 'Ordner' : 'Datei'}</td>
|
||||||
|
<td>${size ? formatBytes(size) : '—'}</td>
|
||||||
|
<td>${modifiedAt ? formatTimestamp(modifiedAt) : '—'}</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<form method="post" action="${baseUrl('/admin/files/rename')}">
|
<button type="button" class="secondary rename-trigger" data-path="${escapedPath}" data-name="${escapedName}">Umbenennen</button>
|
||||||
${csrfField(res.locals.csrfToken)}
|
<button type="button" class="secondary delete-trigger" data-path="${escapedPath}" data-name="${escapedName}">Löschen</button>
|
||||||
<input type="hidden" name="path" value="${escapedPath}" />
|
|
||||||
<input name="newName" placeholder="Neuer Name" required />
|
|
||||||
<button type="submit">Umbenennen</button>
|
|
||||||
</form>
|
|
||||||
<form method="post" action="${baseUrl('/admin/files/delete')}">
|
|
||||||
${csrfField(res.locals.csrfToken)}
|
|
||||||
<input type="hidden" name="path" value="${escapedPath}" />
|
|
||||||
<button type="submit" class="secondary">Löschen</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const tableRows = [
|
const tableRows = [
|
||||||
...dirs.map((entry) => rowForEntry(entry, true)),
|
...dirs.map((entry) => rowForEntry(entry)),
|
||||||
...files.map((entry) => rowForEntry(entry, false)),
|
...files.map((entry) => rowForEntry(entry)),
|
||||||
].join('');
|
].join('');
|
||||||
|
|
||||||
const body = `
|
const body = `
|
||||||
@@ -991,37 +1015,43 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="card">
|
<section class="card browser-shell">
|
||||||
<div class="toolbar">
|
<div class="browser-bar">
|
||||||
<span class="tag">Pfad <span>/${escapeHtml(relativePath || '')}</span></span>
|
<span class="tag">Pfad <span>/${escapeHtml(relativePath || '')}</span></span>
|
||||||
${relativePath ? `<a class="tag" href="${baseUrl(`/admin/files?path=${encodeURIComponent(parentPath)}`)}">← Zurück</a>` : ''}
|
${relativePath ? `<a class="tag primary" href="${baseUrl(`/admin/files?path=${encodeURIComponent(parentPath)}`)}">← Zurück</a>` : ''}
|
||||||
|
<div class="crumbs">
|
||||||
|
<span>Position:</span>
|
||||||
|
${relativePath ? relativePath.split('/').map((segment, idx, parts) => {
|
||||||
|
const crumbPath = parts.slice(0, idx + 1).join('/');
|
||||||
|
return `<a href="${baseUrl(`/admin/files?path=${encodeURIComponent(crumbPath)}`)}">${escapeHtml(segment)}</a>`;
|
||||||
|
}).join('<span>/</span>') : '<span>Root</span>'}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
<div class="grid">
|
||||||
|
<div>
|
||||||
<section class="card grid">
|
<h2>Ordner erstellen</h2>
|
||||||
<div>
|
<form method="post" action="${baseUrl('/admin/files/mkdir')}">
|
||||||
<h2>Ordner erstellen</h2>
|
${csrfField(res.locals.csrfToken)}
|
||||||
<form method="post" action="${baseUrl('/admin/files/mkdir')}">
|
<input type="hidden" name="path" value="${escapeHtml(relativePath)}" />
|
||||||
${csrfField(res.locals.csrfToken)}
|
<label>
|
||||||
<input type="hidden" name="path" value="${escapeHtml(relativePath)}" />
|
Ordnername
|
||||||
<label>
|
<input name="name" placeholder="z.B. Projekte" required />
|
||||||
Ordnername
|
</label>
|
||||||
<input name="name" placeholder="z.B. Projekte" required />
|
<button type="submit">Erstellen</button>
|
||||||
</label>
|
</form>
|
||||||
<button type="submit">Erstellen</button>
|
</div>
|
||||||
</form>
|
<div>
|
||||||
</div>
|
<h2>Datei hochladen</h2>
|
||||||
<div>
|
<form method="post" action="${baseUrl('/admin/files/upload')}" enctype="multipart/form-data">
|
||||||
<h2>Datei hochladen</h2>
|
${csrfField(res.locals.csrfToken)}
|
||||||
<form method="post" action="${baseUrl('/admin/files/upload')}" enctype="multipart/form-data">
|
<input type="hidden" name="path" value="${escapeHtml(relativePath)}" />
|
||||||
${csrfField(res.locals.csrfToken)}
|
<label>
|
||||||
<input type="hidden" name="path" value="${escapeHtml(relativePath)}" />
|
Datei
|
||||||
<label>
|
<input type="file" name="file" required />
|
||||||
Datei
|
</label>
|
||||||
<input type="file" name="file" required />
|
<button type="submit">Hochladen</button>
|
||||||
</label>
|
</form>
|
||||||
<button type="submit">Hochladen</button>
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -1033,6 +1063,8 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => {
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Typ</th>
|
<th>Typ</th>
|
||||||
|
<th>Größe</th>
|
||||||
|
<th>Geändert</th>
|
||||||
<th>Aktionen</th>
|
<th>Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -1042,6 +1074,70 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => {
|
|||||||
</table>
|
</table>
|
||||||
` : '<div class="muted">Keine Eintraege in diesem Ordner.</div>'}
|
` : '<div class="muted">Keine Eintraege in diesem Ordner.</div>'}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<dialog id="rename-dialog">
|
||||||
|
<form method="post" action="${baseUrl('/admin/files/rename')}" class="dialog-card">
|
||||||
|
${csrfField(res.locals.csrfToken)}
|
||||||
|
<input type="hidden" name="path" id="rename-path" />
|
||||||
|
<h2>Umbenennen</h2>
|
||||||
|
<div class="muted" id="rename-current"></div>
|
||||||
|
<label>
|
||||||
|
Neuer Name
|
||||||
|
<input name="newName" id="rename-name" required />
|
||||||
|
</label>
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<button type="button" class="secondary dialog-close">Abbrechen</button>
|
||||||
|
<button type="submit">Speichern</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<dialog id="delete-dialog">
|
||||||
|
<form method="post" action="${baseUrl('/admin/files/delete')}" class="dialog-card">
|
||||||
|
${csrfField(res.locals.csrfToken)}
|
||||||
|
<input type="hidden" name="path" id="delete-path" />
|
||||||
|
<h2>Löschen</h2>
|
||||||
|
<div class="muted" id="delete-current"></div>
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<button type="button" class="secondary dialog-close">Abbrechen</button>
|
||||||
|
<button type="submit">Löschen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const renameDialog = document.getElementById('rename-dialog');
|
||||||
|
const deleteDialog = document.getElementById('delete-dialog');
|
||||||
|
const renamePath = document.getElementById('rename-path');
|
||||||
|
const renameName = document.getElementById('rename-name');
|
||||||
|
const renameCurrent = document.getElementById('rename-current');
|
||||||
|
const deletePath = document.getElementById('delete-path');
|
||||||
|
const deleteCurrent = document.getElementById('delete-current');
|
||||||
|
|
||||||
|
document.querySelectorAll('.rename-trigger').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
renamePath.value = btn.dataset.path || '';
|
||||||
|
renameName.value = btn.dataset.name || '';
|
||||||
|
renameCurrent.textContent = `Aktuell: ${btn.dataset.name || ''}`;
|
||||||
|
renameDialog.showModal();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.delete-trigger').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
deletePath.value = btn.dataset.path || '';
|
||||||
|
deleteCurrent.textContent = `Datei/Ordner: ${btn.dataset.name || ''}`;
|
||||||
|
deleteDialog.showModal();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.dialog-close').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
renameDialog.close();
|
||||||
|
deleteDialog.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
res.send(renderFileManagerPage('Admin-Dateien', body));
|
res.send(renderFileManagerPage('Admin-Dateien', body));
|
||||||
|
|||||||
Reference in New Issue
Block a user