File based settings (#1679)
* 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 * Fix import paths, etc post-merge * Make default length units `mm` and 'metric' * Rename to SettingsAuth* * cargo fmt * Revert Tauri config identifier change * Update clientSideInfra's baseUnits from settings * Break apart CommandBar and CommandBarProvider * Bugfix: don't validate defaultValue when it's not configured * Allow some TauriFS functions to no-op from browser * Sidestep circular deps by loading context and kclManager only from React-land * Update broken import paths * Separate loaders from Router, load settings on every route * Break apart settings types, utils, and constants * Fix Jest tests by decoupling reliance on useLoaderData from SettingsAuthProvider * Fix up Router loader data with "layout routes" https://reactrouter.com/en/main/route/route#layout-routes * Move settings validation and toast to custom hook so the toast renders * fmt * Use forks for Vitest https://vitest.dev/guide/common-errors.html#failed-to-terminate-worker * $APPCONFIG !== $APPDATA only on Linux + change the identifier back since it really doesn't seem to affect app signing * Debugging on Linux * Better directory validation, fix reset settings button * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * defaultDirectory can be empty in browser * fmt * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * re-trigger CI --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
138
src/lib/routeLoaders.ts
Normal file
138
src/lib/routeLoaders.ts
Normal file
@ -0,0 +1,138 @@
|
||||
import { ActionFunction, LoaderFunction, redirect } from 'react-router-dom'
|
||||
import { HomeLoaderData, IndexLoaderData } from './types'
|
||||
import { isTauri } from './isTauri'
|
||||
import { paths } from './paths'
|
||||
import { BROWSER_FILE_NAME } from 'Router'
|
||||
import { SETTINGS_PERSIST_KEY } from 'lib/constants'
|
||||
import { loadAndValidateSettings } from './settings/settingsUtils'
|
||||
import {
|
||||
getInitialDefaultDir,
|
||||
getProjectsInDir,
|
||||
initializeProjectDirectory,
|
||||
PROJECT_ENTRYPOINT,
|
||||
} from './tauriFS'
|
||||
import makeUrlPathRelative from './makeUrlPathRelative'
|
||||
import { sep } from '@tauri-apps/api/path'
|
||||
import { readDir, readTextFile } from '@tauri-apps/api/fs'
|
||||
import { metadata } from 'tauri-plugin-fs-extra-api'
|
||||
import { kclManager } from 'lang/KclSingleton'
|
||||
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
||||
|
||||
// The root loader simply resolves the settings and any errors that
|
||||
// occurred during the settings load
|
||||
export const indexLoader: LoaderFunction = async (): ReturnType<
|
||||
typeof loadAndValidateSettings
|
||||
> => {
|
||||
return await loadAndValidateSettings()
|
||||
}
|
||||
|
||||
// Redirect users to the appropriate onboarding page if they haven't completed it
|
||||
export const onboardingRedirectLoader: ActionFunction = async ({ request }) => {
|
||||
const { settings } = await loadAndValidateSettings()
|
||||
const onboardingStatus = settings.onboardingStatus || ''
|
||||
const notEnRouteToOnboarding = !request.url.includes(paths.ONBOARDING.INDEX)
|
||||
// '' is the initial state, 'done' and 'dismissed' are the final states
|
||||
const hasValidOnboardingStatus =
|
||||
onboardingStatus.length === 0 ||
|
||||
!(onboardingStatus === 'done' || onboardingStatus === 'dismissed')
|
||||
const shouldRedirectToOnboarding =
|
||||
notEnRouteToOnboarding && hasValidOnboardingStatus
|
||||
|
||||
if (shouldRedirectToOnboarding) {
|
||||
return redirect(
|
||||
makeUrlPathRelative(paths.ONBOARDING.INDEX) + onboardingStatus.slice(1)
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const fileLoader: LoaderFunction = async ({
|
||||
params,
|
||||
}): Promise<IndexLoaderData | Response> => {
|
||||
const { settings } = await loadAndValidateSettings()
|
||||
|
||||
const defaultDir = settings.defaultDirectory || ''
|
||||
|
||||
if (params.id && params.id !== BROWSER_FILE_NAME) {
|
||||
const decodedId = decodeURIComponent(params.id)
|
||||
const projectAndFile = decodedId.replace(defaultDir + sep, '')
|
||||
const firstSlashIndex = projectAndFile.indexOf(sep)
|
||||
const projectName = projectAndFile.slice(0, firstSlashIndex)
|
||||
const projectPath = defaultDir + sep + projectName
|
||||
const currentFileName = projectAndFile.slice(firstSlashIndex + 1)
|
||||
|
||||
if (firstSlashIndex === -1 || !currentFileName)
|
||||
return redirect(
|
||||
`${paths.FILE}/${encodeURIComponent(
|
||||
`${params.id}${sep}${PROJECT_ENTRYPOINT}`
|
||||
)}`
|
||||
)
|
||||
|
||||
// TODO: PROJECT_ENTRYPOINT is hardcoded
|
||||
// until we support setting a project's entrypoint file
|
||||
const code = await readTextFile(decodedId)
|
||||
const entrypointMetadata = await metadata(
|
||||
projectPath + sep + PROJECT_ENTRYPOINT
|
||||
)
|
||||
const children = await readDir(projectPath, { recursive: true })
|
||||
kclManager.setCodeAndExecute(code, false)
|
||||
|
||||
// Set the file system manager to the project path
|
||||
// So that WASM gets an updated path for operations
|
||||
fileSystemManager.dir = projectPath
|
||||
|
||||
return {
|
||||
code,
|
||||
project: {
|
||||
name: projectName,
|
||||
path: projectPath,
|
||||
children,
|
||||
entrypointMetadata,
|
||||
},
|
||||
file: {
|
||||
name: currentFileName,
|
||||
path: params.id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
code: '',
|
||||
}
|
||||
}
|
||||
|
||||
// Loads the settings and by extension the projects in the default directory
|
||||
// and returns them to the Home route, along with any errors that occurred
|
||||
export const homeLoader: LoaderFunction = async (): Promise<
|
||||
HomeLoaderData | Response
|
||||
> => {
|
||||
if (!isTauri()) {
|
||||
return redirect(paths.FILE + '/' + BROWSER_FILE_NAME)
|
||||
}
|
||||
const { settings } = await loadAndValidateSettings()
|
||||
const projectDir = await initializeProjectDirectory(
|
||||
settings.defaultDirectory || (await getInitialDefaultDir())
|
||||
)
|
||||
|
||||
if (projectDir.path) {
|
||||
if (projectDir.path !== settings.defaultDirectory) {
|
||||
localStorage.setItem(
|
||||
SETTINGS_PERSIST_KEY,
|
||||
JSON.stringify({
|
||||
...settings,
|
||||
defaultDirectory: projectDir,
|
||||
})
|
||||
)
|
||||
}
|
||||
const projects = await getProjectsInDir(projectDir.path)
|
||||
|
||||
return {
|
||||
projects,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
projects: [],
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user