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:
Frank Noirot
2024-04-02 10:29:34 -04:00
committed by GitHub
parent 77f51530f9
commit d605d4a029
67 changed files with 2470 additions and 1392 deletions

View File

@ -1,148 +1,86 @@
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 { DEFAULT_PROJECT_NAME, SETTINGS_PERSIST_KEY } from 'lib/constants'
import { createSettings, settings } from 'lib/settings/initialSettings'
import {
UnitSystem,
type BaseUnit,
type SettingsMachineContext,
type Toggle,
BaseUnit,
SetEventTypes,
SettingsLevel,
SettingsPaths,
WildcardSetEvent,
} from 'lib/settings/settingsTypes'
export const settingsMachine = createMachine(
{
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlTQAIBBY4qyrXWAbQAYBdRUABwHtYmLP2w8QAD0TsANCACe0gL5K5THHkIlylKgCEAhrDBUAqtmEduSEAKEixNqQgCM7AJz52AJgAsAVg8AZgAOEIA2fxd3XzlFBCCXf3xfdlS0kN9vGIiVNXRmTSJSCnQqAGEDAFswACcDCtE0Wv5iNi5xO2FMUXFnWQVlVRB1Fi0S3QARMAAzAwBXYmpJzFqwAGM0flr5K07Bbt6nRH9w-HcXcPcI8PYAdgu0oLiTu+98EPdQ0-8g8N8gu53HkRgUNARijoytM5otqAAFFoAKw21AActUwHsbF0HH1EFkvOxiSTScSXLFBggrsk7r4AuEQuxAd4oiEQaMitpStQAPLYABG-AMtQgGkYaAMaHm7WsfAOeOOCEC+HCiTevlu5JcYReCBCLhSFzc3m8SWJrJcHLBY0hPKoABUwBJqAB1eq8XgabHy+w9Rygfp69jWjDg8ZQ6gOgAWYBqPtsCv9+P17Hw3juIV+Pn87kiGeeVINIXwuf8rPC4WiVZcQVDhQh3N05mEjHksDQcYTuOTSrp+Du5ZC3g8bizbkp8QCaaelwep3YTP8vnr4btDv4UCgpCo0wF8ygVHhBmwYGI3aTR0DiFupfY-giQSC3iflZfepHvnwQV8Lge93cX4qxCO4VGGbB+AgOBxE5eAcUvANJEQABaXwQj1ZCQLvUkmXpFwzStYZYIjfY-SvJDXBHLxa01Stc0yIE7j1NwKW-NUAl8a4-DuZkwKUIA */
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlnXwFsB7CMYnKfAV20zVgG0AGAXUSgADrVidMtbEJAAPRABYAHAFZ8vAEwA2FVq0aNAZgCcARl0B2ADQgAnolO8LvfBYUXT+48YW+tCgF8Am1QMZgIiUgoqAENhYXw0AAswajA+QSQQUXEsKRl5BCM1DQUDMo0VQwVjJxt7BFMDJXwNWo0LFSU3c0DgkFCsXAiScgAlOHQAAkow4YyZHIl8rMKAWn18Q39q3ScVFQ1NFXqHXiVTfGNqhSdeXi1DJQ1TIJD0IbxCUbIAKgWsks8tJVopjFp8KZjEpqi9lKZDE0TnYzgZXO5PG0fH4+u85l9IuRQlMYsRiFNsGAAO4zD7hAEiMTLEGgda6fAKJoIiyIkwWYwWawoxpGFxaHkKFS1a7woL9bD0OAyQbhRZM4EFRBrUyOLY7SVafaHTSnBDGDqQiz6DRKbwqTxvAZ04bfUhq3KSFlyBxHdQIhR6HTQmoC02OUwKPW7GrPdy8QxygJAA */
id: 'Settings',
predictableActionArguments: true,
context: {} as SettingsMachineContext,
context: {} as ReturnType<typeof createSettings>,
initial: 'idle',
states: {
idle: {
entry: ['setThemeClass', 'setClientSideSceneUnits', 'persistSettings'],
on: {
'Set All Settings': {
actions: [
assign((context, event) => {
return {
...context,
...event.data,
}
}),
'persistSettings',
'setThemeClass',
],
'*': {
target: 'idle',
internal: true,
actions: ['setSettingAtLevel', 'toastSuccess', 'persistSettings'],
},
'Set Base Unit': {
'set.app.onboardingStatus': {
target: 'idle',
internal: true,
actions: ['setSettingAtLevel', 'persistSettings'], // No toast
},
'set.modeling.defaultUnit': {
target: 'idle',
internal: true,
actions: [
assign({
baseUnit: (_, event) => event.data.baseUnit,
}),
'persistSettings',
'setSettingAtLevel',
'toastSuccess',
'setClientSideSceneUnits',
'Execute AST',
],
target: 'idle',
internal: true,
},
'Set Camera Controls': {
actions: [
assign({
cameraControls: (_, event) => event.data.cameraControls,
}),
'persistSettings',
'toastSuccess',
],
target: 'idle',
internal: true,
},
'Set Default Directory': {
actions: [
assign({
defaultDirectory: (_, event) => event.data.defaultDirectory,
}),
'persistSettings',
'toastSuccess',
],
target: 'idle',
internal: true,
},
'Set Default Project Name': {
actions: [
assign({
defaultProjectName: (_, event) =>
event.data.defaultProjectName.trim() || DEFAULT_PROJECT_NAME,
}),
'persistSettings',
'toastSuccess',
],
target: 'idle',
internal: true,
},
'Set Onboarding Status': {
actions: [
assign({
onboardingStatus: (_, event) => event.data.onboardingStatus,
}),
'persistSettings',
],
},
'set.app.theme': {
target: 'idle',
internal: true,
},
'Set Text Wrapping': {
actions: [
assign({
textWrapping: (_, event) => event.data.textWrapping,
}),
'persistSettings',
'toastSuccess',
],
target: 'idle',
internal: true,
},
'Set Theme': {
actions: [
assign({
theme: (_, event) => event.data.theme,
}),
'persistSettings',
'setSettingAtLevel',
'toastSuccess',
'setThemeClass',
'setEngineTheme',
'persistSettings',
],
},
'Reset settings': {
target: 'idle',
internal: true,
},
'Set Unit System': {
actions: [
assign({
unitSystem: (_, event) => event.data.unitSystem,
baseUnit: (_, event) =>
event.data.unitSystem === 'imperial' ? 'in' : 'mm',
}),
'persistSettings',
'toastSuccess',
'resetSettings',
'setThemeClass',
'setEngineTheme',
'setClientSideSceneUnits',
'Execute AST',
],
target: 'idle',
internal: true,
},
'Toggle Debug Panel': {
actions: [
assign({
showDebugPanel: (context) => {
return !context.showDebugPanel
},
}),
'persistSettings',
'toastSuccess',
],
},
'Set all settings': {
target: 'idle',
internal: true,
actions: [
'setAllSettings',
'setThemeClass',
'setEngineTheme',
'setClientSideSceneUnits',
'Execute AST',
'persistSettings',
],
},
},
},
@ -150,44 +88,61 @@ export const settingsMachine = createMachine(
tsTypes: {} as import('./settingsMachine.typegen').Typegen0,
schema: {
events: {} as
| { type: 'Set All Settings'; data: Partial<SettingsMachineContext> }
| { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } }
| WildcardSetEvent<SettingsPaths>
| SetEventTypes
| {
type: 'Set Camera Controls'
data: { cameraControls: CameraSystem }
type: 'set.app.theme'
data: { level: SettingsLevel; value: Themes }
}
| { type: 'Set Default Directory'; data: { defaultDirectory: string } }
| {
type: 'Set Default Project Name'
data: { defaultProjectName: string }
type: 'set.modeling.units'
data: { level: SettingsLevel; value: BaseUnit }
}
| { type: 'Set Onboarding Status'; data: { onboardingStatus: string } }
| { type: 'Set Text Wrapping'; data: { textWrapping: Toggle } }
| { type: 'Set Theme'; data: { theme: Themes } }
| {
type: 'Set Unit System'
data: { unitSystem: UnitSystem }
}
| { type: 'Toggle Debug Panel' },
| { type: 'Reset settings'; defaultDirectory: string }
| { type: 'Set all settings'; settings: typeof settings },
},
},
{
actions: {
persistSettings: (context) => {
if (isTauri()) {
writeToSettingsFile(context).catch((err) => {
console.error('Error writing settings:', err)
})
resetSettings: assign((context, { defaultDirectory }) => {
// Reset everything except onboarding status,
// which should be preserved
const newSettings = createSettings()
if (context.app.onboardingStatus.user) {
newSettings.app.onboardingStatus.user =
context.app.onboardingStatus.user
}
try {
localStorage.setItem(SETTINGS_PERSIST_KEY, JSON.stringify(context))
} catch (e) {
console.error(e)
// We instead pass in the default directory since it's asynchronous
// to re-initialize, and that can be done by the caller.
newSettings.app.projectDirectory.default = defaultDirectory
return newSettings
}),
setAllSettings: assign((_, event) => {
return event.settings
}),
setSettingAtLevel: assign((context, event) => {
const { level, value } = event.data
const [category, setting] = event.type
.replace(/^set./, '')
.split('.') as [keyof typeof settings, string]
// @ts-ignore
context[category][setting][level] = value
const newContext = {
...context,
[category]: {
...context[category],
// @ts-ignore
[setting]: context[category][setting],
},
}
},
setThemeClass: (context, event) => {
const currentTheme =
event.type === 'Set Theme' ? event.data.theme : context.theme
return newContext
}),
setThemeClass: (context) => {
const currentTheme = context.app.theme.current ?? Themes.System
setThemeClass(
currentTheme === Themes.System ? getSystemTheme() : currentTheme
)