Files
modeling-app/src/lib/settings.ts
Frank Noirot 602e7afef6 File based settings (#1361)
* Rename GlobalStateContext to SettingsAuthContext

* Naive initial impl of settings persistence to file system

* Update app identifier in tauri config

* Add "show in folder" tauri command

* Load from and save to file system in Tauri app

* Add documents drive to tauri permission scope

* Add recursive prop to default dir selection dialog

* Add success toast to web restore defaults action

* Add a way to validate read-in settings

* Update imports to use separate settings lib file

* Validate localStorage-loaded settings, combine error message

* Add a e2e test for validation

* Clean up state state bugs

* Reverse validation looping so new users don't error

* update settingsMachine typegen to remove conflicts

* Fmt

* Fix TS errors
2024-02-15 14:14:14 -05:00

96 lines
2.7 KiB
TypeScript

import { type Models } from '@kittycad/lib'
import { CameraSystem, cameraSystems } from './cameraControls'
import { Themes } from './theme'
export const DEFAULT_PROJECT_NAME = 'project-$nnn'
export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY'
export const SETTINGS_FILE_NAME = 'settings.json'
export enum UnitSystem {
Imperial = 'imperial',
Metric = 'metric',
}
export const baseUnits = {
imperial: ['in', 'ft', 'yd'],
metric: ['mm', 'cm', 'm'],
} as const
export type BaseUnit = Models['UnitLength_type']
export const baseUnitsUnion = Object.values(baseUnits).flatMap((v) => v)
export type Toggle = 'On' | 'Off'
const toggleAsArray = ['On', 'Off'] as const
export type SettingsMachineContext = {
baseUnit: BaseUnit
cameraControls: CameraSystem
defaultDirectory: string
defaultProjectName: string
onboardingStatus: string
showDebugPanel: boolean
textWrapping: Toggle
theme: Themes
unitSystem: UnitSystem
}
export const initialSettings: SettingsMachineContext = {
baseUnit: 'in' as BaseUnit,
cameraControls: 'KittyCAD' as CameraSystem,
defaultDirectory: '',
defaultProjectName: DEFAULT_PROJECT_NAME,
onboardingStatus: '',
showDebugPanel: false,
textWrapping: 'On' as Toggle,
theme: Themes.System,
unitSystem: UnitSystem.Imperial,
}
function isEnumMember<T extends Record<string, unknown>>(v: unknown, e: T) {
return Object.values(e).includes(v)
}
const settingsValidators: Record<
keyof SettingsMachineContext,
(v: unknown) => boolean
> = {
baseUnit: (v) => baseUnitsUnion.includes(v as BaseUnit),
cameraControls: (v) => cameraSystems.includes(v as CameraSystem),
defaultDirectory: (v) => typeof v === 'string',
defaultProjectName: (v) => typeof v === 'string',
onboardingStatus: (v) => typeof v === 'string',
showDebugPanel: (v) => typeof v === 'boolean',
textWrapping: (v) => toggleAsArray.includes(v as Toggle),
theme: (v) => isEnumMember(v, Themes),
unitSystem: (v) => isEnumMember(v, UnitSystem),
}
function removeInvalidSettingsKeys(s: Record<string, unknown>) {
const validKeys = Object.keys(initialSettings)
for (const key in s) {
if (!validKeys.includes(key)) {
console.warn(`Invalid key found in settings: ${key}`)
delete s[key]
}
}
return s
}
export function validateSettings(s: Record<string, unknown>) {
let settingsNoInvalidKeys = removeInvalidSettingsKeys({ ...s })
let errors: (keyof SettingsMachineContext)[] = []
for (const key in settingsNoInvalidKeys) {
const k = key as keyof SettingsMachineContext
if (!settingsValidators[k](settingsNoInvalidKeys[k])) {
delete settingsNoInvalidKeys[k]
errors.push(k)
}
}
return {
settings: settingsNoInvalidKeys as Partial<SettingsMachineContext>,
errors,
}
}