Tags: Add unused tags pruning to tags management
| @@ -393,6 +393,10 @@ function createTagMapFromList(listElement, key) { | ||
| 393 | 393 | * @returns {Tag[]} A list of tags |
| 394 | 394 | */ |
| 395 | 395 | function getTagsList(key, sort = true) { |
| 396 | + if (key === null || key === undefined) { | |
| 397 | + return []; | |
| 398 | + } | |
| 399 | + | |
| 396 | 400 | if (!Array.isArray(tag_map[key])) { |
| 397 | 401 | tag_map[key] = []; |
| 398 | 402 | return []; |
| @@ -1547,6 +1551,44 @@ function onTagsBackupClick() { | ||
| 1547 | 1551 | download(blob, filename, 'application/json'); |
| 1548 | 1552 | } |
| 1549 | 1553 | |
| 1554 | +async function onTagsPruneClick() { | |
| 1555 | + // Get tags which have zero tag map entries | |
| 1556 | + const allTagsInTagMaps = new Set(Object.values(tag_map).flat()); | |
| 1557 | + const tagsToPrune = tags.filter(tag => !allTagsInTagMaps.has(tag.id)); | |
| 1558 | + | |
| 1559 | + // Get tag maps referring to deleted entities | |
| 1560 | + const allEntityKeys = new Set([...characters.map(c => String(c.avatar)), ...groups.map(g => String(g.id))]); | |
| 1561 | + const tagMapsToPrune = Object.keys(tag_map).filter(key => !allEntityKeys.has(key)); | |
| 1562 | + | |
| 1563 | + if (!tagsToPrune.length && !tagMapsToPrune.length) { | |
| 1564 | + toastr.info(t`No unused tags or references found.`); | |
| 1565 | + return; | |
| 1566 | + } | |
| 1567 | + | |
| 1568 | + const confirm = await Popup.show.confirm(t`Prune ${tagsToPrune.length} tags and ${tagMapsToPrune.length} references`, t`Are you sure you want to remove all unused tags and references to missing or deleted characters and groups?`); | |
| 1569 | + | |
| 1570 | + if (!confirm) { | |
| 1571 | + return; | |
| 1572 | + } | |
| 1573 | + | |
| 1574 | + for (const tag of tagsToPrune) { | |
| 1575 | + tags.splice(tags.indexOf(tag), 1); | |
| 1576 | + } | |
| 1577 | + | |
| 1578 | + for (const key of tagMapsToPrune) { | |
| 1579 | + delete tag_map[key]; | |
| 1580 | + } | |
| 1581 | + | |
| 1582 | + printCharactersDebounced(); | |
| 1583 | + saveSettingsDebounced(); | |
| 1584 | + | |
| 1585 | + // Reprint the tag management popup, without having it to be opened again | |
| 1586 | + const tagContainer = $('#tag_view_list .tag_view_list_tags'); | |
| 1587 | + printViewTagList(tagContainer); | |
| 1588 | + | |
| 1589 | + toastr.success(t`Unused tags pruned successfully.`); | |
| 1590 | +} | |
| 1591 | + | |
| 1550 | 1592 | function onTagCreateClick() { |
| 1551 | 1593 | const tagName = getFreeName('New Tag', tags.map(x => x.name)); |
| 1552 | 1594 | const tag = createNewTag(tagName); |
| @@ -2198,6 +2240,7 @@ export function initTags() { | ||
| 2198 | 2240 | $(document).on('click', '.tag_view_create', onTagCreateClick); |
| 2199 | 2241 | $(document).on('click', '.tag_view_backup', onTagsBackupClick); |
| 2200 | 2242 | $(document).on('click', '.tag_view_restore', onBackupRestoreClick); |
| 2243 | + $(document).on('click', '.tag_view_prune', onTagsPruneClick); | |
| 2201 | 2244 | eventSource.on(event_types.CHARACTER_DUPLICATED, copyTags); |
| 2202 | 2245 | eventSource.makeFirst(event_types.CHAT_CHANGED, () => selected_group ? applyTagsOnGroupSelect() : applyTagsOnCharacterSelect()); |
| 2203 | 2246 | |
| @@ -1,6 +1,10 @@ | ||
| 1 | 1 | <div class="title_restorable alignItemsBaseline"> |
| 2 | 2 | <h3 data-i18n="Tag Management">Tag Management</h3> |
| 3 | 3 | <div class="flex-container alignItemsBaseline"> |
| 4 | + <div class="menu_button menu_button_icon tag_view_prune" data-i18n="[title]Remove unused tags" title="Remove unused tags"> | |
| 5 | + <i class="fa-solid fa-scissors"></i> | |
| 6 | + <span data-i18n="Prune">Prune</span> | |
| 7 | + </div> | |
| 4 | 8 | <div class="menu_button menu_button_icon tag_view_backup" data-i18n="[title]Save your tags to a file" title="Save your tags to a file"> |
| 5 | 9 | <i class="fa-solid fa-file-export"></i> |
| 6 | 10 | <span data-i18n="Backup">Backup</span> |