| 1 | import { AuthSettingsWriteScope } from "@t3tools/contracts"; |
| 2 | import { useEnvironmentsWithScope } from "../../state/session"; |
| 3 | import { |
| 4 | requiredScopesForServerSettingsPatch, |
| 5 | EnvironmentAuthorizationError, |
| 6 | } from "@t3tools/contracts"; |
| 7 | import { readEnvironmentScope } from "../../state/session"; |
| 8 | import * as Cause from "effect/Cause"; |
| 9 | import { AsyncResult } from "effect/reactivity"; |
| 10 | import { |
| 11 | DEFAULT_SERVER_SETTINGS, |
| 12 | type ProjectScopedServerSettingKey, |
| 13 | type ServerSettings, |
| 14 | type UnifiedSettings, |
| 15 | } from "@t3tools/contracts"; |
| 16 | import { useCallback, useMemo } from "react"; |
| 17 | |
| 18 | import { |
| 19 | mergeEnvironmentSettings, |
| 20 | persistClientSettingsPatch, |
| 21 | useClientSettings, |
| 22 | useMigrateModelPreferences, |
| 23 | } from "../../hooks/useSettings"; |
| 24 | import { serverEnvironment } from "../../state/server"; |
| 25 | import { useAtomCommand } from "../../state/use-atom-command"; |
| 26 | import { toastManager } from "../ui/toast"; |
| 27 | import { useOptionalSettingsScope, useSettingsScope } from "./SettingsScopeContext"; |
| 28 | import { |
| 29 | persistScopedSettingsPatch, |
| 30 | planProjectOverridesClear, |
| 31 | planScopedSettingsClear, |
| 32 | planScopedSettingsPatch, |
| 33 | scopedSettingsAreMixed, |
| 34 | scopedSettingsSource, |
| 35 | type ProjectOverrideEntry, |
| 36 | type ScopedSettingsPatch, |
| 37 | } from "./scopedSettings"; |
| 38 | |
| 39 | /** Effective settings for the representative target: project overrides applied on top of its environment. */ |
| 40 | export function useScopedSettings<T = UnifiedSettings>( |
| 41 | selector?: (settings: UnifiedSettings) => T, |
| 42 | ): T { |
| 43 | const { target } = useSettingsScope(); |
| 44 | const clientSettings = useClientSettings(); |
| 45 | const serverSettings = target?.settings ?? DEFAULT_SERVER_SETTINGS; |
| 46 | useMigrateModelPreferences(target?.environmentId ?? null, target?.settings ?? null); |
| 47 | const settings = useMemo( |
| 48 | () => mergeEnvironmentSettings(serverSettings, clientSettings), |
| 49 | [clientSettings, serverSettings], |
| 50 | ); |
| 51 | return useMemo(() => (selector ? selector(settings) : (settings as T)), [selector, settings]); |
| 52 | } |
| 53 | |
| 54 | export function useScopedSettingsMixed(keys: readonly (keyof ServerSettings)[]): boolean { |
| 55 | const { targets } = useSettingsScope(); |
| 56 | return scopedSettingsAreMixed(targets, keys); |
| 57 | } |
| 58 | |
| 59 | /** Where the keys' effective values come from across the selected targets. */ |
| 60 | export function useScopedSettingSource(keys: readonly (keyof ServerSettings)[]) { |
| 61 | const { targets } = useSettingsScope(); |
| 62 | return scopedSettingsSource(targets, keys); |
| 63 | } |
| 64 | |
| 65 | function useRunScopedPlan() { |
| 66 | const persistServer = useAtomCommand(serverEnvironment.updateSettings, { reportFailure: false }); |
| 67 | return useCallback( |
| 68 | (plan: ReturnType<typeof planScopedSettingsPatch>) => { |
| 69 | if (plan.unavailableReason) { |
| 70 | toastManager.add({ |
| 71 | type: "warning", |
| 72 | title: "Setting not saved", |
| 73 | description: plan.unavailableReason, |
| 74 | }); |
| 75 | return; |
| 76 | } |
| 77 | void persistScopedSettingsPatch( |
| 78 | plan, |
| 79 | async (request) => { |
| 80 | const missing = requiredScopesForServerSettingsPatch(request.input.patch).find( |
| 81 | (scope) => !readEnvironmentScope(request.environmentId, scope), |
| 82 | ); |
| 83 | if (missing) |
| 84 | return AsyncResult.failure( |
| 85 | Cause.fail( |
| 86 | new EnvironmentAuthorizationError({ |
| 87 | requiredScope: missing, |
| 88 | message: "This connection lacks permission to change these settings.", |
| 89 | }), |
| 90 | ), |
| 91 | ); |
| 92 | return persistServer(request); |
| 93 | }, |
| 94 | persistClientSettingsPatch, |
| 95 | ).then(({ failedEnvironments, savedEnvironments, savedEnvironmentCount }) => { |
| 96 | if (failedEnvironments.length === 0) return; |
| 97 | toastManager.add({ |
| 98 | type: "error", |
| 99 | title: |
| 100 | savedEnvironmentCount > 0 ? "Setting saved on some environments" : "Setting not saved", |
| 101 | description: [ |
| 102 | ...failedEnvironments.map( |
| 103 | ({ label, message }) => `Could not save on ${label}: ${message}`, |
| 104 | ), |
| 105 | ...(savedEnvironmentCount > 0 |
| 106 | ? [`Saved on ${savedEnvironments.map(({ label }) => label).join(", ")}.`] |
| 107 | : []), |
| 108 | ].join("\n"), |
| 109 | }); |
| 110 | }); |
| 111 | }, |
| 112 | [persistServer], |
| 113 | ); |
| 114 | } |
| 115 | |
| 116 | export function useUpdateScopedSettings() { |
| 117 | const { scope, environments } = useSettingsScope(); |
| 118 | const run = useRunScopedPlan(); |
| 119 | return useCallback( |
| 120 | (patch: ScopedSettingsPatch) => run(planScopedSettingsPatch(scope, environments, patch)), |
| 121 | [environments, run, scope], |
| 122 | ); |
| 123 | } |
| 124 | |
| 125 | /** |
| 126 | * Drop the project overrides for `keys` so the selected checkouts inherit |
| 127 | * again. Rows also render outside the settings layout (provider cards, |
| 128 | * dialogs), where there is no scope and nothing to clear. |
| 129 | */ |
| 130 | export function useClearScopedSettings() { |
| 131 | const context = useOptionalSettingsScope(); |
| 132 | const run = useRunScopedPlan(); |
| 133 | return useCallback( |
| 134 | (keys: readonly ProjectScopedServerSettingKey[]) => { |
| 135 | if (context === null) return; |
| 136 | run(planScopedSettingsClear(context.scope, context.environments, keys)); |
| 137 | }, |
| 138 | [context, run], |
| 139 | ); |
| 140 | } |
| 141 | |
| 142 | /** Clear `keys` on specific project entries, from an environment scope's chain popover. */ |
| 143 | export function useClearProjectOverrides() { |
| 144 | const context = useOptionalSettingsScope(); |
| 145 | const run = useRunScopedPlan(); |
| 146 | return useCallback( |
| 147 | (entries: readonly ProjectOverrideEntry[], keys: readonly ProjectScopedServerSettingKey[]) => { |
| 148 | if (context === null) return; |
| 149 | run(planProjectOverridesClear(context.environments, entries, keys)); |
| 150 | }, |
| 151 | [context, run], |
| 152 | ); |
| 153 | } |
| 154 | |
| 155 | export function useScopedSettingsWriteAllowed() { |
| 156 | const { connectedEnvironments } = useSettingsScope(); |
| 157 | const writable = useEnvironmentsWithScope(connectedEnvironments, AuthSettingsWriteScope); |
| 158 | return ( |
| 159 | connectedEnvironments.length > 0 && |
| 160 | connectedEnvironments.every((target) => writable.has(target.environmentId)) |
| 161 | ); |
| 162 | } |