Small refactors after testing org team settings

This commit is contained in:
Joshen Lim committed 2023-07-12 11:54:01 +08:00
1 parent 439b356f0c
commit 4e1887472d
6 files changed
+246 -228

No files matched your search

@@ -5,7 +5,8 @@ import { Fragment, useState } from 'react'
import { useParams } from 'common/hooks'
import Table from 'components/to-be-cleaned/Table'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import AlertError from 'components/ui/AlertError'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useOrganizationDetailQuery } from 'data/organizations/organization-detail-query'
import { useOrganizationMemberUpdateMutation } from 'data/organizations/organization-member-update-mutation'
import { useOrganizationRolesQuery } from 'data/organizations/organization-roles-query'
@@ -35,8 +36,19 @@ const MembersView = ({ searchString }: MembersViewProps) => {
const { profile } = useProfile()
const { data: permissions } = usePermissionsQuery()
const { data: detailData, isLoading: isLoadingOrgDetails } = useOrganizationDetailQuery({ slug })
const { data: rolesData, isLoading: isLoadingRoles } = useOrganizationRolesQuery({ slug })
const {
data: detailData,
error: detailError,
isLoading: isLoadingOrgDetails,
isError: isErrorOrgDetails,
isSuccess: isSuccessOrgDetails,
} = useOrganizationDetailQuery({ slug })
const {
data: rolesData,
error: rolesError,
isLoading: isLoadingRoles,
isError: isErrorRoles,
} = useOrganizationRolesQuery({ slug })
const { mutateAsync: updateOrganizationMember, isLoading } = useOrganizationMemberUpdateMutation({
onSuccess() {
ui.setNotification({
@@ -44,10 +56,12 @@ const MembersView = ({ searchString }: MembersViewProps) => {
message: `Successfully updated role for ${getUserDisplayName(selectedMember)}`,
})
},
onError() {
onError(error) {
ui.setNotification({
category: 'error',
message: `Failed to update role for ${getUserDisplayName(selectedMember)}`,
message: `Failed to update role for ${getUserDisplayName(selectedMember)}: ${
error.message
}`,
})
},
})
@@ -94,230 +108,233 @@ const MembersView = ({ searchString }: MembersViewProps) => {
setUserRoleChangeModalVisible(false)
}
if (isLoadingOrgDetails) {
return (
<div className="py-4 space-y-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
<ShimmeringLoader className="w-1/2" />
</div>
)
}
return (
<>
<div className="rounded w-full">
<Loading active={!filteredMembers}>
<Table
head={[
<Table.th key="header-user">User</Table.th>,
<Table.th key="header-status"></Table.th>,
<Table.th key="header-role" className="flex items-center space-x-2">
<span>Role</span>
<RolesHelperModal />
</Table.th>,
<Table.th key="header-action"></Table.th>,
]}
body={[
...filteredMembers.map((x: Member, i: number) => {
const [memberRoleId] = x.role_ids ?? []
const role = (roles || []).find((role) => role.id === memberRoleId)
const memberIsUser = x.primary_email == profile?.primary_email
const memberIsPendingInvite = !!x.invited_id
const canRemoveRole = rolesRemovable.includes(memberRoleId)
const disableRoleEdit = !canRemoveRole || memberIsUser || memberIsPendingInvite
const isEmailUser = x.username === x.primary_email
{isLoadingOrgDetails && <GenericSkeletonLoader />}
const validateSelectedRoleToChange = (roleId: any) => {
if (!role || role.id === roleId) return
{isErrorOrgDetails && (
<AlertError error={detailError} subject="Failed to retrieve organization members" />
)}
const selectedRole = (roles || []).find((role) => role.id === roleId)
const canAddRole = rolesAddable.includes(selectedRole?.id ?? -1)
{isErrorRoles && (
<AlertError error={rolesError} subject="Failed to retrieve organization roles" />
)}
if (!canAddRole) {
return ui.setNotification({
category: 'error',
duration: 4000,
message: `You do not have permission to update this team member to ${
selectedRole!.name
}`,
})
{isSuccessOrgDetails && (
<div className="rounded w-full">
<Loading active={!filteredMembers}>
<Table
head={[
<Table.th key="header-user">User</Table.th>,
<Table.th key="header-status"></Table.th>,
<Table.th key="header-role" className="flex items-center space-x-2">
<span>Role</span>
<RolesHelperModal />
</Table.th>,
<Table.th key="header-action"></Table.th>,
]}
body={[
...filteredMembers.map((x: Member, i: number) => {
const [memberRoleId] = x.role_ids ?? []
const role = (roles || []).find((role) => role.id === memberRoleId)
const memberIsUser = x.primary_email == profile?.primary_email
const memberIsPendingInvite = !!x.invited_id
const canRemoveRole = rolesRemovable.includes(memberRoleId)
const disableRoleEdit = !canRemoveRole || memberIsUser || memberIsPendingInvite
const isEmailUser = x.username === x.primary_email
const validateSelectedRoleToChange = (roleId: any) => {
if (!role || role.id === roleId) return
const selectedRole = (roles || []).find((role) => role.id === roleId)
const canAddRole = rolesAddable.includes(selectedRole?.id ?? -1)
if (!canAddRole) {
return ui.setNotification({
category: 'error',
duration: 4000,
message: `You do not have permission to update this team member to ${
selectedRole!.name
}`,
})
}
setUserRoleChangeModalVisible(true)
setSelectedMember({ ...x, oldRoleId: role.id, newRoleId: roleId })
}
setUserRoleChangeModalVisible(true)
setSelectedMember({ ...x, oldRoleId: role.id, newRoleId: roleId })
}
return (
<Fragment key={`member-row-${i}`}>
<Table.tr>
<Table.td>
<div className="flex items-center space-x-4">
<div>
{x.invited_id ? (
<span className="flex p-2 border-2 rounded-full border-scale-700">
<IconUser size={20} strokeWidth={2} />
</span>
) : isEmailUser ? (
<div className="w-[40px] h-[40px] bg-scale-300 border border-scale-400 rounded-full text-scale-900 flex items-center justify-center">
<IconUser strokeWidth={1.5} />
</div>
) : (
<Image
src={`https://github.com/${x.username}.png?size=80`}
width="40"
height="40"
className="border rounded-full"
/>
)}
return (
<Fragment key={`member-row-${i}`}>
<Table.tr>
<Table.td>
<div className="flex items-center space-x-4">
<div>
{x.invited_id ? (
<span className="flex p-2 border-2 rounded-full border-scale-700">
<IconUser size={20} strokeWidth={2} />
</span>
) : isEmailUser ? (
<div className="w-[40px] h-[40px] bg-scale-300 border border-scale-400 rounded-full text-scale-900 flex items-center justify-center">
<IconUser strokeWidth={1.5} />
</div>
) : (
<Image
alt={x.username}
src={`https://github.com/${x.username}.png?size=80`}
width="40"
height="40"
className="border rounded-full"
/>
)}
</div>
<div>
<p className="text-scale-1200">{getUserDisplayName(x)}</p>
{x.invited_id === undefined && (
<p className="text-scale-1100">{x.primary_email}</p>
)}
</div>
</div>
<div>
<p className="text-scale-1200">{getUserDisplayName(x)}</p>
{x.invited_id === undefined && (
<p className="text-scale-1100">{x.primary_email}</p>
)}
</div>
</div>
</Table.td>
</Table.td>
<Table.td>
{x.invited_id && x.invited_at && (
<Badge color={isInviteExpired(x.invited_at) ? 'red' : 'yellow'}>
{isInviteExpired(x.invited_at) ? 'Expired' : 'Invited'}
</Badge>
)}
</Table.td>
<Table.td>
{x.invited_id && x.invited_at && (
<Badge color={isInviteExpired(x.invited_at) ? 'red' : 'yellow'}>
{isInviteExpired(x.invited_at) ? 'Expired' : 'Invited'}
</Badge>
)}
</Table.td>
<Table.td>
{isLoadingRoles ? (
<div className="w-[140px]">
<IconLoader className="animate-spin" size={16} strokeWidth={1.5} />
</div>
) : role !== undefined ? (
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger className="w-[140px]">
<Listbox
className={disableRoleEdit ? 'pointer-events-none' : ''}
disabled={disableRoleEdit}
value={role.id}
onChange={validateSelectedRoleToChange}
>
{roles.map((r: any) => (
<Listbox.Option
key={r.id}
value={r.id}
label={r.name}
disabled={disableRoleEdit}
className="w-36"
>
{r.name}
</Listbox.Option>
))}
</Listbox>
</Tooltip.Trigger>
{memberIsPendingInvite ? (
<Tooltip.Portal>
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
'border border-scale-200 ', //border
].join(' ')}
>
<span className="text-xs text-scale-1200">
Role can only be changed after the user has accepted the
invite
</span>
</div>
</Tooltip.Content>
</Tooltip.Portal>
) : !memberIsUser && !canRemoveRole ? (
<Tooltip.Portal>
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
'border border-scale-200 ', //border
].join(' ')}
>
<span className="text-xs text-scale-1200">
You need additional permissions to manage this team member
</span>
</div>
</Tooltip.Content>
</Tooltip.Portal>
) : (
<></>
)}
</Tooltip.Root>
) : (
<div className="flex items-center space-x-2">
<p className="text-sm text-scale-1100">Invalid role</p>
<Table.td>
{isLoadingRoles ? (
<div className="w-[140px]">
<IconLoader className="animate-spin" size={16} strokeWidth={1.5} />
</div>
) : role !== undefined ? (
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<IconAlertCircle size={16} strokeWidth={1.5} />
<Tooltip.Trigger className="w-[140px]">
<Listbox
className={disableRoleEdit ? 'pointer-events-none' : ''}
disabled={disableRoleEdit}
value={role.id}
onChange={validateSelectedRoleToChange}
>
{roles.map((r: any) => (
<Listbox.Option
key={r.id}
value={r.id}
label={r.name}
disabled={disableRoleEdit}
className="w-36"
>
{r.name}
</Listbox.Option>
))}
</Listbox>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
'border border-scale-200 ', //border
].join(' ')}
>
<span className="text-xs text-scale-1200">
This user has an invalid role, please reach out to us via
support
</span>
</div>
</Tooltip.Content>
</Tooltip.Portal>
{memberIsPendingInvite ? (
<Tooltip.Portal>
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
'border border-scale-200 ', //border
].join(' ')}
>
<span className="text-xs text-scale-1200">
Role can only be changed after the user has accepted the
invite
</span>
</div>
</Tooltip.Content>
</Tooltip.Portal>
) : !memberIsUser && !canRemoveRole ? (
<Tooltip.Portal>
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
'border border-scale-200 ', //border
].join(' ')}
>
<span className="text-xs text-scale-1200">
You need additional permissions to manage this team member
</span>
</div>
</Tooltip.Content>
</Tooltip.Portal>
) : (
<></>
)}
</Tooltip.Root>
) : (
<div className="flex items-center space-x-2">
<p className="text-sm text-scale-1100">Invalid role</p>
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<IconAlertCircle size={16} strokeWidth={1.5} />
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
'border border-scale-200 ', //border
].join(' ')}
>
<span className="text-xs text-scale-1200">
This user has an invalid role, please reach out to us via
support
</span>
</div>
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</div>
)}
</Table.td>
<Table.td>
{!memberIsUser && <MemberActions member={x} roles={roles} />}
</Table.td>
</Table.tr>
</Fragment>
)
}),
...(searchString.length > 0 && filteredMembers.length === 0
? [
<Table.tr
key="no-results"
className="bg-panel-secondary-light dark:bg-panel-secondary-dark"
>
<Table.td colSpan={12}>
<div className="flex items-center space-x-3 opacity-75">
<IconAlertCircle size={16} strokeWidth={2} />
<p className="text-scale-1100">
No users matched the search query "{searchString}"
</p>
</div>
)}
</Table.td>
<Table.td>
{!memberIsUser && <MemberActions member={x} roles={roles} />}
</Table.td>
</Table.tr>
</Fragment>
)
}),
...(searchString.length > 0 && filteredMembers.length === 0
? [
<Table.tr
key="no-results"
className="bg-panel-secondary-light dark:bg-panel-secondary-dark"
>
<Table.td colSpan={12}>
<div className="flex items-center space-x-3 opacity-75">
<IconAlertCircle size={16} strokeWidth={2} />
<p className="text-scale-1100">
No users matched the search query "{searchString}"
</p>
</div>
</Table.td>
</Table.tr>,
]
: []),
<Table.tr
key="footer"
className="bg-panel-secondary-light dark:bg-panel-secondary-dark"
>
<Table.td colSpan={4}>
<p className="text-scale-1100">
{searchString ? `${filteredMembers.length} of ` : ''}
{members.length || '0'} {members.length == 1 ? 'user' : 'users'}
</p>
</Table.td>
</Table.tr>,
]}
/>
</Loading>
</div>
</Table.td>
</Table.tr>,
]
: []),
<Table.tr
key="footer"
className="bg-panel-secondary-light dark:bg-panel-secondary-dark"
>
<Table.td colSpan={4}>
<p className="text-scale-1100">
{searchString ? `${filteredMembers.length} of ` : ''}
{members.length || '0'} {members.length == 1 ? 'user' : 'users'}
</p>
</Table.td>
</Table.tr>,
]}
/>
</Loading>
</div>
)}
<Modal
hideFooter
@@ -19,6 +19,7 @@ import {
ScaffoldContainerLegacy,
ScaffoldFilterAndContent,
ScaffoldActionsContainer,
ScaffoldSectionContent,
} from 'components/layouts/Scaffold'
const TeamSettings = () => {
@@ -133,7 +134,9 @@ const TeamSettings = () => {
</div>
</ScaffoldActionsGroup>
</ScaffoldActionsContainer>
<MembersView searchString={searchString} />
<ScaffoldSectionContent className="w-full">
<MembersView searchString={searchString} />
</ScaffoldSectionContent>
</ScaffoldFilterAndContent>
</ScaffoldContainerLegacy>
)
@@ -2,7 +2,7 @@ import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { useCallback } from 'react'
import { Member } from 'types'
import { Member, ResponseError } from 'types'
import { organizationKeys } from './keys'
export type OrganizationDetailVariables = {
@@ -56,7 +56,7 @@ export async function getOrganizationDetail(
}
export type OrganizationDetailData = Awaited<ReturnType<typeof getOrganizationDetail>>
export type OrganizationDetailError = unknown
export type OrganizationDetailError = ResponseError
export const useOrganizationDetailQuery = <TData = OrganizationDetailData>(
{ slug }: OrganizationDetailVariables,
@@ -1,9 +1,10 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { toast } from 'react-hot-toast'
import { delete_ } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { organizationKeys } from './keys'
import { toast } from 'react-hot-toast'
import { ResponseError } from 'types'
import { organizationKeys } from './keys'
export type OrganizationMemberInviteDeleteVariables = {
slug: string
@@ -18,10 +19,7 @@ export async function deleteOrganizationMemberInvite({
`${API_URL}/organizations/${slug}/members/invite?invited_id=${invitedId}`,
{}
)
if (response.error) {
throw response.error
}
if (response.error) throw response.error
return response
}
@@ -2,7 +2,7 @@ import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { useCallback } from 'react'
import { Role } from 'types'
import { ResponseError, Role } from 'types'
import { organizationKeys } from './keys'
export type OrganizationRolesVariables = {
@@ -32,7 +32,7 @@ export async function getOrganizationRoles(
}
export type OrganizationRolesData = Awaited<ReturnType<typeof getOrganizationRoles>>
export type OrganizationRolesError = unknown
export type OrganizationRolesError = ResponseError
export const useOrganizationRolesQuery = <TData = OrganizationRolesData>(
{ slug }: OrganizationRolesVariables,
+2 -2
View File
@@ -89,7 +89,7 @@ export type SupaResponse<T> = T & ResponseFailure
export type SupaResponseV2<T> = T | ResponseFailure
export interface ResponseError {
code: number
code?: number
message: string
requestId: string
requestId?: string
}