added ability to enable all gifts in one go

This commit is contained in:
onysd 2026-07-22 00:50:00 +03:00
parent 31b323e36a
commit 9844d78c5b
7 changed files with 122 additions and 15 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>telesrv admin</title>
<script type="module" crossorigin src="/assets/index-DzNuxLKt.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DHdrFM5j.css">
<script type="module" crossorigin src="/assets/index-ybbM_ULl.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BVA68zgn.css">
</head>
<body>
<div id="root"></div>

View file

@ -304,6 +304,12 @@ const translations: Record<Language, Record<string, string>> = {
"gifts.replace": "New revision",
"gifts.disable": "Disable",
"gifts.enable": "Enable",
"gifts.bulkSelected": "{count} selected",
"gifts.bulkSelectAll": "Select all visible gifts",
"gifts.bulkSelectOne": "Select gift {id}",
"gifts.bulkEnable": "Enable selected",
"gifts.bulkDisable": "Disable selected",
"gifts.bulkStatusFailed": "{failed} of {total} failed",
"gifts.empty": "No Star Gifts have been imported.",
"gifts.emptyHint": "Import the first animation above to build the gift catalog.",
"gifts.validationReady": "Validation passed",
@ -675,6 +681,12 @@ const translations: Record<Language, Record<string, string>> = {
"gifts.replace": "创建新版本",
"gifts.disable": "停用",
"gifts.enable": "启用",
"gifts.bulkSelected": "已选择 {count} 个",
"gifts.bulkSelectAll": "选择所有可见礼物",
"gifts.bulkSelectOne": "选择礼物 {id}",
"gifts.bulkEnable": "启用所选",
"gifts.bulkDisable": "停用所选",
"gifts.bulkStatusFailed": "{total} 个中有 {failed} 个失败",
"gifts.empty": "尚未导入星星礼物。",
"gifts.emptyHint": "从上方导入第一个动画,开始搭建礼物目录。",
"gifts.validationReady": "校验已通过",
@ -1046,6 +1058,12 @@ const translations: Record<Language, Record<string, string>> = {
"gifts.replace": "Новая версия",
"gifts.disable": "Отключить",
"gifts.enable": "Включить",
"gifts.bulkSelected": "Выбрано: {count}",
"gifts.bulkSelectAll": "Выбрать все видимые подарки",
"gifts.bulkSelectOne": "Выбрать подарок {id}",
"gifts.bulkEnable": "Включить выбранные",
"gifts.bulkDisable": "Отключить выбранные",
"gifts.bulkStatusFailed": "{failed} из {total} не выполнено",
"gifts.empty": "Звездные подарки еще не импортированы.",
"gifts.emptyHint": "Импортируйте первую анимацию, чтобы начать наполнение каталога.",
"gifts.validationReady": "Проверка пройдена",

View file

@ -107,6 +107,10 @@ export function GiftsPage() {
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const [importError, setImportError] = useState("");
const [selected, setSelected] = useState<Set<string>>(new Set());
const [bulkReason, setBulkReason] = useState("");
const [bulkBusy, setBulkBusy] = useState(false);
const [bulkError, setBulkError] = useState("");
async function load() {
setError("");
@ -152,6 +156,61 @@ export function GiftsPage() {
);
}, [gifts, query]);
const allVisibleSelected = visibleGifts.length > 0 && visibleGifts.every((gift) => selected.has(gift.GiftID));
function toggleSelected(giftID: string) {
setSelected((prev) => {
const next = new Set(prev);
if (next.has(giftID)) next.delete(giftID);
else next.add(giftID);
return next;
});
}
function toggleSelectAllVisible() {
setSelected((prev) => {
if (allVisibleSelected) {
const next = new Set(prev);
for (const gift of visibleGifts) next.delete(gift.GiftID);
return next;
}
const next = new Set(prev);
for (const gift of visibleGifts) next.add(gift.GiftID);
return next;
});
}
async function bulkSetEnabled(nextEnabled: boolean) {
if (!bulkReason.trim()) {
setBulkError(t("action.reasonRequired"));
return;
}
setBulkBusy(true);
setBulkError("");
const ids = Array.from(selected);
let failed = 0;
for (const id of ids) {
try {
await api.action("/api/actions/set-gift-enabled", {
gift_id: id,
enabled: nextEnabled,
reason: bulkReason.trim(),
confirm: true
});
} catch {
failed++;
}
}
setBulkBusy(false);
if (failed > 0) {
setBulkError(t("gifts.bulkStatusFailed", { failed, total: ids.length }));
} else {
setSelected(new Set());
setBulkReason("");
}
await load();
}
function uploadForm(confirm: boolean, commandID = "") {
if (!file) throw new Error(t("gifts.fileRequired"));
if (!reason.trim()) throw new Error(t("action.reasonRequired"));
@ -249,12 +308,25 @@ export function GiftsPage() {
<span className="gift-list-summary">{t("gifts.listSummary", { shown: visibleGifts.length, total: gifts.length })}</span>
</div>
</QueryPanel>
{selected.size > 0 && <div className="gift-bulk-toolbar">
<span className="gift-bulk-count">{t("gifts.bulkSelected", { count: selected.size })}</span>
<label className="gift-reason-field gift-bulk-reason"><span>{t("gifts.reason")}</span><input value={bulkReason} placeholder={t("gifts.reasonPlaceholder")} onChange={(e) => setBulkReason(e.target.value)} /></label>
<button className="btn" type="button" onClick={() => bulkSetEnabled(true)} disabled={bulkBusy}>
{bulkBusy ? <Loader2 className="spin" size={14} /> : <CheckCircle2 size={14} />} {t("gifts.bulkEnable")}
</button>
<button className="btn" type="button" onClick={() => bulkSetEnabled(false)} disabled={bulkBusy}>
{bulkBusy ? <Loader2 className="spin" size={14} /> : <Pause size={14} />} {t("gifts.bulkDisable")}
</button>
<button className="btn" type="button" onClick={() => { setSelected(new Set()); setBulkError(""); }} disabled={bulkBusy}>{t("common.close")}</button>
{bulkError && <span className="gift-bulk-error">{bulkError}</span>}
</div>}
<div className="table-wrap gift-table-wrap">
<table className="data-table gift-table">
<thead><tr><th>{t("gifts.animation")}</th><th>{t("gifts.idRevision")}</th><th>{t("gifts.title")}</th><th>{t("gifts.price")}</th><th>{t("gifts.source")}</th><th>{t("gifts.received")}</th><th>{t("common.status")}</th><th>{t("common.updatedAt")}</th><th>{t("common.actions")}</th></tr></thead>
<thead><tr><th className="gift-select-col"><input type="checkbox" checked={allVisibleSelected} onChange={toggleSelectAllVisible} aria-label={t("gifts.bulkSelectAll")} /></th><th>{t("gifts.animation")}</th><th>{t("gifts.idRevision")}</th><th>{t("gifts.title")}</th><th>{t("gifts.price")}</th><th>{t("gifts.source")}</th><th>{t("gifts.received")}</th><th>{t("common.status")}</th><th>{t("common.updatedAt")}</th><th>{t("common.actions")}</th></tr></thead>
<tbody>
{visibleGifts.map((gift) => (
<tr className={gift.Enabled ? "" : "gift-row-disabled"} key={gift.GiftID}>
<td className="gift-select-col"><input type="checkbox" checked={selected.has(gift.GiftID)} onChange={() => toggleSelected(gift.GiftID)} aria-label={t("gifts.bulkSelectOne", { id: gift.GiftID })} /></td>
<td><LottiePreview giftID={gift.GiftID} revision={gift.Revision} compact /></td>
<td className="mono">{gift.GiftID} / {gift.Revision}</td>
<td><strong className="gift-table-title">{gift.Title || `Gift #${gift.GiftID}`}</strong><span className="gift-sort-order">{t("gifts.sortOrder")}: {gift.SortOrder}</span></td>
@ -266,7 +338,7 @@ export function GiftsPage() {
<td><div className="gift-table-actions"><button className="btn compact-btn collectible-button" type="button" onClick={() => setCollectibleGift(gift)}><Gem size={13} />{t("collectibles.manage")}</button><button className="btn compact-btn" type="button" onClick={() => startRevision(gift)}>{t("gifts.replace")}</button><ActionButton compact tone="neutral" label={gift.Enabled ? t("gifts.disable") : t("gifts.enable")} path="/api/actions/set-gift-enabled" payload={() => ({ gift_id: gift.GiftID, enabled: !gift.Enabled })} onDone={() => void load()} /></div></td>
</tr>
))}
{visibleGifts.length === 0 && <EmptyRow colSpan={9} />}
{visibleGifts.length === 0 && <EmptyRow colSpan={10} />}
</tbody>
</table>
</div>

View file

@ -408,8 +408,25 @@
.gift-table-wrap { background: #ffffff; }
.gift-table { min-width: 1080px; }
.gift-table th:first-child { width: 74px; }
.gift-table th:nth-child(2) { width: 74px; }
.gift-table td { vertical-align: middle; }
.gift-select-col { width: 34px; text-align: center; }
.gift-select-col input { width: 15px; height: 15px; }
.gift-bulk-toolbar {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 12px;
margin-bottom: 10px;
background: #f3f8f7;
border: 1px solid var(--line);
border-radius: 9px;
}
.gift-bulk-count { color: var(--text); font-size: 12px; font-weight: 700; white-space: nowrap; }
.gift-bulk-reason { flex: 1; min-width: 160px; }
.gift-bulk-reason input { height: 34px; }
.gift-bulk-error { color: #b42318; font-size: 11px; font-weight: 700; }
.gift-animation-shell.compact { width: 56px; min-height: 56px; overflow: hidden; border: 1px solid var(--line); border-radius: 9px; }
.gift-animation-shell.compact .gift-animation { width: 54px; height: 54px; }
.gift-animation-shell.compact .gift-play { right: 3px; bottom: 3px; width: 20px; height: 20px; }