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