Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { createFileRoute } from '@tanstack/react-router'
import { SettingsMembers } from '@qovery/domains/organizations/feature'

export const Route = createFileRoute('/_authenticated/organization/$organizationId/settings/members')({
component: RouteComponent,
})

function RouteComponent() {
return <div>Hello "/_authenticated/organization/$organizationId/settings/members"!</div>
return <SettingsMembers />
}
1 change: 1 addition & 0 deletions libs/domains/organizations/feature/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,4 @@ export * from './lib/settings-webhook/settings-webhook'
export * from './lib/settings-api-token/settings-api-token'
export * from './lib/settings-danger-zone/settings-danger-zone'
export * from './lib/settings-billing-details/settings-billing-details'
export * from './lib/settings-members/settings-members'
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { queries } from '@qovery/state/util-queries'

export interface UseAvailableRolesProps {
organizationId: string
suspense?: boolean
}

export function useAvailableRoles({ organizationId }: UseAvailableRolesProps) {
export function useAvailableRoles({ organizationId, suspense = false }: UseAvailableRolesProps) {
return useQuery({
...queries.organizations.availableRoles({ organizationId }),
select(data) {
Expand All @@ -14,6 +15,7 @@ export function useAvailableRoles({ organizationId }: UseAvailableRolesProps) {
}
return data.sort((a, b) => (a.name && b.name ? a.name.localeCompare(b.name) : 0))
},
suspense,
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { queries } from '@qovery/state/util-queries'

export interface UseInviteMembersProps {
organizationId: string
suspense?: boolean
}

export function useInviteMembers({ organizationId }: UseInviteMembersProps) {
export function useInviteMembers({ organizationId, suspense = false }: UseInviteMembersProps) {
return useQuery({
...queries.organizations.inviteMembers({ organizationId }),
select(data) {
Expand All @@ -14,6 +15,7 @@ export function useInviteMembers({ organizationId }: UseInviteMembersProps) {
}
return data.sort((a, b) => a.email.localeCompare(b.email))
},
suspense,
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import { queries } from '@qovery/state/util-queries'

export interface UseMembersProps {
organizationId: string
suspense?: boolean
}

export function useMembers({ organizationId }: UseMembersProps) {
export function useMembers({ organizationId, suspense = false }: UseMembersProps) {
return useQuery({
...queries.organizations.members({ organizationId }),
meta: {
notifyOnError: true,
},
suspense,
})
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { renderWithProviders, screen, waitFor } from '@qovery/shared/util-tests'
import * as useCreateInviteMemberHook from '../../hooks/use-create-invite-member/use-create-invite-member'
import CreateModal, { type CreateModalProps } from './create-modal'

const useCreateInviteMemberMock = jest.spyOn(useCreateInviteMemberHook, 'useCreateInviteMember') as jest.Mock

describe('CreateModal', () => {
const createInviteMemberMock = jest.fn()

const props: CreateModalProps = {
availableRoles: [
{
id: 'role-admin',
name: 'Admin',
},
{
id: 'role-viewer',
name: 'Viewer',
},
],
onClose: jest.fn(),
organizationId: 'org-1',
}

beforeEach(() => {
jest.clearAllMocks()
useCreateInviteMemberMock.mockReturnValue({
mutateAsync: createInviteMemberMock,
isLoading: false,
})
})

it('should render successfully', () => {
const { baseElement } = renderWithProviders(<CreateModal {...props} />)
expect(baseElement).toBeTruthy()
})

it('should submit the form', async () => {
createInviteMemberMock.mockResolvedValueOnce(undefined)

const { userEvent } = renderWithProviders(<CreateModal {...props} />)

await userEvent.type(screen.getByTestId('input-email'), 'test@qovery.com')
const submitButton = screen.getByTestId('submit-button')

await waitFor(() => {
expect(submitButton).toBeEnabled()
})

await userEvent.click(submitButton)

await waitFor(() => {
expect(createInviteMemberMock).toHaveBeenCalledWith({
organizationId: 'org-1',
inviteMemberRequest: {
email: 'test@qovery.com',
role_id: 'role-admin',
},
})
expect(props.onClose).toHaveBeenCalled()
})
})
})
Original file line number Diff line number Diff line change
@@ -1,28 +1,54 @@
import { type OrganizationAvailableRole } from 'qovery-typescript-axios'
import { Controller, useFormContext } from 'react-hook-form'
import { type InviteMemberRequest, type OrganizationAvailableRole } from 'qovery-typescript-axios'
import { Controller, FormProvider, useForm, useFormContext } from 'react-hook-form'
import { InputSelect, InputText, ModalCrud } from '@qovery/shared/ui'
import { upperCaseFirstLetter } from '@qovery/shared/util-js'
import { useCreateInviteMember } from '../../hooks/use-create-invite-member/use-create-invite-member'

export interface CreateModalProps {
availableRoles: OrganizationAvailableRole[]
onSubmit: () => void
onClose: () => void
loading?: boolean
availableRoles: OrganizationAvailableRole[]
organizationId?: string
}

export function CreateModal(props: CreateModalProps) {
const { availableRoles } = props
const { control } = useFormContext()
const { organizationId = '', availableRoles, onClose } = props
const { mutateAsync: createInviteMember, isLoading: isLoadingInviteMember } = useCreateInviteMember()

type CreateInviteMemberForm = {
email: string
role_id: string
}

const methods = useForm<CreateInviteMemberForm>({
mode: 'onChange',
defaultValues: {
email: '',
role_id: availableRoles[0]?.id ?? '',
},
})
const { control } = methods

const onSubmit = methods.handleSubmit(async (data) => {
try {
await createInviteMember({
organizationId,
inviteMemberRequest: data as InviteMemberRequest,
})
onClose()
} catch (error) {
console.error(error)
}
})

return (
<ModalCrud
title="Invite team member"
submitLabel="Send invitation"
onSubmit={props.onSubmit}
onClose={props.onClose}
loading={props.loading}
>
<div className="flex w-full">
<FormProvider {...methods}>
<ModalCrud
title="Invite team member"
submitLabel="Send invitation"
onSubmit={onSubmit}
onClose={onClose}
loading={isLoadingInviteMember}
>
<Controller
name="email"
control={control}
Expand Down Expand Up @@ -55,7 +81,7 @@ export function CreateModal(props: CreateModalProps) {
render={({ field, fieldState: { error } }) => (
<InputSelect
dataTestId="input-role"
className="ml-3 w-full"
className="mt-4 w-full"
label="Role"
options={availableRoles.map((availableRole: OrganizationAvailableRole) => ({
label: upperCaseFirstLetter(availableRole.name),
Expand All @@ -69,8 +95,8 @@ export function CreateModal(props: CreateModalProps) {
/>
)}
/>
</div>
</ModalCrud>
</ModalCrud>
</FormProvider>
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { inviteMembersMock, membersMock } from '@qovery/shared/factories'
import { TablePrimitives } from '@qovery/shared/ui'
import { dateMediumLocalFormat } from '@qovery/shared/util-dates'
import { renderWithProviders, screen } from '@qovery/shared/util-tests'
import RowMember, { type RowMemberProps } from './row-member'

const mockOpenModalConfirmation = jest.fn()

jest.mock('@qovery/shared/ui', () => {
const actual = jest.requireActual('@qovery/shared/ui')
return {
...actual,
useModalConfirmation: () => ({
openModalConfirmation: mockOpenModalConfirmation,
}),
}
})

const { Table } = TablePrimitives

const availableRoles = [
{ id: 'role-owner', name: 'Owner' },
{ id: 'role-admin', name: 'Admin' },
]

const columnSizes = [35, 22, 21, 21]

const baseMember = {
...membersMock(1, 'Admin', 'member-1')[0],
role_id: 'role-admin',
role_name: 'Admin',
}

const baseInviteMember = {
...inviteMembersMock(1)[0],
id: 'invite-1',
email: 'invite@qovery.com',
role_id: 'role-admin',
role_name: 'Admin',
}

const renderRowMember = (overrideProps?: Partial<RowMemberProps>) => {
const props: RowMemberProps = {
member: baseMember,
editMemberRole: jest.fn(),
deleteMember: jest.fn(),
transferOwnership: jest.fn(),
columnSizes,
availableRoles,
...overrideProps,
}

return {
props,
...renderWithProviders(
<Table.Root>
<Table.Body>
<RowMember {...props} />
</Table.Body>
</Table.Root>
),
}
}

describe('RowMember', () => {
beforeEach(() => {
jest.clearAllMocks()
})

it('should render successfully', () => {
const { baseElement } = renderRowMember()
expect(baseElement).toBeTruthy()
})

it('should have disabled input for owner', () => {
const ownerMember = {
...membersMock(1, 'Owner', 'member-owner')[0],
role_id: 'role-owner',
role_name: 'Owner',
}

renderRowMember({ member: ownerMember })

expect(screen.getByTestId('input')).toBeDisabled()
})

it('should have disabled input while loading', () => {
renderRowMember({ loadingUpdateRole: true })

expect(screen.getByTestId('input')).toBeDisabled()
})

it('should show last activity and created date', () => {
renderRowMember()

const dateLastActivity = screen.getByTestId('last-activity')
const dateCreatedAt = screen.getByTestId('created-at')

expect(dateLastActivity).toHaveTextContent(/ago/i)
expect(dateCreatedAt).toHaveTextContent(dateMediumLocalFormat(baseMember.created_at))
})

it('should call editMemberRole when selecting a new role', async () => {
const { props, userEvent } = renderRowMember()

await userEvent.click(screen.getByRole('button', { name: /member role/i }))
await userEvent.click(screen.getByRole('menuitem', { name: 'Owner' }))

expect(props.editMemberRole).toHaveBeenCalledWith(baseMember.id, 'role-owner')
})

it('should call transferOwnership when user is owner', async () => {
const member = { ...baseMember }
const transferOwnership = jest.fn()

const { userEvent } = renderRowMember({ member, transferOwnership, userIsOwner: true })

await userEvent.click(screen.getByRole('button', { name: /member actions/i }))
await userEvent.click(screen.getByText('Transfer ownership'))

expect(transferOwnership).toHaveBeenCalledWith(member)
})

it('should call resendInvite for pending members', async () => {
const resendInvite = jest.fn()

const { userEvent } = renderRowMember({
member: baseInviteMember,
resendInvite,
})

await userEvent.click(screen.getByRole('button', { name: /invite actions/i }))
await userEvent.click(screen.getByText('Resend invite'))

expect(resendInvite).toHaveBeenCalledWith(baseInviteMember.id, {
email: baseInviteMember.email,
role_id: baseInviteMember.role_id,
})
})
})
Loading