From 4fcf641d5780e5cf140a54f1c23fd31117ddb7e2 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Thu, 12 Mar 2026 08:49:10 +0800 Subject: [PATCH] fix(bulk-context-menu): escape special characters in data-filepath selector to support double quotes in filenames (#845) --- static/js/components/ContextMenu/BulkContextMenu.js | 5 ++++- static/js/components/RecipeCard.js | 3 ++- static/js/components/shared/ModelModal.js | 10 ++++++++-- static/js/components/shared/PresetTags.js | 5 ++++- static/js/managers/BulkManager.js | 6 ++++-- static/js/utils/modalUtils.js | 10 ++++++++-- static/js/utils/uiHelpers.js | 5 ++++- 7 files changed, 34 insertions(+), 10 deletions(-) diff --git a/static/js/components/ContextMenu/BulkContextMenu.js b/static/js/components/ContextMenu/BulkContextMenu.js index 80eb511d..2349cc2b 100644 --- a/static/js/components/ContextMenu/BulkContextMenu.js +++ b/static/js/components/ContextMenu/BulkContextMenu.js @@ -117,7 +117,10 @@ export class BulkContextMenu extends BaseContextMenu { countSkipStatus(skipState) { let count = 0; for (const filePath of state.selectedModels) { - const card = document.querySelector(`.model-card[data-filepath="${filePath}"]`); + const escapedPath = window.CSS && typeof window.CSS.escape === 'function' + ? window.CSS.escape(filePath) + : filePath.replace(/["\\]/g, '\\$&'); + const card = document.querySelector(`.model-card[data-filepath="${escapedPath}"]`); if (card) { const isSkipped = card.dataset.skip_metadata_refresh === 'true'; if (isSkipped === skipState) { diff --git a/static/js/components/RecipeCard.js b/static/js/components/RecipeCard.js index 322406a9..41b1e3d9 100644 --- a/static/js/components/RecipeCard.js +++ b/static/js/components/RecipeCard.js @@ -201,8 +201,9 @@ class RecipeCard { this.recipe.favorite = isFavorite; // Re-find star icon in case of re-render during fault + const filePathForXpath = this.recipe.file_path.replace(/"/g, '"'); const currentCard = card.ownerDocument.evaluate( - `.//*[@data-filepath="${this.recipe.file_path}"]`, + `.//*[@data-filepath="${filePathForXpath}"]`, card.ownerDocument, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ).singleNodeValue || card; diff --git a/static/js/components/shared/ModelModal.js b/static/js/components/shared/ModelModal.js index 67b59276..04063789 100644 --- a/static/js/components/shared/ModelModal.js +++ b/static/js/components/shared/ModelModal.js @@ -846,8 +846,14 @@ function setupLoraSpecificFields(filePath) { const currentPath = resolveFilePath(); if (!currentPath) return; - const loraCard = document.querySelector(`.model-card[data-filepath="${currentPath}"]`) || - document.querySelector(`.model-card[data-filepath="${filePath}"]`); + const escapedCurrentPath = window.CSS && typeof window.CSS.escape === 'function' + ? window.CSS.escape(currentPath) + : currentPath.replace(/["\\]/g, '\\$&'); + const escapedFilePath = window.CSS && typeof window.CSS.escape === 'function' + ? window.CSS.escape(filePath) + : filePath.replace(/["\\]/g, '\\$&'); + const loraCard = document.querySelector(`.model-card[data-filepath="${escapedCurrentPath}"]`) || + document.querySelector(`.model-card[data-filepath="${escapedFilePath}"]`); const currentPresets = parsePresets(loraCard?.dataset.usage_tips); if (key === 'strength_range') { diff --git a/static/js/components/shared/PresetTags.js b/static/js/components/shared/PresetTags.js index 7aba2e4b..e2754e70 100644 --- a/static/js/components/shared/PresetTags.js +++ b/static/js/components/shared/PresetTags.js @@ -49,7 +49,10 @@ function formatPresetKey(key) { */ window.removePreset = async function(key) { const filePath = document.querySelector('#modelModal .modal-content .file-path').dataset.filepath; - const loraCard = document.querySelector(`.model-card[data-filepath="${filePath}"]`); + const escapedPath = window.CSS && typeof window.CSS.escape === 'function' + ? window.CSS.escape(filePath) + : filePath.replace(/["\\]/g, '\\$&'); + const loraCard = document.querySelector(`.model-card[data-filepath="${escapedPath}"]`); const currentPresets = parsePresets(loraCard.dataset.usage_tips); delete currentPresets[key]; diff --git a/static/js/managers/BulkManager.js b/static/js/managers/BulkManager.js index 211a88de..cebe057b 100644 --- a/static/js/managers/BulkManager.js +++ b/static/js/managers/BulkManager.js @@ -568,7 +568,8 @@ export class BulkManager { } deselectItem(filepath) { - const card = document.querySelector(`.model-card[data-filepath="${filepath}"]`); + const escapedPath = this.escapeAttributeValue(filepath); + const card = document.querySelector(`.model-card[data-filepath="${escapedPath}"]`); if (card) { card.classList.remove('selected'); } @@ -632,7 +633,8 @@ export class BulkManager { for (const filepath of state.selectedModels) { const metadata = metadataCache.get(filepath); if (metadata) { - const card = document.querySelector(`.model-card[data-filepath="${filepath}"]`); + const escapedPath = this.escapeAttributeValue(filepath); + const card = document.querySelector(`.model-card[data-filepath="${escapedPath}"]`); if (card) { this.updateMetadataCacheFromCard(filepath, card); } diff --git a/static/js/utils/modalUtils.js b/static/js/utils/modalUtils.js index e8936f0a..e7e49e2e 100644 --- a/static/js/utils/modalUtils.js +++ b/static/js/utils/modalUtils.js @@ -7,7 +7,10 @@ let pendingExcludePath = null; export function showDeleteModal(filePath) { pendingDeletePath = filePath; - const card = document.querySelector(`.model-card[data-filepath="${filePath}"]`); + const escapedPath = window.CSS && typeof window.CSS.escape === 'function' + ? window.CSS.escape(filePath) + : filePath.replace(/["\\]/g, '\\$&'); + const card = document.querySelector(`.model-card[data-filepath="${escapedPath}"]`); const modelName = card ? card.dataset.name : filePath.split('/').pop(); const modal = modalManager.getModal('deleteModal').element; const modelInfo = modal.querySelector('.delete-model-info'); @@ -47,7 +50,10 @@ export function closeDeleteModal() { export function showExcludeModal(filePath) { pendingExcludePath = filePath; - const card = document.querySelector(`.model-card[data-filepath="${filePath}"]`); + const escapedPath = window.CSS && typeof window.CSS.escape === 'function' + ? window.CSS.escape(filePath) + : filePath.replace(/["\\]/g, '\\$&'); + const card = document.querySelector(`.model-card[data-filepath="${escapedPath}"]`); const modelName = card ? card.dataset.name : filePath.split('/').pop(); const modal = modalManager.getModal('excludeModal').element; const modelInfo = modal.querySelector('.exclude-model-info'); diff --git a/static/js/utils/uiHelpers.js b/static/js/utils/uiHelpers.js index 3dc54ff1..421e3e84 100644 --- a/static/js/utils/uiHelpers.js +++ b/static/js/utils/uiHelpers.js @@ -197,7 +197,10 @@ export function openCivitaiByMetadata(civitaiId, versionId, modelName = null) { } export function openCivitai(filePath) { - const loraCard = document.querySelector(`.model-card[data-filepath="${filePath}"]`); + const escapedPath = window.CSS && typeof window.CSS.escape === 'function' + ? window.CSS.escape(filePath) + : filePath.replace(/["\\]/g, '\\$&'); + const loraCard = document.querySelector(`.model-card[data-filepath="${escapedPath}"]`); if (!loraCard) return; const metaData = JSON.parse(loraCard.dataset.meta);