Skip to content
Source
Sign inSign up
Open History to see the last commit

useScopedSettings.ts

136 lines4.8 KB

1import { AuthSettingsWriteScope } from "@t3tools/contracts";
2import { useEnvironmentsWithScope } from "../../state/session";
3import {
4 requiredScopesForServerSettingsPatch,
5 EnvironmentAuthorizationError,
6} from "@t3tools/contracts";
7import { readEnvironmentScope } from "../../state/session";
8import * as Cause from "effect/Cause";
9import { AsyncResult } from "effect/reactivity";
10import {
11 DEFAULT_SERVER_SETTINGS,
12 type ProjectScopedServerSettingKey,
13 type ServerSettings,
14 type UnifiedSettings,
15} from "@t3tools/contracts";
16​
17import { persistClientSettingsPatch, useMergedSettings } from "../../hooks/useSettings";
18import { serverEnvironment } from "../../state/server";
19import { useAtomCommand } from "../../state/use-atom-command";
20import { toastManager } from "../ui/toast";
21import { useOptionalSettingsScope, useSettingsScope } from "./SettingsScopeContext";
22import {
23 persistScopedSettingsPatch,
24 planProjectOverridesClear,
25 planScopedSettingsClear,
26 planScopedSettingsPatch,
27 scopedSettingsAreMixed,
28 scopedSettingsSource,
29 type ProjectOverrideEntry,
30 type ScopedSettingsPatch,
31} from "./scopedSettings";
32​
33export function useScopedSettings<T = UnifiedSettings>(
34 selector?: (settings: UnifiedSettings) => T,
35): T {
36 const { target } = useSettingsScope();
37 return useMergedSettings(target?.settings ?? DEFAULT_SERVER_SETTINGS, selector);
39​
40export function useScopedSettingsMixed(keys: readonly (keyof ServerSettings)[]): boolean {
41 const { targets } = useSettingsScope();
42 return scopedSettingsAreMixed(targets, keys);
44​
45export function useScopedSettingSource(keys: readonly (keyof ServerSettings)[]) {
46 const { targets } = useSettingsScope();
47 return scopedSettingsSource(targets, keys);
49​
50function useRunScopedPlan() {
51 const persistServer = useAtomCommand(serverEnvironment.updateSettings, { reportFailure: false });
52 return (plan: ReturnType<typeof planScopedSettingsPatch>, onFailed?: () => void) => {
53 if (plan.unavailableReason) {
54 toastManager.add({
55 type: "warning",
56 title: "Setting not saved",
57 description: plan.unavailableReason,
58 });
59 onFailed?.();
60 return;
61 }
62 void persistScopedSettingsPatch(
63 plan,
64 async (request) => {
65 const missing = requiredScopesForServerSettingsPatch(request.input.patch).find(
66 (scope) => !readEnvironmentScope(request.environmentId, scope),
67 );
68 if (missing)
69 return AsyncResult.failure(
70 Cause.fail(
71 new EnvironmentAuthorizationError({
72 requiredScope: missing,
73 message: "This connection lacks permission to change these settings.",
74 }),
75 ),
76 );
77 return persistServer(request);
78 },
79 persistClientSettingsPatch,
80 ).then(({ failedEnvironments, savedEnvironments, savedEnvironmentCount }) => {
81 if (failedEnvironments.length === 0) return;
82 onFailed?.();
83 toastManager.add({
84 type: "error",
85 title:
86 savedEnvironmentCount > 0 ? "Setting saved on some environments" : "Setting not saved",
87 description: [
88 ...failedEnvironments.map(
89 ({ label, message }) => `Could not save on ${label}: ${message}`,
90 ),
91 ...(savedEnvironmentCount > 0
92 ? [`Saved on ${savedEnvironments.map(({ label }) => label).join(", ")}.`]
93 : []),
94 ].join("\n"),
95 });
96 });
97 };
99​
100export function useUpdateScopedSettings() {
101 const { scope, environments, deviceOnly } = useSettingsScope();
102 const run = useRunScopedPlan();
103 /** `onFailed` runs when any target rejects or drops the patch, so a draft can fall back. */
104 return (patch: ScopedSettingsPatch, options?: { readonly onFailed?: () => void }) =>
105 run(planScopedSettingsPatch(scope, environments, patch, { deviceOnly }), options?.onFailed);
107​
108export function useClearScopedSettings() {
109 const context = useOptionalSettingsScope();
110 const run = useRunScopedPlan();
111 return (keys: readonly ProjectScopedServerSettingKey[]) => {
112 if (context === null) return;
113 run(planScopedSettingsClear(context.scope, context.environments, keys));
114 };
116​
117export function useClearProjectOverrides() {
118 const context = useOptionalSettingsScope();
119 const run = useRunScopedPlan();
120 return (
121 entries: readonly ProjectOverrideEntry[],
122 keys: readonly ProjectScopedServerSettingKey[],
123 ) => {
124 if (context === null) return;
125 run(planProjectOverridesClear(context.environments, entries, keys));
126 };
128​
129export function useScopedSettingsWriteAllowed() {
130 const { connectedEnvironments } = useSettingsScope();
131 const writable = useEnvironmentsWithScope(connectedEnvironments, AuthSettingsWriteScope);
132 return (
133 connectedEnvironments.length > 0 &&
134 connectedEnvironments.every((target) => writable.has(target.environmentId))
135 );