Rearchitect settings system to be scoped (#1956)
* BROKEN: start of scopes for each setting * Clean up later: mostly-functional scoped settings! Broken command bar, unimplemented generated settings components * Working persisted project settings in-folder * Start working toward automatic commands and settings UI * Relatively stable, settings-menu-editable * Settings persistence tweaks after merge * Custom settings UI working properly, cleaner types * Allow boolean command types, create Settings UI for them * Add support for option and string Settings input types * Proof of concept settings from command bar * Add all settings to command bar * Allow settings to be hidden on a level * Better command titles for settings * Hide the settings the settings from the commands bar * Derive command defaultValue from *current* settingsMachine context * Fix generated settings UI for 'options' type settings * Pretty settings modal 💅 * Allow for rollback to parent level setting * fmt * Fix tsc errors not related to loading from localStorage * Better setting descriptions, better buttons * Make displayName searchable in command bar * Consolidate constants, get working in browser * Start fixing tests, better types for saved settings payloads * Fix playwright tests * Add a test for the settings modal * Add AtLeast to codespell ignore list * Goofed merge of codespellrc * Try fixing linux E2E tests * Make codespellrc word lowercase * fmt * Fix data-testid in Tauri test * Don't set text settings if nothing changed * Turn off unimplemented settings * Allow for multiple "execution-done" messages to have appeared in snapshot tests * Try fixing up snapshot tests * Switch from .json to .toml settings file format * Use a different method for overriding the default units * Try to force using the new common storage state in snapshot tests * Update tests to use TOML * fmt and remove console logs * Restore units to export * tsc errors, make snapshot tests use TOML * Ensure that snapshot tests use the basicStorageState * Re-organize use of test.use() * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * Update snapshots one more time since lighting changed * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * Fix broken "Show in folder" for project-level settings * Fire all relevant actions after settings reset * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * Properly reset the default directory * Hide settings by platform * Actually honor showDebugPanel * Unify settings hiding logic * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * fix first extrusion snapshot * another attempt to fix extrustion snapshot * Rerun test suite * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * trigger CI * more extrusion stuff * Replace resetSettings console log with comment --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
This commit is contained in:
@ -6,12 +6,9 @@ import withBaseUrl from '../lib/withBaseURL'
|
||||
import React, { createContext, useEffect } from 'react'
|
||||
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
||||
import { settingsMachine } from 'machines/settingsMachine'
|
||||
import {
|
||||
fallbackLoadedSettings,
|
||||
validateSettings,
|
||||
} from 'lib/settings/settingsUtils'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { getThemeColorForEngine, setThemeClass, Themes } from 'lib/theme'
|
||||
import decamelize from 'decamelize'
|
||||
import {
|
||||
AnyStateMachine,
|
||||
ContextFrom,
|
||||
@ -20,10 +17,19 @@ import {
|
||||
StateFrom,
|
||||
} from 'xstate'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { settingsCommandBarConfig } from 'lib/commandBarConfigs/settingsCommandConfig'
|
||||
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
|
||||
import { kclManager, sceneInfra, engineCommandManager } from 'lib/singletons'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { IndexLoaderData } from 'lib/types'
|
||||
import { settings } from 'lib/settings/initialSettings'
|
||||
import {
|
||||
createSettingsCommand,
|
||||
settingsWithCommandConfigs,
|
||||
} from 'lib/commandBarConfigs/settingsCommandConfig'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { Command } from 'lib/commandTypes'
|
||||
import { BaseUnit } from 'lib/settings/settingsTypes'
|
||||
import { saveSettings } from 'lib/settings/settingsUtils'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -49,11 +55,13 @@ export const SettingsAuthProvider = ({
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const loadedSettings = useRouteLoaderData(paths.INDEX) as Awaited<
|
||||
ReturnType<typeof validateSettings>
|
||||
>
|
||||
const loadedSettings = useRouteLoaderData(paths.INDEX) as typeof settings
|
||||
const loadedProject = useRouteLoaderData(paths.FILE) as IndexLoaderData
|
||||
return (
|
||||
<SettingsAuthProviderBase loadedSettings={loadedSettings}>
|
||||
<SettingsAuthProviderBase
|
||||
loadedSettings={loadedSettings}
|
||||
loadedProject={loadedProject}
|
||||
>
|
||||
{children}
|
||||
</SettingsAuthProviderBase>
|
||||
)
|
||||
@ -66,7 +74,7 @@ export const SettingsAuthProviderJest = ({
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const loadedSettings = fallbackLoadedSettings
|
||||
const loadedSettings = settings
|
||||
return (
|
||||
<SettingsAuthProviderBase loadedSettings={loadedSettings}>
|
||||
{children}
|
||||
@ -77,23 +85,25 @@ export const SettingsAuthProviderJest = ({
|
||||
export const SettingsAuthProviderBase = ({
|
||||
children,
|
||||
loadedSettings,
|
||||
loadedProject,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
loadedSettings: Awaited<ReturnType<typeof validateSettings>>
|
||||
loadedSettings: typeof settings
|
||||
loadedProject?: IndexLoaderData
|
||||
}) => {
|
||||
const { settings: initialLoadedContext } = loadedSettings
|
||||
const navigate = useNavigate()
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
|
||||
const [settingsState, settingsSend, settingsActor] = useMachine(
|
||||
settingsMachine,
|
||||
{
|
||||
context: initialLoadedContext,
|
||||
context: loadedSettings,
|
||||
actions: {
|
||||
setClientSideSceneUnits: (context, event) => {
|
||||
const newBaseUnit =
|
||||
event.type === 'Set Base Unit'
|
||||
? event.data.baseUnit
|
||||
: context.baseUnit
|
||||
event.type === 'set.modeling.defaultUnit'
|
||||
? (event.data.value as BaseUnit)
|
||||
: context.modeling.defaultUnit.current
|
||||
sceneInfra.baseUnit = newBaseUnit
|
||||
},
|
||||
setEngineTheme: (context) => {
|
||||
@ -102,39 +112,76 @@ export const SettingsAuthProviderBase = ({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'set_background_color',
|
||||
color: getThemeColorForEngine(context.theme),
|
||||
color: getThemeColorForEngine(context.app.theme.current),
|
||||
},
|
||||
})
|
||||
},
|
||||
toastSuccess: (context, event) => {
|
||||
const truncatedNewValue =
|
||||
'data' in event && event.data instanceof Object
|
||||
? (context[Object.keys(event.data)[0] as keyof typeof context]
|
||||
.toString()
|
||||
.substring(0, 28) as any)
|
||||
: undefined
|
||||
toast.success(
|
||||
event.type +
|
||||
(truncatedNewValue
|
||||
? ` to "${truncatedNewValue}${
|
||||
truncatedNewValue.length === 28 ? '...' : ''
|
||||
}"`
|
||||
: '')
|
||||
)
|
||||
const eventParts = event.type.replace(/^set./, '').split('.') as [
|
||||
keyof typeof settings,
|
||||
string
|
||||
]
|
||||
const truncatedNewValue = event.data.value?.toString().slice(0, 28)
|
||||
const message =
|
||||
`Set ${decamelize(eventParts[1], { separator: ' ' })}` +
|
||||
(truncatedNewValue
|
||||
? ` to "${truncatedNewValue}${
|
||||
truncatedNewValue.length === 28 ? '...' : ''
|
||||
}"${
|
||||
event.data.level === 'project'
|
||||
? ' for this project'
|
||||
: ' as a user default'
|
||||
}`
|
||||
: '')
|
||||
toast.success(message, {
|
||||
duration: message.split(' ').length * 100 + 1500,
|
||||
})
|
||||
},
|
||||
'Execute AST': () => kclManager.executeAst(),
|
||||
persistSettings: (context) =>
|
||||
saveSettings(context, loadedProject?.project?.path),
|
||||
},
|
||||
}
|
||||
)
|
||||
settingsStateRef = settingsState.context
|
||||
|
||||
useStateMachineCommands({
|
||||
machineId: 'settings',
|
||||
state: settingsState,
|
||||
send: settingsSend,
|
||||
commandBarConfig: settingsCommandBarConfig,
|
||||
actor: settingsActor,
|
||||
})
|
||||
// Add settings commands to the command bar
|
||||
// They're treated slightly differently than other commands
|
||||
// Because their state machine doesn't have a meaningful .nextEvents,
|
||||
// and they are configured statically in initialiSettings
|
||||
useEffect(() => {
|
||||
// If the user wants to hide the settings commands
|
||||
//from the command bar don't add them.
|
||||
if (settingsState.context.commandBar.includeSettings.current === false)
|
||||
return
|
||||
|
||||
const commands = settingsWithCommandConfigs(settingsState.context)
|
||||
.map((type) =>
|
||||
createSettingsCommand({
|
||||
type,
|
||||
send: settingsSend,
|
||||
context: settingsState.context,
|
||||
actor: settingsActor,
|
||||
isProjectAvailable: loadedProject !== undefined,
|
||||
})
|
||||
)
|
||||
.filter((c) => c !== null) as Command[]
|
||||
|
||||
commandBarSend({ type: 'Add commands', data: { commands: commands } })
|
||||
|
||||
return () => {
|
||||
commandBarSend({
|
||||
type: 'Remove commands',
|
||||
data: { commands },
|
||||
})
|
||||
}
|
||||
}, [
|
||||
settingsState,
|
||||
settingsSend,
|
||||
settingsActor,
|
||||
commandBarSend,
|
||||
settingsWithCommandConfigs,
|
||||
])
|
||||
|
||||
// Listen for changes to the system theme and update the app theme accordingly
|
||||
// This is only done if the theme setting is set to 'system'.
|
||||
@ -144,7 +191,7 @@ export const SettingsAuthProviderBase = ({
|
||||
useEffect(() => {
|
||||
const matcher = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const listener = (e: MediaQueryListEvent) => {
|
||||
if (settingsState.context.theme !== 'system') return
|
||||
if (settingsState.context.app.theme.current !== 'system') return
|
||||
setThemeClass(e.matches ? Themes.Dark : Themes.Light)
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user