From 013509553495e56f502c6b2c489e4e903deb22d7 Mon Sep 17 00:00:00 2001 From: StreetLamb Date: Wed, 24 Apr 2024 00:05:52 +0800 Subject: [PATCH 1/5] backend: Add new skills to db on db_init, allow adding of skills when updating member. Add wikipedia tool --- backend/app/api/routes/members.py | 10 ++++++- backend/app/core/db.py | 15 +++++++++- backend/app/core/graph/members.py | 6 ++-- backend/app/core/graph/skills.py | 29 +++++++++++++++++++ backend/app/core/graph/tools.py | 11 ------- backend/app/models.py | 2 ++ backend/poetry.lock | 48 ++++++++++++++++++++++++++++++- backend/pyproject.toml | 1 + 8 files changed, 105 insertions(+), 17 deletions(-) create mode 100644 backend/app/core/graph/skills.py delete mode 100644 backend/app/core/graph/tools.py diff --git a/backend/app/api/routes/members.py b/backend/app/api/routes/members.py index ee7f5061..ebc2a4d3 100644 --- a/backend/app/api/routes/members.py +++ b/backend/app/api/routes/members.py @@ -1,7 +1,7 @@ from typing import Any from fastapi import APIRouter, Depends, HTTPException -from sqlmodel import func, select +from sqlmodel import col, func, select from app.api.deps import CurrentUser, SessionDep from app.models import ( @@ -11,6 +11,7 @@ MembersOut, MemberUpdate, Message, + Skill, Team, ) @@ -163,6 +164,13 @@ def update_member( if not member: raise HTTPException(status_code=404, detail="Member not found") + # update member's skills if required + if member_in.skills is not None: + skills = session.exec( + select(Skill).where(col(Skill.id).in_(member_in.skills)) + ).all() + member.skills = skills + update_dict = member_in.model_dump(exclude_unset=True) member.sqlmodel_update(update_dict) session.add(member) diff --git a/backend/app/core/db.py b/backend/app/core/db.py index 782b79d8..ced5accc 100644 --- a/backend/app/core/db.py +++ b/backend/app/core/db.py @@ -2,7 +2,8 @@ from app import crud from app.core.config import settings -from app.models import User, UserCreate +from app.core.graph.skills import all_skills +from app.models import Skill, User, UserCreate engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI)) @@ -32,3 +33,15 @@ def init_db(session: Session) -> None: is_superuser=True, ) user = crud.create_user(session=session, user_create=user_in) + + # Add skills from skills.py to skill table if they don't exist yet + existing_skills = session.exec(select(Skill)).all() + new_skills = [] + for skill in all_skills: + if skill not in existing_skills: + new_skills.append( + Skill(name=skill, description=all_skills[skill].description) + ) + with Session(engine) as session: + session.add_all(new_skills) + session.commit() diff --git a/backend/app/core/graph/members.py b/backend/app/core/graph/members.py index 99b3e02b..2776bf82 100644 --- a/backend/app/core/graph/members.py +++ b/backend/app/core/graph/members.py @@ -9,7 +9,7 @@ from langchain_openai import ChatOpenAI from pydantic import BaseModel, Field -from app.core.graph.tools import all_tools +from app.core.graph.skills import all_skills class Person(BaseModel): @@ -95,10 +95,10 @@ def create_agent( self, llm: ChatOpenAI, prompt: ChatPromptTemplate, tools: list[str] ): """Create the agent executor""" - tools = [all_tools[tool] for tool in tools] + tools = [all_skills[tool].tool for tool in tools] # Tools cannot be empty, add a placeholder if len(tools) < 1: - tools = [all_tools["nothing"]] + tools = [all_skills["nothing"].tool] agent = create_openai_functions_agent(llm, tools, prompt) executor = AgentExecutor(agent=agent, tools=tools) return executor diff --git a/backend/app/core/graph/skills.py b/backend/app/core/graph/skills.py new file mode 100644 index 00000000..9954dd58 --- /dev/null +++ b/backend/app/core/graph/skills.py @@ -0,0 +1,29 @@ +from collections.abc import Callable + +from langchain_community.tools import DuckDuckGoSearchRun, WikipediaQueryRun +from langchain_community.utilities import WikipediaAPIWrapper +from langchain_core.tools import tool +from pydantic import BaseModel + + +class SkillInfo(BaseModel): + description: str + tool: Callable + + +@tool +def nothing(query: str) -> str: + """Placeholder Tool. Does nothing""" + return "" + + +all_skills: dict[str, SkillInfo] = { + "nothing": SkillInfo(description="Does nothing", tool=nothing), + "search": SkillInfo( + description="Searches the web using Duck Duck Go", tool=DuckDuckGoSearchRun() + ), + "wikipedia": SkillInfo( + description="Searches Wikipedia", + tool=WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper()), + ), +} diff --git a/backend/app/core/graph/tools.py b/backend/app/core/graph/tools.py deleted file mode 100644 index 5b6f5561..00000000 --- a/backend/app/core/graph/tools.py +++ /dev/null @@ -1,11 +0,0 @@ -from langchain_community.tools import DuckDuckGoSearchRun -from langchain_core.tools import tool - - -@tool -def nothing(query: str) -> str: - """Placeholder Tool. Does nothing""" - return "" - - -all_tools = {"nothing": nothing, "search": DuckDuckGoSearchRun()} diff --git a/backend/app/models.py b/backend/app/models.py index d13085ef..bf68bd84 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -205,6 +205,7 @@ class MemberUpdate(MemberBase): belongs_to: int | None = None position_x: float | None = None position_y: float | None = None + skills: list[int] | None = None class Member(MemberBase, table=True): @@ -223,6 +224,7 @@ class MemberOut(MemberBase): id: int belongs_to: int owner_of: int | None + skills: list["Skill"] class MembersOut(SQLModel): diff --git a/backend/poetry.lock b/backend/poetry.lock index 0129395e..5109cf0e 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -226,6 +226,27 @@ files = [ tests = ["pytest (>=3.2.1,!=3.3.0)"] typecheck = ["mypy"] +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + [[package]] name = "cachetools" version = "5.3.3" @@ -2696,6 +2717,17 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[[package]] +name = "soupsieve" +version = "2.5" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, + {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, +] + [[package]] name = "sqlalchemy" version = "2.0.29" @@ -3277,6 +3309,20 @@ files = [ {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, ] +[[package]] +name = "wikipedia" +version = "1.4.0" +description = "Wikipedia API for Python" +optional = false +python-versions = "*" +files = [ + {file = "wikipedia-1.4.0.tar.gz", hash = "sha256:db0fad1829fdd441b1852306e9856398204dc0786d2996dd2e0c8bb8e26133b2"}, +] + +[package.dependencies] +beautifulsoup4 = "*" +requests = ">=2.0.0,<3.0.0" + [[package]] name = "yarl" version = "1.9.4" @@ -3383,4 +3429,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "641c1d31ea48c3efe4697426f2e3561753db87ad94a18fd0115e7b0e12f8f202" +content-hash = "6c8d50358efbb21616f777546fe184b1212ec45c7711eadbfbc46f1d4bdfd089" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 57bd82eb..054780d0 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -33,6 +33,7 @@ grandalf = "^0.8" langchain = "^0.1.16" langchain-community = "^0.0.34" duckduckgo-search = "^5.3.0" +wikipedia = "^1.4.0" [tool.poetry.group.dev.dependencies] pytest = "^7.4.3" From c72bbf92d5e16d6668d5483b9e1d193d40ad2f08 Mon Sep 17 00:00:00 2001 From: StreetLamb Date: Thu, 25 Apr 2024 08:16:10 +0800 Subject: [PATCH 2/5] backend: Add skill endpoints --- backend/app/api/routes/skills.py | 38 ++++++++++++++++++++++++++++++++ backend/app/models.py | 19 ++++++++-------- 2 files changed, 48 insertions(+), 9 deletions(-) create mode 100644 backend/app/api/routes/skills.py diff --git a/backend/app/api/routes/skills.py b/backend/app/api/routes/skills.py new file mode 100644 index 00000000..88f3e254 --- /dev/null +++ b/backend/app/api/routes/skills.py @@ -0,0 +1,38 @@ +from typing import Any + +from fastapi import APIRouter, HTTPException +from sqlmodel import func, select + +from app.api.deps import SessionDep +from app.models import ( + Skill, + SkillOut, + SkillsOut, +) + +router = APIRouter() + + +@router.get("/", response_model=SkillsOut) +def read_skills(session: SessionDep, skip: int = 0, limit: int = 100) -> Any: + """ + Retrieve skills. + """ + count_statement = select(func.count()).select_from(Skill) + count = session.exec(count_statement).one() + + statement = select(Skill).offset(skip).limit(limit) + items = session.exec(statement).all() + + return SkillsOut(data=items, count=count) + + +@router.get("/{id}", response_model=SkillOut) +def read_skill(session: SessionDep, id: int) -> Any: + """ + Get skill by ID. + """ + skill = session.get(Skill, id) + if not skill: + raise HTTPException(status_code=404, detail="Skill not found") + return skill diff --git a/backend/app/models.py b/backend/app/models.py index bf68bd84..ae1a8f2b 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -240,17 +240,18 @@ class SkillBase(SQLModel): description: str | None = None -class SkillCreate(SkillBase): - name: str - - -class SkillUpdate(SkillBase): - name: str | None = None - description: str | None = None - - class Skill(SkillBase, table=True): id: int | None = Field(default=None, primary_key=True) members: list["Member"] = Relationship( back_populates="skills", link_model=MemberSkillsLink ) + + +class SkillsOut(SQLModel): + data: list[Skill] + count: int + + +class SkillOut(SkillBase): + id: int + description: str | None From ded8b62dc4c765f6da1235cb03b2a256889e9a88 Mon Sep 17 00:00:00 2001 From: StreetLamb Date: Thu, 25 Apr 2024 23:42:58 +0800 Subject: [PATCH 3/5] Allow updating of member's skills in frontend and populating skills to db table --- backend/app/api/main.py | 3 +- backend/app/api/routes/members.py | 30 ++++-- backend/app/api/routes/skills.py | 4 +- backend/app/api/routes/teams.py | 18 +++- backend/app/core/db.py | 24 +++-- backend/app/models.py | 2 +- frontend/src/client/index.ts | 7 ++ frontend/src/client/models/MemberOut.ts | 3 + frontend/src/client/models/MemberUpdate.ts | 3 + frontend/src/client/models/Skill.ts | 11 +++ frontend/src/client/models/SkillOut.ts | 11 +++ frontend/src/client/models/SkillsOut.ts | 12 +++ frontend/src/client/schemas/$MemberCreate.ts | 1 + frontend/src/client/schemas/$MemberOut.ts | 8 ++ frontend/src/client/schemas/$MemberUpdate.ts | 12 +++ frontend/src/client/schemas/$Skill.ts | 28 ++++++ frontend/src/client/schemas/$SkillOut.ts | 25 +++++ frontend/src/client/schemas/$SkillsOut.ts | 19 ++++ frontend/src/client/schemas/$TeamCreate.ts | 1 + frontend/src/client/schemas/$TeamOut.ts | 1 + frontend/src/client/schemas/$TeamUpdate.ts | 1 + frontend/src/client/services/SkillsService.ts | 63 ++++++++++++ .../src/components/Members/EditMember.tsx | 98 ++++++++++++++----- frontend/src/components/ReactFlow/Flow.tsx | 3 +- 24 files changed, 334 insertions(+), 54 deletions(-) create mode 100644 frontend/src/client/models/Skill.ts create mode 100644 frontend/src/client/models/SkillOut.ts create mode 100644 frontend/src/client/models/SkillsOut.ts create mode 100644 frontend/src/client/schemas/$Skill.ts create mode 100644 frontend/src/client/schemas/$SkillOut.ts create mode 100644 frontend/src/client/schemas/$SkillsOut.ts create mode 100644 frontend/src/client/services/SkillsService.ts diff --git a/backend/app/api/main.py b/backend/app/api/main.py index 3ae1778d..d7fd5d45 100644 --- a/backend/app/api/main.py +++ b/backend/app/api/main.py @@ -1,6 +1,6 @@ from fastapi import APIRouter -from app.api.routes import items, login, members, teams, users, utils +from app.api.routes import items, login, members, skills, teams, users, utils api_router = APIRouter() api_router.include_router(login.router, tags=["login"]) @@ -11,3 +11,4 @@ api_router.include_router( members.router, prefix="/teams/{team_id}/members", tags=["members"] ) +api_router.include_router(skills.router, prefix="/skills", tags=["skills"]) diff --git a/backend/app/api/routes/members.py b/backend/app/api/routes/members.py index ebc2a4d3..09436597 100644 --- a/backend/app/api/routes/members.py +++ b/backend/app/api/routes/members.py @@ -18,12 +18,25 @@ router = APIRouter() -def validate_unique_name_in_team( - session: SessionDep, team_id: int, id: int, member_in: MemberCreate | MemberUpdate +def check_duplicate_names_on_create( + session: SessionDep, team_id: int, member_in: MemberCreate +): + """Check if (name, team_id) is unique""" + statement = select(Member).where( + Member.name == member_in.name, + Member.belongs_to == team_id, + ) + member_unique = session.exec(statement).first() + if member_unique: + raise HTTPException( + status_code=400, detail="Member with this name already exists" + ) + + +def check_duplicate_names_on_update( + session: SessionDep, team_id: int, member_in: MemberUpdate, id: int ): """Check if (name, team_id) is unique""" - if member_in.name is None: - return statement = select(Member).where( Member.name == member_in.name, Member.belongs_to == team_id, @@ -113,7 +126,7 @@ def create_member( current_user: CurrentUser, team_id: int, member_in: MemberCreate, - _: bool = Depends(validate_unique_name_in_team), + _: bool = Depends(check_duplicate_names_on_create), ) -> Any: """ Create new member. @@ -137,7 +150,7 @@ def update_member( team_id: int, id: int, member_in: MemberUpdate, - _: bool = Depends(validate_unique_name_in_team), + _: bool = Depends(check_duplicate_names_on_update), ) -> Any: """ Update a member. @@ -166,9 +179,8 @@ def update_member( # update member's skills if required if member_in.skills is not None: - skills = session.exec( - select(Skill).where(col(Skill.id).in_(member_in.skills)) - ).all() + skill_ids = [skill.id for skill in member_in.skills] + skills = session.exec(select(Skill).where(col(Skill.id).in_(skill_ids))).all() member.skills = skills update_dict = member_in.model_dump(exclude_unset=True) diff --git a/backend/app/api/routes/skills.py b/backend/app/api/routes/skills.py index 88f3e254..37c24067 100644 --- a/backend/app/api/routes/skills.py +++ b/backend/app/api/routes/skills.py @@ -22,9 +22,9 @@ def read_skills(session: SessionDep, skip: int = 0, limit: int = 100) -> Any: count = session.exec(count_statement).one() statement = select(Skill).offset(skip).limit(limit) - items = session.exec(statement).all() + skills = session.exec(statement).all() - return SkillsOut(data=items, count=count) + return SkillsOut(data=skills, count=count) @router.get("/{id}", response_model=SkillOut) diff --git a/backend/app/api/routes/teams.py b/backend/app/api/routes/teams.py index 234cdc49..1bc2d69f 100644 --- a/backend/app/api/routes/teams.py +++ b/backend/app/api/routes/teams.py @@ -62,16 +62,24 @@ router = APIRouter() -async def validate_unique_name(session: SessionDep, team_in: TeamCreate | TeamUpdate): +async def check_duplicate_name_on_create(session: SessionDep, team_in: TeamCreate): """Validate that team name is unique""" - if team_in.name is None: - return statement = select(Team).where(Team.name == team_in.name) team = session.exec(statement).first() if team: raise HTTPException(status_code=400, detail="Team name already exists") +async def check_duplicate_name_on_update( + session: SessionDep, team_in: TeamUpdate, id: int +): + """Validate that team name is unique""" + statement = select(Team).where(Team.name == team_in.name, Team.id != id) + team = session.exec(statement).first() + if team: + raise HTTPException(status_code=400, detail="Team name already exists") + + @router.get("/", response_model=TeamsOut) def read_teams( session: SessionDep, current_user: CurrentUser, skip: int = 0, limit: int = 100 @@ -121,7 +129,7 @@ def create_team( session: SessionDep, current_user: CurrentUser, team_in: TeamCreate, - _: bool = Depends(validate_unique_name), + _: bool = Depends(check_duplicate_name_on_create), ) -> Any: """ Create new team and it's team leader @@ -155,7 +163,7 @@ def update_team( current_user: CurrentUser, id: int, team_in: TeamUpdate, - _: bool = Depends(validate_unique_name), + _: bool = Depends(check_duplicate_name_on_update), ) -> Any: """ Update a team. diff --git a/backend/app/core/db.py b/backend/app/core/db.py index ced5accc..e512cc5e 100644 --- a/backend/app/core/db.py +++ b/backend/app/core/db.py @@ -34,14 +34,18 @@ def init_db(session: Session) -> None: ) user = crud.create_user(session=session, user_create=user_in) - # Add skills from skills.py to skill table if they don't exist yet existing_skills = session.exec(select(Skill)).all() - new_skills = [] - for skill in all_skills: - if skill not in existing_skills: - new_skills.append( - Skill(name=skill, description=all_skills[skill].description) - ) - with Session(engine) as session: - session.add_all(new_skills) - session.commit() + existing_skills_dict = {skill.name: skill for skill in existing_skills} + + for skill_name, skill_info in all_skills.items(): + if skill_name in existing_skills_dict: + existing_skill = existing_skills_dict[skill_name] + if existing_skill.description != skill_info.description: + # Update the existing skill's description + existing_skill.description = skill_info.description + session.add(existing_skill) # Mark the modified object for saving + else: + new_skill = Skill(name=skill_name, description=skill_info.description) + session.add(new_skill) # Prepare new skill for addition to the database + + session.commit() diff --git a/backend/app/models.py b/backend/app/models.py index ae1a8f2b..3944dfb4 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -205,7 +205,7 @@ class MemberUpdate(MemberBase): belongs_to: int | None = None position_x: float | None = None position_y: float | None = None - skills: list[int] | None = None + skills: list["Skill"] | None = None class Member(MemberBase, table=True): diff --git a/frontend/src/client/index.ts b/frontend/src/client/index.ts index 2eaf2bd2..f40fdcb6 100644 --- a/frontend/src/client/index.ts +++ b/frontend/src/client/index.ts @@ -21,6 +21,9 @@ export type { MembersOut } from './models/MembersOut'; export type { MemberUpdate } from './models/MemberUpdate'; export type { Message } from './models/Message'; export type { NewPassword } from './models/NewPassword'; +export type { Skill } from './models/Skill'; +export type { SkillOut } from './models/SkillOut'; +export type { SkillsOut } from './models/SkillsOut'; export type { TeamChat } from './models/TeamChat'; export type { TeamCreate } from './models/TeamCreate'; export type { TeamOut } from './models/TeamOut'; @@ -50,6 +53,9 @@ export { $MembersOut } from './schemas/$MembersOut'; export { $MemberUpdate } from './schemas/$MemberUpdate'; export { $Message } from './schemas/$Message'; export { $NewPassword } from './schemas/$NewPassword'; +export { $Skill } from './schemas/$Skill'; +export { $SkillOut } from './schemas/$SkillOut'; +export { $SkillsOut } from './schemas/$SkillsOut'; export { $TeamChat } from './schemas/$TeamChat'; export { $TeamCreate } from './schemas/$TeamCreate'; export { $TeamOut } from './schemas/$TeamOut'; @@ -68,6 +74,7 @@ export { $ValidationError } from './schemas/$ValidationError'; export { ItemsService } from './services/ItemsService'; export { LoginService } from './services/LoginService'; export { MembersService } from './services/MembersService'; +export { SkillsService } from './services/SkillsService'; export { TeamsService } from './services/TeamsService'; export { UsersService } from './services/UsersService'; export { UtilsService } from './services/UtilsService'; diff --git a/frontend/src/client/models/MemberOut.ts b/frontend/src/client/models/MemberOut.ts index 260f0a27..116d9677 100644 --- a/frontend/src/client/models/MemberOut.ts +++ b/frontend/src/client/models/MemberOut.ts @@ -3,6 +3,8 @@ /* tslint:disable */ /* eslint-disable */ +import type { Skill } from './Skill'; + export type MemberOut = { name: string; backstory?: (string | null); @@ -14,5 +16,6 @@ export type MemberOut = { source?: (number | null); id: number; belongs_to: number; + skills: Array; }; diff --git a/frontend/src/client/models/MemberUpdate.ts b/frontend/src/client/models/MemberUpdate.ts index 069d056e..6e5248bb 100644 --- a/frontend/src/client/models/MemberUpdate.ts +++ b/frontend/src/client/models/MemberUpdate.ts @@ -3,6 +3,8 @@ /* tslint:disable */ /* eslint-disable */ +import type { Skill } from './Skill'; + export type MemberUpdate = { name?: (string | null); backstory?: (string | null); @@ -13,5 +15,6 @@ export type MemberUpdate = { position_y?: (number | null); source?: (number | null); belongs_to?: (number | null); + skills?: (Array | null); }; diff --git a/frontend/src/client/models/Skill.ts b/frontend/src/client/models/Skill.ts new file mode 100644 index 00000000..3ec62db4 --- /dev/null +++ b/frontend/src/client/models/Skill.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type Skill = { + name: string; + description?: (string | null); + id?: (number | null); +}; + diff --git a/frontend/src/client/models/SkillOut.ts b/frontend/src/client/models/SkillOut.ts new file mode 100644 index 00000000..e3565cf8 --- /dev/null +++ b/frontend/src/client/models/SkillOut.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type SkillOut = { + name: string; + description: (string | null); + id: number; +}; + diff --git a/frontend/src/client/models/SkillsOut.ts b/frontend/src/client/models/SkillsOut.ts new file mode 100644 index 00000000..dab97540 --- /dev/null +++ b/frontend/src/client/models/SkillsOut.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { Skill } from './Skill'; + +export type SkillsOut = { + data: Array; + count: number; +}; + diff --git a/frontend/src/client/schemas/$MemberCreate.ts b/frontend/src/client/schemas/$MemberCreate.ts index 525a9a2b..0ecad778 100644 --- a/frontend/src/client/schemas/$MemberCreate.ts +++ b/frontend/src/client/schemas/$MemberCreate.ts @@ -7,6 +7,7 @@ export const $MemberCreate = { name: { type: 'string', isRequired: true, + pattern: '^[a-zA-Z0-9_-]{1,64}$', }, backstory: { type: 'any-of', diff --git a/frontend/src/client/schemas/$MemberOut.ts b/frontend/src/client/schemas/$MemberOut.ts index 236fa7d7..6f0aa034 100644 --- a/frontend/src/client/schemas/$MemberOut.ts +++ b/frontend/src/client/schemas/$MemberOut.ts @@ -7,6 +7,7 @@ export const $MemberOut = { name: { type: 'string', isRequired: true, + pattern: '^[a-zA-Z0-9_-]{1,64}$', }, backstory: { type: 'any-of', @@ -57,5 +58,12 @@ export const $MemberOut = { type: 'number', isRequired: true, }, + skills: { + type: 'array', + contains: { + type: 'Skill', + }, + isRequired: true, + }, }, } as const; diff --git a/frontend/src/client/schemas/$MemberUpdate.ts b/frontend/src/client/schemas/$MemberUpdate.ts index 912f311d..af6cab1c 100644 --- a/frontend/src/client/schemas/$MemberUpdate.ts +++ b/frontend/src/client/schemas/$MemberUpdate.ts @@ -8,6 +8,7 @@ export const $MemberUpdate = { type: 'any-of', contains: [{ type: 'string', + pattern: '^[a-zA-Z0-9_-]{1,64}$', }, { type: 'null', }], @@ -76,5 +77,16 @@ export const $MemberUpdate = { type: 'null', }], }, + skills: { + type: 'any-of', + contains: [{ + type: 'array', + contains: { + type: 'Skill', + }, + }, { + type: 'null', + }], + }, }, } as const; diff --git a/frontend/src/client/schemas/$Skill.ts b/frontend/src/client/schemas/$Skill.ts new file mode 100644 index 00000000..5452b8ab --- /dev/null +++ b/frontend/src/client/schemas/$Skill.ts @@ -0,0 +1,28 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $Skill = { + properties: { + name: { + type: 'string', + isRequired: true, + }, + description: { + type: 'any-of', + contains: [{ + type: 'string', + }, { + type: 'null', + }], + }, + id: { + type: 'any-of', + contains: [{ + type: 'number', + }, { + type: 'null', + }], + }, + }, +} as const; diff --git a/frontend/src/client/schemas/$SkillOut.ts b/frontend/src/client/schemas/$SkillOut.ts new file mode 100644 index 00000000..dd9a28cf --- /dev/null +++ b/frontend/src/client/schemas/$SkillOut.ts @@ -0,0 +1,25 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $SkillOut = { + properties: { + name: { + type: 'string', + isRequired: true, + }, + description: { + type: 'any-of', + contains: [{ + type: 'string', + }, { + type: 'null', + }], + isRequired: true, + }, + id: { + type: 'number', + isRequired: true, + }, + }, +} as const; diff --git a/frontend/src/client/schemas/$SkillsOut.ts b/frontend/src/client/schemas/$SkillsOut.ts new file mode 100644 index 00000000..1d0f6270 --- /dev/null +++ b/frontend/src/client/schemas/$SkillsOut.ts @@ -0,0 +1,19 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $SkillsOut = { + properties: { + data: { + type: 'array', + contains: { + type: 'Skill', + }, + isRequired: true, + }, + count: { + type: 'number', + isRequired: true, + }, + }, +} as const; diff --git a/frontend/src/client/schemas/$TeamCreate.ts b/frontend/src/client/schemas/$TeamCreate.ts index 944de0e6..73b8f4de 100644 --- a/frontend/src/client/schemas/$TeamCreate.ts +++ b/frontend/src/client/schemas/$TeamCreate.ts @@ -7,6 +7,7 @@ export const $TeamCreate = { name: { type: 'string', isRequired: true, + pattern: '^[a-zA-Z0-9_-]{1,64}$', }, description: { type: 'any-of', diff --git a/frontend/src/client/schemas/$TeamOut.ts b/frontend/src/client/schemas/$TeamOut.ts index f12568cd..db97a2e4 100644 --- a/frontend/src/client/schemas/$TeamOut.ts +++ b/frontend/src/client/schemas/$TeamOut.ts @@ -7,6 +7,7 @@ export const $TeamOut = { name: { type: 'string', isRequired: true, + pattern: '^[a-zA-Z0-9_-]{1,64}$', }, description: { type: 'any-of', diff --git a/frontend/src/client/schemas/$TeamUpdate.ts b/frontend/src/client/schemas/$TeamUpdate.ts index 52857980..7f845c36 100644 --- a/frontend/src/client/schemas/$TeamUpdate.ts +++ b/frontend/src/client/schemas/$TeamUpdate.ts @@ -8,6 +8,7 @@ export const $TeamUpdate = { type: 'any-of', contains: [{ type: 'string', + pattern: '^[a-zA-Z0-9_-]{1,64}$', }, { type: 'null', }], diff --git a/frontend/src/client/services/SkillsService.ts b/frontend/src/client/services/SkillsService.ts new file mode 100644 index 00000000..0e93b933 --- /dev/null +++ b/frontend/src/client/services/SkillsService.ts @@ -0,0 +1,63 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { SkillOut } from '../models/SkillOut'; +import type { SkillsOut } from '../models/SkillsOut'; + +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; + +export class SkillsService { + + /** + * Read Skills + * Retrieve skills. + * @returns SkillsOut Successful Response + * @throws ApiError + */ + public static readSkills({ + skip, + limit = 100, + }: { + skip?: number, + limit?: number, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/skills/', + query: { + 'skip': skip, + 'limit': limit, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * Read Skill + * Get skill by ID. + * @returns SkillOut Successful Response + * @throws ApiError + */ + public static readSkill({ + id, + }: { + id: number, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/skills/{id}', + path: { + 'id': id, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + +} diff --git a/frontend/src/components/Members/EditMember.tsx b/frontend/src/components/Members/EditMember.tsx index 4593a486..44633279 100644 --- a/frontend/src/components/Members/EditMember.tsx +++ b/frontend/src/components/Members/EditMember.tsx @@ -15,15 +15,17 @@ import { Textarea, } from "@chakra-ui/react" import useCustomToast from "../../hooks/useCustomToast" -import { useMutation, useQueryClient } from "react-query" +import { useMutation, useQuery, useQueryClient } from "react-query" import { type ApiError, MembersService, type TeamUpdate, type MemberOut, type MemberUpdate, + SkillsService, } from "../../client" -import { type SubmitHandler, useForm } from "react-hook-form" +import { type SubmitHandler, useForm, Controller } from "react-hook-form" +import { Select as MultiSelect, chakraComponents } from "chakra-react-select" interface EditMemberProps { member: MemberOut @@ -32,6 +34,14 @@ interface EditMemberProps { onClose: () => void } +const customSelectOption = { + Option: (props: any) => ( + + {props.children}: {props.data.description} + + ), +} + export function EditMember({ member, teamId, @@ -40,15 +50,36 @@ export function EditMember({ }: EditMemberProps) { const queryClient = useQueryClient() const showToast = useCustomToast() + const { + data: skills, + isLoading, + isError, + error, + } = useQuery("skills", () => SkillsService.readSkills({})) + + if (isError) { + const errDetail = (error as ApiError).body?.detail + showToast("Something went wrong.", `${errDetail}`, "error") + } + const { register, handleSubmit, reset, + control, + watch, formState: { isSubmitting, errors, isDirty }, } = useForm({ mode: "onBlur", criteriaMode: "all", - values: member, + values: { + ...member, + skills: member.skills.map((skill) => ({ + ...skill, + label: skill.name, + value: skill.id, + })), + }, }) const updateMember = async (data: MemberUpdate) => { @@ -82,6 +113,17 @@ export function EditMember({ onClose() } + // Watch the type field to determine whether to disable multiselect + const memberType = watch("type") + + const skillOptions = skills + ? skills.data.map((skill) => ({ + ...skill, + label: skill.name, + value: skill.id, + })) + : [] + return ( @@ -137,28 +179,34 @@ export function EditMember({ className="nodrag nopan" /> - {/* TODO: Add ability to select skills */} - {/* - Skills - - */} + ( + + Skills + + {error?.message} + + )} + />