Skip to content

Commit 642115c

Browse files
committed
fix: address critical PR feedback for export/import functionality
- Extract shared ImportModeDialog component to eliminate code duplication between ModeSelector and ModesView - Add comprehensive test coverage for export/import functionality in ModeSelector - Implement user-facing error notifications with auto-dismiss for import/export operations - Refactor ModeSelector by extracting ModeSelectorFooter component and useModeSelectorExportImport hook - Reduce component complexity and improve maintainability - Remove unused IconButton import Fixes all critical issues identified in PR #6318 review
1 parent 956891b commit 642115c

File tree

7 files changed

+712
-204
lines changed

7 files changed

+712
-204
lines changed

webview-ui/src/components/chat/ModeSelector.tsx

Lines changed: 47 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import React from "react"
22
import { ChevronUp, Check, X } from "lucide-react"
33
import { cn } from "@/lib/utils"
44
import { useRooPortal } from "@/components/ui/hooks/useRooPortal"
5-
import { Popover, PopoverContent, PopoverTrigger, StandardTooltip, Button } from "@/components/ui"
6-
import { IconButton } from "./IconButton"
5+
import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui"
76
import { vscode } from "@/utils/vscode"
87
import { useExtensionState } from "@/context/ExtensionStateContext"
98
import { useAppTranslation } from "@/i18n/TranslationContext"
@@ -12,6 +11,9 @@ import { ModeConfig, CustomModePrompts } from "@roo-code/types"
1211
import { telemetryClient } from "@/utils/TelemetryClient"
1312
import { TelemetryEventName } from "@roo-code/types"
1413
import { Fzf } from "fzf"
14+
import { ImportModeDialog } from "@/components/common/ImportModeDialog"
15+
import { ModeSelectorFooter } from "./ModeSelectorFooter"
16+
import { useModeSelectorExportImport } from "./useModeSelectorExportImport"
1517

1618
// Minimum number of modes required to show search functionality
1719
const SEARCH_THRESHOLD = 6
@@ -45,8 +47,17 @@ export const ModeSelector = ({
4547
const portalContainer = useRooPortal("roo-portal")
4648
const { hasOpenedModeSelector, setHasOpenedModeSelector } = useExtensionState()
4749
const { t } = useAppTranslation()
48-
const [showImportDialog, setShowImportDialog] = React.useState(false)
49-
const [isImporting, setIsImporting] = React.useState(false)
50+
const {
51+
showImportDialog,
52+
isImporting,
53+
exportError,
54+
importError,
55+
handleExport,
56+
handleImport,
57+
openImportDialog,
58+
closeImportDialog,
59+
clearErrors,
60+
} = useModeSelectorExportImport()
5061

5162
const trackModeSelectorOpened = React.useCallback(() => {
5263
// Track telemetry every time the mode selector is opened
@@ -155,23 +166,6 @@ export const ModeSelector = ({
155166
}
156167
}, [open])
157168

158-
// Handle import/export result messages
159-
React.useEffect(() => {
160-
const handler = (event: MessageEvent) => {
161-
const message = event.data
162-
if (message.type === "importModeResult") {
163-
setIsImporting(false)
164-
setShowImportDialog(false)
165-
if (!message.success && message.error !== "cancelled") {
166-
console.error("Failed to import mode:", message.error)
167-
}
168-
}
169-
}
170-
171-
window.addEventListener("message", handler)
172-
return () => window.removeEventListener("message", handler)
173-
}, [])
174-
175169
// Determine if search should be shown
176170
const showSearch = !disableSearch && modes.length > SEARCH_THRESHOLD
177171

@@ -273,131 +267,44 @@ export const ModeSelector = ({
273267
</div>
274268

275269
{/* Bottom bar with buttons on left and title on right */}
276-
<div className="flex flex-row items-center justify-between px-2 py-2 border-t border-vscode-dropdown-border">
277-
<div className="flex flex-row gap-1">
278-
<IconButton
279-
iconClass="codicon-extensions"
280-
title={t("chat:modeSelector.marketplace")}
281-
onClick={() => {
282-
window.postMessage(
283-
{
284-
type: "action",
285-
action: "marketplaceButtonClicked",
286-
values: { marketplaceTab: "mode" },
287-
},
288-
"*",
289-
)
290-
setOpen(false)
291-
}}
292-
/>
293-
<IconButton
294-
iconClass="codicon-export"
295-
title={t("prompts:exportMode.title")}
296-
onClick={() => {
297-
if (value) {
298-
vscode.postMessage({
299-
type: "exportMode",
300-
slug: value,
301-
})
302-
}
303-
setOpen(false)
304-
}}
305-
/>
306-
<IconButton
307-
iconClass="codicon-import"
308-
title={t("prompts:modes.importMode")}
309-
onClick={() => {
310-
setShowImportDialog(true)
311-
setOpen(false)
312-
}}
313-
/>
314-
<IconButton
315-
iconClass="codicon-settings-gear"
316-
title={t("chat:modeSelector.settings")}
317-
onClick={() => {
318-
vscode.postMessage({
319-
type: "switchTab",
320-
tab: "modes",
321-
})
322-
setOpen(false)
323-
}}
324-
/>
325-
</div>
326-
327-
{/* Info icon and title on the right - only show info icon when search bar is visible */}
328-
<div className="flex items-center gap-1 pr-1">
329-
{showSearch && (
330-
<StandardTooltip content={instructionText}>
331-
<span className="codicon codicon-info text-xs text-vscode-descriptionForeground opacity-70 hover:opacity-100 cursor-help" />
332-
</StandardTooltip>
333-
)}
334-
<h4 className="m-0 font-medium text-sm text-vscode-descriptionForeground">
335-
{t("chat:modeSelector.title")}
336-
</h4>
337-
</div>
338-
</div>
270+
<ModeSelectorFooter
271+
selectedMode={value}
272+
showSearch={showSearch}
273+
instructionText={instructionText}
274+
onExport={() => handleExport(value)}
275+
onImport={openImportDialog}
276+
onClose={() => setOpen(false)}
277+
/>
339278
</div>
340279
</PopoverContent>
341280
</Popover>
342281

343282
{/* Import Mode Dialog */}
344-
{showImportDialog && (
345-
<div className="fixed inset-0 flex items-center justify-center bg-black/50 z-[1000]">
346-
<div className="bg-vscode-editor-background border border-vscode-editor-lineHighlightBorder rounded-lg shadow-lg p-6 max-w-md w-full">
347-
<h3 className="text-lg font-semibold mb-4">{t("prompts:modes.importMode")}</h3>
348-
<p className="text-sm text-vscode-descriptionForeground mb-4">
349-
{t("prompts:importMode.selectLevel")}
350-
</p>
351-
<div className="space-y-3 mb-6">
352-
<label className="flex items-start gap-2 cursor-pointer">
353-
<input
354-
type="radio"
355-
name="importLevel"
356-
value="project"
357-
className="mt-1"
358-
defaultChecked
359-
/>
360-
<div>
361-
<div className="font-medium">{t("prompts:importMode.project.label")}</div>
362-
<div className="text-xs text-vscode-descriptionForeground">
363-
{t("prompts:importMode.project.description")}
364-
</div>
365-
</div>
366-
</label>
367-
<label className="flex items-start gap-2 cursor-pointer">
368-
<input type="radio" name="importLevel" value="global" className="mt-1" />
369-
<div>
370-
<div className="font-medium">{t("prompts:importMode.global.label")}</div>
371-
<div className="text-xs text-vscode-descriptionForeground">
372-
{t("prompts:importMode.global.description")}
373-
</div>
374-
</div>
375-
</label>
376-
</div>
377-
<div className="flex justify-end gap-2">
378-
<Button variant="secondary" onClick={() => setShowImportDialog(false)}>
379-
{t("prompts:createModeDialog.buttons.cancel")}
380-
</Button>
381-
<Button
382-
variant="default"
383-
onClick={() => {
384-
if (!isImporting) {
385-
const selectedLevel = (
386-
document.querySelector(
387-
'input[name="importLevel"]:checked',
388-
) as HTMLInputElement
389-
)?.value as "global" | "project"
390-
setIsImporting(true)
391-
vscode.postMessage({
392-
type: "importMode",
393-
source: selectedLevel || "project",
394-
})
395-
}
396-
}}
397-
disabled={isImporting}>
398-
{isImporting ? t("prompts:importMode.importing") : t("prompts:importMode.import")}
399-
</Button>
283+
<ImportModeDialog
284+
isOpen={showImportDialog}
285+
onClose={closeImportDialog}
286+
onImport={handleImport}
287+
isImporting={isImporting}
288+
/>
289+
290+
{/* Error notifications */}
291+
{(exportError || importError) && (
292+
<div className="fixed bottom-4 right-4 max-w-sm bg-vscode-notifications-background border border-vscode-notifications-border rounded-md shadow-lg p-4 z-[1001]">
293+
<div className="flex items-start gap-2">
294+
<span className="codicon codicon-error text-vscode-errorForeground flex-shrink-0 mt-0.5"></span>
295+
<div className="flex-1">
296+
<div className="text-sm font-medium text-vscode-notifications-foreground">
297+
{exportError ? t("prompts:exportMode.errorTitle") : t("prompts:importMode.errorTitle")}
298+
</div>
299+
<div className="text-xs text-vscode-descriptionForeground mt-1">
300+
{exportError || importError}
301+
</div>
400302
</div>
303+
<button
304+
onClick={clearErrors}
305+
className="text-vscode-icon-foreground hover:text-vscode-foreground">
306+
<X className="h-4 w-4" />
307+
</button>
401308
</div>
402309
</div>
403310
)}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import React from "react"
2+
import { IconButton } from "./IconButton"
3+
import { StandardTooltip } from "@/components/ui"
4+
import { vscode } from "@/utils/vscode"
5+
import { useAppTranslation } from "@/i18n/TranslationContext"
6+
7+
interface ModeSelectorFooterProps {
8+
selectedMode: string | null
9+
showSearch: boolean
10+
instructionText: string
11+
onExport: () => void
12+
onImport: () => void
13+
onClose: () => void
14+
}
15+
16+
export const ModeSelectorFooter: React.FC<ModeSelectorFooterProps> = ({
17+
selectedMode,
18+
showSearch,
19+
instructionText,
20+
onExport,
21+
onImport,
22+
onClose,
23+
}) => {
24+
const { t } = useAppTranslation()
25+
26+
const handleMarketplaceClick = () => {
27+
window.postMessage(
28+
{
29+
type: "action",
30+
action: "marketplaceButtonClicked",
31+
values: { marketplaceTab: "mode" },
32+
},
33+
"*",
34+
)
35+
onClose()
36+
}
37+
38+
const handleExportClick = () => {
39+
if (selectedMode) {
40+
onExport()
41+
}
42+
onClose()
43+
}
44+
45+
const handleImportClick = () => {
46+
onImport()
47+
onClose()
48+
}
49+
50+
const handleSettingsClick = () => {
51+
vscode.postMessage({
52+
type: "switchTab",
53+
tab: "modes",
54+
})
55+
onClose()
56+
}
57+
58+
return (
59+
<div className="flex flex-row items-center justify-between px-2 py-2 border-t border-vscode-dropdown-border">
60+
<div className="flex flex-row gap-1">
61+
<IconButton
62+
iconClass="codicon-extensions"
63+
title={t("chat:modeSelector.marketplace")}
64+
onClick={handleMarketplaceClick}
65+
/>
66+
<IconButton
67+
iconClass="codicon-export"
68+
title={t("prompts:exportMode.title")}
69+
onClick={handleExportClick}
70+
/>
71+
<IconButton
72+
iconClass="codicon-import"
73+
title={t("prompts:modes.importMode")}
74+
onClick={handleImportClick}
75+
/>
76+
<IconButton
77+
iconClass="codicon-settings-gear"
78+
title={t("chat:modeSelector.settings")}
79+
onClick={handleSettingsClick}
80+
/>
81+
</div>
82+
83+
{/* Info icon and title on the right - only show info icon when search bar is visible */}
84+
<div className="flex items-center gap-1 pr-1">
85+
{showSearch && (
86+
<StandardTooltip content={instructionText}>
87+
<span className="codicon codicon-info text-xs text-vscode-descriptionForeground opacity-70 hover:opacity-100 cursor-help" />
88+
</StandardTooltip>
89+
)}
90+
<h4 className="m-0 font-medium text-sm text-vscode-descriptionForeground">
91+
{t("chat:modeSelector.title")}
92+
</h4>
93+
</div>
94+
</div>
95+
)
96+
}

0 commit comments

Comments
 (0)