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,146 +1,124 @@
import { type CommandSetConfig } from '../commandTypes'
import {
type BaseUnit,
type Toggle,
UnitSystem,
baseUnitsUnion,
Command,
CommandArgument,
CommandArgumentConfig,
} from '../commandTypes'
import {
SettingsPaths,
SettingsLevel,
SettingProps,
} from 'lib/settings/settingsTypes'
import { settingsMachine } from 'machines/settingsMachine'
import { type CameraSystem, cameraSystems } from '../cameraControls'
import { Themes } from '../theme'
import { PathValue } from 'lib/types'
import { AnyStateMachine, ContextFrom, InterpreterFrom } from 'xstate'
import { getPropertyByPath } from 'lib/objectPropertyByPath'
import { buildCommandArgument } from 'lib/createMachineCommand'
import decamelize from 'decamelize'
import { isTauri } from 'lib/isTauri'
// SETTINGS MACHINE
export type SettingsCommandSchema = {
'Set Base Unit': {
baseUnit: BaseUnit
}
'Set Camera Controls': {
cameraControls: CameraSystem
}
'Set Default Project Name': {
defaultProjectName: string
}
'Set Text Wrapping': {
textWrapping: Toggle
}
'Set Theme': {
theme: Themes
}
'Set Unit System': {
unitSystem: UnitSystem
}
// An array of the paths to all of the settings that have commandConfigs
export const settingsWithCommandConfigs = (
s: ContextFrom<typeof settingsMachine>
) =>
Object.entries(s).flatMap(([categoryName, categorySettings]) =>
Object.entries(categorySettings)
.filter(([_, setting]) => setting.commandConfig !== undefined)
.map(([settingName]) => `${categoryName}.${settingName}`)
) as SettingsPaths[]
const levelArgConfig = <T extends AnyStateMachine = AnyStateMachine>(
actor: InterpreterFrom<T>,
isProjectAvailable: boolean,
hideOnLevel?: SettingsLevel
): CommandArgument<SettingsLevel, T> => ({
inputType: 'options' as const,
required: true,
defaultValue:
isProjectAvailable && hideOnLevel !== 'project' ? 'project' : 'user',
skip: true,
options:
isProjectAvailable && hideOnLevel !== 'project'
? [
{ name: 'User', value: 'user' as SettingsLevel },
{
name: 'Project',
value: 'project' as SettingsLevel,
isCurrent: true,
},
]
: [{ name: 'User', value: 'user' as SettingsLevel, isCurrent: true }],
machineActor: actor,
})
interface CreateSettingsArgs {
type: SettingsPaths
send: Function
context: ContextFrom<typeof settingsMachine>
actor: InterpreterFrom<typeof settingsMachine>
isProjectAvailable: boolean
}
export const settingsCommandBarConfig: CommandSetConfig<
typeof settingsMachine,
SettingsCommandSchema
> = {
'Set Base Unit': {
// 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>
const settingConfig = getPropertyByPath(context, type) as SettingProps<
S['default']
>
const valueArgPartialConfig = settingConfig['commandConfig']
const shouldHideOnThisLevel =
settingConfig?.hideOnLevel === 'user' && !isProjectAvailable
const shouldHideOnThisPlatform =
settingConfig.hideOnPlatform &&
(isTauri()
? settingConfig.hideOnPlatform === 'desktop'
: settingConfig.hideOnPlatform === 'web')
if (
!valueArgPartialConfig ||
shouldHideOnThisLevel ||
shouldHideOnThisPlatform
)
return null
const valueArgConfig = {
...valueArgPartialConfig,
required: true,
} as CommandArgumentConfig<S['default']>
// @ts-ignore - TODO figure out this typing for valueArgConfig
const valueArg = buildCommandArgument(valueArgConfig, context, actor)
const command: Command = {
name: type,
displayName: `Settings · ${decamelize(type.replaceAll('.', ' · '), {
separator: ' ',
})}`,
ownerMachine: 'settings',
icon: 'settings',
args: {
baseUnit: {
inputType: 'options',
required: true,
defaultValueFromContext: (context) => context.baseUnit,
options: [],
optionsFromContext: (context) =>
Object.values(baseUnitsUnion).map((v) => ({
name: v,
value: v,
isCurrent: v === context.baseUnit,
})),
},
needsReview: false,
onSubmit: (data) => {
if (data !== undefined && data !== null) {
send({ type: `set.${type}`, data })
} else {
send(type)
}
},
},
'Set Camera Controls': {
icon: 'settings',
args: {
cameraControls: {
inputType: 'options',
required: true,
defaultValueFromContext: (context) => context.cameraControls,
options: [],
optionsFromContext: (context) =>
Object.values(cameraSystems).map((v) => ({
name: v,
value: v,
isCurrent: v === context.cameraControls,
})),
},
level: levelArgConfig(
actor,
isProjectAvailable,
settingConfig.hideOnLevel
),
value: valueArg,
},
},
'Set Default Project Name': {
icon: 'settings',
hide: 'web',
args: {
defaultProjectName: {
inputType: 'string',
required: true,
defaultValueFromContext: (context) => context.defaultProjectName,
},
},
},
'Set Text Wrapping': {
icon: 'settings',
args: {
textWrapping: {
inputType: 'options',
required: true,
defaultValueFromContext: (context) => context.textWrapping,
options: [],
optionsFromContext: (context) => [
{
name: 'On',
value: 'On' as Toggle,
isCurrent: context.textWrapping === 'On',
},
{
name: 'Off',
value: 'Off' as Toggle,
isCurrent: context.textWrapping === 'Off',
},
],
},
},
},
'Set Theme': {
icon: 'settings',
args: {
theme: {
inputType: 'options',
required: true,
defaultValueFromContext: (context) => context.theme,
options: [],
optionsFromContext: (context) =>
Object.values(Themes).map((v) => ({
name: v,
value: v,
isCurrent: v === context.theme,
})),
},
},
},
'Set Unit System': {
icon: 'settings',
args: {
unitSystem: {
inputType: 'options',
required: true,
defaultValueFromContext: (context) => context.unitSystem,
options: [],
optionsFromContext: (context) => [
{
name: 'Imperial',
value: 'imperial' as UnitSystem,
isCurrent: context.unitSystem === 'imperial',
},
{
name: 'Metric',
value: 'metric' as UnitSystem,
isCurrent: context.unitSystem === 'metric',
},
],
},
},
},
}
return command
}