perf: Improve event type page speed by paginating hosts#27371
perf: Improve event type page speed by paginating hosts#27371joeauyeung wants to merge 51 commits intomainfrom
Conversation
Instead of storing all 700+ hosts in React Hook Form state, track only changes via pendingHostChanges (hostsToAdd, hostsToUpdate, hostsToRemove). The backend update handler accepts deltas directly while maintaining backward compatibility with the full hosts array for Platform API consumers. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add cursor-based paginated tRPC endpoints to fetch hosts without loading all of them upfront. Includes repository methods with keyset pagination and user name/avatar data for direct display. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add usePaginatedAvailabilityHosts, usePaginatedAssignmentHosts, and useFetchMoreOnScroll utility for cursor-based infinite scrolling of hosts in the event type editor tabs. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…hosts Refactor EventTeamAssignmentTab, HostLocations, HostEditDialogs, and EventType to consume paginated host data from context instead of form state. Use hostCount instead of hosts array for empty assignment checks. Change tab rendering to lazy functions to avoid mounting all tab hooks. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use host name/avatarUrl from paginated responses directly instead of looking up team members by userId. Remove teamMembers prop from EventAvailabilityTab and its web/platform wrappers. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add searchTeamMembers tRPC endpoint with cursor-based pagination and name/email search. Update CheckedTeamSelect to support server-side filtering and infinite scroll. Replace static teamMembers prop in AddMembersWithSwitch with the new useSearchTeamMembers hook. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Stop loading all 700+ team members in the initial getEventTypeById query. Remove members select from repository queries, replace with targeted queries for current user membership and child owner roles. Remove teamMembers prop threading from web and platform wrappers. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Performance Benchmark Results (700 Hosts)I ran benchmarks comparing form initialization and tab switching performance between Key Performance Improvements
How the Optimizations Achieve These Results1. Lazy Tab Rendering (13.7x faster initialization) Before (main branch): const tabMap = {
setup: <EventSetupTab eventType={eventType} teamMembers={teamMembers} />,
availability: <EventAvailabilityTab eventType={eventType} teamMembers={teamMembers} />,
team: <EventTeamAssignmentTab teamMembers={teamMembers} />,
// All tabs instantiated upfront
};After (this branch): const tabMap = {
setup: () => <EventSetupTab eventType={eventType} />,
availability: () => <EventAvailabilityTab eventType={eventType} />,
team: () => <EventTeamAssignmentTab teamMembers={[]} />,
// Tabs are functions, only called when active
};
// In render:
{tabMap[tabName]?.()}2. Removal of teamMembers from Initial Load (71.4% payload reduction)
3. Paginated Host Fetching
4. HostsProvider Context with Delta Tracking
Benchmark ScriptA benchmark script was created to measure these improvements: npx tsx apps/web/modules/event-types/components/__benchmarks__/runBenchmark.tsSummaryFor event types with 700 hosts, users should experience:
|
apps/web/modules/event-types/components/tabs/assignment/EventTeamAssignmentTab.tsx
Outdated
Show resolved
Hide resolved
Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
When scheduling type changes, all hosts need to be cleared. Previously, only paginated hosts were removed which caused a bug when there were more hosts than fit in one page. This adds a clearAllHosts flag to PendingHostChanges that: - Signals the backend to compute the delta between existing hosts and hostsToAdd - Keeps hosts that are in both sets (updates their properties) - Removes hosts that are not in hostsToAdd - Adds hosts that are only in hostsToAdd The frontend now calls clearAllHosts() when scheduling type changes, and the usePaginatedAssignmentHosts hook properly shows only hostsToAdd when clearAllHosts is true. Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
…teration With paginated host loading, not all hosts are available client-side. Query hasFixedHosts on the first page from the DB and pass it through to FixedHosts so it can determine fixed host state from server data combined with pending changes. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
hostCount was using the static eventType._count.hosts from the initial query, so adding/removing hosts without saving would not trigger the empty assignment warning on navigation. Derive effectiveHostCount from pendingHostChanges to account for unsaved additions and removals. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
team.members was fetched with take: 0 (always empty), so teamMembers was always []. Remove the vestigial data flow: repository members select, enrichment loop, teamMembers mapping, and all downstream prop threading. Replace team.members usages (child owner roles, currentUserMembership) with targeted queries. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add a wrapper function that calls getEventTypeById then fetches and enriches team members separately. The atoms service uses this to restore teamMembers with platform-managed user filtering, while the web tRPC handler continues using the leaner getEventTypeById. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Include membership role in the searchTeamMembers response (was already selected from DB but dropped). Expose raw members from the hook so consumers can access role data. Remove the childOwnerMemberships batch query from getEventTypeById since role will come from the paginated search instead. Make ChildrenEventType.profile optional for new children added from search results. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Wire up useSearchTeamMembers to the ChildrenEventTypes component with debounced search and infinite scroll pagination. Transform search results into ChildrenEventType options with role from the paginated query. Add server-side search and scroll-to-load props to ChildrenEventTypeSelect, matching the pattern used by CheckedTeamSelect for host assignment. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove the two sequential enrichUserWithItsProfile loops (children owners and event type users) from getEventTypeById. No consumer uses the enriched profile on event type users, and children enrichment moves to pagination. Replace children select with _count in the repository. Return childrenCount instead of enriched children array. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…checks Replace assignedUsers array with childrenCount number in checkForEmptyAssignment and useHandleRouteChange. Remove the EventTypeAssignedUsers type. Add PendingChildrenChanges type and pendingChildrenChanges to FormValues. Update both web and platform wrappers to pass childrenCount and reset pending changes on save. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
… event types Add getChildrenForAssignment paginated tRPC endpoint with repository method. Create usePaginatedAssignmentChildren hook following the same pattern as hosts. Update ChildrenEventTypes component to load existing children via pagination and track changes via pendingChildrenChanges delta. Add pendingChildrenChanges to the update schema and reconstruct the full children array on the backend from DB + delta before passing to handleChildrenEventTypes. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
9 issues found across 59 files
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="apps/web/modules/event-types/components/tabs/assignment/EventTeamAssignmentTab.tsx">
<violation number="1" location="apps/web/modules/event-types/components/tabs/assignment/EventTeamAssignmentTab.tsx:215">
P1: Bug: toggling off fixed hosts only removes fixed hosts from loaded pages, not all pages. With 700+ hosts and pagination (limit=20), only the first page's fixed hosts are removed. The scheduling type change correctly uses `clearAllHosts()` to handle this scenario — a similar flag-based approach should be used here so the backend can compute the full delta across all hosts.</violation>
<violation number="2" location="apps/web/modules/event-types/components/tabs/assignment/EventTeamAssignmentTab.tsx:750">
P2: Potential duplicate adds: `assignedOwnerIds` only tracks children from loaded pages (pagination). Already-assigned children on unloaded pages will still appear as dropdown options, potentially causing duplicate additions. Consider adding a server-side deduplication check in the add handler, or use a separate lightweight query to get all assigned user IDs.</violation>
</file>
<file name="packages/features/Segment.tsx">
<violation number="1" location="packages/features/Segment.tsx:148">
P2: Missing error handling after switching to useInfiniteQuery can mask query failures by rendering an empty list instead of an error state. Add a guard so that when the query errors (data undefined and not pending), the UI shows the error message.</violation>
</file>
<file name="apps/web/modules/event-types/components/AddMembersWithSwitch.tsx">
<violation number="1" location="apps/web/modules/event-types/components/AddMembersWithSwitch.tsx:126">
P3: Hardcoded fallback label `User ${host.userId}` bypasses localization. Use `t("user")` (or another existing key) for the fallback label instead of a raw English string.</violation>
</file>
<file name="packages/features/eventtypes/lib/getEventTypeById.ts">
<violation number="1" location="packages/features/eventtypes/lib/getEventTypeById.ts:255">
P2: Sequential `await` in loop causes O(n) round-trips for profile enrichment. For the 700-host scenario this PR targets, this means 700 sequential async calls in `getEventTypeByIdWithTeamMembers`. Use `Promise.all` to parallelize.</violation>
</file>
<file name="packages/features/eventtypes/lib/useHostsForEventType.ts">
<violation number="1" location="packages/features/eventtypes/lib/useHostsForEventType.ts:86">
P2: `addHost` does not check for duplicates in `hostsToAdd`. If called twice with the same `userId` (e.g., double-click, re-render), the host will be added twice. Consider guarding with an existence check before appending.</violation>
<violation number="2" location="packages/features/eventtypes/lib/useHostsForEventType.ts:208">
P2: O(n²) host lookup in `setHosts`: `serverHosts.find()` inside the `for` loop is O(n) per iteration, making the whole block O(n²). With 700 hosts this means ~490k comparisons. Build a `Map` from `serverHosts` for O(1) lookups, consistent with the PR's own stated goal of eliminating O(n²) operations.</violation>
</file>
<file name="packages/trpc/server/routers/viewer/eventTypes/heavy/update.handler.ts">
<violation number="1" location="packages/trpc/server/routers/viewer/eventTypes/heavy/update.handler.ts:492">
P2: Redundant database query — `eventType.hosts` (loaded at the top of the handler) already contains all host `userId`s for this event type. In a PR optimizing for 700+ hosts, this extra round-trip is counter-productive. Reuse the data you already have.</violation>
</file>
<file name="packages/platform/atoms/event-types/hooks/useEventTypeForm.ts">
<violation number="1" location="packages/platform/atoms/event-types/hooks/useEventTypeForm.ts:385">
P2: `hasHostChanges` ignores the `clearAllHosts` flag, so a “clear all hosts” action with empty delta arrays won’t be sent to the backend.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
apps/web/modules/event-types/components/tabs/assignment/EventTeamAssignmentTab.tsx
Show resolved
Hide resolved
apps/web/modules/event-types/components/tabs/assignment/EventTeamAssignmentTab.tsx
Outdated
Show resolved
Hide resolved
packages/trpc/server/routers/viewer/eventTypes/heavy/update.handler.ts
Outdated
Show resolved
Hide resolved
apps/web/modules/event-types/components/AddMembersWithSwitch.tsx
Outdated
Show resolved
Hide resolved
Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
…est mocks Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
Devin AI is resolving merge conflictsThis PR has merge conflicts with the Devin will:
If you prefer to resolve conflicts manually, you can close the Devin session and handle it yourself. |
Co-Authored-By: unknown <>
…ence for paginated hosts Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
There was a problem hiding this comment.
2 issues found across 2 files (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="apps/web/playwright/team-event-type-assignment.e2e.ts">
<violation number="1" location="apps/web/playwright/team-event-type-assignment.e2e.ts:45">
P2: Rule violated: **E2E Tests Best Practices**
Add expect(page).toHaveURL() after page.goto() to comply with the E2E best-practice requirement for fast-fail on unexpected redirects.</violation>
<violation number="2" location="apps/web/playwright/team-event-type-assignment.e2e.ts:85">
P2: Rule violated: **E2E Tests Best Practices**
Avoid text locators in E2E tests per E2E Tests Best Practices; use data-testid and page.getByTestId for these elements to prevent brittle tests.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| } | ||
|
|
||
| async function navigateToAssignmentTab(page: Page, eventId: number) { | ||
| await page.goto(`/event-types/${eventId}?tabName=team`); |
There was a problem hiding this comment.
P2: Rule violated: E2E Tests Best Practices
Add expect(page).toHaveURL() after page.goto() to comply with the E2E best-practice requirement for fast-fail on unexpected redirects.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/playwright/team-event-type-assignment.e2e.ts, line 45:
<comment>Add expect(page).toHaveURL() after page.goto() to comply with the E2E best-practice requirement for fast-fail on unexpected redirects.</comment>
<file context>
@@ -0,0 +1,408 @@
+}
+
+async function navigateToAssignmentTab(page: Page, eventId: number) {
+ await page.goto(`/event-types/${eventId}?tabName=team`);
+ await page.waitForLoadState("networkidle");
+ const form = page.locator("#event-type-form");
</file context>
|
|
||
| const form = await navigateToAssignmentTab(page, teamEvent.id); | ||
|
|
||
| await expect(form.getByText("Fixed hosts").first()).toBeVisible(); |
There was a problem hiding this comment.
P2: Rule violated: E2E Tests Best Practices
Avoid text locators in E2E tests per E2E Tests Best Practices; use data-testid and page.getByTestId for these elements to prevent brittle tests.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/playwright/team-event-type-assignment.e2e.ts, line 85:
<comment>Avoid text locators in E2E tests per E2E Tests Best Practices; use data-testid and page.getByTestId for these elements to prevent brittle tests.</comment>
<file context>
@@ -0,0 +1,408 @@
+
+ const form = await navigateToAssignmentTab(page, teamEvent.id);
+
+ await expect(form.getByText("Fixed hosts").first()).toBeVisible();
+ await scrollToHost(form, TARGET_HOST, page);
+ });
</file context>
Devin AI is addressing Cubic AI's review feedbackA Devin session has been created to address the issues identified by Cubic AI. |
|
Reviewed the two Cubic AI comments on
Both are below the 9/10 confidence threshold, so no changes were made. |
…ring revalidation - Move revalidateEventTypeEditPage from onSuccess to onSettled to prevent revalidatePath from resetting useSearchParams during tab navigation - Always fetch existing children from DB when pendingChildrenChanges is undefined, preventing accidental deletion of child managed event types Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
eunjae-lee
left a comment
There was a problem hiding this comment.
massive job ! overall the code looks good. left some comments. let me test it and will leave more review.
| if (newHost.isFixed !== serverHost.isFixed) { | ||
| changes.isFixed = newHost.isFixed; | ||
| hasChanges = true; | ||
| } | ||
| if (newHost.priority !== serverHost.priority) { | ||
| changes.priority = newHost.priority; | ||
| hasChanges = true; | ||
| } | ||
| if (newHost.weight !== serverHost.weight) { | ||
| changes.weight = newHost.weight; | ||
| hasChanges = true; | ||
| } | ||
| if (newHost.scheduleId !== serverHost.scheduleId) { | ||
| changes.scheduleId = newHost.scheduleId; | ||
| hasChanges = true; | ||
| } | ||
| if (newHost.groupId !== serverHost.groupId) { | ||
| changes.groupId = newHost.groupId; | ||
| hasChanges = true; | ||
| } |
There was a problem hiding this comment.
what if we refactor this part a bit like
["ixFixed", "priority", ...].forEach ?
| // Reconstruct children array from pendingChildrenChanges or preserve existing children | ||
| let resolvedChildren = children; | ||
| if (!children) { | ||
| const existingChildren = await ctx.prisma.eventType.findMany({ |
There was a problem hiding this comment.
can we use a repository instead of direct prisma usage
| const { teamId, cursor, limit, search, memberUserIds } = input; | ||
|
|
||
| // Verify the requesting user is a member of this team | ||
| const callerMembership = await ctx.prisma.membership.findFirst({ |
There was a problem hiding this comment.
let's move this to a repository
| fallbackWarnings, | ||
| troubleshooter: troubleshooter, | ||
| result: matchingTeamMembers.map((user) => ({ | ||
| const sortedMembers = matchingTeamMembers |
There was a problem hiding this comment.
can we update findByIds or create a new method that supports search & limit & cursor out of the box, so that we don't fetch everything and filter at the app level?
|
@Udit-takkar I thought about this but since all of the areas this PR touches is deeply dependent on the initial query for hosts and members it's hard to break this into multiple PRs without causing double network calls. I already discussed this with @volnei and @eunjae-lee |
|
@joeauyeung there are some new trpc endpoints with tests. We can reivew and merge them first and then everything together |
- Comment 23: Refactor repetitive field comparison in setHosts to use COMPARABLE_FIELDS loop instead of individual if blocks - Comment 24: Move direct Prisma query in update.handler.ts to EventTypeRepository.findChildrenByParentId method - Comment 25: Move searchTeamMembers handler Prisma calls to MembershipRepository (hasMembership + searchMembers methods) - Comment 26: Add UserRepository.findByIdsWithPagination for DB-level search/cursor/limit instead of JS-side filtering Co-Authored-By: unknown <>
| ); | ||
| }; | ||
|
|
||
| const useFetchMoreOnScroll = ( |
There was a problem hiding this comment.
Extracted to be used in other places
| formMethods.setValue( | ||
| "hosts", | ||
| hosts.map((host) => ({ ...host, location: null })), | ||
| { shouldDirty: true } | ||
| ); | ||
| // Use setHosts from context instead of form setValue for performance | ||
| setHosts(serverHosts, hosts.map((host) => ({ ...host, location: null }))); | ||
| } |
There was a problem hiding this comment.
One of the main changes in this PR is instead of writing all hosts to the react hook form, we instead track changed host values and write that to the form.
| const userTimeZone = extractHostTimezone({ | ||
| userId: eventType.userId, | ||
| teamId: eventType.teamId, | ||
| hosts: eventType.hosts, | ||
| owner: eventType.owner, | ||
| team: eventType.team, | ||
| }); |
There was a problem hiding this comment.
This is used to display the expiry date of private links. Since we're moving away from relying on the whole hosts and team members in the event type pages, we can just use the owner for personal event types or the perspective of the logged in user to display these times.
| customClassNames, | ||
| teamId, | ||
| isSegmentApplicable, | ||
| serverHosts, |
There was a problem hiding this comment.
Instead of the whole hosts array being passed, these are the hosts from the paginated results
| const handleFixedHostsActivation = useCallback(() => { | ||
| const currentHosts = getValues("hosts"); | ||
| setValue( | ||
| "hosts", | ||
| teamMembers.map((teamMember) => { | ||
| const host = currentHosts.find((host) => host.userId === parseInt(teamMember.value, 10)); | ||
| return { | ||
| isFixed: true, | ||
| userId: parseInt(teamMember.value, 10), | ||
| priority: host?.priority ?? 2, | ||
| weight: host?.weight ?? 100, | ||
| // if host was already added, retain scheduleId and groupId | ||
| scheduleId: host?.scheduleId || teamMember.defaultScheduleId, | ||
| groupId: host?.groupId || null, | ||
| }; | ||
| }), | ||
| { shouldDirty: true } | ||
| ); | ||
| }, [getValues, setValue, teamMembers]); | ||
| // No-op: when "assign all" is toggled ON, the assignAllTeamMembers flag | ||
| // is set by the AssignAllTeamMembers component. The booking system resolves | ||
| // all members at booking time, so we don't need to create individual host entries. | ||
| const handleFixedHostsActivation = useCallback(() => {}, []); |
There was a problem hiding this comment.
When "Assign all members" is selected, we don't need to iterate through the team's members and add them to the react hook form. We can just send this flag to the update endpoint and handle this logic there
| min={0} | ||
| min={1} |
There was a problem hiding this comment.
If a weight is set to 0, should just remove the host
There was a problem hiding this comment.
Dumb component for searching for hosts
| innerClassNames={customClassNames?.assignToSelect?.innerClassNames} | ||
| value={value} | ||
| isMulti | ||
| {...(onSearchChange |
There was a problem hiding this comment.
Implement pagination and searching for specific children event types
There was a problem hiding this comment.
@volnei I specifically left comments in this file to help explain the hooks used when determining changed hosts and whether to write them to the form.
|
|
||
| const matchingTeamMembers = filterMemberIds | ||
| ? allMatchingTeamMembers?.filter((member) => filterMemberIds.includes(member.id)) | ||
| : allMatchingTeamMembers; |
There was a problem hiding this comment.
Remove relying on the fully loaded team members to display matching members to the attribute filter
|
@volnei @eunjae-lee what are your thoughts on @Udit-takkar's comment here? |
Yeah I think that's doable. Also something I tried in the past as well. Pure additions to the backend that are not used anywhere first, and then the second part of the project that uses those backend elements. Can you try Devin this? |
|
Closing in favor of the stacked PR approach: |
What does this PR do?
This PR significantly improves the performance of the event type settings page for teams with many hosts (tested with 700 hosts). The changes address slow form initialization and tab switching by implementing lazy rendering, paginated data fetching, and delta-based state management.
https://www.loom.com/share/28bd91df784f41b5af00dc1f80a8d276
Performance Benchmark Results (700 Hosts)
Key Changes
1. Lazy Tab Rendering
2. Removal of teamMembers from Initial Load
teamMembersarray (~151 KB for 700 members) is no longer loaded upfront3. Paginated Host Fetching
getAssignmentHosts,getAvailabilityHosts,searchTeamMembers@tanstack/react-virtualfor smooth scrolling4. Delta-Based Host Updates
HostsProvidercontext tracks add/update/remove deltas viapendingHostChangesUpdates since last revision (Eunjae's review feedback)
Refactor: Repetitive field comparison in
setHostsreplaced with loop (Comment 23)if (newHost.X !== serverHost.X)blocks with aCOMPARABLE_FIELDSarray andfor...oflooplocationstill handled separately since it usesareLocationsEqualrather than strict equalitypackages/features/eventtypes/lib/useHostsForEventType.tsRefactor: Direct Prisma query in
update.handler.tsmoved to repository (Comment 24)ctx.prisma.eventType.findMany({ where: { parentId } })intoEventTypeRepository.findChildrenByParentId()packages/features/eventtypes/repositories/eventTypeRepository.ts,packages/trpc/server/routers/viewer/eventTypes/heavy/update.handler.tsRefactor:
searchTeamMembershandler Prisma calls moved to MembershipRepository (Comment 25)searchMembers()method toMembershipRepositoryfor paginated team member searchmembershipRepo.hasMembership()(existing method) for auth check andmembershipRepo.searchMembers()for the querypackages/features/membership/repositories/MembershipRepository.ts,packages/trpc/server/routers/viewer/eventTypes/searchTeamMembers.handler.tsRefactor: Added paginated search to UserRepository (Comment 26)
findByIdsWithPagination()method applies search/cursor/limit at DB level instead of loading all users into memoryfindTeamMembersMatchingAttributeLogic.handler.tspackages/features/users/repositories/UserRepository.ts,packages/trpc/server/routers/viewer/attributes/findTeamMembersMatchingAttributeLogic.handler.tsPrevious updates (managed event type fixes)
Fix: Managed event children deleted when admin saves without children changes
pendingChildrenChangesis undefined,resolvedChildrenstayed undefined andhandleChildrenEventTypesinterpreted this as "delete all children"childrenparam is undefined, then apply any pending deltas on toppackages/trpc/server/routers/viewer/eventTypes/heavy/update.handler.tsFix: Tab resets to "Basics" after saving on a non-default tab
revalidateEventTypeEditPageinonSuccesscallsrevalidatePathwhich temporarily emptiesuseSearchParams, causinguseTypedQueryto resettabNameto the default ("setup")revalidateEventTypeEditPagefromonSuccesstoonSettled(after tRPC query invalidations complete), so server cache revalidation doesn't race with tab navigationapps/web/modules/event-types/components/EventTypeWebWrapper.tsxBoth fixes resolve the 2 failing E2E tests in
managed-event-types.e2e.ts— all 16 tests now pass locally.Previous updates (E2E tests & weight fix)
Added: E2E tests for team event type assignment (
apps/web/playwright/team-event-type-assignment.e2e.ts)scrollToHost()helper that scrollsoverflow-y-autocontainers to trigger infinite scroll loading for hosts beyond page 1Fix: Weight changes via "Edit weights" sheet not persisting for paginated hosts
EditWeightsForAllTeamMembers.handleSaveonly processed hosts from the paginatedvalueprop, losing changes for hosts on page 2+handleSaveto useupdateHostfromHostsContextfor individual weight changes, which properly tracks them inpendingHostChangesregardless of pagination stateuseTeamMembersWithSegmenthooksPrevious updates (cubic's review feedback - 9 issues)
Fix:
clearAllHostsflag not checked inhasHostChanges(#9)hasHostChanges()now returnstruewhenclearAllHostsis setFix: Redundant DB query when clearing all hosts (#8)
eventType.hostsalready fetched earlier instead of making a separatefindManyqueryFix: Sequential
awaitin loop replaced withPromise.all(#5)clearAllHostspath usingPromise.allFix: Duplicate hosts in
hostsToAdd(#6)Set-based deduplication check when adding hosts viaaddHost()Fix: O(n²) host lookup in
setHostsreplaced with Map (#7)Array.find()toMaplookup for O(n) complexityFix: Hardcoded fallback label bypasses localization (#4)
"Team Member"tot("team_member")inCheckedHostFieldFix: Missing error handling in
Segment.tsx(#3)isErrorstate handling to display error message when query failsFix: Toggle off fixed hosts only removes loaded pages (#1 - P1)
clearAllHostsflag toPendingHostChangestypehostsToAdd, removes all others from DBFix:
assignedOwnerIdsonly tracks children from loaded pages (#2)pendingChildrenChangeswithclearAllChildrenflag for managed event typesType definitions centralized
HostUpdateInput,PendingHostChangesInput,PendingChildrenChangesInputtopackages/features/eventtypes/lib/types.tsMandatory Tasks (DO NOT REMOVE)
How should this be tested?
Run E2E tests locally:
Human Review Checklist
hasMembershipbehavioral change: The refactoredsearchTeamMembershandler now useshasMembership()which checksaccepted: true, whereas the original handler didn't check membership acceptance status. Confirm this is the intended behavior (more secure - only accepted members can search).in+gtcombination: InMembershipRepository.searchMembers, when bothmemberUserIdsandcursorare provided, it uses{ in: memberUserIds, gt: cursor }on the same field. Confirm Prisma handles this correctly (should filter to IDs in the array AND greater than cursor).findByIdsWithPaginationperformance: The new method does an extracountquery whenlimitis provided. For large datasets, this could be slow. Consider if thetotalcount is actually needed by callers.revalidateEventTypeEditPagefromonSuccesstoonSettleddoesn't cause stale data issues for other sessionsupdate.handler.ts(lines 996-1062) correctly handles: no changes (preserve all), clearAllChildren (replace with childrenToAdd), partial changes (apply deltas)EditWeightsForAllTeamMembers.handleSavecorrectly tracks weight changes viaupdateHostfrom HostsContextscrollToHost()E2E helper reliably triggers pagination loading (scroll containers withoverflow-y-auto)clearAllHostsdelta computation inupdate.handler.tscorrectly handles: hosts to delete (existing but NOT in hostsToAdd), hosts to update (in BOTH), hosts to add (only in hostsToAdd)hostsarrayChecklist
Link to Devin run: https://app.devin.ai/sessions/01bf278940f745cdbd4dc6c8af2038d8
Link to Devin run (merge conflict resolution): https://app.devin.ai/sessions/24a418ad1a3b4f2c9c7ecbcc62d350ae
Link to Devin run (cubic review fixes): https://app.devin.ai/sessions/df4d6e11f27c47beac0715750900622d
Link to Devin run (E2E tests & weight fix): https://app.devin.ai/sessions/c5b9a3599c1f403dbb5d08e02b5ea8b4
Link to Devin run (Eunjae review refactors): https://app.devin.ai/sessions/39f5fe04887743f1a0c7655094e807f5
Requested by: joe@cal.com (@joeauyeung)