mirror of
https://github.com/langgenius/dify.git
synced 2026-03-24 15:57:55 +08:00
refactor(web): migrate members invite overlays to base ui
This commit is contained in:
@ -53,6 +53,9 @@ describe('InviteModal', () => {
|
||||
<InviteModal isEmailSetup={isEmailSetup} onCancel={mockOnCancel} onSend={mockOnSend} />
|
||||
</ToastContext.Provider>,
|
||||
)
|
||||
const fillEmails = (value: string) => {
|
||||
fireEvent.change(screen.getByTestId('mock-email-input'), { target: { value } })
|
||||
}
|
||||
|
||||
it('should render invite modal content', async () => {
|
||||
renderModal()
|
||||
@ -68,12 +71,8 @@ describe('InviteModal', () => {
|
||||
})
|
||||
|
||||
it('should enable send button after entering an email', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
renderModal()
|
||||
|
||||
const input = screen.getByTestId('mock-email-input')
|
||||
await user.type(input, 'user@example.com')
|
||||
fillEmails('user@example.com')
|
||||
|
||||
expect(screen.getByRole('button', { name: /members\.sendInvite/i })).toBeEnabled()
|
||||
})
|
||||
@ -84,7 +83,7 @@ describe('InviteModal', () => {
|
||||
|
||||
renderModal()
|
||||
|
||||
await user.type(screen.getByTestId('mock-email-input'), 'user@example.com')
|
||||
fillEmails('user@example.com')
|
||||
await user.click(screen.getByRole('button', { name: /members\.sendInvite/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
@ -103,8 +102,7 @@ describe('InviteModal', () => {
|
||||
|
||||
renderModal()
|
||||
|
||||
const input = screen.getByTestId('mock-email-input')
|
||||
await user.type(input, 'user@example.com')
|
||||
fillEmails('user@example.com')
|
||||
await user.click(screen.getByRole('button', { name: /members\.sendInvite/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
@ -116,8 +114,6 @@ describe('InviteModal', () => {
|
||||
})
|
||||
|
||||
it('should keep send button disabled when license limit is exceeded', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
vi.mocked(useProviderContextSelector).mockImplementation(selector => selector({
|
||||
licenseLimit: { workspace_members: { size: 10, limit: 10 } },
|
||||
refreshLicenseLimit: mockRefreshLicenseLimit,
|
||||
@ -125,8 +121,7 @@ describe('InviteModal', () => {
|
||||
|
||||
renderModal()
|
||||
|
||||
const input = screen.getByTestId('mock-email-input')
|
||||
await user.type(input, 'user@example.com')
|
||||
fillEmails('user@example.com')
|
||||
|
||||
expect(screen.getByRole('button', { name: /members\.sendInvite/i })).toBeDisabled()
|
||||
})
|
||||
@ -144,9 +139,8 @@ describe('InviteModal', () => {
|
||||
const user = userEvent.setup()
|
||||
renderModal()
|
||||
|
||||
const input = screen.getByTestId('mock-email-input')
|
||||
// Use an email that passes basic validation but fails our strict regex (needs 2+ char TLD)
|
||||
await user.type(input, 'invalid@email.c')
|
||||
fillEmails('invalid@email.c')
|
||||
await user.click(screen.getByRole('button', { name: /members\.sendInvite/i }))
|
||||
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
@ -160,8 +154,7 @@ describe('InviteModal', () => {
|
||||
const user = userEvent.setup()
|
||||
renderModal()
|
||||
|
||||
const input = screen.getByTestId('mock-email-input')
|
||||
await user.type(input, 'user@example.com')
|
||||
fillEmails('user@example.com')
|
||||
|
||||
expect(screen.getByText('user@example.com')).toBeInTheDocument()
|
||||
|
||||
@ -203,7 +196,7 @@ describe('InviteModal', () => {
|
||||
|
||||
renderModal()
|
||||
|
||||
await user.type(screen.getByTestId('mock-email-input'), 'user@example.com')
|
||||
fillEmails('user@example.com')
|
||||
await user.click(screen.getByRole('button', { name: /members\.sendInvite/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
@ -214,8 +207,6 @@ describe('InviteModal', () => {
|
||||
})
|
||||
|
||||
it('should show destructive text color when used size exceeds limit', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
vi.mocked(useProviderContextSelector).mockImplementation(selector => selector({
|
||||
licenseLimit: { workspace_members: { size: 10, limit: 10 } },
|
||||
refreshLicenseLimit: mockRefreshLicenseLimit,
|
||||
@ -223,8 +214,7 @@ describe('InviteModal', () => {
|
||||
|
||||
renderModal()
|
||||
|
||||
const input = screen.getByTestId('mock-email-input')
|
||||
await user.type(input, 'user@example.com')
|
||||
fillEmails('user@example.com')
|
||||
|
||||
// usedSize = 10 + 1 = 11 > limit 10 → destructive color
|
||||
const counter = screen.getByText('11')
|
||||
@ -241,8 +231,7 @@ describe('InviteModal', () => {
|
||||
|
||||
renderModal()
|
||||
|
||||
const input = screen.getByTestId('mock-email-input')
|
||||
await user.type(input, 'user@example.com')
|
||||
fillEmails('user@example.com')
|
||||
|
||||
const sendBtn = screen.getByRole('button', { name: /members\.sendInvite/i })
|
||||
|
||||
@ -264,8 +253,6 @@ describe('InviteModal', () => {
|
||||
})
|
||||
|
||||
it('should show destructive color and disable send button when limit is exactly met with one email', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
// size=10, limit=10 - adding 1 email makes usedSize=11 > limit=10
|
||||
vi.mocked(useProviderContextSelector).mockImplementation(selector => selector({
|
||||
licenseLimit: { workspace_members: { size: 10, limit: 10 } },
|
||||
@ -274,8 +261,7 @@ describe('InviteModal', () => {
|
||||
|
||||
renderModal()
|
||||
|
||||
const input = screen.getByTestId('mock-email-input')
|
||||
await user.type(input, 'user@example.com')
|
||||
fillEmails('user@example.com')
|
||||
|
||||
// isLimitExceeded=true → button is disabled, cannot submit
|
||||
const sendBtn = screen.getByRole('button', { name: /members\.sendInvite/i })
|
||||
@ -293,8 +279,7 @@ describe('InviteModal', () => {
|
||||
|
||||
renderModal()
|
||||
|
||||
const input = screen.getByTestId('mock-email-input')
|
||||
await user.type(input, 'user@example.com')
|
||||
fillEmails('user@example.com')
|
||||
|
||||
const sendBtn = screen.getByRole('button', { name: /members\.sendInvite/i })
|
||||
|
||||
@ -320,11 +305,9 @@ describe('InviteModal', () => {
|
||||
refreshLicenseLimit: mockRefreshLicenseLimit,
|
||||
} as unknown as Parameters<typeof selector>[0]))
|
||||
|
||||
const user = userEvent.setup()
|
||||
renderModal()
|
||||
|
||||
const input = screen.getByTestId('mock-email-input')
|
||||
await user.type(input, 'user@example.com')
|
||||
fillEmails('user@example.com')
|
||||
|
||||
// isLimited=false → no destructive color
|
||||
const counter = screen.getByText('1')
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
.modal {
|
||||
padding: 24px 32px !important;
|
||||
width: 400px !important;
|
||||
}
|
||||
|
||||
.emailsInput {
|
||||
background-color: rgb(243 244 246 / var(--tw-bg-opacity)) !important;
|
||||
}
|
||||
|
||||
.emailBackground {
|
||||
background-color: white !important;
|
||||
}
|
||||
@ -2,20 +2,17 @@
|
||||
import type { RoleKey } from './role-selector'
|
||||
import type { InvitationResult } from '@/models/common'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ReactMultiEmail } from 'react-multi-email'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@/app/components/base/ui/dialog'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import { emailRegex } from '@/config'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useProviderContextSelector } from '@/context/provider-context'
|
||||
import { inviteMember } from '@/service/common'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import s from './index.module.css'
|
||||
import RoleSelector from './role-selector'
|
||||
import 'react-multi-email/dist/style.css'
|
||||
|
||||
@ -34,7 +31,6 @@ const InviteModal = ({
|
||||
const licenseLimit = useProviderContextSelector(s => s.licenseLimit)
|
||||
const refreshLicenseLimit = useProviderContextSelector(s => s.refreshLicenseLimit)
|
||||
const [emails, setEmails] = useState<string[]>([])
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [isLimited, setIsLimited] = useState(false)
|
||||
const [isLimitExceeded, setIsLimitExceeded] = useState(false)
|
||||
const [usedSize, setUsedSize] = useState(licenseLimit.workspace_members.size ?? 0)
|
||||
@ -74,21 +70,28 @@ const InviteModal = ({
|
||||
catch { }
|
||||
}
|
||||
else {
|
||||
notify({ type: 'error', message: t('members.emailInvalid', { ns: 'common' }) })
|
||||
toast.error(t('members.emailInvalid', { ns: 'common' }))
|
||||
}
|
||||
setIsSubmitted()
|
||||
}, [isLimitExceeded, emails, role, locale, onCancel, onSend, notify, t, isSubmitting, refreshLicenseLimit, setIsSubmitted, setIsSubmitting])
|
||||
}, [isLimitExceeded, emails, role, locale, onCancel, onSend, t, isSubmitting, refreshLicenseLimit, setIsSubmitted, setIsSubmitting])
|
||||
|
||||
return (
|
||||
<div className={cn(s.wrap)}>
|
||||
<Modal overflowVisible isShow onClose={noop} className={cn(s.modal)}>
|
||||
<div className="mb-2 flex justify-between">
|
||||
<div className="text-xl font-semibold text-text-primary">{t('members.inviteTeamMember', { ns: 'common' })}</div>
|
||||
<div
|
||||
data-testid="invite-modal-close"
|
||||
className="i-ri-close-line h-4 w-4 cursor-pointer text-text-tertiary"
|
||||
onClick={onCancel}
|
||||
/>
|
||||
<Dialog
|
||||
open
|
||||
onOpenChange={(open) => {
|
||||
if (!open)
|
||||
onCancel()
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
backdropProps={{ forceRender: true }}
|
||||
className="w-[400px] overflow-visible px-8 py-6"
|
||||
>
|
||||
<DialogCloseButton data-testid="invite-modal-close" className="right-8 top-6" />
|
||||
<div className="mb-2 pr-8">
|
||||
<DialogTitle className="text-xl font-semibold text-text-primary">
|
||||
{t('members.inviteTeamMember', { ns: 'common' })}
|
||||
</DialogTitle>
|
||||
</div>
|
||||
<div className="mb-3 text-[13px] text-text-tertiary">{t('members.inviteTeamMemberTip', { ns: 'common' })}</div>
|
||||
{!isEmailSetup && (
|
||||
@ -152,8 +155,8 @@ const InviteModal = ({
|
||||
{t('members.sendInvite', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/app/components/base/ui/popover'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
@ -25,115 +24,111 @@ export type RoleSelectorProps = {
|
||||
|
||||
const RoleSelector = ({ value, onChange }: RoleSelectorProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const { datasetOperatorEnabled } = useProviderContext()
|
||||
const [open, setOpen] = React.useState(false)
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement="bottom-start"
|
||||
offset={4}
|
||||
>
|
||||
<div className="relative">
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => setOpen(v => !v)}
|
||||
className="block"
|
||||
>
|
||||
<PopoverTrigger
|
||||
data-testid="role-selector-trigger"
|
||||
className={cn(
|
||||
'flex w-full cursor-pointer items-center rounded-lg bg-components-input-bg-normal px-3 py-2 hover:bg-state-base-hover',
|
||||
open && 'bg-state-base-hover',
|
||||
)}
|
||||
>
|
||||
<div className="mr-2 grow text-sm leading-5 text-text-primary">{t('members.invitedAsRole', { ns: 'common', role: t(roleI18nKeyMap[value], { ns: 'common' }) })}</div>
|
||||
<div className="i-ri-arrow-down-s-line h-4 w-4 shrink-0 text-text-secondary" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
popupClassName="w-[336px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg"
|
||||
>
|
||||
<div className="p-1">
|
||||
<div
|
||||
data-testid="role-selector-trigger"
|
||||
className={cn('flex cursor-pointer items-center rounded-lg bg-components-input-bg-normal px-3 py-2 hover:bg-state-base-hover', open && 'bg-state-base-hover')}
|
||||
data-testid="role-option-normal"
|
||||
className="cursor-pointer rounded-lg p-2 hover:bg-state-base-hover"
|
||||
onClick={() => {
|
||||
onChange('normal')
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<div className="mr-2 grow text-sm leading-5 text-text-primary">{t('members.invitedAsRole', { ns: 'common', role: t(roleI18nKeyMap[value], { ns: 'common' }) })}</div>
|
||||
<div className="i-ri-arrow-down-s-line h-4 w-4 shrink-0 text-text-secondary" />
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className="z-[1002]">
|
||||
<div className="relative w-[336px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg">
|
||||
<div className="p-1">
|
||||
<div
|
||||
data-testid="role-option-normal"
|
||||
className="cursor-pointer rounded-lg p-2 hover:bg-state-base-hover"
|
||||
onClick={() => {
|
||||
onChange('normal')
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<div className="relative pl-5">
|
||||
<div className="text-sm leading-5 text-text-secondary">{t('members.normal', { ns: 'common' })}</div>
|
||||
<div className="text-xs leading-[18px] text-text-tertiary">{t('members.normalTip', { ns: 'common' })}</div>
|
||||
{value === 'normal' && (
|
||||
<div
|
||||
data-testid="role-option-check"
|
||||
className="i-custom-vender-line-general-check absolute left-0 top-0.5 h-4 w-4 text-text-accent"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-testid="role-option-editor"
|
||||
className="cursor-pointer rounded-lg p-2 hover:bg-state-base-hover"
|
||||
onClick={() => {
|
||||
onChange('editor')
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<div className="relative pl-5">
|
||||
<div className="text-sm leading-5 text-text-secondary">{t('members.editor', { ns: 'common' })}</div>
|
||||
<div className="text-xs leading-[18px] text-text-tertiary">{t('members.editorTip', { ns: 'common' })}</div>
|
||||
{value === 'editor' && (
|
||||
<div
|
||||
data-testid="role-option-check"
|
||||
className="i-custom-vender-line-general-check absolute left-0 top-0.5 h-4 w-4 text-text-accent"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-testid="role-option-admin"
|
||||
className="cursor-pointer rounded-lg p-2 hover:bg-state-base-hover"
|
||||
onClick={() => {
|
||||
onChange('admin')
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<div className="relative pl-5">
|
||||
<div className="text-sm leading-5 text-text-secondary">{t('members.admin', { ns: 'common' })}</div>
|
||||
<div className="text-xs leading-[18px] text-text-tertiary">{t('members.adminTip', { ns: 'common' })}</div>
|
||||
{value === 'admin' && (
|
||||
<div
|
||||
data-testid="role-option-check"
|
||||
className="i-custom-vender-line-general-check absolute left-0 top-0.5 h-4 w-4 text-text-accent"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{datasetOperatorEnabled && (
|
||||
<div className="relative pl-5">
|
||||
<div className="text-sm leading-5 text-text-secondary">{t('members.normal', { ns: 'common' })}</div>
|
||||
<div className="text-xs leading-[18px] text-text-tertiary">{t('members.normalTip', { ns: 'common' })}</div>
|
||||
{value === 'normal' && (
|
||||
<div
|
||||
data-testid="role-option-dataset_operator"
|
||||
className="cursor-pointer rounded-lg p-2 hover:bg-state-base-hover"
|
||||
onClick={() => {
|
||||
onChange('dataset_operator')
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<div className="relative pl-5">
|
||||
<div className="text-sm leading-5 text-text-secondary">{t('members.datasetOperator', { ns: 'common' })}</div>
|
||||
<div className="text-xs leading-[18px] text-text-tertiary">{t('members.datasetOperatorTip', { ns: 'common' })}</div>
|
||||
{value === 'dataset_operator' && (
|
||||
<div
|
||||
data-testid="role-option-check"
|
||||
className="i-custom-vender-line-general-check absolute left-0 top-0.5 h-4 w-4 text-text-accent"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
data-testid="role-option-check"
|
||||
className="i-custom-vender-line-general-check absolute left-0 top-0.5 h-4 w-4 text-text-accent"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</div>
|
||||
</PortalToFollowElem>
|
||||
<div
|
||||
data-testid="role-option-editor"
|
||||
className="cursor-pointer rounded-lg p-2 hover:bg-state-base-hover"
|
||||
onClick={() => {
|
||||
onChange('editor')
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<div className="relative pl-5">
|
||||
<div className="text-sm leading-5 text-text-secondary">{t('members.editor', { ns: 'common' })}</div>
|
||||
<div className="text-xs leading-[18px] text-text-tertiary">{t('members.editorTip', { ns: 'common' })}</div>
|
||||
{value === 'editor' && (
|
||||
<div
|
||||
data-testid="role-option-check"
|
||||
className="i-custom-vender-line-general-check absolute left-0 top-0.5 h-4 w-4 text-text-accent"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-testid="role-option-admin"
|
||||
className="cursor-pointer rounded-lg p-2 hover:bg-state-base-hover"
|
||||
onClick={() => {
|
||||
onChange('admin')
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<div className="relative pl-5">
|
||||
<div className="text-sm leading-5 text-text-secondary">{t('members.admin', { ns: 'common' })}</div>
|
||||
<div className="text-xs leading-[18px] text-text-tertiary">{t('members.adminTip', { ns: 'common' })}</div>
|
||||
{value === 'admin' && (
|
||||
<div
|
||||
data-testid="role-option-check"
|
||||
className="i-custom-vender-line-general-check absolute left-0 top-0.5 h-4 w-4 text-text-accent"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{datasetOperatorEnabled && (
|
||||
<div
|
||||
data-testid="role-option-dataset_operator"
|
||||
className="cursor-pointer rounded-lg p-2 hover:bg-state-base-hover"
|
||||
onClick={() => {
|
||||
onChange('dataset_operator')
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<div className="relative pl-5">
|
||||
<div className="text-sm leading-5 text-text-secondary">{t('members.datasetOperator', { ns: 'common' })}</div>
|
||||
<div className="text-xs leading-[18px] text-text-tertiary">{t('members.datasetOperatorTip', { ns: 'common' })}</div>
|
||||
{value === 'dataset_operator' && (
|
||||
<div
|
||||
data-testid="role-option-check"
|
||||
className="i-custom-vender-line-general-check absolute left-0 top-0.5 h-4 w-4 text-text-accent"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,15 +1,10 @@
|
||||
import type { InvitationResult } from '@/models/common'
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline'
|
||||
import { CheckCircleIcon } from '@heroicons/react/24/solid'
|
||||
import { RiQuestionLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@/app/components/base/ui/dialog'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import s from './index.module.css'
|
||||
import InvitationLink from './invitation-link'
|
||||
|
||||
export type SuccessInvitationResult = Extract<InvitationResult, { status: 'success' }>
|
||||
@ -29,8 +24,18 @@ const InvitedModal = ({
|
||||
const failedInvitationResults = useMemo<FailedInvitationResult[]>(() => invitationResults?.filter(item => item.status !== 'success') as FailedInvitationResult[], [invitationResults])
|
||||
|
||||
return (
|
||||
<div className={s.wrap}>
|
||||
<Modal isShow onClose={noop} className={s.modal}>
|
||||
<Dialog
|
||||
open
|
||||
onOpenChange={(open) => {
|
||||
if (!open)
|
||||
onCancel()
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
backdropProps={{ forceRender: true }}
|
||||
className="w-[480px] p-8"
|
||||
>
|
||||
<DialogCloseButton className="right-8 top-8" />
|
||||
<div className="mb-3 flex justify-between">
|
||||
<div className="
|
||||
flex h-12 w-12 items-center justify-center rounded-xl
|
||||
@ -38,11 +43,10 @@ const InvitedModal = ({
|
||||
shadow-xl
|
||||
"
|
||||
>
|
||||
<CheckCircleIcon className="h-[22px] w-[22px] text-[#039855]" />
|
||||
<div className="i-heroicons-check-circle-solid h-[22px] w-[22px] text-[#039855]" />
|
||||
</div>
|
||||
<XMarkIcon className="h-4 w-4 cursor-pointer" onClick={onCancel} />
|
||||
</div>
|
||||
<div className="mb-1 text-xl font-semibold text-text-primary">{t('members.invitationSent', { ns: 'common' })}</div>
|
||||
<DialogTitle className="mb-1 text-xl font-semibold text-text-primary">{t('members.invitationSent', { ns: 'common' })}</DialogTitle>
|
||||
{!IS_CE_EDITION && (
|
||||
<div className="mb-10 text-sm text-text-tertiary">{t('members.invitationSentTip', { ns: 'common' })}</div>
|
||||
)}
|
||||
@ -54,7 +58,7 @@ const InvitedModal = ({
|
||||
!!successInvitationResults.length
|
||||
&& (
|
||||
<>
|
||||
<div className="font-Medium py-2 text-sm text-text-primary">{t('members.invitationLink', { ns: 'common' })}</div>
|
||||
<div className="py-2 text-sm font-medium text-text-primary">{t('members.invitationLink', { ns: 'common' })}</div>
|
||||
{successInvitationResults.map(item =>
|
||||
<InvitationLink key={item.email} value={item} />)}
|
||||
</>
|
||||
@ -64,18 +68,23 @@ const InvitedModal = ({
|
||||
!!failedInvitationResults.length
|
||||
&& (
|
||||
<>
|
||||
<div className="font-Medium py-2 text-sm text-text-primary">{t('members.failedInvitationEmails', { ns: 'common' })}</div>
|
||||
<div className="py-2 text-sm font-medium text-text-primary">{t('members.failedInvitationEmails', { ns: 'common' })}</div>
|
||||
<div className="flex flex-wrap justify-between gap-y-1">
|
||||
{
|
||||
failedInvitationResults.map(item => (
|
||||
<div key={item.email} className="flex justify-center rounded-md border border-red-300 bg-orange-50 px-1">
|
||||
<Tooltip
|
||||
popupContent={item.message}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-1 text-sm">
|
||||
{item.email}
|
||||
<RiQuestionLine className="h-4 w-4 text-red-300" />
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<div className="flex items-center justify-center gap-1 text-sm">
|
||||
{item.email}
|
||||
<div className="i-ri-question-line h-4 w-4 text-red-300" />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>
|
||||
{item.message}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
),
|
||||
@ -97,8 +106,8 @@ const InvitedModal = ({
|
||||
{t('members.ok', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import copy from 'copy-to-clipboard'
|
||||
import { t } from 'i18next'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip'
|
||||
import s from './index.module.css'
|
||||
|
||||
type IInvitationLinkProps = {
|
||||
@ -38,20 +38,28 @@ const InvitationLink = ({
|
||||
<div className="flex items-center rounded-lg border border-components-input-border-active bg-components-input-bg-normal py-2 hover:bg-state-base-hover" data-testid="invitation-link-container">
|
||||
<div className="flex h-5 grow items-center">
|
||||
<div className="relative h-full grow text-[13px]">
|
||||
<Tooltip
|
||||
popupContent={isCopied ? `${t('copied', { ns: 'appApi' })}` : `${t('copy', { ns: 'appApi' })}`}
|
||||
>
|
||||
<div className="absolute left-0 right-0 top-0 w-full cursor-pointer truncate pl-2 pr-2 text-text-primary" onClick={copyHandle} data-testid="invitation-link-url">{value.url}</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={<div className="absolute left-0 right-0 top-0 w-full cursor-pointer truncate pl-2 pr-2 text-text-primary" onClick={copyHandle} data-testid="invitation-link-url">{value.url}</div>}
|
||||
/>
|
||||
<TooltipContent>
|
||||
{isCopied ? t('copied', { ns: 'appApi' }) : t('copy', { ns: 'appApi' })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="h-4 shrink-0 border bg-divider-regular" />
|
||||
<Tooltip
|
||||
popupContent={isCopied ? `${t('copied', { ns: 'appApi' })}` : `${t('copy', { ns: 'appApi' })}`}
|
||||
>
|
||||
<div className="shrink-0 px-0.5">
|
||||
<div className={`box-border flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded-lg hover:bg-state-base-hover ${s.copyIcon} ${isCopied ? s.copied : ''}`} onClick={copyHandle} data-testid="invitation-link-copy">
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<div className="shrink-0 px-0.5">
|
||||
<div className={`box-border flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded-lg hover:bg-state-base-hover ${s.copyIcon} ${isCopied ? s.copied : ''}`} onClick={copyHandle} data-testid="invitation-link-copy">
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>
|
||||
{isCopied ? t('copied', { ns: 'appApi' }) : t('copy', { ns: 'appApi' })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user