Align dashboard and installer flows with spec
This commit is contained in:
@@ -44,7 +44,7 @@ function renderPackList(packs) {
|
|||||||
const label = document.createElement('label')
|
const label = document.createElement('label')
|
||||||
label.className = 'packOption'
|
label.className = 'packOption'
|
||||||
label.innerHTML = `
|
label.innerHTML = `
|
||||||
<input type="radio" name="packChoice" value="${pack.file}" />
|
<input type="radio" name="packChoice" value="${pack.file}" data-pack-name="${pack.name}" />
|
||||||
<div>
|
<div>
|
||||||
<strong>${pack.name}</strong>
|
<strong>${pack.name}</strong>
|
||||||
<span>${pack.file}</span>
|
<span>${pack.file}</span>
|
||||||
@@ -52,15 +52,6 @@ function renderPackList(packs) {
|
|||||||
`
|
`
|
||||||
packList.appendChild(label)
|
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() {
|
async function bootstrap() {
|
||||||
@@ -71,6 +62,18 @@ async function bootstrap() {
|
|||||||
|
|
||||||
window.installerApi.onLog(appendLog)
|
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) => {
|
document.querySelectorAll('[data-back]').forEach((button) => {
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener('click', () => {
|
||||||
setActiveStep(button.dataset.back)
|
setActiveStep(button.dataset.back)
|
||||||
|
|||||||
@@ -191,21 +191,60 @@ button {
|
|||||||
gap: 18px;
|
gap: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.deleteToolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteActions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.selectableCard {
|
.selectableCard {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selectableCard input[type="checkbox"] {
|
.selectionBox {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 18px;
|
top: 18px;
|
||||||
right: 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 {
|
.selectionTitle {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 700;
|
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 {
|
.adminLoginBody {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { fetchReleaseVersions } from '../../shared/mojang'
|
|||||||
import {
|
import {
|
||||||
createNewPack,
|
createNewPack,
|
||||||
deletePacks,
|
deletePacks,
|
||||||
|
listDashboardPacks,
|
||||||
loadAccounts,
|
loadAccounts,
|
||||||
loadPackDefinition,
|
loadPackDefinition,
|
||||||
loadRootManifest,
|
loadRootManifest,
|
||||||
@@ -59,10 +60,10 @@ opRouter.post('/op/logout', requireAuth, (req, res) => {
|
|||||||
|
|
||||||
opRouter.get('/op/dashboard', requireAuth, async (_req, res, next) => {
|
opRouter.get('/op/dashboard', requireAuth, async (_req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const manifest = await loadRootManifest()
|
const packs = await listDashboardPacks()
|
||||||
res.render('op/dashboard', {
|
res.render('op/dashboard', {
|
||||||
userId: _req.session.userId,
|
userId: _req.session.userId,
|
||||||
packs: manifest.packs
|
packs
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error)
|
next(error)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import fs from 'node:fs'
|
|||||||
import fsp from 'node:fs/promises'
|
import fsp from 'node:fs/promises'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import { accountPath, fileDir, manifestDir, manifestRootPath } from './paths'
|
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 = {
|
const defaultRootManifest: RootManifest = {
|
||||||
packs: [
|
packs: [
|
||||||
@@ -93,6 +93,22 @@ export async function listManifestFiles(): Promise<string[]> {
|
|||||||
.sort((left, right) => left.localeCompare(right))
|
.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> {
|
export async function loadPackDefinition(packKey: string): Promise<PackDefinition | null> {
|
||||||
await ensureProjectFiles()
|
await ensureProjectFiles()
|
||||||
const safeKey = sanitizePackKey(packKey)
|
const safeKey = sanitizePackKey(packKey)
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ export interface PackListEntry {
|
|||||||
file: string
|
file: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DashboardPackEntry extends PackListEntry {
|
||||||
|
registered: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface RootManifest {
|
export interface RootManifest {
|
||||||
packs: PackListEntry[]
|
packs: PackListEntry[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,14 +14,23 @@
|
|||||||
<form method="post" action="/op/dashboard/packs">
|
<form method="post" action="/op/dashboard/packs">
|
||||||
<button class="primaryButton" type="submit">서버팩 추가</button>
|
<button class="primaryButton" type="submit">서버팩 추가</button>
|
||||||
</form>
|
</form>
|
||||||
<form method="post" action="/op/dashboard/packs/delete" class="deleteForm">
|
<form method="post" action="/op/dashboard/packs/delete" class="deleteForm" data-delete-form>
|
||||||
<button class="dangerButton" type="submit">선택 삭제</button>
|
<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">
|
<div class="cardRow">
|
||||||
<% packs.forEach((pack) => { %>
|
<% packs.forEach((pack) => { %>
|
||||||
<label class="packCard selectableCard">
|
<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 class="selectionTitle"><%= pack.name %></span>
|
||||||
<span><code><%= pack.file %></code></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>
|
<a class="ghostLink" href="/op/dashboard/<%= pack.file %>">편집</a>
|
||||||
</label>
|
</label>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
@@ -29,5 +38,33 @@
|
|||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user