feat: hide dotfiles in file explorer (#1516)
* feat: hide dotfiles in file explorer * refactor: rename to "hidden files" and read flag from store Rename the explorer dotfile toggle to show-semantics "hidden files" so copy and code share one cross-platform vocabulary, and stop prop-drilling the flag now that it lives in the panel store. - explorerShowHiddenFiles (default true) replaces explorerHideDotFiles; i18n keys + translations switched from "dotfiles" to "hidden files" - imperative helpers read usePanelStore.getState() instead of taking the flag as a parameter; flag stays threaded only through the reactive tree-row render path - drop dead usePanelState exposure; collapse migration double-guard * fix: address Greptile review on hidden-file path matching - isHiddenExplorerPath no longer treats ".." segments as hidden (only "." was exempt before); add tests for ".." and "../sibling" - assert the showHiddenFiles i18n key alongside hideHiddenFiles --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
co-authored by
Mohamed Boudra
parent
f4450cda51
commit
eb94b70848
@@ -22,6 +22,8 @@ import {
|
||||
ChevronRight,
|
||||
Copy,
|
||||
Download,
|
||||
Eye,
|
||||
EyeOff,
|
||||
MoreVertical,
|
||||
RotateCw,
|
||||
} from "lucide-react-native";
|
||||
@@ -43,6 +45,7 @@ import { buildWorkspaceExplorerStateKey } from "@/hooks/use-file-explorer-action
|
||||
import { usePanelStore, type SortOption } from "@/stores/panel-store";
|
||||
import { formatTimeAgo } from "@/utils/time";
|
||||
import { buildAbsoluteExplorerPath } from "@/utils/explorer-paths";
|
||||
import { filterVisibleExplorerEntries, isHiddenExplorerPath } from "@/file-explorer/visibility";
|
||||
import { useWebScrollViewScrollbar } from "@/components/use-web-scrollbar";
|
||||
import { isWeb } from "@/constants/platform";
|
||||
|
||||
@@ -267,7 +270,11 @@ export function FileExplorerPane({
|
||||
workspaceRoot: normalizedWorkspaceRoot,
|
||||
});
|
||||
const sortOption = usePanelStore((state) => state.explorerSortOption);
|
||||
const showHiddenFiles = usePanelStore((state) => state.explorerShowHiddenFiles);
|
||||
const setSortOption = usePanelStore((state) => state.setExplorerSortOption);
|
||||
const toggleExplorerShowHiddenFiles = usePanelStore(
|
||||
(state) => state.toggleExplorerShowHiddenFiles,
|
||||
);
|
||||
const expandedPathsArray = usePanelStore((state) =>
|
||||
workspaceStateKey ? state.expandedPathsByWorkspace[workspaceStateKey] : undefined,
|
||||
);
|
||||
@@ -379,6 +386,14 @@ export function FileExplorerPane({
|
||||
setSortOption(SORT_OPTIONS[nextIndex].value);
|
||||
}, [sortOption, setSortOption]);
|
||||
|
||||
const handleToggleHiddenFiles = useCallback(() => {
|
||||
const willShow = !usePanelStore.getState().explorerShowHiddenFiles;
|
||||
toggleExplorerShowHiddenFiles();
|
||||
if (willShow) {
|
||||
requestPersistedExpandedPaths({ workspaceStateKey, requestDirectoryListing });
|
||||
}
|
||||
}, [requestDirectoryListing, toggleExplorerShowHiddenFiles, workspaceStateKey]);
|
||||
|
||||
const refreshExplorer = useCallback(
|
||||
() =>
|
||||
refreshExplorerDirectories({
|
||||
@@ -409,8 +424,8 @@ export function FileExplorerPane({
|
||||
const currentSortLabel = resolveCurrentSortLabel(sortOption, sortLabels);
|
||||
|
||||
const treeRows = useMemo(
|
||||
() => resolveTreeRows({ directories, expandedPaths, sortOption }),
|
||||
[directories, expandedPaths, sortOption],
|
||||
() => resolveTreeRows({ directories, expandedPaths, sortOption, showHiddenFiles }),
|
||||
[directories, expandedPaths, showHiddenFiles, sortOption],
|
||||
);
|
||||
|
||||
const showInitialLoading = resolveShowInitialLoading({
|
||||
@@ -483,6 +498,7 @@ export function FileExplorerPane({
|
||||
scrollbar={scrollbar}
|
||||
renderTreeRow={renderTreeRow}
|
||||
handleSortCycle={handleSortCycle}
|
||||
handleToggleHiddenFiles={handleToggleHiddenFiles}
|
||||
handleRefresh={handleRefresh}
|
||||
handleBackFromError={handleBackFromError}
|
||||
handleRetry={handleRetry}
|
||||
@@ -505,6 +521,7 @@ interface FileExplorerPaneContentProps {
|
||||
scrollbar: ReturnType<typeof useWebScrollViewScrollbar>;
|
||||
renderTreeRow: (info: ListRenderItemInfo<TreeRow>) => ReactElement;
|
||||
handleSortCycle: () => void;
|
||||
handleToggleHiddenFiles: () => void;
|
||||
handleRefresh: () => void;
|
||||
handleBackFromError: () => void;
|
||||
handleRetry: () => void;
|
||||
@@ -527,6 +544,7 @@ function FileExplorerPaneContent(props: FileExplorerPaneContentProps) {
|
||||
scrollbar,
|
||||
renderTreeRow,
|
||||
handleSortCycle,
|
||||
handleToggleHiddenFiles,
|
||||
handleRefresh,
|
||||
handleBackFromError,
|
||||
handleRetry,
|
||||
@@ -534,6 +552,26 @@ function FileExplorerPaneContent(props: FileExplorerPaneContentProps) {
|
||||
iconButtonStyle: iconButtonStyleProp,
|
||||
} = props;
|
||||
|
||||
const showHiddenFiles = usePanelStore((state) => state.explorerShowHiddenFiles);
|
||||
|
||||
const hiddenFilesToggleAccessibilityLabel = showHiddenFiles
|
||||
? t("workspace.fileExplorer.actions.hideHiddenFiles")
|
||||
: t("workspace.fileExplorer.actions.showHiddenFiles");
|
||||
const emptyLabel = showHiddenFiles
|
||||
? t("workspace.fileExplorer.empty.noFiles")
|
||||
: t("workspace.fileExplorer.empty.noVisibleFiles");
|
||||
const hiddenFilesToggleStyle = useCallback(
|
||||
(state: PressableStateCallbackType) => [
|
||||
iconButtonStyleProp(state),
|
||||
!showHiddenFiles && styles.iconButtonActive,
|
||||
],
|
||||
[showHiddenFiles, iconButtonStyleProp],
|
||||
);
|
||||
const hiddenFilesToggleAccessibilityState = useMemo(
|
||||
() => ({ selected: !showHiddenFiles }),
|
||||
[showHiddenFiles],
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<View style={styles.centerState}>
|
||||
@@ -561,14 +599,6 @@ function FileExplorerPaneContent(props: FileExplorerPaneContentProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (treeRows.length === 0) {
|
||||
return (
|
||||
<View style={styles.centerState}>
|
||||
<Text style={styles.emptyText}>{t("workspace.fileExplorer.empty.noFiles")}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={TREE_PANE_CONTAINER_STYLE}>
|
||||
<View style={styles.paneHeader} testID="files-pane-header">
|
||||
@@ -576,45 +606,67 @@ function FileExplorerPaneContent(props: FileExplorerPaneContentProps) {
|
||||
<Text style={styles.sortTriggerText}>{currentSortLabel}</Text>
|
||||
<ChevronDown size={12} color={theme.colors.foregroundMuted} />
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={handleRefresh}
|
||||
disabled={isRefreshFetching}
|
||||
hitSlop={8}
|
||||
style={iconButtonStyleProp}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={
|
||||
isRefreshFetching
|
||||
? t("workspace.fileExplorer.actions.refreshing")
|
||||
: t("workspace.fileExplorer.actions.refresh")
|
||||
}
|
||||
>
|
||||
<View style={styles.refreshIcon}>
|
||||
{isRefreshFetching ? (
|
||||
<LoadingSpinner size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||
<View style={styles.headerActions}>
|
||||
<Pressable
|
||||
onPress={handleToggleHiddenFiles}
|
||||
hitSlop={8}
|
||||
style={hiddenFilesToggleStyle}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={hiddenFilesToggleAccessibilityLabel}
|
||||
accessibilityState={hiddenFilesToggleAccessibilityState}
|
||||
>
|
||||
{showHiddenFiles ? (
|
||||
<Eye size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||
) : (
|
||||
<RotateCw size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||
<EyeOff size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||
)}
|
||||
</View>
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={handleRefresh}
|
||||
disabled={isRefreshFetching}
|
||||
hitSlop={8}
|
||||
style={iconButtonStyleProp}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={
|
||||
isRefreshFetching
|
||||
? t("workspace.fileExplorer.actions.refreshing")
|
||||
: t("workspace.fileExplorer.actions.refresh")
|
||||
}
|
||||
>
|
||||
<View style={styles.refreshIcon}>
|
||||
{isRefreshFetching ? (
|
||||
<LoadingSpinner size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||
) : (
|
||||
<RotateCw size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||
)}
|
||||
</View>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
<FlatList
|
||||
ref={treeListRef}
|
||||
style={styles.treeList}
|
||||
data={treeRows}
|
||||
renderItem={renderTreeRow}
|
||||
keyExtractor={treeRowKeyExtractor}
|
||||
testID="file-explorer-tree-scroll"
|
||||
contentContainerStyle={styles.entriesContent}
|
||||
onLayout={scrollbar.onLayout}
|
||||
onScroll={scrollbar.onScroll}
|
||||
onContentSizeChange={scrollbar.onContentSizeChange}
|
||||
scrollEventThrottle={16}
|
||||
showsVerticalScrollIndicator={!showDesktopWebScrollbar}
|
||||
initialNumToRender={24}
|
||||
maxToRenderPerBatch={40}
|
||||
windowSize={12}
|
||||
/>
|
||||
{scrollbar.overlay}
|
||||
{treeRows.length === 0 ? (
|
||||
<View style={styles.centerState}>
|
||||
<Text style={styles.emptyText}>{emptyLabel}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
ref={treeListRef}
|
||||
style={styles.treeList}
|
||||
data={treeRows}
|
||||
renderItem={renderTreeRow}
|
||||
keyExtractor={treeRowKeyExtractor}
|
||||
testID="file-explorer-tree-scroll"
|
||||
contentContainerStyle={styles.entriesContent}
|
||||
onLayout={scrollbar.onLayout}
|
||||
onScroll={scrollbar.onScroll}
|
||||
onContentSizeChange={scrollbar.onContentSizeChange}
|
||||
scrollEventThrottle={16}
|
||||
showsVerticalScrollIndicator={!showDesktopWebScrollbar}
|
||||
initialNumToRender={24}
|
||||
maxToRenderPerBatch={40}
|
||||
windowSize={12}
|
||||
/>
|
||||
)}
|
||||
{treeRows.length > 0 ? scrollbar.overlay : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -643,12 +695,14 @@ function buildTreeRows({
|
||||
directories,
|
||||
expandedPaths,
|
||||
sortOption,
|
||||
showHiddenFiles,
|
||||
path,
|
||||
depth,
|
||||
}: {
|
||||
directories: Map<string, { path: string; entries: ExplorerEntry[] }>;
|
||||
expandedPaths: Set<string>;
|
||||
sortOption: SortOption;
|
||||
showHiddenFiles: boolean;
|
||||
path: string;
|
||||
depth: number;
|
||||
}): TreeRow[] {
|
||||
@@ -658,7 +712,10 @@ function buildTreeRows({
|
||||
}
|
||||
|
||||
const rows: TreeRow[] = [];
|
||||
const entries = sortEntries(directory.entries, sortOption);
|
||||
const entries = sortEntries(
|
||||
filterVisibleExplorerEntries(directory.entries, showHiddenFiles),
|
||||
sortOption,
|
||||
);
|
||||
|
||||
for (const entry of entries) {
|
||||
rows.push({ entry, depth });
|
||||
@@ -668,6 +725,7 @@ function buildTreeRows({
|
||||
directories,
|
||||
expandedPaths,
|
||||
sortOption,
|
||||
showHiddenFiles,
|
||||
path: entry.path,
|
||||
depth: depth + 1,
|
||||
}),
|
||||
@@ -731,15 +789,24 @@ function resolveTreeRows({
|
||||
directories,
|
||||
expandedPaths,
|
||||
sortOption,
|
||||
showHiddenFiles,
|
||||
}: {
|
||||
directories: Map<string, { path: string; entries: ExplorerEntry[] }>;
|
||||
expandedPaths: Set<string>;
|
||||
sortOption: SortOption;
|
||||
showHiddenFiles: boolean;
|
||||
}): TreeRow[] {
|
||||
if (!directories.get(".")) {
|
||||
return [];
|
||||
}
|
||||
return buildTreeRows({ directories, expandedPaths, sortOption, path: ".", depth: 0 });
|
||||
return buildTreeRows({
|
||||
directories,
|
||||
expandedPaths,
|
||||
sortOption,
|
||||
showHiddenFiles,
|
||||
path: ".",
|
||||
depth: 0,
|
||||
});
|
||||
}
|
||||
|
||||
type StartDownloadFn = ReturnType<typeof useDownloadStore.getState>["startDownload"];
|
||||
@@ -890,12 +957,13 @@ function requestPersistedExpandedPaths({
|
||||
opts?: { recordHistory?: boolean; setCurrentPath?: boolean },
|
||||
) => Promise<boolean>;
|
||||
}): void {
|
||||
const showHiddenFiles = usePanelStore.getState().explorerShowHiddenFiles;
|
||||
const persistedPaths = usePanelStore.getState().expandedPathsByWorkspace[workspaceStateKey ?? ""];
|
||||
if (!persistedPaths) {
|
||||
return;
|
||||
}
|
||||
for (const path of persistedPaths) {
|
||||
if (path !== ".") {
|
||||
if (path !== "." && (showHiddenFiles || !isHiddenExplorerPath(path))) {
|
||||
void requestDirectoryListing(path, {
|
||||
recordHistory: false,
|
||||
setCurrentPath: false,
|
||||
@@ -919,7 +987,10 @@ async function refreshExplorerDirectories({
|
||||
if (!hasWorkspaceScope) {
|
||||
return null;
|
||||
}
|
||||
const directoryPaths = Array.from(expandedPaths);
|
||||
const showHiddenFiles = usePanelStore.getState().explorerShowHiddenFiles;
|
||||
const directoryPaths = Array.from(expandedPaths).filter(
|
||||
(path) => showHiddenFiles || !isHiddenExplorerPath(path),
|
||||
);
|
||||
if (!directoryPaths.includes(".")) {
|
||||
directoryPaths.unshift(".");
|
||||
}
|
||||
@@ -1011,6 +1082,11 @@ const styles = StyleSheet.create((theme) => ({
|
||||
fontSize: theme.fontSize.xs,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
headerActions: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[1],
|
||||
},
|
||||
treeList: {
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
@@ -1154,6 +1230,9 @@ const styles = StyleSheet.create((theme) => ({
|
||||
iconButtonHovered: {
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
iconButtonActive: {
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
refreshIcon: {
|
||||
width: 16,
|
||||
height: 16,
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ExplorerEntry } from "@/stores/session-store";
|
||||
import { filterVisibleExplorerEntries, isHiddenExplorerPath } from "./visibility";
|
||||
|
||||
function makeEntry(name: string, kind: ExplorerEntry["kind"]): ExplorerEntry {
|
||||
return {
|
||||
name,
|
||||
path: name,
|
||||
kind,
|
||||
size: 0,
|
||||
modifiedAt: "2026-01-01T00:00:00.000Z",
|
||||
};
|
||||
}
|
||||
|
||||
describe("file explorer visibility", () => {
|
||||
it("keeps dot-prefixed entries when hidden files are shown", () => {
|
||||
const entries = [makeEntry(".env", "file"), makeEntry("src", "directory")];
|
||||
|
||||
expect(filterVisibleExplorerEntries(entries, true)).toEqual(entries);
|
||||
});
|
||||
|
||||
it("hides dot-prefixed files and directories when hidden files are not shown", () => {
|
||||
const entries = [
|
||||
makeEntry(".env", "file"),
|
||||
makeEntry(".git", "directory"),
|
||||
makeEntry("README.md", "file"),
|
||||
makeEntry("src", "directory"),
|
||||
];
|
||||
|
||||
expect(filterVisibleExplorerEntries(entries, false).map((entry) => entry.name)).toEqual([
|
||||
"README.md",
|
||||
"src",
|
||||
]);
|
||||
});
|
||||
|
||||
it("detects paths nested under dot-prefixed directories", () => {
|
||||
expect(isHiddenExplorerPath(".")).toBe(false);
|
||||
expect(isHiddenExplorerPath("..")).toBe(false);
|
||||
expect(isHiddenExplorerPath("../sibling")).toBe(false);
|
||||
expect(isHiddenExplorerPath("src/components")).toBe(false);
|
||||
expect(isHiddenExplorerPath(".git")).toBe(true);
|
||||
expect(isHiddenExplorerPath("src/.cache/output.json")).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { ExplorerEntry } from "@/stores/session-store";
|
||||
|
||||
export function isHiddenExplorerPath(path: string): boolean {
|
||||
return path
|
||||
.split("/")
|
||||
.some((segment) => segment !== "." && segment !== ".." && segment.startsWith("."));
|
||||
}
|
||||
|
||||
export function filterVisibleExplorerEntries(
|
||||
entries: ExplorerEntry[],
|
||||
showHiddenFiles: boolean,
|
||||
): ExplorerEntry[] {
|
||||
if (showHiddenFiles) {
|
||||
return entries;
|
||||
}
|
||||
return entries.filter((entry) => !entry.name.startsWith("."));
|
||||
}
|
||||
@@ -224,7 +224,10 @@ describe("translation resources", () => {
|
||||
expect(en.importSession.status.connectHost).toBe("Connect to a host to import sessions");
|
||||
expect(en.importSession.actions.refresh).toBe("Refresh sessions");
|
||||
expect(en.workspace.fileExplorer.sort.name).toBe("Name");
|
||||
expect(en.workspace.fileExplorer.actions.hideHiddenFiles).toBe("Hide hidden files");
|
||||
expect(en.workspace.fileExplorer.actions.showHiddenFiles).toBe("Show hidden files");
|
||||
expect(en.workspace.fileExplorer.empty.noFiles).toBe("No files");
|
||||
expect(en.workspace.fileExplorer.empty.noVisibleFiles).toBe("No visible files");
|
||||
expect(en.workspace.setup.status.running).toBe("Running");
|
||||
expect(en.workspace.setup.empty.noCommands).toBe("No setup commands ran for this workspace.");
|
||||
expect(en.workspace.browser.unavailable.title).toBe("Browser is desktop-only");
|
||||
|
||||
@@ -355,9 +355,12 @@ export const ar: TranslationResources = {
|
||||
retry: "أعد المحاولة",
|
||||
refresh: "تحديث الملفات",
|
||||
refreshing: "تحديث الملفات",
|
||||
hideHiddenFiles: "إخفاء الملفات المخفية",
|
||||
showHiddenFiles: "إظهار الملفات المخفية",
|
||||
},
|
||||
empty: {
|
||||
noFiles: "لا توجد ملفات",
|
||||
noVisibleFiles: "لا توجد ملفات مرئية",
|
||||
},
|
||||
states: {
|
||||
unavailable: "Workspace غير متوفر",
|
||||
|
||||
@@ -354,9 +354,12 @@ export const en = {
|
||||
retry: "Retry",
|
||||
refresh: "Refresh files",
|
||||
refreshing: "Refreshing files",
|
||||
hideHiddenFiles: "Hide hidden files",
|
||||
showHiddenFiles: "Show hidden files",
|
||||
},
|
||||
empty: {
|
||||
noFiles: "No files",
|
||||
noVisibleFiles: "No visible files",
|
||||
},
|
||||
states: {
|
||||
unavailable: "Workspace is unavailable",
|
||||
|
||||
@@ -358,9 +358,12 @@ export const es: TranslationResources = {
|
||||
retry: "Rever",
|
||||
refresh: "Actualizar archivos",
|
||||
refreshing: "Actualizar archivos",
|
||||
hideHiddenFiles: "Ocultar archivos ocultos",
|
||||
showHiddenFiles: "Mostrar archivos ocultos",
|
||||
},
|
||||
empty: {
|
||||
noFiles: "Sin archivos",
|
||||
noVisibleFiles: "No hay archivos visibles",
|
||||
},
|
||||
states: {
|
||||
unavailable: "Workspaceno está disponible",
|
||||
|
||||
@@ -358,9 +358,12 @@ export const fr: TranslationResources = {
|
||||
retry: "Réessayer",
|
||||
refresh: "Actualiser les fichiers",
|
||||
refreshing: "Actualisation des fichiers",
|
||||
hideHiddenFiles: "Masquer les fichiers cachés",
|
||||
showHiddenFiles: "Afficher les fichiers cachés",
|
||||
},
|
||||
empty: {
|
||||
noFiles: "Aucun fichier",
|
||||
noVisibleFiles: "Aucun fichier visible",
|
||||
},
|
||||
states: {
|
||||
unavailable: "Workspacen'est pas disponible",
|
||||
|
||||
@@ -357,9 +357,12 @@ export const ru: TranslationResources = {
|
||||
retry: "Повторить попытку",
|
||||
refresh: "Обновить файлы",
|
||||
refreshing: "Обновление файлов",
|
||||
hideHiddenFiles: "Скрыть скрытые файлы",
|
||||
showHiddenFiles: "Показать скрытые файлы",
|
||||
},
|
||||
empty: {
|
||||
noFiles: "Нет файлов",
|
||||
noVisibleFiles: "Нет видимых файлов",
|
||||
},
|
||||
states: {
|
||||
unavailable: "Workspace недоступен",
|
||||
|
||||
@@ -355,9 +355,12 @@ export const zhCN: TranslationResources = {
|
||||
retry: "重试",
|
||||
refresh: "刷新文件",
|
||||
refreshing: "正在刷新文件",
|
||||
hideHiddenFiles: "隐藏隐藏文件",
|
||||
showHiddenFiles: "显示隐藏文件",
|
||||
},
|
||||
empty: {
|
||||
noFiles: "没有文件",
|
||||
noVisibleFiles: "没有可见文件",
|
||||
},
|
||||
states: {
|
||||
unavailable: "Workspace 不可用",
|
||||
|
||||
@@ -75,6 +75,7 @@ export interface PanelState {
|
||||
sidebarWidth: number;
|
||||
explorerWidth: number;
|
||||
explorerSortOption: SortOption;
|
||||
explorerShowHiddenFiles: boolean;
|
||||
explorerFilesSplitRatio: number;
|
||||
|
||||
// Actions
|
||||
@@ -101,6 +102,7 @@ export interface PanelState {
|
||||
setSidebarWidth: (width: number) => void;
|
||||
setExplorerWidth: (width: number) => void;
|
||||
setExplorerSortOption: (option: SortOption) => void;
|
||||
toggleExplorerShowHiddenFiles: () => void;
|
||||
setExplorerFilesSplitRatio: (ratio: number) => void;
|
||||
}
|
||||
|
||||
@@ -127,6 +129,7 @@ export const usePanelStore = create<PanelState>()(
|
||||
sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
|
||||
explorerWidth: DEFAULT_EXPLORER_SIDEBAR_WIDTH,
|
||||
explorerSortOption: "name",
|
||||
explorerShowHiddenFiles: true,
|
||||
explorerFilesSplitRatio: DEFAULT_EXPLORER_FILES_SPLIT_RATIO,
|
||||
|
||||
toggleFocusMode: () =>
|
||||
@@ -262,6 +265,8 @@ export const usePanelStore = create<PanelState>()(
|
||||
setSidebarWidth: (width) => set({ sidebarWidth: clampSidebarWidth(width) }),
|
||||
setExplorerWidth: (width) => set({ explorerWidth: clampExplorerWidth(width) }),
|
||||
setExplorerSortOption: (option) => set({ explorerSortOption: option }),
|
||||
toggleExplorerShowHiddenFiles: () =>
|
||||
set((state) => ({ explorerShowHiddenFiles: !state.explorerShowHiddenFiles })),
|
||||
setExplorerFilesSplitRatio: (ratio) =>
|
||||
set({
|
||||
explorerFilesSplitRatio: Number.isFinite(ratio)
|
||||
@@ -271,7 +276,7 @@ export const usePanelStore = create<PanelState>()(
|
||||
}),
|
||||
{
|
||||
name: "panel-state",
|
||||
version: 10,
|
||||
version: 11,
|
||||
storage: createJSONStorage(() => AsyncStorage),
|
||||
migrate: (persistedState, version) =>
|
||||
migratePanelState(persistedState, version, { isWeb }) as unknown as PanelState,
|
||||
@@ -285,6 +290,7 @@ export const usePanelStore = create<PanelState>()(
|
||||
sidebarWidth: state.sidebarWidth,
|
||||
explorerWidth: state.explorerWidth,
|
||||
explorerSortOption: state.explorerSortOption,
|
||||
explorerShowHiddenFiles: state.explorerShowHiddenFiles,
|
||||
explorerFilesSplitRatio: state.explorerFilesSplitRatio,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import {
|
||||
buildOpenFileExplorerPatch,
|
||||
buildToggleFileExplorerPatch,
|
||||
migratePanelState,
|
||||
selectIsAgentListOpen,
|
||||
selectIsFileExplorerOpen,
|
||||
selectPanelVisibility,
|
||||
@@ -96,6 +97,14 @@ describe("panel-store explorer tab resolution", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("panel-store migration", () => {
|
||||
it("defaults hidden-file visibility to showing hidden files", () => {
|
||||
const state = migratePanelState({}, 10, { isWeb: false });
|
||||
|
||||
expect(state.explorerShowHiddenFiles).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("panel-store visibility selectors", () => {
|
||||
it("uses mobileView for compact layout visibility", () => {
|
||||
const state = makePanelState({
|
||||
|
||||
@@ -245,6 +245,9 @@ export function migratePanelState(
|
||||
) {
|
||||
state.diffExpandedPathsByWorkspace = {};
|
||||
}
|
||||
if (typeof state.explorerShowHiddenFiles !== "boolean") {
|
||||
state.explorerShowHiddenFiles = true;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user