From e5ce7891a2adde00f5cce438d83455887db37b52 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Mon, 26 Aug 2024 13:04:53 -0700 Subject: [PATCH] fix: consolidate org user menus Signed-off-by: Matt Krick --- .../components/BillingLeaderActionMenu.tsx | 90 ------------ .../client/components/BillingLeaderMenu.tsx | 40 ------ packages/client/components/FlatButton.tsx | 1 + .../client/components/OrgAdminActionMenu.tsx | 92 +++++++++---- .../components/OrgBilling/BillingLeader.tsx | 101 +++----------- .../components/OrgMembers/OrgMembers.tsx | 11 -- .../components/OrgUserRow/OrgMemberRow.tsx | 128 ++---------------- packages/client/ui/Button/Button.tsx | 7 +- .../server/graphql/public/fields/invoices.ts | 18 ++- 9 files changed, 125 insertions(+), 363 deletions(-) delete mode 100644 packages/client/components/BillingLeaderActionMenu.tsx delete mode 100644 packages/client/components/BillingLeaderMenu.tsx diff --git a/packages/client/components/BillingLeaderActionMenu.tsx b/packages/client/components/BillingLeaderActionMenu.tsx deleted file mode 100644 index 79811629b26..00000000000 --- a/packages/client/components/BillingLeaderActionMenu.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import graphql from 'babel-plugin-relay/macro' -import React from 'react' -import {useFragment} from 'react-relay' -import useAtmosphere from '~/hooks/useAtmosphere' -import {BillingLeaderActionMenu_organization$key} from '../__generated__/BillingLeaderActionMenu_organization.graphql' -import {BillingLeaderActionMenu_organizationUser$key} from '../__generated__/BillingLeaderActionMenu_organizationUser.graphql' -import {MenuProps} from '../hooks/useMenu' -import SetOrgUserRoleMutation from '../mutations/SetOrgUserRoleMutation' -import withMutationProps, {WithMutationProps} from '../utils/relay/withMutationProps' -import Menu from './Menu' -import MenuItem from './MenuItem' - -interface Props extends WithMutationProps { - menuProps: MenuProps - isViewerLastBillingLeader: boolean - organizationUser: BillingLeaderActionMenu_organizationUser$key - organization: BillingLeaderActionMenu_organization$key - toggleLeave: () => void - toggleRemove: () => void -} - -const BillingLeaderActionMenu = (props: Props) => { - const { - menuProps, - isViewerLastBillingLeader, - organizationUser: organizationUserRef, - submitting, - submitMutation, - onError, - onCompleted, - organization: organizationRef, - toggleLeave, - toggleRemove - } = props - const organization = useFragment( - graphql` - fragment BillingLeaderActionMenu_organization on Organization { - id - } - `, - organizationRef - ) - const organizationUser = useFragment( - graphql` - fragment BillingLeaderActionMenu_organizationUser on OrganizationUser { - role - user { - id - } - } - `, - organizationUserRef - ) - const atmosphere = useAtmosphere() - const {id: orgId} = organization - const {viewerId} = atmosphere - const {role, user} = organizationUser - const isBillingLeader = role === 'BILLING_LEADER' - const {id: userId} = user - - const setRole = - (role: 'BILLING_LEADER' | null = null) => - () => { - if (submitting) return - submitMutation() - const variables = {orgId, userId, role} - SetOrgUserRoleMutation(atmosphere, variables, {onError, onCompleted}) - } - - return ( - <> - - {isBillingLeader && !isViewerLastBillingLeader && ( - - )} - {!isBillingLeader && ( - - )} - {viewerId === userId && !isViewerLastBillingLeader && ( - - )} - {viewerId !== userId && ( - - )} - - - ) -} - -export default withMutationProps(BillingLeaderActionMenu) diff --git a/packages/client/components/BillingLeaderMenu.tsx b/packages/client/components/BillingLeaderMenu.tsx deleted file mode 100644 index 3082a87b4d1..00000000000 --- a/packages/client/components/BillingLeaderMenu.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react' -import useAtmosphere from '~/hooks/useAtmosphere' -import {MenuProps} from '../hooks/useMenu' -import useMutationProps from '../hooks/useMutationProps' -import SetOrgUserRoleMutation from '../mutations/SetOrgUserRoleMutation' -import Menu from './Menu' -import MenuItem from './MenuItem' - -type Props = { - menuProps: MenuProps - userId: string - toggleLeave: () => void - toggleRemove: () => void - orgId: string -} - -const BillingLeaderMenu = (props: Props) => { - const {menuProps, toggleRemove, userId, toggleLeave, orgId} = props - const atmosphere = useAtmosphere() - const {onError, onCompleted, submitting, submitMutation} = useMutationProps() - const {viewerId} = atmosphere - const isViewer = viewerId === userId - - const removeBillingLeader = () => { - if (submitting) return - submitMutation() - const variables = {orgId, userId, role: null} - SetOrgUserRoleMutation(atmosphere, variables, {onError, onCompleted}) - } - - return ( - - - {isViewer && } - {!isViewer && } - - ) -} - -export default BillingLeaderMenu diff --git a/packages/client/components/FlatButton.tsx b/packages/client/components/FlatButton.tsx index 9af7443b777..aec45c11895 100644 --- a/packages/client/components/FlatButton.tsx +++ b/packages/client/components/FlatButton.tsx @@ -1,3 +1,4 @@ +// DEPRECATED use packages/client/ui/Button/Button.tsx with variant='flat' import styled from '@emotion/styled' import React, {ReactNode} from 'react' import {PALETTE} from '../styles/paletteV3' diff --git a/packages/client/components/OrgAdminActionMenu.tsx b/packages/client/components/OrgAdminActionMenu.tsx index adb94de0977..37119d67ba5 100644 --- a/packages/client/components/OrgAdminActionMenu.tsx +++ b/packages/client/components/OrgAdminActionMenu.tsx @@ -1,28 +1,27 @@ +import {MoreVert} from '@mui/icons-material' +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' import graphql from 'babel-plugin-relay/macro' import React from 'react' import {useFragment} from 'react-relay' import useAtmosphere from '~/hooks/useAtmosphere' import {OrgAdminActionMenu_organization$key} from '../__generated__/OrgAdminActionMenu_organization.graphql' import {OrgAdminActionMenu_organizationUser$key} from '../__generated__/OrgAdminActionMenu_organizationUser.graphql' -import {MenuProps} from '../hooks/useMenu' import useMutationProps from '../hooks/useMutationProps' import SetOrgUserRoleMutation from '../mutations/SetOrgUserRoleMutation' -import Menu from './Menu' -import MenuItem from './MenuItem' +import {Button} from '../ui/Button/Button' +import {Menu} from '../ui/Menu/Menu' +import {MenuContent} from '../ui/Menu/MenuContent' +import {MenuItem} from '../ui/Menu/MenuItem' interface Props { - menuProps: MenuProps - isViewerLastOrgAdmin: boolean organizationUser: OrgAdminActionMenu_organizationUser$key organization: OrgAdminActionMenu_organization$key toggleLeave: () => void toggleRemove: () => void } -const OrgAdminActionMenu = (props: Props) => { +export const OrgAdminActionMenu = (props: Props) => { const { - menuProps, - isViewerLastOrgAdmin, organizationUser: organizationUserRef, organization: organizationRef, toggleLeave, @@ -32,6 +31,12 @@ const OrgAdminActionMenu = (props: Props) => { graphql` fragment OrgAdminActionMenu_organization on Organization { id + isBillingLeader + isOrgAdmin + billingLeaders { + id + role + } } `, organizationRef @@ -49,10 +54,21 @@ const OrgAdminActionMenu = (props: Props) => { ) const atmosphere = useAtmosphere() const {onError, onCompleted, submitting, submitMutation} = useMutationProps() - const {id: orgId} = organization + const { + id: orgId, + isBillingLeader: isViewerBillingLeaderPlus, + isOrgAdmin: isViewerOrgAdmin, + billingLeaders + } = organization const {viewerId} = atmosphere const {role, user} = organizationUser const {id: userId} = user + const orgAdminCount = billingLeaders.filter( + (billingLeader) => billingLeader.role === 'ORG_ADMIN' + ).length + const canEdit = isViewerOrgAdmin || (isViewerBillingLeaderPlus && role !== 'ORG_ADMIN') + const isViewerLastOrgAdmin = isViewerOrgAdmin && orgAdminCount === 1 + const isViewerLastRole = isViewerBillingLeaderPlus && billingLeaders.length === 1 const setRole = (role: 'ORG_ADMIN' | 'BILLING_LEADER' | null = null) => @@ -66,27 +82,53 @@ const OrgAdminActionMenu = (props: Props) => { const isOrgAdmin = role === 'ORG_ADMIN' const isBillingLeader = role === 'BILLING_LEADER' const isSelf = viewerId === userId - const canRemoveSelf = isSelf && !isViewerLastOrgAdmin const roleName = role === 'ORG_ADMIN' ? 'Org Admin' : 'Billing Leader' - + const canRemoveRole = + role && + ((isViewerOrgAdmin && (!isSelf || !isViewerLastOrgAdmin)) || + (isViewerBillingLeaderPlus && isBillingLeader && (!isSelf || !isViewerLastRole))) return ( - <> - - {!isOrgAdmin && } - {!isOrgAdmin && !isBillingLeader && ( - + + + + ) + } + > + + {isViewerOrgAdmin && !isOrgAdmin && ( + {'Promote to Org Admin'} + )} + {isViewerBillingLeaderPlus && !isOrgAdmin && !isBillingLeader && ( + {'Promote to Billing Leader'} )} - {isOrgAdmin && !isSelf && ( - + {isViewerOrgAdmin && isOrgAdmin && (!isSelf || !isViewerLastOrgAdmin) && ( + {'Change to Billing Leader'} )} - {((role && !isSelf) || canRemoveSelf) && ( - + {canRemoveRole && {`Remove ${roleName} role`}} + {isSelf && + ((isOrgAdmin && !isViewerLastOrgAdmin) || + (isBillingLeader && !isViewerLastRole) || + !isViewerBillingLeaderPlus) && ( + {'Leave Organization'} + )} + {!isSelf && (isViewerOrgAdmin || (isViewerBillingLeaderPlus && !isOrgAdmin)) && ( + {'Remove from Organization'} )} - {canRemoveSelf && } - {!isSelf && } - {isSelf && !canRemoveSelf && } - - + {isSelf && + ((isOrgAdmin && isViewerLastOrgAdmin) || (isBillingLeader && isViewerLastRole)) && ( + + {'Contact support@parabol.co to be removed'} + + )} + + ) } diff --git a/packages/client/modules/userDashboard/components/OrgBilling/BillingLeader.tsx b/packages/client/modules/userDashboard/components/OrgBilling/BillingLeader.tsx index 49f7753dde2..40d40e83b76 100644 --- a/packages/client/modules/userDashboard/components/OrgBilling/BillingLeader.tsx +++ b/packages/client/modules/userDashboard/components/OrgBilling/BillingLeader.tsx @@ -5,19 +5,13 @@ import {useFragment} from 'react-relay' import {BillingLeader_orgUser$key} from '../../../../__generated__/BillingLeader_orgUser.graphql' import {BillingLeader_organization$key} from '../../../../__generated__/BillingLeader_organization.graphql' import Avatar from '../../../../components/Avatar/Avatar' -import BillingLeaderMenu from '../../../../components/BillingLeaderMenu' -import FlatButton from '../../../../components/FlatButton' -import IconLabel from '../../../../components/IconLabel' import Row from '../../../../components/Row/Row' import RowActions from '../../../../components/Row/RowActions' import RowInfo from '../../../../components/Row/RowInfo' import RowInfoHeader from '../../../../components/Row/RowInfoHeader' import RowInfoHeading from '../../../../components/Row/RowInfoHeading' import BaseTag from '../../../../components/Tag/BaseTag' -import {MenuPosition} from '../../../../hooks/useCoords' -import useMenu from '../../../../hooks/useMenu' import useModal from '../../../../hooks/useModal' -import useTooltip from '../../../../hooks/useTooltip' import lazyPreload from '../../../../utils/lazyPreload' import LeaveOrgModal from '../LeaveOrgModal/LeaveOrgModal' import RemoveFromOrgModal from '../RemoveFromOrgModal/RemoveFromOrgModal' @@ -35,21 +29,9 @@ const ActionsBlock = styled('div')({ justifyContent: 'flex-end' }) -const MenuToggleBlock = styled('div')({ - width: 32 -}) - -const StyledButton = styled(FlatButton)({ - paddingLeft: 0, - paddingRight: 0, - width: '100%' -}) - -const BillingLeaderActionMenu = lazyPreload( +const OrgAdminActionMenu = lazyPreload( () => - import( - /* webpackChunkName: 'BillingLeaderActionMenu' */ '../../../../components/BillingLeaderActionMenu' - ) + import(/* webpackChunkName: 'OrgAdminActionMenu' */ '../../../../components/OrgAdminActionMenu') ) type Props = { @@ -60,11 +42,11 @@ type Props = { } const BillingLeader = (props: Props) => { - const {billingLeaderRef, isFirstRow, billingLeaderCount, organizationRef} = props - const {togglePortal, originRef, menuPortal, menuProps} = useMenu(MenuPosition.UPPER_RIGHT) + const {billingLeaderRef, isFirstRow, organizationRef} = props const billingLeader = useFragment( graphql` fragment BillingLeader_orgUser on OrganizationUser { + ...OrgAdminActionMenu_organizationUser role user { id @@ -78,37 +60,24 @@ const BillingLeader = (props: Props) => { const organization = useFragment( graphql` fragment BillingLeader_organization on Organization { + ...OrgAdminActionMenu_organization id - isViewerBillingLeader: isBillingLeader + isOrgAdmin + isBillingLeader } `, organizationRef ) - const {id: orgId, isViewerBillingLeader} = organization const { - tooltipPortal, - openTooltip, - closeTooltip, - originRef: tooltipRef - } = useTooltip(MenuPosition.LOWER_CENTER) + id: orgId, + isOrgAdmin: isViewerOrgAdmin, + isBillingLeader: isViewerBillingLeader + } = organization const {togglePortal: toggleLeave, modalPortal: leaveModal} = useModal() const {togglePortal: toggleRemove, modalPortal: removeModal} = useModal() - const {user: billingLeaderUser} = billingLeader + const {user: billingLeaderUser, role} = billingLeader const {id: userId, preferredName, picture} = billingLeaderUser - const isViewerLastBillingLeader = isViewerBillingLeader && billingLeaderCount === 1 - const canViewMenu = !isViewerLastBillingLeader && billingLeader.role !== 'ORG_ADMIN' - - const handleClick = () => { - togglePortal() - closeTooltip() - } - - const handleMouseOver = () => { - if (!canViewMenu) { - openTooltip() - } - } - + const canEdit = isViewerOrgAdmin || (isViewerBillingLeader && role === 'BILLING_LEADER') return ( @@ -122,42 +91,14 @@ const BillingLeader = (props: Props) => { )} - - {isViewerBillingLeader && ( - - - - - {tooltipPortal( - isViewerLastBillingLeader ? ( -
- {'You need to promote another Billing Leader'} -
- {'before you can remove this role.'} -
- ) : ( -
Contact support (love@parabol.co) to remove the Org Admin role
- ) - )} -
- )} - {menuPortal( - - )} -
+ {canEdit && ( + + )}
{leaveModal()} diff --git a/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx b/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx index e56be6deb45..e9534528b3b 100644 --- a/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx +++ b/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx @@ -70,15 +70,6 @@ const OrgMembers = (props: Props) => { const {organization} = viewer if (!organization) return null const {organizationUsers, name: orgName, isBillingLeader} = organization - const billingLeaderCount = organizationUsers.edges.reduce( - (count, {node}) => - ['BILLING_LEADER', 'ORG_ADMIN'].includes(node.role ?? '') ? count + 1 : count, - 0 - ) - const orgAdminCount = organizationUsers.edges.reduce( - (count, {node}) => (['ORG_ADMIN'].includes(node.role ?? '') ? count + 1 : count), - 0 - ) const exportToCSV = async () => { const rows = organizationUsers.edges.map((orgUser, idx) => { @@ -120,8 +111,6 @@ const OrgMembers = (props: Props) => { return ( diff --git a/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx b/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx index d6b0862a091..3ee6efc2135 100644 --- a/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx +++ b/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx @@ -1,8 +1,7 @@ import styled from '@emotion/styled' import graphql from 'babel-plugin-relay/macro' -import React, {forwardRef, Ref} from 'react' +import React from 'react' import {useFragment} from 'react-relay' -import useAtmosphere from '~/hooks/useAtmosphere' import { OrgMemberRow_organization$data, OrgMemberRow_organization$key @@ -12,8 +11,6 @@ import { OrgMemberRow_organizationUser$key } from '../../../../__generated__/OrgMemberRow_organizationUser.graphql' import Avatar from '../../../../components/Avatar/Avatar' -import FlatButton, {FlatButtonProps} from '../../../../components/FlatButton' -import IconLabel from '../../../../components/IconLabel' import Row from '../../../../components/Row/Row' import RowActions from '../../../../components/Row/RowActions' import RowInfo from '../../../../components/Row/RowInfo' @@ -23,13 +20,10 @@ import RowInfoLink from '../../../../components/Row/RowInfoLink' import BaseTag from '../../../../components/Tag/BaseTag' import InactiveTag from '../../../../components/Tag/InactiveTag' import RoleTag from '../../../../components/Tag/RoleTag' -import {MenuPosition} from '../../../../hooks/useCoords' -import useMenu from '../../../../hooks/useMenu' import useModal from '../../../../hooks/useModal' import defaultUserAvatar from '../../../../styles/theme/images/avatar-user.svg' import {Breakpoint} from '../../../../types/constEnums' import lazyPreload from '../../../../utils/lazyPreload' -import withMutationProps, {WithMutationProps} from '../../../../utils/relay/withMutationProps' const AvatarBlock = styled('div')({ display: 'none', @@ -56,45 +50,15 @@ const ActionsBlock = styled('div')({ justifyContent: 'flex-end' }) -const MenuToggleBlock = styled('div')({ - marginLeft: 8, - width: '2rem' -}) - -interface Props extends WithMutationProps { - billingLeaderCount: number - orgAdminCount: number +interface Props { organizationUser: OrgMemberRow_organizationUser$key organization: OrgMemberRow_organization$key } -const StyledButton = styled(FlatButton)({ - paddingLeft: 0, - paddingRight: 0, - width: '100%' -}) - -const StyledFlatButton = styled(FlatButton)({ - paddingLeft: 16, - paddingRight: 16 -}) - -const MenuButton = forwardRef((props: FlatButtonProps, ref: Ref) => ( - - - -)) - const LeaveOrgModal = lazyPreload( () => import(/* webpackChunkName: 'LeaveOrgModal' */ '../LeaveOrgModal/LeaveOrgModal') ) -const BillingLeaderActionMenu = lazyPreload( - () => - import( - /* webpackChunkName: 'BillingLeaderActionMenu' */ '../../../../components/BillingLeaderActionMenu' - ) -) const OrgAdminActionMenu = lazyPreload( () => import(/* webpackChunkName: 'OrgAdminActionMenu' */ '../../../../components/OrgAdminActionMenu') @@ -148,70 +112,31 @@ const UserInfo: React.FC = ({ ) interface UserActionsProps { - isViewerOrgAdmin: boolean - isViewerBillingLeader: boolean - isViewerLastOrgAdmin: boolean - isViewerLastBillingLeader: boolean organization: OrgMemberRow_organization$data organizationUser: OrgMemberRow_organizationUser$data preferredName: string - viewerId: string } const UserActions: React.FC = ({ - isViewerOrgAdmin, - isViewerBillingLeader, - isViewerLastOrgAdmin, - isViewerLastBillingLeader, organizationUser, organization, - preferredName, - viewerId + preferredName }) => { - const {orgId} = organization + const {id: orgId} = organization const { - user: {userId} + user: {id: userId} } = organizationUser - const {togglePortal, originRef, menuPortal, menuProps} = useMenu(MenuPosition.UPPER_RIGHT) const {togglePortal: toggleLeave, modalPortal: leaveModal} = useModal() const {togglePortal: toggleRemove, modalPortal: removeModal} = useModal() - const actionMenuProps = { - menuProps, - originRef, - togglePortal, - toggleLeave, - toggleRemove, - isViewerLastOrgAdmin, - isViewerLastBillingLeader, - organization, - organizationUser - } - - const showLeaveButton = !isViewerOrgAdmin && !isViewerBillingLeader && viewerId === userId - return ( - {showLeaveButton && ( - - Leave Organization - - )} - {(isViewerOrgAdmin || (isViewerBillingLeader && !isViewerLastBillingLeader)) && ( - - - - )} - {isViewerOrgAdmin && menuPortal()} - {!isViewerOrgAdmin && - isViewerBillingLeader && - menuPortal()} + {leaveModal()} {removeModal( @@ -222,21 +147,12 @@ const UserActions: React.FC = ({ } const OrgMemberRow = (props: Props) => { - const atmosphere = useAtmosphere() - const { - billingLeaderCount, - orgAdminCount, - organizationUser: organizationUserRef, - organization: organizationRef - } = props + const {organizationUser: organizationUserRef, organization: organizationRef} = props const organization = useFragment( graphql` fragment OrgMemberRow_organization on Organization { - isViewerBillingLeader: isBillingLeader - isViewerOrgAdmin: isOrgAdmin - orgId: id - ...BillingLeaderActionMenu_organization + id ...OrgAdminActionMenu_organization } `, @@ -247,35 +163,26 @@ const OrgMemberRow = (props: Props) => { graphql` fragment OrgMemberRow_organizationUser on OrganizationUser { user { - userId: id + id email inactive picture preferredName } role - ...BillingLeaderActionMenu_organizationUser ...OrgAdminActionMenu_organizationUser } `, organizationUserRef ) - const {isViewerBillingLeader, isViewerOrgAdmin} = organization - const { user: {email, inactive, picture, preferredName}, role } = organizationUser - const {viewerId} = atmosphere - const isBillingLeader = role === 'BILLING_LEADER' const isOrgAdmin = role === 'ORG_ADMIN' - const isViewerLastBillingLeader = - isViewerBillingLeader && isBillingLeader && billingLeaderCount === 1 - const isViewerLastOrgAdmin = isViewerOrgAdmin && isOrgAdmin && orgAdminCount === 1 - return ( @@ -287,17 +194,12 @@ const OrgMemberRow = (props: Props) => { inactive={inactive} /> ) } -export default withMutationProps(OrgMemberRow) +export default OrgMemberRow diff --git a/packages/client/ui/Button/Button.tsx b/packages/client/ui/Button/Button.tsx index a0e3bc1e373..c93a4a4b353 100644 --- a/packages/client/ui/Button/Button.tsx +++ b/packages/client/ui/Button/Button.tsx @@ -3,12 +3,12 @@ import clsx from 'clsx' import React from 'react' import {twMerge} from 'tailwind-merge' -type Variant = 'primary' | 'secondary' | 'destructive' | 'ghost' | 'link' | 'outline' +type Variant = 'primary' | 'secondary' | 'destructive' | 'ghost' | 'link' | 'outline' | 'flat' type Size = 'sm' | 'md' | 'lg' | 'default' type Shape = 'pill' | 'circle' | 'default' const BASE_STYLES = - 'cursor-pointer inline-flex items-center justify-center whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50' + 'cursor-pointer inline-flex items-center justify-center whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50' // TODO: make sure the styles match the designs const VARIANT_STYLES: Record = { @@ -18,7 +18,8 @@ const VARIANT_STYLES: Record = { 'text-slate-900 border border-slate-400 hover:bg-slate-200 px-2.5 py-1 bg-transparent font-semibold', secondary: 'bg-sky-500 text-white hover:bg-sky-500/80 font-semibold', ghost: 'hover:opacity-80 bg-transparent font-semibold', - link: 'text-primary underline-offset-4 hover:underline' + link: 'text-primary underline-offset-4 hover:underline', + flat: 'rounded-full bg-transparent outline-none shadow-none hover:bg-slate-200 focus:bg-slate-200 active:bg-slate-200 focus-visible:ring-0' } const SIZE_STYLES: Record = { diff --git a/packages/server/graphql/public/fields/invoices.ts b/packages/server/graphql/public/fields/invoices.ts index 990d9355245..c1eee29a95e 100644 --- a/packages/server/graphql/public/fields/invoices.ts +++ b/packages/server/graphql/public/fields/invoices.ts @@ -30,7 +30,7 @@ export const invoices: NonNullable = async ( return {edges: [], pageInfo: {hasNextPage: false, hasPreviousPage: false}} const manager = getStripeManager() - const [session, upcomingInvoice, invoices] = await Promise.all([ + const [sessionRes, upcomingInvoiceRes, invoicesRes] = await Promise.allSettled([ manager.stripe.billingPortal.sessions.create({ customer: stripeId, return_url: makeAppURL(appOrigin, `me/organizations/${orgId}/billing`) @@ -38,6 +38,22 @@ export const invoices: NonNullable = async ( manager.retrieveUpcomingInvoice(stripeId), manager.listInvoices(stripeId) ]) + if ( + sessionRes.status === 'rejected' || + upcomingInvoiceRes.status === 'rejected' || + invoicesRes.status === 'rejected' + ) { + return { + edges: [], + pageInfo: { + hasNextPage: false, + hasPreviousPage: false + } + } + } + const session = sessionRes.value + const upcomingInvoice = upcomingInvoiceRes.value + const invoices = invoicesRes.value const parabolUpcomingInvoice: Invoice = { id: `upcoming_${orgId}`, periodEndAt: fromEpochSeconds(upcomingInvoice.period_end!),