mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 15:15:44 -03:00
feat(bulk-tags): add bulk tag management modal and context menu integration
This commit is contained in:
@@ -26,6 +26,7 @@ export class BulkContextMenu extends BaseContextMenu {
|
|||||||
if (!config) return;
|
if (!config) return;
|
||||||
|
|
||||||
// Update button visibility based on model type
|
// Update button visibility based on model type
|
||||||
|
const addTagsItem = this.menu.querySelector('[data-action="add-tags"]');
|
||||||
const sendToWorkflowItem = this.menu.querySelector('[data-action="send-to-workflow"]');
|
const sendToWorkflowItem = this.menu.querySelector('[data-action="send-to-workflow"]');
|
||||||
const copyAllItem = this.menu.querySelector('[data-action="copy-all"]');
|
const copyAllItem = this.menu.querySelector('[data-action="copy-all"]');
|
||||||
const refreshAllItem = this.menu.querySelector('[data-action="refresh-all"]');
|
const refreshAllItem = this.menu.querySelector('[data-action="refresh-all"]');
|
||||||
@@ -47,6 +48,9 @@ export class BulkContextMenu extends BaseContextMenu {
|
|||||||
if (deleteAllItem) {
|
if (deleteAllItem) {
|
||||||
deleteAllItem.style.display = config.deleteAll ? 'flex' : 'none';
|
deleteAllItem.style.display = config.deleteAll ? 'flex' : 'none';
|
||||||
}
|
}
|
||||||
|
if (addTagsItem) {
|
||||||
|
addTagsItem.style.display = config.addTags ? 'flex' : 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSelectedCountHeader() {
|
updateSelectedCountHeader() {
|
||||||
@@ -64,6 +68,9 @@ export class BulkContextMenu extends BaseContextMenu {
|
|||||||
|
|
||||||
handleMenuAction(action, menuItem) {
|
handleMenuAction(action, menuItem) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
case 'add-tags':
|
||||||
|
bulkManager.showBulkAddTagsModal();
|
||||||
|
break;
|
||||||
case 'send-to-workflow':
|
case 'send-to-workflow':
|
||||||
bulkManager.sendAllModelsToWorkflow();
|
bulkManager.sendAllModelsToWorkflow();
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export function setupTagEditMode() {
|
|||||||
// ...existing helper functions...
|
// ...existing helper functions...
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save tags - 支持LoRA和Checkpoint
|
* Save tags
|
||||||
*/
|
*/
|
||||||
async function saveTags() {
|
async function saveTags() {
|
||||||
const editBtn = document.querySelector('.edit-tags-btn');
|
const editBtn = document.querySelector('.edit-tags-btn');
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export class BulkManager {
|
|||||||
// Model type specific action configurations
|
// Model type specific action configurations
|
||||||
this.actionConfig = {
|
this.actionConfig = {
|
||||||
[MODEL_TYPES.LORA]: {
|
[MODEL_TYPES.LORA]: {
|
||||||
|
addTags: true,
|
||||||
sendToWorkflow: true,
|
sendToWorkflow: true,
|
||||||
copyAll: true,
|
copyAll: true,
|
||||||
refreshAll: true,
|
refreshAll: true,
|
||||||
@@ -21,6 +22,7 @@ export class BulkManager {
|
|||||||
deleteAll: true
|
deleteAll: true
|
||||||
},
|
},
|
||||||
[MODEL_TYPES.EMBEDDING]: {
|
[MODEL_TYPES.EMBEDDING]: {
|
||||||
|
addTags: true,
|
||||||
sendToWorkflow: false,
|
sendToWorkflow: false,
|
||||||
copyAll: false,
|
copyAll: false,
|
||||||
refreshAll: true,
|
refreshAll: true,
|
||||||
@@ -28,6 +30,7 @@ export class BulkManager {
|
|||||||
deleteAll: true
|
deleteAll: true
|
||||||
},
|
},
|
||||||
[MODEL_TYPES.CHECKPOINT]: {
|
[MODEL_TYPES.CHECKPOINT]: {
|
||||||
|
addTags: true,
|
||||||
sendToWorkflow: false,
|
sendToWorkflow: false,
|
||||||
copyAll: false,
|
copyAll: false,
|
||||||
refreshAll: true,
|
refreshAll: true,
|
||||||
@@ -406,6 +409,225 @@ export class BulkManager {
|
|||||||
showToast('toast.models.refreshMetadataFailed', {}, 'error');
|
showToast('toast.models.refreshMetadataFailed', {}, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showBulkAddTagsModal() {
|
||||||
|
if (state.selectedModels.size === 0) {
|
||||||
|
showToast('toast.models.noModelsSelected', {}, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const countElement = document.getElementById('bulkAddTagsCount');
|
||||||
|
if (countElement) {
|
||||||
|
countElement.textContent = state.selectedModels.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any existing tags in the modal
|
||||||
|
const tagsContainer = document.getElementById('bulkTagsItems');
|
||||||
|
if (tagsContainer) {
|
||||||
|
tagsContainer.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
modalManager.showModal('bulkAddTagsModal', null, null, () => {
|
||||||
|
// Cleanup when modal is closed
|
||||||
|
this.cleanupBulkAddTagsModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize the bulk tags editing interface
|
||||||
|
this.initializeBulkTagsInterface();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeBulkTagsInterface() {
|
||||||
|
// Import preset tags from ModelTags.js
|
||||||
|
const PRESET_TAGS = [
|
||||||
|
'character', 'style', 'concept', 'clothing',
|
||||||
|
'poses', 'background', 'vehicle', 'buildings',
|
||||||
|
'objects', 'animal'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Setup tag input behavior
|
||||||
|
const tagInput = document.querySelector('.bulk-metadata-input');
|
||||||
|
if (tagInput) {
|
||||||
|
tagInput.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.addBulkTag(e.target.value.trim());
|
||||||
|
e.target.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create suggestions dropdown
|
||||||
|
const tagForm = document.querySelector('#bulkAddTagsModal .metadata-add-form');
|
||||||
|
if (tagForm) {
|
||||||
|
const suggestionsDropdown = this.createBulkSuggestionsDropdown(PRESET_TAGS);
|
||||||
|
tagForm.appendChild(suggestionsDropdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup save button
|
||||||
|
const saveBtn = document.querySelector('.bulk-save-tags-btn');
|
||||||
|
if (saveBtn) {
|
||||||
|
saveBtn.addEventListener('click', () => {
|
||||||
|
this.saveBulkTags();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createBulkSuggestionsDropdown(presetTags) {
|
||||||
|
const dropdown = document.createElement('div');
|
||||||
|
dropdown.className = 'metadata-suggestions-dropdown';
|
||||||
|
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'metadata-suggestions-header';
|
||||||
|
header.innerHTML = `
|
||||||
|
<span>Suggested Tags</span>
|
||||||
|
<small>Click to add</small>
|
||||||
|
`;
|
||||||
|
dropdown.appendChild(header);
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'metadata-suggestions-container';
|
||||||
|
|
||||||
|
presetTags.forEach(tag => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'metadata-suggestion-item';
|
||||||
|
item.title = tag;
|
||||||
|
item.innerHTML = `<span class="metadata-suggestion-text">${tag}</span>`;
|
||||||
|
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
this.addBulkTag(tag);
|
||||||
|
const input = document.querySelector('.bulk-metadata-input');
|
||||||
|
if (input) {
|
||||||
|
input.value = tag;
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
dropdown.appendChild(container);
|
||||||
|
return dropdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
addBulkTag(tag) {
|
||||||
|
tag = tag.trim().toLowerCase();
|
||||||
|
if (!tag) return;
|
||||||
|
|
||||||
|
const tagsContainer = document.getElementById('bulkTagsItems');
|
||||||
|
if (!tagsContainer) return;
|
||||||
|
|
||||||
|
// Validation: Check length
|
||||||
|
if (tag.length > 30) {
|
||||||
|
showToast('modelTags.validation.maxLength', {}, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation: Check total number
|
||||||
|
const currentTags = tagsContainer.querySelectorAll('.metadata-item');
|
||||||
|
if (currentTags.length >= 30) {
|
||||||
|
showToast('modelTags.validation.maxCount', {}, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation: Check for duplicates
|
||||||
|
const existingTags = Array.from(currentTags).map(tagEl => tagEl.dataset.tag);
|
||||||
|
if (existingTags.includes(tag)) {
|
||||||
|
showToast('modelTags.validation.duplicate', {}, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new tag
|
||||||
|
const newTag = document.createElement('div');
|
||||||
|
newTag.className = 'metadata-item';
|
||||||
|
newTag.dataset.tag = tag;
|
||||||
|
newTag.innerHTML = `
|
||||||
|
<span class="metadata-item-content">${tag}</span>
|
||||||
|
<button class="metadata-delete-btn">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add delete button event listener
|
||||||
|
const deleteBtn = newTag.querySelector('.metadata-delete-btn');
|
||||||
|
deleteBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
newTag.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
tagsContainer.appendChild(newTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveBulkTags() {
|
||||||
|
const tagElements = document.querySelectorAll('#bulkTagsItems .metadata-item');
|
||||||
|
const tags = Array.from(tagElements).map(tag => tag.dataset.tag);
|
||||||
|
|
||||||
|
if (tags.length === 0) {
|
||||||
|
showToast('toast.models.noTagsToAdd', {}, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.selectedModels.size === 0) {
|
||||||
|
showToast('toast.models.noModelsSelected', {}, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const apiClient = getModelApiClient();
|
||||||
|
const filePaths = Array.from(state.selectedModels);
|
||||||
|
let successCount = 0;
|
||||||
|
let failCount = 0;
|
||||||
|
|
||||||
|
// Add tags to each selected model
|
||||||
|
for (const filePath of filePaths) {
|
||||||
|
try {
|
||||||
|
await apiClient.addTags(filePath, { tags: tags });
|
||||||
|
successCount++;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to add tags to ${filePath}:`, error);
|
||||||
|
failCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modalManager.closeModal('bulkAddTagsModal');
|
||||||
|
|
||||||
|
if (successCount > 0) {
|
||||||
|
const currentConfig = MODEL_CONFIG[state.currentPageType];
|
||||||
|
showToast('toast.models.tagsAddedSuccessfully', {
|
||||||
|
count: successCount,
|
||||||
|
tagCount: tags.length,
|
||||||
|
type: currentConfig.displayName.toLowerCase()
|
||||||
|
}, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failCount > 0) {
|
||||||
|
showToast('toast.models.tagsAddFailed', { count: failCount }, 'warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during bulk tag addition:', error);
|
||||||
|
showToast('toast.models.bulkTagsAddFailed', {}, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupBulkAddTagsModal() {
|
||||||
|
// Clear tags container
|
||||||
|
const tagsContainer = document.getElementById('bulkTagsItems');
|
||||||
|
if (tagsContainer) {
|
||||||
|
tagsContainer.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear input
|
||||||
|
const input = document.querySelector('.bulk-metadata-input');
|
||||||
|
if (input) {
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove event listeners (they will be re-added when modal opens again)
|
||||||
|
const saveBtn = document.querySelector('.bulk-save-tags-btn');
|
||||||
|
if (saveBtn) {
|
||||||
|
saveBtn.replaceWith(saveBtn.cloneNode(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const bulkManager = new BulkManager();
|
export const bulkManager = new BulkManager();
|
||||||
|
|||||||
@@ -234,6 +234,19 @@ export class ModalManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add bulkAddTagsModal registration
|
||||||
|
const bulkAddTagsModal = document.getElementById('bulkAddTagsModal');
|
||||||
|
if (bulkAddTagsModal) {
|
||||||
|
this.registerModal('bulkAddTagsModal', {
|
||||||
|
element: bulkAddTagsModal,
|
||||||
|
onClose: () => {
|
||||||
|
this.getModal('bulkAddTagsModal').element.style.display = 'none';
|
||||||
|
document.body.classList.remove('modal-open');
|
||||||
|
},
|
||||||
|
closeOnOutsideClick: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('keydown', this.boundHandleEscape);
|
document.addEventListener('keydown', this.boundHandleEscape);
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,9 @@
|
|||||||
<span>{{ t('loras.bulkOperations.selected', {'count': 0}) }}</span>
|
<span>{{ t('loras.bulkOperations.selected', {'count': 0}) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="context-menu-separator"></div>
|
<div class="context-menu-separator"></div>
|
||||||
|
<div class="context-menu-item" data-action="add-tags">
|
||||||
|
<i class="fas fa-tags"></i> <span>{{ t('loras.bulkOperations.addTags') }}</span>
|
||||||
|
</div>
|
||||||
<div class="context-menu-item" data-action="send-to-workflow">
|
<div class="context-menu-item" data-action="send-to-workflow">
|
||||||
<i class="fas fa-paper-plane"></i> <span>{{ t('loras.bulkOperations.sendToWorkflow') }}</span>
|
<i class="fas fa-paper-plane"></i> <span>{{ t('loras.bulkOperations.sendToWorkflow') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,3 +10,4 @@
|
|||||||
{% include 'components/modals/example_access_modal.html' %}
|
{% include 'components/modals/example_access_modal.html' %}
|
||||||
{% include 'components/modals/download_modal.html' %}
|
{% include 'components/modals/download_modal.html' %}
|
||||||
{% include 'components/modals/move_modal.html' %}
|
{% include 'components/modals/move_modal.html' %}
|
||||||
|
{% include 'components/modals/bulk_add_tags_modal.html' %}
|
||||||
37
templates/components/modals/bulk_add_tags_modal.html
Normal file
37
templates/components/modals/bulk_add_tags_modal.html
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<div id="bulkAddTagsModal" class="modal" style="display: none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3><i class="fas fa-tags"></i> {{ t('modals.bulkAddTags.title') }}</h3>
|
||||||
|
<button class="modal-close" onclick="modalManager.closeModal('bulkAddTagsModal')">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="bulk-add-tags-info">
|
||||||
|
<p>{{ t('modals.bulkAddTags.description') }} <span id="bulkAddTagsCount">0</span> {{ t('modals.bulkAddTags.models') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="model-tags-container bulk-tags-container edit-mode">
|
||||||
|
<div class="metadata-edit-container" style="display: block;">
|
||||||
|
<div class="metadata-edit-content">
|
||||||
|
<div class="metadata-edit-header">
|
||||||
|
<label>{{ t('modals.bulkAddTags.tagsToAdd') }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-items" id="bulkTagsItems">
|
||||||
|
<!-- Tags will be added here dynamically -->
|
||||||
|
</div>
|
||||||
|
<div class="metadata-edit-controls">
|
||||||
|
<button class="save-tags-btn bulk-save-tags-btn" title="{{ t('modals.bulkAddTags.saveChanges') }}">
|
||||||
|
<i class="fas fa-save"></i> {{ t('common.actions.save') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-add-form">
|
||||||
|
<input type="text" class="metadata-input bulk-metadata-input" placeholder="{{ t('modals.bulkAddTags.placeholder') }}">
|
||||||
|
</div>
|
||||||
|
<!-- Suggestions dropdown will be added here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user