From 3c9a0343e98517ac5e06fa225e1ea40606b70cfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AD=8F=E9=A3=8E?= Date: Fri, 13 Mar 2026 11:46:15 +0800 Subject: [PATCH] Remove item management feature from frontend and backend, and add a pending skeleton component. --- backend/app/api/main.py | 3 +- backend/app/api/routes/items.py | 112 ------------ backend/app/api/routes/users.py | 4 +- backend/app/crud.py | 10 -- backend/app/models.py | 42 ----- backend/tests/api/routes/test_items.py | 164 ------------------ backend/tests/conftest.py | 4 +- backend/tests/utils/item.py | 16 -- frontend/src/components/Items/AddItem.tsx | 144 --------------- frontend/src/components/Items/DeleteItem.tsx | 94 ---------- frontend/src/components/Items/EditItem.tsx | 145 ---------------- .../src/components/Items/ItemActionsMenu.tsx | 34 ---- frontend/src/components/Items/columns.tsx | 73 -------- .../{PendingItems.tsx => PendingSkeleton.tsx} | 4 +- .../src/components/Sidebar/AppSidebar.tsx | 3 +- frontend/src/routeTree.gen.ts | 21 --- frontend/src/routes/_layout/items.tsx | 69 -------- frontend/src/routes/_layout/locations.tsx | 4 +- frontend/tests/items.spec.ts | 132 -------------- 19 files changed, 9 insertions(+), 1069 deletions(-) delete mode 100644 backend/app/api/routes/items.py delete mode 100644 backend/tests/api/routes/test_items.py delete mode 100644 backend/tests/utils/item.py delete mode 100644 frontend/src/components/Items/AddItem.tsx delete mode 100644 frontend/src/components/Items/DeleteItem.tsx delete mode 100644 frontend/src/components/Items/EditItem.tsx delete mode 100644 frontend/src/components/Items/ItemActionsMenu.tsx delete mode 100644 frontend/src/components/Items/columns.tsx rename frontend/src/components/Pending/{PendingItems.tsx => PendingSkeleton.tsx} (94%) delete mode 100644 frontend/src/routes/_layout/items.tsx delete mode 100644 frontend/tests/items.spec.ts diff --git a/backend/app/api/main.py b/backend/app/api/main.py index fcca221..4ea6cac 100644 --- a/backend/app/api/main.py +++ b/backend/app/api/main.py @@ -1,13 +1,12 @@ from fastapi import APIRouter -from app.api.routes import items, locations, login, private, users, utils +from app.api.routes import locations, login, private, users, utils from app.core.config import settings api_router = APIRouter() api_router.include_router(login.router) api_router.include_router(users.router) api_router.include_router(utils.router) -api_router.include_router(items.router) api_router.include_router(locations.router) diff --git a/backend/app/api/routes/items.py b/backend/app/api/routes/items.py deleted file mode 100644 index f1929e5..0000000 --- a/backend/app/api/routes/items.py +++ /dev/null @@ -1,112 +0,0 @@ -import uuid -from typing import Any - -from fastapi import APIRouter, HTTPException -from sqlmodel import col, func, select - -from app.api.deps import CurrentUser, SessionDep -from app.models import Item, ItemCreate, ItemPublic, ItemsPublic, ItemUpdate, Message - -router = APIRouter(prefix="/items", tags=["items"]) - - -@router.get("/", response_model=ItemsPublic) -def read_items( - session: SessionDep, current_user: CurrentUser, skip: int = 0, limit: int = 100 -) -> Any: - """ - Retrieve items. - """ - - if current_user.is_superuser: - count_statement = select(func.count()).select_from(Item) - count = session.exec(count_statement).one() - statement = ( - select(Item).order_by(col(Item.created_at).desc()).offset(skip).limit(limit) - ) - items = session.exec(statement).all() - else: - count_statement = ( - select(func.count()) - .select_from(Item) - .where(Item.owner_id == current_user.id) - ) - count = session.exec(count_statement).one() - statement = ( - select(Item) - .where(Item.owner_id == current_user.id) - .order_by(col(Item.created_at).desc()) - .offset(skip) - .limit(limit) - ) - items = session.exec(statement).all() - - return ItemsPublic(data=items, count=count) - - -@router.get("/{id}", response_model=ItemPublic) -def read_item(session: SessionDep, current_user: CurrentUser, id: uuid.UUID) -> Any: - """ - Get item by ID. - """ - item = session.get(Item, id) - if not item: - raise HTTPException(status_code=404, detail="Item not found") - if not current_user.is_superuser and (item.owner_id != current_user.id): - raise HTTPException(status_code=403, detail="Not enough permissions") - return item - - -@router.post("/", response_model=ItemPublic) -def create_item( - *, session: SessionDep, current_user: CurrentUser, item_in: ItemCreate -) -> Any: - """ - Create new item. - """ - item = Item.model_validate(item_in, update={"owner_id": current_user.id}) - session.add(item) - session.commit() - session.refresh(item) - return item - - -@router.put("/{id}", response_model=ItemPublic) -def update_item( - *, - session: SessionDep, - current_user: CurrentUser, - id: uuid.UUID, - item_in: ItemUpdate, -) -> Any: - """ - Update an item. - """ - item = session.get(Item, id) - if not item: - raise HTTPException(status_code=404, detail="Item not found") - if not current_user.is_superuser and (item.owner_id != current_user.id): - raise HTTPException(status_code=403, detail="Not enough permissions") - update_dict = item_in.model_dump(exclude_unset=True) - item.sqlmodel_update(update_dict) - session.add(item) - session.commit() - session.refresh(item) - return item - - -@router.delete("/{id}") -def delete_item( - session: SessionDep, current_user: CurrentUser, id: uuid.UUID -) -> Message: - """ - Delete an item. - """ - item = session.get(Item, id) - if not item: - raise HTTPException(status_code=404, detail="Item not found") - if not current_user.is_superuser and (item.owner_id != current_user.id): - raise HTTPException(status_code=403, detail="Not enough permissions") - session.delete(item) - session.commit() - return Message(message="Item deleted successfully") diff --git a/backend/app/api/routes/users.py b/backend/app/api/routes/users.py index 35f64b6..d4a1c86 100644 --- a/backend/app/api/routes/users.py +++ b/backend/app/api/routes/users.py @@ -13,7 +13,7 @@ from app.api.deps import ( from app.core.config import settings from app.core.security import get_password_hash, verify_password from app.models import ( - Item, + Location, Message, UpdatePassword, User, @@ -224,7 +224,7 @@ def delete_user( raise HTTPException( status_code=403, detail="Super users are not allowed to delete themselves" ) - statement = delete(Item).where(col(Item.owner_id) == user_id) + statement = delete(Location).where(col(Location.owner_id) == user_id) session.exec(statement) session.delete(user) session.commit() diff --git a/backend/app/crud.py b/backend/app/crud.py index d60e252..424c902 100644 --- a/backend/app/crud.py +++ b/backend/app/crud.py @@ -5,8 +5,6 @@ from sqlmodel import Session, select from app.core.security import get_password_hash, verify_password from app.models import ( - Item, - ItemCreate, Location, LocationCreate, User, @@ -68,14 +66,6 @@ def authenticate(*, session: Session, email: str, password: str) -> User | None: return db_user -def create_item(*, session: Session, item_in: ItemCreate, owner_id: uuid.UUID) -> Item: - db_item = Item.model_validate(item_in, update={"owner_id": owner_id}) - session.add(db_item) - session.commit() - session.refresh(db_item) - return db_item - - def create_location(*, session: Session, location_in: LocationCreate, owner_id: uuid.UUID) -> Location: db_location = Location.model_validate(location_in, update={"owner_id": owner_id}) session.add(db_location) diff --git a/backend/app/models.py b/backend/app/models.py index 9c036c1..147d591 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -53,7 +53,6 @@ class User(UserBase, table=True): default_factory=get_datetime_utc, sa_type=DateTime(timezone=True), # type: ignore ) - items: list["Item"] = Relationship(back_populates="owner", cascade_delete=True) locations: list["Location"] = Relationship(back_populates="owner", cascade_delete=True) @@ -68,47 +67,6 @@ class UsersPublic(SQLModel): count: int -# Shared properties -class ItemBase(SQLModel): - title: str = Field(min_length=1, max_length=255) - description: str | None = Field(default=None, max_length=255) - - -# Properties to receive on item creation -class ItemCreate(ItemBase): - pass - - -# Properties to receive on item update -class ItemUpdate(ItemBase): - title: str | None = Field(default=None, min_length=1, max_length=255) # type: ignore - - -# Database model, database table inferred from class name -class Item(ItemBase, table=True): - id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) - created_at: datetime | None = Field( - default_factory=get_datetime_utc, - sa_type=DateTime(timezone=True), # type: ignore - ) - owner_id: uuid.UUID = Field( - foreign_key="user.id", nullable=False, ondelete="CASCADE" - ) - owner: User | None = Relationship(back_populates="items") - - -# Properties to return via API, id is always required -class ItemPublic(ItemBase): - id: uuid.UUID - owner_id: uuid.UUID - created_at: datetime | None = None - - -class ItemsPublic(SQLModel): - data: list[ItemPublic] - count: int - - # Shared properties class LocationBase(SQLModel): title: str = Field(min_length=1, max_length=255) diff --git a/backend/tests/api/routes/test_items.py b/backend/tests/api/routes/test_items.py deleted file mode 100644 index 3e82cd0..0000000 --- a/backend/tests/api/routes/test_items.py +++ /dev/null @@ -1,164 +0,0 @@ -import uuid - -from fastapi.testclient import TestClient -from sqlmodel import Session - -from app.core.config import settings -from tests.utils.item import create_random_item - - -def test_create_item( - client: TestClient, superuser_token_headers: dict[str, str] -) -> None: - data = {"title": "Foo", "description": "Fighters"} - response = client.post( - f"{settings.API_V1_STR}/items/", - headers=superuser_token_headers, - json=data, - ) - assert response.status_code == 200 - content = response.json() - assert content["title"] == data["title"] - assert content["description"] == data["description"] - assert "id" in content - assert "owner_id" in content - - -def test_read_item( - client: TestClient, superuser_token_headers: dict[str, str], db: Session -) -> None: - item = create_random_item(db) - response = client.get( - f"{settings.API_V1_STR}/items/{item.id}", - headers=superuser_token_headers, - ) - assert response.status_code == 200 - content = response.json() - assert content["title"] == item.title - assert content["description"] == item.description - assert content["id"] == str(item.id) - assert content["owner_id"] == str(item.owner_id) - - -def test_read_item_not_found( - client: TestClient, superuser_token_headers: dict[str, str] -) -> None: - response = client.get( - f"{settings.API_V1_STR}/items/{uuid.uuid4()}", - headers=superuser_token_headers, - ) - assert response.status_code == 404 - content = response.json() - assert content["detail"] == "Item not found" - - -def test_read_item_not_enough_permissions( - client: TestClient, normal_user_token_headers: dict[str, str], db: Session -) -> None: - item = create_random_item(db) - response = client.get( - f"{settings.API_V1_STR}/items/{item.id}", - headers=normal_user_token_headers, - ) - assert response.status_code == 403 - content = response.json() - assert content["detail"] == "Not enough permissions" - - -def test_read_items( - client: TestClient, superuser_token_headers: dict[str, str], db: Session -) -> None: - create_random_item(db) - create_random_item(db) - response = client.get( - f"{settings.API_V1_STR}/items/", - headers=superuser_token_headers, - ) - assert response.status_code == 200 - content = response.json() - assert len(content["data"]) >= 2 - - -def test_update_item( - client: TestClient, superuser_token_headers: dict[str, str], db: Session -) -> None: - item = create_random_item(db) - data = {"title": "Updated title", "description": "Updated description"} - response = client.put( - f"{settings.API_V1_STR}/items/{item.id}", - headers=superuser_token_headers, - json=data, - ) - assert response.status_code == 200 - content = response.json() - assert content["title"] == data["title"] - assert content["description"] == data["description"] - assert content["id"] == str(item.id) - assert content["owner_id"] == str(item.owner_id) - - -def test_update_item_not_found( - client: TestClient, superuser_token_headers: dict[str, str] -) -> None: - data = {"title": "Updated title", "description": "Updated description"} - response = client.put( - f"{settings.API_V1_STR}/items/{uuid.uuid4()}", - headers=superuser_token_headers, - json=data, - ) - assert response.status_code == 404 - content = response.json() - assert content["detail"] == "Item not found" - - -def test_update_item_not_enough_permissions( - client: TestClient, normal_user_token_headers: dict[str, str], db: Session -) -> None: - item = create_random_item(db) - data = {"title": "Updated title", "description": "Updated description"} - response = client.put( - f"{settings.API_V1_STR}/items/{item.id}", - headers=normal_user_token_headers, - json=data, - ) - assert response.status_code == 403 - content = response.json() - assert content["detail"] == "Not enough permissions" - - -def test_delete_item( - client: TestClient, superuser_token_headers: dict[str, str], db: Session -) -> None: - item = create_random_item(db) - response = client.delete( - f"{settings.API_V1_STR}/items/{item.id}", - headers=superuser_token_headers, - ) - assert response.status_code == 200 - content = response.json() - assert content["message"] == "Item deleted successfully" - - -def test_delete_item_not_found( - client: TestClient, superuser_token_headers: dict[str, str] -) -> None: - response = client.delete( - f"{settings.API_V1_STR}/items/{uuid.uuid4()}", - headers=superuser_token_headers, - ) - assert response.status_code == 404 - content = response.json() - assert content["detail"] == "Item not found" - - -def test_delete_item_not_enough_permissions( - client: TestClient, normal_user_token_headers: dict[str, str], db: Session -) -> None: - item = create_random_item(db) - response = client.delete( - f"{settings.API_V1_STR}/items/{item.id}", - headers=normal_user_token_headers, - ) - assert response.status_code == 403 - content = response.json() - assert content["detail"] == "Not enough permissions" diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 8073e4d..a8729e8 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -7,7 +7,7 @@ from sqlmodel import Session, delete from app.core.config import settings from app.core.db import engine, init_db from app.main import app -from app.models import Item, Location, User +from app.models import Location, User from tests.utils.user import authentication_token_from_email from tests.utils.utils import get_superuser_token_headers @@ -17,8 +17,6 @@ def db() -> Generator[Session, None, None]: with Session(engine) as session: init_db(session) yield session - statement = delete(Item) - session.execute(statement) statement = delete(Location) session.execute(statement) statement = delete(User) diff --git a/backend/tests/utils/item.py b/backend/tests/utils/item.py deleted file mode 100644 index ee51b35..0000000 --- a/backend/tests/utils/item.py +++ /dev/null @@ -1,16 +0,0 @@ -from sqlmodel import Session - -from app import crud -from app.models import Item, ItemCreate -from tests.utils.user import create_random_user -from tests.utils.utils import random_lower_string - - -def create_random_item(db: Session) -> Item: - user = create_random_user(db) - owner_id = user.id - assert owner_id is not None - title = random_lower_string() - description = random_lower_string() - item_in = ItemCreate(title=title, description=description) - return crud.create_item(session=db, item_in=item_in, owner_id=owner_id) diff --git a/frontend/src/components/Items/AddItem.tsx b/frontend/src/components/Items/AddItem.tsx deleted file mode 100644 index 7c7c10c..0000000 --- a/frontend/src/components/Items/AddItem.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod" -import { useMutation, useQueryClient } from "@tanstack/react-query" -import { Plus } from "lucide-react" -import { useState } from "react" -import { useForm } from "react-hook-form" -import { z } from "zod" - -import { type ItemCreate, ItemsService } from "@/client" -import { Button } from "@/components/ui/button" -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog" -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form" -import { Input } from "@/components/ui/input" -import { LoadingButton } from "@/components/ui/loading-button" -import useCustomToast from "@/hooks/useCustomToast" -import { handleError } from "@/utils" - -const formSchema = z.object({ - title: z.string().min(1, { message: "Title is required" }), - description: z.string().optional(), -}) - -type FormData = z.infer - -const AddItem = () => { - const [isOpen, setIsOpen] = useState(false) - const queryClient = useQueryClient() - const { showSuccessToast, showErrorToast } = useCustomToast() - - const form = useForm({ - resolver: zodResolver(formSchema), - mode: "onBlur", - criteriaMode: "all", - defaultValues: { - title: "", - description: "", - }, - }) - - const mutation = useMutation({ - mutationFn: (data: ItemCreate) => - ItemsService.createItem({ requestBody: data }), - onSuccess: () => { - showSuccessToast("Item created successfully") - form.reset() - setIsOpen(false) - }, - onError: handleError.bind(showErrorToast), - onSettled: () => { - queryClient.invalidateQueries({ queryKey: ["items"] }) - }, - }) - - const onSubmit = (data: FormData) => { - mutation.mutate(data) - } - - return ( - - - - - - - Add Item - - Fill in the details to add a new item. - - -
- -
- ( - - - Title * - - - - - - - )} - /> - - ( - - Description - - - - - - )} - /> -
- - - - - - - Save - - -
- -
-
- ) -} - -export default AddItem diff --git a/frontend/src/components/Items/DeleteItem.tsx b/frontend/src/components/Items/DeleteItem.tsx deleted file mode 100644 index 9e61c34..0000000 --- a/frontend/src/components/Items/DeleteItem.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query" -import { Trash2 } from "lucide-react" -import { useState } from "react" -import { useForm } from "react-hook-form" - -import { ItemsService } from "@/client" -import { Button } from "@/components/ui/button" -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog" -import { DropdownMenuItem } from "@/components/ui/dropdown-menu" -import { LoadingButton } from "@/components/ui/loading-button" -import useCustomToast from "@/hooks/useCustomToast" -import { handleError } from "@/utils" - -interface DeleteItemProps { - id: string - onSuccess: () => void -} - -const DeleteItem = ({ id, onSuccess }: DeleteItemProps) => { - const [isOpen, setIsOpen] = useState(false) - const queryClient = useQueryClient() - const { showSuccessToast, showErrorToast } = useCustomToast() - const { handleSubmit } = useForm() - - const deleteItem = async (id: string) => { - await ItemsService.deleteItem({ id: id }) - } - - const mutation = useMutation({ - mutationFn: deleteItem, - onSuccess: () => { - showSuccessToast("The item was deleted successfully") - setIsOpen(false) - onSuccess() - }, - onError: handleError.bind(showErrorToast), - onSettled: () => { - queryClient.invalidateQueries() - }, - }) - - const onSubmit = async () => { - mutation.mutate(id) - } - - return ( - - e.preventDefault()} - onClick={() => setIsOpen(true)} - > - - Delete Item - - -
- - Delete Item - - This item will be permanently deleted. Are you sure? You will not - be able to undo this action. - - - - - - - - - Delete - - -
-
-
- ) -} - -export default DeleteItem diff --git a/frontend/src/components/Items/EditItem.tsx b/frontend/src/components/Items/EditItem.tsx deleted file mode 100644 index 3d57f55..0000000 --- a/frontend/src/components/Items/EditItem.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod" -import { useMutation, useQueryClient } from "@tanstack/react-query" -import { Pencil } from "lucide-react" -import { useState } from "react" -import { useForm } from "react-hook-form" -import { z } from "zod" - -import { type ItemPublic, ItemsService } from "@/client" -import { Button } from "@/components/ui/button" -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog" -import { DropdownMenuItem } from "@/components/ui/dropdown-menu" -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form" -import { Input } from "@/components/ui/input" -import { LoadingButton } from "@/components/ui/loading-button" -import useCustomToast from "@/hooks/useCustomToast" -import { handleError } from "@/utils" - -const formSchema = z.object({ - title: z.string().min(1, { message: "Title is required" }), - description: z.string().optional(), -}) - -type FormData = z.infer - -interface EditItemProps { - item: ItemPublic - onSuccess: () => void -} - -const EditItem = ({ item, onSuccess }: EditItemProps) => { - const [isOpen, setIsOpen] = useState(false) - const queryClient = useQueryClient() - const { showSuccessToast, showErrorToast } = useCustomToast() - - const form = useForm({ - resolver: zodResolver(formSchema), - mode: "onBlur", - criteriaMode: "all", - defaultValues: { - title: item.title, - description: item.description ?? undefined, - }, - }) - - const mutation = useMutation({ - mutationFn: (data: FormData) => - ItemsService.updateItem({ id: item.id, requestBody: data }), - onSuccess: () => { - showSuccessToast("Item updated successfully") - setIsOpen(false) - onSuccess() - }, - onError: handleError.bind(showErrorToast), - onSettled: () => { - queryClient.invalidateQueries({ queryKey: ["items"] }) - }, - }) - - const onSubmit = (data: FormData) => { - mutation.mutate(data) - } - - return ( - - e.preventDefault()} - onClick={() => setIsOpen(true)} - > - - Edit Item - - -
- - - Edit Item - - Update the item details below. - - -
- ( - - - Title * - - - - - - - )} - /> - - ( - - Description - - - - - - )} - /> -
- - - - - - - Save - - -
- -
-
- ) -} - -export default EditItem diff --git a/frontend/src/components/Items/ItemActionsMenu.tsx b/frontend/src/components/Items/ItemActionsMenu.tsx deleted file mode 100644 index 1efe7bf..0000000 --- a/frontend/src/components/Items/ItemActionsMenu.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { EllipsisVertical } from "lucide-react" -import { useState } from "react" - -import type { ItemPublic } from "@/client" -import { Button } from "@/components/ui/button" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import DeleteItem from "../Items/DeleteItem" -import EditItem from "../Items/EditItem" - -interface ItemActionsMenuProps { - item: ItemPublic -} - -export const ItemActionsMenu = ({ item }: ItemActionsMenuProps) => { - const [open, setOpen] = useState(false) - - return ( - - - - - - setOpen(false)} /> - setOpen(false)} /> - - - ) -} diff --git a/frontend/src/components/Items/columns.tsx b/frontend/src/components/Items/columns.tsx deleted file mode 100644 index b41be2a..0000000 --- a/frontend/src/components/Items/columns.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import type { ColumnDef } from "@tanstack/react-table" -import { Check, Copy } from "lucide-react" - -import type { ItemPublic } from "@/client" -import { Button } from "@/components/ui/button" -import { useCopyToClipboard } from "@/hooks/useCopyToClipboard" -import { cn } from "@/lib/utils" -import { ItemActionsMenu } from "./ItemActionsMenu" - -function CopyId({ id }: { id: string }) { - const [copiedText, copy] = useCopyToClipboard() - const isCopied = copiedText === id - - return ( -
- {id} - -
- ) -} - -export const columns: ColumnDef[] = [ - { - accessorKey: "id", - header: "ID", - cell: ({ row }) => , - }, - { - accessorKey: "title", - header: "Title", - cell: ({ row }) => ( - {row.original.title} - ), - }, - { - accessorKey: "description", - header: "Description", - cell: ({ row }) => { - const description = row.original.description - return ( - - {description || "No description"} - - ) - }, - }, - { - id: "actions", - header: () => Actions, - cell: ({ row }) => ( -
- -
- ), - }, -] diff --git a/frontend/src/components/Pending/PendingItems.tsx b/frontend/src/components/Pending/PendingSkeleton.tsx similarity index 94% rename from frontend/src/components/Pending/PendingItems.tsx rename to frontend/src/components/Pending/PendingSkeleton.tsx index 9658335..f05b49e 100644 --- a/frontend/src/components/Pending/PendingItems.tsx +++ b/frontend/src/components/Pending/PendingSkeleton.tsx @@ -8,7 +8,7 @@ import { TableRow, } from "@/components/ui/table" -const PendingItems = () => ( +const PendingSkeleton = () => ( @@ -43,4 +43,4 @@ const PendingItems = () => (
) -export default PendingItems +export default PendingSkeleton diff --git a/frontend/src/components/Sidebar/AppSidebar.tsx b/frontend/src/components/Sidebar/AppSidebar.tsx index a175600..deace7d 100644 --- a/frontend/src/components/Sidebar/AppSidebar.tsx +++ b/frontend/src/components/Sidebar/AppSidebar.tsx @@ -1,4 +1,4 @@ -import { Briefcase, Home, MapPin, Users } from "lucide-react" +import { Home, MapPin, Users } from "lucide-react" import { SidebarAppearance } from "@/components/Common/Appearance" import { Logo } from "@/components/Common/Logo" @@ -15,7 +15,6 @@ import { User } from "./User" const baseItems: Item[] = [ { icon: Home, title: "Dashboard", path: "/" }, { icon: MapPin, title: "Locations", path: "/locations" }, - { icon: Briefcase, title: "Items", path: "/items" }, ] export function AppSidebar() { diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 9b5145d..54d2a18 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -17,7 +17,6 @@ import { Route as LayoutRouteImport } from './routes/_layout' import { Route as LayoutIndexRouteImport } from './routes/_layout/index' import { Route as LayoutSettingsRouteImport } from './routes/_layout/settings' import { Route as LayoutLocationsRouteImport } from './routes/_layout/locations' -import { Route as LayoutItemsRouteImport } from './routes/_layout/items' import { Route as LayoutAdminRouteImport } from './routes/_layout/admin' const SignupRoute = SignupRouteImport.update({ @@ -59,11 +58,6 @@ const LayoutLocationsRoute = LayoutLocationsRouteImport.update({ path: '/locations', getParentRoute: () => LayoutRoute, } as any) -const LayoutItemsRoute = LayoutItemsRouteImport.update({ - id: '/items', - path: '/items', - getParentRoute: () => LayoutRoute, -} as any) const LayoutAdminRoute = LayoutAdminRouteImport.update({ id: '/admin', path: '/admin', @@ -77,7 +71,6 @@ export interface FileRoutesByFullPath { '/reset-password': typeof ResetPasswordRoute '/signup': typeof SignupRoute '/admin': typeof LayoutAdminRoute - '/items': typeof LayoutItemsRoute '/locations': typeof LayoutLocationsRoute '/settings': typeof LayoutSettingsRoute } @@ -87,7 +80,6 @@ export interface FileRoutesByTo { '/reset-password': typeof ResetPasswordRoute '/signup': typeof SignupRoute '/admin': typeof LayoutAdminRoute - '/items': typeof LayoutItemsRoute '/locations': typeof LayoutLocationsRoute '/settings': typeof LayoutSettingsRoute '/': typeof LayoutIndexRoute @@ -100,7 +92,6 @@ export interface FileRoutesById { '/reset-password': typeof ResetPasswordRoute '/signup': typeof SignupRoute '/_layout/admin': typeof LayoutAdminRoute - '/_layout/items': typeof LayoutItemsRoute '/_layout/locations': typeof LayoutLocationsRoute '/_layout/settings': typeof LayoutSettingsRoute '/_layout/': typeof LayoutIndexRoute @@ -114,7 +105,6 @@ export interface FileRouteTypes { | '/reset-password' | '/signup' | '/admin' - | '/items' | '/locations' | '/settings' fileRoutesByTo: FileRoutesByTo @@ -124,7 +114,6 @@ export interface FileRouteTypes { | '/reset-password' | '/signup' | '/admin' - | '/items' | '/locations' | '/settings' | '/' @@ -136,7 +125,6 @@ export interface FileRouteTypes { | '/reset-password' | '/signup' | '/_layout/admin' - | '/_layout/items' | '/_layout/locations' | '/_layout/settings' | '/_layout/' @@ -208,13 +196,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutLocationsRouteImport parentRoute: typeof LayoutRoute } - '/_layout/items': { - id: '/_layout/items' - path: '/items' - fullPath: '/items' - preLoaderRoute: typeof LayoutItemsRouteImport - parentRoute: typeof LayoutRoute - } '/_layout/admin': { id: '/_layout/admin' path: '/admin' @@ -227,7 +208,6 @@ declare module '@tanstack/react-router' { interface LayoutRouteChildren { LayoutAdminRoute: typeof LayoutAdminRoute - LayoutItemsRoute: typeof LayoutItemsRoute LayoutLocationsRoute: typeof LayoutLocationsRoute LayoutSettingsRoute: typeof LayoutSettingsRoute LayoutIndexRoute: typeof LayoutIndexRoute @@ -235,7 +215,6 @@ interface LayoutRouteChildren { const LayoutRouteChildren: LayoutRouteChildren = { LayoutAdminRoute: LayoutAdminRoute, - LayoutItemsRoute: LayoutItemsRoute, LayoutLocationsRoute: LayoutLocationsRoute, LayoutSettingsRoute: LayoutSettingsRoute, LayoutIndexRoute: LayoutIndexRoute, diff --git a/frontend/src/routes/_layout/items.tsx b/frontend/src/routes/_layout/items.tsx deleted file mode 100644 index a4df200..0000000 --- a/frontend/src/routes/_layout/items.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { useSuspenseQuery } from "@tanstack/react-query" -import { createFileRoute } from "@tanstack/react-router" -import { Search } from "lucide-react" -import { Suspense } from "react" - -import { ItemsService } from "@/client" -import { DataTable } from "@/components/Common/DataTable" -import AddItem from "@/components/Items/AddItem" -import { columns } from "@/components/Items/columns" -import PendingItems from "@/components/Pending/PendingItems" - -function getItemsQueryOptions() { - return { - queryFn: () => ItemsService.readItems({ skip: 0, limit: 100 }), - queryKey: ["items"], - } -} - -export const Route = createFileRoute("/_layout/items")({ - component: Items, - head: () => ({ - meta: [ - { - title: "Items - FastAPI Template", - }, - ], - }), -}) - -function ItemsTableContent() { - const { data: items } = useSuspenseQuery(getItemsQueryOptions()) - - if (items.data.length === 0) { - return ( -
-
- -
-

You don't have any items yet

-

Add a new item to get started

-
- ) - } - - return -} - -function ItemsTable() { - return ( - }> - - - ) -} - -function Items() { - return ( -
-
-
-

Items

-

Create and manage your items

-
- -
- -
- ) -} diff --git a/frontend/src/routes/_layout/locations.tsx b/frontend/src/routes/_layout/locations.tsx index f4e7c4e..b0283f6 100644 --- a/frontend/src/routes/_layout/locations.tsx +++ b/frontend/src/routes/_layout/locations.tsx @@ -7,7 +7,7 @@ import { LocationsService } from "@/client" import { DataTable } from "@/components/Common/DataTable" import AddLocation from "@/components/Locations/AddLocation" import { columns } from "@/components/Locations/columns" -import PendingItems from "@/components/Pending/PendingItems" +import PendingSkeleton from "@/components/Pending/PendingSkeleton" function getLocationsQueryOptions() { return { @@ -47,7 +47,7 @@ function LocationsTableContent() { function LocationsTable() { return ( - }> + }> ) diff --git a/frontend/tests/items.spec.ts b/frontend/tests/items.spec.ts deleted file mode 100644 index 5a43731..0000000 --- a/frontend/tests/items.spec.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { expect, test } from "@playwright/test" -import { createUser } from "./utils/privateApi" -import { - randomEmail, - randomItemDescription, - randomItemTitle, - randomPassword, -} from "./utils/random" -import { logInUser } from "./utils/user" - -test("Items page is accessible and shows correct title", async ({ page }) => { - await page.goto("/items") - await expect(page.getByRole("heading", { name: "Items" })).toBeVisible() - await expect(page.getByText("Create and manage your items")).toBeVisible() -}) - -test("Add Item button is visible", async ({ page }) => { - await page.goto("/items") - await expect(page.getByRole("button", { name: "Add Item" })).toBeVisible() -}) - -test.describe("Items management", () => { - test.use({ storageState: { cookies: [], origins: [] } }) - let email: string - const password = randomPassword() - - test.beforeAll(async () => { - email = randomEmail() - await createUser({ email, password }) - }) - - test.beforeEach(async ({ page }) => { - await logInUser(page, email, password) - await page.goto("/items") - }) - - test("Create a new item successfully", async ({ page }) => { - const title = randomItemTitle() - const description = randomItemDescription() - - await page.getByRole("button", { name: "Add Item" }).click() - await page.getByLabel("Title").fill(title) - await page.getByLabel("Description").fill(description) - await page.getByRole("button", { name: "Save" }).click() - - await expect(page.getByText("Item created successfully")).toBeVisible() - await expect(page.getByText(title)).toBeVisible() - }) - - test("Create item with only required fields", async ({ page }) => { - const title = randomItemTitle() - - await page.getByRole("button", { name: "Add Item" }).click() - await page.getByLabel("Title").fill(title) - await page.getByRole("button", { name: "Save" }).click() - - await expect(page.getByText("Item created successfully")).toBeVisible() - await expect(page.getByText(title)).toBeVisible() - }) - - test("Cancel item creation", async ({ page }) => { - await page.getByRole("button", { name: "Add Item" }).click() - await page.getByLabel("Title").fill("Test Item") - await page.getByRole("button", { name: "Cancel" }).click() - - await expect(page.getByRole("dialog")).not.toBeVisible() - }) - - test("Title is required", async ({ page }) => { - await page.getByRole("button", { name: "Add Item" }).click() - await page.getByLabel("Title").fill("") - await page.getByLabel("Title").blur() - - await expect(page.getByText("Title is required")).toBeVisible() - }) - - test.describe("Edit and Delete", () => { - let itemTitle: string - - test.beforeEach(async ({ page }) => { - itemTitle = randomItemTitle() - - await page.getByRole("button", { name: "Add Item" }).click() - await page.getByLabel("Title").fill(itemTitle) - await page.getByRole("button", { name: "Save" }).click() - await expect(page.getByText("Item created successfully")).toBeVisible() - await expect(page.getByRole("dialog")).not.toBeVisible() - }) - - test("Edit an item successfully", async ({ page }) => { - const itemRow = page.getByRole("row").filter({ hasText: itemTitle }) - await itemRow.getByRole("button").last().click() - await page.getByRole("menuitem", { name: "Edit Item" }).click() - - const updatedTitle = randomItemTitle() - await page.getByLabel("Title").fill(updatedTitle) - await page.getByRole("button", { name: "Save" }).click() - - await expect(page.getByText("Item updated successfully")).toBeVisible() - await expect(page.getByText(updatedTitle)).toBeVisible() - }) - - test("Delete an item successfully", async ({ page }) => { - const itemRow = page.getByRole("row").filter({ hasText: itemTitle }) - await itemRow.getByRole("button").last().click() - await page.getByRole("menuitem", { name: "Delete Item" }).click() - - await page.getByRole("button", { name: "Delete" }).click() - - await expect( - page.getByText("The item was deleted successfully"), - ).toBeVisible() - await expect(page.getByText(itemTitle)).not.toBeVisible() - }) - }) -}) - -test.describe("Items empty state", () => { - test.use({ storageState: { cookies: [], origins: [] } }) - - test("Shows empty state message when no items exist", async ({ page }) => { - const email = randomEmail() - const password = randomPassword() - await createUser({ email, password }) - await logInUser(page, email, password) - - await page.goto("/items") - - await expect(page.getByText("You don't have any items yet")).toBeVisible() - await expect(page.getByText("Add a new item to get started")).toBeVisible() - }) -})