mirror of
https://github.com/supabase/supabase.git
synced 2026-10-19 08:09:11 +03:00
* Update the design of the sonner toasts. Add the close button by default. * Migrate studio and www apps to use the SonnerToaster. * Migrate all toasts from studio. * Migrate all leftover toasts in studio. * Add a new toast component with progress. Use it in studio. * Migrate the design-system app. * Refactor the consent toast to use sonner. * Switch docs to use the new sonner toasts. * Remove toast examples from the design-system app. * Remove all toast-related components and old code. * Fix the progress bar in the toast progress component. Also make the bottom components vertically centered. * Fix the width of the toast progress. * Use text-foreground-lighter instead of muted for ToastProgress text * Rename ToastProgress to SonnerProgress. * Shorten the text in sonner progress. * Use the correct classes for the close button. Add a const var for the default toast duration. Remove the custom width class from sonner. * Set the position for all progress toasts to bottom right. Set the duration for all toasts to the default (when reusing a toast id from loading/progress toast, the duration is set to infinity). * Fix the playwright tests. * Refactor imports to use ui instead of @ui. * Change all imports of react-hot-toast with sonner. These components were merged since the last commit to this branch. * Remove react-hot-toast lib. --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com> Co-authored-by: Jonathan Summers-Muir <MildTomato@users.noreply.github.com>
240 lines
9.4 KiB
TypeScript
240 lines
9.4 KiB
TypeScript
import { useQueryClient } from '@tanstack/react-query'
|
|
import { useState } from 'react'
|
|
import { toast } from 'sonner'
|
|
|
|
import { useParams } from 'common'
|
|
import { organizationKeys } from 'data/organization-members/keys'
|
|
import { useOrganizationMemberAssignRoleMutation } from 'data/organization-members/organization-member-role-assign-mutation'
|
|
import { useOrganizationMemberUnassignRoleMutation } from 'data/organization-members/organization-member-role-unassign-mutation'
|
|
import { useOrganizationMemberUpdateRoleMutation } from 'data/organization-members/organization-member-role-update-mutation'
|
|
import {
|
|
OrganizationRole,
|
|
useOrganizationRolesV2Query,
|
|
} from 'data/organization-members/organization-roles-query'
|
|
import { organizationKeys as organizationKeysV1 } from 'data/organizations/keys'
|
|
import { OrganizationMember } from 'data/organizations/organization-members-query'
|
|
import { useProjectsQuery } from 'data/projects/projects-query'
|
|
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
|
|
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
|
import {
|
|
ProjectRoleConfiguration,
|
|
deriveChanges,
|
|
deriveRoleChangeActions,
|
|
formatMemberRoleToProjectRoleConfiguration,
|
|
} from './UpdateRolesPanel.utils'
|
|
|
|
interface UpdateRolesConfirmationModal {
|
|
visible: boolean
|
|
member: OrganizationMember
|
|
projectsRoleConfiguration: ProjectRoleConfiguration[]
|
|
onClose: (success?: boolean) => void
|
|
}
|
|
|
|
export const UpdateRolesConfirmationModal = ({
|
|
visible,
|
|
member,
|
|
projectsRoleConfiguration,
|
|
onClose,
|
|
}: UpdateRolesConfirmationModal) => {
|
|
const { slug } = useParams()
|
|
const queryClient = useQueryClient()
|
|
const organization = useSelectedOrganization()
|
|
const { data: projects } = useProjectsQuery()
|
|
const { data: allRoles } = useOrganizationRolesV2Query({ slug: organization?.slug })
|
|
|
|
// [Joshen] Separate saving state instead of using RQ due to several successive steps
|
|
const [saving, setSaving] = useState(false)
|
|
const { mutateAsync: assignRole } = useOrganizationMemberAssignRoleMutation()
|
|
const { mutateAsync: removeRole } = useOrganizationMemberUnassignRoleMutation({
|
|
onError: () => {},
|
|
})
|
|
const { mutateAsync: updateRole } = useOrganizationMemberUpdateRoleMutation()
|
|
|
|
const availableRoles = allRoles?.org_scoped_roles ?? []
|
|
const { org_scoped_roles, project_scoped_roles } = allRoles ?? {
|
|
org_scoped_roles: [],
|
|
project_scoped_roles: [],
|
|
}
|
|
const orgProjects = (projects ?? []).filter((p) => p.organization_id === organization?.id)
|
|
const originalConfiguration =
|
|
allRoles !== undefined
|
|
? formatMemberRoleToProjectRoleConfiguration(member, allRoles, projects ?? [])
|
|
: []
|
|
const changesToRoles = deriveChanges(
|
|
originalConfiguration,
|
|
projectsRoleConfiguration,
|
|
projects ?? []
|
|
)
|
|
|
|
const onConfirmUpdateMemberRoles = async () => {
|
|
if (slug === undefined) return console.error('Slug is required')
|
|
|
|
setSaving(true)
|
|
const gotrueId = member.gotrue_id
|
|
const existingRoles = member.role_ids
|
|
.map((id) => {
|
|
return [...org_scoped_roles, ...project_scoped_roles].find((r) => r.id === id)
|
|
})
|
|
.filter(Boolean) as OrganizationRole[]
|
|
const isChangeWithinOrgScope =
|
|
projectsRoleConfiguration.length === 1 && projectsRoleConfiguration[0].ref === undefined
|
|
|
|
// Early return if we're just updating org level roles
|
|
// Everything else below is just project level role changes then
|
|
if (isChangeWithinOrgScope) {
|
|
try {
|
|
await assignRole({
|
|
slug,
|
|
gotrueId,
|
|
roleId: projectsRoleConfiguration[0].roleId,
|
|
})
|
|
toast.success(`Successfully updated role for ${member.username}`)
|
|
onClose(true)
|
|
return
|
|
} catch (error: any) {
|
|
setSaving(false)
|
|
return toast.error(`Failed to update role: ${error.message}`)
|
|
}
|
|
}
|
|
|
|
const { toRemove, toAssign, toUpdate } = deriveRoleChangeActions(existingRoles, changesToRoles)
|
|
|
|
try {
|
|
await Promise.all([
|
|
...toAssign.map(({ roleId, projectIds }) =>
|
|
assignRole({
|
|
slug,
|
|
gotrueId,
|
|
roleId,
|
|
projects: projectIds.map((id) => projects?.find((p) => p.id === id)?.ref) as string[],
|
|
})
|
|
),
|
|
])
|
|
await Promise.all([
|
|
...toRemove.map((roleId) => removeRole({ slug, gotrueId, roleId })),
|
|
...toUpdate.map(({ roleId, projectIds }) =>
|
|
updateRole({
|
|
slug,
|
|
gotrueId,
|
|
roleId,
|
|
roleName: project_scoped_roles.find((r) => r.id === roleId)?.name as string,
|
|
projects: projectIds.map((id) => projects?.find((p) => p.id === id)?.ref) as string[],
|
|
})
|
|
),
|
|
])
|
|
|
|
await Promise.all([
|
|
queryClient.invalidateQueries(organizationKeys.rolesV2(slug)),
|
|
queryClient.invalidateQueries(organizationKeysV1.members(slug)),
|
|
])
|
|
toast.success(`Successfully updated role for ${member.username}`)
|
|
onClose(true)
|
|
} catch (error: any) {
|
|
setSaving(false)
|
|
return toast.error(`Failed to update role: ${error.message}`)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<ConfirmationModal
|
|
size="medium"
|
|
visible={visible}
|
|
loading={saving}
|
|
title="Confirm to change roles of member"
|
|
confirmLabel="Update roles"
|
|
confirmLabelLoading="Updating"
|
|
onCancel={() => onClose()}
|
|
onConfirm={onConfirmUpdateMemberRoles}
|
|
>
|
|
<div className="flex flex-col gap-y-3">
|
|
<p className="text-sm text-foreground-light">
|
|
You are making the following changes to the role of{' '}
|
|
<span className="text-foreground">{member.username}</span> in the organization{' '}
|
|
<span className="text-foreground">{organization?.name}</span>:
|
|
</p>
|
|
<div className="flex flex-col gap-y-2">
|
|
{changesToRoles.removed.length !== 0 && (
|
|
<div>
|
|
<p className="text-sm">
|
|
Removing {changesToRoles.removed.length} role
|
|
{changesToRoles.removed.length > 1 ? 's' : ''} for user:
|
|
</p>
|
|
<ul className="list-disc pl-6">
|
|
{changesToRoles.removed.map((x, i) => {
|
|
const role =
|
|
org_scoped_roles.find((y) => y.id === x.roleId) ??
|
|
project_scoped_roles.find((y) => y.id === x.roleId)
|
|
const project = orgProjects.find((y) => y.ref === x.ref)
|
|
const roleName = (role?.name ?? 'Unknown').split('_')[0]
|
|
|
|
return (
|
|
<li key={`update-${i}`} className="text-sm text-foreground-light">
|
|
<span className="text-foreground">{roleName}</span> on{' '}
|
|
<span className={project !== undefined ? 'text-foreground' : ''}>
|
|
{project?.name ?? 'organization'}
|
|
</span>
|
|
</li>
|
|
)
|
|
})}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
{changesToRoles.added.length !== 0 && (
|
|
<div>
|
|
<p className="text-sm">
|
|
Adding {changesToRoles.added.length} role
|
|
{changesToRoles.added.length > 1 ? 's' : ''} for user:
|
|
</p>
|
|
<ul className="list-disc pl-6">
|
|
{changesToRoles.added.map((x, i) => {
|
|
const role = availableRoles.find((y) => y.id === x.roleId)
|
|
const project = orgProjects.find((y) => y.ref === x.ref)
|
|
return (
|
|
<li key={`update-${i}`} className="text-sm text-foreground-light">
|
|
<span className="text-foreground">{role?.name}</span> on{' '}
|
|
<span className={project !== undefined ? 'text-foreground' : ''}>
|
|
{project?.name ?? 'organization'}
|
|
</span>
|
|
</li>
|
|
)
|
|
})}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
{changesToRoles.updated.length !== 0 && (
|
|
<div>
|
|
<p className="text-sm">
|
|
Updating {changesToRoles.updated.length} role
|
|
{changesToRoles.updated.length > 1 ? 's' : ''} for user:
|
|
</p>
|
|
<ul className="list-disc pl-6">
|
|
{changesToRoles.updated.map((x, i) => {
|
|
const originalRole =
|
|
org_scoped_roles.find((y) => y.id === x.originalRole) ??
|
|
project_scoped_roles.find((y) => y.id === x.originalRole)
|
|
const updatedRole = org_scoped_roles.find((y) => y.id === x.updatedRole)
|
|
const project = orgProjects.find((y) => y.ref === x.ref)
|
|
const originalRoleName = (originalRole?.name ?? 'Unknown').split('_')[0]
|
|
|
|
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={project !== undefined ? 'text-foreground' : ''}>
|
|
{project?.name ?? 'organization'}
|
|
</span>
|
|
</li>
|
|
)
|
|
})}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<p className="text-sm text-foreground">
|
|
By changing the role of this member their permissions will change.
|
|
</p>
|
|
</div>
|
|
</ConfirmationModal>
|
|
)
|
|
}
|