Add Suggest Subfolder context menu for CivitAI and CivArchive download results
This commit is contained in:
@@ -378,6 +378,155 @@ test('loaded model context menu locates its exact workflow node and subgraph', (
|
||||
assert.match(resolverDialogSource, /textContent: "Locate Node"/);
|
||||
});
|
||||
|
||||
test('download result context menu offers subfolder suggestions only for CivitAI and CivArchive', () => {
|
||||
const canSuggestDownloadSubfolderFromContextMenu = eval(`(${extractMethod(downloadTargetMethodsSource, 'canSuggestDownloadSubfolderFromContextMenu')})`);
|
||||
|
||||
assert.equal(canSuggestDownloadSubfolderFromContextMenu({
|
||||
context_scope: 'download_table',
|
||||
source: 'civitai',
|
||||
}), true);
|
||||
assert.equal(canSuggestDownloadSubfolderFromContextMenu({
|
||||
context_scope: 'download_table',
|
||||
details_source: 'civarchive',
|
||||
}), true);
|
||||
assert.equal(canSuggestDownloadSubfolderFromContextMenu({
|
||||
context_scope: 'download_table',
|
||||
source: 'huggingface',
|
||||
}), false);
|
||||
assert.equal(canSuggestDownloadSubfolderFromContextMenu({
|
||||
context_scope: 'local_model',
|
||||
source: 'civitai',
|
||||
}), false);
|
||||
assert.match(resolverDialogSource, /data-menu-action": "suggestSubfolder"/);
|
||||
assert.match(resolverDialogSource, /textContent: "Suggest Subfolder"/);
|
||||
});
|
||||
|
||||
test('download result context menu reuses the existing forced subfolder suggestion', async () => {
|
||||
const getContextMenuDownloadMissing = eval(`(${extractMethod(downloadTargetMethodsSource, 'getContextMenuDownloadMissing')})`);
|
||||
const suggestDownloadSubfolderFromContextMenu = eval(`(${extractMethod(downloadTargetMethodsSource, 'suggestDownloadSubfolderFromContextMenu')})`);
|
||||
const missing = {
|
||||
node_id: 19,
|
||||
widget_index: 2,
|
||||
subgraph_id: '',
|
||||
is_top_level: true,
|
||||
};
|
||||
const categoryEl = {
|
||||
closest() {
|
||||
return { id: 'download-target' };
|
||||
},
|
||||
};
|
||||
const subfolderEl = {};
|
||||
const calls = [];
|
||||
const dialog = {
|
||||
missingModels: [missing],
|
||||
contentElement: {
|
||||
querySelector(selector) {
|
||||
if (selector === '#download-category-19-2') return categoryEl;
|
||||
if (selector === '#download-subfolder-19-2') return subfolderEl;
|
||||
return null;
|
||||
},
|
||||
},
|
||||
getMissingModelKey() {
|
||||
return 'missing-19-2';
|
||||
},
|
||||
canSuggestDownloadSubfolderFromContextMenu() {
|
||||
return true;
|
||||
},
|
||||
getContextMenuDownloadMissing,
|
||||
async forceSuggestedDownloadSubfolder(...args) {
|
||||
calls.push(['suggest', ...args]);
|
||||
return {
|
||||
category: 'loras',
|
||||
subfolder: 'Krea 2/Concept',
|
||||
};
|
||||
},
|
||||
refreshDownloadTargetHelp(...args) {
|
||||
calls.push(['refresh', ...args]);
|
||||
},
|
||||
showNotification(...args) {
|
||||
calls.push(['notification', ...args]);
|
||||
},
|
||||
};
|
||||
|
||||
await suggestDownloadSubfolderFromContextMenu.call(dialog, {
|
||||
context_scope: 'download_table',
|
||||
source: 'civitai',
|
||||
missing_key: 'missing-19-2',
|
||||
base_model: 'Krea 2',
|
||||
tags: ['concept'],
|
||||
});
|
||||
|
||||
assert.equal(calls[0][0], 'suggest');
|
||||
assert.notEqual(calls[0][1], missing);
|
||||
assert.equal(calls[0][1].download_source.base_model, 'Krea 2');
|
||||
assert.deepEqual(calls[0][1].download_source.tags, ['concept']);
|
||||
assert.deepEqual(calls[0].slice(2), [categoryEl, subfolderEl]);
|
||||
assert.equal(calls[1][0], 'refresh');
|
||||
assert.equal(calls[1][2].download_source.base_model, 'Krea 2');
|
||||
assert.deepEqual(calls[1].slice(3), [categoryEl, subfolderEl]);
|
||||
assert.deepEqual(calls[2], [
|
||||
'notification',
|
||||
'Suggested subfolder applied: Krea 2/Concept',
|
||||
'success',
|
||||
]);
|
||||
});
|
||||
|
||||
test('context menu dispatches Suggest Subfolder without a silent optional handler', async () => {
|
||||
const handleContextMenuAction = eval(`(${extractMethod(modelInfoMethodsSource, 'handleContextMenuAction')})`);
|
||||
const model = {
|
||||
context_scope: 'download_table',
|
||||
source: 'civarchive',
|
||||
name: 'Cutifyier v2',
|
||||
};
|
||||
const calls = [];
|
||||
const dialog = {
|
||||
_contextMenuModel: model,
|
||||
hideContextMenu() {},
|
||||
suggestDownloadSubfolderFromContextMenu(value) {
|
||||
calls.push(value);
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
|
||||
handleContextMenuAction.call(dialog, 'suggestSubfolder');
|
||||
await Promise.resolve();
|
||||
|
||||
assert.deepEqual(calls, [model]);
|
||||
});
|
||||
|
||||
test('clicked Krea 2 concept metadata resolves to the matching LoRA subfolder', () => {
|
||||
const normalizeFolderToken = eval(`(${extractMethod(downloadTargetMethodsSource, 'normalizeFolderToken')})`);
|
||||
const getSuggestedLoraSubfolder = eval(`(${extractMethod(downloadTargetMethodsSource, 'getSuggestedLoraSubfolder')})`);
|
||||
const dialog = {
|
||||
normalizeFolderToken,
|
||||
normalizeDownloadCategory(value = '') {
|
||||
return String(value || '').toLowerCase();
|
||||
},
|
||||
getCachedSearchSuggestionData() {
|
||||
return {};
|
||||
},
|
||||
getCompatibleCivitaiSearchResult() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
const suggestion = getSuggestedLoraSubfolder.call(dialog, {
|
||||
download_source: {
|
||||
base_model: 'Krea 2',
|
||||
tags: ['concept'],
|
||||
},
|
||||
}, 'loras', [{
|
||||
value: 'KREA2/concept',
|
||||
label: 'KREA2 / concept',
|
||||
baseDirectory: '',
|
||||
segments: ['KREA2', 'concept'],
|
||||
normalizedSegments: ['krea2', 'concept'],
|
||||
}]);
|
||||
|
||||
assert.equal(suggestion.value, 'KREA2/concept');
|
||||
assert.equal(suggestion.matchedBaseModel, 'Krea 2');
|
||||
assert.equal(suggestion.matchedTag, 'concept');
|
||||
});
|
||||
|
||||
test('queued workflow model selection survives missing data and completes after analysis', () => {
|
||||
const queueWorkflowModelReferenceSelection = eval(`(${extractMethod(missingBrowserMethodsSource, 'queueWorkflowModelReferenceSelection')})`);
|
||||
const applyPendingWorkflowModelSelection = eval(`(${extractMethod(missingBrowserMethodsSource, 'applyPendingWorkflowModelSelection')})`);
|
||||
|
||||
@@ -134,6 +134,15 @@ export class ResolverManagerDialog extends ComfyDialog {
|
||||
}),
|
||||
$el("span", { textContent: "Show More" })
|
||||
]),
|
||||
$el("div.mr-context-menu-item.mr-context-menu-action-suggest-subfolder", {
|
||||
"data-menu-action": "suggestSubfolder",
|
||||
onclick: () => this.handleContextMenuAction('suggestSubfolder')
|
||||
}, [
|
||||
$el("span.mr-context-menu-item-icon", {
|
||||
innerHTML: getSvgIcon('lightbulb', 'currentColor', 'mr-context-menu-item-svg')
|
||||
}),
|
||||
$el("span", { textContent: "Suggest Subfolder" })
|
||||
]),
|
||||
$el("div.mr-context-menu-divider.mr-context-menu-divider-source", {
|
||||
"data-menu-divider": "source"
|
||||
}),
|
||||
|
||||
@@ -2091,7 +2091,7 @@ export const downloadTargetMethods = {
|
||||
},
|
||||
|
||||
async forceSuggestedDownloadSubfolder(missing, categoryEl, subfolderEl) {
|
||||
if (!categoryEl || !subfolderEl) return;
|
||||
if (!categoryEl || !subfolderEl) return null;
|
||||
|
||||
const saved = this.getSavedDownloadTargetSelection(missing);
|
||||
const originalCategory = this.normalizeDownloadCategory(this.getDropdownValue(categoryEl));
|
||||
@@ -2114,7 +2114,7 @@ export const downloadTargetMethods = {
|
||||
|
||||
if (!suggestion) {
|
||||
this.showNotification?.('No subfolder suggestion available for this model.', 'info');
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
this.setDropdownValue(categoryEl, category, this.getCategoryDisplayName(category));
|
||||
@@ -2130,6 +2130,102 @@ export const downloadTargetMethods = {
|
||||
...this.getSubfolderSuggestionTrackingPatch(suggestion, 'button')
|
||||
});
|
||||
this.syncDownloadTargetFolderContext(categoryEl, subfolderEl);
|
||||
return {
|
||||
category,
|
||||
subfolder: suggestedSubfolder,
|
||||
suggestion
|
||||
};
|
||||
},
|
||||
|
||||
canSuggestDownloadSubfolderFromContextMenu(model = {}) {
|
||||
const source = String(model.details_source || model.source || '')
|
||||
.toLowerCase()
|
||||
.replace(/[-\s]+/g, '_');
|
||||
return model.context_scope === 'download_table'
|
||||
&& ['civitai', 'civarchive'].includes(source);
|
||||
},
|
||||
|
||||
getContextMenuDownloadMissing(model = {}) {
|
||||
const missingModels = Array.isArray(this.missingModels) ? this.missingModels : [];
|
||||
const missingKey = String(model.missing_key || '');
|
||||
if (missingKey) {
|
||||
const keyedMatch = missingModels.find(missing => (
|
||||
String(this.getMissingModelKey?.(missing) || '') === missingKey
|
||||
));
|
||||
if (keyedMatch) return keyedMatch;
|
||||
}
|
||||
|
||||
return missingModels.find(missing => (
|
||||
String(missing.node_id ?? '') === String(model.node_id ?? '')
|
||||
&& String(missing.widget_index ?? '') === String(model.widget_index ?? '')
|
||||
&& String(missing.subgraph_id || '') === String(model.subgraph_id || '')
|
||||
&& (missing.is_top_level !== false) === (model.is_top_level !== false)
|
||||
)) || null;
|
||||
},
|
||||
|
||||
async suggestDownloadSubfolderFromContextMenu(model = {}) {
|
||||
if (!this.canSuggestDownloadSubfolderFromContextMenu(model)) {
|
||||
console.warn('Model Resolver: ignored Suggest Subfolder for an unsupported context.', {
|
||||
contextScope: model.context_scope || '',
|
||||
source: model.details_source || model.source || ''
|
||||
});
|
||||
this.showNotification?.('Subfolder suggestions are available here only for CivitAI and CivArchive results.', 'info');
|
||||
return null;
|
||||
}
|
||||
|
||||
const missing = this.getContextMenuDownloadMissing(model);
|
||||
if (!missing) {
|
||||
console.warn('Model Resolver: could not match the context-menu result to a missing model.', {
|
||||
missingKey: model.missing_key || '',
|
||||
nodeId: model.node_id ?? '',
|
||||
widgetIndex: model.widget_index ?? ''
|
||||
});
|
||||
this.showNotification?.('Could not find the model download controls.', 'info');
|
||||
return null;
|
||||
}
|
||||
|
||||
const categoryEl = this.contentElement?.querySelector(
|
||||
`#download-category-${missing.node_id}-${missing.widget_index}`
|
||||
);
|
||||
const subfolderEl = this.contentElement?.querySelector(
|
||||
`#download-subfolder-${missing.node_id}-${missing.widget_index}`
|
||||
);
|
||||
if (!categoryEl || !subfolderEl) {
|
||||
console.warn('Model Resolver: download target controls were not found for Suggest Subfolder.', {
|
||||
nodeId: missing.node_id ?? '',
|
||||
widgetIndex: missing.widget_index ?? ''
|
||||
});
|
||||
this.showNotification?.('Could not find the model download controls.', 'info');
|
||||
return null;
|
||||
}
|
||||
|
||||
const sourceMetadata = {
|
||||
...model,
|
||||
source: model.details_source || model.source || ''
|
||||
};
|
||||
const suggestionMissing = {
|
||||
...missing,
|
||||
download_source: {
|
||||
...(missing.download_source || {}),
|
||||
...sourceMetadata
|
||||
}
|
||||
};
|
||||
const applied = await this.forceSuggestedDownloadSubfolder(
|
||||
suggestionMissing,
|
||||
categoryEl,
|
||||
subfolderEl
|
||||
);
|
||||
const container = categoryEl.closest?.('.mr-download-target') || this.contentElement;
|
||||
this.refreshDownloadTargetHelp(container, suggestionMissing, categoryEl, subfolderEl);
|
||||
if (applied?.subfolder) {
|
||||
console.info('Model Resolver: applied context-menu subfolder suggestion.', {
|
||||
category: applied.category,
|
||||
subfolder: applied.subfolder,
|
||||
source: sourceMetadata.source
|
||||
});
|
||||
this.showNotification?.(`Suggested subfolder applied: ${applied.subfolder}`, 'success');
|
||||
}
|
||||
return applied;
|
||||
},
|
||||
|
||||
applySearchResultSuggestion(missing) {
|
||||
|
||||
@@ -1996,6 +1996,10 @@ export const searchPanelMethods = {
|
||||
name: modelName,
|
||||
filename: downloadFilename,
|
||||
missing_key: this.getMissingModelKey(missing),
|
||||
node_id: missing.node_id,
|
||||
widget_index: missing.widget_index,
|
||||
subgraph_id: missing.subgraph_id || '',
|
||||
is_top_level: missing.is_top_level !== false,
|
||||
category: rowCategory
|
||||
}
|
||||
: null;
|
||||
|
||||
@@ -41,6 +41,7 @@ export const modelInfoMethods = {
|
||||
const isLocalModelContext = model?.context_scope === 'local_model' || model?.context_scope === 'local_match';
|
||||
const isFolderOnlyContext = isDownloadFolderContext || isDownloadRootContext;
|
||||
const isSourceModelContext = !isDownloadTableContext && !isFolderOnlyContext && !isDownloadQueueContext && !isDownloadHistoryContext;
|
||||
const showSuggestSubfolder = this.canSuggestDownloadSubfolderFromContextMenu?.(model) || false;
|
||||
const hasLocalPath = Boolean(model?.open_path || model?.folder_path || model?.download_directory || model?.directory || model?.path || model?.resolved_path);
|
||||
const showOpenFolder = !isDownloadTableContext && hasLocalPath;
|
||||
const showCompareHashes = (isLocalModelContext || isDownloadFolderContext)
|
||||
@@ -53,13 +54,14 @@ export const modelInfoMethods = {
|
||||
&& model?.node_id !== '';
|
||||
this.setContextMenuItemVisible('showInfo', isSourceModelContext);
|
||||
this.setContextMenuItemVisible('showMore', canShowMore);
|
||||
this.setContextMenuItemVisible('suggestSubfolder', showSuggestSubfolder);
|
||||
this.setContextMenuItemVisible('source', showSourceLink);
|
||||
this.setContextMenuItemVisible('civitai', false);
|
||||
this.setContextMenuItemVisible('locateNode', showLocateNode);
|
||||
this.setContextMenuItemVisible('switchWorkflow', showSwitchWorkflow);
|
||||
this.setContextMenuItemVisible('compareHashes', showCompareHashes);
|
||||
this.setContextMenuItemVisible('openFolder', showOpenFolder);
|
||||
this.setContextMenuDividerVisible('source', showSourceLink && (isSourceModelContext || canShowMore));
|
||||
this.setContextMenuDividerVisible('source', showSourceLink && (isSourceModelContext || canShowMore || showSuggestSubfolder));
|
||||
this.setContextMenuDividerVisible(
|
||||
'workflow',
|
||||
(showLocateNode || showSwitchWorkflow) && (isSourceModelContext || canShowMore || showSourceLink)
|
||||
@@ -126,6 +128,22 @@ export const modelInfoMethods = {
|
||||
this.showModelInfo(model);
|
||||
} else if (action === 'showMore') {
|
||||
this.showSourceModelDetails(model);
|
||||
} else if (action === 'suggestSubfolder') {
|
||||
if (typeof this.suggestDownloadSubfolderFromContextMenu !== 'function') {
|
||||
console.error('Model Resolver: Suggest Subfolder handler is unavailable.');
|
||||
this.showNotification?.('Suggest Subfolder is unavailable. Reload ComfyUI and try again.', 'error');
|
||||
return;
|
||||
}
|
||||
console.info('Model Resolver: Suggest Subfolder requested from the download result context menu.', {
|
||||
source: model.details_source || model.source || '',
|
||||
model: model.name || model.filename || '',
|
||||
baseModel: model.base_model || model.baseModel || '',
|
||||
tags: model.tags || []
|
||||
});
|
||||
Promise.resolve(this.suggestDownloadSubfolderFromContextMenu(model)).catch(error => {
|
||||
console.error('Model Resolver: context-menu subfolder suggestion failed:', error);
|
||||
this.showNotification?.('Could not apply a subfolder suggestion.', 'error');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -451,6 +469,7 @@ export const modelInfoMethods = {
|
||||
const isDownloadHistoryContext = model?.context_scope === 'download_history';
|
||||
const isFolderOnlyContext = isDownloadFolderContext || isDownloadRootContext;
|
||||
const isSourceModelContext = !isDownloadTableContext && !isFolderOnlyContext && !isDownloadQueueContext && !isDownloadHistoryContext;
|
||||
const showSuggestSubfolder = this.canSuggestDownloadSubfolderFromContextMenu?.(model) || false;
|
||||
const hasLocalPath = Boolean(model?.open_path || model?.folder_path || model?.download_directory || model?.directory || model?.path || model?.resolved_path);
|
||||
const showOpenFolder = !isDownloadTableContext && hasLocalPath;
|
||||
const showCompareHashes = (model?.context_scope === 'local_model' || model?.context_scope === 'local_match' || isDownloadFolderContext)
|
||||
@@ -459,9 +478,10 @@ export const modelInfoMethods = {
|
||||
const showSwitchWorkflow = (isDownloadQueueContext || isDownloadHistoryContext) && Boolean(this.canSwitchToDownloadWorkflow?.(model));
|
||||
const showSourceLink = Boolean(sourceLink?.url);
|
||||
|
||||
this.setContextMenuItemVisible('suggestSubfolder', showSuggestSubfolder);
|
||||
this.setContextMenuItemVisible('source', showSourceLink);
|
||||
this.setContextMenuItemVisible('civitai', false);
|
||||
this.setContextMenuDividerVisible('source', showSourceLink && (isSourceModelContext || canShowMore));
|
||||
this.setContextMenuDividerVisible('source', showSourceLink && (isSourceModelContext || canShowMore || showSuggestSubfolder));
|
||||
this.setContextMenuDividerVisible('workflow', showSwitchWorkflow && (isSourceModelContext || canShowMore || showSourceLink));
|
||||
this.setContextMenuDividerVisible('folder', (showCompareHashes || showOpenFolder) && (isSourceModelContext || canShowMore || showSourceLink || showSwitchWorkflow));
|
||||
this.updateContextMenuSourceItem(sourceLink);
|
||||
|
||||
Reference in New Issue
Block a user