diff --git a/client/src/pages/locations/functions.ts b/client/src/pages/locations/functions.ts index a500400..0091efc 100644 --- a/client/src/pages/locations/functions.ts +++ b/client/src/pages/locations/functions.ts @@ -33,3 +33,19 @@ export function useLocations(): string[] { } }, [query.data]); } + +export async function renameSpoolLocation(location: string, newName: string): Promise { + const response = await fetch(getAPIURL() + "/location/" + location, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: newName, + }), + }); + if (!response.ok) { + throw new Error("Network response was not ok"); + } + return await response.text(); +} diff --git a/client/src/pages/locations/index.tsx b/client/src/pages/locations/index.tsx index e8c61ba..50be7f4 100644 --- a/client/src/pages/locations/index.tsx +++ b/client/src/pages/locations/index.tsx @@ -1,9 +1,9 @@ import { IResourceComponentsProps, useInvalidate, useList, useNavigation, useTranslate } from "@refinedev/core"; -import { Button, theme } from "antd"; +import { Button, Input, theme } from "antd"; import dayjs from "dayjs"; import utc from "dayjs/plugin/utc"; import type { Identifier, XYCoord } from "dnd-core"; -import React, { useEffect, useMemo, useRef } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import { DndProvider, useDrag, useDrop } from "react-dnd"; import { HTML5Backend } from "react-dnd-html5-backend"; @@ -12,7 +12,7 @@ import { Link } from "react-router-dom"; import SpoolIcon from "../../components/spoolIcon"; import { useSetSetting } from "../../utils/querySettings"; import { ISpool } from "../spools/model"; -import { setSpoolLocation, useLocations } from "./functions"; +import { renameSpoolLocation, setSpoolLocation, useLocations } from "./functions"; import "./locations.css"; dayjs.extend(utc); @@ -130,6 +130,7 @@ function LocationContainer({ showDelete, onDelete, moveLocation, + onEditTitle, }: { index: number; title: string; @@ -137,7 +138,11 @@ function LocationContainer({ showDelete?: boolean; onDelete?: () => void; moveLocation: (dragIndex: number, hoverIndex: number) => void; + onEditTitle: (newTitle: string) => void; }) { + const [editTitle, setEditTitle] = useState(false); + const [newTitle, setNewTitle] = useState(title); + const ref = useRef(null); const [{ handlerId }, drop] = useDrop({ accept: ItemTypes.CONTAINER, @@ -189,6 +194,7 @@ function LocationContainer({ const [{ isDragging }, drag] = useDrag({ type: ItemTypes.CONTAINER, + canDrag: !editTitle, item: () => { return { title, index }; }, @@ -203,7 +209,28 @@ function LocationContainer({ return (

- {title} + {editTitle ? ( + setEditTitle(false)} + onChange={(e) => setNewTitle(e.target.value)} + onPressEnter={() => { + setEditTitle(false); + return onEditTitle(newTitle); + }} + /> + ) : ( + { + setNewTitle(title); + setEditTitle(true); + }} + > + {title} + + )} {showDelete &&

@@ -213,6 +240,7 @@ function LocationContainer({ function LocationMetaContainer() { const t = useTranslate(); + const invalidate = useInvalidate(); const settingsLocations = useLocations(); const setLocationsSetting = useSetSetting("locations"); @@ -266,7 +294,23 @@ function LocationMetaContainer() { const newLocs = [...locationsList]; newLocs.splice(dragIndex, 1); newLocs.splice(hoverIndex, 0, locationsList[dragIndex]); - console.log("newLocs", newLocs); + setLocationsSetting.mutate(newLocs); + }; + + const onEditTitle = async (location: string, newTitle: string) => { + if (newTitle == location) return; + if (newTitle == "") return; + if (locationsList.includes(newTitle)) return; + + // Update all spool locations in the database + if (spoolLocations[location] && spoolLocations[location].length > 0) { + await renameSpoolLocation(location, newTitle); + await invalidate({ resource: "spool", invalidates: ["list", "detail"] }); + } + + // Update the value in the settings + const newLocs = [...locationsList]; + newLocs[locationsList.indexOf(location)] = newTitle; setLocationsSetting.mutate(newLocs); }; @@ -284,6 +328,7 @@ function LocationMetaContainer() { setLocationsSetting.mutate(locationsList.filter((l) => l !== loc)); }} moveLocation={moveLocation} + onEditTitle={(newTitle: string) => onEditTitle(loc, newTitle)} /> ); }); diff --git a/client/src/pages/locations/locations.css b/client/src/pages/locations/locations.css index 391d09b..90bef7b 100644 --- a/client/src/pages/locations/locations.css +++ b/client/src/pages/locations/locations.css @@ -6,6 +6,7 @@ .loc-container { padding: 1em; width: 24em; + cursor: pointer; } .loc-container h3 { @@ -13,6 +14,7 @@ align-items: center; justify-content: space-between; width: 100%; + cursor: text; } .loc-container .loc-spools { diff --git a/spoolman/api/v1/other.py b/spoolman/api/v1/other.py index 345ea60..14f7128 100644 --- a/spoolman/api/v1/other.py +++ b/spoolman/api/v1/other.py @@ -4,6 +4,7 @@ import logging from typing import Annotated from fastapi import APIRouter, Depends +from pydantic import BaseModel, Field, RootModel from sqlalchemy.ext.asyncio import AsyncSession from spoolman.database import filament, spool @@ -123,3 +124,25 @@ async def find_locations( db: Annotated[AsyncSession, Depends(get_db_session)], ) -> list[str]: return await spool.find_locations(db=db) + + +class RenameLocationBody(BaseModel): + name: str = Field(description="The new name of the location.", min_length=1) + + +@router.patch( + "/location/{location}", + name="Rename location", + description="Rename a spool location. All spools in this location will be moved to the new location.", + response_model_exclude_none=True, + response_model=RootModel[str], +) +async def rename_location( + location: str, + *, + db: Annotated[AsyncSession, Depends(get_db_session)], + body: RenameLocationBody, +) -> str: + logger.info("Renaming location %s to %s", location, body.name) + await spool.rename_location(db=db, current_name=location, new_name=body.name) + return body.name diff --git a/spoolman/database/spool.py b/spoolman/database/spool.py index 254a85e..2f60fdc 100644 --- a/spoolman/database/spool.py +++ b/spoolman/database/spool.py @@ -462,3 +462,15 @@ async def reset_initial_weight(db: AsyncSession, spool_id: int, weight: float) - await db.commit() await spool_changed(spool, EventType.UPDATED) return spool + + +async def rename_location( + *, + db: AsyncSession, + current_name: str, + new_name: str, +) -> None: + """Rename all spools with the current location name to the new name.""" + await db.execute( + sqlalchemy.update(models.Spool).where(models.Spool.location == current_name).values(location=new_name), + )