ui: toggle back off after failure (#1911)

This commit is contained in:
Lily Delalande
2025-03-28 17:10:18 -04:00
committed by GitHub
parent a837404248
commit b22d173425
3 changed files with 56 additions and 14 deletions
@@ -49,14 +49,22 @@ export default function ExtensionsSection() {
const toggleDirection = extension.enabled ? 'toggleOff' : 'toggleOn'; const toggleDirection = extension.enabled ? 'toggleOff' : 'toggleOn';
const extensionConfig = extractExtensionConfig(extension); const extensionConfig = extractExtensionConfig(extension);
await toggleExtension({ try {
toggle: toggleDirection, await toggleExtension({
extensionConfig: extensionConfig, toggle: toggleDirection,
addToConfig: addExtension, extensionConfig: extensionConfig,
toastOptions: { silent: false }, addToConfig: addExtension,
}); toastOptions: { silent: false },
});
await fetchExtensions(); // Refresh the list after toggling await fetchExtensions(); // Refresh the list after successful toggle
return true; // Indicate success
} catch (error) {
console.error('Toggle extension failed:', error);
// Don't refresh the extension list on failure - this allows our visual state rollback to work
// The actual state in the config hasn't changed anyway
throw error; // Re-throw to let the ExtensionItem component know it failed
}
}; };
const handleConfigureClick = (extension: FixedExtensionEntry) => { const handleConfigureClick = (extension: FixedExtensionEntry) => {
@@ -1,5 +1,4 @@
// ExtensionItem.tsx import React, { useState, useEffect } from 'react';
import React from 'react';
import { Switch } from '../../../ui/switch'; import { Switch } from '../../../ui/switch';
import { Gear } from '../../../icons/Gear'; import { Gear } from '../../../icons/Gear';
import { FixedExtensionEntry } from '../../../ConfigContext'; import { FixedExtensionEntry } from '../../../ConfigContext';
@@ -7,11 +6,46 @@ import { getSubtitle, getFriendlyTitle } from './ExtensionList';
interface ExtensionItemProps { interface ExtensionItemProps {
extension: FixedExtensionEntry; extension: FixedExtensionEntry;
onToggle: (extension: FixedExtensionEntry) => void; onToggle: (extension: FixedExtensionEntry) => Promise<boolean | void>;
onConfigure: (extension: FixedExtensionEntry) => void; onConfigure: (extension: FixedExtensionEntry) => void;
} }
export default function ExtensionItem({ extension, onToggle, onConfigure }: ExtensionItemProps) { export default function ExtensionItem({ extension, onToggle, onConfigure }: ExtensionItemProps) {
// Add local state to track the visual toggle state
const [visuallyEnabled, setVisuallyEnabled] = useState(extension.enabled);
// Track if we're in the process of toggling
const [isToggling, setIsToggling] = useState(false);
const handleToggle = async (ext: FixedExtensionEntry) => {
// Prevent multiple toggles while one is in progress
if (isToggling) return;
setIsToggling(true);
// Immediately update visual state
const newState = !ext.enabled;
setVisuallyEnabled(newState);
try {
// Call the actual toggle function that performs the async operation
await onToggle(ext);
// Success case is handled by the useEffect below when extension.enabled changes
} catch (error) {
// If there was an error, revert the visual state
console.log('Toggle failed, reverting visual state');
setVisuallyEnabled(!newState);
} finally {
setIsToggling(false);
}
};
// Update visual state when the actual extension state changes
useEffect(() => {
if (!isToggling) {
setVisuallyEnabled(extension.enabled);
}
}, [extension.enabled, isToggling]);
const renderFormattedSubtitle = () => { const renderFormattedSubtitle = () => {
const subtitle = getSubtitle(extension); const subtitle = getSubtitle(extension);
return subtitle.split('\n').map((part, index) => ( return subtitle.split('\n').map((part, index) => (
@@ -21,6 +55,7 @@ export default function ExtensionItem({ extension, onToggle, onConfigure }: Exte
</React.Fragment> </React.Fragment>
)); ));
}; };
return ( return (
<div className="rounded-lg border border-borderSubtle p-4 mb-2"> <div className="rounded-lg border border-borderSubtle p-4 mb-2">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
@@ -36,8 +71,8 @@ export default function ExtensionItem({ extension, onToggle, onConfigure }: Exte
</button> </button>
)} )}
<Switch <Switch
checked={extension.enabled} checked={(isToggling && visuallyEnabled) || extension.enabled}
onCheckedChange={() => onToggle(extension)} onCheckedChange={() => handleToggle(extension)}
variant="mono" variant="mono"
/> />
</div> </div>
@@ -7,7 +7,7 @@ import { combineCmdAndArgs } from '../utils';
interface ExtensionListProps { interface ExtensionListProps {
extensions: FixedExtensionEntry[]; extensions: FixedExtensionEntry[];
onToggle: (extension: FixedExtensionEntry) => void; onToggle: (extension: FixedExtensionEntry) => Promise<boolean | void>;
onConfigure: (extension: FixedExtensionEntry) => void; onConfigure: (extension: FixedExtensionEntry) => void;
} }
@@ -25,7 +25,6 @@ export default function ExtensionList({ extensions, onToggle, onConfigure }: Ext
</div> </div>
); );
} }
// Helper functions // Helper functions
// Helper function to get a friendly title from extension name // Helper function to get a friendly title from extension name
export function getFriendlyTitle(extension: FixedExtensionEntry): string { export function getFriendlyTitle(extension: FixedExtensionEntry): string {