Delete Button and Update QoL changes (#60)

* Edit visibility for options depending on administrator status

* Connect delete frontend route to delete backend

* small readme.md changes

* Fix table lines and update bug

* Add clicking outside of drawer to close
This commit is contained in:
Prajwal Moharana 2025-04-25 12:10:36 -04:00 committed by GitHub
parent bdc6600a3f
commit 469236cb04
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 367 additions and 84 deletions

View File

@ -60,12 +60,12 @@ def update(
return resource_svc.update(subject, resource) return resource_svc.update(subject, resource)
@api.delete("", response_model=None, tags=["Resource"]) @api.delete("", response_model=dict, tags=["Resource"])
def delete( def delete(
uuid: str, uuid: str,
resource: Resource, id: int,
user_svc: UserService = Depends(), user_svc: UserService = Depends(),
resource_svc: ResourceService = Depends(), resource_svc: ResourceService = Depends(),
): ):
subject = user_svc.get_user_by_uuid(uuid) subject = user_svc.get_user_by_uuid(uuid)
resource_svc.delete(subject, resource) return resource_svc.delete(subject, id)

View File

@ -19,7 +19,10 @@ openapi_tags = {
# TODO: Create custom exceptions # TODO: Create custom exceptions
@api.post("", response_model=Service, tags=["Service"]) @api.post("", response_model=Service, tags=["Service"])
def create( def create(
uuid: str, service: Service, user_svc: UserService = Depends(), service_svc: ServiceService = Depends() uuid: str,
service: Service,
user_svc: UserService = Depends(),
service_svc: ServiceService = Depends(),
): ):
subject = user_svc.get_user_by_uuid(uuid) subject = user_svc.get_user_by_uuid(uuid)
return service_svc.create(subject, service) return service_svc.create(subject, service)
@ -27,28 +30,42 @@ def create(
@api.get("", response_model=List[Service], tags=["Service"]) @api.get("", response_model=List[Service], tags=["Service"])
def get_all( def get_all(
uuid: str, user_svc: UserService = Depends(), service_svc: ServiceService = Depends() uuid: str,
user_svc: UserService = Depends(),
service_svc: ServiceService = Depends(),
): ):
subject = user_svc.get_user_by_uuid(uuid) subject = user_svc.get_user_by_uuid(uuid)
return service_svc.get_service_by_user(subject) return service_svc.get_service_by_user(subject)
@api.get("/{name}", response_model=Service, tags=["Service"]) @api.get("/{name}", response_model=Service, tags=["Service"])
def get_by_name( def get_by_name(
name: str, uuid: str, user_svc: UserService = Depends(), service_svc: ServiceService = Depends() name: str,
uuid: str,
user_svc: UserService = Depends(),
service_svc: ServiceService = Depends(),
): ):
subject = user_svc.get_user_by_uuid(uuid) subject = user_svc.get_user_by_uuid(uuid)
return service_svc.get_service_by_name(name, subject) return service_svc.get_service_by_name(name, subject)
@api.put("", response_model=Service, tags=["Service"]) @api.put("", response_model=Service, tags=["Service"])
def update( def update(
uuid: str, service: Service, user_svc: UserService = Depends(), service_svc: ServiceService = Depends() uuid: str,
service: Service,
user_svc: UserService = Depends(),
service_svc: ServiceService = Depends(),
): ):
subject = user_svc.get_user_by_uuid(uuid) subject = user_svc.get_user_by_uuid(uuid)
return service_svc.update(subject, service) return service_svc.update(subject, service)
@api.delete("", response_model=None, tags=["Service"])
@api.delete("", response_model=dict, tags=["Service"])
def delete( def delete(
uuid: str, service: Service, user_svc: UserService = Depends(), service_svc: ServiceService = Depends() uuid: str,
id: int,
user_svc: UserService = Depends(),
service_svc: ServiceService = Depends(),
): ):
subject = user_svc.get_user_by_uuid(uuid) subject = user_svc.get_user_by_uuid(uuid)
service_svc.delete(subject, service) return service_svc.delete(subject, id)

View File

@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, HTTPException
from ..services import UserService from ..services import UserService
from ..models.user_model import User, UserTypeEnum from ..models.user_model import User, UserTypeEnum
@ -46,3 +46,14 @@ def update_user(uuid: str, user: User, user_svc: UserService = Depends()):
raise Exception(f"Insufficient permissions for user {subject.uuid}") raise Exception(f"Insufficient permissions for user {subject.uuid}")
return user_svc.update(user) return user_svc.update(user)
@api.delete("/", response_model=dict, tags=["Users"])
def delete_user(uuid: str, id: int, user_svc: UserService = Depends()):
subject = user_svc.get_user_by_uuid(uuid)
try:
user_svc.delete_by_id(id, subject)
return {"message": "User deleted successfully"}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))

View File

@ -93,7 +93,7 @@ class ResourceService:
self._session.commit() self._session.commit()
return entity.to_model() return entity.to_model()
def delete(self, subject: User, resource: Resource) -> None: def delete(self, subject: User, id: int) -> None:
""" """
Delete resource based on id that the user has access to Delete resource based on id that the user has access to
Parameters: Parameters:
@ -106,15 +106,16 @@ class ResourceService:
raise ProgramNotAssignedException( raise ProgramNotAssignedException(
f"User is not {UserTypeEnum.ADMIN}, cannot update service" f"User is not {UserTypeEnum.ADMIN}, cannot update service"
) )
query = select(ResourceEntity).where(ResourceEntity.id == resource.id)
query = select(ResourceEntity).where(ResourceEntity.id == id)
entity = self._session.scalars(query).one_or_none() entity = self._session.scalars(query).one_or_none()
if entity is None: if entity is None:
raise ResourceNotFoundException( raise ResourceNotFoundException(f"No resource found with matching id: {id}")
f"No resource found with matching id: {resource.id}"
)
self._session.delete(entity) self._session.delete(entity)
self._session.commit() self._session.commit()
return {"message": "Resource deleted successfully"}
def get_by_slug(self, user: User, search_string: str) -> list[Resource]: def get_by_slug(self, user: User, search_string: str) -> list[Resource]:
""" """
Get a list of resources given a search string that the user has access to Get a list of resources given a search string that the user has access to

View File

@ -90,12 +90,12 @@ class ServiceService:
return entity.to_model() return entity.to_model()
def delete(self, subject: User, service: Service) -> None: def delete(self, subject: User, id: int) -> None:
"""Deletes a service from the table.""" """Deletes a service from the table."""
if subject.role != UserTypeEnum.ADMIN: if subject.role != UserTypeEnum.ADMIN:
raise ProgramNotAssignedException(f"User is not {UserTypeEnum.ADMIN}") raise ProgramNotAssignedException(f"User is not {UserTypeEnum.ADMIN}")
query = select(ServiceEntity).where(ServiceEntity.id == service.id) query = select(ServiceEntity).where(ServiceEntity.id == id)
entity = self._session.scalars(query).one_or_none() entity = self._session.scalars(query).one_or_none()
if entity is None: if entity is None:
@ -105,3 +105,5 @@ class ServiceService:
self._session.delete(entity) self._session.delete(entity)
self._session.commit() self._session.commit()
return {"message": "Service deleted successfully"}

View File

@ -4,6 +4,7 @@ from sqlalchemy.orm import Session
from ..entities.user_entity import UserEntity from ..entities.user_entity import UserEntity
from ..models.user_model import User from ..models.user_model import User
from sqlalchemy import select from sqlalchemy import select
from ..models.enum_for_models import UserTypeEnum
class UserService: class UserService:
@ -89,6 +90,22 @@ class UserService:
self._session.delete(obj) self._session.delete(obj)
self._session.commit() self._session.commit()
def delete_by_id(self, id: int, user: User) -> None:
"""
Delete a user by id
Args: the id of the user to delete
Returns: none
"""
if user.role != UserTypeEnum.ADMIN:
raise Exception(f"Insufficient permissions for user {user.uuid}")
obj = self._session.get(UserEntity, id)
self._session.delete(obj)
self._session.commit()
def update(self, user: User) -> User: def update(self, user: User) -> User:
""" """
Updates a user Updates a user

View File

@ -0,0 +1,39 @@
import { NextResponse } from "next/server";
export async function DELETE(request: Request) {
const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/resource`;
try {
const { searchParams } = new URL(request.url);
const uuid = searchParams.get("uuid");
const resource_id = searchParams.get("id");
console.log("Resource to be deleted", resource_id);
// Send the POST request to the backend
const response = await fetch(
`${apiEndpoint}?uuid=${uuid}&id=${resource_id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return NextResponse.json(
{ message: "Resource deleted successfully" },
{ status: response.status }
);
} catch (error) {
console.error("Error deleting resource:", error);
return NextResponse.json(
{ error: "Failed to delete resource" },
{ status: 500 }
);
}
}

View File

@ -0,0 +1,39 @@
import { NextResponse } from "next/server";
export async function DELETE(request: Request) {
const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/service`;
try {
const { searchParams } = new URL(request.url);
const uuid = searchParams.get("uuid");
const service_id = searchParams.get("id");
console.log("Service to be deleted", service_id);
// Send the POST request to the backend
const response = await fetch(
`${apiEndpoint}?uuid=${uuid}&id=${service_id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return NextResponse.json(
{ message: "Service deleted successfully" },
{ status: response.status }
);
} catch (error) {
console.error("Error deleting service:", error);
return NextResponse.json(
{ error: "Failed to delete service" },
{ status: 500 }
);
}
}

View File

@ -0,0 +1,39 @@
import { NextResponse } from "next/server";
export async function DELETE(request: Request) {
const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/user`;
try {
const { searchParams } = new URL(request.url);
const uuid = searchParams.get("uuid");
const user_id = searchParams.get("id");
console.log("User to be deleted", user_id);
// Send the POST request to the backend
const response = await fetch(
`${apiEndpoint}?uuid=${uuid}&id=${user_id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return NextResponse.json(
{ message: "User deleted successfully" },
{ status: response.status }
);
} catch (error) {
console.error("Error deleting user:", error);
return NextResponse.json(
{ error: "Failed to delete user" },
{ status: 500 }
);
}
}

View File

@ -1,4 +1,4 @@
import { Dispatch, FunctionComponent, ReactNode, SetStateAction } from "react"; import { Dispatch, FunctionComponent, ReactNode, SetStateAction, useEffect, useRef } from "react";
import React, { useState } from "react"; import React, { useState } from "react";
import { ChevronDoubleLeftIcon } from "@heroicons/react/24/solid"; import { ChevronDoubleLeftIcon } from "@heroicons/react/24/solid";
import { StarIcon as SolidStarIcon, UserIcon } from "@heroicons/react/24/solid"; import { StarIcon as SolidStarIcon, UserIcon } from "@heroicons/react/24/solid";
@ -47,6 +47,47 @@ const Drawer: FunctionComponent<DrawerProps> = ({
const [isFull, setIsFull] = useState(false); const [isFull, setIsFull] = useState(false);
const [isFavorite, setIsFavorite] = useState(false); const [isFavorite, setIsFavorite] = useState(false);
const [tempRowContent, setTempRowContent] = useState(rowContent); const [tempRowContent, setTempRowContent] = useState(rowContent);
const drawerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (drawerRef.current && !drawerRef.current.contains(event.target as Node)) {
if (setRowContent && isOpen) {
// Check if any values have changed
const hasChanges = Object.keys(tempRowContent).some(
(key) => tempRowContent[key] !== rowContent[key]
);
if (hasChanges) {
handleUpdate().then((response) => {
if (response.ok) {
setRowContent((prev: any) => {
return prev.map((row: any) => {
if (row.id === tempRowContent.id) {
return tempRowContent;
}
return row;
});
});
}
});
}
}
setIsOpen(false);
if (isFull) {
setIsFull(false);
}
}
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen, tempRowContent, rowContent]);
const handleTempRowContentChangeHTML = ( const handleTempRowContentChangeHTML = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement> e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
@ -114,8 +155,7 @@ const Drawer: FunctionComponent<DrawerProps> = ({
const toggleFavorite = () => setIsFavorite(!isFavorite); const toggleFavorite = () => setIsFavorite(!isFavorite);
const drawerClassName = `fixed top-0 right-0 w-1/2 h-full bg-white transform ease-in-out duration-300 z-20 ${ const drawerClassName = `fixed top-0 right-0 w-1/2 h-full bg-white transform ease-in-out duration-300 z-20 ${isOpen ? "translate-x-0 shadow-xl" : "translate-x-full"
isOpen ? "translate-x-0 shadow-xl" : "translate-x-full"
} ${isFull ? "w-full" : "w-1/2"}`; } ${isFull ? "w-full" : "w-1/2"}`;
const iconComponent = isFull ? ( const iconComponent = isFull ? (
@ -140,7 +180,7 @@ const Drawer: FunctionComponent<DrawerProps> = ({
> >
Open Open
</button> </button>
<div className={drawerClassName}> <div ref={drawerRef} className={drawerClassName}>
<div className="flex items-center justify-between p-4"> <div className="flex items-center justify-between p-4">
<div className="flex flex-row items-center justify-between space-x-2"> <div className="flex flex-row items-center justify-between space-x-2">
<span className="h-5 text-purple-200 w-5"> <span className="h-5 text-purple-200 w-5">
@ -196,7 +236,7 @@ const Drawer: FunctionComponent<DrawerProps> = ({
} }
setPresetOptions={ setPresetOptions={
detail.presetOptionsSetter || detail.presetOptionsSetter ||
(() => {}) (() => { })
} }
singleValue={true} singleValue={true}
onTagsChange={( onTagsChange={(
@ -246,7 +286,7 @@ const Drawer: FunctionComponent<DrawerProps> = ({
} }
setPresetOptions={ setPresetOptions={
detail.presetOptionsSetter || detail.presetOptionsSetter ||
(() => {}) (() => { })
} }
onTagsChange={( onTagsChange={(
tags: Set<string> tags: Set<string>

View File

@ -143,6 +143,7 @@ export default function ResourceTable({
columns={columns} columns={columns}
details={resourceDetails} details={resourceDetails}
createEndpoint={`/api/resource/create?uuid=${user?.uuid}`} createEndpoint={`/api/resource/create?uuid=${user?.uuid}`}
deleteEndpoint={`/api/resource/delete?uuid=${user?.uuid}`}
isAdmin={user?.role === "ADMIN"} isAdmin={user?.role === "ADMIN"}
/> />
); );

View File

@ -27,7 +27,7 @@ export function RowOpenAction<T extends DataPoint>({
updateRoute, updateRoute,
}: RowOpenActionProps<T>) { }: RowOpenActionProps<T>) {
return ( return (
<div className="font-semibold group flex flex-row items-center justify-between pr-2"> <div className="font-semibold group flex flex-row items-center justify-between px-2">
{title} {title}
<span> <span>
<Drawer <Drawer

View File

@ -5,41 +5,78 @@ import {
ArrowUpRightIcon, ArrowUpRightIcon,
EllipsisVerticalIcon, EllipsisVerticalIcon,
EyeSlashIcon, EyeSlashIcon,
EyeIcon,
} from "@heroicons/react/24/solid"; } from "@heroicons/react/24/solid";
import Button from "../Button"; import Button from "../Button";
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { RowOption } from "./RowOption"; import { RowOption } from "./RowOption";
export const RowOptionMenu = ({ onDelete, onHide }) => { interface RowOptionMenuProps {
const [menuOpen, setMenuOpen] = useState(false); onDelete: () => void;
const openMenu = () => setMenuOpen(true); onHide: () => void;
const closeMenu = () => setMenuOpen(false); visible: boolean;
}
// TODO: Hide menu if clicked elsewhere export const RowOptionMenu = ({
onDelete,
onHide,
visible,
}: RowOptionMenuProps) => {
const [menuOpen, setMenuOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const handleDelete = () => {
if (window.confirm("Are you sure you want to delete this item?")) {
onDelete();
setMenuOpen(false);
}
};
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
menuRef.current &&
buttonRef.current &&
!menuRef.current.contains(event.target as Node) &&
!buttonRef.current.contains(event.target as Node)
) {
setMenuOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
return ( return (
<> <>
<button <button
ref={buttonRef}
className="items-end" className="items-end"
onClick={() => setMenuOpen(!menuOpen)} onClick={() => setMenuOpen(!menuOpen)}
> >
<EllipsisVerticalIcon className="h-4" /> <EllipsisVerticalIcon className="h-4" />
</button> </button>
<div <div
ref={menuRef}
className={ className={
"justify-start border border-gray-200 shadow-lg flex flex-col absolute bg-white w-auto p-2 rounded [&>*]:rounded z-10" + "justify-start border border-gray-200 shadow-lg flex flex-col absolute bg-white w-auto p-2 rounded [&>*]:rounded z-10" +
(!menuOpen ? " invisible" : "") (!menuOpen ? " invisible" : "")
} }
> >
<RowOption icon={TrashIcon} label="Delete" onClick={onDelete} />
<RowOption <RowOption
icon={ArrowUpRightIcon} icon={TrashIcon}
label="Open" label="Delete"
onClick={() => { onClick={handleDelete}
/* handle open */ />
}} <RowOption
icon={visible ? EyeSlashIcon : EyeIcon}
label={visible ? "Hide" : "Show"}
onClick={onHide}
/> />
<RowOption icon={EyeSlashIcon} label="Hide" onClick={onHide} />
</div> </div>
</> </>
); );

View File

@ -195,6 +195,7 @@ export default function ServiceTable({
columns={columns} columns={columns}
details={serviceDetails} details={serviceDetails}
createEndpoint={`/api/service/create?uuid=${user?.uuid}`} createEndpoint={`/api/service/create?uuid=${user?.uuid}`}
deleteEndpoint={`/api/service/delete?uuid=${user?.uuid}`}
isAdmin={user?.role === "ADMIN"} isAdmin={user?.role === "ADMIN"}
/> />
); );

View File

@ -28,6 +28,7 @@ type TableProps<T extends DataPoint> = {
columns: ColumnDef<T, any>[]; columns: ColumnDef<T, any>[];
details: Details[]; details: Details[];
createEndpoint: string; createEndpoint: string;
deleteEndpoint: string;
isAdmin?: boolean; isAdmin?: boolean;
}; };
@ -79,12 +80,24 @@ export default function Table<T extends DataPoint>({
columns, columns,
details, details,
createEndpoint, createEndpoint,
deleteEndpoint,
isAdmin = false, isAdmin = false,
}: TableProps<T>) { }: TableProps<T>) {
console.log(data); const offset = isAdmin ? 1 : 0;
const columnHelper = createColumnHelper<T>(); const columnHelper = createColumnHelper<T>();
const deleteRow = async (id: number) => {
const response = await fetch(`${deleteEndpoint}&id=${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
return response.ok;
};
const createRow = async (newItem: any) => { const createRow = async (newItem: any) => {
const response = await fetch(createEndpoint, { const response = await fetch(createEndpoint, {
method: "POST", method: "POST",
@ -97,9 +110,9 @@ export default function Table<T extends DataPoint>({
return response; return response;
}; };
// /** Sorting function based on visibility */ /** Sorting function based on visibility */
// const visibilitySort = (a: T, b: T) => const visibilitySort = (a: T, b: T) =>
// a.visible === b.visible ? 0 : a.visible ? -1 : 1; a.visible === b.visible ? 0 : a.visible ? -1 : 1;
// // Sort data on load // // Sort data on load
// useEffect(() => { // useEffect(() => {
@ -115,36 +128,59 @@ export default function Table<T extends DataPoint>({
// ); // );
// }; // };
// const hideData = (dataId: number) => { const hideData = (dataId: number) => {
// console.log(`Toggling visibility for data with ID: ${dataId}`); console.log(`Toggling visibility for data with ID: ${dataId}`);
// setData((currentData) => { setData((currentData) => {
// const newData = currentData const newData = currentData
// .map((data) => .map((data) =>
// data.id === dataId data.id === dataId
// ? { ...data, visible: !data.visible } ? { ...data, visible: !data.visible }
// : data : data
// ) )
// .sort(visibilitySort); .sort((a, b) => {
// First sort by visibility
const visibilityResult = visibilitySort(a, b);
if (visibilityResult !== 0) return visibilityResult;
// Then sort by id
return a.id - b.id;
});
// console.log(newData); console.log(newData);
// return newData; return newData;
// }); });
// }; };
// Add data manipulation options to the first column // Add data manipulation options to the first column
if (isAdmin) {
columns.unshift( columns.unshift(
columnHelper.display({ columnHelper.display({
id: "options", id: "options",
cell: (props) => ( cell: (props) => (
<RowOptionMenu <RowOptionMenu
onDelete={() => {}} onDelete={() => {
onHide={() => {}} deleteRow(props.row.original.id).then(
// onDelete={() => deleteData(props.row.original.id)} (response) => {
// onHide={() => hideData(props.row.original.id)} if (response) {
setData((prev) =>
prev.filter(
(data) =>
data.id !==
props.row.original.id
)
);
} else {
alert("Failed to delete row!");
}
}
);
}}
onHide={() => hideData(props.row.original.id)}
visible={props.row.original.visible}
/> />
), ),
}) })
); );
}
// Searching // Searching
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
@ -186,7 +222,7 @@ export default function Table<T extends DataPoint>({
scope="col" scope="col"
className={ className={
"p-2 border-gray-200 border-y font-medium " + "p-2 border-gray-200 border-y font-medium " +
(1 < i && i < columns.length - 1 (0 + offset < i && i < columns.length - 1
? "border-x" ? "border-x"
: "") : "")
} }
@ -207,8 +243,7 @@ export default function Table<T extends DataPoint>({
{table.getRowModel().rows.map((row) => { {table.getRowModel().rows.map((row) => {
// Individual row // Individual row
const isDataVisible = row.original.visible; const isDataVisible = row.original.visible;
const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${ const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${!isDataVisible ? "bg-gray-200 text-gray-500" : ""
!isDataVisible ? "bg-gray-200 text-gray-500" : ""
}`; }`;
return ( return (
<tr className={rowClassNames} key={row.id}> <tr className={rowClassNames} key={row.id}>
@ -216,7 +251,7 @@ export default function Table<T extends DataPoint>({
<td <td
key={cell.id} key={cell.id}
className={ className={
"[&:nth-child(n+3)]:border-x relative first:text-left first:px-0 last:border-none" `[&:nth-child(n+${2 + offset})]:border-x relative first:text-left first:px-0 last:border-none`
} }
> >
{flexRender( {flexRender(
@ -248,10 +283,13 @@ export default function Table<T extends DataPoint>({
createRow(newItem).then((response) => { createRow(newItem).then((response) => {
if (response.ok) { if (response.ok) {
newItem.visible = true; newItem.visible = true;
response.json().then((data) => {
newItem.id = data.id;
setData((prev) => [ setData((prev) => [
...prev, ...prev,
newItem, newItem,
]); ]);
});
} }
}); });

View File

@ -143,6 +143,7 @@ export default function UserTable({ data, setData, user }: UserTableProps) {
columns={columns} columns={columns}
details={userDetails} details={userDetails}
createEndpoint={`/api/user/create?uuid=${user?.uuid}`} createEndpoint={`/api/user/create?uuid=${user?.uuid}`}
deleteEndpoint={`/api/user/delete?uuid=${user?.uuid}`}
isAdmin={user?.role === "ADMIN"} isAdmin={user?.role === "ADMIN"}
/> />
); );