mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-07-13 20:21:16 -03:00
fix(settings): reject checkpoints↔unet path overlap in extra folder paths with inline error UI
Changes: - Backend: _validate_folder_paths() now checks checkpoints↔unet overlap within the same library using os.path.realpath() for symlink resolution - Backend: set() calls _validate_folder_paths() for both folder_paths and extra_folder_paths before writing - Backend: extracted _normalize_path_set() helper to eliminate duplicated normalization logic - Frontend: inline error display with red border + error message below the conflicting input, no save triggered - Frontend: path normalization (strip trailing slash, lowercase) in pre-check to reduce false negatives vs backend realpath - Frontend: asymmetric error UX — message only on the user-edited side, red border on the pre-existing conflict side - CSS: has-error styles with hardcoded rgba fallback for older browsers - i18n: checkpointUnetOverlap + checkpointUnetOverlapInline keys added to all 10 locale files
This commit is contained in:
@@ -1562,6 +1562,29 @@ input:checked + .toggle-slider:before {
|
||||
box-shadow: 0 0 0 2px rgba(var(--lora-accent-rgb, 79, 70, 229), 0.1);
|
||||
}
|
||||
|
||||
.extra-folder-path-row .path-controls .extra-folder-path-input.has-error {
|
||||
border-color: var(--lora-error);
|
||||
background-color: rgba(220, 53, 69, 0.08);
|
||||
background-color: rgba(from var(--lora-error) r g b / 0.08);
|
||||
}
|
||||
|
||||
.extra-folder-path-row .path-controls .extra-folder-path-input.has-error:focus {
|
||||
box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.15);
|
||||
box-shadow: 0 0 0 2px rgba(from var(--lora-error) r g b / 0.15);
|
||||
}
|
||||
|
||||
.extra-folder-path-error {
|
||||
color: var(--lora-error);
|
||||
font-size: 0.8em;
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.extra-folder-path-error.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.extra-folder-path-row .path-controls .remove-path-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
@@ -1693,13 +1693,15 @@ export class SettingsManager {
|
||||
<input type="text" class="extra-folder-path-input"
|
||||
placeholder="${translate('settings.extraFolderPaths.pathPlaceholder', {}, '/path/to/models')}" value="${path}"
|
||||
onblur="settingsManager.updateExtraFolderPaths('${modelType}')"
|
||||
onfocus="settingsManager.clearExtraFolderPathError(this)"
|
||||
onkeydown="if(event.key === 'Enter') { this.blur(); }" />
|
||||
<button type="button" class="remove-path-btn"
|
||||
onclick="this.parentElement.parentElement.remove(); settingsManager.updateExtraFolderPaths('${modelType}')"
|
||||
onclick="settingsManager.removeExtraFolderPathRow(this, '${modelType}')"
|
||||
title="${translate('common.actions.delete', {}, 'Delete')}">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="extra-folder-path-error"></div>
|
||||
`;
|
||||
|
||||
container.appendChild(row);
|
||||
@@ -1713,7 +1715,63 @@ export class SettingsManager {
|
||||
}
|
||||
}
|
||||
|
||||
clearExtraFolderPathError(input) {
|
||||
input.classList.remove('has-error');
|
||||
const row = input.closest('.extra-folder-path-row');
|
||||
if (row) {
|
||||
const errEl = row.querySelector('.extra-folder-path-error');
|
||||
if (errEl) {
|
||||
errEl.classList.remove('visible');
|
||||
errEl.textContent = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_clearAllExtraFolderPathErrors() {
|
||||
document.querySelectorAll('.extra-folder-path-input.has-error').forEach((input) => {
|
||||
input.classList.remove('has-error');
|
||||
});
|
||||
document.querySelectorAll('.extra-folder-path-error.visible').forEach((el) => {
|
||||
el.classList.remove('visible');
|
||||
el.textContent = '';
|
||||
});
|
||||
}
|
||||
|
||||
_markExtraFolderPathsError(modelType, overlappingPaths, showMessage = false) {
|
||||
const container = document.getElementById(`extraFolderPaths-${modelType}`);
|
||||
if (!container) return;
|
||||
|
||||
const inputs = container.querySelectorAll('.extra-folder-path-input');
|
||||
inputs.forEach((input) => {
|
||||
const val = input.value.trim();
|
||||
if (val && overlappingPaths.includes(val)) {
|
||||
input.classList.add('has-error');
|
||||
if (showMessage) {
|
||||
const row = input.closest('.extra-folder-path-row');
|
||||
if (row) {
|
||||
const errEl = row.querySelector('.extra-folder-path-error');
|
||||
if (errEl) {
|
||||
errEl.textContent = translate('settings.extraFolderPaths.validation.checkpointUnetOverlapInline', {}, 'This path is also used for a different model type. Use separate folders for checkpoints and diffusion models.');
|
||||
errEl.classList.add('visible');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeExtraFolderPathRow(btn, modelType) {
|
||||
const row = btn.closest('.extra-folder-path-row');
|
||||
if (row) {
|
||||
row.remove();
|
||||
this.updateExtraFolderPaths(modelType);
|
||||
}
|
||||
}
|
||||
|
||||
async updateExtraFolderPaths(changedModelType) {
|
||||
// Clear previous errors
|
||||
this._clearAllExtraFolderPathErrors();
|
||||
|
||||
const extraFolderPaths = {};
|
||||
|
||||
// Collect paths for all model types
|
||||
@@ -1734,6 +1792,32 @@ export class SettingsManager {
|
||||
extraFolderPaths[modelType] = paths;
|
||||
});
|
||||
|
||||
// Client-side pre-check: checkpoints and unet must not share the same path.
|
||||
// Normalise paths to reduce false negatives vs the backend's realpath + normcase.
|
||||
const normalise = (p) => p.replace(/[/\\]+$/, '').toLowerCase();
|
||||
const ckptSet = new Set((extraFolderPaths.checkpoints || []).map(normalise));
|
||||
const unetSet = new Set((extraFolderPaths.unet || []).map(normalise));
|
||||
const ckptOverlap = (extraFolderPaths.checkpoints || []).filter(p => p && unetSet.has(normalise(p)));
|
||||
const unetOverlap = (extraFolderPaths.unet || []).filter(p => p && ckptSet.has(normalise(p)));
|
||||
const hasOverlap = ckptOverlap.length > 0 || unetOverlap.length > 0;
|
||||
|
||||
if (hasOverlap) {
|
||||
// Error message only on the side the user just edited.
|
||||
// The other side gets red border only (passive conflict indicator).
|
||||
if (changedModelType === 'checkpoints') {
|
||||
this._markExtraFolderPathsError('checkpoints', ckptOverlap, true);
|
||||
this._markExtraFolderPathsError('unet', unetOverlap, false);
|
||||
} else if (changedModelType === 'unet') {
|
||||
this._markExtraFolderPathsError('unet', unetOverlap, true);
|
||||
this._markExtraFolderPathsError('checkpoints', ckptOverlap, false);
|
||||
} else {
|
||||
// Pre-existing conflict from direct config edit — mark both without messages
|
||||
this._markExtraFolderPathsError('checkpoints', ckptOverlap, false);
|
||||
this._markExtraFolderPathsError('unet', unetOverlap, false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if paths have actually changed
|
||||
const currentPaths = state.global.settings.extra_folder_paths || {};
|
||||
const pathsChanged = JSON.stringify(currentPaths) !== JSON.stringify(extraFolderPaths);
|
||||
|
||||
Reference in New Issue
Block a user