From 38a7fcfedd3dc126065e2c5400d5bd821e11db8a Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Tue, 20 Jan 2026 07:01:49 +0530 Subject: [PATCH] fix: filter segment members to only show those with host entries (#26816) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What does this PR do? Fixes a bug where the UI shows team members who match segment criteria even when they don't have host entries in the database. When `assignAllTeamMembers` is `true` and segment filtering is enabled, members would appear in the Assignment tab but wouldn't actually be assigned during booking because they lack host entries. **The Problem:** - The filtering logic in `EditWeightsForAllTeamMembers` short-circuited when `assignAllTeamMembers` was true, showing all segment-matching members regardless of whether they had host entries - The `handleSave` function only updates hosts that already exist in the `value` array, filtering out members without host entries - This created a misleading UI where members appeared but wouldn't be assigned **The Fix:** - Removed the `assignAllTeamMembers` prop from `EditWeightsForAllTeamMembers` component entirely since the filtering should always check for existing host entries - Updated the filter in `EditWeightsForAllTeamMembers.tsx` to always check for existing host entries in the `value` array - Added `filterMemberIds` prop to `Segment` component to filter matching team members to only show those who are hosts of the event-type - `AddMembersWithSwitch` now passes the list of non-fixed host user IDs to the Segment component via `filterMemberIds` ## Mandatory Tasks (DO NOT REMOVE) - [x] I have self-reviewed the code (A decent size PR without self-review might be rejected). - [x] I have updated the developer docs in /docs if this PR makes changes that would require a [documentation change](https://cal.com/docs). N/A - no documentation changes needed. - [ ] I confirm automated tests are in place that prove my fix is effective or that my feature works. ## How should this be tested? 1. Create a team event type with round-robin scheduling 2. Enable "Assign all team members" toggle 3. Enable segment filtering with criteria that matches some team members 4. Verify that only members who have host entries (are actually assigned to the event type) appear in the "Edit team member weights" sheet 5. Members matching segment criteria but without host entries should NOT be displayed ## Checklist - [x] My code follows the style guidelines of this project - [x] I have checked if my changes generate no new warnings --- ### Human Review Checklist - [ ] Verify the filtering logic correctly shows only members with host entries in both `EditWeightsForAllTeamMembers` and `Segment` components - [ ] Confirm this aligns with the actual booking behavior (members without host entries won't be assigned) - [ ] Check that `filterMemberIds` prop is correctly passed through `AddMembersWithSwitch` → `MembersSegmentWithToggle` → `Segment` → `MatchingTeamMembers` - [ ] Verify the filter `value.filter((host) => !host.isFixed).map((host) => host.userId)` correctly extracts non-fixed host IDs --- Link to Devin run: https://app.devin.ai/sessions/3333b41226d0452e98ef7ac358511f5b Requested by: @hariombalhara --- .../components/AddMembersWithSwitch.tsx | 23 ++--- .../EditWeightsForAllTeamMembers.tsx | 23 ++--- .../assignment/EventTeamAssignmentTab.tsx | 92 +++++++++---------- packages/features/Segment.tsx | 31 ++++--- 4 files changed, 81 insertions(+), 88 deletions(-) diff --git a/apps/web/modules/event-types/components/AddMembersWithSwitch.tsx b/apps/web/modules/event-types/components/AddMembersWithSwitch.tsx index a4c0d35cdf..905874c144 100644 --- a/apps/web/modules/event-types/components/AddMembersWithSwitch.tsx +++ b/apps/web/modules/event-types/components/AddMembersWithSwitch.tsx @@ -1,26 +1,23 @@ -import { useMemo, type ComponentProps, type Dispatch, type SetStateAction } from "react"; -import { useFormContext } from "react-hook-form"; -import { Controller } from "react-hook-form"; -import type { Options } from "react-select"; - import { AddMembersWithSwitchPlatformWrapper } from "@calcom/atoms/add-members-switch/AddMembersWithSwitchPlatformWrapper"; -import { AddMembersWithSwitchWebWrapper } from "./AddMembersWithSwitchWebWrapper"; import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform"; -import { Segment } from "@calcom/features/Segment"; import type { FormValues, Host, SettingsToggleClassNames, TeamMember, } from "@calcom/features/eventtypes/lib/types"; +import { Segment } from "@calcom/features/Segment"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { AttributesQueryValue } from "@calcom/lib/raqb/types"; -import { Label } from "@calcom/ui/components/form"; -import { SettingsToggle } from "@calcom/ui/components/form"; +import { Label, SettingsToggle } from "@calcom/ui/components/form"; +import { type ComponentProps, type Dispatch, type SetStateAction, useMemo } from "react"; +import { Controller, useFormContext } from "react-hook-form"; +import type { Options } from "react-select"; +import { AddMembersWithSwitchWebWrapper } from "./AddMembersWithSwitchWebWrapper"; import AssignAllTeamMembers from "./AssignAllTeamMembers"; -import CheckedTeamSelect from "./CheckedTeamSelect"; import type { CheckedSelectOption, CheckedTeamSelectCustomClassNames } from "./CheckedTeamSelect"; +import CheckedTeamSelect from "./CheckedTeamSelect"; interface IUserToValue { id: number | null; @@ -130,6 +127,7 @@ function MembersSegmentWithToggle({ rrSegmentQueryValue, setRrSegmentQueryValue, className, + filterMemberIds, }: { teamId: number; assignRRMembersUsingSegment: boolean; @@ -137,6 +135,7 @@ function MembersSegmentWithToggle({ rrSegmentQueryValue: AttributesQueryValue | null; setRrSegmentQueryValue: (value: AttributesQueryValue) => void; className?: string; + filterMemberIds?: number[]; }) { const { t } = useLocale(); const onQueryValueChange = ({ queryValue }: { queryValue: AttributesQueryValue }) => { @@ -162,6 +161,7 @@ function MembersSegmentWithToggle({ queryValue={rrSegmentQueryValue} onQueryValueChange={onQueryValueChange} className={className} + filterMemberIds={filterMemberIds} /> )} @@ -193,7 +193,7 @@ export type AddMembersWithSwitchProps = { customClassNames?: AddMembersWithSwitchCustomClassNames; }; -const enum AssignmentState { +enum AssignmentState { TOGGLES_OFF_AND_ALL_TEAM_MEMBERS_NOT_APPLICABLE = "TOGGLES_OFF_AND_ALL_TEAM_MEMBERS_NOT_APPLICABLE", TOGGLES_OFF_AND_ALL_TEAM_MEMBERS_APPLICABLE = "TOGGLES_OFF_AND_ALL_TEAM_MEMBERS_APPLICABLE", ALL_TEAM_MEMBERS_ENABLED_AND_SEGMENT_APPLICABLE = "ALL_TEAM_MEMBERS_ENABLED_AND_SEGMENT_APPLICABLE", @@ -303,6 +303,7 @@ export function AddMembersWithSwitch({ setAssignRRMembersUsingSegment={setAssignRRMembersUsingSegment} rrSegmentQueryValue={rrSegmentQueryValue} setRrSegmentQueryValue={setRrSegmentQueryValue} + filterMemberIds={value.filter((host) => !host.isFixed).map((host) => host.userId)} /> )} diff --git a/apps/web/modules/event-types/components/EditWeightsForAllTeamMembers.tsx b/apps/web/modules/event-types/components/EditWeightsForAllTeamMembers.tsx index c1d57dd896..54eee476aa 100644 --- a/apps/web/modules/event-types/components/EditWeightsForAllTeamMembers.tsx +++ b/apps/web/modules/event-types/components/EditWeightsForAllTeamMembers.tsx @@ -1,8 +1,6 @@ "use client"; -import Link from "next/link"; -import { useState, useEffect, useRef, useMemo } from "react"; - +import { useTeamMembersWithSegmentPlatform } from "@calcom/atoms/event-types/hooks/useTeamMembersWithSegmentPlatform"; import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform"; import type { Host, TeamMember } from "@calcom/features/eventtypes/lib/types"; import ServerTrans from "@calcom/lib/components/ServerTrans"; @@ -10,8 +8,7 @@ import { downloadAsCsv } from "@calcom/lib/csvUtils"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { AttributesQueryValue } from "@calcom/lib/raqb/types"; import { Avatar } from "@calcom/ui/components/avatar"; -import { buttonClasses } from "@calcom/ui/components/button"; -import { Button } from "@calcom/ui/components/button"; +import { Button, buttonClasses } from "@calcom/ui/components/button"; import { TextField } from "@calcom/ui/components/form"; import { Icon } from "@calcom/ui/components/icon"; import { @@ -24,11 +21,9 @@ import { SheetTitle, } from "@calcom/ui/components/sheet"; import { showToast } from "@calcom/ui/components/toast"; - -import { - useTeamMembersWithSegmentPlatform, -} from "@calcom/atoms/event-types/hooks/useTeamMembersWithSegmentPlatform"; import { useTeamMembersWithSegment } from "@calcom/web/modules/event-types/hooks/useTeamMembersWithSegment"; +import Link from "next/link"; +import { useEffect, useMemo, useRef, useState } from "react"; type TeamMemberItemProps = { member: Omit & { weight?: number }; @@ -102,7 +97,6 @@ interface Props { teamMembers: TeamMember[]; value: Host[]; onChange: (hosts: Host[]) => void; - assignAllTeamMembers: boolean; assignRRMembersUsingSegment: boolean; teamId?: number; queryValue?: AttributesQueryValue | null; @@ -112,7 +106,6 @@ export const EditWeightsForAllTeamMembers = ({ teamMembers: initialTeamMembers, value, onChange, - assignAllTeamMembers, assignRRMembersUsingSegment, teamId, queryValue, @@ -240,13 +233,9 @@ export const EditWeightsForAllTeamMembers = ({ member.email.toLowerCase().includes(searchQuery.toLowerCase()) ) .filter((member) => { - // When assignAllTeamMembers is false, only include members that exist in value array - return ( - assignAllTeamMembers || - value.some((host) => !host.isFixed && host.userId === parseInt(member.value, 10)) - ); + return value.some((host) => !host.isFixed && host.userId === parseInt(member.value, 10)); }); - }, [teamMembers, localWeights, searchQuery, assignAllTeamMembers, value]); + }, [teamMembers, localWeights, searchQuery, value]); return ( <> diff --git a/apps/web/modules/event-types/components/tabs/assignment/EventTeamAssignmentTab.tsx b/apps/web/modules/event-types/components/tabs/assignment/EventTeamAssignmentTab.tsx index 54818fbc9e..0cc746e6cb 100644 --- a/apps/web/modules/event-types/components/tabs/assignment/EventTeamAssignmentTab.tsx +++ b/apps/web/modules/event-types/components/tabs/assignment/EventTeamAssignmentTab.tsx @@ -1,11 +1,21 @@ -import type { TFunction } from "i18next"; -import Link from "next/link"; -import { useCallback, useEffect, useRef, useState } from "react"; -import type { ComponentProps, Dispatch, SetStateAction } from "react"; -import { Controller, useFormContext, useWatch } from "react-hook-form"; -import type { Options } from "react-select"; -import { v4 as uuidv4 } from "uuid"; - +import type { + EventTypeSetupProps, + FormValues, + Host, + SelectClassNames, + SettingsToggleClassNames, + TeamMember, +} from "@calcom/features/eventtypes/lib/types"; +import { sortHosts } from "@calcom/lib/bookings/hostGroupUtils"; +import ServerTrans from "@calcom/lib/components/ServerTrans"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { RRTimestampBasis, SchedulingType } from "@calcom/prisma/enums"; +import classNames from "@calcom/ui/classNames"; +import { Button } from "@calcom/ui/components/button"; +import { Label, Select, SettingsToggle } from "@calcom/ui/components/form"; +import { Icon } from "@calcom/ui/components/icon"; +import { RadioAreaGroup as RadioArea } from "@calcom/ui/components/radio"; +import { Tooltip } from "@calcom/ui/components/tooltip"; import type { AddMembersWithSwitchCustomClassNames } from "@calcom/web/modules/event-types/components/AddMembersWithSwitch"; import AddMembersWithSwitch, { mapUserToValue, @@ -14,28 +24,15 @@ import AssignAllTeamMembers from "@calcom/web/modules/event-types/components/Ass import type { ChildrenEventTypeSelectCustomClassNames } from "@calcom/web/modules/event-types/components/ChildrenEventTypeSelect"; import ChildrenEventTypeSelect from "@calcom/web/modules/event-types/components/ChildrenEventTypeSelect"; import { EditWeightsForAllTeamMembers } from "@calcom/web/modules/event-types/components/EditWeightsForAllTeamMembers"; -import { sortHosts } from "@calcom/lib/bookings/hostGroupUtils"; import { LearnMoreLink } from "@calcom/web/modules/event-types/components/LearnMoreLink"; import WeightDescription from "@calcom/web/modules/event-types/components/WeightDescription"; -import type { - FormValues, - TeamMember, - EventTypeSetupProps, - Host, - SelectClassNames, - SettingsToggleClassNames, -} from "@calcom/features/eventtypes/lib/types"; -import ServerTrans from "@calcom/lib/components/ServerTrans"; -import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { RRTimestampBasis, SchedulingType } from "@calcom/prisma/enums"; -import classNames from "@calcom/ui/classNames"; -import { Button } from "@calcom/ui/components/button"; -import { Label } from "@calcom/ui/components/form"; -import { Select } from "@calcom/ui/components/form"; -import { SettingsToggle } from "@calcom/ui/components/form"; -import { Icon } from "@calcom/ui/components/icon"; -import { RadioAreaGroup as RadioArea } from "@calcom/ui/components/radio"; -import { Tooltip } from "@calcom/ui/components/tooltip"; +import type { TFunction } from "i18next"; +import Link from "next/link"; +import type { ComponentProps, Dispatch, SetStateAction } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { Controller, useFormContext, useWatch } from "react-hook-form"; +import type { Options } from "react-select"; +import { v4 as uuidv4 } from "uuid"; export type EventTeamAssignmentTabCustomClassNames = { assignmentType?: { @@ -506,7 +503,6 @@ const RoundRobinHosts = ({ teamMembers={teamMembers} value={value} onChange={handleWeightsChange} - assignAllTeamMembers={assignAllTeamMembers} assignRRMembersUsingSegment={assignRRMembersUsingSegment} teamId={teamId} queryValue={rrSegmentQueryValue} @@ -666,10 +662,10 @@ const Hosts = ({ return existingHost ? { - ...newValue, - scheduleId: existingHost.scheduleId, - groupId: existingHost.groupId, - } + ...newValue, + scheduleId: existingHost.scheduleId, + groupId: existingHost.groupId, + } : newValue; }); }; @@ -724,7 +720,7 @@ const Hosts = ({ ), MANAGED: <>, }; - return !!schedulingType ? schedulingTypeRender[schedulingType] : <>; + return schedulingType ? schedulingTypeRender[schedulingType] : <>; }} /> ); @@ -745,17 +741,17 @@ export const EventTeamAssignmentTab = ({ label: string; // description: string; }[] = [ - { - value: "COLLECTIVE", - label: t("collective"), - // description: t("collective_description"), - }, - { - value: "ROUND_ROBIN", - label: t("round_robin"), - // description: t("round_robin_description"), - }, - ]; + { + value: "COLLECTIVE", + label: t("collective"), + // description: t("collective_description"), + }, + { + value: "ROUND_ROBIN", + label: t("round_robin"), + // description: t("round_robin_description"), + }, + ]; const pendingMembers = (member: (typeof teamMembers)[number]) => !!eventType.team?.parentId || !!member.username; const teamMembersOptions = teamMembers @@ -887,13 +883,11 @@ export const EventTeamAssignmentTab = ({ {(eventType.team?.rrTimestampBasis && eventType.team?.rrTimestampBasis !== RRTimestampBasis.CREATED_AT) || - hostGroups?.length > 1 ? ( + hostGroups?.length > 1 ? ( diff --git a/packages/features/Segment.tsx b/packages/features/Segment.tsx index 01edec03ed..90e0f61d6a 100644 --- a/packages/features/Segment.tsx +++ b/packages/features/Segment.tsx @@ -1,21 +1,19 @@ "use client"; -import { useCallback, useState } from "react"; -import { Query, Builder, Utils as QbUtils } from "react-awesome-query-builder"; -import type { ImmutableTree, BuilderProps } from "react-awesome-query-builder"; -import type { JsonTree } from "react-awesome-query-builder"; - import { buildStateFromQueryValue } from "@calcom/app-store/_utils/raqb/raqbUtils.client"; import { - withRaqbSettingsAndWidgets, ConfigFor, + withRaqbSettingsAndWidgets, } from "@calcom/app-store/routing-forms/components/react-awesome-query-builder/config/uiConfig"; import { getQueryBuilderConfigForAttributes } from "@calcom/app-store/routing-forms/lib/getQueryBuilderConfig"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { isEqual } from "@calcom/lib/isEqual"; import type { AttributesQueryValue } from "@calcom/lib/raqb/types"; -import { trpc, type RouterOutputs } from "@calcom/trpc/react"; +import { type RouterOutputs, trpc } from "@calcom/trpc/react"; import cn from "@calcom/ui/classNames"; +import { useCallback, useState } from "react"; +import type { BuilderProps, ImmutableTree, JsonTree } from "react-awesome-query-builder"; +import { Builder, Utils as QbUtils, Query } from "react-awesome-query-builder"; export type Attributes = RouterOutputs["viewer"]["appRoutingForms"]["getAttributesForTeam"]; export function useAttributes(teamId: number) { @@ -34,12 +32,14 @@ function SegmentWithAttributes({ queryValue: initialQueryValue, onQueryValueChange, className, + filterMemberIds, }: { attributes: Attributes; teamId: number; queryValue: AttributesQueryValue | null; onQueryValueChange: ({ queryValue }: { queryValue: AttributesQueryValue }) => void; className?: string; + filterMemberIds?: number[]; }) { const attributesQueryBuilderConfig = getQueryBuilderConfigForAttributes({ attributes, @@ -91,7 +91,7 @@ function SegmentWithAttributes({ />
- +
); @@ -100,17 +100,19 @@ function SegmentWithAttributes({ function MatchingTeamMembers({ teamId, queryValue, + filterMemberIds, }: { teamId: number; queryValue: AttributesQueryValue | null; + filterMemberIds?: number[]; }) { const { t } = useLocale(); // 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 - ) + (child) => child.properties?.value?.[0] !== undefined && child.properties?.value?.[0] !== null + ) : false; const { data: matchingTeamMembersWithResult, isPending } = @@ -158,7 +160,11 @@ function MatchingTeamMembers({ } if (!matchingTeamMembersWithResult) return {t("something_went_wrong")}; - const { result: matchingTeamMembers } = matchingTeamMembersWithResult; + const { result: allMatchingTeamMembers } = matchingTeamMembersWithResult; + + const matchingTeamMembers = filterMemberIds + ? allMatchingTeamMembers?.filter((member) => filterMemberIds.includes(member.id)) + : allMatchingTeamMembers; return (
@@ -184,11 +190,13 @@ export function Segment({ queryValue, onQueryValueChange, className, + filterMemberIds, }: { teamId: number; queryValue: AttributesQueryValue | null; onQueryValueChange: ({ queryValue }: { queryValue: AttributesQueryValue }) => void; className?: string; + filterMemberIds?: number[]; }) { const { attributes, isPending } = useAttributes(teamId); const { t } = useLocale(); @@ -205,6 +213,7 @@ export function Segment({ queryValue={queryValue} onQueryValueChange={onQueryValueChange} className={className} + filterMemberIds={filterMemberIds} /> ); }