Compare commits

..

5 Commits

Author SHA1 Message Date
pmoharana-cmd
638f109896 Defaults created_at to current time 2024-11-04 16:25:47 -05:00
pmoharana-cmd
fd8935f73f Merge main 2024-11-04 16:25:19 -05:00
Emma Foster
99e43c7b30
Resource Service and Tests (#41)
* Implement Resource service, tests, and test data with 97% coverage.

* Update slug service to return empty list and corresponding tests

* Update resource update tests to grab resource by id from the db to check
2024-11-04 15:10:58 -05:00
Nicolas Asanov
2e0dd3b987
Refactored Table Components (#43)
* Created mock/test table and resource page to see if implementation works

* Fixed typing for TagsInput

* cleaned up imports

* Started moving data manipulation into Table

* moved data manipulation logic into Table

* added useTagsHandler custom hook to consolidate getTagColor and presetOptions state into one function

* Fixed type errors for RowOpenAction

* Refactored ServiceIndex

* Refactored user table

* Updated imports and client facing routes

* added documentation for table components

* Added documentation for TagsHandler

* small changes for cleaner code

* refactored typing for tables. More work needs to be done to ensure tables are overall working properly

* added todo

* updated client paths with new table props

* alterned handleRowUpdate to only use setData

* diverted responsibility of handleRowChange to Drawer instead of Table to remove repetition

* updated documentation

* added sorting util function to Table.tsx to reduce repetition

* Edited sorting func to be more comaptible and edited hideData to be more concise

* formatting

* updated imports

* updated tags for all tables

* removed DataPoint dependecy from User, Service, and Resource models as it was unnecesary

* Added inline documentation to table components

* added documentation for DataPoint model

* Update package-lock.json
2024-11-04 15:10:13 -05:00
pmoharana-cmd
cb54c9829d Remove angular cli dependency 2024-10-15 19:28:32 -04:00
25 changed files with 713 additions and 913 deletions

View File

@ -47,9 +47,6 @@ RUN mkdir -p /etc/apt/keyrings \
&& npm install -g npm@latest \ && npm install -g npm@latest \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Install Angular CLI Globally
RUN npm install -g @angular/cli
# Use a non-root user per https://code.visualstudio.com/remote/advancedcontainers/add-nonroot-user # Use a non-root user per https://code.visualstudio.com/remote/advancedcontainers/add-nonroot-user
ARG USERNAME=vscode ARG USERNAME=vscode
ARG USER_UID=1000 ARG USER_UID=1000

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

@ -1,123 +1,138 @@
from fastapi import Depends from fastapi import Depends
from ..database import db_session from ..database import db_session
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import and_, select from sqlalchemy import 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 ProgramNotAssignedException, ResourceNotFoundException from .exceptions import ProgramNotAssignedException, ResourceNotFoundException
class ResourceService: 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) -> list[Resource]: 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:
entities = ( entities = (
self._session.query(ResourceEntity) self._session.query(ResourceEntity)
.where(ResourceEntity.program == program) .where(ResourceEntity.program == program)
.all() .all()
) )
for entity in entities: for entity in entities:
resources.append(entity.to_model()) resources.append(entity.to_model())
return [resource for resource in resources] return [resource for resource in resources]
def get_resource_by_name(self, name: str, subject: User) -> Resource: def get_resource_by_name(self, name: str, subject: User) -> Resource:
"""Get a resource by name.""" """Get a resource by name."""
query = select(ResourceEntity).where( query = select(ResourceEntity).where(
and_( and_(
ResourceEntity.name == name, ResourceEntity.program.in_(subject.program) ResourceEntity.name == name, ResourceEntity.program.in_(subject.program)
) )
) )
entity = self._session.scalars(query).one_or_none() entity = self._session.scalars(query).one_or_none()
if entity is None: if entity is None:
raise ResourceNotFoundException( raise ResourceNotFoundException(
f"Resource with name: {name} does not exist or program has not been assigned." f"Resource with name: {name} does not exist or program has not been assigned."
) )
return entity.to_model() return entity.to_model()
def create(self, subject: User, resource: Resource) -> Resource: def create(self, subject: User, resource: Resource) -> Resource:
""" """
Creates a resource based on the input object and adds it to the table if the user has the right permissions. Creates a resource based on the input object and adds it to the table if the user has the right permissions.
Parameters:
Parameters: user: a valid User model representing the currently logged in User
user: a valid User model representing the currently logged in User resource: Resource object to add to table
resource: Resource object to add to table Returns:
Resource: Object added to table
Returns: """
Resource: Object added to table if subject.role != UserTypeEnum.ADMIN:
""" raise ProgramNotAssignedException(
if subject.role != UserTypeEnum.ADMIN: f"User is not {UserTypeEnum.ADMIN}, cannot update service"
raise ProgramNotAssignedException( )
f"User is not {UserTypeEnum.ADMIN}, cannot update service" resource_entity = ResourceEntity.from_model(resource)
) self._session.add(resource_entity)
resource_entity = ResourceEntity.from_model(resource) self._session.commit()
self._session.add(resource_entity) return resource_entity.to_model()
self._session.commit()
return resource_entity.to_model() def update(self, subject: User, resource: Resource) -> Resource:
"""
def update(self, subject: User, resource: Resource) -> Resource: Update the resource if the user has access
""" Parameters:
Update the resource if the user has access user: a valid User model representing the currently logged in User
resource (ResourceEntity): Resource to update
Parameters: Returns:
user: a valid User model representing the currently logged in User Resource: Updated resource object
resource (ResourceEntity): Resource to update Raises:
ResourceNotFoundException: If no resource is found with the corresponding ID
Returns: """
Resource: Updated resource object if subject.role != UserTypeEnum.ADMIN:
raise ProgramNotAssignedException(
Raises: f"User is not {UserTypeEnum.ADMIN}, cannot update service"
ResourceNotFoundException: If no resource is found with the corresponding ID )
""" query = select(ResourceEntity).where(ResourceEntity.id == resource.id)
if subject.role != UserTypeEnum.ADMIN: entity = self._session.scalars(query).one_or_none()
raise ProgramNotAssignedException( if entity is None:
f"User is not {UserTypeEnum.ADMIN}, cannot update service" raise ResourceNotFoundException(
) f"No resource found with matching id: {resource.id}"
query = select(ResourceEntity).where(ResourceEntity.id == resource.id) )
entity = self._session.scalars(query).one_or_none() entity.name = resource.name
if entity is None: entity.summary = resource.summary
raise ResourceNotFoundException( entity.link = resource.link
f"No resource found with matching id: {resource.id}" entity.program = resource.program
) self._session.commit()
entity.name = resource.name return entity.to_model()
entity.summary = resource.summary
entity.link = resource.link def delete(self, subject: User, resource: Resource) -> None:
entity.program = resource.program """
self._session.commit() Delete resource based on id that the user has access to
return entity.to_model() Parameters:
user: a valid User model representing the currently logged in User
def delete(self, subject: User, resource: Resource) -> None: id: int, a unique resource id
""" Raises:
Delete resource based on id that the user has access to ResourceNotFoundException: If no resource is found with the corresponding id
"""
Parameters: if subject.role != UserTypeEnum.ADMIN:
user: a valid User model representing the currently logged in User raise ProgramNotAssignedException(
id: int, a unique resource id f"User is not {UserTypeEnum.ADMIN}, cannot update service"
)
Raises: query = select(ResourceEntity).where(ResourceEntity.id == resource.id)
ResourceNotFoundException: If no resource is found with the corresponding id entity = self._session.scalars(query).one_or_none()
""" if entity is None:
if subject.role != UserTypeEnum.ADMIN: raise ResourceNotFoundException(
raise ProgramNotAssignedException( f"No resource found with matching id: {resource.id}"
f"User is not {UserTypeEnum.ADMIN}, cannot update service" )
) self._session.delete(entity)
query = select(ResourceEntity).where(ResourceEntity.id == resource.id) self._session.commit()
entity = self._session.scalars(query).one_or_none()
if entity is None: def get_by_slug(self, user: User, search_string: str) -> list[Resource]:
raise ResourceNotFoundException( """
f"No resource found with matching id: {resource.id}" Get a list of resources given a search string that the user has access to
) Parameters:
self._session.delete(entity) user: a valid User model representing the currently logged in User
self._session.commit() 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.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

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

View File

@ -6,6 +6,7 @@ from sqlalchemy.orm import Session
from ...services import UserService from ...services import UserService
from ...services import TagService from ...services import TagService
from ...services import ServiceService from ...services import ServiceService
from ...services import ResourceService
@ -23,4 +24,9 @@ def tag_svc(session: Session):
@pytest.fixture() @pytest.fixture()
def service_svc(session: Session): def service_svc(session: Session):
"""This fixture is used to test the ServiceService class""" """This fixture is used to test the ServiceService class"""
return ServiceService(session) return ServiceService(session)
@pytest.fixture()
def resource_svc(session: Session):
"""This fixture is used to test the ResourceService class"""
return ResourceService(session)

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), created_at=datetime(2023, 6, 5, 11, 30, 0),
) )
resource6 = Resource(
id=6,
name="Resource 6",
summary="New Financial Resource",
link="https://example.com/resource6",
program=ProgramTypeEnum.ECONOMIC,
created_at=datetime(2024, 6, 5, 11, 30, 0),
)
resource5_new = Resource(
id=5,
name="Resource 5",
summary = "Updated shelter and housing resources",
link="https://example.com/resource5/new",
program=ProgramTypeEnum.DOMESTIC,
created_at=datetime(2023, 6, 5, 11, 30, 0),
)
resources = [resource1, resource2, resource3, resource4, resource5] resources = [resource1, resource2, resource3, resource4, resource5]
resource_1 = Resource( resource_1 = Resource(
@ -266,13 +284,11 @@ def reset_table_id_seq(
next_id: int, next_id: int,
) -> None: ) -> None:
"""Reset the ID sequence of an entity table. """Reset the ID sequence of an entity table.
Args: Args:
session (Session) - A SQLAlchemy Session session (Session) - A SQLAlchemy Session
entity (DeclarativeBase) - The SQLAlchemy Entity table to target entity (DeclarativeBase) - The SQLAlchemy Entity table to target
entity_id_column (MappedColumn) - The ID column (should be an int column) entity_id_column (MappedColumn) - The ID column (should be an int column)
next_id (int) - Where the next inserted, autogenerated ID should begin next_id (int) - Where the next inserted, autogenerated ID should begin
Returns: Returns:
None""" None"""
table = entity.__table__ table = entity.__table__
@ -312,4 +328,4 @@ def insert_fake_data(session: Session):
reset_table_id_seq(session, ResourceEntity, ResourceEntity.id, len(resources) + 1) reset_table_id_seq(session, ResourceEntity, ResourceEntity.id, len(resources) + 1)
# Commit all changes # Commit all changes
session.commit() session.commit()

View File

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

View File

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

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import { PageLayout } from "@/components/PageLayout"; import { PageLayout } from "@/components/PageLayout";
import { ServiceTable } from "@/components/Table/ServiceIndex"; import ServiceTable from "@/components/Table/ServiceTable";
import Service from "@/utils/models/Service"; import Service from "@/utils/models/Service";
import { createClient } from "@/utils/supabase/client"; import { createClient } from "@/utils/supabase/client";
@ -9,7 +9,7 @@ import { ClipboardIcon } from "@heroicons/react/24/solid";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export default function Page() { export default function Page() {
const [services, setUsers] = useState<Service[]>([]); const [services, setServices] = useState<Service[]>([]);
useEffect(() => { useEffect(() => {
async function getServices() { async function getServices() {
@ -27,7 +27,7 @@ export default function Page() {
); );
const servicesAPI: Service[] = await serviceListData.json(); const servicesAPI: Service[] = await serviceListData.json();
setUsers(servicesAPI); setServices(servicesAPI);
} }
getServices(); getServices();
@ -37,7 +37,7 @@ export default function Page() {
<div className="min-h-screen flex flex-col"> <div className="min-h-screen flex flex-col">
{/* icon + title */} {/* icon + title */}
<PageLayout title="Services" icon={<ClipboardIcon />}> <PageLayout title="Services" icon={<ClipboardIcon />}>
<ServiceTable users={services} /> <ServiceTable data={services} setData={setServices} />
</PageLayout> </PageLayout>
</div> </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 React, { useState } from "react";
import { ChevronDoubleLeftIcon } from "@heroicons/react/24/solid"; import { ChevronDoubleLeftIcon } from "@heroicons/react/24/solid";
import { import {
@ -23,7 +23,7 @@ type DrawerProps = {
editableContent?: any; editableContent?: any;
onSave?: (content: any) => void; onSave?: (content: any) => void;
rowContent?: any; rowContent?: any;
onRowUpdate?: (content: any) => void; setData: Dispatch<SetStateAction<any>>;
}; };
interface EditContent { interface EditContent {
@ -37,13 +37,21 @@ const Drawer: FunctionComponent<DrawerProps> = ({
onSave, onSave,
editableContent, editableContent,
rowContent, rowContent,
onRowUpdate, 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) => {
setData((prevData: any) =>
prevData.map((row: any) =>
row.id === updatedRow.id ? updatedRow : row
)
);
};
const handleTempRowContentChange = (e) => { const handleTempRowContentChange = (e) => {
const { name, value } = e.target; const { name, value } = e.target;
console.log(name); console.log(name);

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 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 [pageContent, setPageContent] = useState("");
const handleDrawerContentChange = (newContent) => { const handleDrawerContentChange = (newContent: string) => {
setPageContent(newContent); 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"> <div className="font-semibold group flex flex-row items-center justify-between pr-2">
{title} {title}
<span> <span>
{/* Added OnRowUpdate to drawer */}
<Drawer <Drawer
title="My Drawer Title" title="My Drawer Title"
editableContent={pageContent} editableContent={pageContent}
rowContent={rowData} rowContent={rowData}
onSave={handleDrawerContentChange} onSave={handleDrawerContentChange}
onRowUpdate={onRowUpdate} setData={setData}
> >
{pageContent} {pageContent}
</Drawer> </Drawer>
</span> </span>
</div> </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 { import {
Cell,
ColumnDef,
Row, Row,
createColumnHelper, ColumnDef,
flexRender,
getCoreRowModel,
getFilteredRowModel,
sortingFns,
useReactTable, useReactTable,
getCoreRowModel,
flexRender,
createColumnHelper,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import { import {
ChangeEvent, ChangeEvent,
useState, useState,
useEffect, useEffect,
FunctionComponent,
useRef,
ChangeEventHandler,
Key, Key,
Dispatch,
SetStateAction,
} from "react"; } from "react";
import { RowOptionMenu } from "./RowOptionMenu";
import { RowOpenAction } from "./RowOpenAction";
import { TableAction } from "./TableAction"; import { TableAction } from "./TableAction";
import { import { PlusIcon } from "@heroicons/react/24/solid";
AtSymbolIcon,
Bars2Icon,
ArrowDownCircleIcon,
PlusIcon,
} from "@heroicons/react/24/solid";
import TagsInput from "../TagsInput/Index";
import { rankItem } from "@tanstack/match-sorter-utils"; 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 = ( const fuzzyFilter = (
row: Row<any>, row: Row<any>,
columnId: string, columnId: string,
@ -51,131 +43,69 @@ const fuzzyFilter = (
return itemRank.passed; return itemRank.passed;
}; };
// TODO: Rename everything to resources /**
export const ResourceTable = ({ users }: { users: Resource[] }) => { * General componenet that holds shared functionality for any data table component
const columnHelper = createColumnHelper<Resource>(); * @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(() => { useEffect(() => {
const sortedUsers = [...users].sort((a, b) => setData((prevData) => prevData.sort(visibilitySort));
a.visible === b.visible ? 0 : a.visible ? -1 : 1 }, [setData]);
);
setData(sortedUsers);
}, [users]);
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); console.log(data);
setData((currentData) => setData((currentData) =>
currentData.filter((user) => user.id !== userId) currentData.filter((data) => data.id !== dataId)
); );
}; };
const hideUser = (userId: number) => { const hideData = (dataId: number) => {
console.log(`Toggling visibility for user with ID: ${userId}`); console.log(`Toggling visibility for data with ID: ${dataId}`);
setData((currentData) => { setData((currentData) => {
const newData = currentData const newData = currentData
.map((user) => { .map((data) =>
if (user.id === userId) { data.id === dataId
return { ...user, visible: !user.visible }; ? { ...data, visible: !data.visible }
} : data
return user; )
}) .sort(visibilitySort);
.sort((a, b) =>
a.visible === b.visible ? 0 : a.visible ? -1 : 1
);
console.log(newData); console.log(newData);
return newData; return newData;
}); });
}; };
const [presetOptions, setPresetOptions] = useState([
"administrator",
"volunteer",
"employee",
]);
const [tagColors, setTagColors] = useState(new Map());
const getTagColor = (tag: string) => { const addData = () => {
if (!tagColors.has(tag)) { setData([...data]);
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 = [ // Add data manipulation options to the first column
columns.unshift(
columnHelper.display({ columnHelper.display({
id: "options", id: "options",
cell: (props) => ( cell: (props) => (
<RowOptionMenu <RowOptionMenu
onDelete={() => {}} onDelete={() => deleteData(props.row.original.id)}
onHide={() => hideUser(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 // Searching
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
@ -193,16 +123,7 @@ export const ResourceTable = ({ users }: { users: Resource[] }) => {
// TODO: Sorting // TODO: Sorting
// added this fn for editing rows // Define Tanstack table
const handleRowUpdate = (updatedRow: Resource) => {
const dataIndex = data.findIndex((row) => row.id === updatedRow.id);
if (dataIndex !== -1) {
const updatedData = [...data];
updatedData[dataIndex] = updatedRow;
setData(updatedData);
}
};
const table = useReactTable({ const table = useReactTable({
columns, columns,
data, data,
@ -261,9 +182,9 @@ export const ResourceTable = ({ users }: { users: Resource[] }) => {
<tbody> <tbody>
{table.getRowModel().rows.map((row) => { {table.getRowModel().rows.map((row) => {
// Individual 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 ${ 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 ( return (
<tr className={rowClassNames} key={row.id}> <tr className={rowClassNames} key={row.id}>
@ -289,7 +210,7 @@ export const ResourceTable = ({ users }: { users: Resource[] }) => {
<td <td
className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50" className="p-3 border-y border-gray-200 text-gray-600 hover:bg-gray-50"
colSpan={100} colSpan={100}
onClick={addUser} onClick={addData}
> >
<span className="flex ml-1 text-gray-500"> <span className="flex ml-1 text-gray-500">
<PlusIcon className="inline h-4 mr-1" /> <PlusIcon className="inline h-4 mr-1" />
@ -301,4 +222,4 @@ export const ResourceTable = ({ users }: { users: Resource[] }) => {
</table> </table>
</div> </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 "tailwindcss/tailwind.css";
import { TagsArray } from "./TagsArray"; import { TagsArray } from "./TagsArray";
import { TagDropdown } from "./TagDropdown"; import { TagDropdown } from "./TagDropdown";
@ -7,8 +7,8 @@ import { CreateNewTagAction } from "./CreateNewTagAction";
interface TagsInputProps { interface TagsInputProps {
presetOptions: string[]; presetOptions: string[];
presetValue: string | string[]; presetValue: string | string[];
setPresetOptions: () => {}; setPresetOptions: Dispatch<SetStateAction<string[]>>;
getTagColor: () => {}; getTagColor(tag: string): string;
} }
const TagsInput: React.FC<TagsInputProps> = ({ const TagsInput: React.FC<TagsInputProps> = ({

View File

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

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;