refactor(ui): improve path normalization in FilesView

This commit is contained in:
Bohdan Triapitsyn
2026-02-09 11:50:21 +02:00
parent fbf0ec50ee
commit 75c548f2d5
2 changed files with 242 additions and 29 deletions
+119 -1
View File
@@ -27,7 +27,114 @@ type FilesViewTabsActions = {
export type FilesViewTabsStore = FilesViewTabsState & FilesViewTabsActions;
const normalizePath = (value: string): string => value.replace(/\\/g, '/');
const normalizePath = (value: string): string => {
if (!value) return '';
const raw = value.replace(/\\/g, '/');
const hadUncPrefix = raw.startsWith('//');
let normalized = raw.replace(/\/+/g, '/');
if (hadUncPrefix && !normalized.startsWith('//')) {
normalized = `/${normalized}`;
}
const isUnixRoot = normalized === '/';
const isWindowsDriveRoot = /^[A-Za-z]:\/$/.test(normalized);
if (!isUnixRoot && !isWindowsDriveRoot) {
normalized = normalized.replace(/\/+$/, '');
}
return normalized;
};
const toComparablePath = (value: string): string => {
if (/^[A-Za-z]:\//.test(value)) {
return value.toLowerCase();
}
return value;
};
const isPathWithinRoot = (path: string, root: string): boolean => {
const normalizedRoot = normalizePath(root);
const normalizedPath = normalizePath(path);
if (!normalizedRoot || !normalizedPath) return false;
const comparableRoot = toComparablePath(normalizedRoot);
const comparablePath = toComparablePath(normalizedPath);
return comparablePath === comparableRoot || comparablePath.startsWith(`${comparableRoot}/`);
};
const sanitizeByRoot = (input: unknown): Record<string, RootTabsState> => {
if (!input || typeof input !== 'object') {
return {};
}
const source = input as Record<string, unknown>;
const next: Record<string, RootTabsState> = {};
for (const [rawRoot, rawState] of Object.entries(source)) {
const root = normalizePath(rawRoot);
if (!root || !rawState || typeof rawState !== 'object') {
continue;
}
const state = rawState as {
openPaths?: unknown;
selectedPath?: unknown;
expandedPaths?: unknown;
touchedAt?: unknown;
};
const openPaths = Array.isArray(state.openPaths)
? Array.from(new Set(state.openPaths
.filter((value): value is string => typeof value === 'string')
.map((value) => normalizePath(value))
.filter((value) => isPathWithinRoot(value, root))))
: [];
const expandedPaths = Array.isArray(state.expandedPaths)
? Array.from(new Set(state.expandedPaths
.filter((value): value is string => typeof value === 'string')
.map((value) => normalizePath(value))
.filter((value) => isPathWithinRoot(value, root))))
: [];
const selectedPathCandidate = typeof state.selectedPath === 'string'
? normalizePath(state.selectedPath)
: null;
const selectedPath = selectedPathCandidate && isPathWithinRoot(selectedPathCandidate, root)
? selectedPathCandidate
: (openPaths[0] ?? null);
const touchedAt = typeof state.touchedAt === 'number' && Number.isFinite(state.touchedAt)
? state.touchedAt
: Date.now();
const existing = next[root];
if (existing) {
const mergedOpenPaths = Array.from(new Set([...existing.openPaths, ...openPaths]));
const mergedExpandedPaths = Array.from(new Set([...existing.expandedPaths, ...expandedPaths]));
const mergedSelectedPath = existing.selectedPath ?? selectedPath ?? (mergedOpenPaths[0] ?? null);
next[root] = {
openPaths: mergedOpenPaths,
selectedPath: mergedSelectedPath,
expandedPaths: mergedExpandedPaths,
touchedAt: Math.max(existing.touchedAt, touchedAt),
};
continue;
}
next[root] = {
openPaths,
selectedPath,
expandedPaths,
touchedAt,
};
}
return next;
};
const clampRoots = (byRoot: Record<string, RootTabsState>, maxRoots: number): Record<string, RootTabsState> => {
const entries = Object.entries(byRoot);
@@ -292,7 +399,18 @@ export const useFilesViewTabsStore = create<FilesViewTabsStore>()(
}),
{
name: 'files-view-tabs-store',
version: 2,
storage: createJSONStorage(() => getSafeStorage()),
migrate: (persistedState) => {
if (!persistedState || typeof persistedState !== 'object') {
return { byRoot: {} };
}
const rawByRoot = (persistedState as { byRoot?: unknown }).byRoot;
return {
byRoot: sanitizeByRoot(rawByRoot),
};
},
partialize: (state) => ({ byRoot: state.byRoot }),
}
),