mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge pull request #2258 from Wolfsblvt/tag-slash-commands
Add slash commands for tag management
This commit is contained in:
@ -103,7 +103,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#bulkTagsList,
|
#bulkTagsList,
|
||||||
#tagList .tag {
|
#tagList .tag,
|
||||||
|
#groupTagList .tag {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,10 +14,12 @@ import {
|
|||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
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, 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 } from './utils.js';
|
import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight } from './utils.js';
|
||||||
import { power_user } from './power-user.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 {
|
export {
|
||||||
TAG_FOLDER_TYPES,
|
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)`.
|
* 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 {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
|
* @returns {Tag[]} A list of tags
|
||||||
*/
|
*/
|
||||||
function getTagsList(key, sort = true) {
|
function getTagsList(key, sort = true) {
|
||||||
@ -463,35 +465,122 @@ export function getTagKeyForEntityElement(element) {
|
|||||||
return undefined;
|
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) {
|
function addTagToMap(tagId, characterId = null) {
|
||||||
const key = characterId !== null && characterId !== undefined ? getTagKeyForEntity(characterId) : getTagKey();
|
const key = characterId !== null && characterId !== undefined ? getTagKeyForEntity(characterId) : getTagKey();
|
||||||
|
|
||||||
if (!key) {
|
if (!key) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(tag_map[key])) {
|
if (!Array.isArray(tag_map[key])) {
|
||||||
tag_map[key] = [tagId];
|
tag_map[key] = [tagId];
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
if (tag_map[key].includes(tagId))
|
||||||
|
return false;
|
||||||
|
|
||||||
tag_map[key].push(tagId);
|
tag_map[key].push(tagId);
|
||||||
tag_map[key] = tag_map[key].filter(onlyUnique);
|
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) {
|
function removeTagFromMap(tagId, characterId = null) {
|
||||||
const key = characterId !== null && characterId !== undefined ? getTagKeyForEntity(characterId) : getTagKey();
|
const key = characterId !== null && characterId !== undefined ? getTagKeyForEntity(characterId) : getTagKey();
|
||||||
|
|
||||||
if (!key) {
|
if (!key) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(tag_map[key])) {
|
if (!Array.isArray(tag_map[key])) {
|
||||||
tag_map[key] = [];
|
tag_map[key] = [];
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const indexOf = tag_map[key].indexOf(tagId);
|
const indexOf = tag_map[key].indexOf(tagId);
|
||||||
tag_map[key].splice(indexOf, 1);
|
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 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;
|
||||||
|
|
||||||
if (characterIds) {
|
addTagToEntity(tag, characterIds, { tagListSelector: listSelector, tagListOptions: tagListOptions });
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// need to return false to keep the input clear
|
// need to return false to keep the input clear
|
||||||
return false;
|
return false;
|
||||||
@ -635,6 +707,7 @@ function createNewTag(tagName) {
|
|||||||
create_date: Date.now(),
|
create_date: Date.now(),
|
||||||
};
|
};
|
||||||
tags.push(tag);
|
tags.push(tag);
|
||||||
|
console.debug('Created new tag', tag.name, 'with id', tag.id);
|
||||||
return tag;
|
return tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -932,8 +1005,8 @@ function updateTagFilterIndicator() {
|
|||||||
|
|
||||||
function onTagRemoveClick(event) {
|
function onTagRemoveClick(event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const tag = $(this).closest('.tag');
|
const tagElement = $(this).closest('.tag');
|
||||||
const tagId = tag.attr('id');
|
const tagId = tagElement.attr('id');
|
||||||
|
|
||||||
// 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) {
|
||||||
@ -942,24 +1015,13 @@ function onTagRemoveClick(event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tag = tags.find(t => t.id === tagId);
|
||||||
|
|
||||||
// Optional, check for multiple character ids being present.
|
// Optional, check for multiple character ids being present.
|
||||||
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;
|
||||||
|
|
||||||
tag.remove();
|
removeTagFromEntity(tag, characterIds, { tagElement: tagElement });
|
||||||
|
|
||||||
if (characterIds) {
|
|
||||||
characterIds.forEach((characterId) => removeTagFromMap(tagId, characterId));
|
|
||||||
} else {
|
|
||||||
removeTagFromMap(tagId);
|
|
||||||
}
|
|
||||||
|
|
||||||
$(`${getInlineListSelector()} .tag[id="${tagId}"]`).remove();
|
|
||||||
|
|
||||||
printCharactersDebounced();
|
|
||||||
saveSettingsDebounced();
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -1461,6 +1523,194 @@ 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))
|
||||||
|
: (selected_group ? groups.find(x => x.id == selected_group) : characters[this_chid]);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTagsList() {
|
||||||
|
switch (menu_type) {
|
||||||
|
case 'characters':
|
||||||
|
printTagFilters(tag_filter_types.character);
|
||||||
|
printTagFilters(tag_filter_types.group_member);
|
||||||
|
break;
|
||||||
|
case 'character_edit':
|
||||||
|
applyTagsOnCharacterSelect();
|
||||||
|
break;
|
||||||
|
case 'group_edit':
|
||||||
|
select_group_chats(selected_group, true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
updateTagsList();
|
||||||
|
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);
|
||||||
|
updateTagsList();
|
||||||
|
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() {
|
export function initTags() {
|
||||||
createTagInput('#tagInput', '#tagList', { tagOptions: { removable: true } });
|
createTagInput('#tagInput', '#tagList', { tagOptions: { removable: true } });
|
||||||
createTagInput('#groupTagInput', '#groupTagList', { tagOptions: { removable: true } });
|
createTagInput('#groupTagInput', '#groupTagList', { tagOptions: { removable: true } });
|
||||||
@ -1505,4 +1755,6 @@ export function initTags() {
|
|||||||
printCharactersDebounced();
|
printCharactersDebounced();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerTagsSlashCommands();
|
||||||
}
|
}
|
||||||
|
@ -2068,6 +2068,7 @@ input[type="file"] {
|
|||||||
gap: 5px;
|
gap: 5px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bulk_select_checkbox {
|
.bulk_select_checkbox {
|
||||||
|
Reference in New Issue
Block a user