Fix toggling of roles apply to all projects (#34871)

* Store initial state

* fixes

* Minor

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
Terry SuttonandJoshen Lim authored and GitHub committed 2025-04-11 10:41:07 +08:00
1 parent c3a352331a
commit 9f6faa65bb
4 files changed
+71 -58

No files matched your search

@@ -1,34 +1,36 @@
import { useQueryClient } from '@tanstack/react-query'
import { InfoIcon, Check, ExternalLink } from 'lucide-react'
import { Check, ExternalLink, InfoIcon } from 'lucide-react'
import Link from 'next/link'
import { useState, useEffect } from 'react'
import { toast } from 'sonner'
import { useEffect, useState } from 'react'
import tweets from 'shared-data/tweets'
import { toast } from 'sonner'
import { billingPartnerLabel } from 'components/interfaces/Billing/Subscription/Subscription.utils'
import AlertError from 'components/ui/AlertError'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { organizationKeys } from 'data/organizations/keys'
import { OrganizationBillingSubscriptionPreviewResponse } from 'data/organizations/organization-billing-subscription-preview'
import { useOrgSubscriptionUpdateMutation } from 'data/subscriptions/org-subscription-update-mutation'
import { SubscriptionTier } from 'data/subscriptions/types'
import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
import { formatCurrency } from 'lib/helpers'
import {
Badge,
Button,
Card,
CardContent,
Dialog,
DialogContent,
HoverCard,
HoverCardContent,
HoverCardTrigger,
Table,
TableBody,
TableCell,
TableRow,
HoverCard,
HoverCardContent,
HoverCardTrigger,
Card,
CardContent,
Badge,
} from 'ui'
import { useOrgSubscriptionUpdateMutation } from 'data/subscriptions/org-subscription-update-mutation'
import { organizationKeys } from 'data/organizations/keys'
import { formatCurrency } from 'lib/helpers'
import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
import { SubscriptionTier } from 'data/subscriptions/types'
import { billingPartnerLabel } from 'components/interfaces/Billing/Subscription/Subscription.utils'
import PaymentMethodSelection from './PaymentMethodSelection'
import { Button, Dialog, DialogContent } from 'ui'
import { InfoTooltip } from 'ui-patterns/info-tooltip'
import { OrganizationBillingSubscriptionPreviewResponse } from 'data/organizations/organization-billing-subscription-preview'
import PaymentMethodSelection from './PaymentMethodSelection'
const getRandomTweet = () => {
const filteredTweets = tweets.filter((it) => it.text.length < 180)
@@ -576,7 +578,7 @@ const SubscriptionPlanUpdateDialog = ({
onClick={onUpdateSubscription}
className="flex-1"
>
Confirm {planMeta?.change_type === 'downgrade' ? 'Downgrade' : 'Upgrade'}
Confirm {planMeta?.change_type === 'downgrade' ? 'downgrade' : 'upgrade'}
</Button>
</div>
</div>
@@ -219,7 +219,7 @@ export const UpdateRolesConfirmationModal = ({
return (
<li key={`update-${i}`} className="text-sm text-foreground-light">
From <span className="text-foreground">{originalRoleName}</span> to{' '}
<span className="text-foreground">{updatedRole?.name}</span> on{' '}
<span className="text-foreground">{updatedRole?.name ?? 'Unknown'}</span> on{' '}
<span className={project !== undefined ? 'text-foreground' : ''}>
{project?.name ?? 'organization'}
</span>
@@ -3,6 +3,7 @@ import { X } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useParams } from 'common'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { DocsButton } from 'components/ui/DocsButton'
import { useOrganizationRolesV2Query } from 'data/organization-members/organization-roles-query'
import { OrganizationMember } from 'data/organizations/organization-members-query'
@@ -85,21 +86,26 @@ export const UpdateRolesPanel = ({ visible, member, onClose }: UpdateRolesPanelP
allRoles !== undefined
? formatMemberRoleToProjectRoleConfiguration(member, allRoles, projects ?? [])
: []
const originalConfigurationType =
originalConfiguration.length === 1 &&
!!orgScopedRoles.find((r) => r.id === originalConfiguration[0].roleId)
? 'org-scope'
: 'project-scope'
const orgProjects = (projects ?? []).filter((p) => p.organization_id === organization?.id)
const isApplyingRoleToAllProjects =
projectsRoleConfiguration.length === 1 && projectsRoleConfiguration[0]?.ref === undefined
const canSaveRoles = projectsRoleConfiguration.length > 0
const lowerPermissionsRole = orgScopedRoles.find((r) => r.name === 'Developer')?.id
const sortByObject: any = ['Owner', 'Administrator', 'Developer'].reduce((obj, item, index) => {
return { ...obj, [item]: index }
}, {})
const noAccessProjects = orgProjects.filter((project) => {
return !projectsRoleConfiguration.some((p) => p.ref === project.ref)
})
const numberOfProjectsWithAccess = orgProjects.length - noAccessProjects.length
const numberOfAccessHasChanges = originalConfiguration.length !== noAccessProjects.length
const hasNoChanges = isEqual(projectsRoleConfiguration, originalConfiguration)
const onSelectProject = (ref: string) => {
setProjectsRoleConfiguration(
projectsRoleConfiguration.concat({
@@ -131,17 +137,27 @@ export const UpdateRolesPanel = ({ visible, member, onClose }: UpdateRolesPanelP
}
}
const onToggleApplyToAllProjects = () => {
const roleIdToApply =
projectsRoleConfiguration[0]?.roleId ?? lowerPermissionsRole ?? orgScopedRoles[0].id
if (isApplyingRoleToAllProjects) {
setProjectsRoleConfiguration(
orgProjects.map((p) => {
return { ref: p.ref, projectId: p.id, roleId: roleIdToApply }
})
)
const onToggleApplyToAllProjects = (isApplyAllProjects: boolean) => {
const roleIdToApply = lowerPermissionsRole ?? orgScopedRoles[0].id
if (isApplyAllProjects) {
if (originalConfigurationType === 'org-scope') {
console.log('Reset: Org scoped')
setProjectsRoleConfiguration(originalConfiguration)
} else {
setProjectsRoleConfiguration([{ ref: undefined, roleId: roleIdToApply }])
}
} else {
setProjectsRoleConfiguration([{ ref: undefined, roleId: roleIdToApply }])
if (originalConfigurationType === 'project-scope') {
console.log('Reset: Project scoped')
setProjectsRoleConfiguration(originalConfiguration)
} else {
setProjectsRoleConfiguration(
orgProjects.map((p) => {
return { ref: p.ref, projectId: p.id, roleId: roleIdToApply }
})
)
}
}
}
@@ -275,7 +291,7 @@ export const UpdateRolesPanel = ({ visible, member, onClose }: UpdateRolesPanelP
<SelectItem_Shadcn_
key={role.id}
value={role.id.toString()}
className="text-sm"
className="text-sm hover:bg-selection cursor-pointer"
disabled={!canAssignRole}
>
{role.name}
@@ -288,22 +304,21 @@ export const UpdateRolesPanel = ({ visible, member, onClose }: UpdateRolesPanelP
)}
{!isApplyingRoleToAllProjects && (
<Tooltip>
<TooltipTrigger asChild>
<Button
type="text"
disabled={!canRemoveRole}
className="px-1"
icon={<X />}
onClick={() => onRemoveProject(project?.ref)}
/>
</TooltipTrigger>
{!canRemoveRole && (
<TooltipContent side="bottom">
Additional permission required to remove role from member
</TooltipContent>
)}
</Tooltip>
<ButtonTooltip
type="text"
disabled={!canRemoveRole}
className="px-1"
icon={<X />}
onClick={() => onRemoveProject(project?.ref)}
tooltip={{
content: {
side: 'bottom',
text: !canRemoveRole
? 'Additional permission required to remove role from member'
: 'Remove access to project',
},
}}
/>
)}
</div>
</div>
@@ -363,13 +378,9 @@ export const UpdateRolesPanel = ({ visible, member, onClose }: UpdateRolesPanelP
</Button>
<Button
loading={false}
disabled={!canSaveRoles}
disabled={!canSaveRoles || hasNoChanges}
onClick={() => {
if (isEqual(projectsRoleConfiguration, originalConfiguration)) {
onClose()
} else {
setShowConfirmation(true)
}
setShowConfirmation(true)
}}
>
Save roles
@@ -11,8 +11,8 @@ import {
DialogTitle,
cn,
} from 'ui'
import { Admonition } from './../admonition'
import { DialogDescription, DialogHeader } from 'ui/src/components/shadcn/ui/dialog'
import { Admonition } from './../admonition'
export interface ConfirmationModalProps {
loading?: boolean
@@ -85,7 +85,7 @@ const ConfirmationModal = forwardRef<
>
<DialogContent ref={ref} className="p-0 gap-0 pb-5 !block" size={size}>
<DialogHeader className={cn('border-b')} padding={'small'}>
<DialogTitle className="">{title}</DialogTitle>
<DialogTitle>{title}</DialogTitle>
{description && <DialogDescription>{description}</DialogDescription>}
</DialogHeader>
{alert && (