Add slash commands for tag management

This commit is contained in:
Wolfsblvt 2024-05-17 00:50:21 +02:00
parent 909ec4191d
commit 96a9f7108c
1 changed files with 271 additions and 38 deletions

View File

@ -17,7 +17,9 @@ import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, Filte
import { groupCandidatesFilter, groups, selected_group } from './group-chats.js';
import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight } from './utils.js';
import { power_user } from './power-user.js';
import { debounce_timeout } from './constants.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
export {
TAG_FOLDER_TYPES,
@ -357,7 +359,7 @@ function createTagMapFromList(listElement, key) {
* If you have an entity, you can get it's key easily via `getTagKeyForEntity(entity)`.
*
* @param {string} key - The key for which to get tags via the tag map
* @param {boolean} [sort=true] -
* @param {boolean} [sort=true] - Whether the tag list should be sorted
* @returns {Tag[]} A list of tags
*/
function getTagsList(key, sort = true) {
@ -463,35 +465,122 @@ export function getTagKeyForEntityElement(element) {
return undefined;
}
/**
* Adds a tag 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 {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 {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
*/
export function addTagToEntity(tag, entityId, { tagListSelector = null, tagListOptions = {} } = {}) {
let result = false;
// Add tags to the map
if (Array.isArray(entityId)) {
entityId.forEach((id) => result = addTagToMap(tag.id, id) || result);
} else {
result = addTagToMap(tag.id, entityId);
}
// Save and redraw
printCharactersDebounced();
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
tagListOptions.addTag = tag;
// add tag to the UI and internal map - we reprint so sorting and new markup is done correctly
if (tagListSelector) printTagList(tagListSelector, tagListOptions);
const inlineSelector = getInlineListSelector();
if (inlineSelector) {
printTagList($(inlineSelector), tagListOptions);
}
return result;
}
/**
* Removes a tag from a given entity
* @param {Tag} tag - The tag to remove
* @param {string|string[]} entityId - The entity to remove this tag from. Has to be the entity key (e.g. `addTagToEntity`). (Also allows multiple entities to be passed in)
* @param {object} [options={}] - Optional arguments
* @param {JQuery<HTMLElement>|string?} [options.tagListSelector=null] - An optional selector if a specific list should be updated with the tag removed too (for example because the add was triggered for that function)
* @param {JQuery<HTMLElement>?} [options.tagElement=null] - Optionally a direct html element of the tag to be removed, so it can be removed from the UI
* @returns {boolean} Whether at least one tag was removed
*/
export function removeTagFromEntity(tag, entityId, { tagListSelector = null, tagElement = null } = {}) {
let result = false;
// Remove tag from the map
if (Array.isArray(entityId)) {
entityId.forEach((id) => result = removeTagFromMap(tag.id, id) || result);
} else {
result = removeTagFromMap(tag.id, entityId);
}
// Save and redraw
printCharactersDebounced();
saveSettingsDebounced();
// We don't reprint the lists, we can just remove the html elements from them.
if (tagListSelector) {
const $selector = (typeof tagListSelector === 'string') ? $(tagListSelector) : tagListSelector;
$selector.find(`.tag[id="${tag.id}"]`).remove();
}
if (tagElement) tagElement.remove();
$(`${getInlineListSelector()} .tag[id="${tag.id}"]`).remove();
return result;
}
/**
* Adds a tag from a given character. If no character is provided, adds it from the currently active one.
* @param {string} tagId - The id of the tag
* @param {string} characterId - The id/key of the character or group
* @returns {boolean} Whether the tag was added or not
*/
function addTagToMap(tagId, characterId = null) {
const key = characterId !== null && characterId !== undefined ? getTagKeyForEntity(characterId) : getTagKey();
if (!key) {
return;
return false;
}
if (!Array.isArray(tag_map[key])) {
tag_map[key] = [tagId];
return true;
}
else {
if (tag_map[key].includes(tagId))
return false;
tag_map[key].push(tagId);
tag_map[key] = tag_map[key].filter(onlyUnique);
return true;
}
}
/**
* Removes a tag from a given character. If no character is provided, removes it from the currently active one.
* @param {string} tagId - The id of the tag
* @param {string} characterId - The id/key of the character or group
* @returns {boolean} Whether the tag was removed or not
*/
function removeTagFromMap(tagId, characterId = null) {
const key = characterId !== null && characterId !== undefined ? getTagKeyForEntity(characterId) : getTagKey();
if (!key) {
return;
return false;
}
if (!Array.isArray(tag_map[key])) {
tag_map[key] = [];
return false;
}
else {
const indexOf = tag_map[key].indexOf(tagId);
tag_map[key].splice(indexOf, 1);
return indexOf !== -1;
}
}
@ -535,24 +624,7 @@ function selectTag(event, ui, listSelector, { tagListOptions = {} } = {}) {
const characterData = event.target.closest('#bulk_tags_div')?.dataset.characters;
const characterIds = characterData ? JSON.parse(characterData).characterIds : null;
if (characterIds) {
characterIds.forEach((characterId) => addTagToMap(tag.id, characterId));
} else {
addTagToMap(tag.id);
}
printCharactersDebounced();
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
tagListOptions.addTag = tag;
// add tag to the UI and internal map - we reprint so sorting and new markup is done correctly
printTagList(listSelector, tagListOptions);
const inlineSelector = getInlineListSelector();
if (inlineSelector) {
printTagList($(inlineSelector), tagListOptions);
}
addTagToEntity(tag, characterIds, { tagListSelector: listSelector, tagListOptions: tagListOptions });
// need to return false to keep the input clear
return false;
@ -635,6 +707,7 @@ function createNewTag(tagName) {
create_date: Date.now(),
};
tags.push(tag);
console.debug('Created new tag', tag.name, 'with id', id);
return tag;
}
@ -932,8 +1005,8 @@ function updateTagFilterIndicator() {
function onTagRemoveClick(event) {
event.stopPropagation();
const tag = $(this).closest('.tag');
const tagId = tag.attr('id');
const tagElement = $(this).closest('.tag');
const tagId = tagElement.attr('id');
// 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) {
@ -942,24 +1015,13 @@ function onTagRemoveClick(event) {
return;
}
const tag = tags.find(t => t.id === tagId);
// Optional, check for multiple character ids being present.
const characterData = event.target.closest('#bulk_tags_div')?.dataset.characters;
const characterIds = characterData ? JSON.parse(characterData).characterIds : null;
tag.remove();
if (characterIds) {
characterIds.forEach((characterId) => removeTagFromMap(tagId, characterId));
} else {
removeTagFromMap(tagId);
}
$(`${getInlineListSelector()} .tag[id="${tagId}"]`).remove();
printCharactersDebounced();
saveSettingsDebounced();
removeTagFromEntity(tag, characterIds, { tagElement: tagElement });
}
// @ts-ignore
@ -1461,6 +1523,175 @@ function printViewTagList(empty = true) {
}
}
function registerTagsSlashCommands() {
/**
* Gets the key for char/group for a slash command. If none can be found, a toastr will be shown and null returned.
* @param {string?} [charName] The optionally provided char name
* @returns {string?} - The char/group key, or null if none found
*/
function paraGetCharKey(charName) {
const entity = charName ? characters.find(x => x.name === charName) || groups.find(x => x.name == charName) : characters[this_chid] || groups[selected_group];
const key = getTagKeyForEntity(entity);
if (!key) {
toastr.warning(`Character ${charName} not found.`);
return null;
}
return key;
}
/**
* Gets a tag by its name. Optionally can create the tag if it does not exist.
* @param {string} tagName - The name of the tag
* @param {object} options - Optional arguments
* @param {boolean} [options.allowCreate=false] - Whether a new tag should be created if no tag with the name exists
* @returns {Tag?} The tag, or null if not found
*/
function paraGetTag(tagName, { allowCreate = false } = {}) {
if (!tagName) {
toastr.warning(`Tag name must be provided.`);
return null;
}
let tag = tags.find(t => t.name === tagName);
if (allowCreate && !tag) {
tag = createNewTag(tagName);
}
if (!tag) {
toastr.warning(`Tag ${tagName} not found.`);
return null;
}
return tag;
}
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'tag-add',
returns: 'true/false - Whether the tag was added or was assigned already',
/** @param {{name: string}} namedArgs @param {string} tagName @returns {string} */
callback: ({ name }, tagName) => {
const key = paraGetCharKey(name);
if (!key) return 'false';
const tag = paraGetTag(tagName, { allowCreate: true });
if (!tag) return 'false';
const result = addTagToEntity(tag, key);
return String(result);
},
namedArgumentList: [
new SlashCommandNamedArgument('name', 'Character name', [ARGUMENT_TYPE.STRING], false, false, '{{char}}'),
],
unnamedArgumentList: [
new SlashCommandArgument('tag name', [ARGUMENT_TYPE.STRING], true),
],
helpString: `
<div>
Adds a tag to the character. If no character is provided, it adds it to the current character (<code>{{char}}</code>).
If the tag doesn't exist, it is created.
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/tag-add name="Chloe" scenario</code></pre>
will add the tag "scenario" to the character named Chloe.
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'tag-remove',
returns: 'true/false - Whether the tag was removed or wasn\'t assigned already',
/** @param {{name: string}} namedArgs @param {string} tagName @returns {string} */
callback: ({ name }, tagName) => {
const key = paraGetCharKey(name);
if (!key) return 'false';
const tag = paraGetTag(tagName);
if (!tag) return 'false';
const result = removeTagFromEntity(tag, key);
return String(result);
},
namedArgumentList: [
new SlashCommandNamedArgument('name', 'Character name', [ARGUMENT_TYPE.STRING], false, false, '{{char}}'),
],
unnamedArgumentList: [
new SlashCommandArgument('tag name', [ARGUMENT_TYPE.STRING], true),
],
helpString: `
<div>
Removes a tag from the character. If no character is provided, it removes it from the current character (<code>{{char}}</code>).
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/tag-remove name="Chloe" scenario</code></pre>
will remove the tag "scenario" from the character named Chloe.
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'tag-exists',
returns: 'true/false - Whether the given tag name is assigned to the character',
/** @param {{name: string}} namedArgs @param {string} tagName @returns {string} */
callback: ({ name }, tagName) => {
const key = paraGetCharKey(name);
if (!key) return 'false';
const tag = paraGetTag(tagName);
if (!tag) return 'false';
return String(tag_map[key].includes(tag.id));
},
namedArgumentList: [
new SlashCommandNamedArgument('name', 'Character name', [ARGUMENT_TYPE.STRING], false, false, '{{char}}'),
],
unnamedArgumentList: [
new SlashCommandArgument('tag name', [ARGUMENT_TYPE.STRING], true),
],
helpString: `
<div>
Checks whether the given tag is assigned to the character. If no character is provided, it checks the current character (<code>{{char}}</code>).
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/tag-exists name="Chloe" scenario</code></pre>
will return true if the character named Chloe has the tag "scenario".
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'tag-list',
returns: 'Comma-separated list of all assigned tags',
/** @param {{name: string}} namedArgs @returns {string} */
callback: ({ name }) => {
const key = paraGetCharKey(name);
if (!key) return '';
const tags = getTagsList(key);
return tags.map(x => x.name).join(', ');
},
namedArgumentList: [
new SlashCommandNamedArgument('name', 'Character name', [ARGUMENT_TYPE.STRING], false, false, '{{char}}'),
],
helpString: `
<div>
Lists all assigned tags of the character. If no character is provided, it uses the current character (<code>{{char}}</code>).
<br />
Note that there is no special handling for tags containing commas, they will be printed as-is.
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/tag-list name="Chloe"</code></pre>
could return something like <code>OC, scenario, edited, funny</code>
</li>
</ul>
</div>
`,
}));
}
export function initTags() {
createTagInput('#tagInput', '#tagList', { tagOptions: { removable: true } });
createTagInput('#groupTagInput', '#groupTagList', { tagOptions: { removable: true } });
@ -1506,4 +1737,6 @@ export function initTags() {
printCharactersDebounced();
}
}
registerTagsSlashCommands();
}