mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-21 03:01:27 -03:00
feat(sidebar): rename folders from the sidebar (#999)
Follows the folder create/delete work: a typo'd directory could be
removed but not corrected, and for a folder holding models the only fix
was to move every model out by hand.
Adds POST /api/lm/{prefix}/rename-folder. Unlike the delete path this one
deliberately works on folders that hold models — a rename keeps every
file, so nothing is cascaded over: the directory is renamed on disk and
the scanner re-keys the records that pointed at the old prefix (recorded
folder list, cache file_path/folder/preview_url, hash and autov3 index
paths, excluded-model paths, and the metadata sidecars that travelled
with the directory). Ancestors are never touched, and only the leaf name
is accepted so a rename can never escape its parent.
Library roots, top-level symlinks and folders holding a staged delete are
refused; the last because a staging manifest records absolute
original/staged paths, so moving it would break undo and purge. A name
collision is a 409 target_exists conflict.
The sidebar reuses the inline-row idiom from folder creation: prefilled
with the current name, inserted in place of the node with that node
hidden while editing, Enter confirms and Escape/blur cancels. The
persisted selection and the expanded set are re-keyed across the rename
so the user keeps their place in the refreshed tree.
This commit is contained in:
@@ -11,6 +11,7 @@ class FakeMoveService:
|
||||
self._result = result
|
||||
self.received_path = None
|
||||
self.received_dry_run = None
|
||||
self.received_new_name = None
|
||||
|
||||
async def create_folder(self, folder_path):
|
||||
self.received_path = folder_path
|
||||
@@ -21,6 +22,11 @@ class FakeMoveService:
|
||||
self.received_dry_run = dry_run
|
||||
return self._result
|
||||
|
||||
async def rename_folder(self, folder_path, new_name):
|
||||
self.received_path = folder_path
|
||||
self.received_new_name = new_name
|
||||
return self._result
|
||||
|
||||
|
||||
class FakeRequest:
|
||||
def __init__(self, payload):
|
||||
@@ -212,3 +218,116 @@ async def test_delete_folder_invalid_json_body():
|
||||
|
||||
assert response.status == 400
|
||||
assert json.loads(response.text)["success"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rename_folder_success():
|
||||
handler, service = _make_handler(
|
||||
{
|
||||
"success": True,
|
||||
"renamed": True,
|
||||
"folder": "characters/animation",
|
||||
"previous_folder": "characters/anime",
|
||||
}
|
||||
)
|
||||
|
||||
response = await handler.rename_folder(
|
||||
FakeRequest(
|
||||
{"folder_path": "/library/characters/anime", "new_name": "animation"}
|
||||
)
|
||||
)
|
||||
|
||||
assert response.status == 200
|
||||
payload = json.loads(response.text)
|
||||
assert payload["success"] is True
|
||||
assert payload["folder"] == "characters/animation"
|
||||
assert service.received_path == "/library/characters/anime"
|
||||
assert service.received_new_name == "animation"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rename_folder_missing_path():
|
||||
handler, service = _make_handler({"success": True})
|
||||
|
||||
response = await handler.rename_folder(FakeRequest({"new_name": "animation"}))
|
||||
|
||||
assert response.status == 400
|
||||
assert json.loads(response.text)["success"] is False
|
||||
assert service.received_path is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rename_folder_missing_name():
|
||||
handler, service = _make_handler({"success": True})
|
||||
|
||||
response = await handler.rename_folder(
|
||||
FakeRequest({"folder_path": "/library/characters/anime"})
|
||||
)
|
||||
|
||||
assert response.status == 400
|
||||
payload = json.loads(response.text)
|
||||
assert payload["success"] is False
|
||||
assert service.received_new_name is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rename_folder_target_exists_maps_to_409():
|
||||
handler, _service = _make_handler(
|
||||
{
|
||||
"success": False,
|
||||
"code": "target_exists",
|
||||
"error": 'A folder named "animation" already exists here',
|
||||
}
|
||||
)
|
||||
|
||||
response = await handler.rename_folder(
|
||||
FakeRequest(
|
||||
{"folder_path": "/library/characters/anime", "new_name": "animation"}
|
||||
)
|
||||
)
|
||||
|
||||
assert response.status == 409
|
||||
payload = json.loads(response.text)
|
||||
assert payload["code"] == "target_exists"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rename_folder_busy_maps_to_409():
|
||||
handler, _service = _make_handler(
|
||||
{"success": False, "code": "busy", "error": "staged delete pending"}
|
||||
)
|
||||
|
||||
response = await handler.rename_folder(
|
||||
FakeRequest({"folder_path": "/library/full", "new_name": "renamed"})
|
||||
)
|
||||
|
||||
assert response.status == 409
|
||||
assert json.loads(response.text)["code"] == "busy"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rename_folder_invalid_name_maps_to_400():
|
||||
handler, _service = _make_handler(
|
||||
{"success": False, "error": "Invalid characters in folder name"}
|
||||
)
|
||||
|
||||
response = await handler.rename_folder(
|
||||
FakeRequest({"folder_path": "/library/full", "new_name": "a/b"})
|
||||
)
|
||||
|
||||
assert response.status == 400
|
||||
assert json.loads(response.text)["success"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rename_folder_invalid_json_body():
|
||||
class BadJsonRequest:
|
||||
async def json(self):
|
||||
raise ValueError("bad json")
|
||||
|
||||
handler, _service = _make_handler({"success": True})
|
||||
|
||||
response = await handler.rename_folder(BadJsonRequest())
|
||||
|
||||
assert response.status == 400
|
||||
assert json.loads(response.text)["success"] is False
|
||||
|
||||
Reference in New Issue
Block a user