+
{render ?? children}
),
@@ -127,7 +127,7 @@ vi.mock('@langgenius/dify-ui/popover', () => {
if (!isOpen)
return null
return (
-
{children}
+
{children}
)
},
}
@@ -562,7 +562,7 @@ describe('AppPicker', () => {
const onShowChange = vi.fn()
render(
)
- fireEvent.click(screen.getByTestId('portal-trigger'))
+ fireEvent.click(screen.getByTestId('popover-trigger'))
expect(onShowChange).not.toHaveBeenCalled()
})
@@ -570,7 +570,7 @@ describe('AppPicker', () => {
const onShowChange = vi.fn()
render(
)
- fireEvent.click(screen.getByTestId('portal-trigger'))
+ fireEvent.click(screen.getByTestId('popover-trigger'))
expect(onShowChange).toHaveBeenCalledWith(true)
})
})
@@ -683,7 +683,7 @@ describe('AppPicker', () => {
// The component should render without errors
// The component should render without errors
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should handle isShow toggle correctly', () => {
@@ -697,7 +697,7 @@ describe('AppPicker', () => {
// Should not crash
// Should not crash
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should setup intersection observer when isShow is true', () => {
@@ -718,7 +718,7 @@ describe('AppPicker', () => {
// Component should render without errors
// Component should render without errors
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should cleanup observer on component unmount', () => {
@@ -736,7 +736,7 @@ describe('AppPicker', () => {
// Component should still work correctly
// Component should still work correctly
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should not setup IntersectionObserver when observerTarget is null', () => {
@@ -745,7 +745,7 @@ describe('AppPicker', () => {
// The guard at line 84 should prevent setup
// The guard at line 84 should prevent setup
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should debounce onLoadMore calls using loadingRef', () => {
@@ -1555,7 +1555,7 @@ describe('AppSelector', () => {
describe('Rendering', () => {
it('should render without crashing', () => {
renderWithQueryClient(
)
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should render trigger component', () => {
@@ -1588,33 +1588,33 @@ describe('AppSelector', () => {
)
// Should show the app trigger with app info
// Should show the app trigger with app info
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
})
describe('Props', () => {
it('should handle different placement values', () => {
renderWithQueryClient(
)
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should handle different offset values', () => {
renderWithQueryClient(
)
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should handle disabled state', () => {
renderWithQueryClient(
)
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
// Portal should remain closed when disabled
// Portal should remain closed when disabled
- expect(screen.getByTestId('portal-to-follow-elem'))!.toHaveAttribute('data-open', 'false')
+ expect(screen.getByTestId('popover'))!.toHaveAttribute('data-open', 'false')
})
it('should handle scope prop', () => {
renderWithQueryClient(
)
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should handle value with inputs', () => {
@@ -1624,7 +1624,7 @@ describe('AppSelector', () => {
value={{ app_id: 'app-1', inputs: { name: 'test' }, files: [] }}
/>,
)
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should handle value with files', () => {
@@ -1634,7 +1634,7 @@ describe('AppSelector', () => {
value={{ app_id: 'app-1', inputs: {}, files: [{ id: 'file-1' }] }}
/>,
)
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
})
@@ -1642,32 +1642,32 @@ describe('AppSelector', () => {
it('should toggle isShow state when trigger is clicked', () => {
renderWithQueryClient(
)
- const trigger = screen.getAllByTestId('portal-trigger')[0]
+ const trigger = screen.getAllByTestId('popover-trigger')[0]
fireEvent.click(trigger!)
// The portal state should update synchronously - get the first one (outer portal)
// The portal state should update synchronously - get the first one (outer portal)
- expect(screen.getAllByTestId('portal-to-follow-elem')[0])!.toHaveAttribute('data-open', 'true')
+ expect(screen.getAllByTestId('popover')[0])!.toHaveAttribute('data-open', 'true')
})
it('should not toggle isShow when disabled', () => {
renderWithQueryClient(
)
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
- expect(screen.getByTestId('portal-to-follow-elem'))!.toHaveAttribute('data-open', 'false')
+ expect(screen.getByTestId('popover'))!.toHaveAttribute('data-open', 'false')
})
it('should manage search text state', () => {
renderWithQueryClient(
)
- const trigger = screen.getByTestId('portal-trigger')
+ const trigger = screen.getByTestId('popover-trigger')
fireEvent.click(trigger)
// Portal content should be visible after click
// Portal content should be visible after click
- expect(screen.getByTestId('portal-content'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover-content'))!.toBeInTheDocument()
})
it('should render correctly during load more setup', () => {
@@ -1678,7 +1678,7 @@ describe('AppSelector', () => {
// Trigger should be rendered
// Trigger should be rendered
- expect(screen.getByTestId('portal-trigger'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover-trigger'))!.toBeInTheDocument()
})
})
@@ -1689,9 +1689,9 @@ describe('AppSelector', () => {
renderWithQueryClient(
)
// Open the portal
- fireEvent.click(screen.getByTestId('portal-trigger'))
+ fireEvent.click(screen.getByTestId('popover-trigger'))
- expect(screen.getByTestId('portal-content'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover-content'))!.toBeInTheDocument()
})
it('should call onSelect with correct value structure', () => {
@@ -1706,7 +1706,7 @@ describe('AppSelector', () => {
// The component should maintain the correct value structure
// The component should maintain the correct value structure
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should clear inputs when selecting different app', () => {
@@ -1721,7 +1721,7 @@ describe('AppSelector', () => {
// Component renders with existing value
// Component renders with existing value
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should preserve inputs when selecting same app', () => {
@@ -1734,7 +1734,7 @@ describe('AppSelector', () => {
/>,
)
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
})
@@ -1748,7 +1748,7 @@ describe('AppSelector', () => {
}
renderWithQueryClient(
)
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should memoize currentAppInfo correctly', () => {
@@ -1763,7 +1763,7 @@ describe('AppSelector', () => {
/>,
)
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should memoize formattedValue correctly', () => {
@@ -1774,7 +1774,7 @@ describe('AppSelector', () => {
/>,
)
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should be wrapped with React.memo', () => {
@@ -1791,7 +1791,7 @@ describe('AppSelector', () => {
,
)
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
})
@@ -1799,7 +1799,7 @@ describe('AppSelector', () => {
it('should handle load more when hasMore is true', async () => {
mockHasNextPage = true
renderWithQueryClient(
)
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should not trigger load more when already loading', async () => {
@@ -1825,7 +1825,7 @@ describe('AppSelector', () => {
vi.advanceTimersByTime(500)
})
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should render load more area when hasMore is true', () => {
@@ -1836,11 +1836,11 @@ describe('AppSelector', () => {
renderWithQueryClient(
)
// Open the portal
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
+ fireEvent.click(screen.getAllByTestId('popover-trigger')[0]!)
// Should render without errors
// Should render without errors
- expect(screen.getByTestId('portal-content'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover-content'))!.toBeInTheDocument()
})
it('should handle fetchNextPage rejection gracefully in handleLoadMore', async () => {
@@ -1851,7 +1851,7 @@ describe('AppSelector', () => {
// Should not crash even if fetchNextPage rejects
// Should not crash even if fetchNextPage rejects
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should call fetchNextPage when intersection observer triggers handleLoadMore', async () => {
@@ -1862,10 +1862,10 @@ describe('AppSelector', () => {
renderWithQueryClient(
)
// Open the main portal
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
+ fireEvent.click(screen.getAllByTestId('popover-trigger')[0]!)
// Open the inner app picker portal
- const triggers = screen.getAllByTestId('portal-trigger')
+ const triggers = screen.getAllByTestId('popover-trigger')
fireEvent.click(triggers[1]!)
// Simulate intersection to trigger handleLoadMore
@@ -1883,8 +1883,8 @@ describe('AppSelector', () => {
renderWithQueryClient(
)
// Open portals
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
- const triggers = screen.getAllByTestId('portal-trigger')
+ fireEvent.click(screen.getAllByTestId('popover-trigger')[0]!)
+ const triggers = screen.getAllByTestId('popover-trigger')
fireEvent.click(triggers[1]!)
// Trigger first intersection
@@ -1898,7 +1898,7 @@ describe('AppSelector', () => {
// Still only one call due to the picker-level debounce
expect(mockFetchNextPage).toHaveBeenCalledTimes(1)
- expect(screen.getAllByTestId('portal-content').length).toBeGreaterThan(0)
+ expect(screen.getAllByTestId('popover-content').length).toBeGreaterThan(0)
})
it('should skip handleLoadMore when isFetchingNextPage is true', async () => {
@@ -1909,8 +1909,8 @@ describe('AppSelector', () => {
renderWithQueryClient(
)
// Open portals
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
- const triggers = screen.getAllByTestId('portal-trigger')
+ fireEvent.click(screen.getAllByTestId('popover-trigger')[0]!)
+ const triggers = screen.getAllByTestId('popover-trigger')
fireEvent.click(triggers[1]!)
// Trigger intersection
@@ -1928,8 +1928,8 @@ describe('AppSelector', () => {
renderWithQueryClient(
)
// Open portals
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
- const triggers = screen.getAllByTestId('portal-trigger')
+ fireEvent.click(screen.getAllByTestId('popover-trigger')[0]!)
+ const triggers = screen.getAllByTestId('popover-trigger')
fireEvent.click(triggers[1]!)
// Trigger intersection
@@ -1950,7 +1950,7 @@ describe('AppSelector', () => {
/>,
)
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should handle form change without image file', () => {
@@ -1963,7 +1963,7 @@ describe('AppSelector', () => {
/>,
)
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should extract #image# from inputs and add to files array', () => {
@@ -1977,7 +1977,7 @@ describe('AppSelector', () => {
/>,
)
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should preserve existing files when no #image# in inputs', () => {
@@ -1990,7 +1990,7 @@ describe('AppSelector', () => {
/>,
)
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
})
@@ -2010,9 +2010,9 @@ describe('AppSelector', () => {
)
// Open the main portal
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
+ fireEvent.click(screen.getAllByTestId('popover-trigger')[0]!)
- expect(screen.getByTestId('portal-content'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover-content'))!.toBeInTheDocument()
})
it('should preserve inputs when selecting the same app', () => {
@@ -2029,7 +2029,7 @@ describe('AppSelector', () => {
/>,
)
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should handle app selection with empty value', () => {
@@ -2047,9 +2047,9 @@ describe('AppSelector', () => {
)
// Open the main portal
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
+ fireEvent.click(screen.getAllByTestId('popover-trigger')[0]!)
- expect(screen.getByTestId('portal-content'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover-content'))!.toBeInTheDocument()
})
})
@@ -2062,19 +2062,19 @@ describe('AppSelector', () => {
it('should handle empty pages array', () => {
mockAppListData = { pages: [] }
renderWithQueryClient(
)
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should handle undefined data', () => {
mockAppListData = undefined
renderWithQueryClient(
)
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should handle loading state', () => {
mockIsLoading = true
renderWithQueryClient(
)
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should handle app not found in displayedApps', () => {
@@ -2089,7 +2089,7 @@ describe('AppSelector', () => {
/>,
)
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should handle value with empty inputs and files', () => {
@@ -2100,7 +2100,7 @@ describe('AppSelector', () => {
/>,
)
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
})
@@ -2113,7 +2113,7 @@ describe('AppSelector', () => {
// Should not crash
// Should not crash
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
})
})
@@ -2147,11 +2147,11 @@ describe('AppSelector Integration', () => {
renderWithQueryClient(
)
// 1. Click trigger to open picker - get first trigger (outer portal)
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
+ fireEvent.click(screen.getAllByTestId('popover-trigger')[0]!)
// Get the first portal element (outer portal)
// Get the first portal element (outer portal)
- expect(screen.getAllByTestId('portal-to-follow-elem')[0])!.toHaveAttribute('data-open', 'true')
+ expect(screen.getAllByTestId('popover')[0])!.toHaveAttribute('data-open', 'true')
})
it('should handle app change with input preservation logic', () => {
@@ -2163,7 +2163,7 @@ describe('AppSelector Integration', () => {
/>,
)
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
})
@@ -2179,9 +2179,9 @@ describe('AppSelector Integration', () => {
it('should pass correct props to AppPicker', () => {
renderWithQueryClient(
)
- fireEvent.click(screen.getByTestId('portal-trigger'))
+ fireEvent.click(screen.getByTestId('popover-trigger'))
- expect(screen.getByTestId('portal-content'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover-content'))!.toBeInTheDocument()
})
})
@@ -2194,15 +2194,15 @@ describe('AppSelector Integration', () => {
/>,
)
- expect(screen.getByTestId('portal-to-follow-elem'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover'))!.toBeInTheDocument()
})
it('should handle search filtering through app list', () => {
renderWithQueryClient(
)
- fireEvent.click(screen.getByTestId('portal-trigger'))
+ fireEvent.click(screen.getByTestId('popover-trigger'))
- expect(screen.getByTestId('portal-content'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover-content'))!.toBeInTheDocument()
})
})
@@ -2221,11 +2221,11 @@ describe('AppSelector Integration', () => {
)
// Open the main portal
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
+ fireEvent.click(screen.getAllByTestId('popover-trigger')[0]!)
// The inner AppPicker portal is closed by default (isShowChooseApp = false)
// We need to click on the inner trigger to open it
- const innerTriggers = screen.getAllByTestId('portal-trigger')
+ const innerTriggers = screen.getAllByTestId('popover-trigger')
// The second trigger is the inner AppPicker trigger
fireEvent.click(innerTriggers[1]!)
@@ -2256,10 +2256,10 @@ describe('AppSelector Integration', () => {
)
// Open the main portal
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
+ fireEvent.click(screen.getAllByTestId('popover-trigger')[0]!)
// Click on the inner trigger to open app picker
- const innerTriggers = screen.getAllByTestId('portal-trigger')
+ const innerTriggers = screen.getAllByTestId('popover-trigger')
fireEvent.click(innerTriggers[1]!)
// Click on the same app - need to get the one in the app list, not the trigger
@@ -2289,10 +2289,10 @@ describe('AppSelector Integration', () => {
)
// Open the main portal
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
+ fireEvent.click(screen.getAllByTestId('popover-trigger')[0]!)
// Click on inner trigger to open app picker
- const innerTriggers = screen.getAllByTestId('portal-trigger')
+ const innerTriggers = screen.getAllByTestId('popover-trigger')
fireEvent.click(innerTriggers[1]!)
// Click on an app from the dropdown
@@ -2317,9 +2317,9 @@ describe('AppSelector Integration', () => {
renderWithQueryClient(
)
// Open the portal to render the app picker
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
+ fireEvent.click(screen.getAllByTestId('popover-trigger')[0]!)
- expect(screen.getByTestId('portal-content'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover-content'))!.toBeInTheDocument()
})
it('should stay stable after fetchNextPage completes', async () => {
@@ -2329,9 +2329,9 @@ describe('AppSelector Integration', () => {
renderWithQueryClient(
)
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
+ fireEvent.click(screen.getAllByTestId('popover-trigger')[0]!)
- expect(screen.getByTestId('portal-content'))!.toBeInTheDocument()
+ expect(screen.getByTestId('popover-content'))!.toBeInTheDocument()
})
it('should not call fetchNextPage when conditions prevent it', () => {
@@ -2340,7 +2340,7 @@ describe('AppSelector Integration', () => {
renderWithQueryClient(
)
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
+ fireEvent.click(screen.getAllByTestId('popover-trigger')[0]!)
// fetchNextPage should not be called
expect(mockFetchNextPage).not.toHaveBeenCalled()
@@ -2362,10 +2362,10 @@ describe('AppSelector Integration', () => {
)
// Open portal
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
+ fireEvent.click(screen.getAllByTestId('popover-trigger')[0]!)
// formattedValue should include #image# from files
- expect(screen.getAllByTestId('portal-content').length).toBeGreaterThan(0)
+ expect(screen.getAllByTestId('popover-content').length).toBeGreaterThan(0)
})
it('should handle value with no files', () => {
@@ -2381,9 +2381,9 @@ describe('AppSelector Integration', () => {
/>,
)
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
+ fireEvent.click(screen.getAllByTestId('popover-trigger')[0]!)
- expect(screen.getAllByTestId('portal-content').length).toBeGreaterThan(0)
+ expect(screen.getAllByTestId('popover-content').length).toBeGreaterThan(0)
})
it('should handle undefined value.files', () => {
@@ -2399,9 +2399,9 @@ describe('AppSelector Integration', () => {
/>,
)
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
+ fireEvent.click(screen.getAllByTestId('popover-trigger')[0]!)
- expect(screen.getAllByTestId('portal-content').length).toBeGreaterThan(0)
+ expect(screen.getAllByTestId('popover-content').length).toBeGreaterThan(0)
})
it('should call onSelect with transformed inputs when form input changes', () => {
@@ -2430,7 +2430,7 @@ describe('AppSelector Integration', () => {
)
// Open portal to render AppInputsPanel
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
+ fireEvent.click(screen.getAllByTestId('popover-trigger')[0]!)
// Find and interact with the form input (may not exist if schema is empty)
const formInputs = screen.queryAllByPlaceholderText('FormInputField')
@@ -2446,7 +2446,7 @@ describe('AppSelector Integration', () => {
}
else {
// If form inputs aren't rendered, at least verify component rendered
- expect(screen.getAllByTestId('portal-content').length).toBeGreaterThan(0)
+ expect(screen.getAllByTestId('popover-content').length).toBeGreaterThan(0)
}
})
@@ -2482,7 +2482,7 @@ describe('AppSelector Integration', () => {
/>,
)
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
+ fireEvent.click(screen.getAllByTestId('popover-trigger')[0]!)
// Find file uploader and trigger upload - the #image# field will be extracted
const uploadBtns = screen.queryAllByTestId('upload-file-btn')
@@ -2493,7 +2493,7 @@ describe('AppSelector Integration', () => {
}
else {
// Verify component rendered
- expect(screen.getAllByTestId('portal-content').length).toBeGreaterThan(0)
+ expect(screen.getAllByTestId('popover-content').length).toBeGreaterThan(0)
}
})
@@ -2520,7 +2520,7 @@ describe('AppSelector Integration', () => {
/>,
)
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
+ fireEvent.click(screen.getAllByTestId('popover-trigger')[0]!)
// Find form input (may not exist if schema is empty)
const inputs = screen.queryAllByPlaceholderText('PreserveField')
@@ -2536,7 +2536,7 @@ describe('AppSelector Integration', () => {
}
else {
// If form inputs aren't rendered, at least verify component rendered
- expect(screen.getAllByTestId('portal-content').length).toBeGreaterThan(0)
+ expect(screen.getAllByTestId('popover-content').length).toBeGreaterThan(0)
}
})
@@ -2571,7 +2571,7 @@ describe('AppSelector Integration', () => {
/>,
)
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
+ fireEvent.click(screen.getAllByTestId('popover-trigger')[0]!)
// Try to find and click the upload button which triggers #image# form change
const uploadBtn = screen.queryByTestId('upload-file-btn')
@@ -2605,7 +2605,7 @@ describe('AppSelector Integration', () => {
/>,
)
- fireEvent.click(screen.getAllByTestId('portal-trigger')[0]!)
+ fireEvent.click(screen.getAllByTestId('popover-trigger')[0]!)
const inputs = screen.queryAllByPlaceholderText('SimpleInput')
if (inputs.length > 0) {
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/index.spec.tsx
index 34325109eb..7ceb8607c5 100644
--- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/index.spec.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/index.spec.tsx
@@ -5,34 +5,8 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { SupportedCreationMethods } from '@/app/components/plugins/types'
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
-import { CreateButtonType, CreateSubscriptionButton, DEFAULT_METHOD } from '../index'
-
-let mockPortalOpenState = false
-
-vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
- PortalToFollowElem: ({ children, open }: { children: React.ReactNode, open: boolean }) => {
- mockPortalOpenState = open || false
- return (
-
- {children}
-
- )
- },
- PortalToFollowElemTrigger: ({ children, onClick, className }: { children: React.ReactNode, onClick?: () => void, className?: string }) => (
-
- {children}
-
- ),
- PortalToFollowElemContent: ({ children, className }: { children: React.ReactNode, className?: string }) => {
- if (!mockPortalOpenState)
- return null
- return (
-
- {children}
-
- )
- },
-}))
+import { CreateSubscriptionButton } from '../index'
+import { CreateButtonType, DEFAULT_METHOD } from '../types'
vi.mock('@langgenius/dify-ui/toast', () => ({
toast: Object.assign(vi.fn(), {
@@ -107,40 +81,47 @@ vi.mock('../common-modal', () => ({
}))
vi.mock('../oauth-client', () => ({
- OAuthClientSettingsModal: ({ oauthConfig, onClose, showOAuthCreateModal }: {
+ OAuthClientSettingsModal: ({ open, oauthConfig, onOpenChange, showOAuthCreateModal }: {
+ open: boolean
oauthConfig?: TriggerOAuthConfig
- onClose: () => void
+ onOpenChange: (open: boolean) => void
showOAuthCreateModal: (builder: TriggerSubscriptionBuilder) => void
- }) => (
-
-
-
+ )
+ },
}))
vi.mock('@langgenius/dify-ui/select', async () => {
const React = await import('react')
const SelectContext = React.createContext<{
+ onOpenChange?: (open: boolean) => void
onValueChange?: (value: string) => void
}>({})
@@ -160,11 +141,13 @@ vi.mock('@langgenius/dify-ui/select', async () => {
children,
value,
open,
+ onOpenChange,
onValueChange,
}: {
children: React.ReactNode
value: string | null
open?: boolean
+ onOpenChange?: (open: boolean) => void
onValueChange?: (value: string) => void
}) => {
const currentValue = value ?? DEFAULT_METHOD
@@ -175,10 +158,11 @@ vi.mock('@langgenius/dify-ui/select', async () => {
: String(open ?? false)
return (
-
+
@@ -188,7 +172,16 @@ vi.mock('@langgenius/dify-ui/select', async () => {
)
},
SelectTrigger: ({ children, className }: { children: React.ReactNode, render?: React.ReactNode, className?: string }) => {
- return
{children}
+ const context = React.useContext(SelectContext)
+ return (
+
context.onOpenChange?.(true)}
+ >
+ {children}
+
+ )
},
SelectContent: ({ children }: { children: React.ReactNode }) => (
{children}
@@ -281,7 +274,6 @@ const setupMocks = (config: {
describe('CreateSubscriptionButton', () => {
beforeEach(() => {
vi.clearAllMocks()
- mockPortalOpenState = false
setupMocks()
})
@@ -494,6 +486,38 @@ describe('CreateSubscriptionButton', () => {
})
})
+ it('should close dropdown when oauth settings is clicked from option extra action', async () => {
+ // Arrange
+ setupMocks({
+ storeDetail: createStoreDetail(),
+ providerInfo: createProviderInfo({
+ supported_creation_methods: [
+ SupportedCreationMethods.OAUTH,
+ SupportedCreationMethods.APIKEY,
+ SupportedCreationMethods.MANUAL,
+ ],
+ }),
+ oauthConfig: createOAuthConfig({ configured: false }),
+ })
+ const props = createDefaultProps()
+
+ // Act
+ render(
)
+
+ fireEvent.click(screen.getByTestId('custom-trigger'))
+ expect(screen.getByTestId('custom-select'))!.toHaveAttribute('data-open', 'true')
+
+ fireEvent.click(screen.getByRole('button', {
+ name: 'pluginTrigger.subscription.addType.options.oauth.clientSettings',
+ }))
+
+ // Assert
+ await waitFor(() => {
+ expect(screen.getByTestId('oauth-client-modal'))!.toBeInTheDocument()
+ expect(screen.getByTestId('custom-select'))!.toHaveAttribute('data-open', 'false')
+ })
+ })
+
it('should close OAuthClientSettingsModal and refetch config when closed', async () => {
// Arrange
const mockRefetchOAuth = vi.fn()
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/oauth-client.spec.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/oauth-client.spec.tsx
index 46b9499027..21c76ecdca 100644
--- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/oauth-client.spec.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/__tests__/oauth-client.spec.tsx
@@ -110,48 +110,6 @@ Object.defineProperty(navigator, 'clipboard', {
writable: true,
})
-vi.mock('@/app/components/base/modal/modal', () => ({
- default: ({
- children,
- onClose,
- onConfirm,
- onCancel,
- title,
- confirmButtonText,
- cancelButtonText,
- footerSlot,
- onExtraButtonClick,
- extraButtonText,
- }: {
- children: React.ReactNode
- onClose: () => void
- onConfirm: () => void
- onCancel: () => void
- title: string
- confirmButtonText: string
- cancelButtonText?: string
- footerSlot?: React.ReactNode
- onExtraButtonClick?: () => void
- extraButtonText?: string
- }) => (
-
-
{title}
-
{children}
-
- {footerSlot}
- {extraButtonText && (
- {extraButtonText}
- )}
- {cancelButtonText && (
- {cancelButtonText}
- )}
- {confirmButtonText}
- Close
-
-
- ),
-}))
-
let mockFormValues: { values: Record
, isCheckValidated: boolean } = {
values: { client_id: 'test-client-id', client_secret: 'test-client-secret' },
isCheckValidated: true,
@@ -161,10 +119,13 @@ const setMockFormValues = (values: typeof mockFormValues) => {
}
vi.mock('@/app/components/base/form/components/base', () => ({
- BaseForm: React.forwardRef((
- { formSchemas }: { formSchemas: Array<{ name: string, default?: string }> },
- ref: React.ForwardedRef<{ getFormValues: () => { values: Record, isCheckValidated: boolean } }>,
- ) => {
+ BaseForm: ({
+ formSchemas,
+ ref,
+ }: {
+ formSchemas: Array<{ name: string, default?: string }>
+ ref?: React.Ref<{ getFormValues: () => { values: Record, isCheckValidated: boolean } }>
+ }) => {
React.useImperativeHandle(ref, () => ({
getFormValues: () => mockFormValues,
}))
@@ -180,15 +141,24 @@ vi.mock('@/app/components/base/form/components/base', () => ({
))}
)
- }),
+ },
}))
describe('OAuthClientSettingsModal', () => {
const defaultProps = {
+ open: true,
oauthConfig: createMockOAuthConfig(),
- onClose: vi.fn(),
+ onOpenChange: vi.fn(),
showOAuthCreateModal: vi.fn(),
}
+ const title = 'pluginTrigger.modal.oauth.title'
+ const getDialog = () => screen.getByRole('dialog', { name: title })
+ const getCloseButton = () => screen.getByRole('button', { name: 'Close' })
+ const getCancelButton = () => screen.getByRole('button', { name: 'common.operation.cancel' })
+ const getSaveOnlyButton = () => screen.getByRole('button', { name: 'plugin.auth.saveOnly' })
+ const getConfirmButton = () => screen.getByRole('button', {
+ name: /plugin\.auth\.saveAndAuth|pluginTrigger\.modal\.common\.authorizing|pluginTrigger\.modal\.oauth\.authorization\.waitingJump/,
+ })
beforeEach(() => {
vi.clearAllMocks()
@@ -215,7 +185,7 @@ describe('OAuthClientSettingsModal', () => {
it('should render modal with correct title', () => {
render()
- expect(screen.getByTestId('modal-title')).toHaveTextContent('pluginTrigger.modal.oauth.title')
+ expect(screen.getByRole('heading', { name: title })).toBeInTheDocument()
})
it('should render client type selector when system_configured is true', () => {
@@ -332,7 +302,7 @@ describe('OAuthClientSettingsModal', () => {
render()
- fireEvent.click(screen.getByTestId('modal-confirm'))
+ fireEvent.click(getConfirmButton())
expect(mockConfigureOAuth).toHaveBeenCalled()
})
@@ -350,7 +320,7 @@ describe('OAuthClientSettingsModal', () => {
render()
- fireEvent.click(screen.getByTestId('modal-confirm'))
+ fireEvent.click(getConfirmButton())
expect(mockOpenOAuthPopup).toHaveBeenCalledWith(
'https://oauth.example.com/authorize',
@@ -359,7 +329,7 @@ describe('OAuthClientSettingsModal', () => {
})
it('should show success toast and close modal when OAuth callback succeeds', () => {
- const mockOnClose = vi.fn()
+ const mockOnOpenChange = vi.fn()
const mockShowOAuthCreateModal = vi.fn()
mockConfigureOAuth.mockImplementation((params, { onSuccess }) => {
@@ -379,18 +349,18 @@ describe('OAuthClientSettingsModal', () => {
render(
,
)
- fireEvent.click(screen.getByTestId('modal-confirm'))
+ fireEvent.click(getConfirmButton())
expect(mockToastNotify).toHaveBeenCalledWith({
type: 'success',
message: 'pluginTrigger.modal.oauth.authorization.authSuccess',
})
- expect(mockOnClose).toHaveBeenCalled()
+ expect(mockOnOpenChange).toHaveBeenCalledWith(false)
})
it('should show error toast when OAuth initiation fails', () => {
@@ -403,7 +373,7 @@ describe('OAuthClientSettingsModal', () => {
render()
- fireEvent.click(screen.getByTestId('modal-confirm'))
+ fireEvent.click(getConfirmButton())
expect(mockToastNotify).toHaveBeenCalledWith({
type: 'error',
@@ -420,7 +390,7 @@ describe('OAuthClientSettingsModal', () => {
render()
- fireEvent.click(screen.getByTestId('modal-cancel'))
+ fireEvent.click(getSaveOnlyButton())
expect(mockConfigureOAuth).toHaveBeenCalledWith(
expect.objectContaining({
@@ -432,20 +402,20 @@ describe('OAuthClientSettingsModal', () => {
})
it('should show success toast when save only succeeds', () => {
- const mockOnClose = vi.fn()
+ const mockOnOpenChange = vi.fn()
mockConfigureOAuth.mockImplementation((params, { onSuccess }) => {
onSuccess()
})
- render()
+ render()
- fireEvent.click(screen.getByTestId('modal-cancel'))
+ fireEvent.click(getSaveOnlyButton())
expect(mockToastNotify).toHaveBeenCalledWith({
type: 'success',
message: 'pluginTrigger.modal.oauth.save.success',
})
- expect(mockOnClose).toHaveBeenCalled()
+ expect(mockOnOpenChange).toHaveBeenCalledWith(false)
})
})
@@ -469,7 +439,7 @@ describe('OAuthClientSettingsModal', () => {
})
it('should show success toast when remove succeeds', () => {
- const mockOnClose = vi.fn()
+ const mockOnOpenChange = vi.fn()
const configWithCustomEnabled = createMockOAuthConfig({
system_configured: false,
custom_enabled: true,
@@ -484,7 +454,7 @@ describe('OAuthClientSettingsModal', () => {
,
)
@@ -495,7 +465,7 @@ describe('OAuthClientSettingsModal', () => {
type: 'success',
message: 'pluginTrigger.modal.oauth.remove.success',
})
- expect(mockOnClose).toHaveBeenCalled()
+ expect(mockOnOpenChange).toHaveBeenCalledWith(false)
})
it('should show error toast when remove fails', () => {
@@ -522,22 +492,22 @@ describe('OAuthClientSettingsModal', () => {
})
describe('Modal Actions', () => {
- it('should call onClose when close button is clicked', () => {
- const mockOnClose = vi.fn()
- render()
+ it('should call onOpenChange when close button is clicked', () => {
+ const mockOnOpenChange = vi.fn()
+ render()
- fireEvent.click(screen.getByTestId('modal-close'))
+ fireEvent.click(getCloseButton())
- expect(mockOnClose).toHaveBeenCalled()
+ expect(mockOnOpenChange.mock.calls[0]?.[0]).toBe(false)
})
- it('should call onClose when extra button (cancel) is clicked', () => {
- const mockOnClose = vi.fn()
- render()
+ it('should call onOpenChange when cancel button is clicked', () => {
+ const mockOnOpenChange = vi.fn()
+ render()
- fireEvent.click(screen.getByTestId('modal-extra'))
+ fireEvent.click(getCancelButton())
- expect(mockOnClose).toHaveBeenCalled()
+ expect(mockOnOpenChange).toHaveBeenCalledWith(false)
})
})
@@ -545,13 +515,13 @@ describe('OAuthClientSettingsModal', () => {
it('should show default button text initially', () => {
render()
- expect(screen.getByTestId('modal-confirm')).toHaveTextContent('plugin.auth.saveAndAuth')
+ expect(getConfirmButton()).toHaveTextContent('plugin.auth.saveAndAuth')
})
it('should show save only button text', () => {
render()
- expect(screen.getByTestId('modal-cancel')).toHaveTextContent('plugin.auth.saveOnly')
+ expect(getSaveOnlyButton()).toHaveTextContent('plugin.auth.saveOnly')
})
})
@@ -591,7 +561,7 @@ describe('OAuthClientSettingsModal', () => {
it('should handle undefined oauthConfig', () => {
render()
- expect(screen.getByTestId('modal')).toBeInTheDocument()
+ expect(getDialog()).toBeInTheDocument()
})
it('should handle missing provider', () => {
@@ -600,7 +570,7 @@ describe('OAuthClientSettingsModal', () => {
render()
- expect(screen.getByTestId('modal')).toBeInTheDocument()
+ expect(getDialog()).toBeInTheDocument()
})
})
@@ -618,7 +588,7 @@ describe('OAuthClientSettingsModal', () => {
render()
- fireEvent.click(screen.getByTestId('modal-confirm'))
+ fireEvent.click(getConfirmButton())
// Verify OAuth flow was initiated
expect(mockInitiateOAuth).toHaveBeenCalledWith(
@@ -644,13 +614,13 @@ describe('OAuthClientSettingsModal', () => {
render()
- fireEvent.click(screen.getByTestId('modal-confirm'))
+ fireEvent.click(getConfirmButton())
vi.advanceTimersByTime(3000)
expect(mockVerifyBuilder).toHaveBeenCalled()
// Should still be in pending state (polling continues)
- expect(screen.getByTestId('modal-confirm')).toHaveTextContent('pluginTrigger.modal.common.authorizing')
+ expect(getConfirmButton()).toHaveTextContent('pluginTrigger.modal.common.authorizing')
vi.useRealTimers()
})
@@ -765,7 +735,7 @@ describe('OAuthClientSettingsModal', () => {
describe('OAuth callback edge cases', () => {
it('should not show success toast when OAuth callback returns falsy data', () => {
- const mockOnClose = vi.fn()
+ const mockOnOpenChange = vi.fn()
const mockShowOAuthCreateModal = vi.fn()
mockConfigureOAuth.mockImplementation((params, { onSuccess }) => {
@@ -784,12 +754,12 @@ describe('OAuthClientSettingsModal', () => {
render(
,
)
- fireEvent.click(screen.getByTestId('modal-confirm'))
+ fireEvent.click(getConfirmButton())
// Should not show success toast or call callbacks
expect(mockToastNotify).not.toHaveBeenCalledWith(
@@ -811,7 +781,7 @@ describe('OAuthClientSettingsModal', () => {
const customCard = screen.getByText('pluginTrigger.subscription.addType.options.oauth.custom').closest('div')
fireEvent.click(customCard!)
- fireEvent.click(screen.getByTestId('modal-cancel'))
+ fireEvent.click(getSaveOnlyButton())
expect(mockConfigureOAuth).toHaveBeenCalledWith(
expect.objectContaining({
@@ -829,7 +799,7 @@ describe('OAuthClientSettingsModal', () => {
render()
// Default is already selected
- fireEvent.click(screen.getByTestId('modal-cancel'))
+ fireEvent.click(getSaveOnlyButton())
expect(mockConfigureOAuth).toHaveBeenCalledWith(
expect.objectContaining({
@@ -901,7 +871,7 @@ describe('OAuthClientSettingsModal', () => {
it('should show saveAndAuth text by default', () => {
render()
- expect(screen.getByTestId('modal-confirm')).toHaveTextContent('plugin.auth.saveAndAuth')
+ expect(getConfirmButton()).toHaveTextContent('plugin.auth.saveAndAuth')
})
it('should show authorizing text when authorization is pending', () => {
@@ -914,9 +884,9 @@ describe('OAuthClientSettingsModal', () => {
render()
- fireEvent.click(screen.getByTestId('modal-confirm'))
+ fireEvent.click(getConfirmButton())
- expect(screen.getByTestId('modal-confirm')).toHaveTextContent('pluginTrigger.modal.common.authorizing')
+ expect(getConfirmButton()).toHaveTextContent('pluginTrigger.modal.common.authorizing')
})
})
@@ -931,10 +901,10 @@ describe('OAuthClientSettingsModal', () => {
render()
- fireEvent.click(screen.getByTestId('modal-confirm'))
+ fireEvent.click(getConfirmButton())
// After failure, button text should return to default
- expect(screen.getByTestId('modal-confirm')).toHaveTextContent('plugin.auth.saveAndAuth')
+ expect(getConfirmButton()).toHaveTextContent('plugin.auth.saveAndAuth')
})
})
@@ -1013,7 +983,7 @@ describe('OAuthClientSettingsModal', () => {
const customCard = screen.getByText('pluginTrigger.subscription.addType.options.oauth.custom').closest('div')!
fireEvent.click(customCard)
- fireEvent.click(screen.getByTestId('modal-cancel'))
+ fireEvent.click(getSaveOnlyButton())
// Should not call configureOAuth because form validation failed
expect(mockConfigureOAuth).not.toHaveBeenCalled()
@@ -1035,7 +1005,7 @@ describe('OAuthClientSettingsModal', () => {
// Switch to custom type
fireEvent.click(screen.getByText('pluginTrigger.subscription.addType.options.oauth.custom').closest('div')!)
- fireEvent.click(screen.getByTestId('modal-cancel'))
+ fireEvent.click(getSaveOnlyButton())
expect(mockConfigureOAuth).toHaveBeenCalledWith(
expect.objectContaining({
@@ -1062,7 +1032,7 @@ describe('OAuthClientSettingsModal', () => {
// Switch to custom type
fireEvent.click(screen.getByText('pluginTrigger.subscription.addType.options.oauth.custom').closest('div')!)
- fireEvent.click(screen.getByTestId('modal-cancel'))
+ fireEvent.click(getSaveOnlyButton())
expect(mockConfigureOAuth).toHaveBeenCalledWith(
expect.objectContaining({
@@ -1089,7 +1059,7 @@ describe('OAuthClientSettingsModal', () => {
// Switch to custom type
fireEvent.click(screen.getByText('pluginTrigger.subscription.addType.options.oauth.custom').closest('div')!)
- fireEvent.click(screen.getByTestId('modal-cancel'))
+ fireEvent.click(getSaveOnlyButton())
expect(mockConfigureOAuth).toHaveBeenCalledWith(
expect.objectContaining({
@@ -1116,7 +1086,7 @@ describe('OAuthClientSettingsModal', () => {
// Switch to custom type
fireEvent.click(screen.getByText('pluginTrigger.subscription.addType.options.oauth.custom').closest('div')!)
- fireEvent.click(screen.getByTestId('modal-cancel'))
+ fireEvent.click(getSaveOnlyButton())
expect(mockConfigureOAuth).toHaveBeenCalledWith(
expect.objectContaining({
@@ -1148,7 +1118,7 @@ describe('OAuthClientSettingsModal', () => {
render()
- fireEvent.click(screen.getByTestId('modal-confirm'))
+ fireEvent.click(getConfirmButton())
// Advance timer to trigger polling
await vi.advanceTimersByTimeAsync(3000)
@@ -1157,7 +1127,7 @@ describe('OAuthClientSettingsModal', () => {
// Button text should show waitingJump after verified
await waitFor(() => {
- expect(screen.getByTestId('modal-confirm')).toHaveTextContent('pluginTrigger.modal.oauth.authorization.waitingJump')
+ expect(getConfirmButton()).toHaveTextContent('pluginTrigger.modal.oauth.authorization.waitingJump')
})
vi.useRealTimers()
@@ -1180,7 +1150,7 @@ describe('OAuthClientSettingsModal', () => {
render()
- fireEvent.click(screen.getByTestId('modal-confirm'))
+ fireEvent.click(getConfirmButton())
// First poll
await vi.advanceTimersByTimeAsync(3000)
@@ -1191,7 +1161,7 @@ describe('OAuthClientSettingsModal', () => {
expect(mockVerifyBuilder).toHaveBeenCalledTimes(2)
// Should still be in authorizing state
- expect(screen.getByTestId('modal-confirm')).toHaveTextContent('pluginTrigger.modal.common.authorizing')
+ expect(getConfirmButton()).toHaveTextContent('pluginTrigger.modal.common.authorizing')
vi.useRealTimers()
})
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/__tests__/use-oauth-client-state.spec.ts b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/__tests__/use-oauth-client-state.spec.ts
index 82eddf501d..4c88b1207c 100644
--- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/__tests__/use-oauth-client-state.spec.ts
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/__tests__/use-oauth-client-state.spec.ts
@@ -137,7 +137,7 @@ describe('useOAuthClientState', () => {
const defaultParams = {
oauthConfig: createMockOAuthConfig(),
providerName: 'test-provider',
- onClose: vi.fn(),
+ onOpenChange: vi.fn(),
showOAuthCreateModal: vi.fn(),
}
@@ -310,20 +310,20 @@ describe('useOAuthClientState', () => {
)
})
- it('should call onClose and show success toast on success', () => {
+ it('should call onOpenChange and show success toast on success', () => {
mockDeleteOAuth.mockImplementation((provider, { onSuccess }) => onSuccess())
- const onClose = vi.fn()
+ const onOpenChange = vi.fn()
const { result } = renderHook(() => useOAuthClientState({
...defaultParams,
- onClose,
+ onOpenChange,
}))
act(() => {
result.current.handleRemove()
})
- expect(onClose).toHaveBeenCalled()
+ expect(onOpenChange).toHaveBeenCalledWith(false)
expect(mockToastNotify).toHaveBeenCalledWith({
type: 'success',
message: 'pluginTrigger.modal.oauth.remove.success',
@@ -398,20 +398,20 @@ describe('useOAuthClientState', () => {
)
})
- it('should show success toast and call onClose when needAuth is false', () => {
+ it('should show success toast and call onOpenChange when needAuth is false', () => {
mockConfigureOAuth.mockImplementation((params, { onSuccess }) => onSuccess())
- const onClose = vi.fn()
+ const onOpenChange = vi.fn()
const { result } = renderHook(() => useOAuthClientState({
...defaultParams,
- onClose,
+ onOpenChange,
}))
act(() => {
result.current.handleSave(false)
})
- expect(onClose).toHaveBeenCalled()
+ expect(onOpenChange).toHaveBeenCalledWith(false)
expect(mockToastNotify).toHaveBeenCalledWith({
type: 'success',
message: 'pluginTrigger.modal.oauth.save.success',
@@ -495,8 +495,8 @@ describe('useOAuthClientState', () => {
})
})
- it('should call onClose and showOAuthCreateModal on callback success', () => {
- const onClose = vi.fn()
+ it('should call onOpenChange and showOAuthCreateModal on callback success', () => {
+ const onOpenChange = vi.fn()
const showOAuthCreateModal = vi.fn()
const builder = createMockSubscriptionBuilder()
@@ -513,7 +513,7 @@ describe('useOAuthClientState', () => {
const { result } = renderHook(() => useOAuthClientState({
...defaultParams,
- onClose,
+ onOpenChange,
showOAuthCreateModal,
}))
@@ -521,7 +521,7 @@ describe('useOAuthClientState', () => {
result.current.handleSave(true)
})
- expect(onClose).toHaveBeenCalled()
+ expect(onOpenChange).toHaveBeenCalledWith(false)
expect(showOAuthCreateModal).toHaveBeenCalledWith(builder)
expect(mockToastNotify).toHaveBeenCalledWith({
type: 'success',
@@ -530,7 +530,7 @@ describe('useOAuthClientState', () => {
})
it('should not call callbacks when OAuth callback returns falsy', () => {
- const onClose = vi.fn()
+ const onOpenChange = vi.fn()
const showOAuthCreateModal = vi.fn()
mockConfigureOAuth.mockImplementation((params, { onSuccess }) => onSuccess())
@@ -546,7 +546,7 @@ describe('useOAuthClientState', () => {
const { result } = renderHook(() => useOAuthClientState({
...defaultParams,
- onClose,
+ onOpenChange,
showOAuthCreateModal,
}))
@@ -554,7 +554,7 @@ describe('useOAuthClientState', () => {
result.current.handleSave(true)
})
- expect(onClose).not.toHaveBeenCalled()
+ expect(onOpenChange).not.toHaveBeenCalled()
expect(showOAuthCreateModal).not.toHaveBeenCalled()
})
})
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-oauth-client-state.ts b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-oauth-client-state.ts
index 25058e529c..e2bd284d15 100644
--- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-oauth-client-state.ts
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-oauth-client-state.ts
@@ -13,16 +13,20 @@ import {
useVerifyAndUpdateTriggerSubscriptionBuilder,
} from '@/service/use-triggers'
-export enum AuthorizationStatusEnum {
- Pending = 'pending',
- Success = 'success',
- Failed = 'failed',
-}
+export const AuthorizationStatusEnum = {
+ Pending: 'pending',
+ Success: 'success',
+ Failed: 'failed',
+} as const
-export enum ClientTypeEnum {
- Default = 'default',
- Custom = 'custom',
-}
+export type AuthorizationStatusEnum = typeof AuthorizationStatusEnum[keyof typeof AuthorizationStatusEnum]
+
+export const ClientTypeEnum = {
+ Default: 'default',
+ Custom: 'custom',
+} as const
+
+export type ClientTypeEnum = typeof ClientTypeEnum[keyof typeof ClientTypeEnum]
const POLL_INTERVAL_MS = 3000
@@ -41,7 +45,7 @@ export const getErrorMessage = (error: unknown, fallback: string): string => {
type UseOAuthClientStateParams = {
oauthConfig?: TriggerOAuthConfig
providerName: string
- onClose: () => void
+ onOpenChange: (open: boolean) => void
showOAuthCreateModal: (builder: TriggerSubscriptionBuilder) => void
}
@@ -67,7 +71,7 @@ type UseOAuthClientStateReturn = {
export const useOAuthClientState = ({
oauthConfig,
providerName,
- onClose,
+ onOpenChange,
showOAuthCreateModal,
}: UseOAuthClientStateParams): UseOAuthClientStateReturn => {
const { t } = useTranslation()
@@ -119,7 +123,7 @@ export const useOAuthClientState = ({
if (!callbackData)
return
toast.success(t('modal.oauth.authorization.authSuccess', { ns: 'pluginTrigger' }))
- onClose()
+ onOpenChange(false)
showOAuthCreateModal(response.subscription_builder)
})
},
@@ -128,20 +132,20 @@ export const useOAuthClientState = ({
toast.error(t('modal.oauth.authorization.authFailed', { ns: 'pluginTrigger' }))
},
})
- }, [providerName, initiateOAuth, onClose, showOAuthCreateModal, t])
+ }, [providerName, initiateOAuth, onOpenChange, showOAuthCreateModal, t])
// Remove handler
const handleRemove = useCallback(() => {
deleteOAuth(providerName, {
onSuccess: () => {
- onClose()
+ onOpenChange(false)
toast.success(t('modal.oauth.remove.success', { ns: 'pluginTrigger' }))
},
onError: (error: unknown) => {
toast.error(getErrorMessage(error, t('modal.oauth.remove.failed', { ns: 'pluginTrigger' })))
},
})
- }, [providerName, deleteOAuth, onClose, t])
+ }, [providerName, deleteOAuth, onOpenChange, t])
// Save handler
const handleSave = useCallback((needAuth: boolean) => {
@@ -174,11 +178,11 @@ export const useOAuthClientState = ({
handleAuthorization()
return
}
- onClose()
+ onOpenChange(false)
toast.success(t('modal.oauth.save.success', { ns: 'pluginTrigger' }))
},
})
- }, [clientType, providerName, oauthClientSchema, oauthConfig?.params, configureOAuth, handleAuthorization, onClose, t])
+ }, [clientType, providerName, oauthClientSchema, oauthConfig?.params, configureOAuth, handleAuthorization, onOpenChange, t])
// Polling effect for authorization verification
useEffect(() => {
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx
index ca1194e6ed..9091cd337c 100644
--- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx
@@ -68,9 +68,20 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
const onClickClientSettings = useCallback((e: React.MouseEvent) => {
e.stopPropagation()
e.preventDefault()
+ setIsMenuOpen(false)
showClientSettingsModal()
}, [showClientSettingsModal])
+ const handleClientSettingsOpenChange = useCallback((open: boolean) => {
+ if (open) {
+ showClientSettingsModal()
+ return
+ }
+
+ hideClientSettingsModal()
+ refetchOAuthConfig()
+ }, [hideClientSettingsModal, refetchOAuthConfig, showClientSettingsModal])
+
const allOptions = useMemo(() => {
const showCustomBadge = oauthConfig?.custom_enabled && oauthConfig?.custom_configured
@@ -299,11 +310,9 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
{isShowClientSettingsModal
? (
{
- hideClientSettingsModal()
- refetchOAuthConfig()
- }}
+ onOpenChange={handleClientSettingsOpenChange}
showOAuthCreateModal={(builder) => {
showCreateModal({
type: SupportedCreationMethods.OAUTH,
@@ -316,5 +325,3 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
>
)
}
-
-export { CreateButtonType, DEFAULT_METHOD } from './types'
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx
index 450324ae40..3c8b8c6aa7 100644
--- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx
@@ -1,31 +1,36 @@
'use client'
import type { TriggerOAuthConfig, TriggerSubscriptionBuilder } from '@/app/components/workflow/block-selector/types'
import { Button } from '@langgenius/dify-ui/button'
+import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
-import {
- RiClipboardLine,
- RiInformation2Fill,
-} from '@remixicon/react'
+import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { BaseForm } from '@/app/components/base/form/components/base'
-import Modal from '@/app/components/base/modal/modal'
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
import { usePluginStore } from '../../store'
-import { ClientTypeEnum, useOAuthClientState } from './hooks/use-oauth-client-state'
+import { ClientTypeEnum, useOAuthClientState as useOAuthClientSettings } from './hooks/use-oauth-client-state'
type Props = {
+ open: boolean
oauthConfig?: TriggerOAuthConfig
- onClose: () => void
+ onOpenChange: (open: boolean) => void
showOAuthCreateModal: (builder: TriggerSubscriptionBuilder) => void
}
const CLIENT_TYPE_OPTIONS = [ClientTypeEnum.Default, ClientTypeEnum.Custom] as const
-export const OAuthClientSettingsModal = ({ oauthConfig, onClose, showOAuthCreateModal }: Props) => {
+export const OAuthClientSettingsModal = ({ open, oauthConfig, onOpenChange, showOAuthCreateModal }: Props) => {
const { t } = useTranslation()
const detail = usePluginStore(state => state.detail)
const providerName = detail?.provider || ''
+ const closeModal = useCallback(() => onOpenChange(false), [onOpenChange])
+ const oauthClientSettings = useOAuthClientSettings({
+ oauthConfig,
+ providerName,
+ onOpenChange,
+ showOAuthCreateModal,
+ })
const {
clientType,
setClientType,
@@ -34,12 +39,7 @@ export const OAuthClientSettingsModal = ({ oauthConfig, onClose, showOAuthCreate
confirmButtonText,
handleRemove,
handleSave,
- } = useOAuthClientState({
- oauthConfig,
- providerName,
- onClose,
- showOAuthCreateModal,
- })
+ } = oauthClientSettings
const isCustomClient = clientType === ClientTypeEnum.Custom
const showRemoveButton = oauthConfig?.custom_enabled && oauthConfig?.params && isCustomClient
@@ -51,81 +51,116 @@ export const OAuthClientSettingsModal = ({ oauthConfig, onClose, showOAuthCreate
toast.success(t('actionMsg.copySuccessfully', { ns: 'common' }))
}
+ const title = t('modal.oauth.title', { ns: 'pluginTrigger' })
+
return (
- handleSave(false)}
- onConfirm={() => handleSave(true)}
- footerSlot={showRemoveButton && (
-
-
- {t('operation.remove', { ns: 'common' })}
-
-
- )}
+