Add slash commands for tag management
This commit is contained in:
parent
909ec4191d
commit
96a9f7108c
|
@ -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();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue