[Refactor] decouple settingsMachine from React (#5142)

* Remove unnecessary console.log

* Create a global appMachine

* Strip authMachine of side-effects

* Replace react-bound authMachine use with XState actor use

* Fix import goof

* Register auth commands directly!

* Don't provide anything to settingsMachine from React

* Remove unecessary async

* Make it possible to load project settings via a sent event, without React

* Make settingsMachine ready to be an actor

* Remove settingsLoader use

* Replace all useSettingsAuthContext use with direct actor use

* Add logic to clear project settings, fmt

* fmt

* Clear and load project settings from routeLoaders, but using actor

* Remove useRefreshSettings

* Restore use of useToken() that wasn't working for some reason

* Migrate useFileSystemWatcher use to RouteProvider

* Surface wasm_bindgen unavailable error to console

* Remove unnecessary use of Jest settings wrappers

* Replace dynamic import with actor.getSnapshot

* Migrate system theme and theme color watching from useEffects to actors/actions

* Migrate cursor color effect

* Remove unused code that is now in RouteProvider

* Migrate route commands registration further down for now, out of SettingsAuthProvider

* Migrate settings command registration out of SettingsAuthProvider.tsx

* Delete SettingsAuthProvider.tsx!

* Remove unused settingsLoader!

* fmt and remove comments

* Use actor for routeLoader

* Fix project read error due to uninitialized WASM

* Fix user settings load error due to uninitialized WASM

* Move settingsActor into appActor as a spawned child

* Trying to fix unit tests

* Remove unused imports and demo window attachments

* fmt

* Fix testing issues caused by circular dependency

* Add `setThemeColor` to a few actions list it was missing from

* fmt

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Fix "Execute AST" action in browser, where currentProject is `undefined`

* Update commands list when currentProject changes

* Fix `clearProjectSettings`, which was passing along non-settings context

* Fix onboarding test that actually needed the onboarding initially dismissed

* Add scrollIntoView to make this test more reliable

* @lf94's feedback I missed

I got distracted by a million other things last week

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)"

This reverts commit 129226c6ef.

* fmt

* revert bad snapshot

* Fix up camera movement test locator

* Fix test that was flipping the user settings without waiting

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Frank Noirot
2025-02-21 13:47:36 -05:00
committed by GitHub
parent 4d1eaf9381
commit 46b4b01d23
60 changed files with 791 additions and 780 deletions

View File

@ -7,20 +7,23 @@ import {
SettingsPaths,
SettingsLevel,
SettingProps,
SetEventTypes,
} from 'lib/settings/settingsTypes'
import { settingsMachine } from 'machines/settingsMachine'
import { PathValue } from 'lib/types'
import { Actor, AnyStateMachine, ContextFrom } from 'xstate'
import { ActorRefFrom, AnyStateMachine } from 'xstate'
import { getPropertyByPath } from 'lib/objectPropertyByPath'
import { buildCommandArgument } from 'lib/createMachineCommand'
import decamelize from 'decamelize'
import { isDesktop } from 'lib/isDesktop'
import { Setting } from 'lib/settings/initialSettings'
import {
createSettings,
Setting,
SettingsType,
} from 'lib/settings/initialSettings'
// An array of the paths to all of the settings that have commandConfigs
export const settingsWithCommandConfigs = (
s: ContextFrom<typeof settingsMachine>
) =>
export const settingsWithCommandConfigs = (s: SettingsType) =>
Object.entries(s).flatMap(([categoryName, categorySettings]) =>
Object.entries(categorySettings)
.filter(([_, setting]) => setting.commandConfig !== undefined)
@ -28,7 +31,7 @@ export const settingsWithCommandConfigs = (
) as SettingsPaths[]
const levelArgConfig = <T extends AnyStateMachine = AnyStateMachine>(
actor: Actor<T>,
actor: ActorRefFrom<T>,
isProjectAvailable: boolean,
hideOnLevel?: SettingsLevel
): CommandArgument<SettingsLevel, T> => ({
@ -53,23 +56,16 @@ const levelArgConfig = <T extends AnyStateMachine = AnyStateMachine>(
interface CreateSettingsArgs {
type: SettingsPaths
send: Function
context: ContextFrom<typeof settingsMachine>
actor: Actor<typeof settingsMachine>
isProjectAvailable: boolean
actor: ActorRefFrom<typeof settingsMachine>
}
// Takes a Setting with a commandConfig and creates a Command
// that can be used in the CommandBar component.
export function createSettingsCommand({
type,
send,
context,
actor,
isProjectAvailable,
}: CreateSettingsArgs) {
type S = PathValue<typeof context, typeof type>
export function createSettingsCommand({ type, actor }: CreateSettingsArgs) {
type S = PathValue<ReturnType<typeof createSettings>, typeof type>
const context = actor.getSnapshot().context
const isProjectAvailable = context.currentProject !== undefined
const settingConfig = getPropertyByPath(context, type) as SettingProps<
S['default']
>
@ -129,10 +125,18 @@ export function createSettingsCommand({
icon: 'settings',
needsReview: false,
onSubmit: (data) => {
if (data !== undefined && data !== null) {
send({ type: `set.${type}`, data })
if (
data !== undefined &&
data !== null &&
'value' in data &&
'level' in data
) {
// TS would not let me get this to type properly
const coercedData = data as unknown as SetEventTypes['data']
actor.send({ type: `set.${type}`, data: coercedData })
} else {
send({ type })
console.error('Invalid data submitted to settings command', data)
return new Error('Invalid data submitted to settings command', data)
}
},
args: {

View File

@ -4,6 +4,7 @@ import { Project, FileEntry } from 'lib/project'
import {
defaultAppSettings,
initPromise,
parseAppSettings,
parseProjectSettings,
} from 'lang/wasm'
@ -131,11 +132,20 @@ export async function createNewProjectDirectory(
export async function listProjects(
configuration?: DeepPartial<Configuration> | Error
): Promise<Project[]> {
if (configuration === undefined) {
configuration = await readAppSettingsFile()
// Make sure we have wasm initialized.
const initializedResult = await initPromise
if (err(initializedResult)) {
return Promise.reject(initializedResult)
}
if (err(configuration)) return Promise.reject(configuration)
if (configuration === undefined) {
configuration = await readAppSettingsFile().catch((e) => {
console.error(e)
return e
})
}
if (err(configuration) || !configuration) return Promise.reject(configuration)
const projectDir = await ensureProjectDirectoryExists(configuration)
const projects = []
if (!projectDir) return Promise.reject(new Error('projectDir was falsey'))

View File

@ -75,11 +75,11 @@ export async function getProjectMetaByRouteId(
return route
}
export async function parseProjectRoute(
export function parseProjectRoute(
configuration: DeepPartial<Configuration>,
id: string,
pathlib: PlatformPath | undefined
): Promise<ProjectRoute> {
): ProjectRoute {
let projectName = null
let projectPath = ''
let currentFileName = null

View File

@ -13,37 +13,9 @@ import makeUrlPathRelative from './makeUrlPathRelative'
import { codeManager } from 'lib/singletons'
import { fileSystemManager } from 'lang/std/fileSystemManager'
import { getProjectInfo } from './desktop'
import { createSettings } from './settings/initialSettings'
import { normalizeLineEndings } from 'lib/codeEditor'
import { OnboardingStatus } from 'wasm-lib/kcl/bindings/OnboardingStatus'
// The root loader simply resolves the settings and any errors that
// occurred during the settings load
export const settingsLoader: LoaderFunction = async ({
params,
}): Promise<
ReturnType<typeof createSettings> | ReturnType<typeof redirect>
> => {
let { settings, configuration } = await loadAndValidateSettings()
// I don't love that we have to read the settings again here,
// but we need to get the project path to load the project settings
if (params.id) {
const projectPathData = await getProjectMetaByRouteId(
params.id,
configuration
)
if (projectPathData) {
const { projectPath } = projectPathData
const { settings: s } = await loadAndValidateSettings(
projectPath || undefined
)
return s
}
}
return settings
}
import { getSettings, settingsActor } from 'machines/appMachine'
export const telemetryLoader: LoaderFunction = async ({
params,
@ -53,7 +25,7 @@ export const telemetryLoader: LoaderFunction = async ({
// Redirect users to the appropriate onboarding page if they haven't completed it
export const onboardingRedirectLoader: ActionFunction = async (args) => {
const { settings } = await loadAndValidateSettings()
const settings = getSettings()
const onboardingStatus: OnboardingStatus =
settings.app.onboardingStatus.current || ''
const notEnRouteToOnboarding = !args.request.url.includes(
@ -72,7 +44,7 @@ export const onboardingRedirectLoader: ActionFunction = async (args) => {
)
}
return settingsLoader(args)
return null
}
export const fileLoader: LoaderFunction = async (
@ -156,9 +128,17 @@ export const fileLoader: LoaderFunction = async (
? await getProjectInfo(projectPath)
: null
const project = maybeProjectInfo ?? defaultProjectData
// Fire off the event to load the project settings
settingsActor.send({
type: 'load.project',
project,
})
const projectData: IndexLoaderData = {
code,
project: maybeProjectInfo ?? defaultProjectData,
project,
file: {
name: currentFileName || '',
path: currentFilePath || '',
@ -197,5 +177,8 @@ export const homeLoader: LoaderFunction = async ({
PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME + (url.search || '')
)
}
settingsActor.send({
type: 'clear.project',
})
return {}
}

View File

@ -554,3 +554,4 @@ export function createSettings() {
}
export const settings = createSettings()
export type SettingsType = ReturnType<typeof createSettings>

View File

@ -1,7 +1,3 @@
import { Setting, createSettings, settings } from 'lib/settings/initialSettings'
import { SaveSettingsPayload, SettingsLevel } from './settingsTypes'
import { isDesktop } from 'lib/isDesktop'
import { err } from 'lib/trap'
import {
defaultAppSettings,
defaultProjectSettings,
@ -10,9 +6,8 @@ import {
parseProjectSettings,
tomlStringify,
} from 'lang/wasm'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { mouseControlsToCameraSystem } from 'lib/cameraControls'
import { appThemeToTheme } from 'lib/theme'
import { BROWSER_PROJECT_NAME } from 'lib/constants'
import {
getInitialDefaultDir,
readAppSettingsFile,
@ -20,9 +15,14 @@ import {
writeAppSettingsFile,
writeProjectSettingsFile,
} from 'lib/desktop'
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
import { BROWSER_PROJECT_NAME } from 'lib/constants'
import { isDesktop } from 'lib/isDesktop'
import { Setting, createSettings, settings } from 'lib/settings/initialSettings'
import { appThemeToTheme } from 'lib/theme'
import { err } from 'lib/trap'
import { DeepPartial } from 'lib/types'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
import { SaveSettingsPayload, SettingsLevel } from './settingsTypes'
/**
* Convert from a rust settings struct into the JS settings struct.
@ -312,6 +312,22 @@ export function getAllCurrentSettings(
return currentSettings
}
export function clearSettingsAtLevel(
allSettings: typeof settings,
level: SettingsLevel
) {
Object.entries(allSettings).forEach(([category, settingsCategory]) => {
const categoryKey = category as keyof typeof settings
Object.entries(settingsCategory).forEach(
([_, settingValue]: [string, Setting]) => {
settingValue[level] = undefined
}
)
})
return allSettings
}
export function setSettingsAtLevel(
allSettings: typeof settings,
level: SettingsLevel,