From 54f691e0a92ae831ff21a414cae1a44ab495bcf8 Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Fri, 14 Jun 2024 14:26:45 +0200 Subject: [PATCH 01/24] Fix types and other small things --- client/web/src/cody/management/api/client.ts | 2 +- client/web/src/cody/management/api/teamInvites.ts | 4 ++-- client/web/src/cody/management/api/teamMembers.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/web/src/cody/management/api/client.ts b/client/web/src/cody/management/api/client.ts index 96e854b159adc..8cdeda8406d6d 100644 --- a/client/web/src/cody/management/api/client.ts +++ b/client/web/src/cody/management/api/client.ts @@ -76,7 +76,7 @@ export module Client { } // Call is the bundle of data necessary for making an API request. -// This is a sort of "meta request" in the same veign as the `gql` +// This is a sort of "meta request" in the same vein as the `gql` // template tag, see: https://github.com/apollographql/graphql-tag export interface Call { method: 'GET' | 'POST' | 'PATCH' | 'DELETE' diff --git a/client/web/src/cody/management/api/teamInvites.ts b/client/web/src/cody/management/api/teamInvites.ts index a6b2fd06e9416..cfde66bad8746 100644 --- a/client/web/src/cody/management/api/teamInvites.ts +++ b/client/web/src/cody/management/api/teamInvites.ts @@ -11,9 +11,9 @@ export interface TeamInvite { status: TeamInviteStatus error?: string - sentAt: Date + sentAt: string sentBy: string - acceptedAt?: Date + acceptedAt?: string } export interface CreateTeamInviteRequest { diff --git a/client/web/src/cody/management/api/teamMembers.ts b/client/web/src/cody/management/api/teamMembers.ts index 6c7f83a503062..a06a772f1e7cc 100644 --- a/client/web/src/cody/management/api/teamMembers.ts +++ b/client/web/src/cody/management/api/teamMembers.ts @@ -3,8 +3,8 @@ export type TeamRole = 'member' | 'admin' export interface TeamMember { accountId: string displayName: string + email: string avatarUrl: string - role: TeamRole } From b2deac7a528252c9434a865d406de721b1e21e40 Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Fri, 14 Jun 2024 14:27:57 +0200 Subject: [PATCH 02/24] Add new API access functions and hooks --- client/web/src/cody/management/api/client.ts | 8 +++++++ .../api/react-query/subscriptions.ts | 22 ++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/client/web/src/cody/management/api/client.ts b/client/web/src/cody/management/api/client.ts index 8cdeda8406d6d..01c47f8804800 100644 --- a/client/web/src/cody/management/api/client.ts +++ b/client/web/src/cody/management/api/client.ts @@ -20,6 +20,14 @@ export module Client { return { method: 'PATCH', urlSuffix: '/team/current/subscription', requestBody } } + export function getCurrentTeamMembers(): Call { + return { method: 'GET', urlSuffix: '/team/current/members' } + } + + export function getCurrentTeamInvites(): Call { + return { method: 'GET', urlSuffix: '/team/current/invites' } + } + export function previewUpdateCurrentSubscription( requestBody: types.PreviewUpdateSubscriptionRequest ): Call { diff --git a/client/web/src/cody/management/api/react-query/subscriptions.ts b/client/web/src/cody/management/api/react-query/subscriptions.ts index 95a12cac199f2..d14231c887427 100644 --- a/client/web/src/cody/management/api/react-query/subscriptions.ts +++ b/client/web/src/cody/management/api/react-query/subscriptions.ts @@ -15,7 +15,9 @@ import type { PreviewResult, PreviewCreateTeamRequest, GetSubscriptionInvoicesResponse, -} from '../teamSubscriptions' + ListTeamMembersResponse, + ListTeamInvitesResponse +} from '../types' import { callCodyProApi } from './callCodyProApi' import { queryKeys } from './queryKeys' @@ -47,6 +49,24 @@ export const useSubscriptionInvoices = (): UseQueryResult => + useQuery({ + queryKey: queryKeys.teams.currentTeamMembers(), + queryFn: async () => { + const response = await callCodyProApi(Client.getCurrentTeamMembers()) + return response.ok ? response.json() : undefined + }, + }) + +export const useTeamInvites = (): UseQueryResult => + useQuery({ + queryKey: queryKeys.invites.currentTeamInvites(), + queryFn: async () => { + const response = await callCodyProApi(Client.getCurrentTeamInvites()) + return response.ok ? response.json() : undefined + }, + }) + export const useUpdateCurrentSubscription = (): UseMutationResult< Subscription | undefined, Error, From 716d980141905a571d072d54bcc347e48e6689f3 Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Fri, 14 Jun 2024 14:28:23 +0200 Subject: [PATCH 03/24] Use the new hooks for querying the data Not for modifying, yet; that's coming up shortly. --- client/web/src/cody/management/api/client.ts | 12 ++-- .../management/api/react-query/invites.ts | 11 +++- .../management/api/react-query/queryKeys.ts | 1 + .../cody/subscription/subscriptionSummary.ts | 11 ---- .../web/src/cody/team/CodyManageTeamPage.tsx | 56 +++++++++---------- client/web/src/cody/team/InviteUsers.tsx | 2 +- client/web/src/cody/team/TeamMemberList.tsx | 21 +------ client/web/src/cody/util.ts | 25 --------- 8 files changed, 46 insertions(+), 93 deletions(-) delete mode 100644 client/web/src/cody/subscription/subscriptionSummary.ts diff --git a/client/web/src/cody/management/api/client.ts b/client/web/src/cody/management/api/client.ts index 01c47f8804800..c9d31b39d2d7c 100644 --- a/client/web/src/cody/management/api/client.ts +++ b/client/web/src/cody/management/api/client.ts @@ -20,14 +20,6 @@ export module Client { return { method: 'PATCH', urlSuffix: '/team/current/subscription', requestBody } } - export function getCurrentTeamMembers(): Call { - return { method: 'GET', urlSuffix: '/team/current/members' } - } - - export function getCurrentTeamInvites(): Call { - return { method: 'GET', urlSuffix: '/team/current/invites' } - } - export function previewUpdateCurrentSubscription( requestBody: types.PreviewUpdateSubscriptionRequest ): Call { @@ -66,6 +58,10 @@ export module Client { return { method: 'GET', urlSuffix: `/team/${teamId}/invites/${inviteId}` } } + export function getTeamInvites(): Call { + return { method: 'GET', urlSuffix: '/team/current/invites' } + } + export function acceptInvite(teamId: string, inviteId: string): Call { return { method: 'POST', urlSuffix: `/team/${teamId}/invites/${inviteId}/accept` } } diff --git a/client/web/src/cody/management/api/react-query/invites.ts b/client/web/src/cody/management/api/react-query/invites.ts index 37be4c6383ac4..865616d57309e 100644 --- a/client/web/src/cody/management/api/react-query/invites.ts +++ b/client/web/src/cody/management/api/react-query/invites.ts @@ -7,7 +7,7 @@ import { } from '@tanstack/react-query' import { Client } from '../client' -import type { TeamInvite } from '../teamInvites' +import type { TeamInvite, ListTeamInvitesResponse } from '../teamInvites' import { callCodyProApi } from './callCodyProApi' import { queryKeys } from './queryKeys' @@ -27,6 +27,15 @@ export const useInvite = ({ }, }) +export const useTeamInvites = (): UseQueryResult => + useQuery({ + queryKey: queryKeys.invites.teamInvites(), + queryFn: async () => { + const response = await callCodyProApi(Client.getTeamInvites()) + return response.ok ? response.json() : undefined + }, + }) + export const useAcceptInvite = (): UseMutationResult => { const queryClient = useQueryClient() return useMutation({ diff --git a/client/web/src/cody/management/api/react-query/queryKeys.ts b/client/web/src/cody/management/api/react-query/queryKeys.ts index f1949ca3e7d73..be4e009d37524 100644 --- a/client/web/src/cody/management/api/react-query/queryKeys.ts +++ b/client/web/src/cody/management/api/react-query/queryKeys.ts @@ -14,5 +14,6 @@ export const queryKeys = { invites: { all: ['invite'] as const, invite: (teamId: string, inviteId: string) => [...queryKeys.invites.all, teamId, inviteId] as const, + teamInvites: () => [...queryKeys.invites.all, 'team-invites'] as const, }, } diff --git a/client/web/src/cody/subscription/subscriptionSummary.ts b/client/web/src/cody/subscription/subscriptionSummary.ts deleted file mode 100644 index c8b3fe1440a1a..0000000000000 --- a/client/web/src/cody/subscription/subscriptionSummary.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useSSCQuery } from '../util' - -type TeamRole = 'member' | 'admin' - -interface CodySubscriptionSummary { - teamId: string - userRole: TeamRole -} - -export const useCodySubscriptionSummaryData = (): [CodySubscriptionSummary | null, Error | null] => - useSSCQuery('/team/current/subscription/summary') diff --git a/client/web/src/cody/team/CodyManageTeamPage.tsx b/client/web/src/cody/team/CodyManageTeamPage.tsx index 3be0a2473c3c2..86f893c0598f2 100644 --- a/client/web/src/cody/team/CodyManageTeamPage.tsx +++ b/client/web/src/cody/team/CodyManageTeamPage.tsx @@ -14,23 +14,17 @@ import { PageTitle } from '../../components/PageTitle' import { CodyProRoutes } from '../codyProRoutes' import { CodyAlert } from '../components/CodyAlert' import { PageHeaderIcon } from '../components/PageHeaderIcon' -import { useCodySubscriptionSummaryData } from '../subscription/subscriptionSummary' -import { useSSCQuery } from '../util' +import { useTeamInvites } from '../management/api/react-query/invites' +import { useCurrentSubscription, useSubscriptionSummary } from '../management/api/react-query/subscriptions' +import { useTeamMembers } from '../management/api/react-query/teams' import { InviteUsers } from './InviteUsers' -import { TeamMemberList, type TeamMember, type TeamInvite } from './TeamMemberList' +import { TeamMemberList } from './TeamMemberList' interface CodyManageTeamPageProps extends TelemetryV2Props { authenticatedUser: AuthenticatedUser } -type CodySubscriptionStatus = 'active' | 'past_due' | 'unpaid' | 'canceled' | 'trialing' | 'other' - -interface CodySubscription { - subscriptionStatus: CodySubscriptionStatus - maxSeats: number -} - const AuthenticatedCodyManageTeamPage: React.FunctionComponent = ({ telemetryRecorder }) => { useEffect(() => { telemetryRecorder.recordEvent('cody.team.management', 'view') @@ -44,19 +38,19 @@ const AuthenticatedCodyManageTeamPage: React.FunctionComponent('/team/current/subscription') - const isPro = codySubscription?.subscriptionStatus !== 'canceled' - const [codySubscriptionSummary, codySubscriptionSummaryError] = useCodySubscriptionSummaryData() - const isAdmin = codySubscriptionSummary?.userRole === 'admin' - const [memberResponse, membersDataError] = useSSCQuery<{ members: TeamMember[] }>('/team/current/members') - const teamMembers = memberResponse?.members - const [invitesResponse, invitesDataError] = useSSCQuery<{ invites: TeamInvite[] }>('/team/current/invites') - const teamInvites = invitesResponse?.invites + const subscriptionQueryResult = useCurrentSubscription() + const isPro = subscriptionQueryResult.data?.subscriptionStatus !== 'canceled' + const subscriptionSummaryQueryResult = useSubscriptionSummary() + const isAdmin = subscriptionSummaryQueryResult.data?.userRole === 'admin' + const teamMembersQueryResult = useTeamMembers() + const teamMembers = teamMembersQueryResult.data?.members + const teamInvitesQueryResult = useTeamInvites() + const teamInvites = teamInvitesQueryResult.data?.invites const errorMessage = - codySubscriptionError?.message || - codySubscriptionSummaryError?.message || - membersDataError?.message || - invitesDataError?.message + subscriptionQueryResult.error?.message || + subscriptionSummaryQueryResult.error?.message || + teamMembersQueryResult.error?.message || + teamInvitesQueryResult.error?.message useEffect(() => { if (!isPro) { @@ -67,8 +61,8 @@ const AuthenticatedCodyManageTeamPage: React.FunctionComponent { const memberCount = teamMembers?.length ?? 0 const invitesUsed = (teamInvites ?? []).filter(invite => invite.status === 'sent').length - return Math.max((codySubscription?.maxSeats ?? 0) - (memberCount + invitesUsed), 0) - }, [codySubscription?.maxSeats, teamMembers, teamInvites]) + return Math.max((subscriptionQueryResult.data?.maxSeats ?? 0) - (memberCount + invitesUsed), 0) + }, [subscriptionQueryResult.data?.maxSeats, teamMembers, teamInvites]) return ( <> @@ -85,7 +79,10 @@ const AuthenticatedCodyManageTeamPage: React.FunctionComponent telemetryRecorder.recordEvent('cody.team.manage.subscription', 'click', { metadata: { - tier: codySubscription?.subscriptionStatus !== 'canceled' ? 1 : 0, + tier: + subscriptionQueryResult.data?.subscriptionStatus !== 'canceled' + ? 1 + : 0, }, }) } @@ -112,7 +109,10 @@ const AuthenticatedCodyManageTeamPage: React.FunctionComponent - {codySubscriptionError || codySubscriptionSummaryError || membersDataError || invitesDataError ? ( + {subscriptionQueryResult.isError || + subscriptionSummaryQueryResult.isError || + teamMembersQueryResult.isError || + teamInvitesQueryResult.isError ? (

We couldn't load team data this time. Please try a bit later.

{!!errorMessage && ( @@ -134,13 +134,13 @@ const AuthenticatedCodyManageTeamPage: React.FunctionComponent )} [] isAdmin: boolean } -export interface TeamInvite { - id: string - email: string - role: 'admin' | 'member' | 'none' - status: 'sent' | 'errored' | 'accepted' | 'canceled' - error: string | null - sentAt: string | null - acceptedAt: string | null -} - // This tiny function is extracted to make it testable. Same for the "now" parameter. export const formatInviteDate = (sentAt: string | null, now?: Date): string => { try { diff --git a/client/web/src/cody/util.ts b/client/web/src/cody/util.ts index c58c31e560e4d..7187c016451b5 100644 --- a/client/web/src/cody/util.ts +++ b/client/web/src/cody/util.ts @@ -1,6 +1,3 @@ -// The URL to direct users in order to manage their Cody Pro subscription. -import { useState, useEffect } from 'react' - import { CodyProRoutes } from './codyProRoutes' // URL the user needs to navigate to in order to modify their Cody Pro subscription. @@ -71,25 +68,3 @@ export function requestSSC(sscUrl: string, method: string, params?: object): Pro ...(!['GET', 'HEAD'].includes(method) && params ? { body: JSON.stringify(params) } : null), }) } - -// React hook to fetch data through the SSC proxy and convert the response to a more usable format. -// This is a low-level hook that is meant to be used by other hooks that need to fetch data from the SSC API. -export const useSSCQuery = (endpoint: string): [T | null, Error | null] => { - const [data, setData] = useState(null) - const [error, setError] = useState(null) - useEffect(() => { - async function loadData(): Promise { - try { - const response = await requestSSC(endpoint, 'GET') - const responseJson = await response.json() - setData(responseJson) - } catch (error) { - setError(error) - } - } - - void loadData() - }, [endpoint]) - - return [data, error] -} From 932a68ba03ab519c3cb2ff382dcdebf0d2e4c0c7 Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Fri, 14 Jun 2024 22:25:29 +0200 Subject: [PATCH 04/24] Make invites update nicely --- client/web/BUILD.bazel | 1 - client/web/src/cody/management/api/client.ts | 4 ++++ .../web/src/cody/management/api/react-query/invites.ts | 10 +++++++++- client/web/src/cody/team/InviteUsers.tsx | 9 ++++++--- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/client/web/BUILD.bazel b/client/web/BUILD.bazel index 4abc8e96ce1d0..fe1346d9c362a 100644 --- a/client/web/BUILD.bazel +++ b/client/web/BUILD.bazel @@ -278,7 +278,6 @@ ts_project( "src/cody/sidebar/useSidebarSize.tsx", "src/cody/subscription/CodySubscriptionPage.tsx", "src/cody/subscription/queries.tsx", - "src/cody/subscription/subscriptionSummary.ts", "src/cody/switch-account/CodySwitchAccountPage.tsx", "src/cody/team/CodyManageTeamPage.tsx", "src/cody/team/InviteUsers.tsx", diff --git a/client/web/src/cody/management/api/client.ts b/client/web/src/cody/management/api/client.ts index c9d31b39d2d7c..27a44bc2991d9 100644 --- a/client/web/src/cody/management/api/client.ts +++ b/client/web/src/cody/management/api/client.ts @@ -62,6 +62,10 @@ export module Client { return { method: 'GET', urlSuffix: '/team/current/invites' } } + export function sendInvite(requestBody: types.CreateTeamInviteRequest): Call { + return { method: 'POST', urlSuffix: '/team/current/invites', requestBody } + } + export function acceptInvite(teamId: string, inviteId: string): Call { return { method: 'POST', urlSuffix: `/team/${teamId}/invites/${inviteId}/accept` } } diff --git a/client/web/src/cody/management/api/react-query/invites.ts b/client/web/src/cody/management/api/react-query/invites.ts index 865616d57309e..aa540de1ccac0 100644 --- a/client/web/src/cody/management/api/react-query/invites.ts +++ b/client/web/src/cody/management/api/react-query/invites.ts @@ -7,7 +7,7 @@ import { } from '@tanstack/react-query' import { Client } from '../client' -import type { TeamInvite, ListTeamInvitesResponse } from '../teamInvites' +import type { TeamInvite, ListTeamInvitesResponse, CreateTeamInviteRequest } from '../teamInvites' import { callCodyProApi } from './callCodyProApi' import { queryKeys } from './queryKeys' @@ -36,6 +36,14 @@ export const useTeamInvites = (): UseQueryResult => { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: async requestBody => callCodyProApi(Client.sendInvite(requestBody)), + onSuccess: () => queryClient.invalidateQueries({ queryKey: queryKeys.invites.teamInvites() }), + }) +} + export const useAcceptInvite = (): UseMutationResult => { const queryClient = useQueryClient() return useMutation({ diff --git a/client/web/src/cody/team/InviteUsers.tsx b/client/web/src/cody/team/InviteUsers.tsx index 764af5fe0ad80..a5abbfb74e59b 100644 --- a/client/web/src/cody/team/InviteUsers.tsx +++ b/client/web/src/cody/team/InviteUsers.tsx @@ -6,7 +6,8 @@ import { ButtonLink, H2, Link, Text, H3, TextArea } from '@sourcegraph/wildcard' import { CodyAlert } from '../components/CodyAlert' import { CodyContainer } from '../components/CodyContainer' -import { isValidEmailAddress, requestSSC } from '../util' +import { useSendInvite } from '../management/api/react-query/invites' +import { isValidEmailAddress } from '../util' interface InviteUsersProps extends TelemetryV2Props { teamId: string | undefined @@ -24,6 +25,8 @@ export const InviteUsers: React.FunctionComponent = ({ const [invitesSentCount, setInvitesSentCount] = useState(0) const [invitesSendingErrorMessage, setInvitesSendingErrorMessage] = useState(null) + const sendInviteMutation = useSendInvite() + const onSendInvitesClicked = useCallback(async () => { const { emails: emailAddresses, error: emailParsingError } = parseEmailList( emailAddressesString, @@ -42,7 +45,7 @@ export const InviteUsers: React.FunctionComponent = ({ try { const responses = await Promise.all( emailAddresses.map(emailAddress => - requestSSC('/team/current/invites', 'POST', { email: emailAddress, role: 'member' }) + sendInviteMutation.mutateAsync.call(undefined, { email: emailAddress, role: 'member' }) ) ) if (responses.some(response => response.status !== 200)) { @@ -70,7 +73,7 @@ export const InviteUsers: React.FunctionComponent = ({ privateMetadata: { teamId, emailAddresses }, }) } - }, [emailAddressesString, remainingInviteCount, teamId, telemetryRecorder]) + }, [emailAddressesString, remainingInviteCount, sendInviteMutation.mutateAsync, teamId, telemetryRecorder]) return ( <> From 5851ca4df9d79741a05d6c718e111ee9068957bc Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Fri, 14 Jun 2024 23:25:37 +0200 Subject: [PATCH 05/24] Make team member list update nicely --- client/web/src/cody/management/api/client.ts | 8 ++++++ .../management/api/react-query/invites.ts | 13 +++++++--- .../cody/management/api/react-query/teams.ts | 18 +++++++++++-- .../src/cody/management/api/teamMembers.ts | 2 +- .../web/src/cody/team/CodyManageTeamPage.tsx | 20 ++++++++------- client/web/src/cody/team/InviteUsers.tsx | 2 +- client/web/src/cody/team/TeamMemberList.tsx | 24 ++++++++++-------- client/web/src/cody/util.ts | 25 ------------------- 8 files changed, 61 insertions(+), 51 deletions(-) diff --git a/client/web/src/cody/management/api/client.ts b/client/web/src/cody/management/api/client.ts index 27a44bc2991d9..51d9c8c7b786b 100644 --- a/client/web/src/cody/management/api/client.ts +++ b/client/web/src/cody/management/api/client.ts @@ -52,6 +52,10 @@ export module Client { return { method: 'GET', urlSuffix: '/team/current/members' } } + export function updateTeamMember(requestBody: types.UpdateTeamMembersRequest): Call { + return { method: 'PATCH', urlSuffix: '/team/current/members', requestBody } + } + // Invites export function getInvite(teamId: string, inviteId: string): Call { @@ -66,6 +70,10 @@ export module Client { return { method: 'POST', urlSuffix: '/team/current/invites', requestBody } } + export function resendInvite(inviteId: string): Call { + return { method: 'POST', urlSuffix: `/team/current/invites/${inviteId}/resend` } + } + export function acceptInvite(teamId: string, inviteId: string): Call { return { method: 'POST', urlSuffix: `/team/${teamId}/invites/${inviteId}/accept` } } diff --git a/client/web/src/cody/management/api/react-query/invites.ts b/client/web/src/cody/management/api/react-query/invites.ts index aa540de1ccac0..87ba9baf5e84f 100644 --- a/client/web/src/cody/management/api/react-query/invites.ts +++ b/client/web/src/cody/management/api/react-query/invites.ts @@ -44,6 +44,14 @@ export const useSendInvite = (): UseMutationResult => { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: async ({ inviteId }) => callCodyProApi(Client.resendInvite(inviteId)), + onSuccess: () => queryClient.invalidateQueries({ queryKey: queryKeys.invites.teamInvites() }), + }) +} + export const useAcceptInvite = (): UseMutationResult => { const queryClient = useQueryClient() return useMutation({ @@ -57,11 +65,10 @@ export const useAcceptInvite = (): UseMutationResult => { +export const useCancelInvite = (): UseMutationResult => { const queryClient = useQueryClient() return useMutation({ mutationFn: async ({ teamId, inviteId }) => callCodyProApi(Client.cancelInvite(teamId, inviteId)), - onSuccess: (_, { teamId, inviteId }) => - queryClient.invalidateQueries({ queryKey: queryKeys.invites.invite(teamId, inviteId) }), + onSuccess: () => queryClient.invalidateQueries({ queryKey: queryKeys.invites.all }), }) } diff --git a/client/web/src/cody/management/api/react-query/teams.ts b/client/web/src/cody/management/api/react-query/teams.ts index b2b65cc69b022..e9df824ea99a0 100644 --- a/client/web/src/cody/management/api/react-query/teams.ts +++ b/client/web/src/cody/management/api/react-query/teams.ts @@ -1,7 +1,13 @@ -import { useQuery, type UseQueryResult } from '@tanstack/react-query' +import { + useMutation, + useQuery, + useQueryClient, + type UseQueryResult, + type UseMutationResult, +} from '@tanstack/react-query' import { Client } from '../client' -import type { ListTeamMembersResponse } from '../teamMembers' +import type { ListTeamMembersResponse, UpdateTeamMembersRequest } from '../types' import { callCodyProApi } from './callCodyProApi' import { queryKeys } from './queryKeys' @@ -14,3 +20,11 @@ export const useTeamMembers = (): UseQueryResult => { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: async requestBody => callCodyProApi(Client.updateTeamMember(requestBody)), + onSettled: () => queryClient.invalidateQueries({ queryKey: queryKeys.teams.teamMembers() }), + }) +} diff --git a/client/web/src/cody/management/api/teamMembers.ts b/client/web/src/cody/management/api/teamMembers.ts index a06a772f1e7cc..0f482d7bacde6 100644 --- a/client/web/src/cody/management/api/teamMembers.ts +++ b/client/web/src/cody/management/api/teamMembers.ts @@ -19,7 +19,7 @@ export interface ListTeamMembersResponse { } export interface UpdateTeamMembersRequest { - addMembver?: TeamMemberRef + addMember?: TeamMemberRef removeMember?: TeamMemberRef updateMemberRole?: TeamMemberRef } diff --git a/client/web/src/cody/team/CodyManageTeamPage.tsx b/client/web/src/cody/team/CodyManageTeamPage.tsx index 86f893c0598f2..e6b77ca68d267 100644 --- a/client/web/src/cody/team/CodyManageTeamPage.tsx +++ b/client/web/src/cody/team/CodyManageTeamPage.tsx @@ -132,20 +132,22 @@ const AuthenticatedCodyManageTeamPage: React.FunctionComponent )} - {isAdmin && !!remainingInviteCount && ( + {isAdmin && !!remainingInviteCount && !!subscriptionSummaryQueryResult.data && ( )} - + {!!subscriptionSummaryQueryResult.data && ( + + )} ) diff --git a/client/web/src/cody/team/InviteUsers.tsx b/client/web/src/cody/team/InviteUsers.tsx index a5abbfb74e59b..90a83981f4f95 100644 --- a/client/web/src/cody/team/InviteUsers.tsx +++ b/client/web/src/cody/team/InviteUsers.tsx @@ -10,7 +10,7 @@ import { useSendInvite } from '../management/api/react-query/invites' import { isValidEmailAddress } from '../util' interface InviteUsersProps extends TelemetryV2Props { - teamId: string | undefined + teamId: string remainingInviteCount: number } diff --git a/client/web/src/cody/team/TeamMemberList.tsx b/client/web/src/cody/team/TeamMemberList.tsx index cf409fb76b674..e835ad91997c8 100644 --- a/client/web/src/cody/team/TeamMemberList.tsx +++ b/client/web/src/cody/team/TeamMemberList.tsx @@ -8,13 +8,14 @@ import { H2, Text, Badge, Link, ButtonLink } from '@sourcegraph/wildcard' import { CodyAlert } from '../components/CodyAlert' import { CodyContainer } from '../components/CodyContainer' +import { useCancelInvite, useResendInvite } from '../management/api/react-query/invites' +import { useUpdateTeamMember } from '../management/api/react-query/teams' import type { TeamMember, TeamInvite } from '../management/api/types' -import { requestSSC } from '../util' import styles from './TeamMemberList.module.scss' interface TeamMemberListProps extends TelemetryV2Props { - teamId: string | null + teamId: string teamMembers: TeamMember[] invites: Omit[] isAdmin: boolean @@ -41,6 +42,9 @@ export const TeamMemberList: FunctionComponent = ({ }) => { const [loading, setLoading] = useState(false) const [actionResult, setActionResult] = useState<{ message: string; isError: boolean } | null>(null) + const updateTeamMemberMutation = useUpdateTeamMember() + const cancelInviteMutation = useCancelInvite() + const resendInviteMutation = useResendInvite() const updateRole = useCallback( async (accountId: string, newRole: 'member' | 'admin'): Promise => { if (!loading) { @@ -51,7 +55,7 @@ export const TeamMemberList: FunctionComponent = ({ }) try { - const response = await requestSSC('/team/current/members', 'PATCH', { + const response = await updateTeamMemberMutation.mutateAsync.call(undefined, { updateMemberRole: { accountId, teamRole: newRole }, }) if (!response.ok) { @@ -73,7 +77,7 @@ export const TeamMemberList: FunctionComponent = ({ } } }, - [loading, telemetryRecorder, teamId] + [loading, telemetryRecorder, teamId, updateTeamMemberMutation.mutateAsync] ) const revokeInvite = useCallback( @@ -83,7 +87,7 @@ export const TeamMemberList: FunctionComponent = ({ setLoading(true) telemetryRecorder.recordEvent('cody.team.revokeInvite', 'click', { privateMetadata: { teamId } }) - const response = await requestSSC(`/team/current/invites/${inviteId}/cancel`, 'POST') + const response = await cancelInviteMutation.mutateAsync.call(undefined, { teamId, inviteId }) if (!response.ok) { setLoading(false) setActionResult({ @@ -96,7 +100,7 @@ export const TeamMemberList: FunctionComponent = ({ } } }, - [loading, telemetryRecorder, teamId] + [loading, telemetryRecorder, teamId, cancelInviteMutation.mutateAsync] ) const resendInvite = useCallback( @@ -106,7 +110,7 @@ export const TeamMemberList: FunctionComponent = ({ setLoading(true) telemetryRecorder.recordEvent('cody.team.resendInvite', 'click', { privateMetadata: { teamId } }) - const response = await requestSSC(`/team/current/invites/${inviteId}/resend`, 'POST') + const response = await resendInviteMutation.mutateAsync.call(undefined, { inviteId }) if (!response.ok) { setLoading(false) setActionResult({ @@ -121,7 +125,7 @@ export const TeamMemberList: FunctionComponent = ({ telemetryRecorder.recordEvent('cody.team.resendInvite', 'click', { privateMetadata: { teamId } }) }, - [loading, telemetryRecorder, teamId] + [loading, telemetryRecorder, teamId, resendInviteMutation.mutateAsync] ) const removeMember = useCallback( @@ -130,7 +134,7 @@ export const TeamMemberList: FunctionComponent = ({ setLoading(true) telemetryRecorder.recordEvent('cody.team.removeMember', 'click', { privateMetadata: { teamId } }) - const response = await requestSSC('/team/current/members', 'PATCH', { + const response = await updateTeamMemberMutation.mutateAsync.call(undefined, { removeMember: { accountId, teamRole: 'member' }, }) if (!response.ok) { @@ -145,7 +149,7 @@ export const TeamMemberList: FunctionComponent = ({ } } }, - [telemetryRecorder, teamId, loading] + [loading, telemetryRecorder, teamId, updateTeamMemberMutation.mutateAsync] ) const adminCount = useMemo(() => teamMembers?.filter(member => member.role === 'admin').length ?? 0, [teamMembers]) diff --git a/client/web/src/cody/util.ts b/client/web/src/cody/util.ts index 7187c016451b5..6a1e135af9e3f 100644 --- a/client/web/src/cody/util.ts +++ b/client/web/src/cody/util.ts @@ -43,28 +43,3 @@ export function isValidEmailAddress(emailAddress: string): boolean { * and keep in mind that the backend validation has the final say, validation in the web app is only for UX improvement. */ const emailRegex = /^[^@]+@[^@]+\.[^@]+$/ - -/** - * So the request is kinda made to two backends. Dotcom's `.api/ssc/proxy` endpoint - * exchanges the Sourcegraph session credentials for a SAMS access token - * and then proxy the request to the SSC backend. - * @param sscUrl The SSC API URL to call. Example: "/checkout/session". - * @param method E.g. "POST". - * @param params The body to send to the SSC API. Will be JSON-encoded. - * In the case of GET and HEAD, use the query string instead. - */ -export function requestSSC(sscUrl: string, method: string, params?: object): Promise { - // /.api/ssc/proxy endpoint exchanges the Sourcegraph session credentials for a SAMS access token. - // And then proxy the request onto the SSC backend, which will actually create the - // checkout session. - return fetch(`/.api/ssc/proxy${sscUrl}`, { - // Pass along the "sgs" session cookie to identify the caller. - credentials: 'same-origin', - headers: { - ...window.context.xhrHeaders, - 'Content-Type': 'application/json', - }, - method, - ...(!['GET', 'HEAD'].includes(method) && params ? { body: JSON.stringify(params) } : null), - }) -} From 624f8fc02e481410eb45ed04915db094395e7a0d Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Mon, 17 Jun 2024 09:24:44 +0200 Subject: [PATCH 06/24] Remove unneeded checks --- client/web/src/cody/management/api/react-query/invites.ts | 2 +- .../src/cody/management/api/react-query/subscriptions.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/web/src/cody/management/api/react-query/invites.ts b/client/web/src/cody/management/api/react-query/invites.ts index 87ba9baf5e84f..115aef4d1adb1 100644 --- a/client/web/src/cody/management/api/react-query/invites.ts +++ b/client/web/src/cody/management/api/react-query/invites.ts @@ -32,7 +32,7 @@ export const useTeamInvites = (): UseQueryResult { const response = await callCodyProApi(Client.getTeamInvites()) - return response.ok ? response.json() : undefined + return response.json() }, }) diff --git a/client/web/src/cody/management/api/react-query/subscriptions.ts b/client/web/src/cody/management/api/react-query/subscriptions.ts index d14231c887427..ab4c34878abaa 100644 --- a/client/web/src/cody/management/api/react-query/subscriptions.ts +++ b/client/web/src/cody/management/api/react-query/subscriptions.ts @@ -27,7 +27,7 @@ export const useCurrentSubscription = (): UseQueryResult { const response = await callCodyProApi(Client.getCurrentSubscription()) - return response?.json() + return response.json() }, }) @@ -36,7 +36,7 @@ export const useSubscriptionSummary = (): UseQueryResult { const response = await callCodyProApi(Client.getCurrentSubscriptionSummary()) - return response?.json() + return response.json() }, }) @@ -76,7 +76,7 @@ export const useUpdateCurrentSubscription = (): UseMutationResult< return useMutation({ mutationFn: async requestBody => { const response = await callCodyProApi(Client.updateCurrentSubscription(requestBody)) - return response?.json() + return response.json() }, onSuccess: data => { // We get updated subscription data in response - no need to refetch subscription. From 02c6d00b199bd8b7e4a990dc510820df320fc2d4 Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Wed, 19 Jun 2024 11:36:46 +0200 Subject: [PATCH 07/24] Use TeamInvite return value --- client/web/src/cody/management/api/client.ts | 2 +- .../src/cody/management/api/react-query/invites.ts | 6 +++--- client/web/src/cody/team/InviteUsers.tsx | 14 ++------------ 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/client/web/src/cody/management/api/client.ts b/client/web/src/cody/management/api/client.ts index 51d9c8c7b786b..4c5f6f122b748 100644 --- a/client/web/src/cody/management/api/client.ts +++ b/client/web/src/cody/management/api/client.ts @@ -66,7 +66,7 @@ export module Client { return { method: 'GET', urlSuffix: '/team/current/invites' } } - export function sendInvite(requestBody: types.CreateTeamInviteRequest): Call { + export function sendInvite(requestBody: types.CreateTeamInviteRequest): Call { return { method: 'POST', urlSuffix: '/team/current/invites', requestBody } } diff --git a/client/web/src/cody/management/api/react-query/invites.ts b/client/web/src/cody/management/api/react-query/invites.ts index 115aef4d1adb1..878a632f70cff 100644 --- a/client/web/src/cody/management/api/react-query/invites.ts +++ b/client/web/src/cody/management/api/react-query/invites.ts @@ -7,7 +7,7 @@ import { } from '@tanstack/react-query' import { Client } from '../client' -import type { TeamInvite, ListTeamInvitesResponse, CreateTeamInviteRequest } from '../teamInvites' +import type { TeamInvite, ListTeamInvitesResponse, CreateTeamInviteRequest } from '../types' import { callCodyProApi } from './callCodyProApi' import { queryKeys } from './queryKeys' @@ -36,10 +36,10 @@ export const useTeamInvites = (): UseQueryResult => { +export const useSendInvite = (): UseMutationResult => { const queryClient = useQueryClient() return useMutation({ - mutationFn: async requestBody => callCodyProApi(Client.sendInvite(requestBody)), + mutationFn: async requestBody => (await callCodyProApi(Client.sendInvite(requestBody))).json(), onSuccess: () => queryClient.invalidateQueries({ queryKey: queryKeys.invites.teamInvites() }), }) } diff --git a/client/web/src/cody/team/InviteUsers.tsx b/client/web/src/cody/team/InviteUsers.tsx index 90a83981f4f95..b44ed3bcde1e4 100644 --- a/client/web/src/cody/team/InviteUsers.tsx +++ b/client/web/src/cody/team/InviteUsers.tsx @@ -43,22 +43,12 @@ export const InviteUsers: React.FunctionComponent = ({ setInvitesSendingStatus('sending') try { - const responses = await Promise.all( + await Promise.all( emailAddresses.map(emailAddress => sendInviteMutation.mutateAsync.call(undefined, { email: emailAddress, role: 'member' }) ) ) - if (responses.some(response => response.status !== 200)) { - const responsesText = await Promise.all(responses.map(response => response.text())) - setInvitesSendingStatus('error') - setInvitesSendingErrorMessage(`Error sending invites: ${responsesText.join(', ')}`) - telemetryRecorder.recordEvent('cody.team.sendInvites', 'error', { - metadata: { count: emailAddresses.length, softError: 1 }, - privateMetadata: { teamId, emailAddresses }, - }) - - return - } + setInvitesSendingStatus('success') setInvitesSentCount(emailAddresses.length) telemetryRecorder.recordEvent('cody.team.sendInvites', 'success', { From 6a03fe66342bf023647b61f3e451da8ec3385752 Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Wed, 19 Jun 2024 12:03:14 +0200 Subject: [PATCH 08/24] Fix conflict resolution artifact, I think --- client/web/src/cody/management/CodyManagementPage.tsx | 6 +++--- client/web/src/cody/management/api/react-query/invites.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/web/src/cody/management/CodyManagementPage.tsx b/client/web/src/cody/management/CodyManagementPage.tsx index 5dca0194f32b7..904be51b50dcc 100644 --- a/client/web/src/cody/management/CodyManagementPage.tsx +++ b/client/web/src/cody/management/CodyManagementPage.tsx @@ -25,13 +25,13 @@ import { AcceptInviteBanner } from '../invites/AcceptInviteBanner' import { isCodyEnabled } from '../isCodyEnabled' import { CodyOnboarding, type IEditor } from '../onboarding/CodyOnboarding' import { USER_CODY_PLAN, USER_CODY_USAGE } from '../subscription/queries' -import { useCodySubscriptionSummaryData } from '../subscription/subscriptionSummary' import { getManageSubscriptionPageURL } from '../util' import { SubscriptionStats } from './SubscriptionStats' import { UseCodyInEditorSection } from './UseCodyInEditorSection' import styles from './CodyManagementPage.module.scss' +import { useSubscriptionSummary } from './api/react-query/subscriptions' interface CodyManagementPageProps extends TelemetryV2Props { authenticatedUser: AuthenticatedUser | null @@ -74,8 +74,8 @@ export const CodyManagementPage: React.FunctionComponent(null) const [selectedEditorStep, setSelectedEditorStep] = React.useState(null) diff --git a/client/web/src/cody/management/api/react-query/invites.ts b/client/web/src/cody/management/api/react-query/invites.ts index 878a632f70cff..8fe3bb6f296f4 100644 --- a/client/web/src/cody/management/api/react-query/invites.ts +++ b/client/web/src/cody/management/api/react-query/invites.ts @@ -65,7 +65,7 @@ export const useAcceptInvite = (): UseMutationResult => { +export const useCancelInvite = (): UseMutationResult => { const queryClient = useQueryClient() return useMutation({ mutationFn: async ({ teamId, inviteId }) => callCodyProApi(Client.cancelInvite(teamId, inviteId)), From eea0335454c42caf2f3ac6c1663bc15fd873bc7b Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Wed, 19 Jun 2024 12:03:26 +0200 Subject: [PATCH 09/24] Use built-in error --- client/web/src/cody/team/TeamMemberList.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/web/src/cody/team/TeamMemberList.tsx b/client/web/src/cody/team/TeamMemberList.tsx index e835ad91997c8..829ca2b15a52b 100644 --- a/client/web/src/cody/team/TeamMemberList.tsx +++ b/client/web/src/cody/team/TeamMemberList.tsx @@ -87,16 +87,16 @@ export const TeamMemberList: FunctionComponent = ({ setLoading(true) telemetryRecorder.recordEvent('cody.team.revokeInvite', 'click', { privateMetadata: { teamId } }) - const response = await cancelInviteMutation.mutateAsync.call(undefined, { teamId, inviteId }) - if (!response.ok) { + try { + await cancelInviteMutation.mutateAsync.call(undefined, { teamId, inviteId }) + setLoading(false) + setActionResult({ message: 'Invite revoked.', isError: false }) + } catch (error) { setLoading(false) setActionResult({ - message: `We couldn't revoke the invite (${response.status}). Please try again later.`, + message: `We couldn't revoke the invite. The error was: "${error}". Please try again later.`, isError: true, }) - } else { - setLoading(false) - setActionResult({ message: 'Invite revoked.', isError: false }) } } }, From 0639f19b085d3d371da64a883b88f996cedf3835 Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Wed, 19 Jun 2024 13:18:54 +0200 Subject: [PATCH 10/24] Save a request. --- .../web/src/cody/management/CodyManagementPage.tsx | 4 ++-- .../src/cody/management/api/react-query/teams.ts | 14 ++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/client/web/src/cody/management/CodyManagementPage.tsx b/client/web/src/cody/management/CodyManagementPage.tsx index 904be51b50dcc..fe0280e391240 100644 --- a/client/web/src/cody/management/CodyManagementPage.tsx +++ b/client/web/src/cody/management/CodyManagementPage.tsx @@ -27,11 +27,11 @@ import { CodyOnboarding, type IEditor } from '../onboarding/CodyOnboarding' import { USER_CODY_PLAN, USER_CODY_USAGE } from '../subscription/queries' import { getManageSubscriptionPageURL } from '../util' +import { useSubscriptionSummary } from './api/react-query/subscriptions' import { SubscriptionStats } from './SubscriptionStats' import { UseCodyInEditorSection } from './UseCodyInEditorSection' import styles from './CodyManagementPage.module.scss' -import { useSubscriptionSummary } from './api/react-query/subscriptions' interface CodyManagementPageProps extends TelemetryV2Props { authenticatedUser: AuthenticatedUser | null @@ -74,7 +74,7 @@ export const CodyManagementPage: React.FunctionComponent(null) diff --git a/client/web/src/cody/management/api/react-query/teams.ts b/client/web/src/cody/management/api/react-query/teams.ts index e9df824ea99a0..82a23d0e6adf8 100644 --- a/client/web/src/cody/management/api/react-query/teams.ts +++ b/client/web/src/cody/management/api/react-query/teams.ts @@ -7,7 +7,7 @@ import { } from '@tanstack/react-query' import { Client } from '../client' -import type { ListTeamMembersResponse, UpdateTeamMembersRequest } from '../types' +import type { ListTeamMembersResponse, UpdateTeamMembersRequest, TeamMember } from '../types' import { callCodyProApi } from './callCodyProApi' import { queryKeys } from './queryKeys' @@ -21,10 +21,16 @@ export const useTeamMembers = (): UseQueryResult => { +export const useUpdateTeamMember = (): UseMutationResult => { const queryClient = useQueryClient() return useMutation({ - mutationFn: async requestBody => callCodyProApi(Client.updateTeamMember(requestBody)), - onSettled: () => queryClient.invalidateQueries({ queryKey: queryKeys.teams.teamMembers() }), + mutationFn: async requestBody => { + const response = await callCodyProApi(Client.updateTeamMember(requestBody)) + return response?.json() + }, + onSuccess: (data: TeamMember[]) => { + queryClient.setQueryData(queryKeys.teams.teamMembers(), data) + return queryClient.invalidateQueries({ queryKey: queryKeys.teams.teamMembers() }) + }, }) } From a6e8977cee4c3edca6dd71d4ee9887919b649885 Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Wed, 19 Jun 2024 13:28:45 +0200 Subject: [PATCH 11/24] Save a request by updating rather than invalidating --- client/web/src/cody/management/api/react-query/invites.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/web/src/cody/management/api/react-query/invites.ts b/client/web/src/cody/management/api/react-query/invites.ts index 8fe3bb6f296f4..3a259b7741308 100644 --- a/client/web/src/cody/management/api/react-query/invites.ts +++ b/client/web/src/cody/management/api/react-query/invites.ts @@ -69,6 +69,9 @@ export const useCancelInvite = (): UseMutationResult callCodyProApi(Client.cancelInvite(teamId, inviteId)), - onSuccess: () => queryClient.invalidateQueries({ queryKey: queryKeys.invites.all }), + onSuccess: (_, { teamId, inviteId }) => + queryClient.setQueryData(queryKeys.invites.teamInvites(), (prevInvites: TeamInvite[]) => + prevInvites.filter(invite => invite.id !== inviteId) + ), }) } From e8350a68b2463f3609f9eb6135175aea558f026a Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Wed, 19 Jun 2024 13:39:45 +0200 Subject: [PATCH 12/24] Update again + the component, too --- .../cody/management/api/react-query/teams.ts | 1 - client/web/src/cody/team/TeamMemberList.tsx | 30 +++++++------------ 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/client/web/src/cody/management/api/react-query/teams.ts b/client/web/src/cody/management/api/react-query/teams.ts index 82a23d0e6adf8..09171c4a8fa68 100644 --- a/client/web/src/cody/management/api/react-query/teams.ts +++ b/client/web/src/cody/management/api/react-query/teams.ts @@ -30,7 +30,6 @@ export const useUpdateTeamMember = (): UseMutationResult { queryClient.setQueryData(queryKeys.teams.teamMembers(), data) - return queryClient.invalidateQueries({ queryKey: queryKeys.teams.teamMembers() }) }, }) } diff --git a/client/web/src/cody/team/TeamMemberList.tsx b/client/web/src/cody/team/TeamMemberList.tsx index 829ca2b15a52b..e672d6b70431b 100644 --- a/client/web/src/cody/team/TeamMemberList.tsx +++ b/client/web/src/cody/team/TeamMemberList.tsx @@ -55,19 +55,11 @@ export const TeamMemberList: FunctionComponent = ({ }) try { - const response = await updateTeamMemberMutation.mutateAsync.call(undefined, { + await updateTeamMemberMutation.mutateAsync.call(undefined, { updateMemberRole: { accountId, teamRole: newRole }, }) - if (!response.ok) { - setLoading(false) - setActionResult({ - message: `We couldn't modify the user's role (${response.status}). Please try again later.`, - isError: true, - }) - } else { - setLoading(false) - setActionResult({ message: 'Team role updated.', isError: false }) - } + setLoading(false) + setActionResult({ message: 'Team role updated.', isError: false }) } catch (error) { setLoading(false) setActionResult({ @@ -134,18 +126,18 @@ export const TeamMemberList: FunctionComponent = ({ setLoading(true) telemetryRecorder.recordEvent('cody.team.removeMember', 'click', { privateMetadata: { teamId } }) - const response = await updateTeamMemberMutation.mutateAsync.call(undefined, { - removeMember: { accountId, teamRole: 'member' }, - }) - if (!response.ok) { + try { + await updateTeamMemberMutation.mutateAsync.call(undefined, { + removeMember: { accountId, teamRole: 'member' }, + }) + setLoading(false) + setActionResult({ message: 'Team member removed.', isError: false }) + } catch (error) { setLoading(false) setActionResult({ - message: `We couldn't remove the team member. (${response.status}). Please try again later.`, + message: `We couldn't remove the team member. (${error}). Please try again later.`, isError: true, }) - } else { - setLoading(false) - setActionResult({ message: 'Team member removed.', isError: false }) } } }, From 5003503837c4b4efd4fc008286cfa7f7035adc4d Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Wed, 19 Jun 2024 13:41:48 +0200 Subject: [PATCH 13/24] Resend invite fix --- client/web/src/cody/management/api/client.ts | 2 +- .../src/cody/management/api/react-query/invites.ts | 4 ++-- client/web/src/cody/team/TeamMemberList.tsx | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/client/web/src/cody/management/api/client.ts b/client/web/src/cody/management/api/client.ts index 4c5f6f122b748..df63de0e59d6a 100644 --- a/client/web/src/cody/management/api/client.ts +++ b/client/web/src/cody/management/api/client.ts @@ -70,7 +70,7 @@ export module Client { return { method: 'POST', urlSuffix: '/team/current/invites', requestBody } } - export function resendInvite(inviteId: string): Call { + export function resendInvite(inviteId: string): Call { return { method: 'POST', urlSuffix: `/team/current/invites/${inviteId}/resend` } } diff --git a/client/web/src/cody/management/api/react-query/invites.ts b/client/web/src/cody/management/api/react-query/invites.ts index 3a259b7741308..b3d0d869a1f02 100644 --- a/client/web/src/cody/management/api/react-query/invites.ts +++ b/client/web/src/cody/management/api/react-query/invites.ts @@ -44,10 +44,10 @@ export const useSendInvite = (): UseMutationResult => { +export const useResendInvite = (): UseMutationResult => { const queryClient = useQueryClient() return useMutation({ - mutationFn: async ({ inviteId }) => callCodyProApi(Client.resendInvite(inviteId)), + mutationFn: async ({ inviteId }) => (await callCodyProApi(Client.resendInvite(inviteId))).json(), onSuccess: () => queryClient.invalidateQueries({ queryKey: queryKeys.invites.teamInvites() }), }) } diff --git a/client/web/src/cody/team/TeamMemberList.tsx b/client/web/src/cody/team/TeamMemberList.tsx index e672d6b70431b..5501685d24b30 100644 --- a/client/web/src/cody/team/TeamMemberList.tsx +++ b/client/web/src/cody/team/TeamMemberList.tsx @@ -102,16 +102,16 @@ export const TeamMemberList: FunctionComponent = ({ setLoading(true) telemetryRecorder.recordEvent('cody.team.resendInvite', 'click', { privateMetadata: { teamId } }) - const response = await resendInviteMutation.mutateAsync.call(undefined, { inviteId }) - if (!response.ok) { + try { + await resendInviteMutation.mutateAsync.call(undefined, { inviteId }) + setLoading(false) + setActionResult({ message: 'Invite resent.', isError: false }) + } catch (error) { setLoading(false) setActionResult({ - message: `We couldn't resend the invite (${response.status}). Please try again later.`, + message: `We couldn't resend the invite (${error}). Please try again later.`, isError: true, }) - } else { - setLoading(false) - setActionResult({ message: 'Invite resent.', isError: false }) } } From e7ca64d9ee4b33236d8eff49436d1b0c113927c1 Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Wed, 19 Jun 2024 15:32:32 +0200 Subject: [PATCH 14/24] Remove hallucinated return type --- client/web/src/cody/management/api/client.ts | 2 +- .../src/cody/management/api/react-query/teams.ts | 13 ++++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/client/web/src/cody/management/api/client.ts b/client/web/src/cody/management/api/client.ts index df63de0e59d6a..bb11b4ed288d6 100644 --- a/client/web/src/cody/management/api/client.ts +++ b/client/web/src/cody/management/api/client.ts @@ -52,7 +52,7 @@ export module Client { return { method: 'GET', urlSuffix: '/team/current/members' } } - export function updateTeamMember(requestBody: types.UpdateTeamMembersRequest): Call { + export function updateTeamMember(requestBody: types.UpdateTeamMembersRequest): Call { return { method: 'PATCH', urlSuffix: '/team/current/members', requestBody } } diff --git a/client/web/src/cody/management/api/react-query/teams.ts b/client/web/src/cody/management/api/react-query/teams.ts index 09171c4a8fa68..17dd84b017a03 100644 --- a/client/web/src/cody/management/api/react-query/teams.ts +++ b/client/web/src/cody/management/api/react-query/teams.ts @@ -7,7 +7,7 @@ import { } from '@tanstack/react-query' import { Client } from '../client' -import type { ListTeamMembersResponse, UpdateTeamMembersRequest, TeamMember } from '../types' +import type { ListTeamMembersResponse, UpdateTeamMembersRequest } from '../types' import { callCodyProApi } from './callCodyProApi' import { queryKeys } from './queryKeys' @@ -21,15 +21,10 @@ export const useTeamMembers = (): UseQueryResult => { +export const useUpdateTeamMember = (): UseMutationResult => { const queryClient = useQueryClient() return useMutation({ - mutationFn: async requestBody => { - const response = await callCodyProApi(Client.updateTeamMember(requestBody)) - return response?.json() - }, - onSuccess: (data: TeamMember[]) => { - queryClient.setQueryData(queryKeys.teams.teamMembers(), data) - }, + mutationFn: async requestBody => callCodyProApi(Client.updateTeamMember(requestBody)), + onSuccess: () => queryClient.invalidateQueries({ queryKey: queryKeys.teams.teamMembers() }), }) } From a147874051372d4a6df83464110d13da409ce871 Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Wed, 19 Jun 2024 15:39:20 +0200 Subject: [PATCH 15/24] Fix missing isLoaded check --- client/web/src/cody/team/CodyManageTeamPage.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/web/src/cody/team/CodyManageTeamPage.tsx b/client/web/src/cody/team/CodyManageTeamPage.tsx index e6b77ca68d267..75bb716f08f93 100644 --- a/client/web/src/cody/team/CodyManageTeamPage.tsx +++ b/client/web/src/cody/team/CodyManageTeamPage.tsx @@ -39,7 +39,6 @@ const AuthenticatedCodyManageTeamPage: React.FunctionComponent { - if (!isPro) { + if (subscriptionQueryResult.data && subscriptionQueryResult.data.subscriptionStatus !== 'canceled') { navigate('/cody/subscription') } - }, [isPro, navigate]) + }, [navigate, subscriptionQueryResult.data]) const remainingInviteCount = useMemo(() => { const memberCount = teamMembers?.length ?? 0 From a92c844b5549822e44e307c1c7f58143fa3fa281 Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Wed, 19 Jun 2024 15:43:12 +0200 Subject: [PATCH 16/24] Simplify error check --- client/web/src/cody/team/CodyManageTeamPage.tsx | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/client/web/src/cody/team/CodyManageTeamPage.tsx b/client/web/src/cody/team/CodyManageTeamPage.tsx index 75bb716f08f93..fe8a63b925a01 100644 --- a/client/web/src/cody/team/CodyManageTeamPage.tsx +++ b/client/web/src/cody/team/CodyManageTeamPage.tsx @@ -108,17 +108,12 @@ const AuthenticatedCodyManageTeamPage: React.FunctionComponent - {subscriptionQueryResult.isError || - subscriptionSummaryQueryResult.isError || - teamMembersQueryResult.isError || - teamInvitesQueryResult.isError ? ( + {errorMessage ? (

We couldn't load team data this time. Please try a bit later.

- {!!errorMessage && ( - - {errorMessage} - - )} + + {errorMessage} +
) : null} From 670d25caf798fe1c00f264e700830c996eb22a6d Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Wed, 19 Jun 2024 15:54:02 +0200 Subject: [PATCH 17/24] Simplify loading state handling --- client/web/src/cody/team/TeamMemberList.tsx | 131 +++++++++----------- 1 file changed, 60 insertions(+), 71 deletions(-) diff --git a/client/web/src/cody/team/TeamMemberList.tsx b/client/web/src/cody/team/TeamMemberList.tsx index 5501685d24b30..eb4344e58a2cb 100644 --- a/client/web/src/cody/team/TeamMemberList.tsx +++ b/client/web/src/cody/team/TeamMemberList.tsx @@ -40,108 +40,97 @@ export const TeamMemberList: FunctionComponent = ({ isAdmin, telemetryRecorder, }) => { - const [loading, setLoading] = useState(false) const [actionResult, setActionResult] = useState<{ message: string; isError: boolean } | null>(null) const updateTeamMemberMutation = useUpdateTeamMember() const cancelInviteMutation = useCancelInvite() const resendInviteMutation = useResendInvite() + const isLoading = updateTeamMemberMutation.status === 'pending' || cancelInviteMutation.status === 'pending' || resendInviteMutation.status === 'pending' + const updateRole = useCallback( async (accountId: string, newRole: 'member' | 'admin'): Promise => { - if (!loading) { - // Avoids sending multiple requests at once - setLoading(true) - telemetryRecorder.recordEvent('cody.team.revokeAdmin', 'click', { - privateMetadata: { teamId, accountId }, - }) + if (isLoading) { + return + } - try { - await updateTeamMemberMutation.mutateAsync.call(undefined, { - updateMemberRole: { accountId, teamRole: newRole }, - }) - setLoading(false) - setActionResult({ message: 'Team role updated.', isError: false }) - } catch (error) { - setLoading(false) - setActionResult({ - message: `We couldn't modify the user's role. The error was: "${error}". Please try again later.`, - isError: true, - }) - } + telemetryRecorder.recordEvent('cody.team.revokeAdmin', 'click', { + privateMetadata: { teamId, accountId }, + }) + try { + await updateTeamMemberMutation.mutateAsync.call(undefined, { + updateMemberRole: { accountId, teamRole: newRole }, + }) + setActionResult({ message: 'Team role updated.', isError: false }) + } catch (error) { + setActionResult({ + message: `We couldn't modify the user's role. The error was: "${error}". Please try again later.`, + isError: true, + }) } }, - [loading, telemetryRecorder, teamId, updateTeamMemberMutation.mutateAsync] + [isLoading, updateTeamMemberMutation.mutateAsync, telemetryRecorder, teamId] ) const revokeInvite = useCallback( async (inviteId: string): Promise => { - if (!loading) { - // Avoids sending multiple requests at once - setLoading(true) - telemetryRecorder.recordEvent('cody.team.revokeInvite', 'click', { privateMetadata: { teamId } }) - - try { - await cancelInviteMutation.mutateAsync.call(undefined, { teamId, inviteId }) - setLoading(false) - setActionResult({ message: 'Invite revoked.', isError: false }) - } catch (error) { - setLoading(false) - setActionResult({ - message: `We couldn't revoke the invite. The error was: "${error}". Please try again later.`, - isError: true, - }) - } + if (isLoading) { + return + } + telemetryRecorder.recordEvent('cody.team.revokeInvite', 'click', { privateMetadata: { teamId } }) + try { + await cancelInviteMutation.mutateAsync.call(undefined, { teamId, inviteId }) + setActionResult({ message: 'Invite revoked.', isError: false }) + } catch (error) { + setActionResult({ + message: `We couldn't revoke the invite. The error was: "${error}". Please try again later.`, + isError: true, + }) } }, - [loading, telemetryRecorder, teamId, cancelInviteMutation.mutateAsync] + [isLoading, cancelInviteMutation.mutateAsync, telemetryRecorder, teamId] ) const resendInvite = useCallback( async (inviteId: string): Promise => { - if (!loading) { - // Avoids sending multiple requests at once - setLoading(true) - telemetryRecorder.recordEvent('cody.team.resendInvite', 'click', { privateMetadata: { teamId } }) + if (isLoading) { + return + } + telemetryRecorder.recordEvent('cody.team.resendInvite', 'click', { privateMetadata: { teamId } }) - try { - await resendInviteMutation.mutateAsync.call(undefined, { inviteId }) - setLoading(false) - setActionResult({ message: 'Invite resent.', isError: false }) - } catch (error) { - setLoading(false) - setActionResult({ - message: `We couldn't resend the invite (${error}). Please try again later.`, - isError: true, - }) - } + try { + await resendInviteMutation.mutateAsync.call(undefined, { inviteId }) + setActionResult({ message: 'Invite resent.', isError: false }) + } catch (error) { + setActionResult({ + message: `We couldn't resend the invite (${error}). Please try again later.`, + isError: true, + }) } telemetryRecorder.recordEvent('cody.team.resendInvite', 'click', { privateMetadata: { teamId } }) }, - [loading, telemetryRecorder, teamId, resendInviteMutation.mutateAsync] + [isLoading, resendInviteMutation.mutateAsync, telemetryRecorder, teamId] ) const removeMember = useCallback( async (accountId: string): Promise => { - if (!loading) { - setLoading(true) - telemetryRecorder.recordEvent('cody.team.removeMember', 'click', { privateMetadata: { teamId } }) + if (isLoading) { + return + } + telemetryRecorder.recordEvent('cody.team.removeMember', 'click', { privateMetadata: { teamId } }) - try { - await updateTeamMemberMutation.mutateAsync.call(undefined, { - removeMember: { accountId, teamRole: 'member' }, - }) - setLoading(false) - setActionResult({ message: 'Team member removed.', isError: false }) - } catch (error) { - setLoading(false) - setActionResult({ - message: `We couldn't remove the team member. (${error}). Please try again later.`, - isError: true, - }) - } + try { + await updateTeamMemberMutation.mutateAsync.call(undefined, { + removeMember: { accountId, teamRole: 'member' }, + }) + setActionResult({ message: 'Team member removed.', isError: false }) + } catch (error) { + setActionResult({ + message: `We couldn't remove the team member. (${error}). Please try again later.`, + isError: true, + }) } }, - [loading, telemetryRecorder, teamId, updateTeamMemberMutation.mutateAsync] + [isLoading, updateTeamMemberMutation.mutateAsync, telemetryRecorder, teamId] ) const adminCount = useMemo(() => teamMembers?.filter(member => member.role === 'admin').length ?? 0, [teamMembers]) From f0080498269042e39b16c540d7fea007ad870aa5 Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Wed, 19 Jun 2024 16:07:36 +0200 Subject: [PATCH 18/24] Remove invitesSentCount --- client/web/src/cody/team/InviteUsers.tsx | 79 ++++++++++-------------- 1 file changed, 31 insertions(+), 48 deletions(-) diff --git a/client/web/src/cody/team/InviteUsers.tsx b/client/web/src/cody/team/InviteUsers.tsx index b44ed3bcde1e4..894b03cfd874c 100644 --- a/client/web/src/cody/team/InviteUsers.tsx +++ b/client/web/src/cody/team/InviteUsers.tsx @@ -20,20 +20,35 @@ export const InviteUsers: React.FunctionComponent = ({ telemetryRecorder, }) => { const [emailAddressesString, setEmailAddressesString] = useState('') + const emailAddresses = emailAddressesString.split(',').map(email => email.trim()) const [emailAddressErrorMessage, setEmailAddressErrorMessage] = useState(null) - const [invitesSendingStatus, setInvitesSendingStatus] = useState<'idle' | 'sending' | 'success' | 'error'>('idle') - const [invitesSentCount, setInvitesSentCount] = useState(0) - const [invitesSendingErrorMessage, setInvitesSendingErrorMessage] = useState(null) const sendInviteMutation = useSendInvite() + const verifyEmailList = useCallback((): Error | void => { + if (emailAddresses.length === 0) { + return new Error('Please enter at least one email address.') + } + + if (emailAddresses.length > remainingInviteCount) { + return new Error( + `${emailAddresses.length} email addresses entered, but you only have ${remainingInviteCount} seats.` + ) + } + + const invalidEmails = emailAddresses.filter(email => !isValidEmailAddress(email)) + + if (invalidEmails.length > 0) { + return new Error( + `Invalid email address${invalidEmails.length > 1 ? 'es' : ''}: ${invalidEmails.join(', ')}` + ) + } + }, [emailAddresses, remainingInviteCount]) + const onSendInvitesClicked = useCallback(async () => { - const { emails: emailAddresses, error: emailParsingError } = parseEmailList( - emailAddressesString, - remainingInviteCount - ) - if (emailParsingError) { - setEmailAddressErrorMessage(emailParsingError) + const emailListError = verifyEmailList() + if (emailListError) { + setEmailAddressErrorMessage(emailListError.message) return } telemetryRecorder.recordEvent('cody.team.sendInvites', 'click', { @@ -41,7 +56,6 @@ export const InviteUsers: React.FunctionComponent = ({ privateMetadata: { teamId, emailAddresses }, }) - setInvitesSendingStatus('sending') try { await Promise.all( emailAddresses.map(emailAddress => @@ -49,39 +63,35 @@ export const InviteUsers: React.FunctionComponent = ({ ) ) - setInvitesSendingStatus('success') - setInvitesSentCount(emailAddresses.length) telemetryRecorder.recordEvent('cody.team.sendInvites', 'success', { metadata: { count: emailAddresses.length }, privateMetadata: { teamId, emailAddresses }, }) } catch (error) { - setInvitesSendingStatus('error') - setInvitesSendingErrorMessage(`Error sending invites: ${error}`) telemetryRecorder.recordEvent('cody.team.sendInvites', 'error', { metadata: { count: emailAddresses.length, softError: 0 }, - privateMetadata: { teamId, emailAddresses }, + privateMetadata: { teamId, emailAddresses, error }, }) } - }, [emailAddressesString, remainingInviteCount, sendInviteMutation.mutateAsync, teamId, telemetryRecorder]) + }, [emailAddresses, sendInviteMutation.mutateAsync, teamId, telemetryRecorder, verifyEmailList]) return ( <> - {invitesSendingStatus === 'success' && ( + {sendInviteMutation.status === 'success' && (

- {invitesSentCount} {pluralize('invite', invitesSentCount)} sent! + {emailAddresses.length} {pluralize('invite', emailAddresses.length)} sent!

Invitees will receive an email from cody@sourcegraph.com.
)} - {invitesSendingStatus === 'error' && ( + {sendInviteMutation.status === 'error' && (

Invites not sent.

- {invitesSendingErrorMessage} + Error sending invites: {sendInviteMutation.error?.message} If you encounter this issue repeatedly, please contact support at{' '} @@ -113,6 +123,7 @@ export const InviteUsers: React.FunctionComponent = ({ onChange={event => { setEmailAddressErrorMessage(null) setEmailAddressesString(event.target.value) + sendInviteMutation.reset() }} isValid={emailAddressErrorMessage ? false : undefined} /> @@ -133,31 +144,3 @@ export const InviteUsers: React.FunctionComponent = ({ ) } - -function parseEmailList( - emailAddressesString: string, - remainingInviteCount: number -): { emails: string[]; error: string | null } { - const emails = emailAddressesString.split(',').map(email => email.trim()) - if (emails.length === 0) { - return { emails, error: 'Please enter at least one email address.' } - } - - if (emails.length > remainingInviteCount) { - return { - emails, - error: `${emails.length} email addresses entered, but you only have ${remainingInviteCount} seats.`, - } - } - - const invalidEmails = emails.filter(email => !isValidEmailAddress(email)) - - if (invalidEmails.length > 0) { - return { - emails, - error: `Invalid email address${invalidEmails.length > 1 ? 'es' : ''}: ${invalidEmails.join(', ')}`, - } - } - - return { emails, error: null } -} From ba7165e420ffd10b2ea3a1f21c793c0d410af1b5 Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Wed, 19 Jun 2024 16:32:59 +0200 Subject: [PATCH 19/24] Specify more specific invite sending messages --- client/web/src/cody/team/InviteUsers.tsx | 36 ++++++++++++++++-------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/client/web/src/cody/team/InviteUsers.tsx b/client/web/src/cody/team/InviteUsers.tsx index 894b03cfd874c..2c273c7678145 100644 --- a/client/web/src/cody/team/InviteUsers.tsx +++ b/client/web/src/cody/team/InviteUsers.tsx @@ -56,23 +56,37 @@ export const InviteUsers: React.FunctionComponent = ({ privateMetadata: { teamId, emailAddresses }, }) - try { - await Promise.all( - emailAddresses.map(emailAddress => - sendInviteMutation.mutateAsync.call(undefined, { email: emailAddress, role: 'member' }) - ) + const results = await Promise.allSettled( + emailAddresses.map(emailAddress => + sendInviteMutation.mutateAsync.call(undefined, { email: emailAddress, role: 'member' }) ) + ) - telemetryRecorder.recordEvent('cody.team.sendInvites', 'success', { - metadata: { count: emailAddresses.length }, - privateMetadata: { teamId, emailAddresses }, - }) - } catch (error) { + const failures = results + .map((result, index) => ({ + emailAddress: emailAddresses[index], + errorMessage: result.status === 'rejected' ? (result.reason as Error).message : null, + })) + .filter(({ errorMessage }) => errorMessage) + if (failures.length) { + const failureList = failures + .map(({ emailAddress, errorMessage }) => `"${emailAddress}": ${errorMessage}`) + .join(', ') + const errorMessage = `We couldn't send${ + failures.length < emailAddresses.length ? ` ${failures.length} of` : '' + } the ${pluralize('invite', emailAddresses.length)}. This is what we got: ${failureList}` telemetryRecorder.recordEvent('cody.team.sendInvites', 'error', { metadata: { count: emailAddresses.length, softError: 0 }, - privateMetadata: { teamId, emailAddresses, error }, + privateMetadata: { teamId, emailAddresses, error: errorMessage }, }) + setEmailAddressErrorMessage(errorMessage) + return } + + telemetryRecorder.recordEvent('cody.team.sendInvites', 'success', { + metadata: { count: emailAddresses.length }, + privateMetadata: { teamId, emailAddresses }, + }) }, [emailAddresses, sendInviteMutation.mutateAsync, teamId, telemetryRecorder, verifyEmailList]) return ( From b2c6825d0a87c6349b9046b62914e0660e097231 Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Wed, 19 Jun 2024 16:33:09 +0200 Subject: [PATCH 20/24] Prettier --- client/web/src/cody/team/TeamMemberList.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/web/src/cody/team/TeamMemberList.tsx b/client/web/src/cody/team/TeamMemberList.tsx index eb4344e58a2cb..f3046ee88315f 100644 --- a/client/web/src/cody/team/TeamMemberList.tsx +++ b/client/web/src/cody/team/TeamMemberList.tsx @@ -44,7 +44,10 @@ export const TeamMemberList: FunctionComponent = ({ const updateTeamMemberMutation = useUpdateTeamMember() const cancelInviteMutation = useCancelInvite() const resendInviteMutation = useResendInvite() - const isLoading = updateTeamMemberMutation.status === 'pending' || cancelInviteMutation.status === 'pending' || resendInviteMutation.status === 'pending' + const isLoading = + updateTeamMemberMutation.status === 'pending' || + cancelInviteMutation.status === 'pending' || + resendInviteMutation.status === 'pending' const updateRole = useCallback( async (accountId: string, newRole: 'member' | 'admin'): Promise => { From b44c9694793504d12ac99fbb85bfa9d395817f7f Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Wed, 19 Jun 2024 16:52:09 +0200 Subject: [PATCH 21/24] Use return value --- client/web/src/cody/management/api/react-query/invites.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/web/src/cody/management/api/react-query/invites.ts b/client/web/src/cody/management/api/react-query/invites.ts index b3d0d869a1f02..070b6d078a881 100644 --- a/client/web/src/cody/management/api/react-query/invites.ts +++ b/client/web/src/cody/management/api/react-query/invites.ts @@ -40,7 +40,12 @@ export const useSendInvite = (): UseMutationResult (await callCodyProApi(Client.sendInvite(requestBody))).json(), - onSuccess: () => queryClient.invalidateQueries({ queryKey: queryKeys.invites.teamInvites() }), + onSuccess: (newInvite: TeamInvite) => { + queryClient.setQueryData(queryKeys.invites.teamInvites(), (prevInvites: TeamInvite[]) => [ + ...prevInvites, + newInvite, + ]) + }, }) } From 595b1444b3447f13c93233cef6f16d6d5392569b Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Wed, 19 Jun 2024 16:57:19 +0200 Subject: [PATCH 22/24] Resolve conflict artifacts --- .../src/cody/management/api/react-query/subscriptions.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/web/src/cody/management/api/react-query/subscriptions.ts b/client/web/src/cody/management/api/react-query/subscriptions.ts index ab4c34878abaa..369da72d4fff1 100644 --- a/client/web/src/cody/management/api/react-query/subscriptions.ts +++ b/client/web/src/cody/management/api/react-query/subscriptions.ts @@ -16,7 +16,7 @@ import type { PreviewCreateTeamRequest, GetSubscriptionInvoicesResponse, ListTeamMembersResponse, - ListTeamInvitesResponse + ListTeamInvitesResponse, } from '../types' import { callCodyProApi } from './callCodyProApi' @@ -51,7 +51,7 @@ export const useSubscriptionInvoices = (): UseQueryResult => useQuery({ - queryKey: queryKeys.teams.currentTeamMembers(), + queryKey: queryKeys.teams.teamMembers(), queryFn: async () => { const response = await callCodyProApi(Client.getCurrentTeamMembers()) return response.ok ? response.json() : undefined @@ -60,9 +60,9 @@ export const useTeamMembers = (): UseQueryResult => useQuery({ - queryKey: queryKeys.invites.currentTeamInvites(), + queryKey: queryKeys.invites.teamInvites(), queryFn: async () => { - const response = await callCodyProApi(Client.getCurrentTeamInvites()) + const response = await callCodyProApi(Client.getTeamInvites()) return response.ok ? response.json() : undefined }, }) From ada728097278877ce88de52a671a541b53e41ffd Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Wed, 19 Jun 2024 17:36:09 +0200 Subject: [PATCH 23/24] Fix button disabling --- client/web/src/cody/team/TeamMemberList.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/client/web/src/cody/team/TeamMemberList.tsx b/client/web/src/cody/team/TeamMemberList.tsx index f3046ee88315f..de89cc5331d1e 100644 --- a/client/web/src/cody/team/TeamMemberList.tsx +++ b/client/web/src/cody/team/TeamMemberList.tsx @@ -4,7 +4,7 @@ import classNames from 'classnames' import { intlFormatDistance } from 'date-fns' import type { TelemetryV2Props } from '@sourcegraph/shared/src/telemetry' -import { H2, Text, Badge, Link, ButtonLink } from '@sourcegraph/wildcard' +import { H2, Text, Badge, Link, ButtonLink, Button } from '@sourcegraph/wildcard' import { CodyAlert } from '../components/CodyAlert' import { CodyContainer } from '../components/CodyContainer' @@ -137,6 +137,7 @@ export const TeamMemberList: FunctionComponent = ({ ) const adminCount = useMemo(() => teamMembers?.filter(member => member.role === 'admin').length ?? 0, [teamMembers]) + console.log(adminCount) if (!teamMembers) { return null @@ -185,14 +186,14 @@ export const TeamMemberList: FunctionComponent = ({ <>
- updateRole(member.accountId, 'member')} className="ml-2" - aria-disabled={adminCount < 2} + disabled={adminCount < 2} > Revoke admin - +
) : ( From ce7339022f4b17f4fe1c5c97753efd448502ff7a Mon Sep 17 00:00:00 2001 From: David Veszelovszki Date: Wed, 19 Jun 2024 17:39:32 +0200 Subject: [PATCH 24/24] Fixes --- client/web/src/cody/management/api/client.ts | 2 +- .../src/cody/management/api/react-query/invites.ts | 14 +++++++------- client/web/src/cody/team/CodyManageTeamPage.tsx | 4 ++-- client/web/src/cody/team/TeamMemberList.tsx | 1 - 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/client/web/src/cody/management/api/client.ts b/client/web/src/cody/management/api/client.ts index bb11b4ed288d6..fc8ed5e2d3d77 100644 --- a/client/web/src/cody/management/api/client.ts +++ b/client/web/src/cody/management/api/client.ts @@ -66,7 +66,7 @@ export module Client { return { method: 'GET', urlSuffix: '/team/current/invites' } } - export function sendInvite(requestBody: types.CreateTeamInviteRequest): Call { + export function sendInvite(requestBody: types.CreateTeamInviteRequest): Call { return { method: 'POST', urlSuffix: '/team/current/invites', requestBody } } diff --git a/client/web/src/cody/management/api/react-query/invites.ts b/client/web/src/cody/management/api/react-query/invites.ts index 070b6d078a881..3917a0a7a2d04 100644 --- a/client/web/src/cody/management/api/react-query/invites.ts +++ b/client/web/src/cody/management/api/react-query/invites.ts @@ -27,21 +27,21 @@ export const useInvite = ({ }, }) -export const useTeamInvites = (): UseQueryResult => +export const useTeamInvites = (): UseQueryResult[] | undefined> => useQuery({ queryKey: queryKeys.invites.teamInvites(), queryFn: async () => { const response = await callCodyProApi(Client.getTeamInvites()) - return response.json() + return ((await response.json()) as ListTeamInvitesResponse).invites }, }) -export const useSendInvite = (): UseMutationResult => { +export const useSendInvite = (): UseMutationResult, Error, CreateTeamInviteRequest> => { const queryClient = useQueryClient() return useMutation({ mutationFn: async requestBody => (await callCodyProApi(Client.sendInvite(requestBody))).json(), - onSuccess: (newInvite: TeamInvite) => { - queryClient.setQueryData(queryKeys.invites.teamInvites(), (prevInvites: TeamInvite[]) => [ + onSuccess: (newInvite: Omit) => { + queryClient.setQueryData(queryKeys.invites.teamInvites(), (prevInvites: Omit[]) => [ ...prevInvites, newInvite, ]) @@ -52,7 +52,7 @@ export const useSendInvite = (): UseMutationResult => { const queryClient = useQueryClient() return useMutation({ - mutationFn: async ({ inviteId }) => (await callCodyProApi(Client.resendInvite(inviteId))).json(), + mutationFn: async ({ inviteId }) => callCodyProApi(Client.resendInvite(inviteId)), onSuccess: () => queryClient.invalidateQueries({ queryKey: queryKeys.invites.teamInvites() }), }) } @@ -74,7 +74,7 @@ export const useCancelInvite = (): UseMutationResult callCodyProApi(Client.cancelInvite(teamId, inviteId)), - onSuccess: (_, { teamId, inviteId }) => + onSuccess: (_, { inviteId }) => queryClient.setQueryData(queryKeys.invites.teamInvites(), (prevInvites: TeamInvite[]) => prevInvites.filter(invite => invite.id !== inviteId) ), diff --git a/client/web/src/cody/team/CodyManageTeamPage.tsx b/client/web/src/cody/team/CodyManageTeamPage.tsx index fe8a63b925a01..0710f260d5268 100644 --- a/client/web/src/cody/team/CodyManageTeamPage.tsx +++ b/client/web/src/cody/team/CodyManageTeamPage.tsx @@ -44,7 +44,7 @@ const AuthenticatedCodyManageTeamPage: React.FunctionComponent { - if (subscriptionQueryResult.data && subscriptionQueryResult.data.subscriptionStatus !== 'canceled') { + if (subscriptionQueryResult.data?.subscriptionStatus === 'canceled') { navigate('/cody/subscription') } }, [navigate, subscriptionQueryResult.data]) diff --git a/client/web/src/cody/team/TeamMemberList.tsx b/client/web/src/cody/team/TeamMemberList.tsx index de89cc5331d1e..391b4c3d0d6af 100644 --- a/client/web/src/cody/team/TeamMemberList.tsx +++ b/client/web/src/cody/team/TeamMemberList.tsx @@ -137,7 +137,6 @@ export const TeamMemberList: FunctionComponent = ({ ) const adminCount = useMemo(() => teamMembers?.filter(member => member.role === 'admin').length ?? 0, [teamMembers]) - console.log(adminCount) if (!teamMembers) { return null