Align dashboard and installer flows with spec

This commit is contained in:
2026-05-08 19:02:50 +09:00
parent a10ca67210
commit 5ff4e20b5e
6 changed files with 117 additions and 17 deletions

View File

@@ -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)

View File

@@ -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;

View File

@@ -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)

View File

@@ -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)

View File

@@ -3,6 +3,10 @@ export interface PackListEntry {
file: string
}
export interface DashboardPackEntry extends PackListEntry {
registered: boolean
}
export interface RootManifest {
packs: PackListEntry[]
}

View File

@@ -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>