fix(app): make settings navigation deterministic
Settings detail screens must return to their logical parent regardless of the route used to enter them. Keep compact navigation in the header while preserving the desktop project-list affordance.
This commit is contained in:
@@ -45,12 +45,19 @@ import {
|
|||||||
unblockPaseoConfigWrites,
|
unblockPaseoConfigWrites,
|
||||||
} from "../support/helpers/project-settings";
|
} from "../support/helpers/project-settings";
|
||||||
import { gotoAppShell } from "../support/helpers/app";
|
import { gotoAppShell } from "../support/helpers/app";
|
||||||
|
import { openCompactSettings } from "../support/helpers/settings";
|
||||||
import {
|
import {
|
||||||
addProjectFlowInput,
|
addProjectFlowInput,
|
||||||
chooseAddProjectMethod,
|
chooseAddProjectMethod,
|
||||||
openAddProjectFlow,
|
openAddProjectFlow,
|
||||||
} from "../support/helpers/add-project-flow";
|
} from "../support/helpers/add-project-flow";
|
||||||
import { createTempGitRepo } from "../support/helpers/workspace";
|
import { createTempGitRepo } from "../support/helpers/workspace";
|
||||||
|
import {
|
||||||
|
buildOpenProjectRoute,
|
||||||
|
buildProjectsSettingsRoute,
|
||||||
|
buildSettingsRoute,
|
||||||
|
} from "@/utils/host-routes";
|
||||||
|
import { getServerId } from "../support/helpers/server-id";
|
||||||
|
|
||||||
const updatedSetup = ["npm install", "npm run build"];
|
const updatedSetup = ["npm install", "npm run build"];
|
||||||
|
|
||||||
@@ -321,6 +328,33 @@ test.describe("Projects settings", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe("Projects settings — compact navigation", () => {
|
||||||
|
test.use({ viewport: { width: 390, height: 844 } });
|
||||||
|
|
||||||
|
test("project Back returns through Projects and Settings without reopening the project", async ({
|
||||||
|
page,
|
||||||
|
editableProject,
|
||||||
|
}) => {
|
||||||
|
await gotoAppShell(page);
|
||||||
|
await openCompactSettings(page, buildOpenProjectRoute());
|
||||||
|
await page.getByRole("button", { name: "Projects", exact: true }).click();
|
||||||
|
await expect(page).toHaveURL(buildProjectsSettingsRoute(getServerId()));
|
||||||
|
|
||||||
|
await openProjectSettings(page, editableProject.name);
|
||||||
|
await expect(page.getByRole("button", { name: "Back", exact: true })).toHaveCount(1);
|
||||||
|
await expect(page.getByRole("button", { name: "Back to projects", exact: true })).toHaveCount(
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Back", exact: true }).click();
|
||||||
|
await expect(page).toHaveURL(buildProjectsSettingsRoute(getServerId()));
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Back", exact: true }).click();
|
||||||
|
await expect(page).toHaveURL(buildSettingsRoute());
|
||||||
|
await expect(page.getByTestId("settings-sidebar")).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test.describe("Projects settings — error UX", () => {
|
test.describe("Projects settings — error UX", () => {
|
||||||
test("stale-write callout appears on save, disables save, and reload clears it", async ({
|
test("stale-write callout appears on save, disables save, and reload clears it", async ({
|
||||||
page,
|
page,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { test, expect } from "../support/fixtures";
|
|||||||
import {
|
import {
|
||||||
buildHostWorkspaceRoute,
|
buildHostWorkspaceRoute,
|
||||||
buildOpenProjectRoute,
|
buildOpenProjectRoute,
|
||||||
|
buildSettingsHostSectionRoute,
|
||||||
buildSettingsRoute,
|
buildSettingsRoute,
|
||||||
buildSettingsSectionRoute,
|
buildSettingsSectionRoute,
|
||||||
} from "@/utils/host-routes";
|
} from "@/utils/host-routes";
|
||||||
@@ -181,6 +182,28 @@ test.describe("Settings — compact master-detail", () => {
|
|||||||
await expectSettingsSidebarVisible(page);
|
await expectSettingsSidebarVisible(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("host picker settings opens Overview and backs through the settings list", async ({
|
||||||
|
page,
|
||||||
|
withWorkspace,
|
||||||
|
}) => {
|
||||||
|
const workspace = await withWorkspace({ prefix: "host-picker-settings-back-" });
|
||||||
|
const workspaceRoute = buildHostWorkspaceRoute(getServerId(), workspace.workspaceId);
|
||||||
|
|
||||||
|
await openWorkspace(page, workspace);
|
||||||
|
await page.getByRole("button", { name: "Open menu", exact: true }).click();
|
||||||
|
await page.getByTestId("sidebar-hosts-trigger").click();
|
||||||
|
await page.getByRole("button", { name: /Open .* settings/ }).click();
|
||||||
|
|
||||||
|
await expectAppRoute(page, buildSettingsHostSectionRoute(getServerId(), "host"));
|
||||||
|
await expect(page.getByText("Overview", { exact: true })).toBeVisible();
|
||||||
|
|
||||||
|
await goBackInSettings(page);
|
||||||
|
await expectCompactSettingsList(page);
|
||||||
|
|
||||||
|
await goBackInSettings(page);
|
||||||
|
await expectAppRoute(page, workspaceRoute);
|
||||||
|
});
|
||||||
|
|
||||||
test("switching the host picker on the settings list scopes host rows without navigating", async ({
|
test("switching the host picker on the settings list scopes host rows without navigating", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -68,9 +68,9 @@ import {
|
|||||||
buildSchedulesRoute,
|
buildSchedulesRoute,
|
||||||
buildSessionsRoute,
|
buildSessionsRoute,
|
||||||
buildSettingsAddHostRoute,
|
buildSettingsAddHostRoute,
|
||||||
buildSettingsHostSectionRoute,
|
|
||||||
buildSettingsRoute,
|
buildSettingsRoute,
|
||||||
} from "@/utils/host-routes";
|
} from "@/utils/host-routes";
|
||||||
|
import { openHostOverview } from "@/navigation/settings-navigation";
|
||||||
import type { ShortcutKey } from "@/utils/format-shortcut";
|
import type { ShortcutKey } from "@/utils/format-shortcut";
|
||||||
import { SidebarAgentListSkeleton } from "./sidebar-agent-list-skeleton";
|
import { SidebarAgentListSkeleton } from "./sidebar-agent-list-skeleton";
|
||||||
import { SidebarCalloutSlot } from "./sidebar-callout-slot";
|
import { SidebarCalloutSlot } from "./sidebar-callout-slot";
|
||||||
@@ -198,13 +198,13 @@ export const LeftSidebar = memo(function LeftSidebar({ active }: { active: boole
|
|||||||
const handleOpenHostSettingsMobile = useCallback(
|
const handleOpenHostSettingsMobile = useCallback(
|
||||||
(serverId: string) => {
|
(serverId: string) => {
|
||||||
showMobileAgent();
|
showMobileAgent();
|
||||||
router.push(buildSettingsHostSectionRoute(serverId, "connections"));
|
openHostOverview(serverId);
|
||||||
},
|
},
|
||||||
[showMobileAgent],
|
[showMobileAgent],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOpenHostSettingsDesktop = useCallback((serverId: string) => {
|
const handleOpenHostSettingsDesktop = useCallback((serverId: string) => {
|
||||||
router.push(buildSettingsHostSectionRoute(serverId, "connections"));
|
openHostOverview(serverId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleHomeMobile = useCallback(() => {
|
const handleHomeMobile = useCallback(() => {
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { router, type Href } from "expo-router";
|
||||||
|
import { navigateToLastWorkspace } from "@/stores/navigation-active-workspace-store";
|
||||||
|
import {
|
||||||
|
buildOpenProjectRoute,
|
||||||
|
buildProjectSettingsRoute,
|
||||||
|
buildProjectsSettingsRoute,
|
||||||
|
buildSettingsHostSectionRoute,
|
||||||
|
buildSettingsRoute,
|
||||||
|
type HostSectionSlug,
|
||||||
|
type SettingsSectionSlug,
|
||||||
|
} from "@/utils/host-routes";
|
||||||
|
|
||||||
|
export type SettingsView =
|
||||||
|
| { kind: "root" }
|
||||||
|
| { kind: "section"; section: SettingsSectionSlug }
|
||||||
|
| { kind: "host"; serverId: string; section: HostSectionSlug }
|
||||||
|
| { kind: "project"; serverId: string; projectId: string };
|
||||||
|
|
||||||
|
export function openHostOverview(serverId: string): void {
|
||||||
|
router.push(buildSettingsHostSectionRoute(serverId, "host"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openProjectSettings(serverId: string, projectId: string): void {
|
||||||
|
router.push(buildProjectSettingsRoute(serverId, projectId));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function returnFromSettings(view: SettingsView): void {
|
||||||
|
if (view.kind === "root") {
|
||||||
|
if (!navigateToLastWorkspace()) {
|
||||||
|
router.replace(buildOpenProjectRoute());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent =
|
||||||
|
view.kind === "project" ? buildProjectsSettingsRoute(view.serverId) : buildSettingsRoute();
|
||||||
|
router.dismissTo(parent as Href);
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|||||||
import type { TFunction } from "i18next";
|
import type { TFunction } from "i18next";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Pressable, Text, TextInput, View } from "react-native";
|
import { Pressable, Text, TextInput, View } from "react-native";
|
||||||
import { router } from "expo-router";
|
|
||||||
import { useFocusEffect } from "@react-navigation/native";
|
import { useFocusEffect } from "@react-navigation/native";
|
||||||
import { StyleSheet } from "react-native-unistyles";
|
import { StyleSheet } from "react-native-unistyles";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
@@ -47,7 +46,6 @@ import {
|
|||||||
type ProjectConfigDraft,
|
type ProjectConfigDraft,
|
||||||
type ProjectScriptDraft,
|
type ProjectScriptDraft,
|
||||||
} from "@/utils/project-config-form";
|
} from "@/utils/project-config-form";
|
||||||
import { buildProjectsSettingsRoute } from "@/utils/host-routes";
|
|
||||||
import {
|
import {
|
||||||
getProjectHostEntry,
|
getProjectHostEntry,
|
||||||
getProjectSummaryForHostProject,
|
getProjectSummaryForHostProject,
|
||||||
@@ -94,9 +92,16 @@ type ReadProjectConfigData = Awaited<ReturnType<DaemonClient["readProjectConfig"
|
|||||||
export interface ProjectSettingsScreenProps {
|
export interface ProjectSettingsScreenProps {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
onBackToProjects: () => void;
|
||||||
|
showBackToProjects: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProjectSettingsScreen({ serverId, projectId }: ProjectSettingsScreenProps) {
|
export default function ProjectSettingsScreen({
|
||||||
|
serverId,
|
||||||
|
projectId,
|
||||||
|
onBackToProjects,
|
||||||
|
showBackToProjects,
|
||||||
|
}: ProjectSettingsScreenProps) {
|
||||||
const { projects } = useProjects();
|
const { projects } = useProjects();
|
||||||
const project = useMemo(
|
const project = useMemo(
|
||||||
() => getProjectSummaryForHostProject(projects, serverId, projectId),
|
() => getProjectSummaryForHostProject(projects, serverId, projectId),
|
||||||
@@ -116,7 +121,12 @@ export default function ProjectSettingsScreen({ serverId, projectId }: ProjectSe
|
|||||||
selectedHost.repoRoot.trim().length > 0;
|
selectedHost.repoRoot.trim().length > 0;
|
||||||
|
|
||||||
if (!project || !selectedHost || !client || !canEdit) {
|
if (!project || !selectedHost || !client || !canEdit) {
|
||||||
return <NoEditableTarget serverId={serverId} />;
|
return (
|
||||||
|
<NoEditableTarget
|
||||||
|
onBackToProjects={onBackToProjects}
|
||||||
|
showBackToProjects={showBackToProjects}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -125,41 +135,45 @@ export default function ProjectSettingsScreen({ serverId, projectId }: ProjectSe
|
|||||||
selectedHost={selectedHost}
|
selectedHost={selectedHost}
|
||||||
client={client}
|
client={client}
|
||||||
isHostGone={isHostGone}
|
isHostGone={isHostGone}
|
||||||
|
onBackToProjects={onBackToProjects}
|
||||||
|
showBackToProjects={showBackToProjects}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateBackToProjects(serverId: string) {
|
function NoEditableTarget({
|
||||||
router.navigate(buildProjectsSettingsRoute(serverId));
|
onBackToProjects,
|
||||||
}
|
showBackToProjects,
|
||||||
|
}: {
|
||||||
function NoEditableTarget({ serverId }: { serverId: string }) {
|
onBackToProjects: () => void;
|
||||||
|
showBackToProjects: boolean;
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const handleBack = useCallback(() => navigateBackToProjects(serverId), [serverId]);
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.noTargetContainer}>
|
<View style={styles.noTargetContainer}>
|
||||||
<BackToProjectsButton serverId={serverId} />
|
{showBackToProjects ? <BackToProjectsButton onPress={onBackToProjects} /> : null}
|
||||||
<Text style={styles.noTargetText}>{t("settings.project.noEditableTarget")}</Text>
|
<Text style={styles.noTargetText}>{t("settings.project.noEditableTarget")}</Text>
|
||||||
<Button
|
{showBackToProjects ? (
|
||||||
testID="project-settings-back-button"
|
<Button
|
||||||
onPress={handleBack}
|
testID="project-settings-back-button"
|
||||||
variant="secondary"
|
onPress={onBackToProjects}
|
||||||
size="md"
|
variant="secondary"
|
||||||
>
|
size="md"
|
||||||
{t("settings.project.backToProjects")}
|
>
|
||||||
</Button>
|
{t("settings.project.backToProjects")}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BackToProjectsButton({ serverId }: { serverId: string }) {
|
function BackToProjectsButton({ onPress }: { onPress: () => void }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const handleBack = useCallback(() => navigateBackToProjects(serverId), [serverId]);
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
testID="project-settings-back-link"
|
testID="project-settings-back-link"
|
||||||
accessibilityLabel={t("settings.project.backToProjects")}
|
accessibilityLabel={t("settings.project.backToProjects")}
|
||||||
onPress={handleBack}
|
onPress={onPress}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
leftIcon={ArrowLeft}
|
leftIcon={ArrowLeft}
|
||||||
@@ -175,6 +189,8 @@ interface ProjectSettingsBodyProps {
|
|||||||
selectedHost: ProjectHostEntry;
|
selectedHost: ProjectHostEntry;
|
||||||
client: DaemonClient;
|
client: DaemonClient;
|
||||||
isHostGone: boolean;
|
isHostGone: boolean;
|
||||||
|
onBackToProjects: () => void;
|
||||||
|
showBackToProjects: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProjectSettingsBody({
|
function ProjectSettingsBody({
|
||||||
@@ -182,6 +198,8 @@ function ProjectSettingsBody({
|
|||||||
selectedHost,
|
selectedHost,
|
||||||
client,
|
client,
|
||||||
isHostGone,
|
isHostGone,
|
||||||
|
onBackToProjects,
|
||||||
|
showBackToProjects,
|
||||||
}: ProjectSettingsBodyProps) {
|
}: ProjectSettingsBodyProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isEditSheetOpen, setIsEditSheetOpen] = useState(false);
|
const [isEditSheetOpen, setIsEditSheetOpen] = useState(false);
|
||||||
@@ -257,7 +275,7 @@ function ProjectSettingsBody({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View role="main" style={styles.body}>
|
<View role="main" style={styles.body}>
|
||||||
<BackToProjectsButton serverId={selectedHost.serverId} />
|
{showBackToProjects ? <BackToProjectsButton onPress={onBackToProjects} /> : null}
|
||||||
|
|
||||||
<View style={styles.headerBlock}>
|
<View style={styles.headerBlock}>
|
||||||
<View style={styles.titleRow}>
|
<View style={styles.titleRow}>
|
||||||
@@ -306,6 +324,8 @@ function ProjectSettingsBody({
|
|||||||
client,
|
client,
|
||||||
onReload: handleReload,
|
onReload: handleReload,
|
||||||
isHostGone,
|
isHostGone,
|
||||||
|
onBackToProjects,
|
||||||
|
showBackToProjects,
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -322,6 +342,8 @@ interface RenderContentInput {
|
|||||||
client: DaemonClient;
|
client: DaemonClient;
|
||||||
onReload: () => void;
|
onReload: () => void;
|
||||||
isHostGone: boolean;
|
isHostGone: boolean;
|
||||||
|
onBackToProjects: () => void;
|
||||||
|
showBackToProjects: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderContent({
|
function renderContent({
|
||||||
@@ -335,6 +357,8 @@ function renderContent({
|
|||||||
client,
|
client,
|
||||||
onReload,
|
onReload,
|
||||||
isHostGone,
|
isHostGone,
|
||||||
|
onBackToProjects,
|
||||||
|
showBackToProjects,
|
||||||
}: RenderContentInput) {
|
}: RenderContentInput) {
|
||||||
if (readQuery.isLoading) {
|
if (readQuery.isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -353,7 +377,12 @@ function renderContent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isHostGone) {
|
if (isHostGone) {
|
||||||
return <NoEditableTarget serverId={selectedHost.serverId} />;
|
return (
|
||||||
|
<NoEditableTarget
|
||||||
|
onBackToProjects={onBackToProjects}
|
||||||
|
showBackToProjects={showBackToProjects}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!loadedConfig) {
|
if (!loadedConfig) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
import type { ProjectHostEntry, ProjectSummary, WorkspaceSummary } from "@/utils/projects";
|
import type { ProjectHostEntry, ProjectSummary, WorkspaceSummary } from "@/utils/projects";
|
||||||
import type { ProjectHostError, UseProjectsResult } from "@/hooks/use-projects";
|
import type { ProjectHostError, UseProjectsResult } from "@/hooks/use-projects";
|
||||||
|
|
||||||
const { theme, projectsState, navigate } = vi.hoisted(() => ({
|
const { theme, projectsState, push } = vi.hoisted(() => ({
|
||||||
theme: {
|
theme: {
|
||||||
spacing: { 0: 0, 1: 4, "1.5": 6, 2: 8, 3: 12, 4: 16, 6: 24, 8: 32 },
|
spacing: { 0: 0, 1: 4, "1.5": 6, 2: 8, 3: 12, 4: 16, 6: 24, 8: 32 },
|
||||||
iconSize: { sm: 14, md: 20 },
|
iconSize: { sm: 14, md: 20 },
|
||||||
@@ -37,7 +37,7 @@ const { theme, projectsState, navigate } = vi.hoisted(() => ({
|
|||||||
refetch: vi.fn(),
|
refetch: vi.fn(),
|
||||||
} as UseProjectsResult,
|
} as UseProjectsResult,
|
||||||
},
|
},
|
||||||
navigate: vi.fn(),
|
push: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("react-native", () => {
|
vi.mock("react-native", () => {
|
||||||
@@ -125,7 +125,7 @@ vi.mock("lucide-react-native", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
vi.mock("expo-router", () => ({
|
vi.mock("expo-router", () => ({
|
||||||
router: { navigate },
|
router: { push },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("react-i18next", () => ({
|
vi.mock("react-i18next", () => ({
|
||||||
@@ -281,7 +281,7 @@ describe("ProjectsScreen", () => {
|
|||||||
document.body.appendChild(container);
|
document.body.appendChild(container);
|
||||||
root = createRoot(container);
|
root = createRoot(container);
|
||||||
setProjectsState({});
|
setProjectsState({});
|
||||||
navigate.mockReset();
|
push.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -334,8 +334,8 @@ describe("ProjectsScreen", () => {
|
|||||||
row.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
|
row.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(navigate).toHaveBeenCalledTimes(1);
|
expect(push).toHaveBeenCalledTimes(1);
|
||||||
expect(navigate).toHaveBeenCalledWith("/settings/hosts/host-a/projects/project-a");
|
expect(push).toHaveBeenCalledWith("/settings/hosts/host-a/projects/project-a");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not render a kebab menu on the row", () => {
|
it("does not render a kebab menu on the row", () => {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { Pressable, Text, View, type PressableStateCallbackType } from "react-native";
|
import { Pressable, Text, View, type PressableStateCallbackType } from "react-native";
|
||||||
import { router } from "expo-router";
|
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { ChevronRight } from "lucide-react-native";
|
import { ChevronRight } from "lucide-react-native";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -9,7 +8,7 @@ import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
|||||||
import { useProjects, type ProjectHostError } from "@/hooks/use-projects";
|
import { useProjects, type ProjectHostError } from "@/hooks/use-projects";
|
||||||
import { useProjectIcons } from "@/projects/icons";
|
import { useProjectIcons } from "@/projects/icons";
|
||||||
import { settingsStyles } from "@/styles/settings";
|
import { settingsStyles } from "@/styles/settings";
|
||||||
import { buildProjectSettingsRoute } from "@/utils/host-routes";
|
import { openProjectSettings } from "@/navigation/settings-navigation";
|
||||||
import type { ProjectHostEntry, ProjectSummary } from "@/utils/projects";
|
import type { ProjectHostEntry, ProjectSummary } from "@/utils/projects";
|
||||||
|
|
||||||
interface ProjectsScreenProps {
|
interface ProjectsScreenProps {
|
||||||
@@ -112,7 +111,7 @@ function ProjectRow({ project, host, isFirst, iconDataUri }: ProjectRowProps) {
|
|||||||
const { viewKey } = project;
|
const { viewKey } = project;
|
||||||
const { projectName } = host;
|
const { projectName } = host;
|
||||||
const handleNavigate = useCallback(() => {
|
const handleNavigate = useCallback(() => {
|
||||||
router.navigate(buildProjectSettingsRoute(host.serverId, host.projectId));
|
openProjectSettings(host.serverId, host.projectId);
|
||||||
}, [host.projectId, host.serverId]);
|
}, [host.projectId, host.serverId]);
|
||||||
|
|
||||||
const rowStyle = useCallback(
|
const rowStyle = useCallback(
|
||||||
|
|||||||
@@ -121,27 +121,18 @@ import {
|
|||||||
useEnableBuiltInDaemonOption,
|
useEnableBuiltInDaemonOption,
|
||||||
} from "@/desktop/hooks/use-enable-built-in-daemon-option";
|
} from "@/desktop/hooks/use-enable-built-in-daemon-option";
|
||||||
import {
|
import {
|
||||||
buildOpenProjectRoute,
|
|
||||||
buildSettingsHostSectionRoute,
|
buildSettingsHostSectionRoute,
|
||||||
buildSettingsSectionRoute,
|
buildSettingsSectionRoute,
|
||||||
type HostSectionSlug,
|
type HostSectionSlug,
|
||||||
type SettingsSectionSlug,
|
type SettingsSectionSlug,
|
||||||
} from "@/utils/host-routes";
|
} from "@/utils/host-routes";
|
||||||
import {
|
import { useLastWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
|
||||||
navigateToLastWorkspace,
|
import { returnFromSettings, type SettingsView } from "@/navigation/settings-navigation";
|
||||||
useLastWorkspaceSelection,
|
|
||||||
} from "@/stores/navigation-active-workspace-store";
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// View model
|
// View model
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export type SettingsView =
|
|
||||||
| { kind: "root" }
|
|
||||||
| { kind: "section"; section: SettingsSectionSlug }
|
|
||||||
| { kind: "host"; serverId: string; section: HostSectionSlug }
|
|
||||||
| { kind: "project"; serverId: string; projectId: string };
|
|
||||||
|
|
||||||
interface SidebarSectionItem {
|
interface SidebarSectionItem {
|
||||||
id: SettingsSectionSlug;
|
id: SettingsSectionSlug;
|
||||||
labelKey: string;
|
labelKey: string;
|
||||||
@@ -1367,29 +1358,13 @@ export default function SettingsScreen({ view, openAddHostIntent = null }: Setti
|
|||||||
}
|
}
|
||||||
}, [isCompactLayout, router]);
|
}, [isCompactLayout, router]);
|
||||||
|
|
||||||
const handleBackToRoot = useCallback(() => {
|
|
||||||
if (router.canGoBack()) {
|
|
||||||
router.back();
|
|
||||||
} else {
|
|
||||||
router.replace("/settings");
|
|
||||||
}
|
|
||||||
}, [router]);
|
|
||||||
|
|
||||||
const detailProjectServerId = view.kind === "project" ? view.serverId : null;
|
|
||||||
const handleBackFromDetail = useCallback(() => {
|
const handleBackFromDetail = useCallback(() => {
|
||||||
if (detailProjectServerId) {
|
returnFromSettings(view);
|
||||||
router.navigate(buildSettingsHostSectionRoute(detailProjectServerId, "projects"));
|
}, [view]);
|
||||||
return;
|
|
||||||
}
|
|
||||||
handleBackToRoot();
|
|
||||||
}, [detailProjectServerId, handleBackToRoot, router]);
|
|
||||||
|
|
||||||
const handleBackToWorkspace = useCallback(() => {
|
const handleBackToWorkspace = useCallback(() => {
|
||||||
if (navigateToLastWorkspace()) {
|
returnFromSettings({ kind: "root" });
|
||||||
return;
|
}, []);
|
||||||
}
|
|
||||||
router.replace(buildOpenProjectRoute());
|
|
||||||
}, [router]);
|
|
||||||
|
|
||||||
const detailHeader = ((): {
|
const detailHeader = ((): {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -1417,7 +1392,14 @@ export default function SettingsScreen({ view, openAddHostIntent = null }: Setti
|
|||||||
return renderHostSettingsContent(view, handleHostRemoved);
|
return renderHostSettingsContent(view, handleHostRemoved);
|
||||||
}
|
}
|
||||||
if (view.kind === "project") {
|
if (view.kind === "project") {
|
||||||
return <ProjectSettingsScreen serverId={view.serverId} projectId={view.projectId} />;
|
return (
|
||||||
|
<ProjectSettingsScreen
|
||||||
|
serverId={view.serverId}
|
||||||
|
projectId={view.projectId}
|
||||||
|
onBackToProjects={handleBackFromDetail}
|
||||||
|
showBackToProjects={!isCompactLayout}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (view.kind === "section") {
|
if (view.kind === "section") {
|
||||||
switch (view.section) {
|
switch (view.section) {
|
||||||
|
|||||||
Reference in New Issue
Block a user