mirror of
https://github.com/cssgunc/compass.git
synced 2025-04-28 05:49:49 -04:00
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:
parent
bdc6600a3f
commit
469236cb04
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"}
|
||||||
|
|
|
@ -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
|
||||||
|
|
39
compass/app/api/resource/delete/route.ts
Normal file
39
compass/app/api/resource/delete/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
39
compass/app/api/service/delete/route.ts
Normal file
39
compass/app/api/service/delete/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
39
compass/app/api/user/delete/route.ts
Normal file
39
compass/app/api/user/delete/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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">
|
||||||
|
|
|
@ -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"}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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"}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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,
|
||||||
]);
|
]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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"}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue
Block a user