Add refresh loading feedback in file explorer
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
@@ -264,25 +265,35 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
|
||||
setSortOption(SORT_OPTIONS[nextIndex].value);
|
||||
}, [sortOption, setSortOption]);
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
if (!agentId || !requestDirectoryListing) {
|
||||
return;
|
||||
}
|
||||
// Refresh the root directory
|
||||
requestDirectoryListing(agentId, ".", { recordHistory: false, setCurrentPath: false });
|
||||
|
||||
// Also refresh any expanded directories
|
||||
expandedPaths.forEach((path) => {
|
||||
if (path !== ".") {
|
||||
requestDirectoryListing(agentId, path, { recordHistory: false, setCurrentPath: false });
|
||||
const { refetch: refetchExplorer, isFetching: isRefreshFetching } = useQuery({
|
||||
queryKey: ["fileExplorerRefresh", serverId, agentId],
|
||||
queryFn: async () => {
|
||||
if (!agentId) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh the preview if a file is selected
|
||||
if (selectedEntryPath && requestFilePreview) {
|
||||
requestFilePreview(agentId, selectedEntryPath);
|
||||
}
|
||||
}, [agentId, expandedPaths, requestDirectoryListing, requestFilePreview, selectedEntryPath]);
|
||||
const directoryPaths = Array.from(expandedPaths);
|
||||
if (!directoryPaths.includes(".")) {
|
||||
directoryPaths.unshift(".");
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
...directoryPaths.map((path) =>
|
||||
requestDirectoryListing(agentId, path, {
|
||||
recordHistory: false,
|
||||
setCurrentPath: false,
|
||||
})
|
||||
),
|
||||
...(selectedEntryPath ? [requestFilePreview(agentId, selectedEntryPath)] : []),
|
||||
]);
|
||||
return null;
|
||||
},
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
void refetchExplorer();
|
||||
}, [refetchExplorer]);
|
||||
|
||||
const currentSortLabel = SORT_OPTIONS.find((opt) => opt.value === sortOption)?.label ?? "Name";
|
||||
|
||||
@@ -627,15 +638,20 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
|
||||
<View style={styles.paneHeaderRight}>
|
||||
<Pressable
|
||||
onPress={handleRefresh}
|
||||
disabled={isRefreshFetching}
|
||||
hitSlop={8}
|
||||
style={({ hovered, pressed }) => [
|
||||
styles.iconButton,
|
||||
(hovered || pressed) && styles.iconButtonHovered,
|
||||
pressed && styles.iconButtonPressed,
|
||||
isRefreshFetching && styles.iconButtonLoading,
|
||||
]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Refresh files"
|
||||
>
|
||||
<RotateCw size={16} color={theme.colors.foregroundMuted} />
|
||||
<View style={isRefreshFetching ? styles.refreshIconLoading : undefined}>
|
||||
<RotateCw size={16} color={theme.colors.foregroundMuted} />
|
||||
</View>
|
||||
</Pressable>
|
||||
<Pressable style={styles.sortButton} onPress={handleSortCycle}>
|
||||
<Text style={styles.sortButtonText}>{currentSortLabel}</Text>
|
||||
@@ -660,15 +676,20 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
|
||||
<View style={styles.paneHeaderRight}>
|
||||
<Pressable
|
||||
onPress={handleRefresh}
|
||||
disabled={isRefreshFetching}
|
||||
hitSlop={8}
|
||||
style={({ hovered, pressed }) => [
|
||||
styles.iconButton,
|
||||
(hovered || pressed) && styles.iconButtonHovered,
|
||||
pressed && styles.iconButtonPressed,
|
||||
isRefreshFetching && styles.iconButtonLoading,
|
||||
]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Refresh files"
|
||||
>
|
||||
<RotateCw size={16} color={theme.colors.foregroundMuted} />
|
||||
<View style={isRefreshFetching ? styles.refreshIconLoading : undefined}>
|
||||
<RotateCw size={16} color={theme.colors.foregroundMuted} />
|
||||
</View>
|
||||
</Pressable>
|
||||
<Pressable style={styles.sortButton} onPress={handleSortCycle}>
|
||||
<Text style={styles.sortButtonText}>{currentSortLabel}</Text>
|
||||
@@ -1221,6 +1242,16 @@ const styles = StyleSheet.create((theme) => ({
|
||||
iconButtonHovered: {
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
iconButtonPressed: {
|
||||
opacity: 0.8,
|
||||
transform: [{ scale: 0.96 }],
|
||||
},
|
||||
iconButtonLoading: {
|
||||
opacity: 0.8,
|
||||
},
|
||||
refreshIconLoading: {
|
||||
opacity: 0.55,
|
||||
},
|
||||
previewContent: {
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
@@ -41,7 +41,7 @@ export function useFileExplorerActions(serverId: string) {
|
||||
);
|
||||
|
||||
const requestDirectoryListing = useCallback(
|
||||
(
|
||||
async (
|
||||
agentId: string,
|
||||
path: string,
|
||||
options?: { recordHistory?: boolean; setCurrentPath?: boolean }
|
||||
@@ -77,42 +77,40 @@ export function useFileExplorerActions(serverId: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
void client
|
||||
.exploreFileSystem(agentId, normalizedPath, "list")
|
||||
.then((payload) => {
|
||||
updateExplorerState(agentId, (state) => {
|
||||
const nextState: AgentFileExplorerState = {
|
||||
...state,
|
||||
isLoading: false,
|
||||
lastError: payload.error ?? null,
|
||||
pendingRequest: null,
|
||||
directories: state.directories,
|
||||
files: state.files,
|
||||
};
|
||||
|
||||
if (!payload.error && payload.directory) {
|
||||
const directories = new Map(state.directories);
|
||||
directories.set(payload.directory.path, payload.directory);
|
||||
nextState.directories = directories;
|
||||
}
|
||||
|
||||
return nextState;
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
updateExplorerState(agentId, (state) => ({
|
||||
try {
|
||||
const payload = await client.exploreFileSystem(agentId, normalizedPath, "list");
|
||||
updateExplorerState(agentId, (state) => {
|
||||
const nextState: AgentFileExplorerState = {
|
||||
...state,
|
||||
isLoading: false,
|
||||
lastError: error instanceof Error ? error.message : "Failed to list directory",
|
||||
lastError: payload.error ?? null,
|
||||
pendingRequest: null,
|
||||
}));
|
||||
directories: state.directories,
|
||||
files: state.files,
|
||||
};
|
||||
|
||||
if (!payload.error && payload.directory) {
|
||||
const directories = new Map(state.directories);
|
||||
directories.set(payload.directory.path, payload.directory);
|
||||
nextState.directories = directories;
|
||||
}
|
||||
|
||||
return nextState;
|
||||
});
|
||||
} catch (error) {
|
||||
updateExplorerState(agentId, (state) => ({
|
||||
...state,
|
||||
isLoading: false,
|
||||
lastError: error instanceof Error ? error.message : "Failed to list directory",
|
||||
pendingRequest: null,
|
||||
}));
|
||||
}
|
||||
},
|
||||
[client, updateExplorerState]
|
||||
);
|
||||
|
||||
const requestFilePreview = useCallback(
|
||||
(agentId: string, path: string) => {
|
||||
async (agentId: string, path: string) => {
|
||||
const normalizedPath = path && path.length > 0 ? path : ".";
|
||||
updateExplorerState(agentId, (state) => ({
|
||||
...state,
|
||||
@@ -131,36 +129,34 @@ export function useFileExplorerActions(serverId: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
void client
|
||||
.exploreFileSystem(agentId, normalizedPath, "file")
|
||||
.then((payload) => {
|
||||
updateExplorerState(agentId, (state) => {
|
||||
const nextState: AgentFileExplorerState = {
|
||||
...state,
|
||||
isLoading: false,
|
||||
pendingRequest: null,
|
||||
directories: state.directories,
|
||||
files: state.files,
|
||||
};
|
||||
|
||||
if (!payload.error && payload.file) {
|
||||
const files = new Map(state.files);
|
||||
files.set(payload.file.path, payload.file);
|
||||
nextState.files = files;
|
||||
} else if (payload.error) {
|
||||
nextState.lastError = payload.error;
|
||||
}
|
||||
|
||||
return nextState;
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
updateExplorerState(agentId, (state) => ({
|
||||
try {
|
||||
const payload = await client.exploreFileSystem(agentId, normalizedPath, "file");
|
||||
updateExplorerState(agentId, (state) => {
|
||||
const nextState: AgentFileExplorerState = {
|
||||
...state,
|
||||
isLoading: false,
|
||||
pendingRequest: null,
|
||||
}));
|
||||
directories: state.directories,
|
||||
files: state.files,
|
||||
};
|
||||
|
||||
if (!payload.error && payload.file) {
|
||||
const files = new Map(state.files);
|
||||
files.set(payload.file.path, payload.file);
|
||||
nextState.files = files;
|
||||
} else if (payload.error) {
|
||||
nextState.lastError = payload.error;
|
||||
}
|
||||
|
||||
return nextState;
|
||||
});
|
||||
} catch {
|
||||
updateExplorerState(agentId, (state) => ({
|
||||
...state,
|
||||
isLoading: false,
|
||||
pendingRequest: null,
|
||||
}));
|
||||
}
|
||||
},
|
||||
[client, updateExplorerState]
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user