Add rename buttons for ComfyUI workflows and style presets (#5124) * Initial plan * Add rename buttons for ComfyUI workflows and style presets Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * Address review feedback: pencil icon, filename validation, 204 status, error prefix, frontend duplicate check, re-render styles select Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * fix: ensure existing workflow check only considers HTMLOptionElement instances --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com>

23ba3e5bb2e7aad59d0912b2b026a14052936f1b

Copilot <198982749+Copilot@users.noreply.github.com>

Signed
3 files changed, +131 -0Showing whitespace changes
public/scripts/extensions/stable-diffusion/index.js+96 -0
@@ -765,6 +765,48 @@ async function onSaveStyleClick() {
765 saveSettingsDebounced();765 saveSettingsDebounced();
766}766}
767767
768async function onRenameStyleClick() {
769 const selectedStyle = extension_settings.sd.style;
770 const styleObject = extension_settings.sd.styles.find(x => x.name === selectedStyle);
771
772 if (!styleObject) {
773 return;
774 }
775
776 const newName = await callGenericPopup(t`Enter new style name:`, POPUP_TYPE.INPUT, selectedStyle);
777
778 if (!newName) {
779 return;
780 }
781
782 const name = String(newName).trim();
783
784 if (name === selectedStyle) {
785 return;
786 }
787
788 const existingStyle = extension_settings.sd.styles.find(x => x.name === name);
789
790 if (existingStyle) {
791 toastr.error(t`A style with that name already exists`);
792 return;
793 }
794
795 styleObject.name = name;
796 extension_settings.sd.style = name;
797
798 $('#sd_style').empty();
799 for (const style of extension_settings.sd.styles) {
800 const option = document.createElement('option');
801 option.value = style.name;
802 option.text = style.name;
803 option.selected = style.name === extension_settings.sd.style;
804 $('#sd_style').append(option);
805 }
806
807 saveSettingsDebounced();
808}
809
768/**810/**
769 * Modifies prompt based on user inputs.811 * Modifies prompt based on user inputs.
770 * @param {string} prompt Prompt to refine812 * @param {string} prompt Prompt to refine
@@ -4728,6 +4770,58 @@ async function onComfyDeleteWorkflowClick() {
4728 onComfyWorkflowChange();4770 onComfyWorkflowChange();
4729}4771}
47304772
4773async function onComfyRenameWorkflowClick() {
4774 const oldName = extension_settings.sd.comfy_workflow;
4775
4776 if (!oldName) {
4777 return;
4778 }
4779
4780 let newName = await callGenericPopup(t`Enter new workflow name:`, POPUP_TYPE.INPUT, oldName);
4781
4782 if (!newName) {
4783 return;
4784 }
4785
4786 newName = String(newName).trim();
4787
4788 if (!newName.toLowerCase().endsWith('.json')) {
4789 newName += '.json';
4790 }
4791
4792 if (newName === oldName) {
4793 return;
4794 }
4795
4796 const existingWorkflow = Array
4797 .from(document.querySelectorAll('#sd_comfy_workflow option'))
4798 .find(opt => opt instanceof HTMLOptionElement && opt.value === newName);
4799
4800 if (existingWorkflow) {
4801 toastr.warning(t`A workflow with that name already exists`);
4802 return;
4803 }
4804
4805 const response = await fetch('/api/sd/comfy/rename-workflow', {
4806 method: 'POST',
4807 headers: getRequestHeaders(),
4808 body: JSON.stringify({
4809 old_name: oldName,
4810 new_name: newName,
4811 }),
4812 });
4813
4814 if (!response.ok) {
4815 const text = await response.text();
4816 toastr.error(t`Failed to rename workflow.\n\n${text}`);
4817 return;
4818 }
4819
4820 extension_settings.sd.comfy_workflow = newName;
4821 saveSettingsDebounced();
4822 await loadComfyWorkflows();
4823}
4824
4731/**4825/**
4732 * Sends a chat message with the generated image.4826 * Sends a chat message with the generated image.
4733 * @param {string} prompt Prompt used for the image generation4827 * @param {string} prompt Prompt used for the image generation
@@ -5601,9 +5695,11 @@ jQuery(async () => {
5601 $('#sd_comfy_workflow').on('change', onComfyWorkflowChange);5695 $('#sd_comfy_workflow').on('change', onComfyWorkflowChange);
5602 $('#sd_comfy_open_workflow_editor').on('click', onComfyOpenWorkflowEditorClick);5696 $('#sd_comfy_open_workflow_editor').on('click', onComfyOpenWorkflowEditorClick);
5603 $('#sd_comfy_new_workflow').on('click', onComfyNewWorkflowClick);5697 $('#sd_comfy_new_workflow').on('click', onComfyNewWorkflowClick);
5698 $('#sd_comfy_rename_workflow').on('click', onComfyRenameWorkflowClick);
5604 $('#sd_comfy_delete_workflow').on('click', onComfyDeleteWorkflowClick);5699 $('#sd_comfy_delete_workflow').on('click', onComfyDeleteWorkflowClick);
5605 $('#sd_style').on('change', onStyleSelect);5700 $('#sd_style').on('change', onStyleSelect);
5606 $('#sd_save_style').on('click', onSaveStyleClick);5701 $('#sd_save_style').on('click', onSaveStyleClick);
5702 $('#sd_rename_style').on('click', onRenameStyleClick);
5607 $('#sd_delete_style').on('click', onDeleteStyleClick);5703 $('#sd_delete_style').on('click', onDeleteStyleClick);
5608 $('#sd_character_prompt_block').hide();5704 $('#sd_character_prompt_block').hide();
5609 $('#sd_interactive_mode').on('input', onInteractiveModeInput);5705 $('#sd_interactive_mode').on('input', onInteractiveModeInput);
public/scripts/extensions/stable-diffusion/settings.html+6 -0
@@ -275,6 +275,9 @@
275 <div id="sd_comfy_new_workflow" class="menu_button menu_button_icon" data-i18n="[title]Create new workflow" title="Create new workflow">275 <div id="sd_comfy_new_workflow" class="menu_button menu_button_icon" data-i18n="[title]Create new workflow" title="Create new workflow">
276 <i class="fa-solid fa-plus"></i>276 <i class="fa-solid fa-plus"></i>
277 </div>277 </div>
278 <div id="sd_comfy_rename_workflow" class="menu_button menu_button_icon" data-i18n="[title]Rename workflow" title="Rename workflow">
279 <i class="fa-solid fa-pencil"></i>
280 </div>
278 <div id="sd_comfy_delete_workflow" class="menu_button menu_button_icon" data-i18n="[title]Delete workflow" title="Delete workflow">281 <div id="sd_comfy_delete_workflow" class="menu_button menu_button_icon" data-i18n="[title]Delete workflow" title="Delete workflow">
279 <i class="fa-solid fa-trash-can"></i>282 <i class="fa-solid fa-trash-can"></i>
280 </div>283 </div>
@@ -572,6 +575,9 @@
572 <div id="sd_save_style" data-i18n="[title]Save style" title="Save style" class="menu_button">575 <div id="sd_save_style" data-i18n="[title]Save style" title="Save style" class="menu_button">
573 <i class="fa-solid fa-save"></i>576 <i class="fa-solid fa-save"></i>
574 </div>577 </div>
578 <div id="sd_rename_style" data-i18n="[title]Rename style" title="Rename style" class="menu_button">
579 <i class="fa-solid fa-pencil"></i>
580 </div>
575 <div id="sd_delete_style" data-i18n="[title]Delete style" title="Delete style" class="menu_button">581 <div id="sd_delete_style" data-i18n="[title]Delete style" title="Delete style" class="menu_button">
576 <i class="fa-solid fa-trash-can"></i>582 <i class="fa-solid fa-trash-can"></i>
577 </div>583 </div>
src/endpoints/stable-diffusion.js+29 -0
@@ -12,6 +12,7 @@ import mime from 'mime-types';
1212
13import { delay, getBasicAuthHeader, isValidUrl, tryParse } from '../util.js';13import { delay, getBasicAuthHeader, isValidUrl, tryParse } from '../util.js';
14import { readSecret, SECRET_KEYS } from './secrets.js';14import { readSecret, SECRET_KEYS } from './secrets.js';
15import { getFileNameValidationFunction } from '../middleware/validateFileName.js';
15import { AIMLAPI_HEADERS } from '../constants.js';16import { AIMLAPI_HEADERS } from '../constants.js';
1617
17/**18/**
@@ -532,6 +533,34 @@ comfy.post('/delete-workflow', async (request, response) => {
532 }533 }
533});534});
534535
536comfy.post('/rename-workflow', getFileNameValidationFunction('old_name'), getFileNameValidationFunction('new_name'), async (request, response) => {
537 try {
538 const oldName = sanitize(String(request.body.old_name));
539 const newName = sanitize(String(request.body.new_name));
540
541 if (path.extname(oldName).toLowerCase() !== '.json' || path.extname(newName).toLowerCase() !== '.json') {
542 return response.status(400).send('Only JSON workflow files are allowed');
543 }
544
545 const oldPath = path.join(request.user.directories.comfyWorkflows, oldName);
546 const newPath = path.join(request.user.directories.comfyWorkflows, newName);
547
548 if (!fs.existsSync(oldPath)) {
549 return response.status(404).send('Workflow not found');
550 }
551
552 if (fs.existsSync(newPath)) {
553 return response.status(409).send('A workflow with that name already exists');
554 }
555
556 fs.renameSync(oldPath, newPath);
557 return response.sendStatus(204);
558 } catch (error) {
559 console.error('ComfyUI workflow rename failed', error);
560 return response.sendStatus(500);
561 }
562});
563
535comfy.post('/generate', async (request, response) => {564comfy.post('/generate', async (request, response) => {
536 try {565 try {
537 let item;566 let item;