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>
Signed| @@ -765,6 +765,48 @@ async function onSaveStyleClick() { | ||
| 765 | 765 | saveSettingsDebounced(); |
| 766 | 766 | } |
| 767 | 767 | |
| 768 | +async 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 | 811 | * Modifies prompt based on user inputs. |
| 770 | 812 | * @param {string} prompt Prompt to refine |
| @@ -4728,6 +4770,58 @@ async function onComfyDeleteWorkflowClick() { | ||
| 4728 | 4770 | onComfyWorkflowChange(); |
| 4729 | 4771 | } |
| 4730 | 4772 | |
| 4773 | +async 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 | 4826 | * Sends a chat message with the generated image. |
| 4733 | 4827 | * @param {string} prompt Prompt used for the image generation |
| @@ -5601,9 +5695,11 @@ jQuery(async () => { | ||
| 5601 | 5695 | $('#sd_comfy_workflow').on('change', onComfyWorkflowChange); |
| 5602 | 5696 | $('#sd_comfy_open_workflow_editor').on('click', onComfyOpenWorkflowEditorClick); |
| 5603 | 5697 | $('#sd_comfy_new_workflow').on('click', onComfyNewWorkflowClick); |
| 5698 | + $('#sd_comfy_rename_workflow').on('click', onComfyRenameWorkflowClick); | |
| 5604 | 5699 | $('#sd_comfy_delete_workflow').on('click', onComfyDeleteWorkflowClick); |
| 5605 | 5700 | $('#sd_style').on('change', onStyleSelect); |
| 5606 | 5701 | $('#sd_save_style').on('click', onSaveStyleClick); |
| 5702 | + $('#sd_rename_style').on('click', onRenameStyleClick); | |
| 5607 | 5703 | $('#sd_delete_style').on('click', onDeleteStyleClick); |
| 5608 | 5704 | $('#sd_character_prompt_block').hide(); |
| 5609 | 5705 | $('#sd_interactive_mode').on('input', onInteractiveModeInput); |
| @@ -275,6 +275,9 @@ | ||
| 275 | 275 | <div id="sd_comfy_new_workflow" class="menu_button menu_button_icon" data-i18n="[title]Create new workflow" title="Create new workflow"> |
| 276 | 276 | <i class="fa-solid fa-plus"></i> |
| 277 | 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 | 281 | <div id="sd_comfy_delete_workflow" class="menu_button menu_button_icon" data-i18n="[title]Delete workflow" title="Delete workflow"> |
| 279 | 282 | <i class="fa-solid fa-trash-can"></i> |
| 280 | 283 | </div> |
| @@ -572,6 +575,9 @@ | ||
| 572 | 575 | <div id="sd_save_style" data-i18n="[title]Save style" title="Save style" class="menu_button"> |
| 573 | 576 | <i class="fa-solid fa-save"></i> |
| 574 | 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 | 581 | <div id="sd_delete_style" data-i18n="[title]Delete style" title="Delete style" class="menu_button"> |
| 576 | 582 | <i class="fa-solid fa-trash-can"></i> |
| 577 | 583 | </div> |
| @@ -12,6 +12,7 @@ import mime from 'mime-types'; | ||
| 12 | 12 | |
| 13 | 13 | import { delay, getBasicAuthHeader, isValidUrl, tryParse } from '../util.js'; |
| 14 | 14 | import { readSecret, SECRET_KEYS } from './secrets.js'; |
| 15 | +import { getFileNameValidationFunction } from '../middleware/validateFileName.js'; | |
| 15 | 16 | import { AIMLAPI_HEADERS } from '../constants.js'; |
| 16 | 17 | |
| 17 | 18 | /** |
| @@ -532,6 +533,34 @@ comfy.post('/delete-workflow', async (request, response) => { | ||
| 532 | 533 | } |
| 533 | 534 | }); |
| 534 | 535 | |
| 536 | +comfy.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 | + | |
| 535 | 564 | comfy.post('/generate', async (request, response) => { |
| 536 | 565 | try { |
| 537 | 566 | let item; |