mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge pull request #2582 from SillyTavern/improve-tag-backup-restore
Improve Tag Backup Restore functionality
This commit is contained in:
@ -101,6 +101,21 @@ const showPopupHelper = {
|
|||||||
if (typeof result === 'string' || typeof result === 'boolean') throw new Error(`Invalid popup result. CONFIRM popups only support numbers, or null. Result: ${result}`);
|
if (typeof result === 'string' || typeof result === 'boolean') throw new Error(`Invalid popup result. CONFIRM popups only support numbers, or null. Result: ${result}`);
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Asynchronously displays a text popup with the given header and text, returning the clicked result button value.
|
||||||
|
*
|
||||||
|
* @param {string?} header - The header text for the popup.
|
||||||
|
* @param {string?} text - The main text for the popup.
|
||||||
|
* @param {PopupOptions} [popupOptions={}] - Options for the popup.
|
||||||
|
* @return {Promise<POPUP_RESULT>} A Promise that resolves with the result of the user's interaction.
|
||||||
|
*/
|
||||||
|
text: async (header, text, popupOptions = {}) => {
|
||||||
|
const content = PopupUtils.BuildTextWithHeader(header, text);
|
||||||
|
const popup = new Popup(content, POPUP_TYPE.TEXT, null, popupOptions);
|
||||||
|
const result = await popup.show();
|
||||||
|
if (typeof result === 'string' || typeof result === 'boolean') throw new Error(`Invalid popup result. TEXT popups only support numbers, or null. Result: ${result}`);
|
||||||
|
return result;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Popup {
|
export class Popup {
|
||||||
|
@ -21,7 +21,7 @@ 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_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
|
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
|
||||||
import { debounce_timeout } from './constants.js';
|
import { debounce_timeout } from './constants.js';
|
||||||
import { INTERACTABLE_CONTROL_CLASS } from './keyboard.js';
|
import { INTERACTABLE_CONTROL_CLASS } from './keyboard.js';
|
||||||
import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
|
import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||||
@ -1436,18 +1436,28 @@ async function onTagRestoreFileSelect(e) {
|
|||||||
const data = await parseJsonFile(file);
|
const data = await parseJsonFile(file);
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
toastr.warning('Empty file data', 'Tag restore');
|
toastr.warning('Empty file data', 'Tag Restore');
|
||||||
console.log('Tag restore: File data empty.');
|
console.log('Tag restore: File data empty.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.tags || !data.tag_map || !Array.isArray(data.tags) || typeof data.tag_map !== 'object') {
|
if (!data.tags || !data.tag_map || !Array.isArray(data.tags) || typeof data.tag_map !== 'object') {
|
||||||
toastr.warning('Invalid file format', 'Tag restore');
|
toastr.warning('Invalid file format', 'Tag Restore');
|
||||||
console.log('Tag restore: Invalid file format.');
|
console.log('Tag restore: Invalid file format.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prompt user if they want to overwrite existing tags
|
||||||
|
let overwrite = false;
|
||||||
|
if (tags.length > 0) {
|
||||||
|
const result = await Popup.show.confirm('Tag Restore', 'You have existing tags. If the backup contains any of those tags, do you want the backup to overwrite their settings (Name, color, folder state, etc)?',
|
||||||
|
{ okButton: 'Overwrite', cancelButton: 'Keep Existing' });
|
||||||
|
overwrite = result === POPUP_RESULT.AFFIRMATIVE;
|
||||||
|
}
|
||||||
|
|
||||||
const warnings = [];
|
const warnings = [];
|
||||||
|
/** @type {Map<string, string>} Map import tag ids with existing ids on overwrite */
|
||||||
|
const idToActualTagIdMap = new Map();
|
||||||
|
|
||||||
// Import tags
|
// Import tags
|
||||||
for (const tag of data.tags) {
|
for (const tag of data.tags) {
|
||||||
@ -1456,10 +1466,28 @@ async function onTagRestoreFileSelect(e) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tags.find(x => x.id === tag.id)) {
|
// Check against both existing id (direct match) and tag with the same name, which is not allowed.
|
||||||
warnings.push(`Tag with id ${tag.id} already exists.`);
|
let existingTag = tags.find(x => x.id === tag.id);
|
||||||
|
if (existingTag && !overwrite) {
|
||||||
|
warnings.push(`Tag '${tag.name}' with id ${tag.id} already exists.`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
existingTag = getTag(tag.name);
|
||||||
|
if (existingTag && !overwrite) {
|
||||||
|
warnings.push(`Tag with name '${tag.name}' already exists.`);
|
||||||
|
// Remember the tag id, so we can still import the tag map entries for this
|
||||||
|
idToActualTagIdMap.set(tag.id, existingTag.id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingTag) {
|
||||||
|
// On overwrite, we remove and re-add the tag
|
||||||
|
removeFromArray(tags, existingTag);
|
||||||
|
// And remember the ID if it was different, so we can update the tag map accordingly
|
||||||
|
if (existingTag.id !== tag.id) {
|
||||||
|
idToActualTagIdMap.set(existingTag.id, tag.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tags.push(tag);
|
tags.push(tag);
|
||||||
}
|
}
|
||||||
@ -1478,30 +1506,39 @@ async function onTagRestoreFileSelect(e) {
|
|||||||
const groupExists = groups.some(x => String(x.id) === String(key));
|
const groupExists = groups.some(x => String(x.id) === String(key));
|
||||||
|
|
||||||
if (!characterExists && !groupExists) {
|
if (!characterExists && !groupExists) {
|
||||||
warnings.push(`Tag map key ${key} does not exist.`);
|
warnings.push(`Tag map key ${key} does not exist as character or group.`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get existing tag ids for this key or empty array.
|
// Get existing tag ids for this key or empty array.
|
||||||
const existingTagIds = tag_map[key] || [];
|
const existingTagIds = tag_map[key] || [];
|
||||||
// Merge existing and new tag ids. Remove duplicates.
|
|
||||||
tag_map[key] = existingTagIds.concat(tagIds).filter(onlyUnique);
|
// Merge existing and new tag ids. Replace the ones mapped to a new id. Remove duplicates.
|
||||||
|
const combinedTags = existingTagIds.concat(tagIds)
|
||||||
|
.map(tagId => (idToActualTagIdMap.has(tagId)) ? idToActualTagIdMap.get(tagId) : tagId)
|
||||||
|
.filter(onlyUnique);
|
||||||
|
|
||||||
// Verify that all tags exist. Remove tags that don't exist.
|
// Verify that all tags exist. Remove tags that don't exist.
|
||||||
tag_map[key] = tag_map[key].filter(x => tags.some(y => String(y.id) === String(x)));
|
tag_map[key] = combinedTags.filter(tagId => tags.some(y => String(y.id) === String(tagId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (warnings.length) {
|
if (warnings.length) {
|
||||||
toastr.success('Tags restored with warnings. Check console for details.');
|
toastr.warning('Tags restored with warnings. Check console or click on this message for details.', 'Tag Restore', {
|
||||||
|
timeOut: toastr.options.timeOut * 2, // Display double the time
|
||||||
|
onclick: () => Popup.show.text('Tag Restore Warnings', `<samp class="justifyLeft">${DOMPurify.sanitize(warnings.join('\n'))}<samp>`, { allowVerticalScrolling: true }),
|
||||||
|
});
|
||||||
console.warn(`TAG RESTORE REPORT\n====================\n${warnings.join('\n')}`);
|
console.warn(`TAG RESTORE REPORT\n====================\n${warnings.join('\n')}`);
|
||||||
} else {
|
} else {
|
||||||
toastr.success('Tags restored successfully.');
|
toastr.success('Tags restored successfully.', 'Tag Restore');
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#tag_view_restore_input').val('');
|
$('#tag_view_restore_input').val('');
|
||||||
printCharactersDebounced();
|
printCharactersDebounced();
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
|
|
||||||
await onViewTagsListClick();
|
// Reprint the tag management popup, without having it to be opened again
|
||||||
|
const tagContainer = $('#tag_view_list .tag_view_list_tags');
|
||||||
|
printViewTagList(tagContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onBackupRestoreClick() {
|
function onBackupRestoreClick() {
|
||||||
|
@ -470,6 +470,13 @@ kbd {
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
samp {
|
||||||
|
display: block;
|
||||||
|
font-family: var(--monoFontFamily);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
text-align: start;
|
||||||
|
justify-content: left;
|
||||||
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
background-image: linear-gradient(90deg, var(--transparent), var(--SmartThemeBodyColor), var(--transparent));
|
background-image: linear-gradient(90deg, var(--transparent), var(--SmartThemeBodyColor), var(--transparent));
|
||||||
|
Reference in New Issue
Block a user