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 | saveSettingsDebounced(); | 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 | * Modifies prompt based on user inputs. | 811 | * Modifies prompt based on user inputs. |
| 770 | * @param {string} prompt Prompt to refine | 812 | * @param {string} prompt Prompt to refine |
| @@ -4728,6 +4770,58 @@ async function onComfyDeleteWorkflowClick() { | |||
| 4728 | onComfyWorkflowChange(); | 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 | * 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 generation | 4827 | * @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); |
| @@ -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> |
| @@ -12,6 +12,7 @@ import mime from 'mime-types'; | |||
| 12 | 12 | ||
| 13 | import { delay, getBasicAuthHeader, isValidUrl, tryParse } from '../util.js'; | 13 | import { delay, getBasicAuthHeader, isValidUrl, tryParse } from '../util.js'; |
| 14 | import { readSecret, SECRET_KEYS } from './secrets.js'; | 14 | import { readSecret, SECRET_KEYS } from './secrets.js'; |
| 15 | import { getFileNameValidationFunction } from '../middleware/validateFileName.js'; | ||
| 15 | import { AIMLAPI_HEADERS } from '../constants.js'; | 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 | comfy.post('/generate', async (request, response) => { | 564 | comfy.post('/generate', async (request, response) => { |
| 536 | try { | 565 | try { |
| 537 | let item; | 566 | let item; |