Manage the DeepL account's glossaries from the domain settings
Until now the UI could only reach glossaries recorded in domains.json. A
glossary can also sit on the DeepL account with no domain pointing at it —
created before this app, left over from testing, or because domains.json was
reset, which is the normal state of a fresh deployment volume. Such a
glossary is invisible and unreachable here while still consuming the
account's glossary allowance, so on a small plan one forgotten glossary
blocks every save with no way to clear it from the UI.
Adds three endpoints backed by a new section in the domain settings modal:
GET /api/glossaries what is really on the account, each
annotated with the domain using it
DELETE /api/glossaries/:id remove one, freeing a slot, and drop
any local reference to it
POST /api/glossaries/:id/adopt claim an existing glossary for a
domain, fetching its entries so they
become editable
Adopting is a move, not a copy: if another domain already referenced the
glossary, that slot is released. Two domains pointing at one glossary would
mean editing through either silently replaces what the other still lists.
This does not raise the plan's glossary limit; it makes the limit visible
and the single slot reusable without hand-editing domains.json.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
130
index.html
130
index.html
@@ -444,6 +444,21 @@
|
||||
</div>
|
||||
<div id="glossary-msg-en-de"></div>
|
||||
</div>
|
||||
|
||||
<!-- Every glossary that exists on the DeepL account -->
|
||||
<div class="modal-section">
|
||||
<h3>Glossaries on the DeepL account</h3>
|
||||
<div class="current-config">
|
||||
All glossaries that exist on the DeepL account — including any that no domain
|
||||
above is using. Every one of them counts against your DeepL plan's glossary limit,
|
||||
so deleting an unused glossary frees a slot.
|
||||
</div>
|
||||
<div id="accountGlossaryList"></div>
|
||||
<div class="actions">
|
||||
<button class="secondary small-btn" id="accountGlossaryRefreshBtn">Refresh list</button>
|
||||
</div>
|
||||
<div id="accountGlossary-msg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -544,6 +559,7 @@ document.getElementById('domainSettingsBtn').addEventListener('click', async ()
|
||||
await loadDomains(); // pull the latest shared state before showing (other users may have edited it)
|
||||
populateGlossarySection();
|
||||
domainModalOverlay.classList.add('open');
|
||||
loadAccountGlossaries(); // not awaited: it hits DeepL, so don't delay opening
|
||||
});
|
||||
document.getElementById('domainModalCloseBtn').addEventListener('click', () => domainModalOverlay.classList.remove('open'));
|
||||
domainModalOverlay.addEventListener('click', (e) => { if (e.target === domainModalOverlay) domainModalOverlay.classList.remove('open'); });
|
||||
@@ -622,6 +638,120 @@ GLOSSARY_DIRECTIONS.forEach((dir) => {
|
||||
});
|
||||
});
|
||||
|
||||
// --- Glossaries that exist on the DeepL account -------------------------
|
||||
// The two sections above can only reach glossaries this app has recorded.
|
||||
// Glossaries can also exist on the account without any domain pointing at
|
||||
// them, and they still use up the plan's glossary allowance - on a small
|
||||
// plan one forgotten glossary is enough to block every save. This section
|
||||
// lists what is actually on the account so it can be freed or claimed.
|
||||
|
||||
const accountGlossaryMsgEl = () => document.getElementById('accountGlossary-msg');
|
||||
|
||||
function accountGlossaryRow(g) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'entry-row';
|
||||
|
||||
const info = document.createElement('div');
|
||||
info.style.flex = '1';
|
||||
info.style.minWidth = '0';
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.textContent = g.name;
|
||||
title.style.fontWeight = '600';
|
||||
title.style.fontSize = '13px';
|
||||
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'hint';
|
||||
meta.style.marginTop = '2px';
|
||||
const usedBy = g.tracked
|
||||
? `used by ${g.tracked.domainLabel} (${g.tracked.direction === 'de-en' ? 'DE → EN' : 'EN → DE'})`
|
||||
: 'not used by any domain — free to delete';
|
||||
meta.textContent =
|
||||
`${String(g.source_lang).toUpperCase()} → ${String(g.target_lang).toUpperCase()} · ` +
|
||||
`${g.entry_count} term${g.entry_count === 1 ? '' : 's'} · ${usedBy}`;
|
||||
|
||||
info.appendChild(title);
|
||||
info.appendChild(meta);
|
||||
row.appendChild(info);
|
||||
|
||||
// "Use for <domain>" claims the glossary for the domain currently open in
|
||||
// this modal, which also makes its terms editable in the sections above.
|
||||
const alreadyHere = g.tracked && g.tracked.domain === activeDomain;
|
||||
if (g.direction && !alreadyHere) {
|
||||
const useBtn = document.createElement('button');
|
||||
useBtn.className = 'secondary small-btn';
|
||||
useBtn.textContent = `Use for ${DOMAIN_LABELS[activeDomain] || activeDomain}`;
|
||||
useBtn.addEventListener('click', async () => {
|
||||
const msgEl = accountGlossaryMsgEl();
|
||||
clearMsg(msgEl);
|
||||
useBtn.disabled = true;
|
||||
try {
|
||||
const data = await apiPost(`/api/glossaries/${g.glossary_id}/adopt`, { domain: activeDomain });
|
||||
const movedNote = data.movedFrom ? ` (moved away from ${data.movedFrom.domainLabel})` : '';
|
||||
showMsg(msgEl, `"${g.name}" is now used by ${DOMAIN_LABELS[activeDomain]}${movedNote}.`, 'ok');
|
||||
await loadDomains();
|
||||
populateGlossarySection();
|
||||
await loadAccountGlossaries();
|
||||
} catch (e) {
|
||||
showMsg(msgEl, e.message, 'error');
|
||||
useBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
row.appendChild(useBtn);
|
||||
}
|
||||
|
||||
const delBtn = document.createElement('button');
|
||||
delBtn.className = 'secondary small-btn';
|
||||
delBtn.textContent = 'Delete';
|
||||
delBtn.addEventListener('click', async () => {
|
||||
const msgEl = accountGlossaryMsgEl();
|
||||
clearMsg(msgEl);
|
||||
const warning = g.tracked
|
||||
? `Delete "${g.name}" from DeepL?\n\nIt is currently used by ${g.tracked.domainLabel}, which will be left with no terms for that direction.`
|
||||
: `Delete "${g.name}" from DeepL?`;
|
||||
if (!confirm(`${warning}\n\nThis cannot be undone.`)) return;
|
||||
delBtn.disabled = true;
|
||||
try {
|
||||
const resp = await fetch(`/api/glossaries/${g.glossary_id}`, { method: 'DELETE' });
|
||||
if (!resp.ok) throw new Error(((await resp.json()) || {}).error || `Request failed (${resp.status})`);
|
||||
showMsg(msgEl, `"${g.name}" deleted. A glossary slot is now free.`, 'ok');
|
||||
await loadDomains();
|
||||
populateGlossarySection();
|
||||
await loadAccountGlossaries();
|
||||
} catch (e) {
|
||||
showMsg(msgEl, e.message, 'error');
|
||||
delBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
row.appendChild(delBtn);
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
async function loadAccountGlossaries() {
|
||||
const listEl = document.getElementById('accountGlossaryList');
|
||||
listEl.textContent = 'Loading…';
|
||||
try {
|
||||
const resp = await fetch('/api/glossaries');
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new Error(data.error || `Request failed (${resp.status})`);
|
||||
listEl.innerHTML = '';
|
||||
if (!data.glossaries.length) {
|
||||
listEl.innerHTML = '<div class="hint">No glossaries exist on this DeepL account yet.</div>';
|
||||
return;
|
||||
}
|
||||
data.glossaries.forEach((g) => listEl.appendChild(accountGlossaryRow(g)));
|
||||
} catch (e) {
|
||||
listEl.innerHTML = '';
|
||||
showMsg(accountGlossaryMsgEl(), `Could not list glossaries: ${e.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('accountGlossaryRefreshBtn').addEventListener('click', () => {
|
||||
clearMsg(accountGlossaryMsgEl());
|
||||
loadAccountGlossaries();
|
||||
});
|
||||
|
||||
loadDomains();
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user