From 8f40b4387bfd7cc65606d29f482e83852f0c981f Mon Sep 17 00:00:00 2001 From: Ana Garcia Date: Thu, 10 Oct 2024 11:15:22 +0200 Subject: [PATCH] Fix build tree and add search bar --- .../im-team-hierarchy/IMTeamHierarchyItem.tsx | 29 +++-- .../im-team-hierarchy/IMTeamHierarchyView.tsx | 33 +++-- .../IMTeamBuilderPage.tsx | 4 + .../useIMTeamBuilder.ts | 113 +++++++++++++----- 4 files changed, 122 insertions(+), 57 deletions(-) diff --git a/src/webapp/components/im-team-hierarchy/IMTeamHierarchyItem.tsx b/src/webapp/components/im-team-hierarchy/IMTeamHierarchyItem.tsx index 7913f315..a80300c9 100644 --- a/src/webapp/components/im-team-hierarchy/IMTeamHierarchyItem.tsx +++ b/src/webapp/components/im-team-hierarchy/IMTeamHierarchyItem.tsx @@ -7,16 +7,18 @@ import { Maybe } from "../../../utils/ts-utils"; import { Checkbox } from "../checkbox/Checkbox"; import { TeamMember } from "../../../domain/entities/incident-management-team/TeamMember"; import { TeamMemberProfile } from "./TeamMemberProfile"; +import { IMTeamHierarchyOption } from "./IMTeamHierarchyView"; +import { Id } from "../../../domain/entities/Ref"; type IMTeamHierarchyItemProps = { nodeId: string; teamRole: string; member: Maybe; - selected: boolean; disabled?: boolean; onSelectedChange: (nodeId: string, selected: boolean) => void; - children?: React.ReactNode; + subChildren: IMTeamHierarchyOption[]; diseaseOutbreakEventName: string; + selectedItemId: Id; }; export const IMTeamHierarchyItem: React.FC = React.memo(props => { @@ -26,9 +28,9 @@ export const IMTeamHierarchyItem: React.FC = React.mem member, disabled = false, onSelectedChange, - selected, - children, + subChildren, diseaseOutbreakEventName, + selectedItemId, } = props; const [openMemberProfile, setOpenMemberProfile] = React.useState(false); @@ -43,9 +45,9 @@ export const IMTeamHierarchyItem: React.FC = React.mem const onTeamRoleClick = React.useCallback( (event: React.MouseEvent) => { event.preventDefault(); - !disabled && onSelectedChange(nodeId, !selected); + !disabled && onSelectedChange(nodeId, !(selectedItemId === nodeId)); }, - [disabled, nodeId, onSelectedChange, selected] + [disabled, nodeId, onSelectedChange, selectedItemId] ); const onMemberClick = React.useCallback( @@ -67,7 +69,7 @@ export const IMTeamHierarchyItem: React.FC = React.mem @@ -84,7 +86,18 @@ export const IMTeamHierarchyItem: React.FC = React.mem } > - {children} + {subChildren.map(child => ( + + ))} {member && ( diff --git a/src/webapp/components/im-team-hierarchy/IMTeamHierarchyView.tsx b/src/webapp/components/im-team-hierarchy/IMTeamHierarchyView.tsx index a2ce3b9b..fad574f6 100644 --- a/src/webapp/components/im-team-hierarchy/IMTeamHierarchyView.tsx +++ b/src/webapp/components/im-team-hierarchy/IMTeamHierarchyView.tsx @@ -7,13 +7,14 @@ import { Maybe } from "../../../utils/ts-utils"; import { TeamMember } from "../../../domain/entities/incident-management-team/TeamMember"; import { IMTeamHierarchyItem } from "./IMTeamHierarchyItem"; import { Id } from "../../../domain/entities/Ref"; +import { SearchInput } from "../search-input/SearchInput"; export type IMTeamHierarchyOption = { id: Id; teamRole: string; teamRoleId: Id; member: Maybe; - parents: { id: Id; name: string }[]; + parent: Maybe; children: IMTeamHierarchyOption[]; }; @@ -22,13 +23,23 @@ type IMTeamHierarchyViewProps = { selectedItemId: Id; onSelectedItemChange: (nodeId: Id, selected: boolean) => void; diseaseOutbreakEventName: string; + onSearchChange: (term: string) => void; + searchTerm: string; }; export const IMTeamHierarchyView: React.FC = React.memo(props => { - const { onSelectedItemChange, items, selectedItemId, diseaseOutbreakEventName } = props; + const { + onSelectedItemChange, + items, + selectedItemId, + diseaseOutbreakEventName, + searchTerm, + onSearchChange, + } = props; return ( + } @@ -40,23 +51,11 @@ export const IMTeamHierarchyView: React.FC = React.mem nodeId={item.id} teamRole={item.teamRole} member={item.member} - selected={selectedItemId === item.id} + selectedItemId={selectedItemId} onSelectedChange={onSelectedItemChange} diseaseOutbreakEventName={diseaseOutbreakEventName} - > - {item.children && - item.children.map(child => ( - - ))} - + subChildren={item.children} + /> ))} diff --git a/src/webapp/pages/incident-management-team-builder/IMTeamBuilderPage.tsx b/src/webapp/pages/incident-management-team-builder/IMTeamBuilderPage.tsx index 5f9f4082..a2a5c4d4 100644 --- a/src/webapp/pages/incident-management-team-builder/IMTeamBuilderPage.tsx +++ b/src/webapp/pages/incident-management-team-builder/IMTeamBuilderPage.tsx @@ -30,6 +30,8 @@ export const IMTeamBuilderPage: React.FC = React.memo(() => { selectedHierarchyItemId, openDeleteModalData, disableDeletion, + searchTerm, + onSearchChange, onSelectHierarchyItem, goToIncidentManagementTeamRole, onDeleteIncidentManagementTeamMember, @@ -88,6 +90,8 @@ export const IMTeamBuilderPage: React.FC = React.memo(() => { selectedItemId={selectedHierarchyItemId} onSelectedItemChange={onSelectHierarchyItem} diseaseOutbreakEventName={getCurrentEventTracker()?.name || ""} + onSearchChange={onSearchChange} + searchTerm={searchTerm} /> void; disableDeletion: boolean; + onSearchChange: (term: string) => void; + searchTerm: string; }; export function useIMTeamBuilder(id: Id): State { @@ -51,6 +54,7 @@ export function useIMTeamBuilder(id: Id): State { const [openDeleteModalData, setOpenDeleteModalData] = useState( undefined ); + const [searchTerm, setSearchTerm] = useState(""); const getIncidentManagementTeam = useCallback(() => { compositionRoot.incidentManagementTeam.get.execute(id).run( @@ -58,7 +62,7 @@ export function useIMTeamBuilder(id: Id): State { setIncidentManagementTeam(incidentManagementTeam); setIncidentManagementTeamHierarchyItems( mapIncidentManagementTeamToIncidentManagementTeamHierarchyItems( - incidentManagementTeam + incidentManagementTeam?.teamHierarchy ) ); }, @@ -198,6 +202,28 @@ export function useIMTeamBuilder(id: Id): State { } }, [incidentManagementTeam?.teamHierarchy]); + const onSearchChange = useCallback( + (term: string) => { + setSearchTerm(term); + + if (incidentManagementTeamHierarchyItems) { + const filteredIncidentManagementTeamHierarchyItems = term + ? filterIncidentManagementTeamHierarchy( + incidentManagementTeamHierarchyItems, + term + ) + : mapIncidentManagementTeamToIncidentManagementTeamHierarchyItems( + incidentManagementTeam?.teamHierarchy + ); + + setIncidentManagementTeamHierarchyItems( + filteredIncidentManagementTeamHierarchyItems + ); + } + }, + [incidentManagementTeam?.teamHierarchy, incidentManagementTeamHierarchyItems] + ); + const lastUpdated = getDateAsLocaleDateTimeString(new Date()); //TO DO : Fetch sync time from datastore once implemented return { @@ -212,13 +238,15 @@ export function useIMTeamBuilder(id: Id): State { openDeleteModalData, onOpenDeleteModalData, disableDeletion, + searchTerm, + onSearchChange, }; } function mapIncidentManagementTeamToIncidentManagementTeamHierarchyItems( - incidentManagementTeam: Maybe + incidentManagementTeamHierarchy: Maybe ): IMTeamHierarchyOption[] { - if (incidentManagementTeam?.teamHierarchy) { + if (incidentManagementTeamHierarchy) { const createHierarchyItem = ( item: TeamMember, teamRole: TeamRole @@ -237,11 +265,11 @@ function mapIncidentManagementTeamToIncidentManagementTeamHierarchyItems( teamRoles: item.teamRoles, workPosition: item.workPosition, }), - parents: [], + parent: teamRole.reportsToUsername, children: [], }); - const teamMap = incidentManagementTeam?.teamHierarchy.reduce< + const teamMap = incidentManagementTeamHierarchy.reduce< Record >((map, item) => { const hierarchyItems = item.teamRoles?.map(teamRole => @@ -259,34 +287,55 @@ function mapIncidentManagementTeamToIncidentManagementTeamHierarchyItems( ); }, {}); - return incidentManagementTeam.teamHierarchy.reduce((acc, item) => { - return item.teamRoles - ? item.teamRoles.reduce((innerAcc, teamRole) => { - const hierarchyItem = teamMap[teamRole.id]; - if (!hierarchyItem) return innerAcc; - - const reportsToUsername = teamRole.reportsToUsername; - if (reportsToUsername) { - const parentItem = Object.values(teamMap).find( - teamItem => teamItem.member?.username === reportsToUsername - ); - - if (parentItem) { - parentItem.children = [...(parentItem.children || []), hierarchyItem]; - hierarchyItem.parents = [ - ...hierarchyItem.parents, - { id: parentItem.id, name: parentItem.teamRole }, - ]; - } - } - - return hierarchyItem.parents.length === 0 - ? [...innerAcc, hierarchyItem] - : innerAcc; - }, acc) - : acc; - }, []); + return buildTree(teamMap); } else { return []; } } + +function buildTree(teamMap: Record): IMTeamHierarchyOption[] { + const findChildren = (parentUsername: string): IMTeamHierarchyOption[] => + Object.values(teamMap) + .filter(item => item.parent === parentUsername) + .reduce((acc, item) => { + const children = findChildren(item.member?.username || ""); + return [...acc, { ...item, children: [...item.children, ...children] }]; + }, []); + + return Object.values(teamMap).reduce((acc, item) => { + const isRoot = !item.parent; + if (isRoot) { + const children = findChildren(item.member?.username || ""); + return [...acc, { ...item, children: [...item.children, ...children] }]; + } + + return acc; + }, []); +} + +function filterIncidentManagementTeamHierarchy( + items: IMTeamHierarchyOption[], + searchTerm: string +): IMTeamHierarchyOption[] { + return _c( + items.map(item => { + const filteredChildren = filterIncidentManagementTeamHierarchy( + item.children, + searchTerm + ); + + const isMatch = item.teamRole.toLowerCase().includes(searchTerm.toLowerCase()); + + if (isMatch || filteredChildren.length > 0) { + return { + ...item, + children: filteredChildren, + }; + } + + return null; + }) + ) + .compact() + .toArray(); +}