refactor plugin installation query state

This commit is contained in:
yyh 2025-12-26 16:08:21 +08:00
parent 4c0fc56dd5
commit 1785dce04e
No known key found for this signature in database
3 changed files with 80 additions and 45 deletions

View File

@ -86,15 +86,13 @@ const PluginPage = ({
return return
} }
if (bundleInfo) { if (bundleInfo) {
// bundleInfo is a JSON string from URL, needs parsing
try { try {
const parsedBundleInfo = typeof bundleInfo === 'string' ? JSON.parse(bundleInfo) : bundleInfo const { data } = await fetchBundleInfoFromMarketPlace(bundleInfo)
const { data } = await fetchBundleInfoFromMarketPlace(parsedBundleInfo)
setDependencies(data.version.dependencies) setDependencies(data.version.dependencies)
showInstallFromMarketplace() showInstallFromMarketplace()
} }
catch (e) { catch (error) {
console.error('Failed to parse bundle info:', e) console.error('Failed to load bundle info:', error)
} }
} }
})() })()

View File

@ -474,9 +474,10 @@ describe('useQueryParams hooks', () => {
describe('usePluginInstallation', () => { describe('usePluginInstallation', () => {
it('should parse package ids from JSON arrays', () => { it('should parse package ids from JSON arrays', () => {
// Arrange // Arrange
const bundleInfo = { org: 'org', name: 'bundle', version: '1.0.0' }
const { result } = renderWithAdapter( const { result } = renderWithAdapter(
() => usePluginInstallation(), () => usePluginInstallation(),
'?package-ids=%5B%22org%2Fplugin%22%5D&bundle-info=bundle', `?package-ids=%5B%22org%2Fplugin%22%5D&bundle-info=${encodeURIComponent(JSON.stringify(bundleInfo))}`,
) )
// Act // Act
@ -484,7 +485,7 @@ describe('useQueryParams hooks', () => {
// Assert // Assert
expect(state.packageId).toBe('org/plugin') expect(state.packageId).toBe('org/plugin')
expect(state.bundleInfo).toBe('bundle') expect(state.bundleInfo).toEqual(bundleInfo)
}) })
it('should return raw package id when JSON parsing fails', () => { it('should return raw package id when JSON parsing fails', () => {
@ -532,24 +533,26 @@ describe('useQueryParams hooks', () => {
it('should set bundle info when provided', async () => { it('should set bundle info when provided', async () => {
// Arrange // Arrange
const bundleInfo = { org: 'org', name: 'bundle', version: '1.0.0' }
const { result, onUrlUpdate } = renderWithAdapter(() => usePluginInstallation()) const { result, onUrlUpdate } = renderWithAdapter(() => usePluginInstallation())
// Act // Act
act(() => { act(() => {
result.current[1]({ bundleInfo: 'bundle' }) result.current[1]({ bundleInfo })
}) })
// Assert // Assert
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled()) await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0] const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
expect(update.searchParams.get('bundle-info')).toBe('bundle') expect(update.searchParams.get('bundle-info')).toBe(JSON.stringify(bundleInfo))
}) })
it('should clear installation params when state is null', async () => { it('should clear installation params when state is null', async () => {
// Arrange // Arrange
const bundleInfo = { org: 'org', name: 'bundle', version: '1.0.0' }
const { result, onUrlUpdate } = renderWithAdapter( const { result, onUrlUpdate } = renderWithAdapter(
() => usePluginInstallation(), () => usePluginInstallation(),
'?package-ids=%5B%22org%2Fplugin%22%5D&bundle-info=bundle', `?package-ids=%5B%22org%2Fplugin%22%5D&bundle-info=${encodeURIComponent(JSON.stringify(bundleInfo))}`,
) )
// Act // Act
@ -566,9 +569,10 @@ describe('useQueryParams hooks', () => {
it('should preserve bundle info when only packageId is updated', async () => { it('should preserve bundle info when only packageId is updated', async () => {
// Arrange // Arrange
const bundleInfo = { org: 'org', name: 'bundle', version: '1.0.0' }
const { result, onUrlUpdate } = renderWithAdapter( const { result, onUrlUpdate } = renderWithAdapter(
() => usePluginInstallation(), () => usePluginInstallation(),
'?bundle-info=bundle', `?bundle-info=${encodeURIComponent(JSON.stringify(bundleInfo))}`,
) )
// Act // Act
@ -579,7 +583,7 @@ describe('useQueryParams hooks', () => {
// Assert // Assert
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled()) await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0] const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
expect(update.searchParams.get('bundle-info')).toBe('bundle') expect(update.searchParams.get('bundle-info')).toBe(JSON.stringify(bundleInfo))
}) })
}) })
}) })

View File

@ -141,6 +141,47 @@ export function useMarketplaceFilters() {
*/ */
const PACKAGE_IDS_PARAM = 'package-ids' const PACKAGE_IDS_PARAM = 'package-ids'
const BUNDLE_INFO_PARAM = 'bundle-info' const BUNDLE_INFO_PARAM = 'bundle-info'
type BundleInfoQuery = {
org: string
name: string
version: string
}
const parseAsPackageId = createParser<string>({
parse: (value) => {
try {
const parsed = JSON.parse(value)
if (Array.isArray(parsed)) {
const first = parsed[0]
return typeof first === 'string' ? first : null
}
return value
}
catch {
return value
}
},
serialize: value => JSON.stringify([value]),
})
const parseAsBundleInfo = createParser<BundleInfoQuery>({
parse: (value) => {
try {
const parsed = JSON.parse(value) as Partial<BundleInfoQuery>
if (parsed
&& typeof parsed.org === 'string'
&& typeof parsed.name === 'string'
&& typeof parsed.version === 'string') {
return { org: parsed.org, name: parsed.name, version: parsed.version }
}
}
catch {
return null
}
return null
},
serialize: value => JSON.stringify(value),
})
/** /**
* Hook to manage plugin installation state via URL * Hook to manage plugin installation state via URL
@ -149,53 +190,45 @@ const BUNDLE_INFO_PARAM = 'bundle-info'
* @example * @example
* const [installState, setInstallState] = usePluginInstallation() * const [installState, setInstallState] = usePluginInstallation()
* setInstallState({ packageId: 'org/plugin' }) // Sets ?package-ids=["org/plugin"] * setInstallState({ packageId: 'org/plugin' }) // Sets ?package-ids=["org/plugin"]
* setInstallState({ bundleInfo: { org: 'org', name: 'bundle', version: '1.0.0' } }) // Sets ?bundle-info=...
* setInstallState(null) // Clears installation params * setInstallState(null) // Clears installation params
*/ */
export function usePluginInstallation() { export function usePluginInstallation() {
const [packageIds, setPackageIds] = useQueryState( const [installState, setInstallStateState] = useQueryStates(
PACKAGE_IDS_PARAM, {
parseAsString, packageId: parseAsPackageId,
) bundleInfo: parseAsBundleInfo,
const [bundleInfo, setBundleInfo] = useQueryState( },
BUNDLE_INFO_PARAM, {
parseAsString, urlKeys: {
packageId: PACKAGE_IDS_PARAM,
bundleInfo: BUNDLE_INFO_PARAM,
},
},
) )
const setInstallState = useCallback( const setInstallState = useCallback(
(state: { packageId?: string, bundleInfo?: string } | null) => { (state: { packageId?: string, bundleInfo?: BundleInfoQuery } | null) => {
if (!state) { if (!state) {
setPackageIds(null) setInstallStateState(null)
setBundleInfo(null)
return return
} }
if (state.packageId) { const patch: { packageId?: string, bundleInfo?: BundleInfoQuery } = {}
// Store as JSON array for consistency with existing code if (state.packageId)
setPackageIds(JSON.stringify([state.packageId])) patch.packageId = state.packageId
} if (state.bundleInfo)
if (state.bundleInfo) { patch.bundleInfo = state.bundleInfo
setBundleInfo(state.bundleInfo) if (Object.keys(patch).length === 0)
} return
setInstallStateState(patch)
}, },
[setBundleInfo, setPackageIds], [setInstallStateState],
) )
// Parse packageIds from JSON array
const currentPackageId = packageIds
? (() => {
try {
const parsed = JSON.parse(packageIds)
return Array.isArray(parsed) ? parsed[0] : packageIds
}
catch {
return packageIds
}
})()
: null
return [ return [
{ {
packageId: currentPackageId, packageId: installState.packageId,
bundleInfo, bundleInfo: installState.bundleInfo,
}, },
setInstallState, setInstallState,
] as const ] as const