refactor(web): migrate members invite overlays to base ui (#33922)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
yyh 2026-03-23 16:31:41 +08:00 committed by GitHub
parent edb261bc90
commit dc1a68661c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 223 additions and 264 deletions

View File

@ -78,6 +78,7 @@ const ApiBasedExtensionModal: FC<ApiBasedExtensionModalProps> = ({
<Modal
isShow
onClose={noop}
wrapperClassName="z-[1002]"
className="!w-[640px] !max-w-none !p-8 !pb-6"
>
<div className="mb-2 text-xl font-semibold text-text-primary">

View File

@ -69,7 +69,7 @@ const ApiBasedExtensionSelector: FC<ApiBasedExtensionSelectorProps> = ({
)
}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-[102] w-[calc(100%-32px)] max-w-[576px]">
<PortalToFollowElemContent className="z-[1002] w-[calc(100%-32px)] max-w-[576px]">
<div className="z-10 w-full rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg">
<div className="p-1">
<div className="flex items-center justify-between px-3 pb-1 pt-2">

View File

@ -84,7 +84,7 @@ const Configure = ({
{t('dataSource.configure', { ns: 'common' })}
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-[61]">
<PortalToFollowElemContent className="z-[1002]">
<div className="w-[240px] space-y-1.5 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-2 shadow-lg">
{
!!canOAuth && (
@ -104,7 +104,7 @@ const Configure = ({
}
{
!!canApiKey && !!canOAuth && (
<div className="system-2xs-medium-uppercase flex h-4 items-center p-2 text-text-quaternary">
<div className="flex h-4 items-center p-2 text-text-quaternary system-2xs-medium-uppercase">
<div className="mr-2 h-[1px] grow bg-gradient-to-l from-[rgba(16,24,40,0.08)]" />
OR
<div className="ml-2 h-[1px] grow bg-gradient-to-r from-[rgba(16,24,40,0.08)]" />

View File

@ -39,7 +39,7 @@ const Operator = ({
text: (
<div className="flex items-center">
<RiHome9Line className="mr-2 h-4 w-4 text-text-tertiary" />
<div className="system-sm-semibold text-text-secondary">{t('auth.setDefault', { ns: 'plugin' })}</div>
<div className="text-text-secondary system-sm-semibold">{t('auth.setDefault', { ns: 'plugin' })}</div>
</div>
),
},
@ -51,7 +51,7 @@ const Operator = ({
text: (
<div className="flex items-center">
<RiEditLine className="mr-2 h-4 w-4 text-text-tertiary" />
<div className="system-sm-semibold text-text-secondary">{t('operation.rename', { ns: 'common' })}</div>
<div className="text-text-secondary system-sm-semibold">{t('operation.rename', { ns: 'common' })}</div>
</div>
),
},
@ -66,7 +66,7 @@ const Operator = ({
text: (
<div className="flex items-center">
<RiEqualizer2Line className="mr-2 h-4 w-4 text-text-tertiary" />
<div className="system-sm-semibold text-text-secondary">{t('operation.edit', { ns: 'common' })}</div>
<div className="text-text-secondary system-sm-semibold">{t('operation.edit', { ns: 'common' })}</div>
</div>
),
},
@ -81,7 +81,7 @@ const Operator = ({
text: (
<div className="flex items-center">
<RiStickyNoteAddLine className="mr-2 h-4 w-4 text-text-tertiary" />
<div className="system-sm-semibold mb-1 text-text-secondary">{t('dataSource.notion.changeAuthorizedPages', { ns: 'common' })}</div>
<div className="mb-1 text-text-secondary system-sm-semibold">{t('dataSource.notion.changeAuthorizedPages', { ns: 'common' })}</div>
</div>
),
},
@ -98,7 +98,7 @@ const Operator = ({
text: (
<div className="flex items-center">
<RiDeleteBinLine className="mr-2 h-4 w-4 text-text-tertiary" />
<div className="system-sm-semibold text-text-secondary">
<div className="text-text-secondary system-sm-semibold">
{t('operation.remove', { ns: 'common' })}
</div>
</div>
@ -122,7 +122,7 @@ const Operator = ({
items={items}
secondItems={secondItems}
onSelect={handleSelect}
popupClassName="z-[61]"
popupClassName="z-[1002]"
triggerProps={{
size: 'l',
}}

View File

@ -2,11 +2,15 @@ import type { InvitationResponse } from '@/models/common'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { vi } from 'vitest'
import { ToastContext } from '@/app/components/base/toast/context'
import { toast } from '@/app/components/base/ui/toast'
import { useProviderContextSelector } from '@/context/provider-context'
import { inviteMember } from '@/service/common'
import InviteModal from '../index'
const { mockToastError } = vi.hoisted(() => ({
mockToastError: vi.fn(),
}))
vi.mock('@/context/provider-context', () => ({
useProviderContextSelector: vi.fn(),
useProviderContext: vi.fn(() => ({
@ -14,6 +18,11 @@ vi.mock('@/context/provider-context', () => ({
})),
}))
vi.mock('@/service/common')
vi.mock('@/app/components/base/ui/toast', () => ({
toast: {
error: mockToastError,
},
}))
vi.mock('@/context/i18n', () => ({
useLocale: () => 'en-US',
}))
@ -37,7 +46,6 @@ describe('InviteModal', () => {
const mockOnCancel = vi.fn()
const mockOnSend = vi.fn()
const mockRefreshLicenseLimit = vi.fn()
const mockNotify = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
@ -49,10 +57,11 @@ describe('InviteModal', () => {
})
const renderModal = (isEmailSetup = true) => render(
<ToastContext.Provider value={{ notify: mockNotify, close: vi.fn() }}>
<InviteModal isEmailSetup={isEmailSetup} onCancel={mockOnCancel} onSend={mockOnSend} />
</ToastContext.Provider>,
<InviteModal isEmailSetup={isEmailSetup} onCancel={mockOnCancel} onSend={mockOnSend} />,
)
const fillEmails = (value: string) => {
fireEvent.change(screen.getByTestId('mock-email-input'), { target: { value } })
}
it('should render invite modal content', async () => {
renderModal()
@ -68,12 +77,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 +89,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 +108,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 +120,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 +127,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,15 +145,11 @@ 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({
type: 'error',
message: 'common.members.emailInvalid',
})
expect(toast.error).toHaveBeenCalledWith('common.members.emailInvalid')
expect(inviteMember).not.toHaveBeenCalled()
})
@ -160,8 +157,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 +199,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 +210,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 +217,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 +234,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 +256,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 +264,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 +282,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 +308,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')

View File

@ -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;
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>

View File

@ -102,7 +102,7 @@ const Operation = ({
<ChevronDownIcon className={cn('h-4 w-4 shrink-0 group-hover:block', open ? 'block' : 'hidden')} />
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-[999]">
<PortalToFollowElemContent className="z-[1002]">
<div className={cn('inline-flex flex-col rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm')}>
<div className="p-1">
{

View File

@ -141,6 +141,7 @@ const TransferOwnershipModal = ({ onClose, show }: Props) => {
<Modal
isShow={show}
onClose={noop}
wrapperClassName="z-[1002]"
className="!w-[420px] !p-6"
>
<div

View File

@ -77,7 +77,7 @@ const MemberSelector: FC<Props> = ({
<div className={cn('i-ri-arrow-down-s-line h-4 w-4 text-text-quaternary group-hover:text-text-secondary', open && 'text-text-secondary')} />
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-[1000]">
<PortalToFollowElemContent className="z-[1002]">
<div className="min-w-[372px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm">
<div className="p-2 pb-1">
<Input

View File

@ -116,7 +116,7 @@ const AddCustomModel = ({
>
{renderTrigger(open)}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-[100]">
<PortalToFollowElemContent className="z-[1002]">
<div className="w-[320px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg">
<div className="max-h-[304px] overflow-y-auto p-1">
{
@ -136,7 +136,7 @@ const AddCustomModel = ({
modelName={model.model}
/>
<div
className="system-md-regular grow truncate text-text-primary"
className="grow truncate text-text-primary system-md-regular"
title={model.model}
>
{model.model}
@ -148,7 +148,7 @@ const AddCustomModel = ({
{
!notAllowCustomCredential && (
<div
className="system-xs-medium flex cursor-pointer items-center border-t border-t-divider-subtle p-3 text-text-accent-light-mode-only"
className="flex cursor-pointer items-center border-t border-t-divider-subtle p-3 text-text-accent-light-mode-only system-xs-medium"
onClick={() => {
handleOpenModalForAddNewCustomModel()
setOpen(false)

View File

@ -164,7 +164,7 @@ const Authorized = ({
>
{renderTrigger(mergedIsOpen)}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-[100]">
<PortalToFollowElemContent className="z-[1002]">
<div className={cn(
'w-[360px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px]',
popupClassName,
@ -172,7 +172,7 @@ const Authorized = ({
>
{
popupTitle && (
<div className="system-xs-medium px-3 pb-0.5 pt-[10px] text-text-tertiary">
<div className="px-3 pb-0.5 pt-[10px] text-text-tertiary system-xs-medium">
{popupTitle}
</div>
)
@ -218,7 +218,7 @@ const Authorized = ({
}
: undefined,
)}
className="system-xs-medium flex h-[40px] cursor-pointer items-center px-3 text-text-accent-light-mode-only"
className="flex h-[40px] cursor-pointer items-center px-3 text-text-accent-light-mode-only system-xs-medium"
>
<RiAddLine className="mr-1 h-4 w-4" />
{t('modelProvider.auth.addModelCredential', { ns: 'common' })}

View File

@ -53,14 +53,14 @@ const CredentialSelector = ({
triggerPopupSameWidth
>
<PortalToFollowElemTrigger asChild onClick={() => !disabled && setOpen(v => !v)}>
<div className="system-sm-regular flex h-8 w-full items-center justify-between rounded-lg bg-components-input-bg-normal px-2">
<div className="flex h-8 w-full items-center justify-between rounded-lg bg-components-input-bg-normal px-2 system-sm-regular">
{
selectedCredential && (
<div className="flex items-center">
{
!selectedCredential.addNewCredential && <Indicator className="ml-1 mr-2 shrink-0" />
}
<div className="system-sm-regular truncate text-components-input-text-filled" title={selectedCredential.credential_name}>{selectedCredential.credential_name}</div>
<div className="truncate text-components-input-text-filled system-sm-regular" title={selectedCredential.credential_name}>{selectedCredential.credential_name}</div>
{
selectedCredential.from_enterprise && (
<Badge className="shrink-0">Enterprise</Badge>
@ -71,13 +71,13 @@ const CredentialSelector = ({
}
{
!selectedCredential && (
<div className="system-sm-regular grow truncate text-components-input-text-placeholder">{t('modelProvider.auth.selectModelCredential', { ns: 'common' })}</div>
<div className="grow truncate text-components-input-text-placeholder system-sm-regular">{t('modelProvider.auth.selectModelCredential', { ns: 'common' })}</div>
)
}
<RiArrowDownSLine className="h-4 w-4 text-text-quaternary" />
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-[100]">
<PortalToFollowElemContent className="z-[1002]">
<div className="border-ccomponents-panel-border rounded-xl border-[0.5px] bg-components-panel-bg-blur shadow-lg">
<div className="max-h-[320px] overflow-y-auto p-1">
{
@ -98,7 +98,7 @@ const CredentialSelector = ({
{
!notAllowAddNewCredential && (
<div
className="system-xs-medium flex h-10 cursor-pointer items-center border-t border-t-divider-subtle px-7 text-text-accent-light-mode-only"
className="flex h-10 cursor-pointer items-center border-t border-t-divider-subtle px-7 text-text-accent-light-mode-only system-xs-medium"
onClick={handleAddNewCredential}
>
<RiAddLine className="mr-1 h-4 w-4" />

View File

@ -244,6 +244,7 @@ const ModelLoadBalancingModal = ({
<Modal
isShow={Boolean(model) && open}
onClose={onClose}
wrapperClassName="z-[1002]"
className="w-[640px] max-w-none px-8 pt-8"
title={(
<div className="pb-3 font-semibold">

View File

@ -4700,9 +4700,6 @@
"app/components/header/account-setting/data-source-page-new/configure.tsx": {
"no-restricted-imports": {
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 1
}
},
"app/components/header/account-setting/data-source-page-new/hooks/use-marketplace-all-plugins.ts": {
@ -4729,9 +4726,6 @@
"app/components/header/account-setting/data-source-page-new/operator.tsx": {
"no-restricted-imports": {
"count": 2
},
"tailwindcss/enforce-consistent-class-order": {
"count": 5
}
},
"app/components/header/account-setting/data-source-page-new/types.ts": {
@ -4758,28 +4752,10 @@
}
},
"app/components/header/account-setting/members-page/invite-modal/index.tsx": {
"no-restricted-imports": {
"count": 2
},
"react/set-state-in-effect": {
"count": 3
}
},
"app/components/header/account-setting/members-page/invite-modal/role-selector.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"app/components/header/account-setting/members-page/invited-modal/index.tsx": {
"no-restricted-imports": {
"count": 2
}
},
"app/components/header/account-setting/members-page/invited-modal/invitation-link.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"app/components/header/account-setting/members-page/operation/index.tsx": {
"no-restricted-imports": {
"count": 2
@ -4833,9 +4809,6 @@
"app/components/header/account-setting/model-provider-page/model-auth/add-custom-model.tsx": {
"no-restricted-imports": {
"count": 2
},
"tailwindcss/enforce-consistent-class-order": {
"count": 2
}
},
"app/components/header/account-setting/model-provider-page/model-auth/authorized/authorized-item.tsx": {
@ -4847,9 +4820,6 @@
"no-restricted-imports": {
"count": 3
},
"tailwindcss/enforce-consistent-class-order": {
"count": 2
},
"ts/no-explicit-any": {
"count": 2
}
@ -4867,9 +4837,6 @@
"app/components/header/account-setting/model-provider-page/model-auth/credential-selector.tsx": {
"no-restricted-imports": {
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 4
}
},
"app/components/header/account-setting/model-provider-page/model-auth/hooks/use-auth.ts": {