Skip to content

Commit 8b7212c

Browse files
committed
Add room rename feature
1 parent 50a49b0 commit 8b7212c

4 files changed

Lines changed: 129 additions & 0 deletions

File tree

app/routers/rooms.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ class RoomCreate(BaseModel):
2929
name: str
3030

3131

32+
class RoomRename(BaseModel):
33+
name: str
34+
35+
3236
def _rooms_path():
3337
return settings.layout_file.parent / "rooms.json"
3438

@@ -86,6 +90,29 @@ async def create_room(body: RoomCreate) -> Room:
8690
return new_room
8791

8892

93+
@router.patch("/{room_id}", response_model=Room)
94+
async def rename_room(room_id: str, body: RoomRename) -> Room:
95+
"""Rename a room (updates display name; room ID is unchanged)."""
96+
if not ROOM_ID_RE.match(room_id):
97+
raise HTTPException(
98+
status_code=status.HTTP_400_BAD_REQUEST,
99+
detail="Invalid room ID",
100+
)
101+
name = body.name.strip()
102+
if not name:
103+
raise HTTPException(
104+
status_code=status.HTTP_400_BAD_REQUEST,
105+
detail="Room name cannot be empty",
106+
)
107+
rooms = _read_rooms()
108+
for room in rooms:
109+
if room.id == room_id:
110+
room.name = name
111+
_write_rooms(rooms)
112+
return room
113+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Room not found")
114+
115+
89116
@router.delete("/{room_id}", status_code=status.HTTP_204_NO_CONTENT)
90117
async def delete_room(room_id: str) -> None:
91118
"""Delete a room and its layout file. Cannot delete the last room."""

app/static/js/app.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ const DashboardApp = (() => {
249249
return api("POST", "/api/rooms", { name });
250250
}
251251

252+
async function apiRenameRoom(roomId, name) {
253+
return api("PATCH", `/api/rooms/${encodeURIComponent(roomId)}`, { name });
254+
}
255+
252256
async function apiDeleteRoom(roomId) {
253257
const res = await fetch(`/api/rooms/${encodeURIComponent(roomId)}`, { method: "DELETE" });
254258
if (!res.ok) {
@@ -528,6 +532,7 @@ const DashboardApp = (() => {
528532
}
529533

530534
function showNewRoomForm() {
535+
hideRenameRoomForm();
531536
const form = document.getElementById("new-room-form");
532537
if (form) {
533538
form.classList.remove("new-room-form--hidden");
@@ -543,6 +548,43 @@ const DashboardApp = (() => {
543548
if (input) input.value = "";
544549
}
545550

551+
function showRenameRoomForm() {
552+
hideNewRoomForm();
553+
const form = document.getElementById("rename-room-form");
554+
if (form) {
555+
form.classList.remove("new-room-form--hidden");
556+
const input = document.getElementById("rename-room-name");
557+
if (input) {
558+
const current = rooms.find((r) => r.id === currentRoomId);
559+
input.value = current ? current.name : "";
560+
input.focus();
561+
input.select();
562+
}
563+
}
564+
}
565+
566+
function hideRenameRoomForm() {
567+
const form = document.getElementById("rename-room-form");
568+
if (form) form.classList.add("new-room-form--hidden");
569+
const input = document.getElementById("rename-room-name");
570+
if (input) input.value = "";
571+
}
572+
573+
async function renameCurrentRoom() {
574+
const input = document.getElementById("rename-room-name");
575+
const name = input ? input.value.trim() : "";
576+
if (!name) return;
577+
try {
578+
const updated = await apiRenameRoom(currentRoomId, name);
579+
const idx = rooms.findIndex((r) => r.id === currentRoomId);
580+
if (idx !== -1) rooms[idx] = updated;
581+
hideRenameRoomForm();
582+
renderRoomSelector();
583+
} catch (err) {
584+
console.error("Failed to rename room:", err);
585+
}
586+
}
587+
546588
// ── Edit mode ──────────────────────────────────────────────────────
547589

548590
function enterEditMode() {
@@ -560,6 +602,7 @@ const DashboardApp = (() => {
560602
btnEdit.classList.remove("fab--hidden");
561603
grid.setStatic(true);
562604
hideNewRoomForm();
605+
hideRenameRoomForm();
563606

564607
try {
565608
await saveLayout(serializeLayout(), currentRoomId);
@@ -931,6 +974,23 @@ const DashboardApp = (() => {
931974
});
932975
}
933976

977+
const btnRoomRename = document.getElementById("btn-room-rename");
978+
if (btnRoomRename) btnRoomRename.addEventListener("click", showRenameRoomForm);
979+
980+
const btnRoomRenameSave = document.getElementById("btn-room-rename-save");
981+
if (btnRoomRenameSave) btnRoomRenameSave.addEventListener("click", renameCurrentRoom);
982+
983+
const btnRoomCancelRename = document.getElementById("btn-room-cancel-rename");
984+
if (btnRoomCancelRename) btnRoomCancelRename.addEventListener("click", hideRenameRoomForm);
985+
986+
const renameRoomNameInput = document.getElementById("rename-room-name");
987+
if (renameRoomNameInput) {
988+
renameRoomNameInput.addEventListener("keydown", (e) => {
989+
if (e.key === "Enter") renameCurrentRoom();
990+
if (e.key === "Escape") hideRenameRoomForm();
991+
});
992+
}
993+
934994
const btnRoomDelete = document.getElementById("btn-room-delete");
935995
if (btnRoomDelete) btnRoomDelete.addEventListener("click", deleteCurrentRoom);
936996

app/templates/index.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
<button id="btn-room-new" class="btn btn--small" title="Add a new room">
2929
<i class="mdi mdi-home-plus-outline"></i> New Room
3030
</button>
31+
<button id="btn-room-rename" class="btn btn--small" title="Rename this room">
32+
<i class="mdi mdi-pencil-outline"></i>
33+
</button>
3134
<button id="btn-room-delete" class="btn btn--small" title="Delete this room">
3235
<i class="mdi mdi-home-remove-outline"></i>
3336
</button>
@@ -37,6 +40,11 @@
3740
<button id="btn-room-create" class="btn btn--primary btn--small">Create</button>
3841
<button id="btn-room-cancel-new" class="btn btn--small">Cancel</button>
3942
</div>
43+
<div id="rename-room-form" class="new-room-form new-room-form--hidden">
44+
<input id="rename-room-name" type="text" class="toolbar__room-input" placeholder="Room name…" maxlength="40" />
45+
<button id="btn-room-rename-save" class="btn btn--primary btn--small">Save</button>
46+
<button id="btn-room-cancel-rename" class="btn btn--small">Cancel</button>
47+
</div>
4048
</div>
4149
<div class="toolbar__right">
4250
<select id="theme-select" class="btn" title="Theme">

tests/test_rooms.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,40 @@ def test_delete_room_invalid_id(client):
8080
assert res.status_code in (400, 404, 422)
8181

8282

83+
def test_rename_room(client):
84+
"""PATCH /api/rooms/{room_id} updates the room's display name."""
85+
new = client.post("/api/rooms", json={"name": "Old Name"}).json()
86+
room_id = new["id"]
87+
88+
res = client.patch(f"/api/rooms/{room_id}", json={"name": "New Name"})
89+
assert res.status_code == 200
90+
assert res.json()["name"] == "New Name"
91+
assert res.json()["id"] == room_id # ID is unchanged
92+
93+
# Name persists in listing
94+
rooms = client.get("/api/rooms").json()
95+
match = next(r for r in rooms if r["id"] == room_id)
96+
assert match["name"] == "New Name"
97+
98+
99+
def test_rename_room_empty_name(client):
100+
"""PATCH /api/rooms/{room_id} with empty name returns 400."""
101+
res = client.patch("/api/rooms/default", json={"name": " "})
102+
assert res.status_code == 400
103+
104+
105+
def test_rename_room_not_found(client):
106+
"""PATCH /api/rooms/{room_id} returns 404 for unknown room."""
107+
res = client.patch("/api/rooms/does_not_exist", json={"name": "Whatever"})
108+
assert res.status_code == 404
109+
110+
111+
def test_rename_room_invalid_id(client):
112+
"""PATCH /api/rooms/{room_id} returns 400 for invalid IDs."""
113+
res = client.patch("/api/rooms/../../etc/passwd", json={"name": "X"})
114+
assert res.status_code in (400, 404, 422)
115+
116+
83117
# ── Per-room layout isolation ────────────────────────────────────────
84118

85119

0 commit comments

Comments
 (0)