From cb54c9829d39874ba4aabefebd9ef15087f7ffc1 Mon Sep 17 00:00:00 2001 From: pmoharana-cmd <pmoharana032474@gmail.com> Date: Tue, 15 Oct 2024 19:28:32 -0400 Subject: [PATCH 1/3] Remove angular cli dependency --- .devcontainer/Dockerfile | 3 --- 1 file changed, 3 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index f27a6ec..40c5bb9 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -47,9 +47,6 @@ RUN mkdir -p /etc/apt/keyrings \ && npm install -g npm@latest \ && rm -rf /var/lib/apt/lists/* -# Install Angular CLI Globally -RUN npm install -g @angular/cli - # Use a non-root user per https://code.visualstudio.com/remote/advancedcontainers/add-nonroot-user ARG USERNAME=vscode ARG USER_UID=1000 From 2e0dd3b98707126f2770bc8ee65ce6266bacf276 Mon Sep 17 00:00:00 2001 From: Nicolas Asanov <98969136+naasanov@users.noreply.github.com> Date: Mon, 4 Nov 2024 15:10:13 -0500 Subject: [PATCH 2/3] 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 --- compass/app/admin/page.tsx | 4 +- compass/app/resource/page.tsx | 6 +- compass/app/service/page.tsx | 8 +- compass/components/Drawer/Drawer.tsx | 504 +++++++++-------- compass/components/Table/Index.tsx | 306 ---------- compass/components/Table/ResourceTable.tsx | 89 +++ compass/components/Table/RowOpenAction.tsx | 62 +- compass/components/Table/ServiceIndex.tsx | 312 ----------- compass/components/Table/ServiceTable.tsx | 108 ++++ .../Table/{ResourceIndex.tsx => Table.tsx} | 528 ++++++++---------- compass/components/Table/UserTable.tsx | 95 ++++ compass/components/TagsInput/Index.tsx | 6 +- compass/components/TagsInput/TagsArray.tsx | 2 +- compass/components/TagsInput/TagsHandler.tsx | 35 ++ compass/utils/models/DataPoint.ts | 9 + 15 files changed, 864 insertions(+), 1210 deletions(-) delete mode 100644 compass/components/Table/Index.tsx create mode 100644 compass/components/Table/ResourceTable.tsx delete mode 100644 compass/components/Table/ServiceIndex.tsx create mode 100644 compass/components/Table/ServiceTable.tsx rename compass/components/Table/{ResourceIndex.tsx => Table.tsx} (55%) create mode 100644 compass/components/Table/UserTable.tsx create mode 100644 compass/components/TagsInput/TagsHandler.tsx create mode 100644 compass/utils/models/DataPoint.ts diff --git a/compass/app/admin/page.tsx b/compass/app/admin/page.tsx index fede31d..abe2a8c 100644 --- a/compass/app/admin/page.tsx +++ b/compass/app/admin/page.tsx @@ -1,7 +1,7 @@ "use client"; import { PageLayout } from "@/components/PageLayout"; -import { Table } from "@/components/Table/Index"; +import UserTable from "@/components/Table/UserTable"; import User from "@/utils/models/User"; import { createClient } from "@/utils/supabase/client"; @@ -38,7 +38,7 @@ export default function Page() { <div className="min-h-screen flex flex-col"> {/* icon + title */} <PageLayout title="Users" icon={<UsersIcon />}> - <Table users={users} /> + <UserTable data={users} setData={setUsers} /> </PageLayout> </div> ); diff --git a/compass/app/resource/page.tsx b/compass/app/resource/page.tsx index fc4df14..68a620f 100644 --- a/compass/app/resource/page.tsx +++ b/compass/app/resource/page.tsx @@ -1,8 +1,8 @@ "use client"; import { PageLayout } from "@/components/PageLayout"; -import { ResourceTable } from "@/components/Table/ResourceIndex"; import Resource from "@/utils/models/Resource"; +import ResourceTable from "@/components/Table/ResourceTable"; import { createClient } from "@/utils/supabase/client"; import { BookmarkIcon } from "@heroicons/react/24/solid"; @@ -27,7 +27,7 @@ export default function Page() { ); const resourcesAPI: Resource[] = await userListData.json(); - + setResources(resourcesAPI); } @@ -38,7 +38,7 @@ export default function Page() { <div className="min-h-screen flex flex-col"> {/* icon + title */} <PageLayout title="Resources" icon={<BookmarkIcon />}> - <ResourceTable users={resources} /> + <ResourceTable data={resources} setData={setResources} /> </PageLayout> </div> ); diff --git a/compass/app/service/page.tsx b/compass/app/service/page.tsx index 8ebea4f..efe6337 100644 --- a/compass/app/service/page.tsx +++ b/compass/app/service/page.tsx @@ -1,7 +1,7 @@ "use client"; import { PageLayout } from "@/components/PageLayout"; -import { ServiceTable } from "@/components/Table/ServiceIndex"; +import ServiceTable from "@/components/Table/ServiceTable"; import Service from "@/utils/models/Service"; import { createClient } from "@/utils/supabase/client"; @@ -9,7 +9,7 @@ import { ClipboardIcon } from "@heroicons/react/24/solid"; import { useEffect, useState } from "react"; export default function Page() { - const [services, setUsers] = useState<Service[]>([]); + const [services, setServices] = useState<Service[]>([]); useEffect(() => { async function getServices() { @@ -27,7 +27,7 @@ export default function Page() { ); const servicesAPI: Service[] = await serviceListData.json(); - setUsers(servicesAPI); + setServices(servicesAPI); } getServices(); @@ -37,7 +37,7 @@ export default function Page() { <div className="min-h-screen flex flex-col"> {/* icon + title */} <PageLayout title="Services" icon={<ClipboardIcon />}> - <ServiceTable users={services} /> + <ServiceTable data={services} setData={setServices} /> </PageLayout> </div> ); diff --git a/compass/components/Drawer/Drawer.tsx b/compass/components/Drawer/Drawer.tsx index 6879f75..17b4557 100644 --- a/compass/components/Drawer/Drawer.tsx +++ b/compass/components/Drawer/Drawer.tsx @@ -1,247 +1,257 @@ -import { FunctionComponent, ReactNode } from "react"; -import React, { useState } from "react"; -import { ChevronDoubleLeftIcon } from "@heroicons/react/24/solid"; -import { - StarIcon as SolidStarIcon, - EnvelopeIcon, - UserIcon, -} from "@heroicons/react/24/solid"; -import { - ArrowsPointingOutIcon, - ArrowsPointingInIcon, - StarIcon as OutlineStarIcon, - ListBulletIcon, -} from "@heroicons/react/24/outline"; -import TagsInput from "../TagsInput/Index"; - -type DrawerProps = { - title: string; - children: ReactNode; - onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void; - type?: "button" | "submit" | "reset"; // specify possible values for type - disabled?: boolean; - editableContent?: any; - onSave?: (content: any) => void; - rowContent?: any; - onRowUpdate?: (content: any) => void; -}; - -interface EditContent { - content: string; - isEditing: boolean; -} - -const Drawer: FunctionComponent<DrawerProps> = ({ - title, - children, - onSave, - editableContent, - rowContent, - onRowUpdate, -}) => { - const [isOpen, setIsOpen] = useState(false); - const [isFull, setIsFull] = useState(false); - const [isFavorite, setIsFavorite] = useState(false); - const [tempRowContent, setTempRowContent] = useState(rowContent); - - const handleTempRowContentChange = (e) => { - const { name, value } = e.target; - console.log(name); - console.log(value); - setTempRowContent((prevContent) => ({ - ...prevContent, - [name]: value, - })); - }; - - const handleEnterPress = (e) => { - if (e.key === "Enter") { - e.preventDefault(); - // Update the rowContent with the temporaryRowContent - if (onRowUpdate) { - onRowUpdate(tempRowContent); - } - } - }; - - const toggleDrawer = () => { - setIsOpen(!isOpen); - if (isFull) { - setIsFull(!isFull); - } - }; - - const toggleDrawerFullScreen = () => setIsFull(!isFull); - - 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" - } ${isFull ? "w-full" : "w-1/2"}`; - - const iconComponent = isFull ? ( - <ArrowsPointingInIcon className="h-5 w-5" /> - ) : ( - <ArrowsPointingOutIcon className="h-5 w-5" /> - ); - - const favoriteIcon = isFavorite ? ( - <SolidStarIcon className="h-5 w-5" /> - ) : ( - <OutlineStarIcon className="h-5 w-5" /> - ); - - const [presetOptions, setPresetOptions] = useState([ - "administrator", - "volunteer", - "employee", - ]); - const [rolePresetOptions, setRolePresetOptions] = useState([ - "domestic", - "community", - "economic", - ]); - 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); - }; - - return ( - <div> - <button - 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} - > - Open - </button> - <div className={drawerClassName}></div> - <div className={drawerClassName}> - <div className="flex items-center justify-between p-4"> - <div className="flex flex-row items-center justify-between space-x-2"> - <span className="h-5 text-purple-200 w-5"> - <UserIcon /> - </span> - <h2 className="text-lg text-gray-800 font-semibold"> - {rowContent.username} - </h2> - </div> - <div> - <button - onClick={toggleFavorite} - className="py-2 text-gray-500 hover:text-gray-800 mr-2" - > - {favoriteIcon} - </button> - <button - onClick={toggleDrawerFullScreen} - className="py-2 text-gray-500 hover:text-gray-800 mr-2" - > - {iconComponent} - </button> - <button - onClick={toggleDrawer} - className="py-2 text-gray-500 hover:text-gray-800" - > - <ChevronDoubleLeftIcon className="h-5 w-5" /> - </button> - </div> - </div> - <div className="p-4"> - <table className="p-4"> - <tbody className="items-center"> - <tr className="w-full text-xs items-center flex flex-row justify-between"> - <div className="flex flex-row space-x-2 text-gray-500 items-center"> - <td> - <UserIcon className="h-4 w-4" /> - </td> - <td className="w-32">Username</td> - </div> - <td className="w-3/4 w-3/4 p-2 pl-0"> - <input - type="text" - name="username" - value={tempRowContent.username} - onChange={handleTempRowContentChange} - onKeyDown={handleEnterPress} - className="ml-2 w-full p-1 focus:outline-gray-200 hover:bg-gray-50" - /> - </td> - </tr> - <tr className="w-full text-xs items-center flex flex-row justify-between"> - <div className="flex flex-row space-x-2 text-gray-500 items-center"> - <td> - <ListBulletIcon className="h-4 w-4" /> - </td> - <td className="w-32">Role</td> - </div> - <td className="w-3/4 hover:bg-gray-50"> - <TagsInput - presetValue={tempRowContent.role} - presetOptions={presetOptions} - setPresetOptions={setPresetOptions} - getTagColor={getTagColor} - setTagColors={setTagColors} - /> - </td> - </tr> - <tr className="w-full text-xs items-center flex flex-row justify-between"> - <div className="flex flex-row space-x-2 text-gray-500 items-center"> - <td> - <EnvelopeIcon className="h-4 w-4" /> - </td> - <td className="w-32">Email</td> - </div> - <td className="w-3/4 p-2 pl-0"> - <input - type="text" - name="email" - value={tempRowContent.email} - onChange={handleTempRowContentChange} - 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> - <tr className="w-full text-xs items-center flex flex-row justify-between"> - <div className="flex flex-row space-x-2 text-gray-500 items-center"> - <td> - <ListBulletIcon className="h-4 w-4" /> - </td> - <td className="w-32">Type of Program</td> - </div> - <td className="w-3/4 hover:bg-gray-50"> - {/* {rowContent.program} */} - <TagsInput - presetValue={tempRowContent.program} - presetOptions={rolePresetOptions} - setPresetOptions={setRolePresetOptions} - getTagColor={getTagColor} - setTagColors={setTagColors} - /> - </td> - </tr> - </tbody> - </table> - <br /> - </div> - </div> - </div> - ); -}; - -export default Drawer; +import { Dispatch, FunctionComponent, ReactNode, SetStateAction } from "react"; +import React, { useState } from "react"; +import { ChevronDoubleLeftIcon } from "@heroicons/react/24/solid"; +import { + StarIcon as SolidStarIcon, + EnvelopeIcon, + UserIcon, +} from "@heroicons/react/24/solid"; +import { + ArrowsPointingOutIcon, + ArrowsPointingInIcon, + StarIcon as OutlineStarIcon, + ListBulletIcon, +} from "@heroicons/react/24/outline"; +import TagsInput from "../TagsInput/Index"; + +type DrawerProps = { + title: string; + children: ReactNode; + onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void; + type?: "button" | "submit" | "reset"; // specify possible values for type + disabled?: boolean; + editableContent?: any; + onSave?: (content: any) => void; + rowContent?: any; + setData: Dispatch<SetStateAction<any>>; +}; + +interface EditContent { + content: string; + isEditing: boolean; +} + +const Drawer: FunctionComponent<DrawerProps> = ({ + title, + children, + onSave, + editableContent, + rowContent, + setData, +}) => { + const [isOpen, setIsOpen] = useState(false); + const [isFull, setIsFull] = useState(false); + const [isFavorite, setIsFavorite] = useState(false); + const [tempRowContent, setTempRowContent] = useState(rowContent); + + const onRowUpdate = (updatedRow: any) => { + setData((prevData: any) => ( + prevData.map((row: any) => ( + row.id === updatedRow.id + ? updatedRow + : row + )) + )) + }; + + const handleTempRowContentChange = (e) => { + const { name, value } = e.target; + console.log(name); + console.log(value); + setTempRowContent((prevContent) => ({ + ...prevContent, + [name]: value, + })); + }; + + const handleEnterPress = (e) => { + if (e.key === "Enter") { + e.preventDefault(); + // Update the rowContent with the temporaryRowContent + if (onRowUpdate) { + onRowUpdate(tempRowContent); + } + } + }; + + const toggleDrawer = () => { + setIsOpen(!isOpen); + if (isFull) { + setIsFull(!isFull); + } + }; + + const toggleDrawerFullScreen = () => setIsFull(!isFull); + + 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" + } ${isFull ? "w-full" : "w-1/2"}`; + + const iconComponent = isFull ? ( + <ArrowsPointingInIcon className="h-5 w-5" /> + ) : ( + <ArrowsPointingOutIcon className="h-5 w-5" /> + ); + + const favoriteIcon = isFavorite ? ( + <SolidStarIcon className="h-5 w-5" /> + ) : ( + <OutlineStarIcon className="h-5 w-5" /> + ); + + const [presetOptions, setPresetOptions] = useState([ + "administrator", + "volunteer", + "employee", + ]); + const [rolePresetOptions, setRolePresetOptions] = useState([ + "domestic", + "community", + "economic", + ]); + 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); + }; + + return ( + <div> + <button + 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} + > + Open + </button> + <div className={drawerClassName}></div> + <div className={drawerClassName}> + <div className="flex items-center justify-between p-4"> + <div className="flex flex-row items-center justify-between space-x-2"> + <span className="h-5 text-purple-200 w-5"> + <UserIcon /> + </span> + <h2 className="text-lg text-gray-800 font-semibold"> + {rowContent.username} + </h2> + </div> + <div> + <button + onClick={toggleFavorite} + className="py-2 text-gray-500 hover:text-gray-800 mr-2" + > + {favoriteIcon} + </button> + <button + onClick={toggleDrawerFullScreen} + className="py-2 text-gray-500 hover:text-gray-800 mr-2" + > + {iconComponent} + </button> + <button + onClick={toggleDrawer} + className="py-2 text-gray-500 hover:text-gray-800" + > + <ChevronDoubleLeftIcon className="h-5 w-5" /> + </button> + </div> + </div> + <div className="p-4"> + <table className="p-4"> + <tbody className="items-center"> + <tr className="w-full text-xs items-center flex flex-row justify-between"> + <div className="flex flex-row space-x-2 text-gray-500 items-center"> + <td> + <UserIcon className="h-4 w-4" /> + </td> + <td className="w-32">Username</td> + </div> + <td className="w-3/4 w-3/4 p-2 pl-0"> + <input + type="text" + name="username" + value={tempRowContent.username} + onChange={handleTempRowContentChange} + onKeyDown={handleEnterPress} + className="ml-2 w-full p-1 focus:outline-gray-200 hover:bg-gray-50" + /> + </td> + </tr> + <tr className="w-full text-xs items-center flex flex-row justify-between"> + <div className="flex flex-row space-x-2 text-gray-500 items-center"> + <td> + <ListBulletIcon className="h-4 w-4" /> + </td> + <td className="w-32">Role</td> + </div> + <td className="w-3/4 hover:bg-gray-50"> + <TagsInput + presetValue={tempRowContent.role} + presetOptions={presetOptions} + setPresetOptions={setPresetOptions} + getTagColor={getTagColor} + setTagColors={setTagColors} + /> + </td> + </tr> + <tr className="w-full text-xs items-center flex flex-row justify-between"> + <div className="flex flex-row space-x-2 text-gray-500 items-center"> + <td> + <EnvelopeIcon className="h-4 w-4" /> + </td> + <td className="w-32">Email</td> + </div> + <td className="w-3/4 p-2 pl-0"> + <input + type="text" + name="email" + value={tempRowContent.email} + onChange={handleTempRowContentChange} + 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> + <tr className="w-full text-xs items-center flex flex-row justify-between"> + <div className="flex flex-row space-x-2 text-gray-500 items-center"> + <td> + <ListBulletIcon className="h-4 w-4" /> + </td> + <td className="w-32">Type of Program</td> + </div> + <td className="w-3/4 hover:bg-gray-50"> + {/* {rowContent.program} */} + <TagsInput + presetValue={tempRowContent.program} + presetOptions={rolePresetOptions} + setPresetOptions={setRolePresetOptions} + getTagColor={getTagColor} + setTagColors={setTagColors} + /> + </td> + </tr> + </tbody> + </table> + <br /> + </div> + </div> + </div> + ); +}; + +export default Drawer; diff --git a/compass/components/Table/Index.tsx b/compass/components/Table/Index.tsx deleted file mode 100644 index 931b039..0000000 --- a/compass/components/Table/Index.tsx +++ /dev/null @@ -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> - ); -}; diff --git a/compass/components/Table/ResourceTable.tsx b/compass/components/Table/ResourceTable.tsx new file mode 100644 index 0000000..a02162a --- /dev/null +++ b/compass/components/Table/ResourceTable.tsx @@ -0,0 +1,89 @@ +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}/> +} diff --git a/compass/components/Table/RowOpenAction.tsx b/compass/components/Table/RowOpenAction.tsx index 9a7103c..3678630 100644 --- a/compass/components/Table/RowOpenAction.tsx +++ b/compass/components/Table/RowOpenAction.tsx @@ -1,28 +1,34 @@ -import Drawer from "@/components/Drawer/Drawer"; -import { ChangeEvent, useState } from "react"; - -export const RowOpenAction = ({ title, rowData, onRowUpdate }) => { - const [pageContent, setPageContent] = useState(""); - - const handleDrawerContentChange = (newContent) => { - setPageContent(newContent); - }; - - return ( - <div className="font-semibold group flex flex-row items-center justify-between pr-2"> - {title} - <span> - {/* Added OnRowUpdate to drawer */} - <Drawer - title="My Drawer Title" - editableContent={pageContent} - rowContent={rowData} - onSave={handleDrawerContentChange} - onRowUpdate={onRowUpdate} - > - {pageContent} - </Drawer> - </span> - </div> - ); -}; +import Drawer from "@/components/Drawer/Drawer"; +import DataPoint from "@/utils/models/DataPoint"; +import { Dispatch, SetStateAction, useState } from "react"; + +type RowOpenActionProps<T extends DataPoint> = { + title: string, + rowData: T, + setData: Dispatch<SetStateAction<T[]>> +} + +export function RowOpenAction<T extends DataPoint>({ title, rowData, setData }: RowOpenActionProps<T>) { + const [pageContent, setPageContent] = useState(""); + + const handleDrawerContentChange = (newContent: string) => { + setPageContent(newContent); + }; + + return ( + <div className="font-semibold group flex flex-row items-center justify-between pr-2"> + {title} + <span> + <Drawer + title="My Drawer Title" + editableContent={pageContent} + rowContent={rowData} + onSave={handleDrawerContentChange} + setData={setData} + > + {pageContent} + </Drawer> + </span> + </div> + ); +}; diff --git a/compass/components/Table/ServiceIndex.tsx b/compass/components/Table/ServiceIndex.tsx deleted file mode 100644 index 6895984..0000000 --- a/compass/components/Table/ServiceIndex.tsx +++ /dev/null @@ -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> - ); -}; diff --git a/compass/components/Table/ServiceTable.tsx b/compass/components/Table/ServiceTable.tsx new file mode 100644 index 0000000..05e42b2 --- /dev/null +++ b/compass/components/Table/ServiceTable.tsx @@ -0,0 +1,108 @@ +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} /> +}; diff --git a/compass/components/Table/ResourceIndex.tsx b/compass/components/Table/Table.tsx similarity index 55% rename from compass/components/Table/ResourceIndex.tsx rename to compass/components/Table/Table.tsx index a714836..ef0a002 100644 --- a/compass/components/Table/ResourceIndex.tsx +++ b/compass/components/Table/Table.tsx @@ -1,304 +1,224 @@ -// 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 Resource from "@/utils/models/Resource"; - -// 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 resources -export const ResourceTable = ({ users }: { users: Resource[] }) => { - const columnHelper = createColumnHelper<Resource>(); - - 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("link", { - header: () => ( - <> - <Bars2Icon className="inline align-top h-4" /> Link - </> - ), - cell: (info) => ( - <a - href={info.getValue()} - target={"_blank"} - className="ml-2 text-gray-500 underline hover:text-gray-400" - > - {info.getValue()} - </a> - ), - }), - columnHelper.accessor("program", { - header: () => ( - <> - <Bars2Icon className="inline align-top h-4" /> Program - </> - ), - cell: (info) => <TagsInput presetValue={info.getValue()} />, - }), - - columnHelper.accessor("summary", { - header: () => ( - <> - <Bars2Icon className="inline align-top h-4" /> Summary - </> - ), - cell: (info) => ( - <span className="ml-2 text-gray-500">{info.getValue()}</span> - ), - }), - ]; - - const [data, setData] = useState<Resource[]>([...users]); - - const addUser = () => { - setData([...data]); - }; - - // Searching - const [query, setQuery] = useState(""); - 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: Resource) => { - 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> - ); -}; +import { + Row, + ColumnDef, + useReactTable, + getCoreRowModel, + flexRender, + createColumnHelper + } from "@tanstack/react-table"; +import { + ChangeEvent, + useState, + useEffect, + Key, + Dispatch, + SetStateAction +} from "react"; +import { TableAction } from "./TableAction"; +import { PlusIcon } from "@heroicons/react/24/solid"; +import { rankItem } from "@tanstack/match-sorter-utils"; +import { RowOptionMenu } from "./RowOptionMenu"; +import DataPoint from "@/utils/models/DataPoint"; + +type TableProps<T extends DataPoint> = { + data: T[], + setData: Dispatch<SetStateAction<T[]>>, + columns: ColumnDef<T, any>[] +}; + +/** Fuzzy search function */ +const fuzzyFilter = ( + row: Row<any>, + columnId: string, + 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; +}; + +/** + * General componenet that holds shared functionality for any data table component + * @param props.data Stateful list of data to be held in the table + * @param props.setData State setter for the list of data + * @param props.columns Column definitions made with Tanstack columnHelper +*/ +export default function Table<T extends DataPoint>({ data, setData, columns }: TableProps<T>) { + const columnHelper = createColumnHelper<T>(); + + /** Sorting function based on visibility */ + const visibilitySort = (a: T, b: T) => ( + a.visible === b.visible + ? 0 + : a.visible ? -1 : 1 + ) + + // Sort data on load + useEffect(() => { + setData(prevData => prevData.sort(visibilitySort)) + }, [setData]); + + // Data manipulation methods + // TODO: Connect data manipulation methods to the database (deleteData, hideData, addData) + const deleteData = (dataId: number) => { + console.log(data); + setData((currentData) => + currentData.filter((data) => data.id !== dataId) + ); + }; + + const hideData = (dataId: number) => { + console.log(`Toggling visibility for data with ID: ${dataId}`); + setData(currentData => { + const newData = currentData + .map(data => ( + data.id === dataId + ? { ...data, visible: !data.visible } + : data + )) + .sort(visibilitySort); + + console.log(newData); + return newData; + }); + }; + + const addData = () => { + setData([...data]); + }; + + // Add data manipulation options to the first column + columns.unshift( + columnHelper.display({ + id: "options", + cell: (props) => ( + <RowOptionMenu + onDelete={() => deleteData(props.row.original.id)} + onHide={() => hideData(props.row.original.id)} + /> + ), + }) + ) + + // 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 + + // Define Tanstack table + 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 isDataVisible = row.original.visible; + 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}> + {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={addData} + > + <span className="flex ml-1 text-gray-500"> + <PlusIcon className="inline h-4 mr-1" /> + New + </span> + </td> + </tr> + </tfoot> + </table> + </div> + ); +}; diff --git a/compass/components/Table/UserTable.tsx b/compass/components/Table/UserTable.tsx new file mode 100644 index 0000000..0f81d66 --- /dev/null +++ b/compass/components/Table/UserTable.tsx @@ -0,0 +1,95 @@ +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}/> +} diff --git a/compass/components/TagsInput/Index.tsx b/compass/components/TagsInput/Index.tsx index 19c2a77..f4b021e 100644 --- a/compass/components/TagsInput/Index.tsx +++ b/compass/components/TagsInput/Index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef } from "react"; +import React, { useState, useRef, Dispatch, SetStateAction } from "react"; import "tailwindcss/tailwind.css"; import { TagsArray } from "./TagsArray"; import { TagDropdown } from "./TagDropdown"; @@ -7,8 +7,8 @@ import { CreateNewTagAction } from "./CreateNewTagAction"; interface TagsInputProps { presetOptions: string[]; presetValue: string | string[]; - setPresetOptions: () => {}; - getTagColor: () => {}; + setPresetOptions: Dispatch<SetStateAction<string[]>>; + getTagColor(tag: string): string; } const TagsInput: React.FC<TagsInputProps> = ({ diff --git a/compass/components/TagsInput/TagsArray.tsx b/compass/components/TagsInput/TagsArray.tsx index c014e7c..845e739 100644 --- a/compass/components/TagsInput/TagsArray.tsx +++ b/compass/components/TagsInput/TagsArray.tsx @@ -7,7 +7,7 @@ export interface Tags { } export const TagsArray = ({ tags, handleDelete, active = false }: Tags) => { - console.log(tags); + // console.log(tags); return ( <div className="flex ml-2 flex-wrap gap-2 items-center"> diff --git a/compass/components/TagsInput/TagsHandler.tsx b/compass/components/TagsInput/TagsHandler.tsx new file mode 100644 index 0000000..e8fde75 --- /dev/null +++ b/compass/components/TagsInput/TagsHandler.tsx @@ -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 } +} \ No newline at end of file diff --git a/compass/utils/models/DataPoint.ts b/compass/utils/models/DataPoint.ts new file mode 100644 index 0000000..b7dcacb --- /dev/null +++ b/compass/utils/models/DataPoint.ts @@ -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; \ No newline at end of file From 99e43c7b309d36e64086b6bd33100c0854ff30e8 Mon Sep 17 00:00:00 2001 From: Emma Foster <111466810+emmalynfoster@users.noreply.github.com> Date: Mon, 4 Nov 2024 15:10:58 -0500 Subject: [PATCH 3/3] 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 --- backend/services/resource.py | 45 ++++--- backend/test/conftest.py | 3 +- backend/test/services/fixtures.py | 8 +- backend/test/services/resource_test.py | 126 ++++++++++++++++++++ backend/test/services/resource_test_data.py | 22 +++- 5 files changed, 174 insertions(+), 30 deletions(-) create mode 100644 backend/test/services/resource_test.py diff --git a/backend/services/resource.py b/backend/services/resource.py index 2648ad6..67959c7 100644 --- a/backend/services/resource.py +++ b/backend/services/resource.py @@ -35,17 +35,15 @@ class ResourceService: def create(self, user: User, resource: Resource) -> Resource: """ Creates a resource based on the input object and adds it to the table if the user has the right permissions. - Parameters: user: a valid User model representing the currently logged in User resource: Resource object to add to table - Returns: Resource: Object added to table """ - if resource.role != user.role or resource.group != user.group: + if user.role != UserTypeEnum.ADMIN: raise PermissionError( - "User does not have permission to add resources in this role or group." + "User does not have permission to add resources in this program." ) resource_entity = ResourceEntity.from_model(resource) @@ -57,14 +55,11 @@ class ResourceService: def get_by_id(self, user: User, id: int) -> Resource: """ Gets a resource based on the resource id that the user has access to - Parameters: user: a valid User model representing the currently logged in User id: int, the id of the resource - Returns: Resource - Raises: ResourceNotFoundException: If no resource is found with id """ @@ -72,8 +67,7 @@ class ResourceService: self._session.query(ResourceEntity) .filter( ResourceEntity.id == id, - ResourceEntity.role == user.role, - ResourceEntity.group == user.group, + ResourceEntity.program.in_(user.program), ) .one_or_none() ) @@ -86,18 +80,15 @@ class ResourceService: def update(self, user: User, resource: ResourceEntity) -> Resource: """ Update the resource if the user has access - Parameters: user: a valid User model representing the currently logged in User resource (ResourceEntity): Resource to update - Returns: Resource: Updated resource object - Raises: ResourceNotFoundException: If no resource is found with the corresponding ID """ - if resource.role != user.role or resource.group != user.group: + if user.role != UserTypeEnum.ADMIN: raise PermissionError( "User does not have permission to update this resource." ) @@ -109,7 +100,11 @@ class ResourceService: f"No resource found with matching id: {resource.id}" ) - obj.update_from_model(resource) # Assuming an update method exists + obj.name = resource.name + obj.summary = resource.summary + obj.link = resource.link + obj.program = resource.program + self._session.commit() return obj.to_model() @@ -117,20 +112,21 @@ class ResourceService: def delete(self, user: User, id: int) -> None: """ Delete resource based on id that the user has access to - Parameters: user: a valid User model representing the currently logged in User id: int, a unique resource id - Raises: ResourceNotFoundException: If no resource is found with the corresponding id """ + if user.role != UserTypeEnum.ADMIN: + raise PermissionError( + "User does not have permission to delete this resource." + ) + resource = ( self._session.query(ResourceEntity) .filter( ResourceEntity.id == id, - ResourceEntity.role == user.role, - ResourceEntity.group == user.group, ) .one_or_none() ) @@ -144,22 +140,21 @@ class ResourceService: def get_by_slug(self, user: User, search_string: str) -> list[Resource]: """ Get a list of resources given a search string that the user has access to - Parameters: user: a valid User model representing the currently logged in User search_string: a string to search resources by - Returns: list[Resource]: list of resources relating to the string - Raises: ResourceNotFoundException if no resource is found with the corresponding slug """ query = select(ResourceEntity).where( - ResourceEntity.title.ilike(f"%{search_string}%"), - ResourceEntity.role == user.role, - ResourceEntity.group == user.group, + ResourceEntity.name.ilike(f"%{search_string}%"), + ResourceEntity.program.in_(user.program) ) entities = self._session.scalars(query).all() - return [entity.to_model() for entity in entities] + if not entities: + return [] + + return [entity.to_model() for entity in entities] \ No newline at end of file diff --git a/backend/test/conftest.py b/backend/test/conftest.py index b91a2ac..f231859 100644 --- a/backend/test/conftest.py +++ b/backend/test/conftest.py @@ -4,7 +4,7 @@ import pytest from sqlalchemy import Engine, create_engine, text from sqlalchemy.orm import Session from sqlalchemy.exc import OperationalError -from .services import user_test_data, tag_test_data, service_test_data +from .services import user_test_data, tag_test_data, service_test_data, resource_test_data from ..database import _engine_str from ..env import getenv @@ -57,5 +57,6 @@ def setup_insert_data_fixture(session: Session): user_test_data.insert_fake_data(session) tag_test_data.insert_fake_data(session) service_test_data.insert_fake_data(session) + resource_test_data.insert_fake_data(session) session.commit() yield diff --git a/backend/test/services/fixtures.py b/backend/test/services/fixtures.py index 9fb349a..213f1bf 100644 --- a/backend/test/services/fixtures.py +++ b/backend/test/services/fixtures.py @@ -6,6 +6,7 @@ from sqlalchemy.orm import Session from ...services import UserService from ...services import TagService from ...services import ServiceService +from ...services import ResourceService @@ -23,4 +24,9 @@ def tag_svc(session: Session): @pytest.fixture() def service_svc(session: Session): """This fixture is used to test the ServiceService class""" - return ServiceService(session) \ No newline at end of file + return ServiceService(session) + +@pytest.fixture() +def resource_svc(session: Session): + """This fixture is used to test the ResourceService class""" + return ResourceService(session) \ No newline at end of file diff --git a/backend/test/services/resource_test.py b/backend/test/services/resource_test.py new file mode 100644 index 0000000..7d9b007 --- /dev/null +++ b/backend/test/services/resource_test.py @@ -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 == [] \ No newline at end of file diff --git a/backend/test/services/resource_test_data.py b/backend/test/services/resource_test_data.py index bb39266..7634df7 100644 --- a/backend/test/services/resource_test_data.py +++ b/backend/test/services/resource_test_data.py @@ -50,6 +50,24 @@ resource5 = Resource( created_at=datetime(2023, 6, 5, 11, 30, 0), ) +resource6 = Resource( + id=6, + name="Resource 6", + summary="New Financial Resource", + link="https://example.com/resource6", + program=ProgramTypeEnum.ECONOMIC, + created_at=datetime(2024, 6, 5, 11, 30, 0), +) + +resource5_new = Resource( + id=5, + name="Resource 5", + summary = "Updated shelter and housing resources", + link="https://example.com/resource5/new", + program=ProgramTypeEnum.DOMESTIC, + created_at=datetime(2023, 6, 5, 11, 30, 0), +) + resources = [resource1, resource2, resource3, resource4, resource5] resource_1 = Resource( @@ -266,13 +284,11 @@ def reset_table_id_seq( next_id: int, ) -> None: """Reset the ID sequence of an entity table. - Args: session (Session) - A SQLAlchemy Session entity (DeclarativeBase) - The SQLAlchemy Entity table to target entity_id_column (MappedColumn) - The ID column (should be an int column) next_id (int) - Where the next inserted, autogenerated ID should begin - Returns: None""" table = entity.__table__ @@ -312,4 +328,4 @@ def insert_fake_data(session: Session): reset_table_id_seq(session, ResourceEntity, ResourceEntity.id, len(resources) + 1) # Commit all changes - session.commit() + session.commit() \ No newline at end of file