Pull changes from main and test authorization

This commit is contained in:
Aidan Kim 2024-11-19 13:31:58 -05:00
commit 1c310612e4
39 changed files with 972 additions and 1135 deletions

View File

@ -47,9 +47,6 @@ RUN mkdir -p /etc/apt/keyrings \
&& npm install -g npm@latest \
&& 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
ARG USERNAME=vscode
ARG USER_UID=1000

View File

@ -1,10 +1,11 @@
import jwt
from datetime import datetime, timedelta, timezone
from fastapi import Cookie, Depends, HTTPException, Response, status, APIRouter
from fastapi import Depends, HTTPException, Request, Response, status, APIRouter
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from backend.env import getenv
from backend.models.user_model import User
from ..services import UserService
from backend.services import UserService
from backend.env import getenv
api = APIRouter(prefix="/api/authentication")
@ -19,23 +20,31 @@ ACCESS_TOKEN_EXPIRE_MINUTES = getenv("ACCESS_TOKEN_EXPIRE_MINUTES")
REFRESH_TOKEN_EXPIRE_DAYS = getenv("REFRESH_TOKEN_EXPIRE_DAYS")
def create_access_token(user_id: str) -> str:
expiration = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
expiration = datetime.now(timezone.utc) + timedelta(minutes=int(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=REFRESH_TOKEN_EXPIRE_DAYS)
expiration = datetime.now(timezone.utc) + timedelta(days=int(REFRESH_TOKEN_EXPIRE_DAYS))
payload = {"user_id": user_id, "exp": expiration}
token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
return token
def registered_user(
token: HTTPAuthorizationCredentials = Depends(HTTPBearer()),
request: Request,
user_service: UserService = Depends()
) -> 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:
payload = jwt.decode(token.credentials, JWT_SECRET, algorithms=[JWT_ALGORITHM])
payload = jwt.decode(access_token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
user_id = payload.get("user_id")
user = user_service.get_user_by_id(user_id)
@ -56,7 +65,7 @@ def registered_user(
detail="Invalid token"
)
@api.post("", include_in_schema=False, tags=["Authentication"])
@api.post("", tags=["Authentication"])
def return_bearer_token(user_id: str, response: Response, user_service: UserService = Depends()):
user = user_service.get_user_by_id(user_id)
if not user:
@ -77,26 +86,6 @@ def return_bearer_token(user_id: str, response: Response, user_service: UserServ
return {"message": "Tokens set as cookies"}
@api.post("/refresh", tags=["Authentication"])
def refresh_access_token(response: Response, refresh_token: str = Depends(Cookie(None))):
try:
payload = jwt.decode(refresh_token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
user_id = payload.get("user_id")
if not user_id:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token")
new_access_token = create_access_token(user_id)
response.set_cookie(
key="access_token", value=new_access_token, httponly=True, secure=True, max_age=ACCESS_TOKEN_EXPIRE_MINUTES * 60
)
return {"message": "Access token refreshed"}
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Refresh token expired")
except jwt.PyJWTError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token")
@api.get("", include_in_schema=False, tags=["Authentication"])
@api.get("", tags=["Authentication"])
def get_user_id(user_service: UserService = Depends()):
return user_service.all()

View File

@ -1,10 +1,11 @@
from fastapi import APIRouter, Depends, HTTPException, status
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 backend.models.user_model import User
from ..services import ResourceService
from ..models.resource_model import Resource
api = APIRouter(prefix="/api/resource")
@ -13,33 +14,40 @@ openapi_tags = {
"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"])
def create(
subject: User = Depends(registered_user),
resource: Resource = Depends(),
resource_svc: ResourceService = Depends(),
resource: Resource, subject: User = Depends(registered_user), resource_svc: ResourceService = Depends()
):
return resource_svc.create(subject, resource)
@api.get("", response_model=List[Resource], tags=["Resource"])
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)
@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"])
def update(
subject: User = Depends(registered_user),
resource: Resource = Depends(),
resource_svc: ResourceService = Depends(),
uuid: str, resource: Resource, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends()
):
subject = user_svc.get_user_by_uuid(uuid)
return resource_svc.update(subject, resource)
@api.delete("", response_model=None, tags=["Resource"])
def delete(
subject: User = Depends(registered_user),
resource: Resource = Depends(),
resource_svc: ResourceService = Depends(),
resource: Resource, subject: User = Depends(registered_user), resource_svc: ResourceService = Depends()
):
resource_svc.delete(subject, resource)
resource_svc.delete(subject, resource)

View File

@ -1,10 +1,11 @@
from fastapi import APIRouter, Depends, HTTPException, status
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 backend.models.user_model import User
from ..services import ServiceService
from ..models.service_model import Service
api = APIRouter(prefix="/api/service")
@ -13,33 +14,37 @@ openapi_tags = {
"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"])
def create(
subject: User = Depends(registered_user),
service: Service = Depends(),
service_svc: ServiceService = Depends(),
service: Service, subject: User = Depends(registered_user), service_svc: ServiceService = Depends()
):
return service_svc.create(subject, service)
@api.get("", response_model=List[Service], tags=["Service"])
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)
@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"])
def update(
subject: User = Depends(registered_user),
service: Service = Depends(),
service_svc: ServiceService = Depends(),
service: Service, subject: User = Depends(registered_user), service_svc: ServiceService = Depends()
):
return service_svc.update(subject, service)
@api.delete("", response_model=None, tags=["Service"])
def delete(
subject: User = Depends(registered_user),
service: Service = Depends(),
service_svc: ServiceService = Depends(),
service: Service, subject: User = Depends(registered_user), service_svc: ServiceService = Depends()
):
service_svc.delete(subject, service)
service_svc.delete(subject, service)

View File

@ -1,11 +1,13 @@
from fastapi import APIRouter, Depends
from typing import List
from .authentication import registered_user
from backend.models.tag_model import Tag
from backend.models.user_model import User
from backend.services.tag import TagService
from .authentication import registered_user
from typing import List
api = APIRouter(prefix="/api/tag")
openapi_tags = {
@ -13,33 +15,25 @@ openapi_tags = {
"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"])
def create(
subject: User = Depends(registered_user),
tag: Tag = Depends(),
tag_service: TagService = Depends(),
):
def create(subject: User, tag: Tag, tag_service: TagService = Depends()):
return tag_service.create(subject, tag)
@api.get("", response_model=List[Tag], tags=["Tag"])
def get_all(
subject: User = Depends(registered_user),
tag_svc: TagService = Depends()
):
def get_all(subject: User, tag_svc: TagService = Depends()):
return tag_svc.get_all()
@api.put("", response_model=Tag, tags=["Tag"])
def update(
subject: User = Depends(registered_user),
tag: Tag = Depends(),
tag_svc: TagService = Depends(),
):
return tag_svc.update(subject, tag)
def update(subject: User, tag: Tag, tag_svc: TagService = Depends()):
return tag_svc.delete(subject, tag)
@api.delete("", response_model=None, tags=["Tag"])
def delete(
subject: User = Depends(registered_user),
tag: Tag = Depends(),
tag_svc: TagService = Depends(),
):
tag_svc.delete(subject, tag)
def delete(subject: User, tag: Tag, tag_svc: TagService = Depends()):
tag_svc.delete(subject, tag)

147
backend/api/test_routes.md Normal file
View File

@ -0,0 +1,147 @@
# 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"
}'
```

View File

@ -12,4 +12,4 @@ class Resource(BaseModel):
summary: str = Field(..., max_length=300, description="The summary of the resource")
link: str = Field(..., max_length=150, description="link to the resource")
program: ProgramTypeEnum
created_at: Optional[datetime]
created_at: Optional[datetime] = datetime.now()

View File

@ -8,7 +8,7 @@ from .enum_for_models import ProgramTypeEnum
class Service(BaseModel):
id: int | None = None
created_at: datetime | None = None
created_at: datetime | None = datetime.now()
name: str
status: str
summary: str

View File

@ -10,4 +10,4 @@ class Tag(BaseModel):
content: str = Field(
..., max_length=600, description="content associated with the tag"
)
created_at: datetime | None = None
created_at: datetime | None = datetime.now()

View File

@ -14,5 +14,5 @@ class User(BaseModel):
group: str
program: List[ProgramTypeEnum]
role: UserTypeEnum
created_at: Optional[datetime]
created_at: Optional[datetime] = datetime.now()
uuid: str | None = None

View File

@ -20,6 +20,8 @@ class UserPermissionException(Exception):
class ServiceNotFoundException(Exception):
"""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):
"""Exception for when the user does not have correct access for requested services."""

View File

@ -1,12 +1,12 @@
from fastapi import Depends
from ..database import db_session
from sqlalchemy.orm import Session
from sqlalchemy import select
from sqlalchemy import and_, select
from ..models.resource_model import Resource
from ..entities.resource_entity import ResourceEntity
from ..models.user_model import User, UserTypeEnum
from .exceptions import ResourceNotFoundException
from .exceptions import ProgramNotAssignedException, ResourceNotFoundException
class ResourceService:
@ -24,137 +24,115 @@ class ResourceService:
programs = subject.program
resources = []
for program in programs:
entities = self._session.query(ResourceEntity).where(ResourceEntity.program == program).all()
entities = (
self._session.query(ResourceEntity)
.where(ResourceEntity.program == program)
.all()
)
for entity in entities:
resources.append(entity.to_model())
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:
"""
Creates a resource based on the input object and adds it to the table if the user has the right permissions.
Parameters:
user: a valid User model representing the currently logged in User
resource: Resource object to add to table
Returns:
Resource: Object added to table
"""
# Ask about what the requirements for making a resource are.
if resource.role != subject.role or resource.group != subject.group:
raise PermissionError(
"User does not have permission to add resources in this role or group."
if subject.role != UserTypeEnum.ADMIN:
raise ProgramNotAssignedException(
f"User is not {UserTypeEnum.ADMIN}, cannot update service"
)
resource_entity = ResourceEntity.from_model(resource)
self._session.add(resource_entity)
self._session.commit()
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:
"""
Update the resource if the user has access
Parameters:
user: a valid User model representing the currently logged in User
resource (ResourceEntity): Resource to update
Returns:
Resource: Updated resource object
Raises:
ResourceNotFoundException: If no resource is found with the corresponding ID
"""
if resource.role != subject.role or resource.group != subject.group:
raise PermissionError(
"User does not have permission to update this resource."
if subject.role != UserTypeEnum.ADMIN:
raise ProgramNotAssignedException(
f"User is not {UserTypeEnum.ADMIN}, cannot update service"
)
query = select(ResourceEntity).where(ResourceEntity.id == resource.id)
entity = self._session.scalars(query).one_or_none()
if entity is None:
raise ResourceNotFoundException(
f"No resource found with matching id: {resource.id}"
)
entity.name = resource.name
entity.summary = resource.summary
entity.link = resource.link
entity.program = resource.program
self._session.commit()
return entity.to_model()
def delete(self, subject: User, resource: Resource) -> None:
"""
Delete resource based on id that the user has access to
Parameters:
user: a valid User model representing the currently logged in User
id: int, a unique resource id
Raises:
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)
entity = self._session.scalars(query).one_or_none()
if entity is None:
raise ResourceNotFoundException(f"No resource found with matching id: {resource.id}")
raise ResourceNotFoundException(
f"No resource found with matching id: {resource.id}"
)
self._session.delete(entity)
self._session.commit()
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
Parameters:
user: a valid User model representing the currently logged in User
search_string: a string to search resources by
Returns:
list[Resource]: list of resources relating to the string
Raises:
ResourceNotFoundException if no resource is found with the corresponding slug
"""
query = select(ResourceEntity).where(
ResourceEntity.title.ilike(f"%{search_string}%"),
ResourceEntity.role == user.role,
ResourceEntity.group == user.group,
ResourceEntity.name.ilike(f"%{search_string}%"),
ResourceEntity.program.in_(user.program),
)
entities = self._session.scalars(query).all()
if not entities:
return []
return [entity.to_model() for entity in entities]

View File

@ -27,37 +27,24 @@ class ServiceService:
return [service.to_model() for service in entities]
else:
programs = subject.program
resources = []
services = []
for program in programs:
entities = self._session.query(ServiceEntity).where(ServiceEntity.program == program).all()
for entity in entities:
resources.append(entity.to_model())
return [service for service in resources]
services.append(entity.to_model())
return [service for service in services]
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:
def get_service_by_name(self, name: str, subject: User) -> Service:
"""Service method getting services by id."""
query = select(ServiceEntity).filter(ServiceEntity.id == id)
query = select(ServiceEntity).where(
and_(
ServiceEntity.name == name, ServiceEntity.program.in_(subject.program)
)
)
entity = self._session.scalars(query).one_or_none()
if entity is None:
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")
raise ServiceNotFoundException(f"Service with name: {name} does not exist or program has not been assigned")
return entity.to_model()

View File

@ -4,7 +4,7 @@ import pytest
from sqlalchemy import Engine, create_engine, text
from sqlalchemy.orm import Session
from sqlalchemy.exc import OperationalError
from .services import user_test_data, tag_test_data, service_test_data
from .services import user_test_data, tag_test_data, service_test_data, resource_test_data
from ..database import _engine_str
from ..env import getenv
@ -57,5 +57,6 @@ def setup_insert_data_fixture(session: Session):
user_test_data.insert_fake_data(session)
tag_test_data.insert_fake_data(session)
service_test_data.insert_fake_data(session)
resource_test_data.insert_fake_data(session)
session.commit()
yield

View File

@ -6,6 +6,7 @@ from sqlalchemy.orm import Session
from ...services import UserService
from ...services import TagService
from ...services import ServiceService
from ...services import ResourceService
@ -23,4 +24,9 @@ def tag_svc(session: Session):
@pytest.fixture()
def service_svc(session: Session):
"""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)

View File

@ -0,0 +1,126 @@
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 == []

View File

@ -50,6 +50,24 @@ resource5 = Resource(
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]
resource_1 = Resource(
@ -266,13 +284,11 @@ def reset_table_id_seq(
next_id: int,
) -> None:
"""Reset the ID sequence of an entity table.
Args:
session (Session) - A SQLAlchemy Session
entity (DeclarativeBase) - The SQLAlchemy Entity table to target
entity_id_column (MappedColumn) - The ID column (should be an int column)
next_id (int) - Where the next inserted, autogenerated ID should begin
Returns:
None"""
table = entity.__table__
@ -312,4 +328,4 @@ def insert_fake_data(session: Session):
reset_table_id_seq(session, ResourceEntity, ResourceEntity.id, len(resources) + 1)
# Commit all changes
session.commit()
session.commit()

View File

@ -2,7 +2,6 @@
import Sidebar from "@/components/Sidebar/Sidebar";
import React, { useState } from "react";
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
import { createClient } from "@/utils/supabase/client";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
@ -14,7 +13,7 @@ export default function RootLayout({
}: {
children: React.ReactNode;
}) {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const router = useRouter();
const [user, setUser] = useState<User>();
@ -56,34 +55,13 @@ export default function RootLayout({
<div className="flex-row">
{user ? (
<div>
{/* button to open sidebar */}
<button
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
className={`fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0`}
aria-label={"Open sidebar"}
>
{
!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 */}
<Sidebar
setIsSidebarOpen={setIsSidebarOpen}
isSidebarOpen={isSidebarOpen}
name={user.username}
email={user.email}
isAdmin={user.role === Role.ADMIN}
/>
<div
className={`flex-1 transition duration-300 ease-in-out ${
isSidebarOpen ? "ml-64" : "ml-0"

View File

@ -1,7 +1,7 @@
"use client";
import { PageLayout } from "@/components/PageLayout";
import { Table } from "@/components/Table/Index";
import UserTable from "@/components/Table/UserTable";
import User from "@/utils/models/User";
import { createClient } from "@/utils/supabase/client";
@ -38,7 +38,7 @@ export default function Page() {
<div className="min-h-screen flex flex-col">
{/* icon + title */}
<PageLayout title="Users" icon={<UsersIcon />}>
<Table users={users} />
<UserTable data={users} setData={setUsers} />
</PageLayout>
</div>
);

View File

@ -9,7 +9,7 @@ export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const uuid = searchParams.get("uuid");
const data = await fetch(`${apiEndpoint}?user_id=${uuid}`);
const data = await fetch(`${apiEndpoint}?uuid=${uuid}`);
const resourceData: Resource[] = await data.json();
// TODO: Remove make every resource visible

View File

@ -9,7 +9,7 @@ export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const uuid = searchParams.get("uuid");
const data = await fetch(`${apiEndpoint}?user_id=${uuid}`);
const data = await fetch(`${apiEndpoint}?uuid=${uuid}`);
const serviceData: Service[] = await data.json();
// TODO: Remove make every service visible

View File

@ -1,7 +1,6 @@
"use client";
import Sidebar from "@/components/Sidebar/Sidebar";
import React, { useState } from "react";
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
import { createClient } from "@/utils/supabase/client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
@ -13,7 +12,7 @@ export default function RootLayout({
}: {
children: React.ReactNode;
}) {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const [user, setUser] = useState<User>();
const router = useRouter();
@ -45,34 +44,13 @@ export default function RootLayout({
<div className="flex-row">
{user ? (
<div>
{/* button to open sidebar */}
<button
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
className={`fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0`}
aria-label={"Open sidebar"}
>
{
!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 */}
<Sidebar
name={user.username}
email={user.email}
setIsSidebarOpen={setIsSidebarOpen}
isSidebarOpen={isSidebarOpen}
isAdmin={user.role === Role.ADMIN}
/>
<div
className={`flex-1 transition duration-300 ease-in-out ${
isSidebarOpen ? "ml-64" : "ml-0"

View File

@ -2,7 +2,6 @@
import Sidebar from "@/components/Sidebar/Sidebar";
import React, { useState } from "react";
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
import { createClient } from "@/utils/supabase/client";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
@ -14,7 +13,7 @@ export default function RootLayout({
}: {
children: React.ReactNode;
}) {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const router = useRouter();
const [user, setUser] = useState<User>();
@ -48,33 +47,14 @@ export default function RootLayout({
<div className="flex-row">
{user ? (
<div>
{/* button to open sidebar */}
<button
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
className={`fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0`}
aria-label={"Open sidebar"}
>
{
!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>
<Sidebar
setIsSidebarOpen={setIsSidebarOpen}
isSidebarOpen={isSidebarOpen}
name={user.username}
email={user.email}
isAdmin={user.role === Role.ADMIN}
/>
{/* </div>*/}
{/* page ui */}
<div
className={`flex-1 transition duration-300 ease-in-out ${

View File

@ -1,8 +1,8 @@
"use client";
import { PageLayout } from "@/components/PageLayout";
import { ResourceTable } from "@/components/Table/ResourceIndex";
import Resource from "@/utils/models/Resource";
import ResourceTable from "@/components/Table/ResourceTable";
import { createClient } from "@/utils/supabase/client";
import { BookmarkIcon } from "@heroicons/react/24/solid";
@ -38,7 +38,7 @@ export default function Page() {
<div className="min-h-screen flex flex-col">
{/* icon + title */}
<PageLayout title="Resources" icon={<BookmarkIcon />}>
<ResourceTable users={resources} />
<ResourceTable data={resources} setData={setResources} />
</PageLayout>
</div>
);

View File

@ -2,7 +2,6 @@
import Sidebar from "@/components/Sidebar/Sidebar";
import React, { useState } from "react";
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
import { createClient } from "@/utils/supabase/client";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
@ -14,7 +13,7 @@ export default function RootLayout({
}: {
children: React.ReactNode;
}) {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const router = useRouter();
const [user, setUser] = useState<User>();
@ -48,34 +47,13 @@ export default function RootLayout({
<div className="flex-row">
{user ? (
<div>
{/* button to open sidebar */}
<button
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
className={`fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0`}
aria-label={"Open sidebar"}
>
{
!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 */}
<Sidebar
setIsSidebarOpen={setIsSidebarOpen}
isSidebarOpen={isSidebarOpen}
name={user.username}
email={user.email}
isAdmin={user.role === Role.ADMIN}
/>
<div
className={`flex-1 transition duration-300 ease-in-out ${
isSidebarOpen ? "ml-64" : "ml-0"

View File

@ -1,7 +1,7 @@
"use client";
import { PageLayout } from "@/components/PageLayout";
import { ServiceTable } from "@/components/Table/ServiceIndex";
import ServiceTable from "@/components/Table/ServiceTable";
import Service from "@/utils/models/Service";
import { createClient } from "@/utils/supabase/client";
@ -9,7 +9,7 @@ import { ClipboardIcon } from "@heroicons/react/24/solid";
import { useEffect, useState } from "react";
export default function Page() {
const [services, setUsers] = useState<Service[]>([]);
const [services, setServices] = useState<Service[]>([]);
useEffect(() => {
async function getServices() {
@ -27,7 +27,7 @@ export default function Page() {
);
const servicesAPI: Service[] = await serviceListData.json();
setUsers(servicesAPI);
setServices(servicesAPI);
}
getServices();
@ -37,7 +37,7 @@ export default function Page() {
<div className="min-h-screen flex flex-col">
{/* icon + title */}
<PageLayout title="Services" icon={<ClipboardIcon />}>
<ServiceTable users={services} />
<ServiceTable data={services} setData={setServices} />
</PageLayout>
</div>
);

View File

@ -1,4 +1,4 @@
import { FunctionComponent, ReactNode } from "react";
import { Dispatch, FunctionComponent, ReactNode, SetStateAction } from "react";
import React, { useState } from "react";
import { ChevronDoubleLeftIcon } from "@heroicons/react/24/solid";
import {
@ -23,7 +23,7 @@ type DrawerProps = {
editableContent?: any;
onSave?: (content: any) => void;
rowContent?: any;
onRowUpdate?: (content: any) => void;
setData: Dispatch<SetStateAction<any>>;
};
interface EditContent {
@ -37,13 +37,21 @@ const Drawer: FunctionComponent<DrawerProps> = ({
onSave,
editableContent,
rowContent,
onRowUpdate,
setData,
}) => {
const [isOpen, setIsOpen] = useState(false);
const [isFull, setIsFull] = useState(false);
const [isFavorite, setIsFavorite] = useState(false);
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 { name, value } = e.target;
console.log(name);

View File

@ -2,6 +2,7 @@ import React from "react";
import {
HomeIcon,
ChevronDoubleLeftIcon,
ChevronDoubleRightIcon,
BookmarkIcon,
ClipboardIcon,
BookOpenIcon,
@ -12,6 +13,7 @@ import { UserProfile } from "../resource/UserProfile";
interface SidebarProps {
setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>;
isSidebarOpen: boolean;
name: string;
email: string;
isAdmin: boolean;
@ -19,70 +21,96 @@ interface SidebarProps {
const Sidebar: React.FC<SidebarProps> = ({
setIsSidebarOpen,
isSidebarOpen,
name,
email,
isAdmin: admin,
}) => {
return (
<div className="w-64 h-full border border-gray-200 bg-gray-50 px-4">
{/* 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 className="flex flex-col space-y-8">
{/* user + logout button */}
<div className="flex items-center p-4 space-x-2 border border-gray-200 rounded-md ">
<UserProfile name={name} email={email} />
</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"
/>
)}
<>
{/* Button to open the sidebar. */}
<button
onClick={() => setIsSidebarOpen(true)}
className={
"fixed z-20 p-2 text-gray-500 hover:text-gray-800 left-0"
}
aria-label={"Open sidebar"}
>
{!isSidebarOpen && (
<ChevronDoubleRightIcon className="h-5 w-5" />
)}
</button>
<SidebarItem
icon={<HomeIcon />}
text="Home"
active={true}
redirect="/home"
/>
<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>
{/* The sidebar itself. */}
<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 className="flex flex-col space-y-8">
{/* user + logout button */}
<div className="flex items-center p-4 space-x-2 border border-gray-200 rounded-md ">
<UserProfile name={name} email={email} />
</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"
/>
)}
<SidebarItem
icon={<HomeIcon />}
text="Home"
active={true}
redirect="/home"
/>
<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>
</>
);
};

View File

@ -1,306 +0,0 @@
// 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>
);
};

View File

@ -0,0 +1,82 @@
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} />;
}

View File

@ -1,10 +1,21 @@
import Drawer from "@/components/Drawer/Drawer";
import { ChangeEvent, useState } from "react";
import DataPoint from "@/utils/models/DataPoint";
import { Dispatch, SetStateAction, useState } from "react";
export const RowOpenAction = ({ title, rowData, onRowUpdate }) => {
type RowOpenActionProps<T extends DataPoint> = {
title: string;
rowData: T;
setData: Dispatch<SetStateAction<T[]>>;
};
export function RowOpenAction<T extends DataPoint>({
title,
rowData,
setData,
}: RowOpenActionProps<T>) {
const [pageContent, setPageContent] = useState("");
const handleDrawerContentChange = (newContent) => {
const handleDrawerContentChange = (newContent: string) => {
setPageContent(newContent);
};
@ -12,17 +23,16 @@ export const RowOpenAction = ({ title, rowData, onRowUpdate }) => {
<div className="font-semibold group flex flex-row items-center justify-between pr-2">
{title}
<span>
{/* Added OnRowUpdate to drawer */}
<Drawer
title="My Drawer Title"
editableContent={pageContent}
rowContent={rowData}
onSave={handleDrawerContentChange}
onRowUpdate={onRowUpdate}
setData={setData}
>
{pageContent}
</Drawer>
</span>
</div>
);
};
}

View File

@ -1,312 +0,0 @@
// 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>
);
};

View File

@ -0,0 +1,103 @@
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} />;
}

View File

@ -1,40 +1,32 @@
// for showcasing to compass
import users from "./users.json";
import {
Cell,
ColumnDef,
Row,
createColumnHelper,
flexRender,
getCoreRowModel,
getFilteredRowModel,
sortingFns,
ColumnDef,
useReactTable,
getCoreRowModel,
flexRender,
createColumnHelper,
} from "@tanstack/react-table";
import {
ChangeEvent,
useState,
useEffect,
FunctionComponent,
useRef,
ChangeEventHandler,
Key,
Dispatch,
SetStateAction,
} 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 { PlusIcon } from "@heroicons/react/24/solid";
import { rankItem } from "@tanstack/match-sorter-utils";
import Resource from "@/utils/models/Resource";
import { RowOptionMenu } from "./RowOptionMenu";
import DataPoint from "@/utils/models/DataPoint";
// For search
type TableProps<T extends DataPoint> = {
data: T[];
setData: Dispatch<SetStateAction<T[]>>;
columns: ColumnDef<T, any>[];
};
/** Fuzzy search function */
const fuzzyFilter = (
row: Row<any>,
columnId: string,
@ -51,131 +43,69 @@ const fuzzyFilter = (
return itemRank.passed;
};
// TODO: Rename everything to resources
export const ResourceTable = ({ users }: { users: Resource[] }) => {
const columnHelper = createColumnHelper<Resource>();
/**
* General componenet that holds shared functionality for any data table component
* @param props.data Stateful list of data to be held in the table
* @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(() => {
const sortedUsers = [...users].sort((a, b) =>
a.visible === b.visible ? 0 : a.visible ? -1 : 1
);
setData(sortedUsers);
}, [users]);
setData((prevData) => prevData.sort(visibilitySort));
}, [setData]);
const deleteUser = (userId: number) => {
// Data manipulation methods
// TODO: Connect data manipulation methods to the database (deleteData, hideData, addData)
const deleteData = (dataId: number) => {
console.log(data);
setData((currentData) =>
currentData.filter((user) => user.id !== userId)
currentData.filter((data) => data.id !== dataId)
);
};
const hideUser = (userId: number) => {
console.log(`Toggling visibility for user with ID: ${userId}`);
const hideData = (dataId: number) => {
console.log(`Toggling visibility for data with ID: ${dataId}`);
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
);
.map((data) =>
data.id === dataId
? { ...data, visible: !data.visible }
: data
)
.sort(visibilitySort);
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 addData = () => {
setData([...data]);
};
const columns = [
// Add data manipulation options to the first column
columns.unshift(
columnHelper.display({
id: "options",
cell: (props) => (
<RowOptionMenu
onDelete={() => {}}
onHide={() => hideUser(props.row.original.id)}
onDelete={() => deleteData(props.row.original.id)}
onHide={() => hideData(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
const [query, setQuery] = useState("");
@ -193,16 +123,7 @@ export const ResourceTable = ({ users }: { users: Resource[] }) => {
// TODO: Sorting
// 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);
}
};
// Define Tanstack table
const table = useReactTable({
columns,
data,
@ -261,9 +182,9 @@ export const ResourceTable = ({ users }: { users: Resource[] }) => {
<tbody>
{table.getRowModel().rows.map((row) => {
// Individual row
const isUserVisible = row.original.visible;
const isDataVisible = row.original.visible;
const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${
!isUserVisible ? "bg-gray-200 text-gray-500" : ""
!isDataVisible ? "bg-gray-200 text-gray-500" : ""
}`;
return (
<tr className={rowClassNames} key={row.id}>
@ -289,7 +210,7 @@ export const ResourceTable = ({ users }: { users: Resource[] }) => {
<td
className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50"
colSpan={100}
onClick={addUser}
onClick={addData}
>
<span className="flex ml-1 text-gray-500">
<PlusIcon className="inline h-4 mr-1" />
@ -301,4 +222,4 @@ export const ResourceTable = ({ users }: { users: Resource[] }) => {
</table>
</div>
);
};
}

View File

@ -0,0 +1,89 @@
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} />;
}

View File

@ -1,4 +1,4 @@
import React, { useState, useRef } from "react";
import React, { useState, useRef, Dispatch, SetStateAction } from "react";
import "tailwindcss/tailwind.css";
import { TagsArray } from "./TagsArray";
import { TagDropdown } from "./TagDropdown";
@ -7,8 +7,8 @@ import { CreateNewTagAction } from "./CreateNewTagAction";
interface TagsInputProps {
presetOptions: string[];
presetValue: string | string[];
setPresetOptions: () => {};
getTagColor: () => {};
setPresetOptions: Dispatch<SetStateAction<string[]>>;
getTagColor(tag: string): string;
}
const TagsInput: React.FC<TagsInputProps> = ({

View File

@ -7,7 +7,7 @@ export interface Tags {
}
export const TagsArray = ({ tags, handleDelete, active = false }: Tags) => {
console.log(tags);
// console.log(tags);
return (
<div className="flex ml-2 flex-wrap gap-2 items-center">

View File

@ -0,0 +1,35 @@
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 };
}

View File

@ -0,0 +1,9 @@
/**
* Represents metadata of the Resource, Service, and User models to be used in a table
*/
interface DataPoint {
id: number;
visible: boolean;
}
export default DataPoint;