mirror of
https://github.com/cssgunc/compass.git
synced 2025-04-21 18:59:49 -04:00
Compare commits
No commits in common. "1c310612e4c4d5992a85f96224c7ea3815751ac5" and "61dcfde469e48bceb7942e3b896630c6147a1d70" have entirely different histories.
1c310612e4
...
61dcfde469
|
@ -47,6 +47,9 @@ RUN mkdir -p /etc/apt/keyrings \
|
||||||
&& npm install -g npm@latest \
|
&& npm install -g npm@latest \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install Angular CLI Globally
|
||||||
|
RUN npm install -g @angular/cli
|
||||||
|
|
||||||
# Use a non-root user per https://code.visualstudio.com/remote/advancedcontainers/add-nonroot-user
|
# Use a non-root user per https://code.visualstudio.com/remote/advancedcontainers/add-nonroot-user
|
||||||
ARG USERNAME=vscode
|
ARG USERNAME=vscode
|
||||||
ARG USER_UID=1000
|
ARG USER_UID=1000
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import jwt
|
import jwt
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta
|
||||||
from fastapi import Depends, HTTPException, Request, Response, status, APIRouter
|
from fastapi import Depends, HTTPException, status, APIRouter
|
||||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
|
|
||||||
from backend.models.user_model import User
|
from backend.models.user_model import User
|
||||||
from backend.services import UserService
|
from ..services import UserService
|
||||||
from backend.env import getenv
|
|
||||||
|
|
||||||
|
auth_router = APIRouter()
|
||||||
api = APIRouter(prefix="/api/authentication")
|
api = APIRouter(prefix="/api/authentication")
|
||||||
|
|
||||||
openapi_tags = {
|
openapi_tags = {
|
||||||
|
@ -14,37 +13,22 @@ openapi_tags = {
|
||||||
"description": "Authentication of users and distributes bearer tokens",
|
"description": "Authentication of users and distributes bearer tokens",
|
||||||
}
|
}
|
||||||
|
|
||||||
JWT_SECRET = getenv("JWT_SECRET")
|
JWT_SECRET = "Sample Secret"
|
||||||
JWT_ALGORITHM = getenv("JWT_ALGORITHM")
|
JWT_ALGORITHM = "HS256"
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES = getenv("ACCESS_TOKEN_EXPIRE_MINUTES")
|
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
||||||
REFRESH_TOKEN_EXPIRE_DAYS = getenv("REFRESH_TOKEN_EXPIRE_DAYS")
|
|
||||||
|
|
||||||
def create_access_token(user_id: str) -> str:
|
def create_access_token(user_id: str) -> str:
|
||||||
expiration = datetime.now(timezone.utc) + timedelta(minutes=int(ACCESS_TOKEN_EXPIRE_MINUTES))
|
expiration = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
payload = {"user_id": user_id, "exp": expiration}
|
|
||||||
token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
|
|
||||||
return token
|
|
||||||
|
|
||||||
def create_refresh_token(user_id: str) -> str:
|
|
||||||
expiration = datetime.now(timezone.utc) + timedelta(days=int(REFRESH_TOKEN_EXPIRE_DAYS))
|
|
||||||
payload = {"user_id": user_id, "exp": expiration}
|
payload = {"user_id": user_id, "exp": expiration}
|
||||||
token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
|
token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
|
||||||
return token
|
return token
|
||||||
|
|
||||||
def registered_user(
|
def registered_user(
|
||||||
request: Request,
|
token: HTTPAuthorizationCredentials = Depends(HTTPBearer()),
|
||||||
user_service: UserService = Depends()
|
user_service: UserService = Depends()
|
||||||
) -> User:
|
) -> User:
|
||||||
access_token = request.cookies.get("access_token")
|
|
||||||
|
|
||||||
if not access_token:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
detail="Missing access token"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payload = jwt.decode(access_token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
|
payload = jwt.decode(token.credentials, JWT_SECRET, algorithms=[JWT_ALGORITHM])
|
||||||
user_id = payload.get("user_id")
|
user_id = payload.get("user_id")
|
||||||
|
|
||||||
user = user_service.get_user_by_id(user_id)
|
user = user_service.get_user_by_id(user_id)
|
||||||
|
@ -65,8 +49,8 @@ def registered_user(
|
||||||
detail="Invalid token"
|
detail="Invalid token"
|
||||||
)
|
)
|
||||||
|
|
||||||
@api.post("", tags=["Authentication"])
|
@auth_router.post("/api/authentication", tags=["Authentication"])
|
||||||
def return_bearer_token(user_id: str, response: Response, user_service: UserService = Depends()):
|
def return_bearer_token(user_id: str, user_service: UserService = Depends()):
|
||||||
user = user_service.get_user_by_id(user_id)
|
user = user_service.get_user_by_id(user_id)
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
@ -74,18 +58,9 @@ def return_bearer_token(user_id: str, response: Response, user_service: UserServ
|
||||||
detail="Invalid user ID"
|
detail="Invalid user ID"
|
||||||
)
|
)
|
||||||
|
|
||||||
access_token = create_access_token(user_id)
|
access_token = create_access_token(user_id=user_id)
|
||||||
refresh_token = create_refresh_token(user_id)
|
return {"access_token": access_token}
|
||||||
|
|
||||||
response.set_cookie(
|
@auth_router.get("/api/authentication", tags=["Authentication"])
|
||||||
key="access_token", value=access_token, httponly=True, secure=True, max_age=ACCESS_TOKEN_EXPIRE_MINUTES * 60
|
|
||||||
)
|
|
||||||
response.set_cookie(
|
|
||||||
key="refresh_token", value=refresh_token, httponly=True, secure=True, max_age=REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"message": "Tokens set as cookies"}
|
|
||||||
|
|
||||||
@api.get("", tags=["Authentication"])
|
|
||||||
def get_user_id(user_service: UserService = Depends()):
|
def get_user_id(user_service: UserService = Depends()):
|
||||||
return user_service.all()
|
return user_service.all()
|
|
@ -1,11 +1,10 @@
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from typing import List
|
from typing import List
|
||||||
from fastapi import APIRouter, Depends
|
|
||||||
|
|
||||||
from backend.models.user_model import User
|
|
||||||
from ..services import ResourceService, UserService
|
|
||||||
from ..models.resource_model import Resource
|
|
||||||
|
|
||||||
from .authentication import registered_user
|
from .authentication import registered_user
|
||||||
|
from backend.models.user_model import User
|
||||||
|
from ..services import ResourceService
|
||||||
|
from ..models.resource_model import Resource
|
||||||
|
|
||||||
api = APIRouter(prefix="/api/resource")
|
api = APIRouter(prefix="/api/resource")
|
||||||
|
|
||||||
|
@ -14,40 +13,33 @@ openapi_tags = {
|
||||||
"description": "Resource search and related operations.",
|
"description": "Resource search and related operations.",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# TODO: Add security using HTTP Bearer Tokens
|
|
||||||
# TODO: Enable authorization by passing user uuid to API
|
|
||||||
# TODO: Create custom exceptions
|
|
||||||
@api.post("", response_model=Resource, tags=["Resource"])
|
@api.post("", response_model=Resource, tags=["Resource"])
|
||||||
def create(
|
def create(
|
||||||
resource: Resource, subject: User = Depends(registered_user), resource_svc: ResourceService = Depends()
|
subject: User = Depends(registered_user),
|
||||||
|
resource: Resource = Depends(),
|
||||||
|
resource_svc: ResourceService = Depends(),
|
||||||
):
|
):
|
||||||
return resource_svc.create(subject, resource)
|
return resource_svc.create(subject, resource)
|
||||||
|
|
||||||
|
|
||||||
@api.get("", response_model=List[Resource], tags=["Resource"])
|
@api.get("", response_model=List[Resource], tags=["Resource"])
|
||||||
def get_all(
|
def get_all(
|
||||||
subject: User = Depends(registered_user), resource_svc: ResourceService = Depends()
|
subject: User = Depends(registered_user),
|
||||||
|
resource_svc: ResourceService = Depends()
|
||||||
):
|
):
|
||||||
return resource_svc.get_resource_by_user(subject)
|
return resource_svc.get_resource_by_user(subject)
|
||||||
|
|
||||||
@api.get("/{name}", response_model=Resource, tags=["Resource"])
|
|
||||||
def get_by_name(
|
|
||||||
name:str, subject: User = Depends(registered_user), resource_svc: ResourceService = Depends()
|
|
||||||
):
|
|
||||||
return resource_svc.get_resource_by_name(name, subject)
|
|
||||||
|
|
||||||
|
|
||||||
@api.put("", response_model=Resource, tags=["Resource"])
|
@api.put("", response_model=Resource, tags=["Resource"])
|
||||||
def update(
|
def update(
|
||||||
uuid: str, resource: Resource, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends()
|
subject: User = Depends(registered_user),
|
||||||
|
resource: Resource = Depends(),
|
||||||
|
resource_svc: ResourceService = Depends(),
|
||||||
):
|
):
|
||||||
subject = user_svc.get_user_by_uuid(uuid)
|
|
||||||
return resource_svc.update(subject, resource)
|
return resource_svc.update(subject, resource)
|
||||||
|
|
||||||
|
|
||||||
@api.delete("", response_model=None, tags=["Resource"])
|
@api.delete("", response_model=None, tags=["Resource"])
|
||||||
def delete(
|
def delete(
|
||||||
resource: Resource, subject: User = Depends(registered_user), resource_svc: ResourceService = Depends()
|
subject: User = Depends(registered_user),
|
||||||
|
resource: Resource = Depends(),
|
||||||
|
resource_svc: ResourceService = Depends(),
|
||||||
):
|
):
|
||||||
resource_svc.delete(subject, resource)
|
resource_svc.delete(subject, resource)
|
|
@ -1,11 +1,10 @@
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from typing import List
|
from typing import List
|
||||||
from fastapi import APIRouter, Depends
|
|
||||||
|
|
||||||
from backend.models.user_model import User
|
|
||||||
from ..services import ServiceService, UserService
|
|
||||||
from ..models.service_model import Service
|
|
||||||
|
|
||||||
from .authentication import registered_user
|
from .authentication import registered_user
|
||||||
|
from backend.models.user_model import User
|
||||||
|
from ..services import ServiceService
|
||||||
|
from ..models.service_model import Service
|
||||||
|
|
||||||
api = APIRouter(prefix="/api/service")
|
api = APIRouter(prefix="/api/service")
|
||||||
|
|
||||||
|
@ -14,37 +13,33 @@ openapi_tags = {
|
||||||
"description": "Service search and related operations.",
|
"description": "Service search and related operations.",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# TODO: Add security using HTTP Bearer Tokens
|
|
||||||
# TODO: Enable authorization by passing user uuid to API
|
|
||||||
# TODO: Create custom exceptions
|
|
||||||
@api.post("", response_model=Service, tags=["Service"])
|
@api.post("", response_model=Service, tags=["Service"])
|
||||||
def create(
|
def create(
|
||||||
service: Service, subject: User = Depends(registered_user), service_svc: ServiceService = Depends()
|
subject: User = Depends(registered_user),
|
||||||
|
service: Service = Depends(),
|
||||||
|
service_svc: ServiceService = Depends(),
|
||||||
):
|
):
|
||||||
return service_svc.create(subject, service)
|
return service_svc.create(subject, service)
|
||||||
|
|
||||||
|
|
||||||
@api.get("", response_model=List[Service], tags=["Service"])
|
@api.get("", response_model=List[Service], tags=["Service"])
|
||||||
def get_all(
|
def get_all(
|
||||||
subject: User = Depends(registered_user), service_svc: ServiceService = Depends()
|
subject: User = Depends(registered_user),
|
||||||
|
service_svc: ServiceService = Depends()
|
||||||
):
|
):
|
||||||
return service_svc.get_service_by_user(subject)
|
return service_svc.get_service_by_user(subject)
|
||||||
|
|
||||||
@api.get("/{name}", response_model=Service, tags=["Service"])
|
|
||||||
def get_by_name(
|
|
||||||
name: str, subject: User = Depends(registered_user), service_svc: ServiceService = Depends()
|
|
||||||
):
|
|
||||||
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(
|
||||||
service: Service, subject: User = Depends(registered_user), service_svc: ServiceService = Depends()
|
subject: User = Depends(registered_user),
|
||||||
|
service: Service = Depends(),
|
||||||
|
service_svc: ServiceService = Depends(),
|
||||||
):
|
):
|
||||||
return service_svc.update(subject, service)
|
return service_svc.update(subject, service)
|
||||||
|
|
||||||
@api.delete("", response_model=None, tags=["Service"])
|
@api.delete("", response_model=None, tags=["Service"])
|
||||||
def delete(
|
def delete(
|
||||||
service: Service, subject: User = Depends(registered_user), service_svc: ServiceService = Depends()
|
subject: User = Depends(registered_user),
|
||||||
|
service: Service = Depends(),
|
||||||
|
service_svc: ServiceService = Depends(),
|
||||||
):
|
):
|
||||||
service_svc.delete(subject, service)
|
service_svc.delete(subject, service)
|
|
@ -1,13 +1,11 @@
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from .authentication import registered_user
|
||||||
from backend.models.tag_model import Tag
|
from backend.models.tag_model import Tag
|
||||||
from backend.models.user_model import User
|
from backend.models.user_model import User
|
||||||
from backend.services.tag import TagService
|
from backend.services.tag import TagService
|
||||||
|
|
||||||
from .authentication import registered_user
|
|
||||||
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
api = APIRouter(prefix="/api/tag")
|
api = APIRouter(prefix="/api/tag")
|
||||||
|
|
||||||
openapi_tags = {
|
openapi_tags = {
|
||||||
|
@ -15,25 +13,33 @@ openapi_tags = {
|
||||||
"description": "Tag CRUD operations.",
|
"description": "Tag CRUD operations.",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# TODO: Add security using HTTP Bearer Tokens
|
|
||||||
# TODO: Enable authorization by passing user uuid to API
|
|
||||||
# TODO: Create custom exceptions
|
|
||||||
@api.post("", response_model=Tag, tags=["Tag"])
|
@api.post("", response_model=Tag, tags=["Tag"])
|
||||||
def create(subject: User, tag: Tag, tag_service: TagService = Depends()):
|
def create(
|
||||||
|
subject: User = Depends(registered_user),
|
||||||
|
tag: Tag = Depends(),
|
||||||
|
tag_service: TagService = Depends(),
|
||||||
|
):
|
||||||
return tag_service.create(subject, tag)
|
return tag_service.create(subject, tag)
|
||||||
|
|
||||||
|
|
||||||
@api.get("", response_model=List[Tag], tags=["Tag"])
|
@api.get("", response_model=List[Tag], tags=["Tag"])
|
||||||
def get_all(subject: User, tag_svc: TagService = Depends()):
|
def get_all(
|
||||||
|
subject: User = Depends(registered_user),
|
||||||
|
tag_svc: TagService = Depends()
|
||||||
|
):
|
||||||
return tag_svc.get_all()
|
return tag_svc.get_all()
|
||||||
|
|
||||||
|
|
||||||
@api.put("", response_model=Tag, tags=["Tag"])
|
@api.put("", response_model=Tag, tags=["Tag"])
|
||||||
def update(subject: User, tag: Tag, tag_svc: TagService = Depends()):
|
def update(
|
||||||
return tag_svc.delete(subject, tag)
|
subject: User = Depends(registered_user),
|
||||||
|
tag: Tag = Depends(),
|
||||||
|
tag_svc: TagService = Depends(),
|
||||||
|
):
|
||||||
|
return tag_svc.update(subject, tag)
|
||||||
|
|
||||||
@api.delete("", response_model=None, tags=["Tag"])
|
@api.delete("", response_model=None, tags=["Tag"])
|
||||||
def delete(subject: User, tag: Tag, tag_svc: TagService = Depends()):
|
def delete(
|
||||||
tag_svc.delete(subject, tag)
|
subject: User = Depends(registered_user),
|
||||||
|
tag: Tag = Depends(),
|
||||||
|
tag_svc: TagService = Depends(),
|
||||||
|
):
|
||||||
|
tag_svc.delete(subject, tag)
|
|
@ -1,147 +0,0 @@
|
||||||
# Synopsis
|
|
||||||
Collection of sample curl requests for api routes.
|
|
||||||
|
|
||||||
# Resources
|
|
||||||
## Get All
|
|
||||||
Given an admin UUID, gets all of the resources from ResourceEntity.
|
|
||||||
```
|
|
||||||
curl -X 'GET' \
|
|
||||||
'http://127.0.0.1:8000/api/resource?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \
|
|
||||||
-H 'accept: application/json'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Get by Name
|
|
||||||
Given the name of a resource and an admin UUID, gets a resource from ResourceEntity by name.
|
|
||||||
```
|
|
||||||
curl -X 'GET' \
|
|
||||||
'http://127.0.0.1:8000/api/resource/Financial%20Empowerment%20Center?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \
|
|
||||||
-H 'accept: application/json'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Create
|
|
||||||
Given an admin UUID and a new resource object, adds a resource to ResourceEntity.
|
|
||||||
```
|
|
||||||
curl -X 'POST' \
|
|
||||||
'http://127.0.0.1:8000/api/resource?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \
|
|
||||||
-H 'accept: application/json' \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d '{
|
|
||||||
"id": 25,
|
|
||||||
"name": "algorithms and analysis textbook",
|
|
||||||
"summary": "textbook written by kevin sun for c550",
|
|
||||||
"link": "kevinsun.org",
|
|
||||||
"program": "DOMESTIC",
|
|
||||||
"created_at": "2024-11-04T20:07:31.875166"
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Update
|
|
||||||
Given an admin UUID and a modified resource object, updates the resource with a matching ID if it exists.
|
|
||||||
```
|
|
||||||
curl -X 'PUT' \
|
|
||||||
'http://127.0.0.1:8000/api/resource?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \
|
|
||||||
-H 'accept: application/json' \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d '{
|
|
||||||
"id": 25,
|
|
||||||
"name": "algorithms and analysis textbook",
|
|
||||||
"summary": "textbook written by the goat himself, kevin sun, for c550",
|
|
||||||
"link": "kevinsun.org",
|
|
||||||
"program": "DOMESTIC",
|
|
||||||
"created_at": "2024-11-04T20:07:31.875166"
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Delete
|
|
||||||
Given an admin UUID and a resource object, deletes the resource with a matching ID if it exists.
|
|
||||||
```
|
|
||||||
curl -X 'DELETE' \
|
|
||||||
'http://127.0.0.1:8000/api/resource?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \
|
|
||||||
-H 'accept: application/json' \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d '{
|
|
||||||
"id": 25,
|
|
||||||
"name": "algorithms and analysis textbook",
|
|
||||||
"summary": "textbook written by the goat himself, kevin sun, for c550",
|
|
||||||
"link": "kevinsun.org",
|
|
||||||
"program": "DOMESTIC",
|
|
||||||
"created_at": "2024-11-04T20:07:31.875166"
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
# Services
|
|
||||||
## Get All
|
|
||||||
Given an admin UUID, gets all of the services from ServiceEntity.
|
|
||||||
```
|
|
||||||
curl -X 'GET' \
|
|
||||||
'http://127.0.0.1:8000/api/service?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \
|
|
||||||
-H 'accept: application/json'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Get by Name
|
|
||||||
Given the name of a service and an admin UUID, gets a service from ServiceEntity by name.
|
|
||||||
```
|
|
||||||
curl -X 'GET' \
|
|
||||||
'http://127.0.0.1:8000/api/service/Shelter%20Placement?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \
|
|
||||||
-H 'accept: application/json'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Create
|
|
||||||
Given an admin UUID and a new service object, adds a service to ServiceEntity.
|
|
||||||
```
|
|
||||||
curl -X 'POST' \
|
|
||||||
'http://127.0.0.1:8000/api/service?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \
|
|
||||||
-H 'accept: application/json' \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d '{
|
|
||||||
"id": 25,
|
|
||||||
"created_at": "2024-11-04T20:07:31.890412",
|
|
||||||
"name": "c550 tutoring",
|
|
||||||
"status": "open",
|
|
||||||
"summary": "tutoring for kevin sun'\''s c550 class",
|
|
||||||
"requirements": [
|
|
||||||
"must be in c550"
|
|
||||||
],
|
|
||||||
"program": "COMMUNITY"
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Update
|
|
||||||
Given an admin UUID and a modified service object, updates the service with a matching ID if it exists.
|
|
||||||
```
|
|
||||||
curl -X 'PUT' \
|
|
||||||
'http://127.0.0.1:8000/api/service?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \
|
|
||||||
-H 'accept: application/json' \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d '{
|
|
||||||
"id": 25,
|
|
||||||
"created_at": "2024-11-04T20:07:31.890412",
|
|
||||||
"name": "c550 tutoring",
|
|
||||||
"status": "closed",
|
|
||||||
"summary": "tutoring for kevin sun'\''s c550 class",
|
|
||||||
"requirements": [
|
|
||||||
"must be in c550"
|
|
||||||
],
|
|
||||||
"program": "COMMUNITY"
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Delete
|
|
||||||
Given an admin UUID and a service object, deletes the service with a matching ID if it exists.
|
|
||||||
```
|
|
||||||
curl -X 'DELETE' \
|
|
||||||
'http://127.0.0.1:8000/api/service?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \
|
|
||||||
-H 'accept: application/json' \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d '{
|
|
||||||
"id": 25,
|
|
||||||
"created_at": "2024-11-04T20:07:31.890412",
|
|
||||||
"name": "c550 tutoring",
|
|
||||||
"status": "closed",
|
|
||||||
"summary": "tutoring for kevin sun'\''s c550 class",
|
|
||||||
"requirements": [
|
|
||||||
"must be in c550"
|
|
||||||
],
|
|
||||||
"program": "COMMUNITY"
|
|
||||||
}'
|
|
||||||
```
|
|
|
@ -26,11 +26,13 @@ app = FastAPI(
|
||||||
|
|
||||||
app.add_middleware(GZipMiddleware)
|
app.add_middleware(GZipMiddleware)
|
||||||
|
|
||||||
feature_apis = [user, health, service, resource, tag, authentication]
|
feature_apis = [user, health, service, resource, tag]
|
||||||
|
|
||||||
for feature_api in feature_apis:
|
for feature_api in feature_apis:
|
||||||
app.include_router(feature_api.api)
|
app.include_router(feature_api.api)
|
||||||
|
|
||||||
|
app.include_router(authentication.auth_router)
|
||||||
|
|
||||||
# Add application-wide exception handling middleware for commonly encountered API Exceptions
|
# Add application-wide exception handling middleware for commonly encountered API Exceptions
|
||||||
@app.exception_handler(Exception)
|
@app.exception_handler(Exception)
|
||||||
def permission_exception_handler(request: Request, e: Exception):
|
def permission_exception_handler(request: Request, e: Exception):
|
||||||
|
|
|
@ -12,4 +12,4 @@ class Resource(BaseModel):
|
||||||
summary: str = Field(..., max_length=300, description="The summary of the resource")
|
summary: str = Field(..., max_length=300, description="The summary of the resource")
|
||||||
link: str = Field(..., max_length=150, description="link to the resource")
|
link: str = Field(..., max_length=150, description="link to the resource")
|
||||||
program: ProgramTypeEnum
|
program: ProgramTypeEnum
|
||||||
created_at: Optional[datetime] = datetime.now()
|
created_at: Optional[datetime]
|
||||||
|
|
|
@ -8,7 +8,7 @@ from .enum_for_models import ProgramTypeEnum
|
||||||
|
|
||||||
class Service(BaseModel):
|
class Service(BaseModel):
|
||||||
id: int | None = None
|
id: int | None = None
|
||||||
created_at: datetime | None = datetime.now()
|
created_at: datetime | None = None
|
||||||
name: str
|
name: str
|
||||||
status: str
|
status: str
|
||||||
summary: str
|
summary: str
|
||||||
|
|
|
@ -10,4 +10,4 @@ class Tag(BaseModel):
|
||||||
content: str = Field(
|
content: str = Field(
|
||||||
..., max_length=600, description="content associated with the tag"
|
..., max_length=600, description="content associated with the tag"
|
||||||
)
|
)
|
||||||
created_at: datetime | None = datetime.now()
|
created_at: datetime | None = None
|
||||||
|
|
|
@ -14,5 +14,5 @@ class User(BaseModel):
|
||||||
group: str
|
group: str
|
||||||
program: List[ProgramTypeEnum]
|
program: List[ProgramTypeEnum]
|
||||||
role: UserTypeEnum
|
role: UserTypeEnum
|
||||||
created_at: Optional[datetime] = datetime.now()
|
created_at: Optional[datetime]
|
||||||
uuid: str | None = None
|
uuid: str | None = None
|
||||||
|
|
|
@ -20,8 +20,6 @@ class UserPermissionException(Exception):
|
||||||
class ServiceNotFoundException(Exception):
|
class ServiceNotFoundException(Exception):
|
||||||
"""Exception for when the service being requested is not in the table."""
|
"""Exception for when the service being requested is not in the table."""
|
||||||
|
|
||||||
class TagNotFoundException(Exception):
|
|
||||||
"""Exception for when the tag being requested is not in the table."""
|
|
||||||
|
|
||||||
class ProgramNotAssignedException(Exception):
|
class ProgramNotAssignedException(Exception):
|
||||||
"""Exception for when the user does not have correct access for requested services."""
|
"""Exception for when the user does not have correct access for requested services."""
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
from ..database import db_session
|
from ..database import db_session
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy import and_, select
|
from sqlalchemy import select
|
||||||
from ..models.resource_model import Resource
|
from ..models.resource_model import Resource
|
||||||
from ..entities.resource_entity import ResourceEntity
|
from ..entities.resource_entity import ResourceEntity
|
||||||
from ..models.user_model import User, UserTypeEnum
|
from ..models.user_model import User, UserTypeEnum
|
||||||
|
|
||||||
from .exceptions import ProgramNotAssignedException, ResourceNotFoundException
|
from .exceptions import ResourceNotFoundException
|
||||||
|
|
||||||
|
|
||||||
class ResourceService:
|
class ResourceService:
|
||||||
|
@ -24,115 +24,137 @@ class ResourceService:
|
||||||
programs = subject.program
|
programs = subject.program
|
||||||
resources = []
|
resources = []
|
||||||
for program in programs:
|
for program in programs:
|
||||||
entities = (
|
entities = self._session.query(ResourceEntity).where(ResourceEntity.program == program).all()
|
||||||
self._session.query(ResourceEntity)
|
|
||||||
.where(ResourceEntity.program == program)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
for entity in entities:
|
for entity in entities:
|
||||||
resources.append(entity.to_model())
|
resources.append(entity.to_model())
|
||||||
return [resource for resource in resources]
|
return [resource for resource in resources]
|
||||||
|
|
||||||
def get_resource_by_name(self, name: str, subject: User) -> Resource:
|
|
||||||
"""Get a resource by name."""
|
|
||||||
query = select(ResourceEntity).where(
|
|
||||||
and_(
|
|
||||||
ResourceEntity.name == name, ResourceEntity.program.in_(subject.program)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
entity = self._session.scalars(query).one_or_none()
|
|
||||||
if entity is None:
|
|
||||||
raise ResourceNotFoundException(
|
|
||||||
f"Resource with name: {name} does not exist or program has not been assigned."
|
|
||||||
)
|
|
||||||
return entity.to_model()
|
|
||||||
|
|
||||||
def create(self, subject: User, resource: Resource) -> Resource:
|
def create(self, subject: User, resource: Resource) -> Resource:
|
||||||
"""
|
"""
|
||||||
Creates a resource based on the input object and adds it to the table if the user has the right permissions.
|
Creates a resource based on the input object and adds it to the table if the user has the right permissions.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
user: a valid User model representing the currently logged in User
|
user: a valid User model representing the currently logged in User
|
||||||
resource: Resource object to add to table
|
resource: Resource object to add to table
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Resource: Object added to table
|
Resource: Object added to table
|
||||||
"""
|
"""
|
||||||
if subject.role != UserTypeEnum.ADMIN:
|
# Ask about what the requirements for making a resource are.
|
||||||
raise ProgramNotAssignedException(
|
if resource.role != subject.role or resource.group != subject.group:
|
||||||
f"User is not {UserTypeEnum.ADMIN}, cannot update service"
|
raise PermissionError(
|
||||||
|
"User does not have permission to add resources in this role or group."
|
||||||
)
|
)
|
||||||
|
|
||||||
resource_entity = ResourceEntity.from_model(resource)
|
resource_entity = ResourceEntity.from_model(resource)
|
||||||
self._session.add(resource_entity)
|
self._session.add(resource_entity)
|
||||||
self._session.commit()
|
self._session.commit()
|
||||||
|
|
||||||
return resource_entity.to_model()
|
return resource_entity.to_model()
|
||||||
|
|
||||||
|
def get_by_id(self, user: User, id: int) -> Resource:
|
||||||
|
"""
|
||||||
|
Gets a resource based on the resource id that the user has access to
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
user: a valid User model representing the currently logged in User
|
||||||
|
id: int, the id of the resource
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Resource
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ResourceNotFoundException: If no resource is found with id
|
||||||
|
"""
|
||||||
|
resource = (
|
||||||
|
self._session.query(ResourceEntity)
|
||||||
|
.filter(
|
||||||
|
ResourceEntity.id == id,
|
||||||
|
ResourceEntity.role == user.role,
|
||||||
|
ResourceEntity.group == user.group,
|
||||||
|
)
|
||||||
|
.one_or_none()
|
||||||
|
)
|
||||||
|
|
||||||
|
if resource is None:
|
||||||
|
raise ResourceNotFoundException(f"No resource found with id: {id}")
|
||||||
|
|
||||||
|
return resource.to_model()
|
||||||
|
|
||||||
def update(self, subject: User, resource: Resource) -> Resource:
|
def update(self, subject: User, resource: Resource) -> Resource:
|
||||||
"""
|
"""
|
||||||
Update the resource if the user has access
|
Update the resource if the user has access
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
user: a valid User model representing the currently logged in User
|
user: a valid User model representing the currently logged in User
|
||||||
resource (ResourceEntity): Resource to update
|
resource (ResourceEntity): Resource to update
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Resource: Updated resource object
|
Resource: Updated resource object
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ResourceNotFoundException: If no resource is found with the corresponding ID
|
ResourceNotFoundException: If no resource is found with the corresponding ID
|
||||||
"""
|
"""
|
||||||
if subject.role != UserTypeEnum.ADMIN:
|
if resource.role != subject.role or resource.group != subject.group:
|
||||||
raise ProgramNotAssignedException(
|
raise PermissionError(
|
||||||
f"User is not {UserTypeEnum.ADMIN}, cannot update service"
|
"User does not have permission to update this resource."
|
||||||
)
|
)
|
||||||
|
|
||||||
query = select(ResourceEntity).where(ResourceEntity.id == resource.id)
|
query = select(ResourceEntity).where(ResourceEntity.id == resource.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: {resource.id}"
|
f"No resource found with matching id: {resource.id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
entity.name = resource.name
|
entity.name = resource.name
|
||||||
entity.summary = resource.summary
|
entity.summary = resource.summary
|
||||||
entity.link = resource.link
|
entity.link = resource.link
|
||||||
entity.program = resource.program
|
entity.program = resource.program
|
||||||
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, resource: Resource) -> 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:
|
||||||
user: a valid User model representing the currently logged in User
|
user: a valid User model representing the currently logged in User
|
||||||
id: int, a unique resource id
|
id: int, a unique resource id
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ResourceNotFoundException: If no resource is found with the corresponding id
|
ResourceNotFoundException: If no resource is found with the corresponding id
|
||||||
"""
|
"""
|
||||||
if subject.role != UserTypeEnum.ADMIN:
|
|
||||||
raise ProgramNotAssignedException(
|
|
||||||
f"User is not {UserTypeEnum.ADMIN}, cannot update service"
|
|
||||||
)
|
|
||||||
query = select(ResourceEntity).where(ResourceEntity.id == resource.id)
|
query = select(ResourceEntity).where(ResourceEntity.id == resource.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: {resource.id}")
|
||||||
f"No resource found with matching id: {resource.id}"
|
|
||||||
)
|
|
||||||
self._session.delete(entity)
|
self._session.delete(entity)
|
||||||
self._session.commit()
|
self._session.commit()
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
user: a valid User model representing the currently logged in User
|
user: a valid User model representing the currently logged in User
|
||||||
search_string: a string to search resources by
|
search_string: a string to search resources by
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list[Resource]: list of resources relating to the string
|
list[Resource]: list of resources relating to the string
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ResourceNotFoundException if no resource is found with the corresponding slug
|
ResourceNotFoundException if no resource is found with the corresponding slug
|
||||||
"""
|
"""
|
||||||
query = select(ResourceEntity).where(
|
query = select(ResourceEntity).where(
|
||||||
ResourceEntity.name.ilike(f"%{search_string}%"),
|
ResourceEntity.title.ilike(f"%{search_string}%"),
|
||||||
ResourceEntity.program.in_(user.program),
|
ResourceEntity.role == user.role,
|
||||||
|
ResourceEntity.group == user.group,
|
||||||
)
|
)
|
||||||
entities = self._session.scalars(query).all()
|
entities = self._session.scalars(query).all()
|
||||||
|
|
||||||
if not entities:
|
|
||||||
return []
|
|
||||||
|
|
||||||
return [entity.to_model() for entity in entities]
|
return [entity.to_model() for entity in entities]
|
||||||
|
|
|
@ -27,24 +27,37 @@ class ServiceService:
|
||||||
return [service.to_model() for service in entities]
|
return [service.to_model() for service in entities]
|
||||||
else:
|
else:
|
||||||
programs = subject.program
|
programs = subject.program
|
||||||
services = []
|
resources = []
|
||||||
for program in programs:
|
for program in programs:
|
||||||
entities = self._session.query(ServiceEntity).where(ServiceEntity.program == program).all()
|
entities = self._session.query(ServiceEntity).where(ServiceEntity.program == program).all()
|
||||||
for entity in entities:
|
for entity in entities:
|
||||||
services.append(entity.to_model())
|
resources.append(entity.to_model())
|
||||||
return [service for service in services]
|
return [service for service in resources]
|
||||||
|
|
||||||
def get_service_by_name(self, name: str, subject: User) -> Service:
|
def get_service_by_program(self, program: ProgramTypeEnum) -> list[Service]:
|
||||||
|
"""Service method getting services belonging to a particular program."""
|
||||||
|
query = select(ServiceEntity).filter(ServiceEntity.program == program)
|
||||||
|
entities = self._session.scalars(query)
|
||||||
|
|
||||||
|
return [entity.to_model() for entity in entities]
|
||||||
|
|
||||||
|
def get_service_by_id(self, id: int) -> Service:
|
||||||
"""Service method getting services by id."""
|
"""Service method getting services by id."""
|
||||||
query = select(ServiceEntity).where(
|
query = select(ServiceEntity).filter(ServiceEntity.id == id)
|
||||||
and_(
|
|
||||||
ServiceEntity.name == name, ServiceEntity.program.in_(subject.program)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
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 ServiceNotFoundException(f"Service with name: {name} does not exist or program has not been assigned")
|
raise ServiceNotFoundException(f"Service with id: {id} does not exist")
|
||||||
|
|
||||||
|
return entity.to_model()
|
||||||
|
|
||||||
|
def get_service_by_name(self, name: str) -> Service:
|
||||||
|
"""Service method getting services by id."""
|
||||||
|
query = select(ServiceEntity).filter(ServiceEntity.name == name)
|
||||||
|
entity = self._session.scalars(query).one_or_none()
|
||||||
|
|
||||||
|
if entity is None:
|
||||||
|
raise ServiceNotFoundException(f"Service with name: {name} does not exist")
|
||||||
|
|
||||||
return entity.to_model()
|
return entity.to_model()
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import pytest
|
||||||
from sqlalchemy import Engine, create_engine, text
|
from sqlalchemy import Engine, create_engine, text
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy.exc import OperationalError
|
from sqlalchemy.exc import OperationalError
|
||||||
from .services import user_test_data, tag_test_data, service_test_data, resource_test_data
|
from .services import user_test_data, tag_test_data, service_test_data
|
||||||
|
|
||||||
from ..database import _engine_str
|
from ..database import _engine_str
|
||||||
from ..env import getenv
|
from ..env import getenv
|
||||||
|
@ -57,6 +57,5 @@ def setup_insert_data_fixture(session: Session):
|
||||||
user_test_data.insert_fake_data(session)
|
user_test_data.insert_fake_data(session)
|
||||||
tag_test_data.insert_fake_data(session)
|
tag_test_data.insert_fake_data(session)
|
||||||
service_test_data.insert_fake_data(session)
|
service_test_data.insert_fake_data(session)
|
||||||
resource_test_data.insert_fake_data(session)
|
|
||||||
session.commit()
|
session.commit()
|
||||||
yield
|
yield
|
||||||
|
|
|
@ -6,7 +6,6 @@ from sqlalchemy.orm import Session
|
||||||
from ...services import UserService
|
from ...services import UserService
|
||||||
from ...services import TagService
|
from ...services import TagService
|
||||||
from ...services import ServiceService
|
from ...services import ServiceService
|
||||||
from ...services import ResourceService
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,9 +23,4 @@ def tag_svc(session: Session):
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def service_svc(session: Session):
|
def service_svc(session: Session):
|
||||||
"""This fixture is used to test the ServiceService class"""
|
"""This fixture is used to test the ServiceService class"""
|
||||||
return ServiceService(session)
|
return ServiceService(session)
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def resource_svc(session: Session):
|
|
||||||
"""This fixture is used to test the ResourceService class"""
|
|
||||||
return ResourceService(session)
|
|
|
@ -1,126 +0,0 @@
|
||||||
from backend.models.user_model import User
|
|
||||||
from backend.entities.resource_entity import ResourceEntity
|
|
||||||
from ...models.enum_for_models import ProgramTypeEnum
|
|
||||||
from backend.services.resource import ResourceService
|
|
||||||
from backend.services.tag import TagService
|
|
||||||
from backend.services.exceptions import ResourceNotFoundException
|
|
||||||
from . import resource_test_data
|
|
||||||
from . import user_test_data
|
|
||||||
from .fixtures import resource_svc, user_svc, tag_svc
|
|
||||||
from backend.models.resource_model import Resource
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_resource_by_user_volunteer(resource_svc: ResourceService):
|
|
||||||
""" Test getting resources by a volunteer """
|
|
||||||
resources = resource_svc.get_resource_by_user(user_test_data.volunteer)
|
|
||||||
assert len(resources) == 2
|
|
||||||
assert isinstance(resources[0], Resource)
|
|
||||||
|
|
||||||
def test_get_resources_admin(resource_svc: ResourceService):
|
|
||||||
""" Test getting resources by an admin """
|
|
||||||
resources = resource_svc.get_resource_by_user(user_test_data.admin)
|
|
||||||
assert len(resources) == len(resource_test_data.resources)
|
|
||||||
assert isinstance(resources[0], Resource)
|
|
||||||
|
|
||||||
def test_get_resources_employee(resource_svc: ResourceService):
|
|
||||||
""" Test getting by an employee """
|
|
||||||
resources = resource_svc.get_resource_by_user(user_test_data.employee)
|
|
||||||
assert len(resources) == 5
|
|
||||||
assert isinstance(resources[0], Resource)
|
|
||||||
|
|
||||||
def test_create_resource_admin(resource_svc: ResourceService):
|
|
||||||
""" Test creating resources as an admin """
|
|
||||||
resource = resource_svc.create(user_test_data.admin, resource_test_data.resource6)
|
|
||||||
assert resource.name == resource_test_data.resource6.name
|
|
||||||
assert isinstance(resource, Resource)
|
|
||||||
|
|
||||||
def test_create_not_permitted(resource_svc: ResourceService):
|
|
||||||
""" Test creating resources without permission """
|
|
||||||
with pytest.raises(PermissionError):
|
|
||||||
resource = resource_svc.create(user_test_data.volunteer, resource_test_data.resource1)
|
|
||||||
pytest.fail()
|
|
||||||
|
|
||||||
def test_get_by_id(resource_svc: ResourceService):
|
|
||||||
""" Test getting a resource by id as an admin """
|
|
||||||
test_resource = resource_test_data.resource1
|
|
||||||
resource = resource_svc.get_by_id(user_test_data.admin, test_resource.id)
|
|
||||||
assert resource is not None
|
|
||||||
assert resource.id == test_resource.id
|
|
||||||
assert resource.name == test_resource.name
|
|
||||||
|
|
||||||
def test_get_by_id_no_access(resource_svc: ResourceService):
|
|
||||||
""" Test getting a resourced with an id no accessible to an employee """
|
|
||||||
test_resource = resource_test_data.resource2
|
|
||||||
with pytest.raises(ResourceNotFoundException):
|
|
||||||
resource = resource_svc.get_by_id(user_test_data.employee, test_resource.id)
|
|
||||||
pytest.fail()
|
|
||||||
|
|
||||||
def test_update(resource_svc: ResourceService):
|
|
||||||
""" Test updating a resource by an admin """
|
|
||||||
updated_resource = resource_test_data.resource5_new
|
|
||||||
resource = resource_svc.update(user_test_data.admin, updated_resource)
|
|
||||||
db_resource = resource_svc.get_by_id(user_test_data.admin, resource.id)
|
|
||||||
assert resource.id == updated_resource.id
|
|
||||||
assert resource.name == updated_resource.name
|
|
||||||
assert resource.summary == updated_resource.summary
|
|
||||||
assert resource.link == updated_resource.link
|
|
||||||
assert resource.program == updated_resource.program
|
|
||||||
assert db_resource.id == updated_resource.id
|
|
||||||
assert db_resource.name == updated_resource.name
|
|
||||||
assert db_resource.summary == updated_resource.summary
|
|
||||||
assert db_resource.link == updated_resource.link
|
|
||||||
assert db_resource.program == updated_resource.program
|
|
||||||
|
|
||||||
|
|
||||||
def test_update_no_permission(resource_svc: ResourceService):
|
|
||||||
""" Test updating a resource without permission """
|
|
||||||
with pytest.raises(PermissionError):
|
|
||||||
resource = resource_svc.update(user_test_data.employee, resource_test_data.resource5_new)
|
|
||||||
pytest.fail()
|
|
||||||
|
|
||||||
def test_delete(resource_svc: ResourceService):
|
|
||||||
""" Test deleting a resource as an admin """
|
|
||||||
resource_svc.delete(user_test_data.admin, resource_test_data.resource5.id)
|
|
||||||
resources = resource_svc.get_resource_by_user(user_test_data.admin)
|
|
||||||
assert len(resources) == len(resource_test_data.resources) - 1
|
|
||||||
|
|
||||||
def test_delete_no_permission(resource_svc: ResourceService):
|
|
||||||
""" Test deleting a resource with no permission """
|
|
||||||
with pytest.raises(PermissionError):
|
|
||||||
resource = resource_svc.delete(user_test_data.employee, resource_test_data.resource5.id)
|
|
||||||
pytest.fail()
|
|
||||||
|
|
||||||
def test_get_1_by_slug(resource_svc: ResourceService):
|
|
||||||
""" Test getting 1 resource with a specific search """
|
|
||||||
resource_to_test = resource_test_data.resource1
|
|
||||||
slug = "Resource 1"
|
|
||||||
resources = resource_svc.get_by_slug(user_test_data.admin, slug)
|
|
||||||
assert len(resources) == 1
|
|
||||||
resource = resources[0]
|
|
||||||
assert resource.id == resource_to_test.id
|
|
||||||
assert resource.name == resource_to_test.name
|
|
||||||
assert resource.summary == resource_to_test.summary
|
|
||||||
assert resource.link == resource_to_test.link
|
|
||||||
assert resource.program == resource_to_test.program
|
|
||||||
|
|
||||||
def test_get_by_slug(resource_svc: ResourceService):
|
|
||||||
""" Test a generic search to get all resources """
|
|
||||||
slug = "Resource"
|
|
||||||
resources = resource_svc.get_by_slug(user_test_data.admin, slug)
|
|
||||||
assert len(resources) == 5
|
|
||||||
|
|
||||||
def test_get_by_slug_not_found(resource_svc: ResourceService):
|
|
||||||
""" Test getting a resource that does not exist """
|
|
||||||
slug = "Not Found"
|
|
||||||
resources = resource_svc.get_by_slug(user_test_data.admin, slug)
|
|
||||||
assert len(resources) == 0
|
|
||||||
assert resources == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_by_slug_no_permission(resource_svc: ResourceService):
|
|
||||||
""" Test getting a resource the user does not have access to """
|
|
||||||
slug = "Resource 2"
|
|
||||||
resources = resource_svc.get_by_slug(user_test_data.employee, slug)
|
|
||||||
assert len(resources) == 0
|
|
||||||
assert resources == []
|
|
|
@ -50,24 +50,6 @@ resource5 = Resource(
|
||||||
created_at=datetime(2023, 6, 5, 11, 30, 0),
|
created_at=datetime(2023, 6, 5, 11, 30, 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
resource6 = Resource(
|
|
||||||
id=6,
|
|
||||||
name="Resource 6",
|
|
||||||
summary="New Financial Resource",
|
|
||||||
link="https://example.com/resource6",
|
|
||||||
program=ProgramTypeEnum.ECONOMIC,
|
|
||||||
created_at=datetime(2024, 6, 5, 11, 30, 0),
|
|
||||||
)
|
|
||||||
|
|
||||||
resource5_new = Resource(
|
|
||||||
id=5,
|
|
||||||
name="Resource 5",
|
|
||||||
summary = "Updated shelter and housing resources",
|
|
||||||
link="https://example.com/resource5/new",
|
|
||||||
program=ProgramTypeEnum.DOMESTIC,
|
|
||||||
created_at=datetime(2023, 6, 5, 11, 30, 0),
|
|
||||||
)
|
|
||||||
|
|
||||||
resources = [resource1, resource2, resource3, resource4, resource5]
|
resources = [resource1, resource2, resource3, resource4, resource5]
|
||||||
|
|
||||||
resource_1 = Resource(
|
resource_1 = Resource(
|
||||||
|
@ -284,11 +266,13 @@ def reset_table_id_seq(
|
||||||
next_id: int,
|
next_id: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Reset the ID sequence of an entity table.
|
"""Reset the ID sequence of an entity table.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
session (Session) - A SQLAlchemy Session
|
session (Session) - A SQLAlchemy Session
|
||||||
entity (DeclarativeBase) - The SQLAlchemy Entity table to target
|
entity (DeclarativeBase) - The SQLAlchemy Entity table to target
|
||||||
entity_id_column (MappedColumn) - The ID column (should be an int column)
|
entity_id_column (MappedColumn) - The ID column (should be an int column)
|
||||||
next_id (int) - Where the next inserted, autogenerated ID should begin
|
next_id (int) - Where the next inserted, autogenerated ID should begin
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
None"""
|
None"""
|
||||||
table = entity.__table__
|
table = entity.__table__
|
||||||
|
@ -328,4 +312,4 @@ def insert_fake_data(session: Session):
|
||||||
reset_table_id_seq(session, ResourceEntity, ResourceEntity.id, len(resources) + 1)
|
reset_table_id_seq(session, ResourceEntity, ResourceEntity.id, len(resources) + 1)
|
||||||
|
|
||||||
# Commit all changes
|
# Commit all changes
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import Sidebar from "@/components/Sidebar/Sidebar";
|
import Sidebar from "@/components/Sidebar/Sidebar";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
|
||||||
import { createClient } from "@/utils/supabase/client";
|
import { createClient } from "@/utils/supabase/client";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
@ -13,7 +14,7 @@ export default function RootLayout({
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [user, setUser] = useState<User>();
|
const [user, setUser] = useState<User>();
|
||||||
|
|
||||||
|
@ -55,13 +56,34 @@ export default function RootLayout({
|
||||||
<div className="flex-row">
|
<div className="flex-row">
|
||||||
{user ? (
|
{user ? (
|
||||||
<div>
|
<div>
|
||||||
<Sidebar
|
{/* button to open sidebar */}
|
||||||
setIsSidebarOpen={setIsSidebarOpen}
|
<button
|
||||||
isSidebarOpen={isSidebarOpen}
|
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||||
name={user.username}
|
className={`fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0`}
|
||||||
email={user.email}
|
aria-label={"Open sidebar"}
|
||||||
isAdmin={user.role === Role.ADMIN}
|
>
|
||||||
/>
|
{
|
||||||
|
!isSidebarOpen && (
|
||||||
|
<ChevronDoubleRightIcon className="h-5 w-5" />
|
||||||
|
) // Icon for closing the sidebar
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
{/* sidebar */}
|
||||||
|
<div
|
||||||
|
className={`absolute inset-y-0 left-0 transform ${
|
||||||
|
isSidebarOpen
|
||||||
|
? "translate-x-0"
|
||||||
|
: "-translate-x-full"
|
||||||
|
} w-64 transition duration-300 ease-in-out`}
|
||||||
|
>
|
||||||
|
<Sidebar
|
||||||
|
setIsSidebarOpen={setIsSidebarOpen}
|
||||||
|
name={user.username}
|
||||||
|
email={user.email}
|
||||||
|
isAdmin={user.role === Role.ADMIN}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* page ui */}
|
||||||
<div
|
<div
|
||||||
className={`flex-1 transition duration-300 ease-in-out ${
|
className={`flex-1 transition duration-300 ease-in-out ${
|
||||||
isSidebarOpen ? "ml-64" : "ml-0"
|
isSidebarOpen ? "ml-64" : "ml-0"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { PageLayout } from "@/components/PageLayout";
|
import { PageLayout } from "@/components/PageLayout";
|
||||||
import UserTable from "@/components/Table/UserTable";
|
import { Table } from "@/components/Table/Index";
|
||||||
import User from "@/utils/models/User";
|
import User from "@/utils/models/User";
|
||||||
import { createClient } from "@/utils/supabase/client";
|
import { createClient } from "@/utils/supabase/client";
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ export default function Page() {
|
||||||
<div className="min-h-screen flex flex-col">
|
<div className="min-h-screen flex flex-col">
|
||||||
{/* icon + title */}
|
{/* icon + title */}
|
||||||
<PageLayout title="Users" icon={<UsersIcon />}>
|
<PageLayout title="Users" icon={<UsersIcon />}>
|
||||||
<UserTable data={users} setData={setUsers} />
|
<Table users={users} />
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -9,7 +9,7 @@ export async function GET(request: Request) {
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const uuid = searchParams.get("uuid");
|
const uuid = searchParams.get("uuid");
|
||||||
|
|
||||||
const data = await fetch(`${apiEndpoint}?uuid=${uuid}`);
|
const data = await fetch(`${apiEndpoint}?user_id=${uuid}`);
|
||||||
|
|
||||||
const resourceData: Resource[] = await data.json();
|
const resourceData: Resource[] = await data.json();
|
||||||
// TODO: Remove make every resource visible
|
// TODO: Remove make every resource visible
|
||||||
|
|
|
@ -9,7 +9,7 @@ export async function GET(request: Request) {
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const uuid = searchParams.get("uuid");
|
const uuid = searchParams.get("uuid");
|
||||||
|
|
||||||
const data = await fetch(`${apiEndpoint}?uuid=${uuid}`);
|
const data = await fetch(`${apiEndpoint}?user_id=${uuid}`);
|
||||||
|
|
||||||
const serviceData: Service[] = await data.json();
|
const serviceData: Service[] = await data.json();
|
||||||
// TODO: Remove make every service visible
|
// TODO: Remove make every service visible
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
import Sidebar from "@/components/Sidebar/Sidebar";
|
import Sidebar from "@/components/Sidebar/Sidebar";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
|
||||||
import { createClient } from "@/utils/supabase/client";
|
import { createClient } from "@/utils/supabase/client";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
@ -12,7 +13,7 @@ export default function RootLayout({
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
const [user, setUser] = useState<User>();
|
const [user, setUser] = useState<User>();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@ -44,13 +45,34 @@ export default function RootLayout({
|
||||||
<div className="flex-row">
|
<div className="flex-row">
|
||||||
{user ? (
|
{user ? (
|
||||||
<div>
|
<div>
|
||||||
<Sidebar
|
{/* button to open sidebar */}
|
||||||
name={user.username}
|
<button
|
||||||
email={user.email}
|
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||||
setIsSidebarOpen={setIsSidebarOpen}
|
className={`fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0`}
|
||||||
isSidebarOpen={isSidebarOpen}
|
aria-label={"Open sidebar"}
|
||||||
isAdmin={user.role === Role.ADMIN}
|
>
|
||||||
/>
|
{
|
||||||
|
!isSidebarOpen && (
|
||||||
|
<ChevronDoubleRightIcon className="h-5 w-5" />
|
||||||
|
) // Icon for closing the sidebar
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
{/* sidebar */}
|
||||||
|
<div
|
||||||
|
className={`absolute inset-y-0 left-0 transform ${
|
||||||
|
isSidebarOpen
|
||||||
|
? "translate-x-0"
|
||||||
|
: "-translate-x-full"
|
||||||
|
} w-64 transition duration-300 ease-in-out`}
|
||||||
|
>
|
||||||
|
<Sidebar
|
||||||
|
name={user.username}
|
||||||
|
email={user.email}
|
||||||
|
setIsSidebarOpen={setIsSidebarOpen}
|
||||||
|
isAdmin={user.role === Role.ADMIN}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* page ui */}
|
||||||
<div
|
<div
|
||||||
className={`flex-1 transition duration-300 ease-in-out ${
|
className={`flex-1 transition duration-300 ease-in-out ${
|
||||||
isSidebarOpen ? "ml-64" : "ml-0"
|
isSidebarOpen ? "ml-64" : "ml-0"
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import Sidebar from "@/components/Sidebar/Sidebar";
|
import Sidebar from "@/components/Sidebar/Sidebar";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
|
||||||
import { createClient } from "@/utils/supabase/client";
|
import { createClient } from "@/utils/supabase/client";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
@ -13,7 +14,7 @@ export default function RootLayout({
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [user, setUser] = useState<User>();
|
const [user, setUser] = useState<User>();
|
||||||
|
|
||||||
|
@ -47,14 +48,33 @@ export default function RootLayout({
|
||||||
<div className="flex-row">
|
<div className="flex-row">
|
||||||
{user ? (
|
{user ? (
|
||||||
<div>
|
<div>
|
||||||
<Sidebar
|
{/* button to open sidebar */}
|
||||||
setIsSidebarOpen={setIsSidebarOpen}
|
<button
|
||||||
isSidebarOpen={isSidebarOpen}
|
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||||
name={user.username}
|
className={`fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0`}
|
||||||
email={user.email}
|
aria-label={"Open sidebar"}
|
||||||
isAdmin={user.role === Role.ADMIN}
|
>
|
||||||
/>
|
{
|
||||||
{/* </div>*/}
|
!isSidebarOpen && (
|
||||||
|
<ChevronDoubleRightIcon className="h-5 w-5" />
|
||||||
|
) // Icon for closing the sidebar
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
{/* sidebar */}
|
||||||
|
<div
|
||||||
|
className={`absolute inset-y-0 left-0 transform ${
|
||||||
|
isSidebarOpen
|
||||||
|
? "translate-x-0"
|
||||||
|
: "-translate-x-full"
|
||||||
|
} w-64 transition duration-300 ease-in-out`}
|
||||||
|
>
|
||||||
|
<Sidebar
|
||||||
|
setIsSidebarOpen={setIsSidebarOpen}
|
||||||
|
name={user.username}
|
||||||
|
email={user.email}
|
||||||
|
isAdmin={user.role === Role.ADMIN}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{/* page ui */}
|
{/* page ui */}
|
||||||
<div
|
<div
|
||||||
className={`flex-1 transition duration-300 ease-in-out ${
|
className={`flex-1 transition duration-300 ease-in-out ${
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { PageLayout } from "@/components/PageLayout";
|
import { PageLayout } from "@/components/PageLayout";
|
||||||
|
import { ResourceTable } from "@/components/Table/ResourceIndex";
|
||||||
import Resource from "@/utils/models/Resource";
|
import Resource from "@/utils/models/Resource";
|
||||||
import ResourceTable from "@/components/Table/ResourceTable";
|
|
||||||
import { createClient } from "@/utils/supabase/client";
|
import { createClient } from "@/utils/supabase/client";
|
||||||
|
|
||||||
import { BookmarkIcon } from "@heroicons/react/24/solid";
|
import { BookmarkIcon } from "@heroicons/react/24/solid";
|
||||||
|
@ -38,7 +38,7 @@ export default function Page() {
|
||||||
<div className="min-h-screen flex flex-col">
|
<div className="min-h-screen flex flex-col">
|
||||||
{/* icon + title */}
|
{/* icon + title */}
|
||||||
<PageLayout title="Resources" icon={<BookmarkIcon />}>
|
<PageLayout title="Resources" icon={<BookmarkIcon />}>
|
||||||
<ResourceTable data={resources} setData={setResources} />
|
<ResourceTable users={resources} />
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import Sidebar from "@/components/Sidebar/Sidebar";
|
import Sidebar from "@/components/Sidebar/Sidebar";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
|
||||||
import { createClient } from "@/utils/supabase/client";
|
import { createClient } from "@/utils/supabase/client";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
@ -13,7 +14,7 @@ export default function RootLayout({
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [user, setUser] = useState<User>();
|
const [user, setUser] = useState<User>();
|
||||||
|
|
||||||
|
@ -47,13 +48,34 @@ export default function RootLayout({
|
||||||
<div className="flex-row">
|
<div className="flex-row">
|
||||||
{user ? (
|
{user ? (
|
||||||
<div>
|
<div>
|
||||||
<Sidebar
|
{/* button to open sidebar */}
|
||||||
setIsSidebarOpen={setIsSidebarOpen}
|
<button
|
||||||
isSidebarOpen={isSidebarOpen}
|
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||||
name={user.username}
|
className={`fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0`}
|
||||||
email={user.email}
|
aria-label={"Open sidebar"}
|
||||||
isAdmin={user.role === Role.ADMIN}
|
>
|
||||||
/>
|
{
|
||||||
|
!isSidebarOpen && (
|
||||||
|
<ChevronDoubleRightIcon className="h-5 w-5" />
|
||||||
|
) // Icon for closing the sidebar
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
{/* sidebar */}
|
||||||
|
<div
|
||||||
|
className={`absolute inset-y-0 left-0 transform ${
|
||||||
|
isSidebarOpen
|
||||||
|
? "translate-x-0"
|
||||||
|
: "-translate-x-full"
|
||||||
|
} w-64 transition duration-300 ease-in-out`}
|
||||||
|
>
|
||||||
|
<Sidebar
|
||||||
|
setIsSidebarOpen={setIsSidebarOpen}
|
||||||
|
name={user.username}
|
||||||
|
email={user.email}
|
||||||
|
isAdmin={user.role === Role.ADMIN}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* page ui */}
|
||||||
<div
|
<div
|
||||||
className={`flex-1 transition duration-300 ease-in-out ${
|
className={`flex-1 transition duration-300 ease-in-out ${
|
||||||
isSidebarOpen ? "ml-64" : "ml-0"
|
isSidebarOpen ? "ml-64" : "ml-0"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { PageLayout } from "@/components/PageLayout";
|
import { PageLayout } from "@/components/PageLayout";
|
||||||
import ServiceTable from "@/components/Table/ServiceTable";
|
import { ServiceTable } from "@/components/Table/ServiceIndex";
|
||||||
import Service from "@/utils/models/Service";
|
import Service from "@/utils/models/Service";
|
||||||
import { createClient } from "@/utils/supabase/client";
|
import { createClient } from "@/utils/supabase/client";
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import { ClipboardIcon } from "@heroicons/react/24/solid";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const [services, setServices] = useState<Service[]>([]);
|
const [services, setUsers] = useState<Service[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getServices() {
|
async function getServices() {
|
||||||
|
@ -27,7 +27,7 @@ export default function Page() {
|
||||||
);
|
);
|
||||||
|
|
||||||
const servicesAPI: Service[] = await serviceListData.json();
|
const servicesAPI: Service[] = await serviceListData.json();
|
||||||
setServices(servicesAPI);
|
setUsers(servicesAPI);
|
||||||
}
|
}
|
||||||
|
|
||||||
getServices();
|
getServices();
|
||||||
|
@ -37,7 +37,7 @@ export default function Page() {
|
||||||
<div className="min-h-screen flex flex-col">
|
<div className="min-h-screen flex flex-col">
|
||||||
{/* icon + title */}
|
{/* icon + title */}
|
||||||
<PageLayout title="Services" icon={<ClipboardIcon />}>
|
<PageLayout title="Services" icon={<ClipboardIcon />}>
|
||||||
<ServiceTable data={services} setData={setServices} />
|
<ServiceTable users={services} />
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Dispatch, FunctionComponent, ReactNode, SetStateAction } from "react";
|
import { FunctionComponent, ReactNode } 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 {
|
import {
|
||||||
|
@ -23,7 +23,7 @@ type DrawerProps = {
|
||||||
editableContent?: any;
|
editableContent?: any;
|
||||||
onSave?: (content: any) => void;
|
onSave?: (content: any) => void;
|
||||||
rowContent?: any;
|
rowContent?: any;
|
||||||
setData: Dispatch<SetStateAction<any>>;
|
onRowUpdate?: (content: any) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface EditContent {
|
interface EditContent {
|
||||||
|
@ -37,21 +37,13 @@ const Drawer: FunctionComponent<DrawerProps> = ({
|
||||||
onSave,
|
onSave,
|
||||||
editableContent,
|
editableContent,
|
||||||
rowContent,
|
rowContent,
|
||||||
setData,
|
onRowUpdate,
|
||||||
}) => {
|
}) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
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 onRowUpdate = (updatedRow: any) => {
|
|
||||||
setData((prevData: any) =>
|
|
||||||
prevData.map((row: any) =>
|
|
||||||
row.id === updatedRow.id ? updatedRow : row
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTempRowContentChange = (e) => {
|
const handleTempRowContentChange = (e) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
console.log(name);
|
console.log(name);
|
||||||
|
|
|
@ -2,7 +2,6 @@ import React from "react";
|
||||||
import {
|
import {
|
||||||
HomeIcon,
|
HomeIcon,
|
||||||
ChevronDoubleLeftIcon,
|
ChevronDoubleLeftIcon,
|
||||||
ChevronDoubleRightIcon,
|
|
||||||
BookmarkIcon,
|
BookmarkIcon,
|
||||||
ClipboardIcon,
|
ClipboardIcon,
|
||||||
BookOpenIcon,
|
BookOpenIcon,
|
||||||
|
@ -13,7 +12,6 @@ import { UserProfile } from "../resource/UserProfile";
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
isSidebarOpen: boolean;
|
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
|
@ -21,96 +19,70 @@ interface SidebarProps {
|
||||||
|
|
||||||
const Sidebar: React.FC<SidebarProps> = ({
|
const Sidebar: React.FC<SidebarProps> = ({
|
||||||
setIsSidebarOpen,
|
setIsSidebarOpen,
|
||||||
isSidebarOpen,
|
|
||||||
name,
|
name,
|
||||||
email,
|
email,
|
||||||
isAdmin: admin,
|
isAdmin: admin,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="w-64 h-full border border-gray-200 bg-gray-50 px-4">
|
||||||
{/* Button to open the sidebar. */}
|
{/* button to close sidebar */}
|
||||||
<button
|
<div className="flex justify-end">
|
||||||
onClick={() => setIsSidebarOpen(true)}
|
<button
|
||||||
className={
|
onClick={() => setIsSidebarOpen(false)}
|
||||||
"fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0"
|
className="py-2 text-gray-500 hover:text-gray-800"
|
||||||
}
|
aria-label="Close sidebar"
|
||||||
aria-label={"Open sidebar"}
|
>
|
||||||
>
|
<ChevronDoubleLeftIcon className="h-5 w-5" />
|
||||||
{!isSidebarOpen && (
|
</button>
|
||||||
<ChevronDoubleRightIcon className="h-5 w-5" />
|
</div>
|
||||||
)}
|
<div className="flex flex-col space-y-8">
|
||||||
</button>
|
{/* user + logout button */}
|
||||||
|
<div className="flex items-center p-4 space-x-2 border border-gray-200 rounded-md ">
|
||||||
{/* The sidebar itself. */}
|
<UserProfile name={name} email={email} />
|
||||||
<div
|
|
||||||
className={
|
|
||||||
"fixed left-0 w-64 h-full border border-gray-200 bg-gray-50 px-4 " +
|
|
||||||
(isSidebarOpen
|
|
||||||
? "translate-x-0" // Open
|
|
||||||
: "-translate-x-full opacity-25") + // Closed
|
|
||||||
" transition duration-300 ease-out" // More animation properties
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{/* Button to close sidebar */}
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<button
|
|
||||||
onClick={() => setIsSidebarOpen(false)}
|
|
||||||
className="py-2 text-gray-500 hover:text-gray-800"
|
|
||||||
aria-label="Close sidebar"
|
|
||||||
>
|
|
||||||
<ChevronDoubleLeftIcon className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
{/* navigation menu */}
|
||||||
|
<div className="flex flex-col space-y-2">
|
||||||
|
<h4 className="text-xs font-semibold text-gray-500">
|
||||||
|
Pages
|
||||||
|
</h4>
|
||||||
|
<nav className="flex flex-col">
|
||||||
|
{admin && (
|
||||||
|
<SidebarItem
|
||||||
|
icon={<LockClosedIcon />}
|
||||||
|
text="Admin"
|
||||||
|
active={true}
|
||||||
|
redirect="/admin"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col space-y-8">
|
<SidebarItem
|
||||||
{/* user + logout button */}
|
icon={<HomeIcon />}
|
||||||
<div className="flex items-center p-4 space-x-2 border border-gray-200 rounded-md ">
|
text="Home"
|
||||||
<UserProfile name={name} email={email} />
|
active={true}
|
||||||
</div>
|
redirect="/home"
|
||||||
{/* navigation menu */}
|
/>
|
||||||
<div className="flex flex-col space-y-2">
|
<SidebarItem
|
||||||
<h4 className="text-xs font-semibold text-gray-500">
|
icon={<BookmarkIcon />}
|
||||||
Pages
|
text="Resources"
|
||||||
</h4>
|
active={true}
|
||||||
<nav className="flex flex-col">
|
redirect="/resource"
|
||||||
{admin && (
|
/>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon={<LockClosedIcon />}
|
icon={<ClipboardIcon />}
|
||||||
text="Admin"
|
text="Services"
|
||||||
active={true}
|
active={true}
|
||||||
redirect="/admin"
|
redirect="/service"
|
||||||
/>
|
/>
|
||||||
)}
|
<SidebarItem
|
||||||
|
icon={<BookOpenIcon />}
|
||||||
<SidebarItem
|
text="Training Manuals"
|
||||||
icon={<HomeIcon />}
|
active={true}
|
||||||
text="Home"
|
redirect="/training-manuals"
|
||||||
active={true}
|
/>
|
||||||
redirect="/home"
|
</nav>
|
||||||
/>
|
|
||||||
<SidebarItem
|
|
||||||
icon={<BookmarkIcon />}
|
|
||||||
text="Resources"
|
|
||||||
active={true}
|
|
||||||
redirect="/resource"
|
|
||||||
/>
|
|
||||||
<SidebarItem
|
|
||||||
icon={<ClipboardIcon />}
|
|
||||||
text="Services"
|
|
||||||
active={true}
|
|
||||||
redirect="/service"
|
|
||||||
/>
|
|
||||||
<SidebarItem
|
|
||||||
icon={<BookOpenIcon />}
|
|
||||||
text="Training Manuals"
|
|
||||||
active={true}
|
|
||||||
redirect="/training-manuals"
|
|
||||||
/>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
306
compass/components/Table/Index.tsx
Normal file
306
compass/components/Table/Index.tsx
Normal file
|
@ -0,0 +1,306 @@
|
||||||
|
// for showcasing to compass
|
||||||
|
|
||||||
|
import users from "./users.json";
|
||||||
|
import {
|
||||||
|
Cell,
|
||||||
|
ColumnDef,
|
||||||
|
Row,
|
||||||
|
createColumnHelper,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getFilteredRowModel,
|
||||||
|
sortingFns,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
import {
|
||||||
|
ChangeEvent,
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
FunctionComponent,
|
||||||
|
useRef,
|
||||||
|
ChangeEventHandler,
|
||||||
|
Key,
|
||||||
|
} from "react";
|
||||||
|
import { RowOptionMenu } from "./RowOptionMenu";
|
||||||
|
import { RowOpenAction } from "./RowOpenAction";
|
||||||
|
import { TableAction } from "./TableAction";
|
||||||
|
import {
|
||||||
|
AtSymbolIcon,
|
||||||
|
Bars2Icon,
|
||||||
|
ArrowDownCircleIcon,
|
||||||
|
PlusIcon,
|
||||||
|
} from "@heroicons/react/24/solid";
|
||||||
|
import TagsInput from "../TagsInput/Index";
|
||||||
|
import { rankItem } from "@tanstack/match-sorter-utils";
|
||||||
|
import User from "@/utils/models/User";
|
||||||
|
|
||||||
|
// For search
|
||||||
|
const fuzzyFilter = (
|
||||||
|
row: Row<any>,
|
||||||
|
columnId: string,
|
||||||
|
value: any,
|
||||||
|
addMeta: (meta: any) => void
|
||||||
|
) => {
|
||||||
|
// Rank the item
|
||||||
|
const itemRank = rankItem(row.getValue(columnId), value);
|
||||||
|
|
||||||
|
// Store the ranking info
|
||||||
|
addMeta(itemRank);
|
||||||
|
|
||||||
|
// Return if the item should be filtered in/out
|
||||||
|
return itemRank.passed;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Table = ({ users }: { users: User[] }) => {
|
||||||
|
const columnHelper = createColumnHelper<User>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const sortedUsers = [...users].sort((a, b) =>
|
||||||
|
a.visible === b.visible ? 0 : a.visible ? -1 : 1
|
||||||
|
);
|
||||||
|
setData(sortedUsers);
|
||||||
|
}, [users]);
|
||||||
|
|
||||||
|
const deleteUser = (userId: number) => {
|
||||||
|
console.log(data);
|
||||||
|
setData((currentData) =>
|
||||||
|
currentData.filter((user) => user.id !== userId)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideUser = (userId: number) => {
|
||||||
|
console.log(`Toggling visibility for user with ID: ${userId}`);
|
||||||
|
setData((currentData) => {
|
||||||
|
const newData = currentData
|
||||||
|
.map((user) => {
|
||||||
|
if (user.id === userId) {
|
||||||
|
return { ...user, visible: !user.visible };
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
})
|
||||||
|
.sort((a, b) =>
|
||||||
|
a.visible === b.visible ? 0 : a.visible ? -1 : 1
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(newData);
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const [presetOptions, setPresetOptions] = useState([
|
||||||
|
"administrator",
|
||||||
|
"volunteer",
|
||||||
|
"employee",
|
||||||
|
]);
|
||||||
|
const [tagColors, setTagColors] = useState(new Map());
|
||||||
|
|
||||||
|
const getTagColor = (tag: string) => {
|
||||||
|
if (!tagColors.has(tag)) {
|
||||||
|
const colors = [
|
||||||
|
"bg-cyan-100",
|
||||||
|
"bg-blue-100",
|
||||||
|
"bg-green-100",
|
||||||
|
"bg-yellow-100",
|
||||||
|
"bg-purple-100",
|
||||||
|
];
|
||||||
|
const randomColor =
|
||||||
|
colors[Math.floor(Math.random() * colors.length)];
|
||||||
|
setTagColors(new Map(tagColors).set(tag, randomColor));
|
||||||
|
}
|
||||||
|
return tagColors.get(tag);
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
columnHelper.display({
|
||||||
|
id: "options",
|
||||||
|
cell: (props) => (
|
||||||
|
<RowOptionMenu
|
||||||
|
onDelete={() => deleteUser(props.row.original.id)}
|
||||||
|
onHide={() => hideUser(props.row.original.id)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("username", {
|
||||||
|
header: () => (
|
||||||
|
<>
|
||||||
|
<Bars2Icon className="inline align-top h-4" /> Username
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
cell: (info) => (
|
||||||
|
<RowOpenAction
|
||||||
|
title={info.getValue()}
|
||||||
|
rowData={info.row.original}
|
||||||
|
onRowUpdate={handleRowUpdate}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("role", {
|
||||||
|
header: () => (
|
||||||
|
<>
|
||||||
|
<ArrowDownCircleIcon className="inline align-top h-4" />{" "}
|
||||||
|
Role
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
cell: (info) => (
|
||||||
|
<TagsInput
|
||||||
|
presetValue={info.getValue()}
|
||||||
|
presetOptions={presetOptions}
|
||||||
|
setPresetOptions={setPresetOptions}
|
||||||
|
getTagColor={getTagColor}
|
||||||
|
setTagColors={setTagColors}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("email", {
|
||||||
|
header: () => (
|
||||||
|
<>
|
||||||
|
<AtSymbolIcon className="inline align-top h-4" /> Email
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
cell: (info) => (
|
||||||
|
<span className="ml-2 text-gray-500 underline hover:text-gray-400">
|
||||||
|
{info.getValue()}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("program", {
|
||||||
|
header: () => (
|
||||||
|
<>
|
||||||
|
<ArrowDownCircleIcon className="inline align-top h-4" />{" "}
|
||||||
|
Program
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
cell: (info) => <TagsInput presetValue={info.getValue()} />,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const [data, setData] = useState<User[]>([...users]);
|
||||||
|
|
||||||
|
const addUser = () => {
|
||||||
|
setData([...data]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Searching
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const handleSearchChange = (e: ChangeEvent) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
setQuery(String(target.value));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCellChange = (e: ChangeEvent, key: Key) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
console.log(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Filtering
|
||||||
|
|
||||||
|
// TODO: Sorting
|
||||||
|
|
||||||
|
// added this fn for editing rows
|
||||||
|
const handleRowUpdate = (updatedRow: User) => {
|
||||||
|
const dataIndex = data.findIndex((row) => row.id === updatedRow.id);
|
||||||
|
if (dataIndex !== -1) {
|
||||||
|
const updatedData = [...data];
|
||||||
|
updatedData[dataIndex] = updatedRow;
|
||||||
|
setData(updatedData);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
filterFns: {
|
||||||
|
fuzzy: fuzzyFilter,
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
globalFilter: query,
|
||||||
|
},
|
||||||
|
onGlobalFilterChange: setQuery,
|
||||||
|
globalFilterFn: fuzzyFilter,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRowData = (row: any) => {
|
||||||
|
const rowData: any = {};
|
||||||
|
row.cells.forEach((cell: any) => {
|
||||||
|
rowData[cell.column.id] = cell.value;
|
||||||
|
});
|
||||||
|
// Use rowData object containing data from all columns for the current row
|
||||||
|
console.log(rowData);
|
||||||
|
return rowData;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row justify-end">
|
||||||
|
<TableAction query={query} handleChange={handleSearchChange} />
|
||||||
|
</div>
|
||||||
|
<table className="w-full text-xs text-left rtl:text-right">
|
||||||
|
<thead className="text-xs text-gray-500 capitalize">
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<tr key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header, i) => (
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className={
|
||||||
|
"p-2 border-gray-200 border-y font-medium " +
|
||||||
|
(1 < i && i < columns.length - 1
|
||||||
|
? "border-x"
|
||||||
|
: "")
|
||||||
|
}
|
||||||
|
key={header.id}
|
||||||
|
>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{table.getRowModel().rows.map((row) => {
|
||||||
|
// Individual row
|
||||||
|
const isUserVisible = row.original.visible;
|
||||||
|
const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${
|
||||||
|
!isUserVisible ? "bg-gray-200 text-gray-500" : ""
|
||||||
|
}`;
|
||||||
|
return (
|
||||||
|
<tr className={rowClassNames} key={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell, i) => (
|
||||||
|
<td
|
||||||
|
key={cell.id}
|
||||||
|
className={
|
||||||
|
"[&:nth-child(n+3)]:border-x relative first:text-left first:px-0 last:border-none"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext()
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50"
|
||||||
|
colSpan={100}
|
||||||
|
onClick={addUser}
|
||||||
|
>
|
||||||
|
<span className="flex ml-1 text-gray-500">
|
||||||
|
<PlusIcon className="inline h-4 mr-1" />
|
||||||
|
New
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,32 +1,40 @@
|
||||||
|
// for showcasing to compass
|
||||||
|
|
||||||
|
import users from "./users.json";
|
||||||
import {
|
import {
|
||||||
Row,
|
Cell,
|
||||||
ColumnDef,
|
ColumnDef,
|
||||||
useReactTable,
|
Row,
|
||||||
getCoreRowModel,
|
|
||||||
flexRender,
|
|
||||||
createColumnHelper,
|
createColumnHelper,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getFilteredRowModel,
|
||||||
|
sortingFns,
|
||||||
|
useReactTable,
|
||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
import {
|
import {
|
||||||
ChangeEvent,
|
ChangeEvent,
|
||||||
useState,
|
useState,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
FunctionComponent,
|
||||||
|
useRef,
|
||||||
|
ChangeEventHandler,
|
||||||
Key,
|
Key,
|
||||||
Dispatch,
|
|
||||||
SetStateAction,
|
|
||||||
} from "react";
|
} from "react";
|
||||||
import { TableAction } from "./TableAction";
|
|
||||||
import { PlusIcon } from "@heroicons/react/24/solid";
|
|
||||||
import { rankItem } from "@tanstack/match-sorter-utils";
|
|
||||||
import { RowOptionMenu } from "./RowOptionMenu";
|
import { RowOptionMenu } from "./RowOptionMenu";
|
||||||
import DataPoint from "@/utils/models/DataPoint";
|
import { RowOpenAction } from "./RowOpenAction";
|
||||||
|
import { TableAction } from "./TableAction";
|
||||||
|
import {
|
||||||
|
AtSymbolIcon,
|
||||||
|
Bars2Icon,
|
||||||
|
ArrowDownCircleIcon,
|
||||||
|
PlusIcon,
|
||||||
|
} from "@heroicons/react/24/solid";
|
||||||
|
import TagsInput from "../TagsInput/Index";
|
||||||
|
import { rankItem } from "@tanstack/match-sorter-utils";
|
||||||
|
import Resource from "@/utils/models/Resource";
|
||||||
|
|
||||||
type TableProps<T extends DataPoint> = {
|
// For search
|
||||||
data: T[];
|
|
||||||
setData: Dispatch<SetStateAction<T[]>>;
|
|
||||||
columns: ColumnDef<T, any>[];
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Fuzzy search function */
|
|
||||||
const fuzzyFilter = (
|
const fuzzyFilter = (
|
||||||
row: Row<any>,
|
row: Row<any>,
|
||||||
columnId: string,
|
columnId: string,
|
||||||
|
@ -43,69 +51,131 @@ const fuzzyFilter = (
|
||||||
return itemRank.passed;
|
return itemRank.passed;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// TODO: Rename everything to resources
|
||||||
* General componenet that holds shared functionality for any data table component
|
export const ResourceTable = ({ users }: { users: Resource[] }) => {
|
||||||
* @param props.data Stateful list of data to be held in the table
|
const columnHelper = createColumnHelper<Resource>();
|
||||||
* @param props.setData State setter for the list of data
|
|
||||||
* @param props.columns Column definitions made with Tanstack columnHelper
|
|
||||||
*/
|
|
||||||
export default function Table<T extends DataPoint>({
|
|
||||||
data,
|
|
||||||
setData,
|
|
||||||
columns,
|
|
||||||
}: TableProps<T>) {
|
|
||||||
const columnHelper = createColumnHelper<T>();
|
|
||||||
|
|
||||||
/** Sorting function based on visibility */
|
|
||||||
const visibilitySort = (a: T, b: T) =>
|
|
||||||
a.visible === b.visible ? 0 : a.visible ? -1 : 1;
|
|
||||||
|
|
||||||
// Sort data on load
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setData((prevData) => prevData.sort(visibilitySort));
|
const sortedUsers = [...users].sort((a, b) =>
|
||||||
}, [setData]);
|
a.visible === b.visible ? 0 : a.visible ? -1 : 1
|
||||||
|
);
|
||||||
|
setData(sortedUsers);
|
||||||
|
}, [users]);
|
||||||
|
|
||||||
// Data manipulation methods
|
const deleteUser = (userId: number) => {
|
||||||
// TODO: Connect data manipulation methods to the database (deleteData, hideData, addData)
|
|
||||||
const deleteData = (dataId: number) => {
|
|
||||||
console.log(data);
|
console.log(data);
|
||||||
setData((currentData) =>
|
setData((currentData) =>
|
||||||
currentData.filter((data) => data.id !== dataId)
|
currentData.filter((user) => user.id !== userId)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const hideData = (dataId: number) => {
|
const hideUser = (userId: number) => {
|
||||||
console.log(`Toggling visibility for data with ID: ${dataId}`);
|
console.log(`Toggling visibility for user with ID: ${userId}`);
|
||||||
setData((currentData) => {
|
setData((currentData) => {
|
||||||
const newData = currentData
|
const newData = currentData
|
||||||
.map((data) =>
|
.map((user) => {
|
||||||
data.id === dataId
|
if (user.id === userId) {
|
||||||
? { ...data, visible: !data.visible }
|
return { ...user, visible: !user.visible };
|
||||||
: data
|
}
|
||||||
)
|
return user;
|
||||||
.sort(visibilitySort);
|
})
|
||||||
|
.sort((a, b) =>
|
||||||
|
a.visible === b.visible ? 0 : a.visible ? -1 : 1
|
||||||
|
);
|
||||||
|
|
||||||
console.log(newData);
|
console.log(newData);
|
||||||
return newData;
|
return newData;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
const [presetOptions, setPresetOptions] = useState([
|
||||||
|
"administrator",
|
||||||
|
"volunteer",
|
||||||
|
"employee",
|
||||||
|
]);
|
||||||
|
const [tagColors, setTagColors] = useState(new Map());
|
||||||
|
|
||||||
const addData = () => {
|
const getTagColor = (tag: string) => {
|
||||||
setData([...data]);
|
if (!tagColors.has(tag)) {
|
||||||
|
const colors = [
|
||||||
|
"bg-cyan-100",
|
||||||
|
"bg-blue-100",
|
||||||
|
"bg-green-100",
|
||||||
|
"bg-yellow-100",
|
||||||
|
"bg-purple-100",
|
||||||
|
];
|
||||||
|
const randomColor =
|
||||||
|
colors[Math.floor(Math.random() * colors.length)];
|
||||||
|
setTagColors(new Map(tagColors).set(tag, randomColor));
|
||||||
|
}
|
||||||
|
return tagColors.get(tag);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add data manipulation options to the first column
|
const columns = [
|
||||||
columns.unshift(
|
|
||||||
columnHelper.display({
|
columnHelper.display({
|
||||||
id: "options",
|
id: "options",
|
||||||
cell: (props) => (
|
cell: (props) => (
|
||||||
<RowOptionMenu
|
<RowOptionMenu
|
||||||
onDelete={() => deleteData(props.row.original.id)}
|
onDelete={() => {}}
|
||||||
onHide={() => hideData(props.row.original.id)}
|
onHide={() => hideUser(props.row.original.id)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
})
|
}),
|
||||||
);
|
columnHelper.accessor("name", {
|
||||||
|
header: () => (
|
||||||
|
<>
|
||||||
|
<Bars2Icon className="inline align-top h-4" /> Name
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
cell: (info) => (
|
||||||
|
<RowOpenAction
|
||||||
|
title={info.getValue()}
|
||||||
|
rowData={info.row.original}
|
||||||
|
onRowUpdate={handleRowUpdate}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("link", {
|
||||||
|
header: () => (
|
||||||
|
<>
|
||||||
|
<Bars2Icon className="inline align-top h-4" /> Link
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
cell: (info) => (
|
||||||
|
<a
|
||||||
|
href={info.getValue()}
|
||||||
|
target={"_blank"}
|
||||||
|
className="ml-2 text-gray-500 underline hover:text-gray-400"
|
||||||
|
>
|
||||||
|
{info.getValue()}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("program", {
|
||||||
|
header: () => (
|
||||||
|
<>
|
||||||
|
<Bars2Icon className="inline align-top h-4" /> Program
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
cell: (info) => <TagsInput presetValue={info.getValue()} />,
|
||||||
|
}),
|
||||||
|
|
||||||
|
columnHelper.accessor("summary", {
|
||||||
|
header: () => (
|
||||||
|
<>
|
||||||
|
<Bars2Icon className="inline align-top h-4" /> Summary
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
cell: (info) => (
|
||||||
|
<span className="ml-2 text-gray-500">{info.getValue()}</span>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const [data, setData] = useState<Resource[]>([...users]);
|
||||||
|
|
||||||
|
const addUser = () => {
|
||||||
|
setData([...data]);
|
||||||
|
};
|
||||||
|
|
||||||
// Searching
|
// Searching
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
@ -123,7 +193,16 @@ export default function Table<T extends DataPoint>({
|
||||||
|
|
||||||
// TODO: Sorting
|
// TODO: Sorting
|
||||||
|
|
||||||
// Define Tanstack table
|
// added this fn for editing rows
|
||||||
|
const handleRowUpdate = (updatedRow: Resource) => {
|
||||||
|
const dataIndex = data.findIndex((row) => row.id === updatedRow.id);
|
||||||
|
if (dataIndex !== -1) {
|
||||||
|
const updatedData = [...data];
|
||||||
|
updatedData[dataIndex] = updatedRow;
|
||||||
|
setData(updatedData);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
columns,
|
columns,
|
||||||
data,
|
data,
|
||||||
|
@ -182,9 +261,9 @@ export default function Table<T extends DataPoint>({
|
||||||
<tbody>
|
<tbody>
|
||||||
{table.getRowModel().rows.map((row) => {
|
{table.getRowModel().rows.map((row) => {
|
||||||
// Individual row
|
// Individual row
|
||||||
const isDataVisible = row.original.visible;
|
const isUserVisible = 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" : ""
|
!isUserVisible ? "bg-gray-200 text-gray-500" : ""
|
||||||
}`;
|
}`;
|
||||||
return (
|
return (
|
||||||
<tr className={rowClassNames} key={row.id}>
|
<tr className={rowClassNames} key={row.id}>
|
||||||
|
@ -210,7 +289,7 @@ export default function Table<T extends DataPoint>({
|
||||||
<td
|
<td
|
||||||
className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50"
|
className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50"
|
||||||
colSpan={100}
|
colSpan={100}
|
||||||
onClick={addData}
|
onClick={addUser}
|
||||||
>
|
>
|
||||||
<span className="flex ml-1 text-gray-500">
|
<span className="flex ml-1 text-gray-500">
|
||||||
<PlusIcon className="inline h-4 mr-1" />
|
<PlusIcon className="inline h-4 mr-1" />
|
||||||
|
@ -222,4 +301,4 @@ export default function Table<T extends DataPoint>({
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
|
@ -1,82 +0,0 @@
|
||||||
import { Bars2Icon } from "@heroicons/react/24/solid";
|
|
||||||
import { Dispatch, SetStateAction, useState } from "react";
|
|
||||||
import useTagsHandler from "@/components/TagsInput/TagsHandler";
|
|
||||||
import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
|
|
||||||
import { RowOpenAction } from "@/components/Table/RowOpenAction";
|
|
||||||
import Table from "@/components/Table/Table";
|
|
||||||
import TagsInput from "@/components/TagsInput/Index";
|
|
||||||
import Resource from "@/utils/models/Resource";
|
|
||||||
|
|
||||||
type ResourceTableProps = {
|
|
||||||
data: Resource[];
|
|
||||||
setData: Dispatch<SetStateAction<Resource[]>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Table componenet used for displaying resources
|
|
||||||
* @param props.data Stateful list of resources to be displayed by the table
|
|
||||||
* @param props.setData State setter for the list of resources
|
|
||||||
*/
|
|
||||||
export default function ResourceTable({ data, setData }: ResourceTableProps) {
|
|
||||||
const columnHelper = createColumnHelper<Resource>();
|
|
||||||
|
|
||||||
// Set up tag handling
|
|
||||||
const programProps = useTagsHandler(["community", "domestic", "economic"]);
|
|
||||||
|
|
||||||
// Define Tanstack columns
|
|
||||||
const columns: ColumnDef<Resource, any>[] = [
|
|
||||||
columnHelper.accessor("name", {
|
|
||||||
header: () => (
|
|
||||||
<>
|
|
||||||
<Bars2Icon className="inline align-top h-4" /> Name
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
cell: (info) => (
|
|
||||||
<RowOpenAction
|
|
||||||
title={info.getValue()}
|
|
||||||
rowData={info.row.original}
|
|
||||||
setData={setData}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("link", {
|
|
||||||
header: () => (
|
|
||||||
<>
|
|
||||||
<Bars2Icon className="inline align-top h-4" /> Link
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
cell: (info) => (
|
|
||||||
<a
|
|
||||||
href={info.getValue()}
|
|
||||||
target={"_blank"}
|
|
||||||
className="ml-2 text-gray-500 underline hover:text-gray-400"
|
|
||||||
>
|
|
||||||
{info.getValue()}
|
|
||||||
</a>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("program", {
|
|
||||||
header: () => (
|
|
||||||
<>
|
|
||||||
<Bars2Icon className="inline align-top h-4" /> Program
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
cell: (info) => (
|
|
||||||
<TagsInput presetValue={info.getValue()} {...programProps} />
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
|
|
||||||
columnHelper.accessor("summary", {
|
|
||||||
header: () => (
|
|
||||||
<>
|
|
||||||
<Bars2Icon className="inline align-top h-4" /> Summary
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
cell: (info) => (
|
|
||||||
<span className="ml-2 text-gray-500">{info.getValue()}</span>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
return <Table data={data} setData={setData} columns={columns} />;
|
|
||||||
}
|
|
|
@ -1,21 +1,10 @@
|
||||||
import Drawer from "@/components/Drawer/Drawer";
|
import Drawer from "@/components/Drawer/Drawer";
|
||||||
import DataPoint from "@/utils/models/DataPoint";
|
import { ChangeEvent, useState } from "react";
|
||||||
import { Dispatch, SetStateAction, useState } from "react";
|
|
||||||
|
|
||||||
type RowOpenActionProps<T extends DataPoint> = {
|
export const RowOpenAction = ({ title, rowData, onRowUpdate }) => {
|
||||||
title: string;
|
|
||||||
rowData: T;
|
|
||||||
setData: Dispatch<SetStateAction<T[]>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function RowOpenAction<T extends DataPoint>({
|
|
||||||
title,
|
|
||||||
rowData,
|
|
||||||
setData,
|
|
||||||
}: RowOpenActionProps<T>) {
|
|
||||||
const [pageContent, setPageContent] = useState("");
|
const [pageContent, setPageContent] = useState("");
|
||||||
|
|
||||||
const handleDrawerContentChange = (newContent: string) => {
|
const handleDrawerContentChange = (newContent) => {
|
||||||
setPageContent(newContent);
|
setPageContent(newContent);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -23,16 +12,17 @@ export function RowOpenAction<T extends DataPoint>({
|
||||||
<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 pr-2">
|
||||||
{title}
|
{title}
|
||||||
<span>
|
<span>
|
||||||
|
{/* Added OnRowUpdate to drawer */}
|
||||||
<Drawer
|
<Drawer
|
||||||
title="My Drawer Title"
|
title="My Drawer Title"
|
||||||
editableContent={pageContent}
|
editableContent={pageContent}
|
||||||
rowContent={rowData}
|
rowContent={rowData}
|
||||||
onSave={handleDrawerContentChange}
|
onSave={handleDrawerContentChange}
|
||||||
setData={setData}
|
onRowUpdate={onRowUpdate}
|
||||||
>
|
>
|
||||||
{pageContent}
|
{pageContent}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
312
compass/components/Table/ServiceIndex.tsx
Normal file
312
compass/components/Table/ServiceIndex.tsx
Normal file
|
@ -0,0 +1,312 @@
|
||||||
|
// for showcasing to compass
|
||||||
|
|
||||||
|
import users from "./users.json";
|
||||||
|
import {
|
||||||
|
Cell,
|
||||||
|
ColumnDef,
|
||||||
|
Row,
|
||||||
|
createColumnHelper,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getFilteredRowModel,
|
||||||
|
sortingFns,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
import {
|
||||||
|
ChangeEvent,
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
FunctionComponent,
|
||||||
|
useRef,
|
||||||
|
ChangeEventHandler,
|
||||||
|
Key,
|
||||||
|
} from "react";
|
||||||
|
import { RowOptionMenu } from "./RowOptionMenu";
|
||||||
|
import { RowOpenAction } from "./RowOpenAction";
|
||||||
|
import { TableAction } from "./TableAction";
|
||||||
|
import {
|
||||||
|
AtSymbolIcon,
|
||||||
|
Bars2Icon,
|
||||||
|
ArrowDownCircleIcon,
|
||||||
|
PlusIcon,
|
||||||
|
} from "@heroicons/react/24/solid";
|
||||||
|
import TagsInput from "../TagsInput/Index";
|
||||||
|
import { rankItem } from "@tanstack/match-sorter-utils";
|
||||||
|
import Service from "@/utils/models/Service";
|
||||||
|
|
||||||
|
// For search
|
||||||
|
const fuzzyFilter = (
|
||||||
|
row: Row<any>,
|
||||||
|
columnId: string,
|
||||||
|
value: any,
|
||||||
|
addMeta: (meta: any) => void
|
||||||
|
) => {
|
||||||
|
// Rank the item
|
||||||
|
const itemRank = rankItem(row.getValue(columnId), value);
|
||||||
|
|
||||||
|
// Store the ranking info
|
||||||
|
addMeta(itemRank);
|
||||||
|
|
||||||
|
// Return if the item should be filtered in/out
|
||||||
|
return itemRank.passed;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Rename everything to service
|
||||||
|
export const ServiceTable = ({ users }: { users: Service[] }) => {
|
||||||
|
const columnHelper = createColumnHelper<Service>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const sortedUsers = [...users].sort((a, b) =>
|
||||||
|
a.visible === b.visible ? 0 : a.visible ? -1 : 1
|
||||||
|
);
|
||||||
|
setData(sortedUsers);
|
||||||
|
}, [users]);
|
||||||
|
|
||||||
|
const deleteUser = (userId: number) => {
|
||||||
|
console.log(data);
|
||||||
|
setData((currentData) =>
|
||||||
|
currentData.filter((user) => user.id !== userId)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideUser = (userId: number) => {
|
||||||
|
console.log(`Toggling visibility for user with ID: ${userId}`);
|
||||||
|
setData((currentData) => {
|
||||||
|
const newData = currentData
|
||||||
|
.map((user) => {
|
||||||
|
if (user.id === userId) {
|
||||||
|
return { ...user, visible: !user.visible };
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
})
|
||||||
|
.sort((a, b) =>
|
||||||
|
a.visible === b.visible ? 0 : a.visible ? -1 : 1
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(newData);
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const [presetOptions, setPresetOptions] = useState([
|
||||||
|
"administrator",
|
||||||
|
"volunteer",
|
||||||
|
"employee",
|
||||||
|
]);
|
||||||
|
const [tagColors, setTagColors] = useState(new Map());
|
||||||
|
|
||||||
|
const getTagColor = (tag: string) => {
|
||||||
|
if (!tagColors.has(tag)) {
|
||||||
|
const colors = [
|
||||||
|
"bg-cyan-100",
|
||||||
|
"bg-blue-100",
|
||||||
|
"bg-green-100",
|
||||||
|
"bg-yellow-100",
|
||||||
|
"bg-purple-100",
|
||||||
|
];
|
||||||
|
const randomColor =
|
||||||
|
colors[Math.floor(Math.random() * colors.length)];
|
||||||
|
setTagColors(new Map(tagColors).set(tag, randomColor));
|
||||||
|
}
|
||||||
|
return tagColors.get(tag);
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
columnHelper.display({
|
||||||
|
id: "options",
|
||||||
|
cell: (props) => (
|
||||||
|
<RowOptionMenu
|
||||||
|
onDelete={() => {}}
|
||||||
|
onHide={() => hideUser(props.row.original.id)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("name", {
|
||||||
|
header: () => (
|
||||||
|
<>
|
||||||
|
<Bars2Icon className="inline align-top h-4" /> Name
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
cell: (info) => (
|
||||||
|
<RowOpenAction
|
||||||
|
title={info.getValue()}
|
||||||
|
rowData={info.row.original}
|
||||||
|
onRowUpdate={handleRowUpdate}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("status", {
|
||||||
|
header: () => (
|
||||||
|
<>
|
||||||
|
<Bars2Icon className="inline align-top h-4" /> Status
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
cell: (info) => (
|
||||||
|
<span className="ml-2 text-gray-500">{info.getValue()}</span>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("program", {
|
||||||
|
header: () => (
|
||||||
|
<>
|
||||||
|
<Bars2Icon className="inline align-top h-4" /> Program
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
cell: (info) => <TagsInput presetValue={info.getValue()} />,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("requirements", {
|
||||||
|
header: () => (
|
||||||
|
<>
|
||||||
|
<Bars2Icon className="inline align-top h-4" /> Requirements
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
cell: (info) => (
|
||||||
|
<TagsInput
|
||||||
|
presetValue={
|
||||||
|
info.getValue()[0] !== "" ? info.getValue() : ["N/A"]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
|
||||||
|
columnHelper.accessor("summary", {
|
||||||
|
header: () => (
|
||||||
|
<>
|
||||||
|
<Bars2Icon className="inline align-top h-4" /> Summary
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
cell: (info) => (
|
||||||
|
<span className="ml-2 text-gray-500">{info.getValue()}</span>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const [data, setData] = useState<Service[]>([...users]);
|
||||||
|
|
||||||
|
const addUser = () => {
|
||||||
|
setData([...data]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Searching
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const handleSearchChange = (e: ChangeEvent) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
setQuery(String(target.value));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCellChange = (e: ChangeEvent, key: Key) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
console.log(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Filtering
|
||||||
|
|
||||||
|
// TODO: Sorting
|
||||||
|
|
||||||
|
// added this fn for editing rows
|
||||||
|
const handleRowUpdate = (updatedRow: Service) => {
|
||||||
|
const dataIndex = data.findIndex((row) => row.id === updatedRow.id);
|
||||||
|
if (dataIndex !== -1) {
|
||||||
|
const updatedData = [...data];
|
||||||
|
updatedData[dataIndex] = updatedRow;
|
||||||
|
setData(updatedData);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
filterFns: {
|
||||||
|
fuzzy: fuzzyFilter,
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
globalFilter: query,
|
||||||
|
},
|
||||||
|
onGlobalFilterChange: setQuery,
|
||||||
|
globalFilterFn: fuzzyFilter,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRowData = (row: any) => {
|
||||||
|
const rowData: any = {};
|
||||||
|
row.cells.forEach((cell: any) => {
|
||||||
|
rowData[cell.column.id] = cell.value;
|
||||||
|
});
|
||||||
|
// Use rowData object containing data from all columns for the current row
|
||||||
|
console.log(rowData);
|
||||||
|
return rowData;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row justify-end">
|
||||||
|
<TableAction query={query} handleChange={handleSearchChange} />
|
||||||
|
</div>
|
||||||
|
<table className="w-full text-xs text-left rtl:text-right">
|
||||||
|
<thead className="text-xs text-gray-500 capitalize">
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<tr key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header, i) => (
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className={
|
||||||
|
"p-2 border-gray-200 border-y font-medium " +
|
||||||
|
(1 < i && i < columns.length - 1
|
||||||
|
? "border-x"
|
||||||
|
: "")
|
||||||
|
}
|
||||||
|
key={header.id}
|
||||||
|
>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{table.getRowModel().rows.map((row) => {
|
||||||
|
// Individual row
|
||||||
|
const isUserVisible = row.original.visible;
|
||||||
|
const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${
|
||||||
|
!isUserVisible ? "bg-gray-200 text-gray-500" : ""
|
||||||
|
}`;
|
||||||
|
return (
|
||||||
|
<tr className={rowClassNames} key={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell, i) => (
|
||||||
|
<td
|
||||||
|
key={cell.id}
|
||||||
|
className={
|
||||||
|
"[&:nth-child(n+3)]:border-x relative first:text-left first:px-0 last:border-none"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext()
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50"
|
||||||
|
colSpan={100}
|
||||||
|
onClick={addUser}
|
||||||
|
>
|
||||||
|
<span className="flex ml-1 text-gray-500">
|
||||||
|
<PlusIcon className="inline h-4 mr-1" />
|
||||||
|
New
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,103 +0,0 @@
|
||||||
import { Bars2Icon } from "@heroicons/react/24/solid";
|
|
||||||
import { Dispatch, SetStateAction } from "react";
|
|
||||||
import useTagsHandler from "@/components/TagsInput/TagsHandler";
|
|
||||||
import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
|
|
||||||
import Table from "@/components/Table/Table";
|
|
||||||
import { RowOpenAction } from "@/components/Table/RowOpenAction";
|
|
||||||
import TagsInput from "@/components/TagsInput/Index";
|
|
||||||
import Service from "@/utils/models/Service";
|
|
||||||
|
|
||||||
type ServiceTableProps = {
|
|
||||||
data: Service[];
|
|
||||||
setData: Dispatch<SetStateAction<Service[]>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Table componenet used for displaying services
|
|
||||||
* @param props.data Stateful list of services to be displayed by the table
|
|
||||||
* @param props.setData State setter for the list of services
|
|
||||||
*/
|
|
||||||
export default function ServiceTable({ data, setData }: ServiceTableProps) {
|
|
||||||
const columnHelper = createColumnHelper<Service>();
|
|
||||||
|
|
||||||
// Set up tag handling
|
|
||||||
const programProps = useTagsHandler(["community", "domestic", "economic"]);
|
|
||||||
|
|
||||||
// TODO: Dynamically or statically get full list of preset requirement tag options
|
|
||||||
const requirementProps = useTagsHandler([
|
|
||||||
"anonymous",
|
|
||||||
"confidential",
|
|
||||||
"referral required",
|
|
||||||
"safety assessment",
|
|
||||||
"intake required",
|
|
||||||
"income eligibility",
|
|
||||||
"initial assessment",
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Define Tanstack columns
|
|
||||||
const columns: ColumnDef<Service, any>[] = [
|
|
||||||
columnHelper.accessor("name", {
|
|
||||||
header: () => (
|
|
||||||
<>
|
|
||||||
<Bars2Icon className="inline align-top h-4" /> Name
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
cell: (info) => (
|
|
||||||
<RowOpenAction
|
|
||||||
title={info.getValue()}
|
|
||||||
rowData={info.row.original}
|
|
||||||
setData={setData}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("status", {
|
|
||||||
header: () => (
|
|
||||||
<>
|
|
||||||
<Bars2Icon className="inline align-top h-4" /> Status
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
cell: (info) => (
|
|
||||||
<span className="ml-2 text-gray-500">{info.getValue()}</span>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("program", {
|
|
||||||
header: () => (
|
|
||||||
<>
|
|
||||||
<Bars2Icon className="inline align-top h-4" /> Program
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
cell: (info) => (
|
|
||||||
<TagsInput presetValue={info.getValue()} {...programProps} />
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("requirements", {
|
|
||||||
header: () => (
|
|
||||||
<>
|
|
||||||
<Bars2Icon className="inline align-top h-4" /> Requirements
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
cell: (info) => (
|
|
||||||
// TODO: Setup different tag handler for requirements
|
|
||||||
<TagsInput
|
|
||||||
presetValue={
|
|
||||||
info.getValue()[0] !== "" ? info.getValue() : ["N/A"]
|
|
||||||
}
|
|
||||||
{...requirementProps}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
|
|
||||||
columnHelper.accessor("summary", {
|
|
||||||
header: () => (
|
|
||||||
<>
|
|
||||||
<Bars2Icon className="inline align-top h-4" /> Summary
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
cell: (info) => (
|
|
||||||
<span className="ml-2 text-gray-500">{info.getValue()}</span>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
return <Table data={data} setData={setData} columns={columns} />;
|
|
||||||
}
|
|
|
@ -1,89 +0,0 @@
|
||||||
import {
|
|
||||||
ArrowDownCircleIcon,
|
|
||||||
AtSymbolIcon,
|
|
||||||
Bars2Icon,
|
|
||||||
} from "@heroicons/react/24/solid";
|
|
||||||
import { Dispatch, SetStateAction } from "react";
|
|
||||||
import useTagsHandler from "@/components/TagsInput/TagsHandler";
|
|
||||||
import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
|
|
||||||
import Table from "@/components/Table/Table";
|
|
||||||
import { RowOpenAction } from "@/components/Table/RowOpenAction";
|
|
||||||
import TagsInput from "@/components/TagsInput/Index";
|
|
||||||
import User from "@/utils/models/User";
|
|
||||||
|
|
||||||
type UserTableProps = {
|
|
||||||
data: User[];
|
|
||||||
setData: Dispatch<SetStateAction<User[]>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Table componenet used for displaying users
|
|
||||||
* @param props.data Stateful list of users to be displayed by the table
|
|
||||||
* @param props.setData State setter for the list of users
|
|
||||||
*/
|
|
||||||
export default function UserTable({ data, setData }: UserTableProps) {
|
|
||||||
const columnHelper = createColumnHelper<User>();
|
|
||||||
|
|
||||||
// Set up tag handling
|
|
||||||
const roleProps = useTagsHandler([
|
|
||||||
"administrator",
|
|
||||||
"volunteer",
|
|
||||||
"employee",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const programProps = useTagsHandler(["community", "domestic", "economic"]);
|
|
||||||
|
|
||||||
// Define Tanstack columns
|
|
||||||
const columns: ColumnDef<User, any>[] = [
|
|
||||||
columnHelper.accessor("username", {
|
|
||||||
header: () => (
|
|
||||||
<>
|
|
||||||
<Bars2Icon className="inline align-top h-4" /> Username
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
cell: (info) => (
|
|
||||||
<RowOpenAction
|
|
||||||
title={info.getValue()}
|
|
||||||
rowData={info.row.original}
|
|
||||||
setData={setData}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("role", {
|
|
||||||
header: () => (
|
|
||||||
<>
|
|
||||||
<ArrowDownCircleIcon className="inline align-top h-4" />{" "}
|
|
||||||
Role
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
cell: (info) => (
|
|
||||||
<TagsInput presetValue={info.getValue()} {...roleProps} />
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("email", {
|
|
||||||
header: () => (
|
|
||||||
<>
|
|
||||||
<AtSymbolIcon className="inline align-top h-4" /> Email
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
cell: (info) => (
|
|
||||||
<span className="ml-2 text-gray-500 underline hover:text-gray-400">
|
|
||||||
{info.getValue()}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor("program", {
|
|
||||||
header: () => (
|
|
||||||
<>
|
|
||||||
<ArrowDownCircleIcon className="inline align-top h-4" />{" "}
|
|
||||||
Program
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
cell: (info) => (
|
|
||||||
<TagsInput presetValue={info.getValue()} {...programProps} />
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
return <Table<User> data={data} setData={setData} columns={columns} />;
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState, useRef, Dispatch, SetStateAction } from "react";
|
import React, { useState, useRef } from "react";
|
||||||
import "tailwindcss/tailwind.css";
|
import "tailwindcss/tailwind.css";
|
||||||
import { TagsArray } from "./TagsArray";
|
import { TagsArray } from "./TagsArray";
|
||||||
import { TagDropdown } from "./TagDropdown";
|
import { TagDropdown } from "./TagDropdown";
|
||||||
|
@ -7,8 +7,8 @@ import { CreateNewTagAction } from "./CreateNewTagAction";
|
||||||
interface TagsInputProps {
|
interface TagsInputProps {
|
||||||
presetOptions: string[];
|
presetOptions: string[];
|
||||||
presetValue: string | string[];
|
presetValue: string | string[];
|
||||||
setPresetOptions: Dispatch<SetStateAction<string[]>>;
|
setPresetOptions: () => {};
|
||||||
getTagColor(tag: string): string;
|
getTagColor: () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const TagsInput: React.FC<TagsInputProps> = ({
|
const TagsInput: React.FC<TagsInputProps> = ({
|
||||||
|
|
|
@ -7,7 +7,7 @@ export interface Tags {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TagsArray = ({ tags, handleDelete, active = false }: Tags) => {
|
export const TagsArray = ({ tags, handleDelete, active = false }: Tags) => {
|
||||||
// console.log(tags);
|
console.log(tags);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex ml-2 flex-wrap gap-2 items-center">
|
<div className="flex ml-2 flex-wrap gap-2 items-center">
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom hook used to handle the state of tag options and colors
|
|
||||||
* @param initialOptions Initial value for preset options
|
|
||||||
* @returns An object with three fields intended to be passed into a `TagsInput` component:
|
|
||||||
* - `presetOptions` - the current state of tag options
|
|
||||||
* - `setPresetOptions` - the state setter for presetOptions
|
|
||||||
* - `getTagColor` - function that retrieves the color for the given tag
|
|
||||||
*/
|
|
||||||
export default function useTagsHandler(initialOptions: string[]) {
|
|
||||||
const [presetOptions, setPresetOptions] = useState(initialOptions);
|
|
||||||
const [tagColors, setTagColors] = useState(new Map<string, string>());
|
|
||||||
|
|
||||||
const getTagColor = (tag: string): string => {
|
|
||||||
if (!tagColors.has(tag)) {
|
|
||||||
const colors = [
|
|
||||||
"bg-cyan-100",
|
|
||||||
"bg-blue-100",
|
|
||||||
"bg-green-100",
|
|
||||||
"bg-yellow-100",
|
|
||||||
"bg-purple-100",
|
|
||||||
];
|
|
||||||
const randomColor =
|
|
||||||
colors[Math.floor(Math.random() * colors.length)];
|
|
||||||
setTagColors(new Map(tagColors).set(tag, randomColor));
|
|
||||||
return randomColor;
|
|
||||||
}
|
|
||||||
// Since we populate any missing keys, .get will never return undefined,
|
|
||||||
// so we are safe to typecast to prevent a type error
|
|
||||||
return tagColors.get(tag) as string;
|
|
||||||
};
|
|
||||||
|
|
||||||
return { presetOptions, setPresetOptions, getTagColor };
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
/**
|
|
||||||
* Represents metadata of the Resource, Service, and User models to be used in a table
|
|
||||||
*/
|
|
||||||
interface DataPoint {
|
|
||||||
id: number;
|
|
||||||
visible: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DataPoint;
|
|
Loading…
Reference in New Issue
Block a user