diff --git a/e2e/playwright/flow-tests.spec.ts b/e2e/playwright/flow-tests.spec.ts index c2c465bc9..be6da17e7 100644 --- a/e2e/playwright/flow-tests.spec.ts +++ b/e2e/playwright/flow-tests.spec.ts @@ -3,7 +3,6 @@ import { secrets } from './secrets' import { getUtils } from './test-utils' import waitOn from 'wait-on' import { Themes } from '../../src/lib/theme' -import { initialSettings } from '../../src/lib/settings' /* debug helper: unfortunately we do rely on exact coord mouse clicks in a few places @@ -387,53 +386,6 @@ test('Auto complete works', async ({ page }) => { |> xLine(5, %)`) }) -// Stored settings validation test -test('Stored settings are validated and fall back to defaults', async ({ - page, - context, -}) => { - // Override beforeEach test setup - // with corrupted settings - await context.addInitScript(async () => { - const storedSettings = JSON.parse( - localStorage.getItem('SETTINGS_PERSIST_KEY') || '{}' - ) - - // Corrupt the settings - storedSettings.baseUnit = 'invalid' - storedSettings.cameraControls = `() => alert('hack the planet')` - storedSettings.defaultDirectory = 123 - storedSettings.defaultProjectName = false - - localStorage.setItem('SETTINGS_PERSIST_KEY', JSON.stringify(storedSettings)) - }) - - await page.setViewportSize({ width: 1200, height: 500 }) - await page.goto('/', { waitUntil: 'domcontentloaded' }) - - // Check the toast appeared - await expect( - page.getByText(`Error validating persisted settings:`, { exact: false }) - ).toBeVisible() - - // Check the settings were reset - const storedSettings = JSON.parse( - await page.evaluate( - () => localStorage.getItem('SETTINGS_PERSIST_KEY') || '{}' - ) - ) - await expect(storedSettings.baseUnit).toBe(initialSettings.baseUnit) - await expect(storedSettings.cameraControls).toBe( - initialSettings.cameraControls - ) - await expect(storedSettings.defaultDirectory).toBe( - initialSettings.defaultDirectory - ) - await expect(storedSettings.defaultProjectName).toBe( - initialSettings.defaultProjectName - ) -}) - // Onboarding tests test('Onboarding redirects and code updating', async ({ page, context }) => { const u = getUtils(page) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index d685fcb92..5915c1927 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -143,25 +143,6 @@ async fn get_user( Ok(user_info) } -/// Open the selected path in the system file manager. -/// From this GitHub comment: https://github.com/tauri-apps/tauri/issues/4062#issuecomment-1338048169 -/// But with the Linux support removed since we don't need it for now. -#[tauri::command] -fn show_in_folder(path: String) { - #[cfg(target_os = "windows")] - { - Command::new("explorer") - .args(["/select,", &path]) // The comma after select is not a typo - .spawn() - .unwrap(); - } - - #[cfg(target_os = "macos")] - { - Command::new("open").args(["-R", &path]).spawn().unwrap(); - } -} - fn main() { tauri::Builder::default() .setup(|_app| { @@ -178,8 +159,7 @@ fn main() { get_user, login, read_toml, - read_txt_file, - show_in_folder, + read_txt_file ]) .plugin(tauri_plugin_fs_extra::init()) .run(tauri::generate_context!()) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 50a39d2f5..c2f521662 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -23,8 +23,7 @@ "fs": { "scope": [ "$HOME/**/*", - "$APPDATA/**/*", - "$DOCUMENT/**/*" + "$APPDATA/**/*" ], "all": true }, @@ -61,7 +60,7 @@ "icons/icon.icns", "icons/icon.ico" ], - "identifier": "zoo-modeling-app", + "identifier": "io.kittycad.modeling-app", "longDescription": "", "macOS": { "entitlements": null, diff --git a/src/App.tsx b/src/App.tsx index 2d08fb6b0..e7993c948 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,7 +22,7 @@ import { getNormalisedCoordinates } from './lib/utils' import { useLoaderData, useNavigate } from 'react-router-dom' import { type IndexLoaderData } from 'lib/types' import { paths } from 'lib/paths' -import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' +import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { onboardingPaths } from 'routes/Onboarding/paths' import { CodeMenu } from 'components/CodeMenu' import { TextEditor } from 'components/TextEditor' @@ -53,7 +53,7 @@ export function App() { streamDimensions: s.streamDimensions, })) - const { settings } = useSettingsAuthContext() + const { settings } = useGlobalStateContext() const { showDebugPanel, onboardingStatus, theme } = settings?.context || {} const { state, send } = useModelingContext() diff --git a/src/Auth.tsx b/src/Auth.tsx index 19260e605..8b1b188a9 100644 --- a/src/Auth.tsx +++ b/src/Auth.tsx @@ -1,9 +1,9 @@ import Loading from './components/Loading' -import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' +import { useGlobalStateContext } from 'hooks/useGlobalStateContext' // Wrapper around protected routes, used in src/Router.tsx export const Auth = ({ children }: React.PropsWithChildren) => { - const { auth } = useSettingsAuthContext() + const { auth } = useGlobalStateContext() const isLoggingIn = auth?.state.matches('checkIfLoggedIn') return isLoggingIn ? ( diff --git a/src/Router.tsx b/src/Router.tsx index ce2e7faa8..4b593401d 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -29,9 +29,11 @@ import { import { metadata } from 'tauri-plugin-fs-extra-api' import DownloadAppBanner from './components/DownloadAppBanner' import { WasmErrBanner } from './components/WasmErrBanner' -import { SettingsAuthStateProvider } from './components/SettingsAuthStateProvider' -import { settingsMachine } from './machines/settingsMachine' -import { SETTINGS_PERSIST_KEY } from 'lib/settings' +import { GlobalStateProvider } from './components/GlobalStateProvider' +import { + SETTINGS_PERSIST_KEY, + settingsMachine, +} from './machines/settingsMachine' import { ContextFrom } from 'xstate' import CommandBarProvider from 'components/CommandBar/CommandBar' import { TEST, VITE_KC_SENTRY_DSN } from './env' @@ -89,9 +91,7 @@ const addGlobalContextToElements = ( ...route, element: ( - - {route.element} - + {route.element} ), } @@ -229,42 +229,32 @@ const router = createBrowserRouter( const projectDir = await initializeProjectDirectory( persistedSettings.defaultDirectory || '' ) - let newDefaultDirectory: string | undefined = undefined - if (projectDir.path) { - if (projectDir.path !== persistedSettings.defaultDirectory) { - localStorage.setItem( - SETTINGS_PERSIST_KEY, - JSON.stringify({ - ...persistedSettings, - defaultDirectory: projectDir, - }) - ) - newDefaultDirectory = projectDir.path - } - const projectsNoMeta = (await readDir(projectDir.path)).filter( - isProjectDirectory - ) - const projects = await Promise.all( - projectsNoMeta.map(async (p: FileEntry) => ({ - entrypointMetadata: await metadata( - p.path + sep + PROJECT_ENTRYPOINT - ), - ...p, - })) + if (projectDir !== persistedSettings.defaultDirectory) { + localStorage.setItem( + SETTINGS_PERSIST_KEY, + JSON.stringify({ + ...persistedSettings, + defaultDirectory: projectDir, + }) ) + newDefaultDirectory = projectDir + } + const projectsNoMeta = (await readDir(projectDir)).filter( + isProjectDirectory + ) + const projects = await Promise.all( + projectsNoMeta.map(async (p: FileEntry) => ({ + entrypointMetadata: await metadata( + p.path + sep + PROJECT_ENTRYPOINT + ), + ...p, + })) + ) - return { - projects, - newDefaultDirectory, - error: projectDir.error, - } - } else { - return { - projects: [], - newDefaultDirectory, - error: projectDir.error, - } + return { + projects, + newDefaultDirectory, } }, children: [ diff --git a/src/clientSideScene/ClientSideSceneComp.tsx b/src/clientSideScene/ClientSideSceneComp.tsx index c09feaeed..7325336aa 100644 --- a/src/clientSideScene/ClientSideSceneComp.tsx +++ b/src/clientSideScene/ClientSideSceneComp.tsx @@ -2,7 +2,7 @@ import { useRef, useEffect, useState } from 'react' import { useModelingContext } from 'hooks/useModelingContext' import { cameraMouseDragGuards } from 'lib/cameraControls' -import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' +import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useStore } from 'useStore' import { DEBUG_SHOW_BOTH_SCENES, @@ -38,7 +38,7 @@ export const ClientSideScene = ({ cameraControls, }: { cameraControls: ReturnType< - typeof useSettingsAuthContext + typeof useGlobalStateContext >['settings']['context']['cameraControls'] }) => { const canvasRef = useRef(null) diff --git a/src/components/AppHeader.tsx b/src/components/AppHeader.tsx index 275c4aea3..5f1b663b6 100644 --- a/src/components/AppHeader.tsx +++ b/src/components/AppHeader.tsx @@ -2,7 +2,7 @@ import { Toolbar } from '../Toolbar' import UserSidebarMenu from './UserSidebarMenu' import { type IndexLoaderData } from 'lib/types' import ProjectSidebarMenu from './ProjectSidebarMenu' -import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' +import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import styles from './AppHeader.module.css' import { NetworkHealthIndicator } from './NetworkHealthIndicator' import { useCommandsContext } from 'hooks/useCommandsContext' @@ -25,7 +25,7 @@ export const AppHeader = ({ }: AppHeaderProps) => { const platform = usePlatform() const { commandBarSend } = useCommandsContext() - const { auth } = useSettingsAuthContext() + const { auth } = useGlobalStateContext() const user = auth?.context?.user return ( diff --git a/src/components/ExportButton.tsx b/src/components/ExportButton.tsx index d577f5929..581ccbb99 100644 --- a/src/components/ExportButton.tsx +++ b/src/components/ExportButton.tsx @@ -6,7 +6,7 @@ import React from 'react' import { useFormik } from 'formik' import { Models } from '@kittycad/lib' import { engineCommandManager } from '../lang/std/engineConnection' -import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' +import { useGlobalStateContext } from 'hooks/useGlobalStateContext' type OutputFormat = Models['OutputFormat_type'] type OutputTypeKey = OutputFormat['type'] @@ -29,7 +29,7 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => { context: { baseUnit }, }, }, - } = useSettingsAuthContext() + } = useGlobalStateContext() const defaultType = 'gltf' const [type, setType] = React.useState(defaultType) diff --git a/src/components/SettingsAuthStateProvider.tsx b/src/components/GlobalStateProvider.tsx similarity index 56% rename from src/components/SettingsAuthStateProvider.tsx rename to src/components/GlobalStateProvider.tsx index b38a8a306..10724241e 100644 --- a/src/components/SettingsAuthStateProvider.tsx +++ b/src/components/GlobalStateProvider.tsx @@ -5,12 +5,7 @@ import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine' import withBaseUrl from '../lib/withBaseURL' import React, { createContext, useEffect, useRef } from 'react' import useStateMachineCommands from '../hooks/useStateMachineCommands' -import { settingsMachine } from 'machines/settingsMachine' -import { - initialSettings, - SETTINGS_PERSIST_KEY, - validateSettings, -} from 'lib/settings' +import { SETTINGS_PERSIST_KEY, settingsMachine } from 'machines/settingsMachine' import { toast } from 'react-hot-toast' import { setThemeClass, Themes } from 'lib/theme' import { @@ -23,7 +18,6 @@ import { import { isTauri } from 'lib/isTauri' import { settingsCommandBarConfig } from 'lib/commandBarConfigs/settingsCommandConfig' import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig' -import { initializeProjectDirectory, readSettingsFile } from 'lib/tauriFS' type MachineContext = { state: StateFrom @@ -31,14 +25,14 @@ type MachineContext = { send: Prop, 'send'> } -type SettingsAuthContext = { +type GlobalContext = { auth: MachineContext settings: MachineContext } -export const SettingsAuthStateContext = createContext({} as SettingsAuthContext) +export const GlobalStateContext = createContext({} as GlobalContext) -export const SettingsAuthStateProvider = ({ +export const GlobalStateProvider = ({ children, }: { children: React.ReactNode @@ -46,17 +40,14 @@ export const SettingsAuthStateProvider = ({ const navigate = useNavigate() // Settings machine setup - // Load settings from local storage - // and validate them const retrievedSettings = useRef( - validateSettings( - JSON.parse(localStorage?.getItem(SETTINGS_PERSIST_KEY) || '{}') - ) + localStorage?.getItem(SETTINGS_PERSIST_KEY) || '{}' ) const persistedSettings = Object.assign( - {}, - initialSettings, - retrievedSettings.current.settings + settingsMachine.initialState.context, + JSON.parse(retrievedSettings.current) as Partial< + (typeof settingsMachine)['context'] + > ) const [settingsState, settingsSend] = useMachine(settingsMachine, { @@ -81,75 +72,6 @@ export const SettingsAuthStateProvider = ({ }, }) - // If the app is running in the Tauri context, - // try to read the settings from a file - // after doing some validation on them - useEffect(() => { - async function getFileBasedSettings() { - if (isTauri()) { - const newSettings = await readSettingsFile() - - if (newSettings) { - if (newSettings.defaultDirectory) { - const newDefaultDirectory = await initializeProjectDirectory( - newSettings.defaultDirectory || '' - ) - if (newDefaultDirectory.error !== null) { - toast.error(newDefaultDirectory.error.message) - } - - if (newDefaultDirectory.path !== null) { - newSettings.defaultDirectory = newDefaultDirectory.path - } - } - const { settings: validatedSettings, errors: validationErrors } = - validateSettings(newSettings) - - retrievedSettings.current = Object.assign( - {}, - initialSettings, - retrievedSettings.current, - validatedSettings - ) - - settingsSend({ - type: 'Set All Settings', - data: validatedSettings, - }) - - return validationErrors - } - } else { - // If the app is not running in the Tauri context, - // just use the settings from local storage - // after they've been validated to ensure they are correct. - settingsSend({ - type: 'Set All Settings', - data: retrievedSettings.current.settings, - }) - } - return [] - } - - // If there were validation errors either from local storage or from the file, - // log them to the console and show a toast message to the user. - void getFileBasedSettings().then((validationErrors: string[]) => { - const combinedErrors = new Set([ - ...retrievedSettings.current.errors, - ...validationErrors, - ]) - - if (combinedErrors.size > 0) { - const errorMessage = - 'Error validating persisted settings: ' + - Array.from(combinedErrors).join(', ') + - '. Using defaults.' - console.error(errorMessage) - toast.error(errorMessage) - } - }) - }, [settingsSend]) - useStateMachineCommands({ machineId: 'settings', state: settingsState, @@ -197,7 +119,7 @@ export const SettingsAuthStateProvider = ({ }) return ( - {children} - + ) } -export default SettingsAuthStateProvider +export default GlobalStateProvider export function logout() { localStorage.removeItem(TOKEN_PERSIST_KEY) diff --git a/src/components/ModelingMachineProvider.tsx b/src/components/ModelingMachineProvider.tsx index 87ea7313a..70ff6b94e 100644 --- a/src/components/ModelingMachineProvider.tsx +++ b/src/components/ModelingMachineProvider.tsx @@ -10,7 +10,7 @@ import { } from 'xstate' import { SetSelections, modelingMachine } from 'machines/modelingMachine' import { useSetupEngineManager } from 'hooks/useSetupEngineManager' -import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' +import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { isCursorInSketchCommandRange } from 'lang/util' import { engineCommandManager } from 'lang/std/engineConnection' import { kclManager, useKclContext } from 'lang/KclSingleton' @@ -53,7 +53,7 @@ export const ModelingMachineProvider = ({ }: { children: React.ReactNode }) => { - const { auth } = useSettingsAuthContext() + const { auth } = useGlobalStateContext() const { code } = useKclContext() const token = auth?.context?.token const streamRef = useRef(null) diff --git a/src/components/NetworkHealthIndicator.test.tsx b/src/components/NetworkHealthIndicator.test.tsx index c08c2d1d9..40937ecb8 100644 --- a/src/components/NetworkHealthIndicator.test.tsx +++ b/src/components/NetworkHealthIndicator.test.tsx @@ -1,6 +1,6 @@ import { fireEvent, render, screen } from '@testing-library/react' import { BrowserRouter } from 'react-router-dom' -import { SettingsAuthStateProvider } from './SettingsAuthStateProvider' +import { GlobalStateProvider } from './GlobalStateProvider' import CommandBarProvider from './CommandBar/CommandBar' import { NETWORK_HEALTH_TEXT, @@ -13,7 +13,7 @@ function TestWrap({ children }: { children: React.ReactNode }) { return ( - {children} + {children} ) diff --git a/src/components/ProjectSidebarMenu.test.tsx b/src/components/ProjectSidebarMenu.test.tsx index e95ed079f..6aef589c0 100644 --- a/src/components/ProjectSidebarMenu.test.tsx +++ b/src/components/ProjectSidebarMenu.test.tsx @@ -2,7 +2,7 @@ import { fireEvent, render, screen } from '@testing-library/react' import { BrowserRouter } from 'react-router-dom' import ProjectSidebarMenu from './ProjectSidebarMenu' import { type ProjectWithEntryPointMetadata } from 'lib/types' -import { SettingsAuthStateProvider } from './SettingsAuthStateProvider' +import { GlobalStateProvider } from './GlobalStateProvider' import CommandBarProvider from './CommandBar/CommandBar' import { APP_NAME } from 'lib/constants' @@ -42,9 +42,9 @@ describe('ProjectSidebarMenu tests', () => { render( - + - + ) @@ -63,9 +63,9 @@ describe('ProjectSidebarMenu tests', () => { render( - + - + ) @@ -79,12 +79,12 @@ describe('ProjectSidebarMenu tests', () => { render( - + - + ) diff --git a/src/components/Stream.tsx b/src/components/Stream.tsx index e4c00edbe..b934b598c 100644 --- a/src/components/Stream.tsx +++ b/src/components/Stream.tsx @@ -10,7 +10,7 @@ import { useStore } from '../useStore' import { getNormalisedCoordinates, throttle } from '../lib/utils' import Loading from './Loading' import { cameraMouseDragGuards } from 'lib/cameraControls' -import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' +import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { Models } from '@kittycad/lib' import { engineCommandManager } from '../lang/std/engineConnection' import { useModelingContext } from 'hooks/useModelingContext' @@ -34,7 +34,7 @@ export const Stream = ({ className = '' }: { className?: string }) => { setDidDragInStream: s.setDidDragInStream, streamDimensions: s.streamDimensions, })) - const { settings } = useSettingsAuthContext() + const { settings } = useGlobalStateContext() const cameraControls = settings?.context?.cameraControls const { state } = useModelingContext() const { isExecuting } = useKclContext() diff --git a/src/components/TextEditor.tsx b/src/components/TextEditor.tsx index bb4894909..e1891afed 100644 --- a/src/components/TextEditor.tsx +++ b/src/components/TextEditor.tsx @@ -8,7 +8,7 @@ import Server from '../editor/lsp/server' import Client from '../editor/lsp/client' import { TEST } from 'env' import { useCommandsContext } from 'hooks/useCommandsContext' -import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' +import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useConvertToVariable } from 'hooks/useToolbarGuards' import { Themes } from 'lib/theme' import { useMemo, useRef } from 'react' @@ -72,7 +72,7 @@ export const TextEditor = ({ } = useModelingContext() const { settings: { context: { textWrapping } = {} } = {}, auth } = - useSettingsAuthContext() + useGlobalStateContext() const { commandBarSend } = useCommandsContext() const { enable: convertEnabled, handleClick: convertCallback } = useConvertToVariable() diff --git a/src/components/UserSidebarMenu.test.tsx b/src/components/UserSidebarMenu.test.tsx index d50e8893d..5c14046fb 100644 --- a/src/components/UserSidebarMenu.test.tsx +++ b/src/components/UserSidebarMenu.test.tsx @@ -7,7 +7,7 @@ import { createRoutesFromElements, } from 'react-router-dom' import { Models } from '@kittycad/lib' -import { SettingsAuthStateProvider } from './SettingsAuthStateProvider' +import { GlobalStateProvider } from './GlobalStateProvider' import CommandBarProvider from './CommandBar/CommandBar' type User = Models['User_type'] @@ -107,7 +107,7 @@ function TestWrap({ children }: { children: React.ReactNode }) { path="/file/:id" element={ - {children} + {children} } /> diff --git a/src/components/UserSidebarMenu.tsx b/src/components/UserSidebarMenu.tsx index 266428ff6..5b170020f 100644 --- a/src/components/UserSidebarMenu.tsx +++ b/src/components/UserSidebarMenu.tsx @@ -6,7 +6,7 @@ import { useLocation, useNavigate } from 'react-router-dom' import { Fragment, useState } from 'react' import { paths } from 'lib/paths' import { Models } from '@kittycad/lib' -import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' +import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' type User = Models['User_type'] @@ -17,7 +17,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => { const displayedName = getDisplayName(user) const [imageLoadFailed, setImageLoadFailed] = useState(false) const navigate = useNavigate() - const send = useSettingsAuthContext()?.auth?.send + const send = useGlobalStateContext()?.auth?.send // Fallback logic for displaying user's "name": // 1. user.name diff --git a/src/hooks/useGlobalStateContext.ts b/src/hooks/useGlobalStateContext.ts new file mode 100644 index 000000000..b15a101a4 --- /dev/null +++ b/src/hooks/useGlobalStateContext.ts @@ -0,0 +1,6 @@ +import { GlobalStateContext } from 'components/GlobalStateProvider' +import { useContext } from 'react' + +export const useGlobalStateContext = () => { + return useContext(GlobalStateContext) +} diff --git a/src/hooks/useSettingsAuthContext.ts b/src/hooks/useSettingsAuthContext.ts deleted file mode 100644 index 141737cf4..000000000 --- a/src/hooks/useSettingsAuthContext.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { SettingsAuthStateContext } from 'components/SettingsAuthStateProvider' -import { useContext } from 'react' - -export const useSettingsAuthContext = () => { - return useContext(SettingsAuthStateContext) -} diff --git a/src/lib/commandBarConfigs/settingsCommandConfig.ts b/src/lib/commandBarConfigs/settingsCommandConfig.ts index 3a892ef84..15b092f03 100644 --- a/src/lib/commandBarConfigs/settingsCommandConfig.ts +++ b/src/lib/commandBarConfigs/settingsCommandConfig.ts @@ -1,6 +1,11 @@ import { CommandSetConfig } from '../commandTypes' -import { BaseUnit, Toggle, UnitSystem, baseUnitsUnion } from 'lib/settings' -import { settingsMachine } from 'machines/settingsMachine' +import { + BaseUnit, + Toggle, + UnitSystem, + baseUnitsUnion, + settingsMachine, +} from 'machines/settingsMachine' import { CameraSystem, cameraSystems } from '../cameraControls' import { Themes } from '../theme' diff --git a/src/lib/settings.ts b/src/lib/settings.ts deleted file mode 100644 index 91e912259..000000000 --- a/src/lib/settings.ts +++ /dev/null @@ -1,95 +0,0 @@ -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>(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) { - 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) { - 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, - errors, - } -} diff --git a/src/lib/tauriFS.ts b/src/lib/tauriFS.ts index cd137dd69..398e9b141 100644 --- a/src/lib/tauriFS.ts +++ b/src/lib/tauriFS.ts @@ -3,16 +3,12 @@ import { createDir, exists, readDir, - readTextFile, writeTextFile, } from '@tauri-apps/api/fs' -import { appConfigDir, documentDir, homeDir, sep } from '@tauri-apps/api/path' +import { documentDir, homeDir, sep } from '@tauri-apps/api/path' import { isTauri } from './isTauri' import { type ProjectWithEntryPointMetadata } from 'lib/types' import { metadata } from 'tauri-plugin-fs-extra-api' -import { settingsMachine } from 'machines/settingsMachine' -import { ContextFrom } from 'xstate' -import { SETTINGS_FILE_NAME } from 'lib/settings' const PROJECT_FOLDER = 'zoo-modeling-app-projects' export const FILE_EXT = '.kcl' @@ -21,101 +17,39 @@ const INDEX_IDENTIFIER = '$n' // $nn.. will pad the number with 0s export const MAX_PADDING = 7 const RELEVANT_FILE_TYPES = ['kcl'] -type PathWithPossibleError = { - path: string | null - error: Error | null -} - -export async function getInitialDefaultDir() { - let dir - try { - dir = await documentDir() - } catch (e) { - dir = `${await homeDir()}Documents/` // for headless Linux (eg. Github Actions) - } - - return dir + PROJECT_FOLDER -} - // Initializes the project directory and returns the path -// with any Errors that occurred -export async function initializeProjectDirectory( - directory: string -): Promise { +export async function initializeProjectDirectory(directory: string) { if (!isTauri()) { throw new Error( 'initializeProjectDirectory() can only be called from a Tauri app' ) } - let returnValue: PathWithPossibleError = { - path: null, - error: null, - } - if (directory) { - returnValue = await testAndCreateDir(directory, returnValue) - } - - // If the directory from settings does not exist or could not be created, - // use the default directory - if (returnValue.path === null) { - const INITIAL_DEFAULT_DIR = await getInitialDefaultDir() - const defaultReturnValue = await testAndCreateDir( - INITIAL_DEFAULT_DIR, - returnValue, - { - exists: 'Error checking default directory.', - create: 'Error creating default directory.', - } - ) - returnValue.path = defaultReturnValue.path - returnValue.error = - returnValue.error === null ? defaultReturnValue.error : returnValue.error - } - - return returnValue -} - -async function testAndCreateDir( - directory: string, - returnValue = { - path: null, - error: null, - } as PathWithPossibleError, - errorMessages = { - exists: - 'Error checking directory at path from saved settings. Using default.', - create: - 'Error creating directory at path from saved settings. Using default.', - } -): Promise { - const dirExists = await exists(directory).catch((e) => { - console.error(`Error checking directory ${directory}. Original error:`, e) - return new Error(errorMessages.exists) - }) - - if (dirExists instanceof Error) { - returnValue.error = dirExists - } else if (dirExists && dirExists === true) { - const newDirCreated = await createDir(directory, { recursive: true }).catch( - (e) => { - console.error( - `Error creating directory ${directory}. Original error:`, - e - ) - return new Error(errorMessages.create) - } - ) - - if (newDirCreated instanceof Error) { - returnValue.error = newDirCreated - } else { - returnValue.path = directory + const dirExists = await exists(directory) + if (!dirExists) { + await createDir(directory, { recursive: true }) } + return directory } - return returnValue + let docDirectory: string + try { + docDirectory = await documentDir() + } catch (e) { + console.log('error', e) + docDirectory = `${await homeDir()}Documents/` // for headless Linux (eg. Github Actions) + } + + const INITIAL_DEFAULT_DIR = docDirectory + PROJECT_FOLDER + + const defaultDirExists = await exists(INITIAL_DEFAULT_DIR) + + if (!defaultDirExists) { + await createDir(INITIAL_DEFAULT_DIR, { recursive: true }) + } + + return INITIAL_DEFAULT_DIR } export function isProjectDirectory(fileOrDir: Partial) { @@ -366,44 +300,3 @@ function getPaddedIdentifierRegExp() { const escapedIdentifier = escapeRegExpChars(INDEX_IDENTIFIER) return new RegExp(`${escapedIdentifier}(${escapedIdentifier.slice(-1)}*)`) } - -export async function getSettingsFilePath() { - const dir = await appConfigDir() - return dir + SETTINGS_FILE_NAME -} - -export async function writeToSettingsFile( - settings: ContextFrom -) { - return writeTextFile( - await getSettingsFilePath(), - JSON.stringify(settings, null, 2) - ) -} - -export async function readSettingsFile(): Promise | null> { - const dir = await appConfigDir() - const path = dir + SETTINGS_FILE_NAME - const dirExists = await exists(dir) - if (!dirExists) { - await createDir(dir, { recursive: true }) - } - - const settingsExist = dirExists ? await exists(path) : false - - if (!settingsExist) { - console.log(`Settings file does not exist at ${path}`) - await writeToSettingsFile(settingsMachine.initialState.context) - return null - } - - try { - const settings = await readTextFile(path) - return JSON.parse(settings) - } catch (e) { - console.error('Error reading settings file:', e) - return null - } -} diff --git a/src/lib/types.ts b/src/lib/types.ts index d351b4c7d..ce323c06c 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -13,5 +13,4 @@ export type ProjectWithEntryPointMetadata = FileEntry & { export type HomeLoaderData = { projects: ProjectWithEntryPointMetadata[] newDefaultDirectory?: string - error: Error | null } diff --git a/src/machines/settingsMachine.ts b/src/machines/settingsMachine.ts index dbb132c05..08649aadc 100644 --- a/src/machines/settingsMachine.ts +++ b/src/machines/settingsMachine.ts @@ -1,43 +1,49 @@ import { assign, createMachine } from 'xstate' import { Themes, getSystemTheme, setThemeClass } from '../lib/theme' import { CameraSystem } from 'lib/cameraControls' -import { isTauri } from 'lib/isTauri' -import { writeToSettingsFile } from 'lib/tauriFS' -import { - BaseUnit, - DEFAULT_PROJECT_NAME, - SETTINGS_PERSIST_KEY, - SettingsMachineContext, - Toggle, - UnitSystem, - initialSettings, -} from 'lib/settings' +import { Models } from '@kittycad/lib' + +export const DEFAULT_PROJECT_NAME = 'project-$nnn' + +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' + +export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY' export const settingsMachine = createMachine( { /** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlTQAIAVACzAFswBtABgF1FQAHAe1iYsfbNxAAPRAA42+AEwB2KQFYAzGznKAnADZli1QBoQAT2kBGKfm37lOned3nzqgL6vjlLLgJFSFdCoAETAAMwBDAFdiagAFACc+ACswAGNqADlw5nYuJBB+QWFRfMkEABY5fDYa2rra83LjMwQdLWV8BXLyuxlVLU1Ld090bzxCEnJKYLComODMeLS0PniTXLFCoUwRMTK7fC1zNql7NgUjtnKjU0RlBSqpLVUVPVUda60tYZAvHHG-FNAgBVbBCKjIEywNBMDb5LbFPaILqdfRSORsS4qcxXZqIHqyK6qY4XOxsGTKco-P4+Cb+aYAIXCsDAVFBQjhvAE212pWkskUKnUml0+gUNxaqkU+EccnKF1UCnucnMcjcHl+o3+vkmZBofCgUFIMwARpEoFRYuFsGBiJyCtzEXzWrJlGxlKdVFKvfY1XiEBjyvhVOVzBdzu13pYFNStbTAQFqAB5bAmvjheIQf4QtDhNCRWD2hE7EqgfayHTEh7lHQNSxSf1Scz4cpHHFyFVujTKczuDXYPgQOBiGl4TaOktIhAAWg6X3nC4Xp39050sYw2rpYHHRUnztVhPJqmUlIGbEriv9WhrLZ6uibHcqUr7riAA */ id: 'Settings', predictableActionArguments: true, - context: { ...initialSettings }, + context: { + 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, + }, initial: 'idle', states: { idle: { entry: ['setThemeClass'], on: { - 'Set All Settings': { - actions: [ - assign((context, event) => { - return { - ...context, - ...event.data, - } - }), - 'persistSettings', - 'setThemeClass', - ], - target: 'idle', - internal: true, - }, 'Set Base Unit': { actions: [ assign({ @@ -151,7 +157,6 @@ export const settingsMachine = createMachine( tsTypes: {} as import('./settingsMachine.typegen').Typegen0, schema: { events: {} as - | { type: 'Set All Settings'; data: Partial } | { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } } | { type: 'Set Camera Controls' @@ -175,11 +180,6 @@ export const settingsMachine = createMachine( { actions: { persistSettings: (context) => { - if (isTauri()) { - writeToSettingsFile(context).catch((err) => { - console.error('Error writing settings:', err) - }) - } try { localStorage.setItem(SETTINGS_PERSIST_KEY, JSON.stringify(context)) } catch (e) { diff --git a/src/routes/Home.tsx b/src/routes/Home.tsx index 227275665..5190f6088 100644 --- a/src/routes/Home.tsx +++ b/src/routes/Home.tsx @@ -29,9 +29,9 @@ import { getSortIcon, } from '../lib/sorting' import useStateMachineCommands from '../hooks/useStateMachineCommands' -import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' +import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { useCommandsContext } from 'hooks/useCommandsContext' -import { DEFAULT_PROJECT_NAME } from 'lib/settings' +import { DEFAULT_PROJECT_NAME } from 'machines/settingsMachine' import { sep } from '@tauri-apps/api/path' import { homeCommandBarConfig } from 'lib/commandBarConfigs/homeCommandConfig' import { useHotkeys } from 'react-hotkeys-hook' @@ -42,17 +42,14 @@ import { isTauri } from 'lib/isTauri' const Home = () => { const { commandBarSend } = useCommandsContext() const navigate = useNavigate() - const { - projects: loadedProjects, - newDefaultDirectory, - error, - } = useLoaderData() as HomeLoaderData + const { projects: loadedProjects, newDefaultDirectory } = + useLoaderData() as HomeLoaderData const { settings: { context: { defaultDirectory, defaultProjectName }, send: sendToSettings, }, - } = useSettingsAuthContext() + } = useGlobalStateContext() // Set the default directory if it's been updated // during the loading of the home page. This is wrapped @@ -60,17 +57,11 @@ const Home = () => { useEffect(() => { if (newDefaultDirectory) { sendToSettings({ - type: 'Set All Settings', + type: 'Set Default Directory', data: { defaultDirectory: newDefaultDirectory }, }) } - - // Toast any errors that occurred during the loading process - if (error) { - toast.error(error.message) - } }, []) - useHotkeys( isTauri() ? 'mod+,' : 'shift+mod+,', () => navigate(paths.HOME + paths.SETTINGS), diff --git a/src/routes/Onboarding/Camera.tsx b/src/routes/Onboarding/Camera.tsx index 60a73db7c..0d8c91fd7 100644 --- a/src/routes/Onboarding/Camera.tsx +++ b/src/routes/Onboarding/Camera.tsx @@ -2,7 +2,7 @@ import { OnboardingButtons, useDismiss, useNextClick } from '.' import { onboardingPaths } from 'routes/Onboarding/paths' import { useStore } from '../../useStore' import { SettingsSection } from 'routes/Settings' -import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' +import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { CameraSystem, cameraMouseDragGuards, @@ -22,7 +22,7 @@ export default function Units() { context: { cameraControls }, }, }, - } = useSettingsAuthContext() + } = useGlobalStateContext() return (
diff --git a/src/routes/Onboarding/Introduction.tsx b/src/routes/Onboarding/Introduction.tsx index 01a3bb224..d96b8abcb 100644 --- a/src/routes/Onboarding/Introduction.tsx +++ b/src/routes/Onboarding/Introduction.tsx @@ -5,7 +5,7 @@ import { useNextClick, } from '.' import { onboardingPaths } from 'routes/Onboarding/paths' -import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' +import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { Themes, getSystemTheme } from 'lib/theme' import { bracket } from 'lib/exampleKcl' import { @@ -31,7 +31,7 @@ function OnboardingWithNewFile() { settings: { context: { defaultDirectory }, }, - } = useSettingsAuthContext() + } = useGlobalStateContext() async function createAndOpenNewProject() { const projects = await getProjectsInDir(defaultDirectory) @@ -111,7 +111,7 @@ export default function Introduction() { context: { theme }, }, }, - } = useSettingsAuthContext() + } = useGlobalStateContext() const getLogoTheme = () => theme === Themes.Light || (theme === Themes.System && getSystemTheme() === Themes.Light) diff --git a/src/routes/Onboarding/ParametricModeling.tsx b/src/routes/Onboarding/ParametricModeling.tsx index caefb639d..cb21294ed 100644 --- a/src/routes/Onboarding/ParametricModeling.tsx +++ b/src/routes/Onboarding/ParametricModeling.tsx @@ -3,7 +3,7 @@ import { onboardingPaths } from 'routes/Onboarding/paths' import { useStore } from '../../useStore' import { useBackdropHighlight } from 'hooks/useBackdropHighlight' import { Themes, getSystemTheme } from 'lib/theme' -import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' +import { useGlobalStateContext } from 'hooks/useGlobalStateContext' export default function ParametricModeling() { const { buttonDownInStream } = useStore((s) => ({ @@ -13,7 +13,7 @@ export default function ParametricModeling() { settings: { context: { theme }, }, - } = useSettingsAuthContext() + } = useGlobalStateContext() const getImageTheme = () => theme === Themes.Light || (theme === Themes.System && getSystemTheme() === Themes.Light) diff --git a/src/routes/Onboarding/Units.tsx b/src/routes/Onboarding/Units.tsx index 5cc92b684..e3cf620db 100644 --- a/src/routes/Onboarding/Units.tsx +++ b/src/routes/Onboarding/Units.tsx @@ -1,11 +1,12 @@ import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons' -import { BaseUnit, baseUnits, UnitSystem } from 'lib/settings' +import { BaseUnit, baseUnits } from '../../machines/settingsMachine' import { ActionButton } from '../../components/ActionButton' import { SettingsSection } from '../Settings' import { Toggle } from '../../components/Toggle/Toggle' import { useDismiss, useNextClick } from '.' import { onboardingPaths } from 'routes/Onboarding/paths' -import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' +import { useGlobalStateContext } from 'hooks/useGlobalStateContext' +import { UnitSystem } from 'machines/settingsMachine' export default function Units() { const dismiss = useDismiss() @@ -15,7 +16,7 @@ export default function Units() { send, context: { unitSystem, baseUnit }, }, - } = useSettingsAuthContext() + } = useGlobalStateContext() return (
diff --git a/src/routes/Onboarding/index.tsx b/src/routes/Onboarding/index.tsx index b27987bbe..82da6dd25 100644 --- a/src/routes/Onboarding/index.tsx +++ b/src/routes/Onboarding/index.tsx @@ -5,7 +5,7 @@ import Camera from './Camera' import Sketching from './Sketching' import { useCallback } from 'react' import makeUrlPathRelative from '../../lib/makeUrlPathRelative' -import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' +import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import Streaming from './Streaming' import CodeEditor from './CodeEditor' import ParametricModeling from './ParametricModeling' @@ -78,7 +78,7 @@ export function useNextClick(newStatus: string) { const filePath = useAbsoluteFilePath() const { settings: { send }, - } = useSettingsAuthContext() + } = useGlobalStateContext() const navigate = useNavigate() return useCallback(() => { @@ -94,7 +94,7 @@ export function useDismiss() { const filePath = useAbsoluteFilePath() const { settings: { send }, - } = useSettingsAuthContext() + } = useGlobalStateContext() const navigate = useNavigate() return useCallback(() => { diff --git a/src/routes/Settings.tsx b/src/routes/Settings.tsx index 70bd5af4b..91f5ae601 100644 --- a/src/routes/Settings.tsx +++ b/src/routes/Settings.tsx @@ -5,38 +5,31 @@ import { open } from '@tauri-apps/api/dialog' import { BaseUnit, DEFAULT_PROJECT_NAME, - SETTINGS_PERSIST_KEY, baseUnits, - initialSettings, - UnitSystem, -} from 'lib/settings' +} from '../machines/settingsMachine' import { Toggle } from '../components/Toggle/Toggle' import { useLocation, useNavigate, useRouteLoaderData } from 'react-router-dom' import { useHotkeys } from 'react-hotkeys-hook' import { type IndexLoaderData } from 'lib/types' import { paths } from 'lib/paths' import { Themes } from '../lib/theme' -import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' +import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { CameraSystem, cameraSystems, cameraMouseDragGuards, } from 'lib/cameraControls' +import { UnitSystem } from 'machines/settingsMachine' import { useDotDotSlash } from 'hooks/useDotDotSlash' import { createNewProject, getNextProjectIndex, getProjectsInDir, - getSettingsFilePath, - initializeProjectDirectory, interpolateProjectNameWithIndex, } from 'lib/tauriFS' import { ONBOARDING_PROJECT_NAME } from './Onboarding' import { sep } from '@tauri-apps/api/path' import { bracket } from 'lib/exampleKcl' -import { isTauri } from 'lib/isTauri' -import { invoke } from '@tauri-apps/api' -import toast from 'react-hot-toast' export const Settings = () => { const APP_VERSION = import.meta.env.PACKAGE_VERSION || 'unknown' @@ -62,14 +55,11 @@ export const Settings = () => { }, }, }, - } = useSettingsAuthContext() + } = useGlobalStateContext() async function handleDirectorySelection() { - // the `recursive` property added following - // this advice for permissions: https://github.com/tauri-apps/tauri/issues/4851#issuecomment-1210711455 const newDirectory = await open({ directory: true, - recursive: true, defaultPath: defaultDirectory || paths.INDEX, title: 'Choose a new default directory', }) @@ -316,59 +306,6 @@ export const Settings = () => { Replay Onboarding -

- Your settings are saved in{' '} - {isTauri() - ? 'a file in the app data folder for your OS.' - : "your browser's local storage."}{' '} - {isTauri() ? ( - - - - - ) : ( - - )} -

{/* This uses a Vite plugin, set in vite.config.ts to inject the version from package.json */} diff --git a/src/routes/SignIn.tsx b/src/routes/SignIn.tsx index ef6fb2eb5..e7ab03485 100644 --- a/src/routes/SignIn.tsx +++ b/src/routes/SignIn.tsx @@ -4,7 +4,7 @@ import { invoke } from '@tauri-apps/api/tauri' import { VITE_KC_SITE_BASE_URL, VITE_KC_API_BASE_URL } from '../env' import { Themes, getSystemTheme } from '../lib/theme' import { paths } from 'lib/paths' -import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' +import { useGlobalStateContext } from 'hooks/useGlobalStateContext' import { APP_NAME } from 'lib/constants' const SignIn = () => { @@ -20,7 +20,7 @@ const SignIn = () => { context: { theme }, }, }, - } = useSettingsAuthContext() + } = useGlobalStateContext() const signInTauri = async () => { // We want to invoke our command to login via device auth.