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
13 changes: 13 additions & 0 deletions apps/web/src/components/settings/SettingsScopeSentence.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ export const SETTINGS_DEVICE_ONLY_PATHS: ReadonlySet<string> = new Set([
"/settings/connections",
]);

/** Pages saved per environment that ignore the project, so only the environment is a target. */
export const SETTINGS_ENVIRONMENT_ONLY_PATHS: ReadonlySet<string> = new Set([
"/settings/providers",
]);

interface SettingsScopeMenuProps {
readonly value: SettingsScopeSearch;
readonly groups: readonly SidebarProjectSnapshot[];
Expand All @@ -62,6 +67,14 @@ export function SettingsScopeSentence() {
environments,
onChange: scope.selectScope,
};
if (SETTINGS_ENVIRONMENT_ONLY_PATHS.has(pathname)) {
return (
<p className="flex min-w-0 items-center gap-1.5 px-3 text-base text-muted-foreground sm:px-4">
<span className="shrink-0">Applying settings on</span>
<EnvironmentScopeMenu {...props} />
</p>
);
}
Comment thread
yordis marked this conversation as resolved.
return (
<p className="flex min-w-0 flex-wrap items-center gap-x-1.5 gap-y-1 px-3 text-base text-muted-foreground sm:px-4">
{/* Each connective stays with its picker so a wrap never strands "on". */}
Expand Down
33 changes: 33 additions & 0 deletions apps/web/src/components/settings/settingsScope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
SidebarProjectSnapshot,
} from "../../sidebarProjectGrouping";
import { resolveSettingsScope, validateSettingsScopeSearch } from "./settingsScope";
import { selectSingleEnvironmentScope } from "./settingsScopeAxis";

const laptopId = EnvironmentId.make("laptop");
const serverId = EnvironmentId.make("server");
Expand Down Expand Up @@ -203,3 +204,35 @@ describe("settings scope resolution", () => {
});
});
});

describe("single-environment scope", () => {
const connected = environments.map((environment) => ({
...environment,
connection: { phase: "connected" as const },
}));
const select = (search: Parameters<typeof resolveSettingsScope>[0]) =>
selectSingleEnvironmentScope(
search,
resolveSettingsScope(search, groups, environments),
connected,
laptopId,
);

it("targets the checkout's environment for a link without a machine", () => {
expect(select({ project: "t3code", checkout: third.physicalProjectKey })).toEqual({
machine: serverId,
});
});

it("narrows to where the project is registered", () => {
expect(select({ project: "other" })).toEqual({ machine: serverId });
});

it("falls back to the primary environment when the project is gone", () => {
expect(select({ project: "missing" })).toEqual({ machine: laptopId });
});

it("keeps an explicit machine and drops the project", () => {
expect(select({ project: "other", machine: laptopId })).toEqual({ machine: laptopId });
});
});
18 changes: 11 additions & 7 deletions apps/web/src/components/settings/settingsScopeAxis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ export function selectProjectAxis(search: SettingsScopeSearch, value: string): S
return next;
}

/** Provider configuration always belongs to one environment, including project scopes. */
/**
* Provider configuration always belongs to one environment. A project or
* checkout only picks which one when no machine is chosen and never reaches
* the result, so a stale project cannot leave the page without a target.
*/
export function selectSingleEnvironmentScope(
search: SettingsScopeSearch,
scope: ResolvedSettingsScope,
Expand All @@ -64,14 +68,14 @@ export function selectSingleEnvironmentScope(
}[],
primaryEnvironmentId: string | null,
): SettingsScopeSearch {
if (search.machine || scope.kind === "unavailable") return search;
const selectedIds = new Set(scope.environmentIds);
const candidates = environments.filter((environment) =>
selectedIds.has(environment.environmentId),
);
if (search.machine) return { machine: search.machine };
const selectedIds = scope.kind === "unavailable" ? null : new Set(scope.environmentIds);
const candidates = selectedIds
? environments.filter((environment) => selectedIds.has(environment.environmentId))
: environments;
const selected =
candidates.find((environment) => environment.environmentId === primaryEnvironmentId) ??
candidates.find((environment) => environment.connection.phase === "connected") ??
candidates[0];
return selected ? { ...search, machine: selected.environmentId } : search;
return selected ? { machine: selected.environmentId } : {};
}
14 changes: 10 additions & 4 deletions apps/web/src/routes/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import {
} from "../components/settings/SettingsScopeContext";
import { useEnvironments } from "../state/environments";
import { SettingsScopeNotice } from "../components/settings/SettingsScopeNotice";
import { SETTINGS_DEVICE_ONLY_PATHS } from "../components/settings/SettingsScopeSentence";
import {
SETTINGS_DEVICE_ONLY_PATHS,
SETTINGS_ENVIRONMENT_ONLY_PATHS,
} from "../components/settings/SettingsScopeSentence";
import { SettingsPageContainer } from "../components/settings/settingsLayout";
import {
retainSettingsScope,
Expand Down Expand Up @@ -151,19 +154,22 @@ function SettingsRouteLayout() {
const rawSearch = Route.useSearch();
const navigate = Route.useNavigate();
const pathname = useLocation({ select: (location) => location.pathname });
const environmentOnly = SETTINGS_ENVIRONMENT_ONLY_PATHS.has(pathname);
return (
<SettingsScopeProvider
search={rawSearch}
singleEnvironment={pathname === "/settings/providers"}
singleEnvironment={environmentOnly}
onChange={(next) => {
// Send every axis so the retain middleware sees an explicit target
// even when the choice is "all", which is the absence of a key.
// Environment-only pages never see the project, so carry it over for
// the next page; a checkout belongs to the previous environment.
void navigate({
to: pathname,
search: () => ({
project: next.project,
project: environmentOnly ? rawSearch.project : next.project,
machine: next.machine,
checkout: next.checkout,
checkout: environmentOnly ? undefined : next.checkout,
}),
hash: "",
resetScroll: false,
Expand Down