From 38e7eb03f998da077b1d87a1103c2cfdd090298f Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Fri, 24 Jan 2025 09:41:13 +0000 Subject: [PATCH] fix: attribute assignment hosts (#18831) * load correct users in query state for attribute rr * add segment loading state * skeleton * fix query value * update tests * fix type * use query by testId so no error is thrown * ensure queryValue children has valid values before we trigger api key * chore: remove lg --------- Co-authored-by: Udit Takkar --- packages/features/Segment.test.tsx | 4 +- packages/features/Segment.tsx | 48 ++++++-- .../EditWeightsForAllTeamMembers.tsx | 104 +++++++++++++++--- .../assignment/EventTeamAssignmentTab.tsx | 71 ++++++------ 4 files changed, 170 insertions(+), 57 deletions(-) diff --git a/packages/features/Segment.test.tsx b/packages/features/Segment.test.tsx index 2cd26eb143..c26c77e787 100644 --- a/packages/features/Segment.test.tsx +++ b/packages/features/Segment.test.tsx @@ -116,7 +116,7 @@ describe("Segment", () => { await waitFor(() => { // Query builder container should be present expect(screen.getByTestId("query-builder-container")).toBeInTheDocument(); - expect(screen.queryByText("loading")).not.toBeInTheDocument(); + expect(screen.queryByTestId("segment_loading_state")).not.toBeInTheDocument(); }); }); @@ -128,7 +128,7 @@ describe("Segment", () => { render(); await waitFor(() => { - expect(screen.getByText("loading")).toBeInTheDocument(); + expect(screen.getByTestId("segment_loading_state")).toBeInTheDocument(); }); }); diff --git a/packages/features/Segment.tsx b/packages/features/Segment.tsx index 4c5af7407a..9f2ae2b172 100644 --- a/packages/features/Segment.tsx +++ b/packages/features/Segment.tsx @@ -105,14 +105,48 @@ function MatchingTeamMembers({ queryValue: AttributesQueryValue | null; }) { const { t } = useLocale(); - const { data: matchingTeamMembersWithResult, isPending } = - trpc.viewer.attributes.findTeamMembersMatchingAttributeLogic.useQuery({ - teamId, - attributesQueryValue: queryValue, - _enablePerf: true, - }); - if (isPending) return {t("loading")}; + // Check if queryValue has valid children properties value + const hasValidValue = queryValue?.children1 + ? Object.values(queryValue.children1).some( + (child) => child.properties?.value?.[0] !== undefined && child.properties?.value?.[0] !== null + ) + : false; + + const { data: matchingTeamMembersWithResult, isPending } = + trpc.viewer.attributes.findTeamMembersMatchingAttributeLogic.useQuery( + { + teamId, + attributesQueryValue: queryValue, + _enablePerf: true, + }, + { + enabled: hasValidValue, + } + ); + + if (isPending) { + return ( +
+
+
+
+
    + {[...Array(3)].map((_, index) => ( +
  • +
    +
    +
    +
    +
  • + ))} +
+
+ ); + } + if (!matchingTeamMembersWithResult) return {t("something_went_wrong")}; const { result: matchingTeamMembers } = matchingTeamMembersWithResult; if (!matchingTeamMembers || !queryValue) { diff --git a/packages/features/eventtypes/components/EditWeightsForAllTeamMembers.tsx b/packages/features/eventtypes/components/EditWeightsForAllTeamMembers.tsx index bb4401f30f..1c2e05d794 100644 --- a/packages/features/eventtypes/components/EditWeightsForAllTeamMembers.tsx +++ b/packages/features/eventtypes/components/EditWeightsForAllTeamMembers.tsx @@ -7,7 +7,8 @@ import { useState, useEffect, useRef, useMemo } from "react"; import type { Host, TeamMember } from "@calcom/features/eventtypes/lib/types"; import { downloadAsCsv } from "@calcom/lib/csvUtils"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import "@calcom/ui"; +import type { AttributesQueryValue } from "@calcom/lib/raqb/types"; +import { trpc } from "@calcom/trpc"; import { Avatar, Button, @@ -25,7 +26,7 @@ import { } from "@calcom/ui"; type TeamMemberItemProps = { - member: TeamMember & { weight?: number }; + member: Omit & { weight?: number }; onWeightChange: (memberId: string, weight: number) => void; }; @@ -92,29 +93,93 @@ const TeamMemberItem = ({ member, onWeightChange }: TeamMemberItemProps) => { ); }; +interface UseTeamMembersWithSegmentProps { + initialTeamMembers: TeamMember[]; + assignRRMembersUsingSegment: boolean; + teamId?: number; + queryValue?: AttributesQueryValue | null; + value: Host[]; +} + +const useTeamMembersWithSegment = ({ + initialTeamMembers, + assignRRMembersUsingSegment, + teamId, + queryValue, + value, +}: UseTeamMembersWithSegmentProps) => { + const { data: matchingTeamMembersWithResult, isPending } = + trpc.viewer.attributes.findTeamMembersMatchingAttributeLogic.useQuery( + { + teamId: teamId || 0, + attributesQueryValue: queryValue as AttributesQueryValue, + _enablePerf: true, + }, + { + enabled: assignRRMembersUsingSegment && !!queryValue && !!teamId, + } + ); + + const teamMembers = useMemo(() => { + if (assignRRMembersUsingSegment && matchingTeamMembersWithResult?.result) { + return matchingTeamMembersWithResult.result.map((member) => ({ + value: member.id.toString(), + label: member.name || member.email, + email: member.email, + avatar: "", // Add avatar with fallback to empty string + })); + } + return initialTeamMembers; + }, [assignRRMembersUsingSegment, matchingTeamMembersWithResult, initialTeamMembers]); + + const localWeightsInitialValues = useMemo( + () => + teamMembers.reduce>((acc, member) => { + const memberInValue = value.find((host) => host.userId === parseInt(member.value, 10)); + acc[member.value] = memberInValue?.weight ?? 100; + return acc; + }, {}), + [teamMembers, value] + ); + + return { + teamMembers, + localWeightsInitialValues, + isPending, + }; +}; + interface Props { teamMembers: TeamMember[]; value: Host[]; onChange: (hosts: Host[]) => void; assignAllTeamMembers: boolean; + assignRRMembersUsingSegment: boolean; + teamId?: number; + queryValue?: AttributesQueryValue | null; } export const EditWeightsForAllTeamMembers = ({ - teamMembers, + teamMembers: initialTeamMembers, value, onChange, assignAllTeamMembers, + assignRRMembersUsingSegment, + teamId, + queryValue, }: Props) => { const [isOpen, setIsOpen] = useState(false); const { t } = useLocale(); const [searchQuery, setSearchQuery] = useState(""); - const localWeightsInitialValues = teamMembers.reduce>((acc, member) => { - // When assignAllTeamMembers is false, only include members that exist in value array - // Find the member in the value array and use its weight if it exists - const memberInValue = value.find((host) => host.userId === parseInt(member.value, 10)); - acc[member.value] = memberInValue?.weight ?? 100; - return acc; - }, {}); + + const { teamMembers, localWeightsInitialValues } = useTeamMembersWithSegment({ + initialTeamMembers, + assignRRMembersUsingSegment, + teamId, + queryValue, + value, + }); + const [localWeights, setLocalWeights] = useState>(localWeightsInitialValues); const [uploadErrors, setUploadErrors] = useState>([]); const [isErrorsExpanded, setIsErrorsExpanded] = useState(true); @@ -124,10 +189,21 @@ export const EditWeightsForAllTeamMembers = ({ }; const handleSave = () => { - const updatedValue = value.map((host) => ({ - ...host, - weight: localWeights[host.userId.toString()] ?? host.weight ?? 100, - })); + // Create a map of existing hosts for easy lookup + const existingHostsMap = new Map(value.map((host) => [host.userId.toString(), host])); + + // Create the updated value by processing all team members + const updatedValue = teamMembers.map((member) => { + const existingHost = existingHostsMap.get(member.value); + return { + ...existingHost, + userId: parseInt(member.value, 10), + isFixed: existingHost?.isFixed ?? false, + priority: existingHost?.priority ?? 0, + weight: localWeights[member.value] ?? existingHost?.weight ?? 100, + }; + }); + onChange(updatedValue); setIsOpen(false); }; diff --git a/packages/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab.tsx b/packages/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab.tsx index 2cd833a8cc..c69c07d578 100644 --- a/packages/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab.tsx +++ b/packages/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab.tsx @@ -299,6 +299,10 @@ const RoundRobinHosts = ({ control, name: "isRRWeightsEnabled", }); + const rrSegmentQueryValue = useWatch({ + control, + name: "rrSegmentQueryValue", + }); return (
@@ -319,40 +323,39 @@ const RoundRobinHosts = ({

- {!assignRRMembersUsingSegment && ( - <> - - name="isRRWeightsEnabled" - render={({ field: { value: isRRWeightsEnabled, onChange } }) => ( - { - onChange(active); - const rrHosts = getValues("hosts").filter((host) => !host.isFixed); - const sortedRRHosts = rrHosts.sort((a, b) => sortHosts(a, b, active)); - setValue("hosts", sortedRRHosts); - }}> - {!assignRRMembersUsingSegment ? ( - { - const sortedRRHosts = hosts.sort((a, b) => sortHosts(a, b, true)); - setValue("hosts", sortedRRHosts, { shouldDirty: true }); - }} - assignAllTeamMembers={assignAllTeamMembers} - /> - ) : null} - - )} - /> - - )} + <> + + name="isRRWeightsEnabled" + render={({ field: { value: isRRWeightsEnabled, onChange } }) => ( + { + onChange(active); + const rrHosts = getValues("hosts").filter((host) => !host.isFixed); + const sortedRRHosts = rrHosts.sort((a, b) => sortHosts(a, b, active)); + setValue("hosts", sortedRRHosts); + }}> + { + const sortedRRHosts = hosts.sort((a, b) => sortHosts(a, b, true)); + setValue("hosts", sortedRRHosts, { shouldDirty: true }); + }} + assignAllTeamMembers={assignAllTeamMembers} + assignRRMembersUsingSegment={assignRRMembersUsingSegment} + teamId={teamId} + queryValue={rrSegmentQueryValue} + /> + + )} + /> +