Open History to see the last commit
1import { type EnvironmentId, T3_PROJECT_FILE_NAME, type T3ProjectFile } from "@t3tools/contracts";
2import { parseT3ProjectFile } from "@t3tools/shared/t3ProjectFile";
3import { useAtomValue } from "@effect/atom-react";
4import { AsyncResult, Atom } from "effect/reactivity";
5import { createContext, type ReactNode, useContext } from "react";
6
7import { useEnvironments, usePrimaryEnvironmentId } from "../../state/environments";
8import { getProjectFileQueryAtom, optimisticFileAtom } from "../files/projectFilesQueryState";
9import { useSettingsProjectGroups } from "./useSettingsProjectGroups";
10import {
11 applyPendingScopedWrites,
12 pendingScopedWritesAtom,
13 resolveScopedSettingsTargets,
14 selectScopedSettingsEnvironments,
15} from "./scopedSettings";
16import { resolveSettingsScope, type SettingsScopeSearch } from "./settingsScope";
17import { selectSingleEnvironmentScope } from "./settingsScopeAxis";
18
19type ProjectFileMember = readonly [
20 environmentId: EnvironmentId,
21 workspaceRoot: string,
22 physicalProjectKey: string,
23];
24
25const memberProjectFilesAtom = Atom.family((membersKey: string) => {
26 const members = JSON.parse(membersKey) as ReadonlyArray<ProjectFileMember>;
27 return Atom.make((get) => {
28 const files = new Map<string, T3ProjectFile | null>();
29 for (const [environmentId, workspaceRoot, physicalProjectKey] of members) {
30 const result = get(
31 getProjectFileQueryAtom(environmentId, workspaceRoot, T3_PROJECT_FILE_NAME),
32 );
33 if (result.waiting) continue;
34 const data =
35 get(optimisticFileAtom(environmentId, workspaceRoot, T3_PROJECT_FILE_NAME))?.data ??
36 AsyncResult.getOrElse(result, () => null);
37 files.set(
38 physicalProjectKey,
39 data === null || data.exists === false || data.truncated
40 ? null
41 : parseT3ProjectFile(data.contents),
42 );
43 }
44 return files;
45 }).pipe(Atom.withLabel(`web-settings-member-project-files:${membersKey}`));
46});
47
48function useMemberProjectFiles(scope: ReturnType<typeof resolveSettingsScope>) {
49 const members = scope.kind === "project" || scope.kind === "checkout" ? scope.members : [];
50 return useAtomValue(
51 memberProjectFilesAtom(
52 JSON.stringify(
53 members.map((member): ProjectFileMember => [
54 member.environmentId,
55 member.workspaceRoot,
56 member.physicalProjectKey,
57 ]),
58 ),
59 ),
60 );
61}
62
63function useResolvedSettingsScope(rawSearch: SettingsScopeSearch, singleEnvironment: boolean) {
64 const groups = useSettingsProjectGroups();
65 const { environments: availableEnvironments } = useEnvironments();
66 const primaryEnvironmentId = usePrimaryEnvironmentId();
67 const search = singleEnvironment
68 ? selectSingleEnvironmentScope(
69 rawSearch,
70 resolveSettingsScope(rawSearch, groups, availableEnvironments),
71 availableEnvironments,
72 primaryEnvironmentId,
73 )
74 : rawSearch;
75 const scope = resolveSettingsScope(search, groups, availableEnvironments);
76 const projectFiles = useMemberProjectFiles(scope);
77 const pendingWrites = useAtomValue(pendingScopedWritesAtom);
78 const selected = selectScopedSettingsEnvironments(
79 scope,
80 availableEnvironments,
81 primaryEnvironmentId,
82 );
83 const targets = resolveScopedSettingsTargets(
84 scope,
85 applyPendingScopedWrites(selected.connectedEnvironments, pendingWrites),
86 projectFiles,
87 );
88 const target =
89 targets.find((candidate) => candidate.environmentId === selected.environment?.environmentId) ??
90 targets[0] ??
91 null;
92 return { scope, groups, search, ...selected, targets, target };
93}
94
95const SettingsScopeContext = createContext<
96 | (ReturnType<typeof useResolvedSettingsScope> & {
97 singleEnvironment: boolean;
98 deviceOnly: boolean;
99 search: SettingsScopeSearch;
100 selectScope: (next: SettingsScopeSearch) => void;
101 })
102 | null
103>(null);
104
105export function SettingsScopeProvider({
106 search,
107 onChange,
108 children,
109 singleEnvironment = false,
110 deviceOnly = false,
111}: {
112 singleEnvironment?: boolean;
113 /** The page hides the scope picker, so its client settings save for this device. */
114 deviceOnly?: boolean;
115 search: SettingsScopeSearch;
116 onChange: (next: SettingsScopeSearch) => void;
117 children: ReactNode;
118}) {
119 const resolved = useResolvedSettingsScope(search, singleEnvironment);
120 const value = { ...resolved, singleEnvironment, deviceOnly, selectScope: onChange };
121 return <SettingsScopeContext value={value}>{children}</SettingsScopeContext>;
122}
123
124export function useOptionalSettingsScope() {
125 return useContext(SettingsScopeContext);
126}
127
128export function useSettingsScope() {
129 const scope = useOptionalSettingsScope();
130 if (scope === null) throw new Error("Settings scope must be read inside SettingsScopeProvider.");
131 return scope;
132}