mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: auto-refresh marketplace data when organization settings change (#6446)
- Added organizationSettingsVersion to ExtensionState type - Modified ClineProvider to include version in state sent to webview - Updated CloudService callback to trigger marketplace refresh on settings change - Added logic to MarketplaceView to detect version changes and request data refresh - Added comprehensive tests for the new functionality This ensures marketplace items (MCPs and modes) stay in sync when organization settings are updated in the cloud. --------- Co-authored-by: Roo Code <roomote@roocode.com> Co-authored-by: John Richmond <5629+jr@users.noreply.github.com>
This commit is contained in:
parent
75f93c41cf
commit
2217a3d548
6 changed files with 170 additions and 57 deletions
|
|
@ -1505,6 +1505,7 @@ export class ClineProvider
|
|||
cloudIsAuthenticated,
|
||||
sharingEnabled,
|
||||
organizationAllowList,
|
||||
organizationSettingsVersion,
|
||||
maxConcurrentFileReads,
|
||||
condensingApiConfigId,
|
||||
customCondensingPrompt,
|
||||
|
|
@ -1617,6 +1618,7 @@ export class ClineProvider
|
|||
cloudIsAuthenticated: cloudIsAuthenticated ?? false,
|
||||
sharingEnabled: sharingEnabled ?? false,
|
||||
organizationAllowList,
|
||||
organizationSettingsVersion,
|
||||
condensingApiConfigId,
|
||||
customCondensingPrompt,
|
||||
codebaseIndexModels: codebaseIndexModels ?? EMBEDDING_MODEL_PROFILES,
|
||||
|
|
@ -1703,6 +1705,19 @@ export class ClineProvider
|
|||
)
|
||||
}
|
||||
|
||||
let organizationSettingsVersion: number = -1
|
||||
|
||||
try {
|
||||
if (CloudService.hasInstance()) {
|
||||
const settings = CloudService.instance.getOrganizationSettings()
|
||||
organizationSettingsVersion = settings?.version ?? -1
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[getState] failed to get organization settings version: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
|
||||
// Return the same structure as before
|
||||
return {
|
||||
apiConfiguration: providerSettings,
|
||||
|
|
@ -1786,6 +1801,7 @@ export class ClineProvider
|
|||
cloudIsAuthenticated,
|
||||
sharingEnabled,
|
||||
organizationAllowList,
|
||||
organizationSettingsVersion,
|
||||
// Explicitly add condensing settings
|
||||
condensingApiConfigId: stateValues.condensingApiConfigId,
|
||||
customCondensingPrompt: stateValues.customCondensingPrompt,
|
||||
|
|
|
|||
|
|
@ -313,6 +313,7 @@ export type ExtensionState = Pick<
|
|||
cloudApiUrl?: string
|
||||
sharingEnabled: boolean
|
||||
organizationAllowList: OrganizationAllowList
|
||||
organizationSettingsVersion?: number
|
||||
|
||||
autoCondenseContext: boolean
|
||||
autoCondenseContextPercent: number
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useEffect, useMemo } from "react"
|
||||
import { useState, useEffect, useMemo, useContext } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Tab, TabContent, TabHeader } from "../common/Tab"
|
||||
import { MarketplaceViewStateManager } from "./MarketplaceViewStateManager"
|
||||
|
|
@ -8,6 +8,7 @@ import { vscode } from "@/utils/vscode"
|
|||
import { MarketplaceListView } from "./MarketplaceListView"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { TooltipProvider } from "@/components/ui/tooltip"
|
||||
import { ExtensionStateContext } from "@/context/ExtensionStateContext"
|
||||
|
||||
interface MarketplaceViewProps {
|
||||
onDone?: () => void
|
||||
|
|
@ -18,6 +19,20 @@ export function MarketplaceView({ stateManager, onDone, targetTab }: Marketplace
|
|||
const { t } = useAppTranslation()
|
||||
const [state, manager] = useStateManager(stateManager)
|
||||
const [hasReceivedInitialState, setHasReceivedInitialState] = useState(false)
|
||||
const extensionState = useContext(ExtensionStateContext)
|
||||
const [lastOrganizationSettingsVersion, setLastOrganizationSettingsVersion] = useState<number>(
|
||||
extensionState?.organizationSettingsVersion ?? -1,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const currentVersion = extensionState?.organizationSettingsVersion ?? -1
|
||||
if (currentVersion !== lastOrganizationSettingsVersion) {
|
||||
vscode.postMessage({
|
||||
type: "fetchMarketplaceData",
|
||||
})
|
||||
}
|
||||
setLastOrganizationSettingsVersion(currentVersion)
|
||||
}, [extensionState?.organizationSettingsVersion, lastOrganizationSettingsVersion])
|
||||
|
||||
// Track when we receive the initial state
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import { render, screen } from "@/utils/test-utils"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
|
||||
import { render, waitFor } from "@testing-library/react"
|
||||
import { vi, describe, it, expect, beforeEach } from "vitest"
|
||||
import { MarketplaceView } from "../MarketplaceView"
|
||||
import { MarketplaceViewStateManager } from "../MarketplaceViewStateManager"
|
||||
import { ExtensionStateContext } from "@/context/ExtensionStateContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
|
||||
vi.mock("@/utils/vscode", () => ({
|
||||
vscode: {
|
||||
postMessage: vi.fn(),
|
||||
getState: vi.fn(() => ({})),
|
||||
setState: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
|
|
@ -18,70 +17,146 @@ vi.mock("@/i18n/TranslationContext", () => ({
|
|||
}),
|
||||
}))
|
||||
|
||||
vi.mock("../useStateManager", () => ({
|
||||
useStateManager: () => [
|
||||
{
|
||||
allItems: [],
|
||||
displayItems: [],
|
||||
isFetching: false,
|
||||
activeTab: "mcp",
|
||||
filters: { type: "", search: "", tags: [] },
|
||||
},
|
||||
{
|
||||
transition: vi.fn(),
|
||||
onStateChange: vi.fn(() => vi.fn()),
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
vi.mock("../MarketplaceListView", () => ({
|
||||
MarketplaceListView: ({ filterByType }: { filterByType: string }) => (
|
||||
<div data-testid="marketplace-list-view">MarketplaceListView - {filterByType}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock Tab components to avoid ExtensionStateContext dependency
|
||||
vi.mock("@/components/common/Tab", () => ({
|
||||
Tab: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
||||
TabHeader: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
||||
TabContent: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
||||
TabList: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
||||
TabTrigger: ({ children, ...props }: any) => <button {...props}>{children}</button>,
|
||||
}))
|
||||
|
||||
describe("MarketplaceView", () => {
|
||||
const mockOnDone = vi.fn()
|
||||
const mockStateManager = new MarketplaceViewStateManager()
|
||||
let stateManager: MarketplaceViewStateManager
|
||||
let mockExtensionState: any
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
stateManager = new MarketplaceViewStateManager()
|
||||
|
||||
// Initialize state manager with some test data
|
||||
stateManager.transition({
|
||||
type: "FETCH_COMPLETE",
|
||||
payload: {
|
||||
items: [
|
||||
{
|
||||
id: "test-mcp",
|
||||
name: "Test MCP",
|
||||
type: "mcp" as const,
|
||||
description: "Test MCP server",
|
||||
tags: ["test"],
|
||||
content: "Test content",
|
||||
url: "https://test.com",
|
||||
author: "Test Author",
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
mockExtensionState = {
|
||||
organizationSettingsVersion: 1,
|
||||
// Add other required properties for the context
|
||||
didHydrateState: true,
|
||||
showWelcome: false,
|
||||
theme: {},
|
||||
mcpServers: [],
|
||||
filePaths: [],
|
||||
openedTabs: [],
|
||||
commands: [],
|
||||
organizationAllowList: { allowAll: true, providers: {} },
|
||||
cloudIsAuthenticated: false,
|
||||
sharingEnabled: false,
|
||||
hasOpenedModeSelector: false,
|
||||
setHasOpenedModeSelector: vi.fn(),
|
||||
alwaysAllowFollowupQuestions: false,
|
||||
setAlwaysAllowFollowupQuestions: vi.fn(),
|
||||
followupAutoApproveTimeoutMs: 60000,
|
||||
setFollowupAutoApproveTimeoutMs: vi.fn(),
|
||||
profileThresholds: {},
|
||||
setProfileThresholds: vi.fn(),
|
||||
// ... other required context properties
|
||||
}
|
||||
})
|
||||
|
||||
it("renders without crashing", () => {
|
||||
render(<MarketplaceView stateManager={mockStateManager} onDone={mockOnDone} />)
|
||||
it("should trigger fetchMarketplaceData when organization settings version changes", async () => {
|
||||
const { rerender } = render(
|
||||
<ExtensionStateContext.Provider value={mockExtensionState}>
|
||||
<MarketplaceView stateManager={stateManager} />
|
||||
</ExtensionStateContext.Provider>,
|
||||
)
|
||||
|
||||
expect(screen.getByText("marketplace:title")).toBeInTheDocument()
|
||||
expect(screen.getByText("marketplace:done")).toBeInTheDocument()
|
||||
// Initial render should not trigger fetch (version hasn't changed)
|
||||
expect(vscode.postMessage).not.toHaveBeenCalledWith({
|
||||
type: "fetchMarketplaceData",
|
||||
})
|
||||
|
||||
// Update the organization settings version
|
||||
mockExtensionState = {
|
||||
...mockExtensionState,
|
||||
organizationSettingsVersion: 2,
|
||||
}
|
||||
|
||||
// Re-render with updated context
|
||||
rerender(
|
||||
<ExtensionStateContext.Provider value={mockExtensionState}>
|
||||
<MarketplaceView stateManager={stateManager} />
|
||||
</ExtensionStateContext.Provider>,
|
||||
)
|
||||
|
||||
// Wait for the effect to run
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "fetchMarketplaceData",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("calls onDone when Done button is clicked", async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<MarketplaceView stateManager={mockStateManager} onDone={mockOnDone} />)
|
||||
it("should trigger fetchMarketplaceData when organization settings version changes from -1", async () => {
|
||||
// Start with -1 version (default)
|
||||
mockExtensionState = {
|
||||
...mockExtensionState,
|
||||
organizationSettingsVersion: -1,
|
||||
}
|
||||
|
||||
await user.click(screen.getByText("marketplace:done"))
|
||||
expect(mockOnDone).toHaveBeenCalledTimes(1)
|
||||
const { rerender } = render(
|
||||
<ExtensionStateContext.Provider value={mockExtensionState}>
|
||||
<MarketplaceView stateManager={stateManager} />
|
||||
</ExtensionStateContext.Provider>,
|
||||
)
|
||||
|
||||
// Clear any initial calls
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Update to a defined version
|
||||
mockExtensionState = {
|
||||
...mockExtensionState,
|
||||
organizationSettingsVersion: 1,
|
||||
}
|
||||
|
||||
rerender(
|
||||
<ExtensionStateContext.Provider value={mockExtensionState}>
|
||||
<MarketplaceView stateManager={stateManager} />
|
||||
</ExtensionStateContext.Provider>,
|
||||
)
|
||||
|
||||
// Should trigger fetch when transitioning from -1 to 1
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "fetchMarketplaceData",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("renders tab buttons", () => {
|
||||
render(<MarketplaceView stateManager={mockStateManager} onDone={mockOnDone} />)
|
||||
it("should not trigger fetchMarketplaceData when organization settings version remains the same", async () => {
|
||||
const { rerender } = render(
|
||||
<ExtensionStateContext.Provider value={mockExtensionState}>
|
||||
<MarketplaceView stateManager={stateManager} />
|
||||
</ExtensionStateContext.Provider>,
|
||||
)
|
||||
|
||||
expect(screen.getByText("MCP")).toBeInTheDocument()
|
||||
expect(screen.getByText("Modes")).toBeInTheDocument()
|
||||
})
|
||||
// Re-render with same version
|
||||
rerender(
|
||||
<ExtensionStateContext.Provider value={mockExtensionState}>
|
||||
<MarketplaceView stateManager={stateManager} />
|
||||
</ExtensionStateContext.Provider>,
|
||||
)
|
||||
|
||||
it("renders MarketplaceListView", () => {
|
||||
render(<MarketplaceView stateManager={mockStateManager} onDone={mockOnDone} />)
|
||||
|
||||
expect(screen.getByTestId("marketplace-list-view")).toBeInTheDocument()
|
||||
// Should not trigger fetch when version hasn't changed
|
||||
await waitFor(() => {
|
||||
expect(vscode.postMessage).not.toHaveBeenCalledWith({
|
||||
type: "fetchMarketplaceData",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -13,7 +13,10 @@ export function useStateManager(existingManager?: MarketplaceViewStateManager) {
|
|||
prevState.isFetching !== newState.isFetching ||
|
||||
prevState.activeTab !== newState.activeTab ||
|
||||
JSON.stringify(prevState.allItems) !== JSON.stringify(newState.allItems) ||
|
||||
JSON.stringify(prevState.organizationMcps) !== JSON.stringify(newState.organizationMcps) ||
|
||||
JSON.stringify(prevState.displayItems) !== JSON.stringify(newState.displayItems) ||
|
||||
JSON.stringify(prevState.displayOrganizationMcps) !==
|
||||
JSON.stringify(newState.displayOrganizationMcps) ||
|
||||
JSON.stringify(prevState.filters) !== JSON.stringify(newState.filters)
|
||||
|
||||
return hasChanged ? newState : prevState
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||
openedTabs: Array<{ label: string; isActive: boolean; path?: string }>
|
||||
commands: Command[]
|
||||
organizationAllowList: OrganizationAllowList
|
||||
organizationSettingsVersion: number
|
||||
cloudIsAuthenticated: boolean
|
||||
sharingEnabled: boolean
|
||||
maxConcurrentFileReads?: number
|
||||
|
|
@ -226,6 +227,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
cloudIsAuthenticated: false,
|
||||
sharingEnabled: false,
|
||||
organizationAllowList: ORGANIZATION_ALLOW_ALL,
|
||||
organizationSettingsVersion: -1,
|
||||
autoCondenseContext: true,
|
||||
autoCondenseContextPercent: 100,
|
||||
profileThresholds: {},
|
||||
|
|
@ -392,6 +394,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
screenshotQuality: state.screenshotQuality,
|
||||
routerModels: extensionRouterModels,
|
||||
cloudIsAuthenticated: state.cloudIsAuthenticated ?? false,
|
||||
organizationSettingsVersion: state.organizationSettingsVersion ?? -1,
|
||||
marketplaceItems,
|
||||
marketplaceInstalledMetadata,
|
||||
profileThresholds: state.profileThresholds ?? {},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue