[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:
@ -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: {
|
||||
|
||||
@ -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'))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {}
|
||||
}
|
||||
|
||||
@ -554,3 +554,4 @@ export function createSettings() {
|
||||
}
|
||||
|
||||
export const settings = createSettings()
|
||||
export type SettingsType = ReturnType<typeof createSettings>
|
||||
|
||||
@ -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,
|
||||
|
||||
Reference in New Issue
Block a user