Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 23 additions & 45 deletions apps/mobile/src/features/projects/AddProjectScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,14 @@ import {
sortAddProjectProviderSources,
type AddProjectRemoteSource,
} from "@t3tools/client-runtime/operations/projects";
import {
filterFilesystemBrowseEntries,
getFilesystemBrowsePath,
} from "@t3tools/client-runtime/state/filesystem";
import {
appendBrowsePathSegment,
canNavigateUp,
ensureBrowseDirectoryPath,
getBrowseDirectoryPath,
getBrowseLeafPathSegment,
getBrowseParentPath,
hasTrailingPathSeparator,
inferProjectTitleFromPath,
isFilesystemBrowseQuery,
} from "@t3tools/client-runtime/state/projects";
import { CommandId, type EnvironmentId, ProjectId } from "@t3tools/contracts";
import { StackActions, useNavigation } from "@react-navigation/native";
Expand Down Expand Up @@ -61,11 +59,6 @@ const environmentOptionOrder = Order.mapInput(
(environment: EnvironmentOption) => ({ label: environment.label }),
);

const browseEntryOrder = Order.mapInput(
Order.String,
(entry: { readonly name: string }) => entry.name,
);

function platformFromOs(os: string | null | undefined): string {
if (os === "windows") return "Win32";
if (os === "darwin") return "MacIntel";
Expand Down Expand Up @@ -594,20 +587,13 @@ function FolderBrowser(props: {
readonly setPathInput: (path: string) => void;
}) {
const accentColor = useThemeColor("--color-icon-muted");
const browseDirectoryPath = useMemo(
() =>
isFilesystemBrowseQuery(props.pathInput, props.environment.platform)
? getBrowseDirectoryPath(props.pathInput)
: "",
const browsePath = useMemo(
() => getFilesystemBrowsePath(props.pathInput, props.environment.platform),
[props.environment.platform, props.pathInput],
);
const browseFilterQuery =
browseDirectoryPath.length > 0 && !hasTrailingPathSeparator(props.pathInput)
? getBrowseLeafPathSegment(props.pathInput).toLowerCase()
: "";
const browseInput = useMemo(
() => (browseDirectoryPath.length > 0 ? { partialPath: browseDirectoryPath } : null),
[browseDirectoryPath],
() => (browsePath.directoryPath.length > 0 ? { partialPath: browsePath.directoryPath } : null),
[browsePath.directoryPath],
);
const browseState = useEnvironmentQuery(
browseInput === null
Expand All @@ -617,20 +603,10 @@ function FolderBrowser(props: {
input: browseInput,
}),
);
const visibleBrowseEntries = useMemo(
() =>
Arr.sort(
Arr.filter(
browseState.data?.entries ?? [],
(entry) =>
!entry.name.startsWith(".") && entry.name.toLowerCase().startsWith(browseFilterQuery),
),
browseEntryOrder,
),
[browseFilterQuery, browseState.data?.entries],
const { visibleEntries: visibleBrowseEntries } = useMemo(
() => filterFilesystemBrowseEntries(browseState.data?.entries ?? [], browsePath.filterQuery),
[browsePath.filterQuery, browseState.data?.entries],
);
const parentBrowsePath = getBrowseParentPath(browseDirectoryPath);
const canBrowseUpPath = canNavigateUp(browseDirectoryPath);

return (
<>
Expand All @@ -642,7 +618,7 @@ function FolderBrowser(props: {
<ActivityIndicator color={accentColor} />
</View>
) : null}
{canBrowseUpPath ? (
{browsePath.canBrowseUp ? (
<ListRow
title=".."
icon={
Expand All @@ -656,7 +632,9 @@ function FolderBrowser(props: {
isFirst
right={null}
onPress={() => {
if (parentBrowsePath) props.setPathInput(parentBrowsePath);
if (browsePath.parentPath) {
props.setPathInput(browsePath.parentPath);
}
}}
/>
) : null}
Expand All @@ -665,15 +643,15 @@ function FolderBrowser(props: {
key={entry.fullPath}
title={entry.name}
icon={<SymbolView name="folder" size={17} tintColor={accentColor} type="monochrome" />}
isFirst={index === 0 && !canBrowseUpPath}
isFirst={index === 0 && !browsePath.canBrowseUp}
right={null}
onPress={() =>
props.setPathInput(
browseDirectoryPath.length > 0
? appendBrowsePathSegment(browseDirectoryPath, entry.name)
: ensureBrowseDirectoryPath(entry.fullPath),
)
}
onPress={() => {
const nextPath =
browsePath.directoryPath.length > 0
? appendBrowsePathSegment(browsePath.directoryPath, entry.name)
: ensureBrowseDirectoryPath(entry.fullPath);
props.setPathInput(nextPath);
}}
/>
))}
</ListSection>
Expand Down
64 changes: 0 additions & 64 deletions apps/web/src/components/CommandPalette.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import type { Thread } from "../types";
import {
buildBrowseGroups,
buildThreadActionItems,
canPreloadBrowsePath,
createBrowseNavigationCoordinator,
enumerateCommandPaletteItems,
filterCommandPaletteGroups,
type CommandPaletteGroup,
Expand Down Expand Up @@ -234,65 +232,3 @@ describe("buildBrowseGroups", () => {
expect(actionSettled).toBe(true);
});
});

describe("createBrowseNavigationCoordinator", () => {
it("only commits the latest overlapping navigation", async () => {
const coordinator = createBrowseNavigationCoordinator();
let finishFirst: (() => void) | undefined;
let finishSecond: (() => void) | undefined;
const commits: string[] = [];

const first = coordinator.run({
load: () =>
new Promise<void>((resolve) => {
finishFirst = resolve;
}),
commit: () => {
commits.push("first");
},
});
const second = coordinator.run({
load: () =>
new Promise<void>((resolve) => {
finishSecond = resolve;
}),
commit: () => {
commits.push("second");
},
});

finishSecond?.();
await expect(second).resolves.toBe(true);
finishFirst?.();
await expect(first).resolves.toBe(false);
expect(commits).toEqual(["second"]);
});

it("does not commit after newer user input invalidates the navigation", async () => {
const coordinator = createBrowseNavigationCoordinator();
let finishNavigation: (() => void) | undefined;
const commit = vi.fn();
const navigation = coordinator.run({
load: () =>
new Promise<void>((resolve) => {
finishNavigation = resolve;
}),
commit,
});

coordinator.invalidate();
finishNavigation?.();

await expect(navigation).resolves.toBe(false);
expect(commit).not.toHaveBeenCalled();
});
});

describe("canPreloadBrowsePath", () => {
it("only preloads paths for connected environments", () => {
expect(canPreloadBrowsePath("connected")).toBe(true);
expect(canPreloadBrowsePath("offline")).toBe(false);
expect(canPreloadBrowsePath("reconnecting")).toBe(false);
expect(canPreloadBrowsePath(null)).toBe(false);
});
});
60 changes: 1 addition & 59 deletions apps/web/src/components/CommandPalette.logic.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import {
type KeybindingCommand,
type FilesystemBrowseEntry,
type KeybindingCommand,
THREAD_JUMP_KEYBINDING_COMMANDS,
} from "@t3tools/contracts";
import type { EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection";
import type { SidebarThreadSortOrder } from "@t3tools/contracts/settings";
import * as Arr from "effect/Array";
import * as Result from "effect/Result";
Expand All @@ -16,39 +15,6 @@ export const RECENT_THREAD_LIMIT = 12;
export const ITEM_ICON_CLASS = "size-4 text-muted-foreground/80";
export const ADDON_ICON_CLASS = "size-4";

export interface BrowseNavigationCoordinator {
readonly invalidate: () => void;
readonly run: (input: {
readonly load: () => Promise<void>;
readonly commit: () => void;
}) => Promise<boolean>;
}

export function createBrowseNavigationCoordinator(): BrowseNavigationCoordinator {
let generation = 0;

return {
invalidate: () => {
generation += 1;
},
run: async (input) => {
const navigationGeneration = ++generation;
await input.load();
if (navigationGeneration !== generation) {
return false;
}
input.commit();
return true;
},
};
}

export function canPreloadBrowsePath(
connectionPhase: EnvironmentConnectionPhase | null | undefined,
): boolean {
return connectionPhase === "connected";
}

export interface CommandPaletteItem {
readonly kind: "action" | "submenu";
readonly value: string;
Expand Down Expand Up @@ -104,30 +70,6 @@ export function enumerateCommandPaletteItems(

export type CommandPaletteMode = "root" | "root-browse" | "submenu" | "submenu-browse";

export function filterBrowseEntries(input: {
browseEntries: ReadonlyArray<FilesystemBrowseEntry>;
browseFilterQuery: string;
}): {
filteredEntries: FilesystemBrowseEntry[];
exactEntry: FilesystemBrowseEntry | null;
} {
const lowerFilter = input.browseFilterQuery.toLowerCase();
const showHidden = input.browseFilterQuery.startsWith(".");

const filteredEntries = input.browseEntries.filter(
(entry) =>
entry.name.toLowerCase().startsWith(lowerFilter) &&
(showHidden || !entry.name.startsWith(".")),
);

const exactEntry =
input.browseFilterQuery.length > 0
? (filteredEntries.find((entry) => entry.name === input.browseFilterQuery) ?? null)
: null;

return { filteredEntries, exactEntry };
}

export function normalizeSearchText(value: string): string {
return value.trim().toLowerCase().replace(/\s+/g, " ");
}
Expand Down
Loading
Loading