Merge branch 'feat/model-plugins-implementing' into deploy/dev

This commit is contained in:
yyh
2026-03-11 22:23:12 +08:00
17 changed files with 220 additions and 175 deletions

View File

@ -170,9 +170,13 @@ vi.mock('@/service/use-plugins', () => ({
}))
// Mock config
vi.mock('@/config', () => ({
MARKETPLACE_API_PREFIX: 'https://marketplace.example.com',
}))
vi.mock('@/config', async () => {
const actual = await vi.importActual<typeof import('@/config')>('@/config')
return {
...actual,
MARKETPLACE_API_PREFIX: 'https://marketplace.example.com',
}
})
// Mock mitt context
vi.mock('@/context/mitt-context', () => ({

View File

@ -9,40 +9,6 @@ import ModelParameterModal from '../index'
// ==================== Mock Setup ====================
// Mock shared state for portal
let mockPortalOpenState = false
vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
PortalToFollowElem: ({ children, open }: { children: React.ReactNode, open: boolean }) => {
mockPortalOpenState = open || false
return (
<div data-testid="portal-elem" data-open={open}>
{children}
</div>
)
},
PortalToFollowElemTrigger: ({ children, onClick, className }: { children: React.ReactNode, onClick: () => void, className?: string }) => (
<div data-testid="portal-trigger" onClick={onClick} className={className}>
{children}
</div>
),
PortalToFollowElemContent: ({ children, className }: { children: React.ReactNode, className?: string }) => {
if (!mockPortalOpenState)
return null
return (
<div data-testid="portal-content" className={className}>
{children}
</div>
)
},
}))
vi.mock('@/app/components/base/toast', () => ({
default: {
notify: vi.fn(),
},
}))
// Mock provider context
const mockProviderContextValue = {
isAPIKeySet: true,
@ -87,6 +53,8 @@ vi.mock('@/utils/completion-params', () => ({
fetchAndMergeValidCompletionParams: (...args: unknown[]) => mockFetchAndMergeValidCompletionParams(...args),
}))
const mockToastNotify = vi.spyOn(Toast, 'notify')
// Mock child components
vi.mock('@/app/components/header/account-setting/model-provider-page/model-selector', () => ({
default: ({ defaultModel, modelList, scopeFeatures, onSelect }: {
@ -108,30 +76,33 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/model-selec
}))
vi.mock('@/app/components/header/account-setting/model-provider-page/model-parameter-modal/trigger', () => ({
default: ({ disabled, hasDeprecated, modelDisabled, currentProvider, currentModel, providerName, modelId, isInWorkflow }: {
disabled?: boolean
hasDeprecated?: boolean
modelDisabled?: boolean
default: ({ currentProvider, currentModel, providerName, modelId, isInWorkflow }: {
currentProvider?: Model
currentModel?: ModelItem
providerName?: string
modelId?: string
isInWorkflow?: boolean
}) => (
<div
data-testid="trigger"
data-disabled={disabled}
data-has-deprecated={hasDeprecated}
data-model-disabled={modelDisabled}
data-provider={providerName}
data-model={modelId}
data-in-workflow={isInWorkflow}
data-has-current-provider={!!currentProvider}
data-has-current-model={!!currentModel}
>
Trigger
</div>
),
}) => {
const hasDeprecated = !currentProvider || !currentModel
const modelDisabled = currentModel?.status !== ModelStatusEnum.active
const disabled = !mockProviderContextValue.isAPIKeySet || hasDeprecated || modelDisabled
return (
<div
data-testid="trigger"
data-disabled={disabled}
data-has-deprecated={hasDeprecated}
data-model-disabled={modelDisabled}
data-provider={providerName}
data-model={modelId}
data-in-workflow={isInWorkflow}
data-has-current-provider={!!currentProvider}
data-has-current-model={!!currentModel}
>
Trigger
</div>
)
},
}))
vi.mock('@/app/components/header/account-setting/model-provider-page/model-parameter-modal/agent-model-trigger', () => ({
@ -273,7 +244,7 @@ const setupModelLists = (config: {
describe('ModelParameterModal', () => {
beforeEach(() => {
vi.clearAllMocks()
mockPortalOpenState = false
mockToastNotify.mockReturnValue({})
mockProviderContextValue.isAPIKeySet = true
mockProviderContextValue.modelProviders = []
setupModelLists()
@ -356,7 +327,7 @@ describe('ModelParameterModal', () => {
render(<ModelParameterModal {...props} />)
// Assert
expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument()
expect(screen.queryByTestId('model-selector')).not.toBeInTheDocument()
})
it('should render model selector inside portal content when open', async () => {
@ -365,13 +336,12 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
// Assert
await waitFor(() => {
expect(screen.getByTestId('portal-content')).toBeInTheDocument()
expect(screen.getByTestId('model-selector')).toBeInTheDocument()
})
expect(screen.getByTestId('model-selector')).toBeInTheDocument()
})
})
@ -405,12 +375,11 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
// Assert
await waitFor(() => {
const content = screen.getByTestId('portal-content')
expect(content.querySelector('.custom-popup-class')).toBeInTheDocument()
expect(document.querySelector('.custom-popup-class')).toBeInTheDocument()
})
})
@ -422,7 +391,7 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
// Assert
const selector = screen.getByTestId('model-selector')
@ -438,13 +407,13 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument()
expect(screen.queryByTestId('model-selector')).not.toBeInTheDocument()
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
// Assert
await waitFor(() => {
expect(screen.getByTestId('portal-content')).toBeInTheDocument()
expect(screen.getByTestId('model-selector')).toBeInTheDocument()
})
})
@ -454,15 +423,15 @@ describe('ModelParameterModal', () => {
// Act
const { rerender } = render(<ModelParameterModal {...props} />)
expect(screen.getByTestId('portal-elem')).toHaveAttribute('data-open', 'false')
expect(screen.queryByTestId('model-selector')).not.toBeInTheDocument()
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
// Force a re-render to ensure state is stable
rerender(<ModelParameterModal {...props} />)
// Assert - open state should remain false due to readonly
expect(screen.getByTestId('portal-elem')).toHaveAttribute('data-open', 'false')
expect(screen.queryByTestId('model-selector')).not.toBeInTheDocument()
})
})
@ -474,7 +443,7 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
// Assert
await waitFor(() => {
@ -489,7 +458,7 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
// Assert
await waitFor(() => {
@ -512,7 +481,7 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
// Assert
await waitFor(() => {
@ -530,7 +499,7 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
// Assert
await waitFor(() => {
@ -547,7 +516,7 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
// Assert
await waitFor(() => {
@ -564,7 +533,7 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
// Assert
await waitFor(() => {
@ -581,7 +550,7 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
// Assert
await waitFor(() => {
@ -598,7 +567,7 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
// Assert
await waitFor(() => {
@ -615,7 +584,7 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
// Assert
await waitFor(() => {
@ -632,7 +601,7 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
// Assert
await waitFor(() => {
@ -831,7 +800,7 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
await waitFor(() => {
fireEvent.click(screen.getByTestId('model-selector'))
@ -856,7 +825,7 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
await waitFor(() => {
fireEvent.click(screen.getByTestId('model-selector'))
@ -888,7 +857,7 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
await waitFor(() => {
fireEvent.click(screen.getByTestId('model-selector'))
@ -915,7 +884,7 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
await waitFor(() => {
fireEvent.click(screen.getByTestId('model-selector'))
@ -951,7 +920,7 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
await waitFor(() => {
const panel = screen.getByTestId('llm-params-panel')
@ -988,7 +957,7 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
await waitFor(() => {
const panel = screen.getByTestId('tts-params-panel')
@ -1025,7 +994,7 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
// Assert
await waitFor(() => {
@ -1051,7 +1020,7 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
// Assert
await waitFor(() => {
@ -1077,7 +1046,7 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
// Assert
await waitFor(() => {
@ -1104,12 +1073,11 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
// Assert
await waitFor(() => {
const content = screen.getByTestId('portal-content')
expect(content.querySelector('.bg-divider-subtle')).toBeInTheDocument()
expect(document.querySelector('.bg-divider-subtle')).toBeInTheDocument()
})
})
})
@ -1146,7 +1114,7 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
// Assert
await waitFor(() => {
@ -1185,7 +1153,7 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
// Assert
await waitFor(() => {
@ -1264,7 +1232,7 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
// Assert
await waitFor(() => {
@ -1280,7 +1248,7 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
// Assert - defaultModel is created with undefined provider
await waitFor(() => {
@ -1297,7 +1265,7 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
// Assert - defaultModel is created with undefined model
await waitFor(() => {
@ -1314,7 +1282,7 @@ describe('ModelParameterModal', () => {
// Act
render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
// Assert - when defaultModel is undefined, attribute is not set (returns null)
await waitFor(() => {
@ -1350,14 +1318,13 @@ describe('ModelParameterModal', () => {
// Act
const { rerender } = render(<ModelParameterModal {...props} />)
fireEvent.click(screen.getByTestId('portal-trigger'))
fireEvent.click(screen.getByTestId('trigger'))
await waitFor(() => {
expect(screen.getByTestId('model-selector')).toHaveAttribute('data-model-list-count', '1')
})
// Rerender with different scope
mockPortalOpenState = true
rerender(<ModelParameterModal {...props} scope={ModelTypeEnum.textEmbedding} />)
// Assert
@ -1398,7 +1365,7 @@ describe('ModelParameterModal', () => {
render(<ModelParameterModal {...props} />)
// Assert
const trigger = screen.getByTestId('portal-trigger')
const trigger = screen.getByTestId('trigger')
expect(trigger).toBeInTheDocument()
})
})

View File

@ -191,7 +191,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
<div className="relative">
<PopoverTrigger
render={(
<div className="block">
<button type="button" className="block w-full border-none bg-transparent p-0 text-left [color:inherit] [font:inherit]">
{
renderTrigger
? renderTrigger({
@ -224,7 +224,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
)
)
}
</div>
</button>
)}
/>
<PopoverContent

View File

@ -46,6 +46,7 @@ const PluginItem: FC<PluginItemProps> = ({
</div>
{onClear && (
<button
type="button"
className="hidden h-6 w-6 shrink-0 items-center justify-center rounded-md hover:bg-state-base-hover-alt group-hover/item:flex"
onClick={onClear}
>

View File

@ -99,6 +99,12 @@ const UpdatePluginModal: FC<Props> = ({
}) as Awaited<ReturnType<typeof updateFromMarketPlace>> & FailedUpgradeResponse
if (response.task?.status === TaskStatus.failed) {
const failedPlugin = response.task.plugins?.find(plugin => plugin.plugin_unique_identifier === targetPackageInfo.id)
?? response.task.plugins?.[0]
Toast.notify({
type: 'error',
message: failedPlugin?.message || t('error', { ns: 'common' }),
})
setUploadStep(UploadStep.notStarted)
return
}
@ -132,7 +138,7 @@ const UpdatePluginModal: FC<Props> = ({
}
if (uploadStep === UploadStep.installed)
onSave()
}, [onSave, uploadStep, check, originalPackageInfo.id, handleRefetch, targetPackageInfo.id])
}, [onSave, uploadStep, check, originalPackageInfo.id, handleRefetch, t, targetPackageInfo.id])
const { mutateAsync } = useRemoveAutoUpgrade()
const invalidateReferenceSettings = useInvalidateReferenceSettings()

View File

@ -74,6 +74,7 @@ const PluginVersionPicker: FC<Props> = ({
}}
>
<PopoverTrigger
disabled={disabled}
className={cn('inline-flex cursor-pointer items-center', disabled && 'cursor-default')}
>
{trigger}