API Routes for Resources and Services (#40)

* Implemented API routes for getting all, creating, updating, and deleting resources, services, and tags.

* Updated main.py for API routes to include tags and rolled entities back.

* Created API routes for create, update, delete, get_all, and get_by_name. Deleted service methods for get by id.

* Defaults created_at to current time

* Write markdown file for HTTP requests using curl

---------

Co-authored-by: pmoharana-cmd <pmoharana032474@gmail.com>
This commit is contained in:
Aidan Kim 2024-11-05 19:12:03 -05:00 committed by GitHub
parent 99e43c7b30
commit 7d705ac743
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 1455 additions and 1226 deletions

View File

@ -1,26 +1,56 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from ..services import ResourceService, UserService
from ..models.resource_model import Resource from backend.models.user_model import User
from ..services import ResourceService, UserService
from typing import List from ..models.resource_model import Resource
api = APIRouter(prefix="/api/resource") from typing import List
openapi_tags = { api = APIRouter(prefix="/api/resource")
"name": "Resource",
"description": "Resource search and related operations.", openapi_tags = {
} "name": "Resource",
"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 # TODO: Add security using HTTP Bearer Tokens
@api.get("", response_model=List[Resource], tags=["Resource"]) # TODO: Enable authorization by passing user uuid to API
def get_all( # TODO: Create custom exceptions
user_id: str, @api.post("", response_model=Resource, tags=["Resource"])
resource_svc: ResourceService = Depends(), def create(
user_svc: UserService = Depends(), uuid: str, resource: Resource, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends()
): ):
subject = user_svc.get_user_by_uuid(user_id) subject = user_svc.get_user_by_uuid(uuid)
return resource_svc.create(subject, resource)
return resource_svc.get_resource_by_user(subject)
@api.get("", response_model=List[Resource], tags=["Resource"])
def get_all(
uuid: str, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends()
):
subject = user_svc.get_user_by_uuid(uuid)
return resource_svc.get_resource_by_user(subject)
@api.get("/{name}", response_model=Resource, tags=["Resource"])
def get_by_name(
name:str, uuid:str, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends()
):
subject = user_svc.get_user_by_uuid(uuid)
return resource_svc.get_resource_by_name(name, subject)
@api.put("", response_model=Resource, tags=["Resource"])
def update(
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(
uuid: str, resource: Resource, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends()
):
subject = user_svc.get_user_by_uuid(uuid)
resource_svc.delete(subject, resource)

View File

@ -1,26 +1,54 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from ..services import ServiceService, UserService
from ..models.service_model import Service from backend.models.user_model import User
from ..services import ServiceService, UserService
from typing import List from ..models.service_model import Service
api = APIRouter(prefix="/api/service") from typing import List
openapi_tags = { api = APIRouter(prefix="/api/service")
"name": "Service",
"description": "Service search and related operations.", openapi_tags = {
} "name": "Service",
"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 # TODO: Add security using HTTP Bearer Tokens
@api.get("", response_model=List[Service], tags=["Service"]) # TODO: Enable authorization by passing user uuid to API
def get_all( # TODO: Create custom exceptions
user_id: str, @api.post("", response_model=Service, tags=["Service"])
service_svc: ServiceService = Depends(), def create(
user_svc: UserService = Depends(), uuid: str, service: Service, user_svc: UserService = Depends(), service_svc: ServiceService = Depends()
): ):
subject = user_svc.get_user_by_uuid(user_id) subject = user_svc.get_user_by_uuid(uuid)
return service_svc.create(subject, service)
return service_svc.get_service_by_user(subject)
@api.get("", response_model=List[Service], tags=["Service"])
def get_all(
uuid: str, user_svc: UserService = Depends(), service_svc: ServiceService = Depends()
):
subject = user_svc.get_user_by_uuid(uuid)
return service_svc.get_service_by_user(subject)
@api.get("/{name}", response_model=Service, tags=["Service"])
def get_by_name(
name: str, uuid: str, user_svc: UserService = Depends(), service_svc: ServiceService = Depends()
):
subject = user_svc.get_user_by_uuid(uuid)
return service_svc.get_service_by_name(name, subject)
@api.put("", response_model=Service, tags=["Service"])
def update(
uuid: str, service: Service, user_svc: UserService = Depends(), service_svc: ServiceService = Depends()
):
subject = user_svc.get_user_by_uuid(uuid)
return service_svc.update(subject, service)
@api.delete("", response_model=None, tags=["Service"])
def delete(
uuid: str, service: Service, user_svc: UserService = Depends(), service_svc: ServiceService = Depends()
):
subject = user_svc.get_user_by_uuid(uuid)
service_svc.delete(subject, service)

51
backend/api/tag.py Normal file
View File

@ -0,0 +1,51 @@
from fastapi import APIRouter, Depends
from backend.models.tag_model import Tag
from backend.models.user_model import User
from backend.services.tag import TagService
from ..services import ResourceService, UserService
from ..models.resource_model import Resource
from typing import List
api = APIRouter(prefix="/api/tag")
openapi_tags = {
"name": "Tag",
"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,
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,
tag_svc: TagService=Depends()
):
return tag_svc.get_all()
@api.put("", response_model=Tag, tags=["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,
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

@ -1,67 +1,67 @@
""" Defines the table for storing resources """ """ Defines the table for storing resources """
# Import our mapped SQL types from SQLAlchemy # Import our mapped SQL types from SQLAlchemy
from sqlalchemy import Integer, String, DateTime, Enum from sqlalchemy import Integer, String, DateTime, Enum
# Import mapping capabilities from the SQLAlchemy ORM # Import mapping capabilities from the SQLAlchemy ORM
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
# Import the EntityBase that we are extending # Import the EntityBase that we are extending
from .entity_base import EntityBase from .entity_base import EntityBase
# Import datetime for created_at type # Import datetime for created_at type
from datetime import datetime from datetime import datetime
# Import self for to model # Import self for to model
from typing import Self from typing import Self
from backend.entities.program_enum import Program_Enum from backend.entities.program_enum import Program_Enum
from ..models.resource_model import Resource from ..models.resource_model import Resource
class ResourceEntity(EntityBase): class ResourceEntity(EntityBase):
# set table name # set table name
__tablename__ = "resource" __tablename__ = "resource"
# set fields # set fields
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
name: Mapped[str] = mapped_column(String(64), nullable=False) name: Mapped[str] = mapped_column(String(64), nullable=False)
summary: Mapped[str] = mapped_column(String(100), nullable=False) summary: Mapped[str] = mapped_column(String(100), nullable=False)
link: Mapped[str] = mapped_column(String, nullable=False) link: Mapped[str] = mapped_column(String, nullable=False)
program: Mapped[Program_Enum] = mapped_column(Enum(Program_Enum), nullable=False) program: Mapped[Program_Enum] = mapped_column(Enum(Program_Enum), nullable=False)
# relationships # relationships
resourceTags: Mapped[list["ResourceTagEntity"]] = relationship( resourceTags: Mapped[list["ResourceTagEntity"]] = relationship(
back_populates="resource", cascade="all,delete" back_populates="resource", cascade="all,delete"
) )
@classmethod @classmethod
def from_model(cls, model: Resource) -> Self: def from_model(cls, model: Resource) -> Self:
""" """
Create a UserEntity from a User model. Create a UserEntity from a User model.
Args: Args:
model (User): The model to create the entity from. model (User): The model to create the entity from.
Returns: Returns:
Self: The entity (not yet persisted). Self: The entity (not yet persisted).
""" """
return cls( return cls(
id=model.id, id=model.id,
created_at=model.created_at, created_at=model.created_at,
name=model.name, name=model.name,
summary=model.summary, summary=model.summary,
link=model.link, link=model.link,
program=model.program, program=model.program,
) )
def to_model(self) -> Resource: def to_model(self) -> Resource:
return Resource( return Resource(
id=self.id, id=self.id,
created_at=self.created_at, created_at=self.created_at,
name=self.name, name=self.name,
summary=self.summary, summary=self.summary,
link=self.link, link=self.link,
program=self.program, program=self.program,
) )

View File

@ -1,46 +1,46 @@
""" Defines the table for resource tags """ """ Defines the table for resource tags """
# Import our mapped SQL types from SQLAlchemy # Import our mapped SQL types from SQLAlchemy
from sqlalchemy import ForeignKey, Integer, String, DateTime from sqlalchemy import ForeignKey, Integer, String, DateTime
# Import mapping capabilities from the SQLAlchemy ORM # Import mapping capabilities from the SQLAlchemy ORM
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
# Import the EntityBase that we are extending # Import the EntityBase that we are extending
from .entity_base import EntityBase from .entity_base import EntityBase
# Import datetime for created_at type # Import datetime for created_at type
from datetime import datetime from datetime import datetime
# Import self for to model # Import self for to model
from typing import Self from typing import Self
class ResourceTagEntity(EntityBase): class ResourceTagEntity(EntityBase):
# set table name to user in the database # set table name to user in the database
__tablename__ = "resource_tag" __tablename__ = "resource_tag"
# set fields or 'columns' for the user table # set fields or 'columns' for the user table
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
resourceId: Mapped[int] = mapped_column(ForeignKey("resource.id")) resourceId: Mapped[int] = mapped_column(ForeignKey("resource.id"))
tagId: Mapped[int] = mapped_column(ForeignKey("tag.id")) tagId: Mapped[int] = mapped_column(ForeignKey("tag.id"))
# relationships # relationships
resource: Mapped["ResourceEntity"] = relationship(back_populates="resourceTags") resource: Mapped["ResourceEntity"] = relationship(back_populates="resourceTags")
tag: Mapped["TagEntity"] = relationship(back_populates="resourceTags") tag: Mapped["TagEntity"] = relationship(back_populates="resourceTags")
# @classmethod # @classmethod
# def from_model (cls, model: resource_tag_model) -> Self: # def from_model (cls, model: resource_tag_model) -> Self:
# return cls ( # return cls (
# id = model.id, # id = model.id,
# resourceId = model.resourceId, # resourceId = model.resourceId,
# tagId = model.tagId, # tagId = model.tagId,
# ) # )
# def to_model (self) -> resource_tag_model: # def to_model (self) -> resource_tag_model:
# return user_model( # return user_model(
# id = self.id, # id = self.id,
# resourceId = self.resourceId, # resourceId = self.resourceId,
# tagId = self.tagId, # tagId = self.tagId,
# ) # )

View File

@ -1,47 +1,47 @@
""" Defines the table for storing services """ """ Defines the table for storing services """
# Import our mapped SQL types from SQLAlchemy # Import our mapped SQL types from SQLAlchemy
from sqlalchemy import Integer, String, DateTime, ARRAY from sqlalchemy import Integer, String, DateTime, ARRAY
# Import mapping capabilities from the SQLAlchemy ORM # Import mapping capabilities from the SQLAlchemy ORM
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
# Import the EntityBase that we are extending # Import the EntityBase that we are extending
from .entity_base import EntityBase from .entity_base import EntityBase
# Import datetime for created_at type # Import datetime for created_at type
from datetime import datetime from datetime import datetime
# Import enums for Program # Import enums for Program
import enum import enum
from sqlalchemy import Enum from sqlalchemy import Enum
from backend.models.service_model import Service from backend.models.service_model import Service
from typing import Self from typing import Self
from backend.models.enum_for_models import ProgramTypeEnum from backend.models.enum_for_models import ProgramTypeEnum
class ServiceEntity(EntityBase): class ServiceEntity(EntityBase):
# set table name # set table name
__tablename__ = "service" __tablename__ = "service"
# set fields # set fields
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
name: Mapped[str] = mapped_column(String(32), nullable=False) name: Mapped[str] = mapped_column(String(32), nullable=False)
status: Mapped[str] = mapped_column(String(32), nullable=False) status: Mapped[str] = mapped_column(String(32), nullable=False)
summary: Mapped[str] = mapped_column(String(100), nullable=False) summary: Mapped[str] = mapped_column(String(100), nullable=False)
requirements: Mapped[list[str]] = mapped_column(ARRAY(String)) requirements: Mapped[list[str]] = mapped_column(ARRAY(String))
program: Mapped[ProgramTypeEnum] = mapped_column(Enum(ProgramTypeEnum), nullable=False) program: Mapped[ProgramTypeEnum] = mapped_column(Enum(ProgramTypeEnum), nullable=False)
# relationships # relationships
serviceTags: Mapped[list["ServiceTagEntity"]] = relationship( serviceTags: Mapped[list["ServiceTagEntity"]] = relationship(
back_populates="service", cascade="all,delete" back_populates="service", cascade="all,delete"
) )
def to_model(self) -> Service: def to_model(self) -> Service:
return Service(id=self.id, name=self.name, status=self.status, summary=self.summary, requirements=self.requirements, program=self.program) return Service(id=self.id, name=self.name, status=self.status, summary=self.summary, requirements=self.requirements, program=self.program)
@classmethod @classmethod
def from_model(cls, model:Service) -> Self: def from_model(cls, model:Service) -> Self:
return cls(id=model.id, name=model.name, status=model.status, summary=model.summary, requirements=model.requirements, program=model.program) return cls(id=model.id, name=model.name, status=model.status, summary=model.summary, requirements=model.requirements, program=model.program)

View File

@ -1,25 +1,26 @@
""" Defines the table for service tags """ """ Defines the table for service tags """
# Import our mapped SQL types from SQLAlchemy # Import our mapped SQL types from SQLAlchemy
from sqlalchemy import ForeignKey, Integer from sqlalchemy import ForeignKey, Integer
# Import mapping capabilities from the SQLAlchemy ORM # Import mapping capabilities from the SQLAlchemy ORM
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
# Import the EntityBase that we are extending # Import the EntityBase that we are extending
from .entity_base import EntityBase from .entity_base import EntityBase
class ServiceTagEntity(EntityBase): class ServiceTagEntity(EntityBase):
# set table name to user in the database # set table name to user in the database
__tablename__ = "service_tag" __tablename__ = "service_tag"
# set fields or 'columns' for the user table # set fields or 'columns' for the user table
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
serviceId: Mapped[int] = mapped_column(ForeignKey("service.id")) serviceId: Mapped[int] = mapped_column(ForeignKey("service.id"))
tagId: Mapped[int] = mapped_column(ForeignKey("tag.id")) tagId: Mapped[int] = mapped_column(ForeignKey("tag.id"))
# relationships # relationships
service: Mapped["ServiceEntity"] = relationship(back_populates="serviceTags") service: Mapped["ServiceEntity"] = relationship(back_populates="serviceTags")
tag: Mapped["TagEntity"] = relationship(back_populates="serviceTags") tag: Mapped["TagEntity"] = relationship(back_populates="serviceTags")

View File

@ -1,65 +1,64 @@
""" Defines the table for storing tags """ """ Defines the table for storing tags """
# Import our mapped SQL types from SQLAlchemy # Import our mapped SQL types from SQLAlchemy
from sqlalchemy import Integer, String, DateTime from sqlalchemy import Integer, String, DateTime
# Import mapping capabilities from the SQLAlchemy ORM # Import mapping capabilities from the SQLAlchemy ORM
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
# Import the EntityBase that we are extending # Import the EntityBase that we are extending
from .entity_base import EntityBase from .entity_base import EntityBase
# Import datetime for created_at type # Import datetime for created_at type
from datetime import datetime from datetime import datetime
from ..models.tag_model import Tag from ..models.tag_model import Tag
from typing import Self from typing import Self
class TagEntity(EntityBase): class TagEntity(EntityBase):
#set table name #set table name
__tablename__ = "tag" __tablename__ = "tag"
#set fields #set fields
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
content: Mapped[str] = mapped_column(String(100), nullable=False) content: Mapped[str] = mapped_column(String(100), nullable=False)
#relationships #relationships
resourceTags: Mapped[list["ResourceTagEntity"]] = relationship(back_populates="tag", cascade="all,delete") resourceTags: Mapped[list["ResourceTagEntity"]] = relationship(back_populates="tag", cascade="all,delete")
serviceTags: Mapped[list["ServiceTagEntity"]] = relationship(back_populates="tag", cascade="all,delete") serviceTags: Mapped[list["ServiceTagEntity"]] = relationship(back_populates="tag", cascade="all,delete")
@classmethod @classmethod
def from_model(cls, model: Tag) -> Self: def from_model(cls, model: Tag) -> Self:
""" """
Create a user entity from model Create a user entity from model
Args: model (User): the model to create the entity from Args: model (User): the model to create the entity from
Returns: Returns:
self: The entity self: The entity
""" """
return cls( return cls(
id=model.id, id=model.id,
content=model.id, created_at=model.created_at,
) content=model.id,
)
def to_model(self) -> Tag:
""" def to_model(self) -> Tag:
Create a user model from entity """
Create a user model from entity
Returns:
User: A User model for API usage Returns:
""" User: A User model for API usage
"""
return Tag(
id=self.id, return Tag(
content=self.content, id=self.id,
) create_at=self.created_at,
content=self.content,
)

View File

@ -1,34 +1,36 @@
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.middleware.gzip import GZipMiddleware from fastapi.middleware.gzip import GZipMiddleware
from .api import user, health, service, resource
from .api import user, health, service, resource, tag
description = """
Welcome to the **COMPASS** RESTful Application Programming Interface. description = """
""" Welcome to the **COMPASS** RESTful Application Programming Interface.
"""
app = FastAPI(
title="Compass API", app = FastAPI(
version="0.0.1", title="Compass API",
description=description, version="0.0.1",
openapi_tags=[ description=description,
user.openapi_tags, openapi_tags=[
health.openapi_tags, user.openapi_tags,
service.openapi_tags, health.openapi_tags,
resource.openapi_tags, service.openapi_tags,
], resource.openapi_tags,
) tag.openapi_tags
],
app.add_middleware(GZipMiddleware) )
feature_apis = [user, health, service, resource] app.add_middleware(GZipMiddleware)
for feature_api in feature_apis: feature_apis = [user, health, service, resource, tag]
app.include_router(feature_api.api)
for feature_api in feature_apis:
app.include_router(feature_api.api)
# Add application-wide exception handling middleware for commonly encountered API Exceptions
@app.exception_handler(Exception)
def permission_exception_handler(request: Request, e: Exception): # Add application-wide exception handling middleware for commonly encountered API Exceptions
return JSONResponse(status_code=403, content={"message": str(e)}) @app.exception_handler(Exception)
def permission_exception_handler(request: Request, e: Exception):
return JSONResponse(status_code=403, content={"message": str(e)})

View File

@ -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] created_at: Optional[datetime] = datetime.now()

View File

@ -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 = None created_at: datetime | None = datetime.now()
name: str name: str
status: str status: str
summary: str summary: str

View File

@ -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 = None created_at: datetime | None = datetime.now()

View File

@ -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] created_at: Optional[datetime] = datetime.now()
uuid: str | None = None uuid: str | None = None

View File

@ -20,6 +20,8 @@ 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."""

View File

@ -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 select from sqlalchemy import and_, 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 ResourceNotFoundException from .exceptions import ProgramNotAssignedException, ResourceNotFoundException
class ResourceService: class ResourceService:
@ -14,25 +14,40 @@ class ResourceService:
def __init__(self, session: Session = Depends(db_session)): def __init__(self, session: Session = Depends(db_session)):
self._session = session self._session = session
def get_resource_by_user(self, subject: User): def get_resource_by_user(self, subject: User) -> list[Resource]:
"""Resource method getting all of the resources that a user has access to based on role""" """Resource method getting all of the resources that a user has access to based on role"""
if subject.role != UserTypeEnum.VOLUNTEER: if subject.role != UserTypeEnum.VOLUNTEER:
query = select(ResourceEntity) query = select(ResourceEntity)
entities = self._session.scalars(query).all() entities = self._session.scalars(query).all()
return [resource.to_model() for resource in entities] return [resource.to_model() for resource in entities]
else: else:
programs = subject.program programs = subject.program
resources = [] resources = []
for program in programs: for program in programs:
query = select(ResourceEntity).filter(ResourceEntity.program == program) entities = (
entities = self._session.scalars(query).all() self._session.query(ResourceEntity)
.where(ResourceEntity.program == program)
.all()
)
for entity in entities: for entity in entities:
resources.append(entity) resources.append(entity.to_model())
return [resource for resource in resources]
return [resource.to_model() 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, user: 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:
@ -41,43 +56,16 @@ class ResourceService:
Returns: Returns:
Resource: Object added to table Resource: Object added to table
""" """
if user.role != UserTypeEnum.ADMIN: if subject.role != UserTypeEnum.ADMIN:
raise PermissionError( raise ProgramNotAssignedException(
"User does not have permission to add resources in this program." f"User is not {UserTypeEnum.ADMIN}, cannot update service"
) )
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: def update(self, subject: User, resource: Resource) -> 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.program.in_(user.program),
)
.one_or_none()
)
if resource is None:
raise ResourceNotFoundException(f"No resource found with id: {id}")
return resource.to_model()
def update(self, user: User, resource: ResourceEntity) -> Resource:
""" """
Update the resource if the user has access Update the resource if the user has access
Parameters: Parameters:
@ -88,28 +76,24 @@ class ResourceService:
Raises: Raises:
ResourceNotFoundException: If no resource is found with the corresponding ID ResourceNotFoundException: If no resource is found with the corresponding ID
""" """
if user.role != UserTypeEnum.ADMIN: if subject.role != UserTypeEnum.ADMIN:
raise PermissionError( raise ProgramNotAssignedException(
"User does not have permission to update this resource." f"User is not {UserTypeEnum.ADMIN}, cannot update service"
) )
query = select(ResourceEntity).where(ResourceEntity.id == resource.id)
obj = self._session.get(ResourceEntity, resource.id) if resource.id else None entity = self._session.scalars(query).one_or_none()
if entity is None:
if obj 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
obj.name = resource.name entity.summary = resource.summary
obj.summary = resource.summary entity.link = resource.link
obj.link = resource.link entity.program = resource.program
obj.program = resource.program
self._session.commit() self._session.commit()
return entity.to_model()
return obj.to_model() def delete(self, subject: User, resource: Resource) -> None:
def delete(self, user: User, id: int) -> None:
""" """
Delete resource based on id that the user has access to Delete resource based on id that the user has access to
Parameters: Parameters:
@ -118,23 +102,17 @@ class ResourceService:
Raises: Raises:
ResourceNotFoundException: If no resource is found with the corresponding id ResourceNotFoundException: If no resource is found with the corresponding id
""" """
if user.role != UserTypeEnum.ADMIN: if subject.role != UserTypeEnum.ADMIN:
raise PermissionError( raise ProgramNotAssignedException(
"User does not have permission to delete this resource." f"User is not {UserTypeEnum.ADMIN}, cannot update service"
) )
query = select(ResourceEntity).where(ResourceEntity.id == resource.id)
resource = ( entity = self._session.scalars(query).one_or_none()
self._session.query(ResourceEntity) if entity is None:
.filter( raise ResourceNotFoundException(
ResourceEntity.id == id, f"No resource found with matching id: {resource.id}"
) )
.one_or_none() self._session.delete(entity)
)
if resource is None:
raise ResourceNotFoundException(f"No resource found with matching id: {id}")
self._session.delete(resource)
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]:
@ -150,11 +128,11 @@ class ResourceService:
""" """
query = select(ResourceEntity).where( query = select(ResourceEntity).where(
ResourceEntity.name.ilike(f"%{search_string}%"), ResourceEntity.name.ilike(f"%{search_string}%"),
ResourceEntity.program.in_(user.program) ResourceEntity.program.in_(user.program),
) )
entities = self._session.scalars(query).all() entities = self._session.scalars(query).all()
if not entities: if not entities:
return [] return []
return [entity.to_model() for entity in entities] return [entity.to_model() for entity in entities]

View File

@ -1,127 +1,101 @@
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 func, select, and_, func, or_, exists, or_ from sqlalchemy import func, select, and_, func, or_, exists, or_
from backend.models.service_model import Service from backend.models.service_model import Service
from backend.models.user_model import User from backend.models.user_model import User
from backend.entities.service_entity import ServiceEntity from backend.entities.service_entity import ServiceEntity
from backend.models.enum_for_models import ProgramTypeEnum, UserTypeEnum from backend.models.enum_for_models import ProgramTypeEnum, UserTypeEnum
from backend.services.exceptions import ( from backend.services.exceptions import (
ServiceNotFoundException, ServiceNotFoundException,
ProgramNotAssignedException, ProgramNotAssignedException,
) )
class ServiceService: class ServiceService:
def __init__(self, session: Session = Depends(db_session)): def __init__(self, session: Session = Depends(db_session)):
self._session = session self._session = session
def get_service_by_program(self, program: ProgramTypeEnum) -> list[Service]: def get_service_by_user(self, subject: User) -> list[Service]:
"""Service method getting services belonging to a particular program.""" """Resource method getting all of the resources that a user has access to based on role"""
query = select(ServiceEntity).filter(ServiceEntity.program == program) if subject.role != UserTypeEnum.VOLUNTEER:
entities = self._session.scalars(query) query = select(ServiceEntity)
entities = self._session.scalars(query).all()
return [entity.to_model() for entity in entities] return [service.to_model() for service in entities]
else:
def get_service_by_id(self, id: int) -> Service: programs = subject.program
"""Service method getting services by id.""" services = []
query = select(ServiceEntity).filter(ServiceEntity.id == id) for program in programs:
entity = self._session.scalars(query).one_or_none() entities = self._session.query(ServiceEntity).where(ServiceEntity.program == program).all()
for entity in entities:
if entity is None: services.append(entity.to_model())
raise ServiceNotFoundException(f"Service with id: {id} does not exist") return [service for service in services]
return entity.to_model() def get_service_by_name(self, name: str, subject: User) -> Service:
"""Service method getting services by id."""
def get_service_by_name(self, name: str) -> Service: query = select(ServiceEntity).where(
"""Service method getting services by id.""" and_(
query = select(ServiceEntity).filter(ServiceEntity.name == name) ServiceEntity.name == name, ServiceEntity.program.in_(subject.program)
entity = self._session.scalars(query).one_or_none() )
)
if entity is None: entity = self._session.scalars(query).one_or_none()
raise ServiceNotFoundException(f"Service with name: {name} does not exist")
if entity is None:
return entity.to_model() raise ServiceNotFoundException(f"Service with name: {name} does not exist or program has not been assigned")
def get_service_by_user(self, subject: User): return entity.to_model()
"""Service method getting all of the services that a user has access to based on role"""
if subject.role != UserTypeEnum.VOLUNTEER: def create(self, subject: User, service: Service) -> Service:
query = select(ServiceEntity) """Creates/adds a service to the table."""
entities = self._session.scalars(query).all() if subject.role != UserTypeEnum.ADMIN:
raise ProgramNotAssignedException(
return [service.to_model() for service in entities] f"User is not {UserTypeEnum.ADMIN}, cannot create service"
else: )
programs = subject.program
services = [] service_entity = ServiceEntity.from_model(service)
for program in programs: self._session.add(service_entity)
query = select(ServiceEntity).filter(ServiceEntity.program == program) self._session.commit()
entities = self._session.scalars(query).all() return service_entity.to_model()
for entity in entities:
services.append(entity) def update(self, subject: User, service: Service) -> Service:
"""Updates a service if in the table."""
return [service.to_model() for service in services] if subject.role != UserTypeEnum.ADMIN:
raise ProgramNotAssignedException(
def get_all(self, subject: User) -> list[Service]: f"User is not {UserTypeEnum.ADMIN}, cannot update service"
"""Service method retrieving all of the services in the table.""" )
if subject.role == UserTypeEnum.VOLUNTEER:
raise ProgramNotAssignedException( query = select(ServiceEntity).where(ServiceEntity.id == service.id)
f"User is not {UserTypeEnum.ADMIN} or {UserTypeEnum.VOLUNTEER}, cannot get all" entity = self._session.scalars(query).one_or_none()
)
if entity is None:
query = select(ServiceEntity) raise ServiceNotFoundException(
entities = self._session.scalars(query).all() "The service you are searching for does not exist."
)
return [service.to_model() for service in entities]
entity.name = service.name
def create(self, subject: User, service: Service) -> Service: entity.status = service.status
"""Creates/adds a service to the table.""" entity.summary = service.summary
if subject.role != UserTypeEnum.ADMIN: entity.requirements = service.requirements
raise ProgramNotAssignedException( entity.program = service.program
f"User is not {UserTypeEnum.ADMIN}, cannot create service" self._session.commit()
)
return entity.to_model()
service_entity = ServiceEntity.from_model(service)
self._session.add(service_entity) def delete(self, subject: User, service: Service) -> None:
self._session.commit() """Deletes a service from the table."""
return service_entity.to_model() if subject.role != UserTypeEnum.ADMIN:
raise ProgramNotAssignedException(f"User is not {UserTypeEnum.ADMIN}")
def update(self, subject: User, service: Service) -> Service:
"""Updates a service if in the table.""" query = select(ServiceEntity).where(ServiceEntity.id == service.id)
if subject.role != UserTypeEnum.ADMIN: entity = self._session.scalars(query).one_or_none()
raise ProgramNotAssignedException(
f"User is not {UserTypeEnum.ADMIN}, cannot update service" if entity is None:
) raise ServiceNotFoundException(
"The service you are searching for does not exist."
service_entity = self._session.get(ServiceEntity, service.id) )
if service_entity is None: self._session.delete(entity)
raise ServiceNotFoundException( self._session.commit()
"The service you are searching for does not exist."
)
service_entity.name = service.name
service_entity.status = service.status
service_entity.summary = service.summary
service_entity.requirements = service.requirements
service_entity.program = service.program
self._session.commit()
return service_entity.to_model()
def delete(self, subject: User, service: Service) -> None:
"""Deletes a service from the table."""
if subject.role != UserTypeEnum.ADMIN:
raise ProgramNotAssignedException(f"User is not {UserTypeEnum.ADMIN}")
service_entity = self._session.get(ServiceEntity, service.id)
if service_entity is None:
raise ServiceNotFoundException(
"The service you are searching for does not exist."
)
self._session.delete(service_entity)
self._session.commit()

View File

@ -1,20 +1,52 @@
from fastapi import Depends from fastapi import Depends
from ..database import db_session
from sqlalchemy.orm import Session from backend.models.enum_for_models import UserTypeEnum
from ..models.tag_model import Tag from backend.models.user_model import User
from ..entities.tag_entity import TagEntity from backend.services.exceptions import TagNotFoundException
from sqlalchemy import select from ..database import db_session
from sqlalchemy.orm import Session
from ..models.tag_model import Tag
class TagService: from ..entities.tag_entity import TagEntity
from sqlalchemy import select
def __init__(self, session: Session = Depends(db_session)):
self._session = session # Add in checks for user permission?
class TagService:
def all(self) -> list[Tag]:
"""Returns a list of all Tags""" def __init__(self, session: Session = Depends(db_session)):
self._session = session
query = select(TagEntity)
entities = self._session.scalars(query).all() def get_all(self) -> list[Tag]:
"""Returns a list of all Tags"""
return [entity.to_model() for entity in entities] query = select(TagEntity)
entities = self._session.scalars(query).all()
return [entity.to_model() for entity in entities]
def create(self, subject: User, tag: Tag) -> Tag:
entity = TagEntity.from_model(tag)
self._session.add(entity)
self._session.commit()
return entity.to_model()
def update(self, subject: User, tag: Tag) -> Tag:
query = select(TagEntity).where(TagEntity.id == tag.id)
entity = self._session.scalars(query).one_or_none()
if entity is None:
raise TagNotFoundException(f"Tag with id {tag.id} does not exist")
entity.content = tag.content
self._session.commit()
return entity.to_model()
def delete(self, subject: User, tag: Tag) -> None:
query = select(TagEntity).where(TagEntity.id == tag.id)
entity = self._session.scalars(query).one_or_none()
if entity is None:
raise TagNotFoundException(f"Tag with id {tag.id} does not exist")
self._session.delete(entity)
self._session.commit()

View File

@ -27,7 +27,7 @@ export default function Page() {
); );
const resourcesAPI: Resource[] = await userListData.json(); const resourcesAPI: Resource[] = await userListData.json();
setResources(resourcesAPI); setResources(resourcesAPI);
} }

View File

@ -1,257 +1,255 @@
import { Dispatch, FunctionComponent, ReactNode, SetStateAction } from "react"; import { Dispatch, FunctionComponent, ReactNode, SetStateAction } 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 {
StarIcon as SolidStarIcon, StarIcon as SolidStarIcon,
EnvelopeIcon, EnvelopeIcon,
UserIcon, UserIcon,
} from "@heroicons/react/24/solid"; } from "@heroicons/react/24/solid";
import { import {
ArrowsPointingOutIcon, ArrowsPointingOutIcon,
ArrowsPointingInIcon, ArrowsPointingInIcon,
StarIcon as OutlineStarIcon, StarIcon as OutlineStarIcon,
ListBulletIcon, ListBulletIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import TagsInput from "../TagsInput/Index"; import TagsInput from "../TagsInput/Index";
type DrawerProps = { type DrawerProps = {
title: string; title: string;
children: ReactNode; children: ReactNode;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void; onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
type?: "button" | "submit" | "reset"; // specify possible values for type type?: "button" | "submit" | "reset"; // specify possible values for type
disabled?: boolean; disabled?: boolean;
editableContent?: any; editableContent?: any;
onSave?: (content: any) => void; onSave?: (content: any) => void;
rowContent?: any; rowContent?: any;
setData: Dispatch<SetStateAction<any>>; setData: Dispatch<SetStateAction<any>>;
}; };
interface EditContent { interface EditContent {
content: string; content: string;
isEditing: boolean; isEditing: boolean;
} }
const Drawer: FunctionComponent<DrawerProps> = ({ const Drawer: FunctionComponent<DrawerProps> = ({
title, title,
children, children,
onSave, onSave,
editableContent, editableContent,
rowContent, rowContent,
setData, setData,
}) => { }) => {
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) => { const onRowUpdate = (updatedRow: any) => {
setData((prevData: any) => ( setData((prevData: any) =>
prevData.map((row: any) => ( prevData.map((row: any) =>
row.id === updatedRow.id row.id === updatedRow.id ? updatedRow : row
? updatedRow )
: row );
)) };
))
}; const handleTempRowContentChange = (e) => {
const { name, value } = e.target;
const handleTempRowContentChange = (e) => { console.log(name);
const { name, value } = e.target; console.log(value);
console.log(name); setTempRowContent((prevContent) => ({
console.log(value); ...prevContent,
setTempRowContent((prevContent) => ({ [name]: value,
...prevContent, }));
[name]: value, };
}));
}; const handleEnterPress = (e) => {
if (e.key === "Enter") {
const handleEnterPress = (e) => { e.preventDefault();
if (e.key === "Enter") { // Update the rowContent with the temporaryRowContent
e.preventDefault(); if (onRowUpdate) {
// Update the rowContent with the temporaryRowContent onRowUpdate(tempRowContent);
if (onRowUpdate) { }
onRowUpdate(tempRowContent); }
} };
}
}; const toggleDrawer = () => {
setIsOpen(!isOpen);
const toggleDrawer = () => { if (isFull) {
setIsOpen(!isOpen); setIsFull(!isFull);
if (isFull) { }
setIsFull(!isFull); };
}
}; const toggleDrawerFullScreen = () => setIsFull(!isFull);
const toggleDrawerFullScreen = () => setIsFull(!isFull); const toggleFavorite = () => setIsFavorite(!isFavorite);
const toggleFavorite = () => setIsFavorite(!isFavorite); const drawerClassName = `fixed top-0 right-0 w-1/2 h-full bg-white transform ease-in-out duration-300 z-20 ${
isOpen ? "translate-x-0 shadow-xl" : "translate-x-full"
const drawerClassName = `fixed top-0 right-0 w-1/2 h-full bg-white transform ease-in-out duration-300 z-20 ${ } ${isFull ? "w-full" : "w-1/2"}`;
isOpen ? "translate-x-0 shadow-xl" : "translate-x-full"
} ${isFull ? "w-full" : "w-1/2"}`; const iconComponent = isFull ? (
<ArrowsPointingInIcon className="h-5 w-5" />
const iconComponent = isFull ? ( ) : (
<ArrowsPointingInIcon className="h-5 w-5" /> <ArrowsPointingOutIcon className="h-5 w-5" />
) : ( );
<ArrowsPointingOutIcon className="h-5 w-5" />
); const favoriteIcon = isFavorite ? (
<SolidStarIcon className="h-5 w-5" />
const favoriteIcon = isFavorite ? ( ) : (
<SolidStarIcon className="h-5 w-5" /> <OutlineStarIcon className="h-5 w-5" />
) : ( );
<OutlineStarIcon className="h-5 w-5" />
); const [presetOptions, setPresetOptions] = useState([
"administrator",
const [presetOptions, setPresetOptions] = useState([ "volunteer",
"administrator", "employee",
"volunteer", ]);
"employee", const [rolePresetOptions, setRolePresetOptions] = useState([
]); "domestic",
const [rolePresetOptions, setRolePresetOptions] = useState([ "community",
"domestic", "economic",
"community", ]);
"economic", const [tagColors, setTagColors] = useState(new Map());
]);
const [tagColors, setTagColors] = useState(new Map()); const getTagColor = (tag: string) => {
if (!tagColors.has(tag)) {
const getTagColor = (tag: string) => { const colors = [
if (!tagColors.has(tag)) { "bg-cyan-100",
const colors = [ "bg-blue-100",
"bg-cyan-100", "bg-green-100",
"bg-blue-100", "bg-yellow-100",
"bg-green-100", "bg-purple-100",
"bg-yellow-100", ];
"bg-purple-100", const randomColor =
]; colors[Math.floor(Math.random() * colors.length)];
const randomColor = setTagColors(new Map(tagColors).set(tag, randomColor));
colors[Math.floor(Math.random() * colors.length)]; }
setTagColors(new Map(tagColors).set(tag, randomColor)); return tagColors.get(tag);
} };
return tagColors.get(tag);
}; return (
<div>
return ( <button
<div> className={
<button "ml-2 text-xs uppercase opacity-0 group-hover:opacity-100 text-gray-500 font-medium border border-gray-200 bg-white shadow hover:bg-gray-50 p-2 rounded-md"
className={ }
"ml-2 text-xs uppercase opacity-0 group-hover:opacity-100 text-gray-500 font-medium border border-gray-200 bg-white shadow hover:bg-gray-50 p-2 rounded-md" onClick={toggleDrawer}
} >
onClick={toggleDrawer} Open
> </button>
Open <div className={drawerClassName}></div>
</button> <div className={drawerClassName}>
<div className={drawerClassName}></div> <div className="flex items-center justify-between p-4">
<div className={drawerClassName}> <div className="flex flex-row items-center justify-between space-x-2">
<div className="flex items-center justify-between p-4"> <span className="h-5 text-purple-200 w-5">
<div className="flex flex-row items-center justify-between space-x-2"> <UserIcon />
<span className="h-5 text-purple-200 w-5"> </span>
<UserIcon /> <h2 className="text-lg text-gray-800 font-semibold">
</span> {rowContent.username}
<h2 className="text-lg text-gray-800 font-semibold"> </h2>
{rowContent.username} </div>
</h2> <div>
</div> <button
<div> onClick={toggleFavorite}
<button className="py-2 text-gray-500 hover:text-gray-800 mr-2"
onClick={toggleFavorite} >
className="py-2 text-gray-500 hover:text-gray-800 mr-2" {favoriteIcon}
> </button>
{favoriteIcon} <button
</button> onClick={toggleDrawerFullScreen}
<button className="py-2 text-gray-500 hover:text-gray-800 mr-2"
onClick={toggleDrawerFullScreen} >
className="py-2 text-gray-500 hover:text-gray-800 mr-2" {iconComponent}
> </button>
{iconComponent} <button
</button> onClick={toggleDrawer}
<button className="py-2 text-gray-500 hover:text-gray-800"
onClick={toggleDrawer} >
className="py-2 text-gray-500 hover:text-gray-800" <ChevronDoubleLeftIcon className="h-5 w-5" />
> </button>
<ChevronDoubleLeftIcon className="h-5 w-5" /> </div>
</button> </div>
</div> <div className="p-4">
</div> <table className="p-4">
<div className="p-4"> <tbody className="items-center">
<table className="p-4"> <tr className="w-full text-xs items-center flex flex-row justify-between">
<tbody className="items-center"> <div className="flex flex-row space-x-2 text-gray-500 items-center">
<tr className="w-full text-xs items-center flex flex-row justify-between"> <td>
<div className="flex flex-row space-x-2 text-gray-500 items-center"> <UserIcon className="h-4 w-4" />
<td> </td>
<UserIcon className="h-4 w-4" /> <td className="w-32">Username</td>
</td> </div>
<td className="w-32">Username</td> <td className="w-3/4 w-3/4 p-2 pl-0">
</div> <input
<td className="w-3/4 w-3/4 p-2 pl-0"> type="text"
<input name="username"
type="text" value={tempRowContent.username}
name="username" onChange={handleTempRowContentChange}
value={tempRowContent.username} onKeyDown={handleEnterPress}
onChange={handleTempRowContentChange} className="ml-2 w-full p-1 focus:outline-gray-200 hover:bg-gray-50"
onKeyDown={handleEnterPress} />
className="ml-2 w-full p-1 focus:outline-gray-200 hover:bg-gray-50" </td>
/> </tr>
</td> <tr className="w-full text-xs items-center flex flex-row justify-between">
</tr> <div className="flex flex-row space-x-2 text-gray-500 items-center">
<tr className="w-full text-xs items-center flex flex-row justify-between"> <td>
<div className="flex flex-row space-x-2 text-gray-500 items-center"> <ListBulletIcon className="h-4 w-4" />
<td> </td>
<ListBulletIcon className="h-4 w-4" /> <td className="w-32">Role</td>
</td> </div>
<td className="w-32">Role</td> <td className="w-3/4 hover:bg-gray-50">
</div> <TagsInput
<td className="w-3/4 hover:bg-gray-50"> presetValue={tempRowContent.role}
<TagsInput presetOptions={presetOptions}
presetValue={tempRowContent.role} setPresetOptions={setPresetOptions}
presetOptions={presetOptions} getTagColor={getTagColor}
setPresetOptions={setPresetOptions} setTagColors={setTagColors}
getTagColor={getTagColor} />
setTagColors={setTagColors} </td>
/> </tr>
</td> <tr className="w-full text-xs items-center flex flex-row justify-between">
</tr> <div className="flex flex-row space-x-2 text-gray-500 items-center">
<tr className="w-full text-xs items-center flex flex-row justify-between"> <td>
<div className="flex flex-row space-x-2 text-gray-500 items-center"> <EnvelopeIcon className="h-4 w-4" />
<td> </td>
<EnvelopeIcon className="h-4 w-4" /> <td className="w-32">Email</td>
</td> </div>
<td className="w-32">Email</td> <td className="w-3/4 p-2 pl-0">
</div> <input
<td className="w-3/4 p-2 pl-0"> type="text"
<input name="email"
type="text" value={tempRowContent.email}
name="email" onChange={handleTempRowContentChange}
value={tempRowContent.email} onKeyDown={handleEnterPress}
onChange={handleTempRowContentChange} className="ml-2 w-80 p-1 font-normal hover:text-gray-400 focus:outline-gray-200 hover:bg-gray-50 underline text-gray-500"
onKeyDown={handleEnterPress} />
className="ml-2 w-80 p-1 font-normal hover:text-gray-400 focus:outline-gray-200 hover:bg-gray-50 underline text-gray-500" </td>
/> </tr>
</td> <tr className="w-full text-xs items-center flex flex-row justify-between">
</tr> <div className="flex flex-row space-x-2 text-gray-500 items-center">
<tr className="w-full text-xs items-center flex flex-row justify-between"> <td>
<div className="flex flex-row space-x-2 text-gray-500 items-center"> <ListBulletIcon className="h-4 w-4" />
<td> </td>
<ListBulletIcon className="h-4 w-4" /> <td className="w-32">Type of Program</td>
</td> </div>
<td className="w-32">Type of Program</td> <td className="w-3/4 hover:bg-gray-50">
</div> {/* {rowContent.program} */}
<td className="w-3/4 hover:bg-gray-50"> <TagsInput
{/* {rowContent.program} */} presetValue={tempRowContent.program}
<TagsInput presetOptions={rolePresetOptions}
presetValue={tempRowContent.program} setPresetOptions={setRolePresetOptions}
presetOptions={rolePresetOptions} getTagColor={getTagColor}
setPresetOptions={setRolePresetOptions} setTagColors={setTagColors}
getTagColor={getTagColor} />
setTagColors={setTagColors} </td>
/> </tr>
</td> </tbody>
</tr> </table>
</tbody> <br />
</table> </div>
<br /> </div>
</div> </div>
</div> );
</div> };
);
}; export default Drawer;
export default Drawer;

View File

@ -8,24 +8,20 @@ import TagsInput from "@/components/TagsInput/Index";
import Resource from "@/utils/models/Resource"; import Resource from "@/utils/models/Resource";
type ResourceTableProps = { type ResourceTableProps = {
data: Resource[], data: Resource[];
setData: Dispatch<SetStateAction<Resource[]>> setData: Dispatch<SetStateAction<Resource[]>>;
} };
/** /**
* Table componenet used for displaying resources * Table componenet used for displaying resources
* @param props.data Stateful list of resources to be displayed by the table * @param props.data Stateful list of resources to be displayed by the table
* @param props.setData State setter for the list of resources * @param props.setData State setter for the list of resources
*/ */
export default function ResourceTable({ data, setData }: ResourceTableProps ) { export default function ResourceTable({ data, setData }: ResourceTableProps) {
const columnHelper = createColumnHelper<Resource>(); const columnHelper = createColumnHelper<Resource>();
// Set up tag handling // Set up tag handling
const programProps = useTagsHandler([ const programProps = useTagsHandler(["community", "domestic", "economic"]);
"community",
"domestic",
"economic",
])
// Define Tanstack columns // Define Tanstack columns
const columns: ColumnDef<Resource, any>[] = [ const columns: ColumnDef<Resource, any>[] = [
@ -66,10 +62,7 @@ export default function ResourceTable({ data, setData }: ResourceTableProps ) {
</> </>
), ),
cell: (info) => ( cell: (info) => (
<TagsInput <TagsInput presetValue={info.getValue()} {...programProps} />
presetValue={info.getValue()}
{...programProps}
/>
), ),
}), }),
@ -85,5 +78,5 @@ export default function ResourceTable({ data, setData }: ResourceTableProps ) {
}), }),
]; ];
return <Table data={data} setData={setData} columns={columns}/> return <Table data={data} setData={setData} columns={columns} />;
} }

View File

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

View File

@ -1,108 +1,103 @@
import { Bars2Icon } from "@heroicons/react/24/solid"; import { Bars2Icon } from "@heroicons/react/24/solid";
import { Dispatch, SetStateAction } from "react"; import { Dispatch, SetStateAction } from "react";
import useTagsHandler from "@/components/TagsInput/TagsHandler"; import useTagsHandler from "@/components/TagsInput/TagsHandler";
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"; import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
import Table from "@/components/Table/Table"; import Table from "@/components/Table/Table";
import { RowOpenAction } from "@/components/Table/RowOpenAction"; import { RowOpenAction } from "@/components/Table/RowOpenAction";
import TagsInput from "@/components/TagsInput/Index"; import TagsInput from "@/components/TagsInput/Index";
import Service from "@/utils/models/Service"; import Service from "@/utils/models/Service";
type ServiceTableProps = { type ServiceTableProps = {
data: Service[], data: Service[];
setData: Dispatch<SetStateAction<Service[]>> setData: Dispatch<SetStateAction<Service[]>>;
} };
/** /**
* Table componenet used for displaying services * Table componenet used for displaying services
* @param props.data Stateful list of services to be displayed by the table * @param props.data Stateful list of services to be displayed by the table
* @param props.setData State setter for the list of services * @param props.setData State setter for the list of services
*/ */
export default function ServiceTable({ data, setData }: ServiceTableProps ) { export default function ServiceTable({ data, setData }: ServiceTableProps) {
const columnHelper = createColumnHelper<Service>(); const columnHelper = createColumnHelper<Service>();
// Set up tag handling // Set up tag handling
const programProps = useTagsHandler([ const programProps = useTagsHandler(["community", "domestic", "economic"]);
"community",
"domestic", // TODO: Dynamically or statically get full list of preset requirement tag options
"economic", const requirementProps = useTagsHandler([
]) "anonymous",
"confidential",
// TODO: Dynamically or statically get full list of preset requirement tag options "referral required",
const requirementProps = useTagsHandler([ "safety assessment",
'anonymous', "intake required",
'confidential', "income eligibility",
'referral required', "initial assessment",
'safety assessment', ]);
'intake required',
'income eligibility', // Define Tanstack columns
'initial assessment', const columns: ColumnDef<Service, any>[] = [
]) columnHelper.accessor("name", {
header: () => (
// Define Tanstack columns <>
const columns: ColumnDef<Service, any>[] = [ <Bars2Icon className="inline align-top h-4" /> Name
columnHelper.accessor("name", { </>
header: () => ( ),
<> cell: (info) => (
<Bars2Icon className="inline align-top h-4" /> Name <RowOpenAction
</> title={info.getValue()}
), rowData={info.row.original}
cell: (info) => ( setData={setData}
<RowOpenAction />
title={info.getValue()} ),
rowData={info.row.original} }),
setData={setData} columnHelper.accessor("status", {
/> header: () => (
), <>
}), <Bars2Icon className="inline align-top h-4" /> Status
columnHelper.accessor("status", { </>
header: () => ( ),
<> cell: (info) => (
<Bars2Icon className="inline align-top h-4" /> Status <span className="ml-2 text-gray-500">{info.getValue()}</span>
</> ),
), }),
cell: (info) => ( columnHelper.accessor("program", {
<span className="ml-2 text-gray-500">{info.getValue()}</span> header: () => (
), <>
}), <Bars2Icon className="inline align-top h-4" /> Program
columnHelper.accessor("program", { </>
header: () => ( ),
<> cell: (info) => (
<Bars2Icon className="inline align-top h-4" /> Program <TagsInput presetValue={info.getValue()} {...programProps} />
</> ),
), }),
cell: (info) => ( columnHelper.accessor("requirements", {
<TagsInput header: () => (
presetValue={info.getValue()} <>
{...programProps} <Bars2Icon className="inline align-top h-4" /> Requirements
/> </>
), ),
}), cell: (info) => (
columnHelper.accessor("requirements", { // TODO: Setup different tag handler for requirements
header: () => ( <TagsInput
<> presetValue={
<Bars2Icon className="inline align-top h-4" /> Requirements info.getValue()[0] !== "" ? info.getValue() : ["N/A"]
</> }
), {...requirementProps}
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
</>
columnHelper.accessor("summary", { ),
header: () => ( cell: (info) => (
<> <span className="ml-2 text-gray-500">{info.getValue()}</span>
<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} />;
), }
}),
];
return <Table data={data} setData={setData} columns={columns} />
};

View File

@ -1,224 +1,225 @@
import { import {
Row, Row,
ColumnDef, ColumnDef,
useReactTable, useReactTable,
getCoreRowModel, getCoreRowModel,
flexRender, flexRender,
createColumnHelper createColumnHelper,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import { import {
ChangeEvent, ChangeEvent,
useState, useState,
useEffect, useEffect,
Key, Key,
Dispatch, Dispatch,
SetStateAction SetStateAction,
} from "react"; } from "react";
import { TableAction } from "./TableAction"; import { TableAction } from "./TableAction";
import { PlusIcon } from "@heroicons/react/24/solid"; import { PlusIcon } from "@heroicons/react/24/solid";
import { rankItem } from "@tanstack/match-sorter-utils"; import { rankItem } from "@tanstack/match-sorter-utils";
import { RowOptionMenu } from "./RowOptionMenu"; import { RowOptionMenu } from "./RowOptionMenu";
import DataPoint from "@/utils/models/DataPoint"; import DataPoint from "@/utils/models/DataPoint";
type TableProps<T extends DataPoint> = { type TableProps<T extends DataPoint> = {
data: T[], data: T[];
setData: Dispatch<SetStateAction<T[]>>, setData: Dispatch<SetStateAction<T[]>>;
columns: ColumnDef<T, any>[] columns: ColumnDef<T, any>[];
}; };
/** Fuzzy search function */ /** Fuzzy search function */
const fuzzyFilter = ( const fuzzyFilter = (
row: Row<any>, row: Row<any>,
columnId: string, columnId: string,
value: any, value: any,
addMeta: (meta: any) => void addMeta: (meta: any) => void
) => { ) => {
// Rank the item // Rank the item
const itemRank = rankItem(row.getValue(columnId), value); const itemRank = rankItem(row.getValue(columnId), value);
// Store the ranking info // Store the ranking info
addMeta(itemRank); addMeta(itemRank);
// Return if the item should be filtered in/out // Return if the item should be filtered in/out
return itemRank.passed; return itemRank.passed;
}; };
/** /**
* General componenet that holds shared functionality for any data table component * 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.data Stateful list of data to be held in the table
* @param props.setData State setter for the list of data * @param props.setData State setter for the list of data
* @param props.columns Column definitions made with Tanstack columnHelper * @param props.columns Column definitions made with Tanstack columnHelper
*/ */
export default function Table<T extends DataPoint>({ data, setData, columns }: TableProps<T>) { export default function Table<T extends DataPoint>({
const columnHelper = createColumnHelper<T>(); data,
setData,
/** Sorting function based on visibility */ columns,
const visibilitySort = (a: T, b: T) => ( }: TableProps<T>) {
a.visible === b.visible const columnHelper = createColumnHelper<T>();
? 0
: a.visible ? -1 : 1 /** 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(() => { // Sort data on load
setData(prevData => prevData.sort(visibilitySort)) useEffect(() => {
}, [setData]); setData((prevData) => prevData.sort(visibilitySort));
}, [setData]);
// Data manipulation methods
// TODO: Connect data manipulation methods to the database (deleteData, hideData, addData) // Data manipulation methods
const deleteData = (dataId: number) => { // TODO: Connect data manipulation methods to the database (deleteData, hideData, addData)
console.log(data); const deleteData = (dataId: number) => {
setData((currentData) => console.log(data);
currentData.filter((data) => data.id !== dataId) setData((currentData) =>
); currentData.filter((data) => data.id !== dataId)
}; );
};
const hideData = (dataId: number) => {
console.log(`Toggling visibility for data with ID: ${dataId}`); const hideData = (dataId: number) => {
setData(currentData => { console.log(`Toggling visibility for data with ID: ${dataId}`);
const newData = currentData setData((currentData) => {
.map(data => ( const newData = currentData
data.id === dataId .map((data) =>
? { ...data, visible: !data.visible } data.id === dataId
: data ? { ...data, visible: !data.visible }
)) : data
.sort(visibilitySort); )
.sort(visibilitySort);
console.log(newData);
return newData; console.log(newData);
}); return newData;
}; });
};
const addData = () => {
setData([...data]); const addData = () => {
}; setData([...data]);
};
// Add data manipulation options to the first column
columns.unshift( // Add data manipulation options to the first column
columnHelper.display({ columns.unshift(
id: "options", columnHelper.display({
cell: (props) => ( id: "options",
<RowOptionMenu cell: (props) => (
onDelete={() => deleteData(props.row.original.id)} <RowOptionMenu
onHide={() => hideData(props.row.original.id)} onDelete={() => deleteData(props.row.original.id)}
/> onHide={() => hideData(props.row.original.id)}
), />
}) ),
) })
);
// Searching
const [query, setQuery] = useState(""); // Searching
const handleSearchChange = (e: ChangeEvent) => { const [query, setQuery] = useState("");
const target = e.target as HTMLInputElement; const handleSearchChange = (e: ChangeEvent) => {
setQuery(String(target.value)); const target = e.target as HTMLInputElement;
}; setQuery(String(target.value));
};
const handleCellChange = (e: ChangeEvent, key: Key) => {
const target = e.target as HTMLInputElement; const handleCellChange = (e: ChangeEvent, key: Key) => {
console.log(key); const target = e.target as HTMLInputElement;
}; console.log(key);
};
// TODO: Filtering
// TODO: Filtering
// TODO: Sorting
// TODO: Sorting
// Define Tanstack table
const table = useReactTable({ // Define Tanstack table
columns, const table = useReactTable({
data, columns,
filterFns: { data,
fuzzy: fuzzyFilter, filterFns: {
}, fuzzy: fuzzyFilter,
state: { },
globalFilter: query, state: {
}, globalFilter: query,
onGlobalFilterChange: setQuery, },
globalFilterFn: fuzzyFilter, onGlobalFilterChange: setQuery,
getCoreRowModel: getCoreRowModel(), globalFilterFn: fuzzyFilter,
}); getCoreRowModel: getCoreRowModel(),
});
const handleRowData = (row: any) => {
const rowData: any = {}; const handleRowData = (row: any) => {
row.cells.forEach((cell: any) => { const rowData: any = {};
rowData[cell.column.id] = cell.value; 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); // Use rowData object containing data from all columns for the current row
return rowData; console.log(rowData);
}; return rowData;
};
return (
<div className="flex flex-col"> return (
<div className="flex flex-row justify-end"> <div className="flex flex-col">
<TableAction query={query} handleChange={handleSearchChange} /> <div className="flex flex-row justify-end">
</div> <TableAction query={query} handleChange={handleSearchChange} />
<table className="w-full text-xs text-left rtl:text-right"> </div>
<thead className="text-xs text-gray-500 capitalize"> <table className="w-full text-xs text-left rtl:text-right">
{table.getHeaderGroups().map((headerGroup) => ( <thead className="text-xs text-gray-500 capitalize">
<tr key={headerGroup.id}> {table.getHeaderGroups().map((headerGroup) => (
{headerGroup.headers.map((header, i) => ( <tr key={headerGroup.id}>
<th {headerGroup.headers.map((header, i) => (
scope="col" <th
className={ scope="col"
"p-2 border-gray-200 border-y font-medium " + className={
(1 < i && i < columns.length - 1 "p-2 border-gray-200 border-y font-medium " +
? "border-x" (1 < i && i < columns.length - 1
: "") ? "border-x"
} : "")
key={header.id} }
> key={header.id}
{header.isPlaceholder >
? null {header.isPlaceholder
: flexRender( ? null
header.column.columnDef.header, : flexRender(
header.getContext() header.column.columnDef.header,
)} header.getContext()
</th> )}
))} </th>
</tr> ))}
))} </tr>
</thead> ))}
<tbody> </thead>
{table.getRowModel().rows.map((row) => { <tbody>
// Individual row {table.getRowModel().rows.map((row) => {
const isDataVisible = row.original.visible; // Individual row
const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${ const isDataVisible = row.original.visible;
!isDataVisible ? "bg-gray-200 text-gray-500" : "" const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${
}`; !isDataVisible ? "bg-gray-200 text-gray-500" : ""
return ( }`;
<tr className={rowClassNames} key={row.id}> return (
{row.getVisibleCells().map((cell, i) => ( <tr className={rowClassNames} key={row.id}>
<td {row.getVisibleCells().map((cell, i) => (
key={cell.id} <td
className={ key={cell.id}
"[&:nth-child(n+3)]:border-x relative first:text-left first:px-0 last:border-none" className={
} "[&:nth-child(n+3)]:border-x relative first:text-left first:px-0 last:border-none"
> }
{flexRender( >
cell.column.columnDef.cell, {flexRender(
cell.getContext() cell.column.columnDef.cell,
)} cell.getContext()
</td> )}
))} </td>
</tr> ))}
); </tr>
})} );
</tbody> })}
<tfoot> </tbody>
<tr> <tfoot>
<td <tr>
className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50" <td
colSpan={100} className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50"
onClick={addData} colSpan={100}
> onClick={addData}
<span className="flex ml-1 text-gray-500"> >
<PlusIcon className="inline h-4 mr-1" /> <span className="flex ml-1 text-gray-500">
New <PlusIcon className="inline h-4 mr-1" />
</span> New
</td> </span>
</tr> </td>
</tfoot> </tr>
</table> </tfoot>
</div> </table>
); </div>
}; );
}

View File

@ -1,4 +1,8 @@
import { ArrowDownCircleIcon, AtSymbolIcon, Bars2Icon } from "@heroicons/react/24/solid"; import {
ArrowDownCircleIcon,
AtSymbolIcon,
Bars2Icon,
} from "@heroicons/react/24/solid";
import { Dispatch, SetStateAction } from "react"; import { Dispatch, SetStateAction } from "react";
import useTagsHandler from "@/components/TagsInput/TagsHandler"; import useTagsHandler from "@/components/TagsInput/TagsHandler";
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"; import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
@ -8,31 +12,27 @@ import TagsInput from "@/components/TagsInput/Index";
import User from "@/utils/models/User"; import User from "@/utils/models/User";
type UserTableProps = { type UserTableProps = {
data: User[], data: User[];
setData: Dispatch<SetStateAction<User[]>> setData: Dispatch<SetStateAction<User[]>>;
} };
/** /**
* Table componenet used for displaying users * Table componenet used for displaying users
* @param props.data Stateful list of users to be displayed by the table * @param props.data Stateful list of users to be displayed by the table
* @param props.setData State setter for the list of users * @param props.setData State setter for the list of users
*/ */
export default function UserTable({ data, setData }: UserTableProps ) { export default function UserTable({ data, setData }: UserTableProps) {
const columnHelper = createColumnHelper<User>(); const columnHelper = createColumnHelper<User>();
// Set up tag handling // Set up tag handling
const roleProps = useTagsHandler([ const roleProps = useTagsHandler([
"administrator", "administrator",
"volunteer", "volunteer",
"employee", "employee",
]) ]);
const programProps = useTagsHandler([ const programProps = useTagsHandler(["community", "domestic", "economic"]);
"community",
"domestic",
"economic",
])
// Define Tanstack columns // Define Tanstack columns
const columns: ColumnDef<User, any>[] = [ const columns: ColumnDef<User, any>[] = [
columnHelper.accessor("username", { columnHelper.accessor("username", {
@ -57,10 +57,7 @@ export default function UserTable({ data, setData }: UserTableProps ) {
</> </>
), ),
cell: (info) => ( cell: (info) => (
<TagsInput <TagsInput presetValue={info.getValue()} {...roleProps} />
presetValue={info.getValue()}
{...roleProps}
/>
), ),
}), }),
columnHelper.accessor("email", { columnHelper.accessor("email", {
@ -74,7 +71,7 @@ export default function UserTable({ data, setData }: UserTableProps ) {
{info.getValue()} {info.getValue()}
</span> </span>
), ),
}), }),
columnHelper.accessor("program", { columnHelper.accessor("program", {
header: () => ( header: () => (
<> <>
@ -83,13 +80,10 @@ export default function UserTable({ data, setData }: UserTableProps ) {
</> </>
), ),
cell: (info) => ( cell: (info) => (
<TagsInput <TagsInput presetValue={info.getValue()} {...programProps} />
presetValue={info.getValue()}
{...programProps}
/>
), ),
}), }),
]; ];
return <Table<User> data={data} setData={setData} columns={columns}/> return <Table<User> data={data} setData={setData} columns={columns} />;
} }

View File

@ -1,4 +1,4 @@
import { useState } from 'react'; import { useState } from "react";
/** /**
* Custom hook used to handle the state of tag options and colors * Custom hook used to handle the state of tag options and colors
@ -31,5 +31,5 @@ export default function useTagsHandler(initialOptions: string[]) {
return tagColors.get(tag) as string; return tagColors.get(tag) as string;
}; };
return { presetOptions, setPresetOptions, getTagColor } return { presetOptions, setPresetOptions, getTagColor };
} }

View File

@ -6,4 +6,4 @@ interface DataPoint {
visible: boolean; visible: boolean;
} }
export default DataPoint; export default DataPoint;