WIP: Rework import tags popup for more options

- Rework "import tags" dialog, providing options which tags to import, and rendering the tags there, for manual management
- Refactor tag list function to allow custom remove actions
- Refactor functions to allow adding of multiple tags at once
This commit is contained in:
Wolfsblvt
2024-05-26 20:29:50 +02:00
parent 4f2543f7ae
commit 35e21c3568
2 changed files with 227 additions and 76 deletions

View File

@@ -292,6 +292,14 @@
flex-wrap: nowrap; flex-wrap: nowrap;
} }
.inline-flex {
display: inline-flex;
}
.inline-block {
display: inline-block;
}
.alignitemscenter, .alignitemscenter,
.alignItemsCenter { .alignItemsCenter {
align-items: center; align-items: center;
@@ -348,6 +356,10 @@
margin-right: 5px; margin-right: 5px;
} }
.margin-r2 {
margin-right: 2px;
}
.flex0 { .flex0 {
flex: 0; flex: 0;
} }

View File

@@ -4,7 +4,6 @@ import {
this_chid, this_chid,
callPopup, callPopup,
menu_type, menu_type,
getCharacters,
entitiesFilter, entitiesFilter,
printCharactersDebounced, printCharactersDebounced,
buildAvatarList, buildAvatarList,
@@ -15,12 +14,13 @@ import {
import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js'; import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js';
import { groupCandidatesFilter, groups, select_group_chats, selected_group } from './group-chats.js'; import { groupCandidatesFilter, groups, select_group_chats, selected_group } from './group-chats.js';
import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight, equalsIgnoreCaseAndAccents, includesIgnoreCaseAndAccents } from './utils.js'; import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight, equalsIgnoreCaseAndAccents, includesIgnoreCaseAndAccents, removeFromArray } from './utils.js';
import { power_user } from './power-user.js'; import { power_user } from './power-user.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { SlashCommand } from './slash-commands/SlashCommand.js'; import { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
import { isMobile } from './RossAscends-mods.js'; import { isMobile } from './RossAscends-mods.js';
import { POPUP_TYPE, callGenericPopup } from './popup.js';
export { export {
TAG_FOLDER_TYPES, TAG_FOLDER_TYPES,
@@ -45,6 +45,8 @@ export {
removeTagFromMap, removeTagFromMap,
}; };
/** @typedef {import('../script.js').Character} Character */
const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter'; const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter';
const GROUP_FILTER_SELECTOR = '#rm_group_chats_block .rm_tag_filter'; const GROUP_FILTER_SELECTOR = '#rm_group_chats_block .rm_tag_filter';
const TAG_TEMPLATE = $('#tag_template .tag'); const TAG_TEMPLATE = $('#tag_template .tag');
@@ -467,29 +469,34 @@ export function getTagKeyForEntityElement(element) {
} }
/** /**
* Adds a tag to a given entity * Adds one or more tags to a given entity
* @param {Tag} tag - The tag to add *
* @param {string|string[]} entityId - The entity to add this tag to. Has to be the entity key (e.g. `addTagToEntity`). (Also allows multiple entities to be passed in) * @param {Tag|Tag[]} tag - The tag or tags to add
* @param {string|string[]} entityId - The entity or entities to add this tag to. Has to be the entity key (e.g. `addTagToEntity`).
* @param {object} [options={}] - Optional arguments * @param {object} [options={}] - Optional arguments
* @param {JQuery<HTMLElement>|string?} [options.tagListSelector=null] - An optional selector if a specific list should be updated with the new tag too (for example because the add was triggered for that function) * @param {JQuery<HTMLElement>|string?} [options.tagListSelector=null] - An optional selector if a specific list should be updated with the new tag too (for example because the add was triggered for that function)
* @param {PrintTagListOptions} [options.tagListOptions] - Optional parameters for printing the tag list. Can be set to be consistent with the expected behavior of tags in the list that was defined before. * @param {PrintTagListOptions} [options.tagListOptions] - Optional parameters for printing the tag list. Can be set to be consistent with the expected behavior of tags in the list that was defined before.
* @returns {boolean} Whether at least one tag was added * @returns {boolean} Whether at least one tag was added
*/ */
export function addTagToEntity(tag, entityId, { tagListSelector = null, tagListOptions = {} } = {}) { export function addTagsToEntity(tag, entityId, { tagListSelector = null, tagListOptions = {} } = {}) {
const tags = Array.isArray(tag) ? tag : [tag];
const entityIds = Array.isArray(entityId) ? entityId : [entityId];
let result = false; let result = false;
// Add tags to the map // Add tags to the map
if (Array.isArray(entityId)) { entityIds.forEach((id) => {
entityId.forEach((id) => result = addTagToMap(tag.id, id) || result); tags.forEach((tag) => {
} else { result = addTagToMap(tag.id, id) || result;
result = addTagToMap(tag.id, entityId); });
} });
// Save and redraw // Save and redraw
printCharactersDebounced(); printCharactersDebounced();
saveSettingsDebounced(); saveSettingsDebounced();
// We should manually add the selected tag to the print tag function, so we cover places where the tag list did not automatically include it // We should manually add the selected tag to the print tag function, so we cover places where the tag list did not automatically include it
tagListOptions.addTag = tag; tagListOptions.addTag = tags;
// add tag to the UI and internal map - we reprint so sorting and new markup is done correctly // add tag to the UI and internal map - we reprint so sorting and new markup is done correctly
if (tagListSelector) printTagList(tagListSelector, tagListOptions); if (tagListSelector) printTagList(tagListSelector, tagListOptions);
@@ -625,7 +632,7 @@ function selectTag(event, ui, listSelector, { tagListOptions = {} } = {}) {
const characterData = event.target.closest('#bulk_tags_div')?.dataset.characters; const characterData = event.target.closest('#bulk_tags_div')?.dataset.characters;
const characterIds = characterData ? JSON.parse(characterData).characterIds : null; const characterIds = characterData ? JSON.parse(characterData).characterIds : null;
addTagToEntity(tag, characterIds, { tagListSelector: listSelector, tagListOptions: tagListOptions }); addTagsToEntity(tag, characterIds, { tagListSelector: listSelector, tagListOptions: tagListOptions });
// need to return false to keep the input clear // need to return false to keep the input clear
return false; return false;
@@ -634,75 +641,173 @@ function selectTag(event, ui, listSelector, { tagListOptions = {} } = {}) {
/** /**
* Get a list of existing tags matching a list of provided new tag names * Get a list of existing tags matching a list of provided new tag names
* *
* @param {string[]} new_tags - A list of strings representing tag names * @param {string[]} newTags - A list of strings representing tag names
* @returns List of existing tags * @returns {Tag[]} List of existing tags
*/ */
function getExistingTags(new_tags) { function getExistingTags(newTags) {
let existing_tags = []; let existingTags = [];
for (let tagName of new_tags) { for (let tagName of newTags) {
let foundTag = getTag(tagName); let foundTag = getTag(tagName);
if (foundTag) { if (foundTag) {
existing_tags.push(foundTag.name); existingTags.push(foundTag);
} }
} }
return existing_tags; return existingTags;
} }
async function importTags(imported_char) { const tagImportSettings = {
let imported_tags = imported_char.tags.filter(t => t !== 'ROOT' && t !== 'TAVERN'); ALWAYS_IMPORT_ALL: 1,
let existingTags = await getExistingTags(imported_tags); ONLY_IMPORT_EXISTING: 2,
//make this case insensitive IMPORT_NONE: 3,
let newTags = imported_tags.filter(t => !existingTags.some(existingTag => existingTag.toLowerCase() === t.toLowerCase())); ASK: 4
let selected_tags = ''; };
const existingTagsString = existingTags.length ? (': ' + existingTags.join(', ')) : '';
if (newTags.length === 0) {
await callPopup(`<h3>Importing Tags For ${imported_char.name}</h3><p>${existingTags.length} existing tags have been found${existingTagsString}.</p>`, 'text');
} else {
selected_tags = await callPopup(`<h3>Importing Tags For ${imported_char.name}</h3><p>${existingTags.length} existing tags have been found${existingTagsString}.</p><p>The following ${newTags.length} new tags will be imported.</p>`, 'input', newTags.join(', '));
}
// @ts-ignore
selected_tags = existingTags.concat(selected_tags.split(','));
// @ts-ignore
selected_tags = selected_tags.map(t => t.trim()).filter(t => t !== '');
//Anti-troll measure
if (selected_tags.length > 15) {
selected_tags = selected_tags.slice(0, 15);
}
for (let tagName of selected_tags) {
let tag = getTag(tagName);
if (!tag) { let globalTagImportSetting = tagImportSettings.ASK; // Default setting
tag = createNewTag(tagName);
}
if (!tag_map[imported_char.avatar].includes(tag.id)) { const IMPORT_EXLCUDED_TAGS = ['ROOT', 'TAVERN'];
tag_map[imported_char.avatar].push(tag.id); const ANTI_TROLL_MAX_TAGS = 15;
console.debug('added tag to map', tag, imported_char.name);
} /**
* Imports tags for a given character
*
* @param {Character} character - The character
* @returns {Promise<boolean>} Boolean indicating whether any tag was imported
*/
async function importTags(character) {
// Gather the tags to import based on the selected setting
const tagNamesToImport = await handleTagImport(character);
if (!tagNamesToImport?.length) {
toastr.info('No tags imported', 'Importing Tags');
return;
} }
saveSettingsDebounced(); const tagsToImport = tagNamesToImport.map(tag => getTag(tag, { createNew: true }));
const added = addTagsToEntity(tagsToImport, character.avatar);
// Await the character list, which will automatically reprint it and all tag filters toastr.success(`Imported tags:<br />${tagsToImport.map(x => x.name).join(', ')}`, 'Importing Tags', { escapeHtml: false });
await getCharacters();
// need to return false to keep the input clear return added;
return false; }
/**
* Handles the import of tags for a given character and returns the resulting list of tags to add
*
* @param {Character} character - The character
* @returns {Promise<string[]>} Array of strings representing the tags to import
*/
async function handleTagImport(character) {
/** @type {string[]} */
const importTags = character.tags.map(t => t.trim()).filter(t => t)
.filter(t => !IMPORT_EXLCUDED_TAGS.includes(t))
.slice(0, ANTI_TROLL_MAX_TAGS);
const existingTags = getExistingTags(importTags);
const newTags = importTags.filter(t => !existingTags.some(existingTag => existingTag.name.toLowerCase() === t.toLowerCase()))
.map(newTag);
switch (globalTagImportSetting) {
case tagImportSettings.ALWAYS_IMPORT_ALL:
return existingTags.concat(newTags).map(t => t.name);
case tagImportSettings.ONLY_IMPORT_EXISTING:
return existingTags.map(t => t.name);
case tagImportSettings.ASK:
return await showTagImportPopup(character, existingTags, newTags);
case tagImportSettings.IMPORT_NONE:
default:
return [];
}
}
/**
* Shows a popup to import tags for a given character and returns the resulting list of tags to add
*
* @param {Character} character - The character
* @param {Tag[]} existingTags - List of existing tags
* @param {Tag[]} newTags - List of new tags
* @returns {Promise<string[]>} Array of strings representing the tags to import
*/
async function showTagImportPopup(character, existingTags, newTags) {
/** @type {{[key: string]: import('./popup.js').CustomPopupButton}} */
const importButtons = {
EXISTING: { result: 2, text: 'Import Existing' },
ALL: { result: 3, text: 'Import All' },
NONE: { result: 4, text: 'Import None' },
}
const customButtonsCaptions = Object.values(importButtons).map(button => `&quot;${button.text}&quot;`);
const customButtonsString = customButtonsCaptions.slice(0, -1).join(', ') + ' or ' + customButtonsCaptions.slice(-1);
const popupContent = $(`
<h3>Import Tags For ${character.name}</h3>
<div class="import_avatar_placeholder"></div>
<div class="import_tags_content justifyLeft">
<small>
Click remove on any tag to remove it from this import.<br />
Select one of the import options to finish importing the tags.
</small>
<h4 class="m-t-1">Existing Tags</h4>
<div id="import_existing_tags_list" class="tags"></div>
<h4 class="m-t-1">New Tags</h4>
<div id="import_new_tags_list" class="tags"></div>
<small>
<label class="checkbox flex-container alignitemscenter flexNoGap m-t-3" for="import_remember_option">
<input type="checkbox" id="import_remember_option" name="import_remember_option" />
<span data-i18n="Remember my choice">
Remember my choice
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Remember the chosen import option\nIf ${customButtonsString} is selected, this dialog will not show up anymore.\nTo change this, go to the settings and modify &quot;Tag Import Option&quot;.\n\nIf the &quot;Import&quot; option is chosen, the global setting will stay on &quot;Ask&quot;."
title="Remember the chosen import option\nIf ${customButtonsString} is selected, this dialog will not show up anymore.\nTo change this, go to the settings and modify &quot;Tag Import Option&quot;.\n\nIf the &quot;Import&quot; option is chosen, the global setting will stay on &quot;Ask&quot;.">
</div>
</span>
</label>
</small>
</div>`);
// Print tags after popup is shown, so that events can be added
printTagList(popupContent.find('#import_existing_tags_list'), { tags: existingTags, tagOptions: { removable: true, removeAction: tag => removeFromArray(existingTags, tag) } });
printTagList(popupContent.find('#import_new_tags_list'), { tags: newTags, tagOptions: { removable: true, removeAction: tag => removeFromArray(newTags, tag) } });
const result = await callGenericPopup(popupContent, POPUP_TYPE.TEXT, null, { wider: true, okButton: 'Import', cancelButton: true, customButtons: Object.values(importButtons) });
if (!result) {
return [];
}
switch (result) {
case 1:
case true:
case importButtons.ALL.result: // Default 'Import' option where it imports all selected
return existingTags.concat(newTags).map(t => t.name);
case importButtons.EXISTING.result:
return existingTags.map(t => t.name);
case importButtons.NONE.result:
default:
return [];
}
} }
/** /**
* Gets a tag from the tags array based on the provided tag name (insensitive soft matching) * Gets a tag from the tags array based on the provided tag name (insensitive soft matching)
* Optionally creates the tag if it doesn't exist
* *
* @param {string} tagName - The name of the tag to search for * @param {string} tagName - The name of the tag to search for
* @return {Tag?} The tag object that matches the provided tag name, or undefined if no match is found. * @param {object} [options={}] - Optional parameters
* @param {boolean} [options.createNew=false] - Whether to create the tag if it doesn't exist
* @returns {Tag?} The tag object that matches the provided tag name, or undefined if no match is found
*/ */
function getTag(tagName) { function getTag(tagName, { createNew = false } = {}) {
return tags.find(t => equalsIgnoreCaseAndAccents(t.name, tagName)); let tag = tags.find(t => equalsIgnoreCaseAndAccents(t.name, tagName));
if (!tag && createNew) {
tag = createNewTag(tagName);
}
return tag;
} }
/** /**
* Creates a new tag with default properties and a randomly generated id * Creates a new tag with default properties and a randomly generated id
* *
* Does **not** trigger a save, so it's up to the caller to do that
*
* @param {string} tagName - name of the tag * @param {string} tagName - name of the tag
* @returns {Tag} the newly created tag, or the existing tag if it already exists (with a logged warning) * @returns {Tag} the newly created tag, or the existing tag if it already exists (with a logged warning)
*/ */
@@ -713,7 +818,23 @@ function createNewTag(tagName) {
return existing; return existing;
} }
const tag = { const tag = newTag(tagName);
tags.push(tag);
console.debug('Created new tag', tag.name, 'with id', tag.id);
return tag;
}
/**
* Creates a new tag object with the given tag name and default properties
*
* Not to be confused with `createNewTag`, which actually creates the tag and adds it to the existing list of tags.
* Use this one to create temporary tag objects, for example for drawing.
*
* @param {string} tagName - The name of the tag
* @return {Tag} The newly created tag object
*/
function newTag(tagName) {
return {
id: uuidv4(), id: uuidv4(),
name: tagName, name: tagName,
folder_type: TAG_FOLDER_DEFAULT_TYPE, folder_type: TAG_FOLDER_DEFAULT_TYPE,
@@ -723,9 +844,6 @@ function createNewTag(tagName) {
color2: '', color2: '',
create_date: Date.now(), create_date: Date.now(),
}; };
tags.push(tag);
console.debug('Created new tag', tag.name, 'with id', tag.id);
return tag;
} }
/** /**
@@ -733,6 +851,7 @@ function createNewTag(tagName) {
* @property {boolean} [removable=false] - Whether tags can be removed. * @property {boolean} [removable=false] - Whether tags can be removed.
* @property {boolean} [selectable=false] - Whether tags can be selected. * @property {boolean} [selectable=false] - Whether tags can be selected.
* @property {function} [action=undefined] - Action to perform on tag interaction. * @property {function} [action=undefined] - Action to perform on tag interaction.
* @property {(tag: Tag)=>boolean} [removeAction=undefined] - Action to perform on tag removal instead of the default remove action. If the action returns false, the tag will not be removed.
* @property {boolean} [isGeneralList=false] - If true, indicates that this is the general list of tags. * @property {boolean} [isGeneralList=false] - If true, indicates that this is the general list of tags.
* @property {boolean} [skipExistsCheck=false] - If true, the tag gets added even if a tag with the same id already exists. * @property {boolean} [skipExistsCheck=false] - If true, the tag gets added even if a tag with the same id already exists.
*/ */
@@ -740,7 +859,7 @@ function createNewTag(tagName) {
/** /**
* @typedef {object} PrintTagListOptions - Optional parameters for printing the tag list. * @typedef {object} PrintTagListOptions - Optional parameters for printing the tag list.
* @property {Tag[]|function(): Tag[]} [tags=undefined] - Optional override of tags that should be printed. Those will not be sorted. If no supplied, tags for the relevant character are printed. Can also be a function that returns the tags. * @property {Tag[]|function(): Tag[]} [tags=undefined] - Optional override of tags that should be printed. Those will not be sorted. If no supplied, tags for the relevant character are printed. Can also be a function that returns the tags.
* @property {Tag} [addTag=undefined] - Optionally provide a tag that should be manually added to this print. Either to the overriden tag list or the found tags based on the entity/key. Will respect the tag exists check. * @property {Tag|Tag[]} [addTag=undefined] - Optionally provide one or multiple tags that should be manually added to this print. Either to the overriden tag list or the found tags based on the entity/key. Will respect the tag exists check.
* @property {object|number|string} [forEntityOrKey=undefined] - Optional override for the chosen entity, otherwise the currently selected is chosen. Can be an entity with id property (character, group, tag), or directly an id or tag key. * @property {object|number|string} [forEntityOrKey=undefined] - Optional override for the chosen entity, otherwise the currently selected is chosen. Can be an entity with id property (character, group, tag), or directly an id or tag key.
* @property {boolean|string} [empty=true] - Whether the list should be initially empty. If a string string is provided, 'always' will always empty the list, otherwise it'll evaluate to a boolean. * @property {boolean|string} [empty=true] - Whether the list should be initially empty. If a string string is provided, 'always' will always empty the list, otherwise it'll evaluate to a boolean.
* @property {boolean} [sort=true] - Whether the tags should be sorted via the sort function, or kept as is. * @property {boolean} [sort=true] - Whether the tags should be sorted via the sort function, or kept as is.
@@ -764,8 +883,9 @@ function printTagList(element, { tags = undefined, addTag = undefined, forEntity
$element.empty(); $element.empty();
} }
if (addTag && (tagOptions.skipExistsCheck || !printableTags.some(x => x.id === addTag.id))) { if (addTag) {
printableTags = [...printableTags, addTag]; const addTags = Array.isArray(addTag) ? addTag : [addTag];
printableTags = printableTags.concat(addTags.filter(tag => tagOptions.skipExistsCheck || !printableTags.some(t => t.id === tag.id)));
} }
// one last sort, because we might have modified the tag list or manually retrieved it from a function // one last sort, because we might have modified the tag list or manually retrieved it from a function
@@ -849,7 +969,7 @@ function printTagList(element, { tags = undefined, addTag = undefined, forEntity
* @param {TagOptions} [options={}] - Options for tag behavior * @param {TagOptions} [options={}] - Options for tag behavior
* @returns {void} * @returns {void}
*/ */
function appendTagToList(listElement, tag, { removable = false, selectable = false, action = undefined, isGeneralList = false, skipExistsCheck = false } = {}) { function appendTagToList(listElement, tag, { removable = false, selectable = false, action = undefined, removeAction = undefined, isGeneralList = false, skipExistsCheck = false } = {}) {
if (!listElement) { if (!listElement) {
return; return;
} }
@@ -867,6 +987,13 @@ function appendTagToList(listElement, tag, { removable = false, selectable = fal
tagElement.find('.tag_name').text(tag.name); tagElement.find('.tag_name').text(tag.name);
const removeButton = tagElement.find('.tag_remove'); const removeButton = tagElement.find('.tag_remove');
removable ? removeButton.show() : removeButton.hide(); removable ? removeButton.show() : removeButton.hide();
if (removable && removeAction) {
tagElement.attr('custom-remove-action', String(true));
removeButton.on('click', () => {
const result = removeAction(tag);
if (result !== false) tagElement.remove();
});
}
if (tag.class) { if (tag.class) {
tagElement.addClass(tag.class); tagElement.addClass(tag.class);
@@ -1025,6 +1152,12 @@ function onTagRemoveClick(event) {
const tagElement = $(this).closest('.tag'); const tagElement = $(this).closest('.tag');
const tagId = tagElement.attr('id'); const tagId = tagElement.attr('id');
// If we have a custom remove action, we are not executing anything here in the default handler
if (tagElement.attr('custom-remove-action')) {
console.debug('Custom remove action', tagId);
return;
}
// Check if we are inside the drilldown. If so, we call remove on the bogus folder // Check if we are inside the drilldown. If so, we call remove on the bogus folder
if ($(this).closest('.rm_tag_bogus_drilldown').length > 0) { if ($(this).closest('.rm_tag_bogus_drilldown').length > 0) {
console.debug('Bogus drilldown remove', tagId); console.debug('Bogus drilldown remove', tagId);
@@ -1135,9 +1268,9 @@ function onViewTagsListClick() {
const tagContainer = $('<div class="tag_view_list_tags ui-sortable"></div>'); const tagContainer = $('<div class="tag_view_list_tags ui-sortable"></div>');
html.append(tagContainer); html.append(tagContainer);
callPopup(html, 'text', null, { allowVerticalScrolling: true }); const result = callGenericPopup(html, POPUP_TYPE.TEXT, null, { allowVerticalScrolling: true });
printViewTagList(); printViewTagList(html);
makeTagListDraggable(tagContainer); makeTagListDraggable(tagContainer);
$('#dialogue_popup .tag-color').on('change', (evt) => onTagColorize(evt)); $('#dialogue_popup .tag-color').on('change', (evt) => onTagColorize(evt));
@@ -1443,15 +1576,19 @@ async function onTagDeleteClick() {
const id = $(this).closest('.tag_view_item').attr('id'); const id = $(this).closest('.tag_view_item').attr('id');
const tag = tags.find(x => x.id === id); const tag = tags.find(x => x.id === id);
const otherTags = sortTags(tags.filter(x => x.id !== id).map(x => ({ id: x.id, name: x.name }))); const otherTags = sortTags(tags.filter(x => x.id !== id).map(x => ({ id: x.id, name: x.name })));
const popupText = `
const popupContent = $(`
<h3>Delete Tag</h3> <h3>Delete Tag</h3>
<p>${`Are you sure you want to delete the tag '${tag.name}'?`}</p> <div>Do you want to delete the tag <div id="tag_to_delete" class="tags_inline inline-flex margin-r2"></div>?</div>
<p>If you want to merge all references to this tag into another tag, select it below:</p> <div class="m-t-2 marginBot5">If you want to merge all references to this tag into another tag, select it below:</div>
<select id="merge_tag_select"> <select id="merge_tag_select">
<option value=""> - None - </option> <option value="">--- None ---</option>
${otherTags.map(x => `<option value="${x.id}">${x.name}</option>`).join('')} ${otherTags.map(x => `<option value="${x.id}">${x.name}</option>`).join('')}
</select>`; </select>`);
const result = callPopup(popupText, 'confirm');
appendTagToList(popupContent.find('#tag_to_delete'), tag);
const result = callGenericPopup(popupContent, POPUP_TYPE.CONFIRM);
// Make the select control more fancy on not mobile // Make the select control more fancy on not mobile
if (!isMobile()) { if (!isMobile()) {
@@ -1485,6 +1622,8 @@ async function onTagDeleteClick() {
$(`.tag[id="${id}"]`).remove(); $(`.tag[id="${id}"]`).remove();
$(`.tag_view_item[id="${id}"]`).remove(); $(`.tag_view_item[id="${id}"]`).remove();
toastr.success(`'${tag.name}' deleted${mergeTagId ? ` and merged into '${tags.find(x => x.id === mergeTagId).name}'` : ''}`, 'Delete Tag');
printCharactersDebounced(); printCharactersDebounced();
saveSettingsDebounced(); saveSettingsDebounced();
} }
@@ -1562,8 +1701,8 @@ function copyTags(data) {
tag_map[data.newAvatar] = Array.from(new Set([...prevTagMap, ...newTagMap])); tag_map[data.newAvatar] = Array.from(new Set([...prevTagMap, ...newTagMap]));
} }
function printViewTagList(empty = true) { function printViewTagList(html, empty = true) {
const tagContainer = $('#dialogue_popup .tag_view_list_tags'); const tagContainer = html.find('.tag_view_list_tags');
if (empty) tagContainer.empty(); if (empty) tagContainer.empty();
const everything = Object.values(tag_map).flat(); const everything = Object.values(tag_map).flat();
@@ -1622,7 +1761,7 @@ function registerTagsSlashCommands() {
if (!key) return 'false'; if (!key) return 'false';
const tag = paraGetTag(tagName, { allowCreate: true }); const tag = paraGetTag(tagName, { allowCreate: true });
if (!tag) return 'false'; if (!tag) return 'false';
const result = addTagToEntity(tag, key); const result = addTagsToEntity(tag, key);
return String(result); return String(result);
}, },
namedArgumentList: [ namedArgumentList: [