Align dashboard and installer flows with spec
This commit is contained in:
@@ -44,7 +44,7 @@ function renderPackList(packs) {
|
||||
const label = document.createElement('label')
|
||||
label.className = 'packOption'
|
||||
label.innerHTML = `
|
||||
<input type="radio" name="packChoice" value="${pack.file}" />
|
||||
<input type="radio" name="packChoice" value="${pack.file}" data-pack-name="${pack.name}" />
|
||||
<div>
|
||||
<strong>${pack.name}</strong>
|
||||
<span>${pack.file}</span>
|
||||
@@ -52,15 +52,6 @@ function renderPackList(packs) {
|
||||
`
|
||||
packList.appendChild(label)
|
||||
})
|
||||
|
||||
packList.addEventListener('change', () => {
|
||||
const checked = packList.querySelector('input[name="packChoice"]:checked')
|
||||
if (checked == null) {
|
||||
state.selectedPack = null
|
||||
return
|
||||
}
|
||||
state.selectedPack = packs.find((pack) => pack.file === checked.value) ?? null
|
||||
})
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
@@ -71,6 +62,18 @@ async function bootstrap() {
|
||||
|
||||
window.installerApi.onLog(appendLog)
|
||||
|
||||
packList.addEventListener('change', () => {
|
||||
const checked = packList.querySelector('input[name="packChoice"]:checked')
|
||||
if (checked == null) {
|
||||
state.selectedPack = null
|
||||
return
|
||||
}
|
||||
state.selectedPack = {
|
||||
file: checked.value,
|
||||
name: checked.dataset.packName ?? checked.value
|
||||
}
|
||||
})
|
||||
|
||||
document.querySelectorAll('[data-back]').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
setActiveStep(button.dataset.back)
|
||||
|
||||
@@ -191,21 +191,60 @@ button {
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.deleteToolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.deleteActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.selectableCard {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.selectableCard input[type="checkbox"] {
|
||||
.selectionBox {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
}
|
||||
|
||||
.ghostButton {
|
||||
min-height: 46px;
|
||||
padding: 0 20px;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--line);
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.selectionTitle {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.warningBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
min-height: 30px;
|
||||
padding: 0 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(240, 191, 87, 0.16);
|
||||
color: var(--accent);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.adminLoginBody {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { fetchReleaseVersions } from '../../shared/mojang'
|
||||
import {
|
||||
createNewPack,
|
||||
deletePacks,
|
||||
listDashboardPacks,
|
||||
loadAccounts,
|
||||
loadPackDefinition,
|
||||
loadRootManifest,
|
||||
@@ -59,10 +60,10 @@ opRouter.post('/op/logout', requireAuth, (req, res) => {
|
||||
|
||||
opRouter.get('/op/dashboard', requireAuth, async (_req, res, next) => {
|
||||
try {
|
||||
const manifest = await loadRootManifest()
|
||||
const packs = await listDashboardPacks()
|
||||
res.render('op/dashboard', {
|
||||
userId: _req.session.userId,
|
||||
packs: manifest.packs
|
||||
packs
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from 'node:fs'
|
||||
import fsp from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { accountPath, fileDir, manifestDir, manifestRootPath } from './paths'
|
||||
import { AccountEntry, PackDefinition, PackListEntry, RootManifest } from './types'
|
||||
import { AccountEntry, DashboardPackEntry, PackDefinition, PackListEntry, RootManifest } from './types'
|
||||
|
||||
const defaultRootManifest: RootManifest = {
|
||||
packs: [
|
||||
@@ -93,6 +93,22 @@ export async function listManifestFiles(): Promise<string[]> {
|
||||
.sort((left, right) => left.localeCompare(right))
|
||||
}
|
||||
|
||||
export async function listDashboardPacks(): Promise<DashboardPackEntry[]> {
|
||||
const [manifestFiles, rootManifest] = await Promise.all([
|
||||
listManifestFiles(),
|
||||
loadRootManifest()
|
||||
])
|
||||
|
||||
return manifestFiles.map((file) => {
|
||||
const registeredPack = rootManifest.packs.find((entry) => entry.file === file)
|
||||
return {
|
||||
file,
|
||||
name: registeredPack?.name ?? file,
|
||||
registered: registeredPack != null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function loadPackDefinition(packKey: string): Promise<PackDefinition | null> {
|
||||
await ensureProjectFiles()
|
||||
const safeKey = sanitizePackKey(packKey)
|
||||
|
||||
@@ -3,6 +3,10 @@ export interface PackListEntry {
|
||||
file: string
|
||||
}
|
||||
|
||||
export interface DashboardPackEntry extends PackListEntry {
|
||||
registered: boolean
|
||||
}
|
||||
|
||||
export interface RootManifest {
|
||||
packs: PackListEntry[]
|
||||
}
|
||||
|
||||
@@ -14,14 +14,23 @@
|
||||
<form method="post" action="/op/dashboard/packs">
|
||||
<button class="primaryButton" type="submit">서버팩 추가</button>
|
||||
</form>
|
||||
<form method="post" action="/op/dashboard/packs/delete" class="deleteForm">
|
||||
<button class="dangerButton" type="submit">선택 삭제</button>
|
||||
<form method="post" action="/op/dashboard/packs/delete" class="deleteForm" data-delete-form>
|
||||
<div class="deleteToolbar">
|
||||
<button class="dangerButton" type="button" data-delete-toggle>서버팩 삭제</button>
|
||||
<div class="deleteActions hidden" data-delete-actions>
|
||||
<button class="ghostButton" type="button" data-delete-cancel>취소</button>
|
||||
<button class="dangerButton" type="submit">확인</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cardRow">
|
||||
<% packs.forEach((pack) => { %>
|
||||
<label class="packCard selectableCard">
|
||||
<input type="checkbox" name="packKeys" value="<%= pack.file %>" />
|
||||
<input class="selectionBox hidden" type="checkbox" name="packKeys" value="<%= pack.file %>" />
|
||||
<span class="selectionTitle"><%= pack.name %></span>
|
||||
<span><code><%= pack.file %></code></span>
|
||||
<% if (!pack.registered) { %>
|
||||
<span class="warningBadge">manifest.json 미등록</span>
|
||||
<% } %>
|
||||
<a class="ghostLink" href="/op/dashboard/<%= pack.file %>">편집</a>
|
||||
</label>
|
||||
<% }) %>
|
||||
@@ -29,5 +38,33 @@
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
<script>
|
||||
(() => {
|
||||
const form = document.querySelector('[data-delete-form]')
|
||||
if (form == null) {
|
||||
return
|
||||
}
|
||||
|
||||
const toggleButton = form.querySelector('[data-delete-toggle]')
|
||||
const cancelButton = form.querySelector('[data-delete-cancel]')
|
||||
const actions = form.querySelector('[data-delete-actions]')
|
||||
const checkboxes = form.querySelectorAll('.selectionBox')
|
||||
|
||||
const setDeleteMode = (enabled) => {
|
||||
form.classList.toggle('deleteMode', enabled)
|
||||
actions.classList.toggle('hidden', !enabled)
|
||||
checkboxes.forEach((checkbox) => {
|
||||
checkbox.classList.toggle('hidden', !enabled)
|
||||
if (!enabled) {
|
||||
checkbox.checked = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
toggleButton?.addEventListener('click', () => setDeleteMode(true))
|
||||
cancelButton?.addEventListener('click', () => setDeleteMode(false))
|
||||
setDeleteMode(false)
|
||||
})()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user