1
0
Fork 0

document new shared menus

This commit is contained in:
Enoch Riese 2023-05-31 17:42:16 -05:00
parent a8891d6091
commit b975355f45
9 changed files with 285 additions and 84 deletions

View file

@ -58,6 +58,15 @@ const inputs = {
export const ns = ['core-settings', 'modal']
/**
* The core settings menu
* @param {Object} options.update settings and ui update functions
* @param {Object} options.settings core settings
* @param {Object} options.patternConfig the configuration from the pattern
* @param {String} options.language the menu language
* @param {Object} options.account the user account data
* @param {Boolean|React.Com options.DynamicDocs A docs component
*/
export const CoreSettings = ({
update,
settings,

View file

@ -10,19 +10,29 @@ export const PaperlessSettingInput = ListInput
export const MarginSettingInput = MmInput
export const ScaleSettingInput = SliderInput
/** an input for the 'only' setting. toggles individual parts*/
export const OnlySettingInput = (props) => {
const { config, updateFunc, current } = props
// set up choice titles
config.choiceTitles = {}
config.list.forEach((p) => (config.choiceTitles[p] = p))
// make an update function that toggles the parts
const onlyUpdateFunc = useCallback(
(path, part) => {
// if there's no part being set, it's a reset
if (part === undefined) return updateFunc(path, part)
// add or remove the part from the set
let newParts = new Set(current || [])
if (newParts.has(part)) newParts.delete(part)
else newParts.add(part)
// if the set is now empty, reset
if (newParts.size < 1) newParts = undefined
// otherwise use the new set
else newParts = [...newParts]
updateFunc(path, newParts)
@ -33,11 +43,16 @@ export const OnlySettingInput = (props) => {
return <ListInput {...props} updateFunc={onlyUpdateFunc} />
}
/** An input for the samm setting */
export const SaMmSettingInput = (props) => {
const { updateFunc, units, config } = props
// the update function to switch the 'sa' setting along with samm
const mmUpdateFunc = useCallback(
(_path, newCurrent) => {
// convert to millimeters if there's a value
newCurrent = newCurrent === undefined ? measurementAsMm(config.dflt, units) : newCurrent
// update both values to match
updateFunc([
[['samm'], newCurrent],
[['sa'], newCurrent],
@ -56,14 +71,20 @@ export const SaMmSettingInput = (props) => {
)
}
/** An input for the sabool setting */
export const SaBoolSettingInput = (props) => {
const { updateFunc, samm } = props
// the update function to toggle the 'sa' setting based on 'sabool'
const saUpdateFunc = useCallback(
(_path, newCurrent) =>
(_path, newCurrent) => {
updateFunc([
// update sabool to the new current
[['sabool'], newCurrent],
// set sa based on whether there's a current value or not
[['sa'], newCurrent ? samm : undefined],
]),
])
},
[updateFunc, samm]
)

View file

@ -1,7 +1,7 @@
// Components
import { OptionsIcon } from 'shared/components/icons.mjs'
import { optionsMenuStructure } from 'shared/utils.mjs'
import { optionType } from 'shared/utils.mjs'
import { optionType, formatMm } from 'shared/utils.mjs'
import {
BoolInput,
ConstantInput,
@ -24,6 +24,22 @@ import { MenuItem } from '../shared/menu-item.mjs'
export const ns = ['design-options']
const PctOptionInput = (props) => {
const { config, settings, changed } = props
const currentOrDefault = changed ? props.current : config.dflt / 100
return (
<PctInput {...props}>
<div className="flex flex-row justify-around">
<span className={changed ? 'text-accent' : 'text-secondary'}>
{config.toAbs && settings.measurements
? formatMm(config.toAbs(currentOrDefault, settings))
: ' '}
</span>
</div>
</PctInput>
)
}
// Facilitate lookup of the input component
const inputs = {
bool: BoolInput,
@ -32,7 +48,7 @@ const inputs = {
deg: DegInput,
list: ListInput,
mm: () => <span>FIXME: Mm options are deprecated. Please report this </span>,
pct: PctInput,
pct: PctOptionInput,
}
// Facilitate lookup of the value component
@ -55,16 +71,13 @@ const emojis = {
groupDflt: '📁',
}
export const DesignOption = ({
name,
current,
config,
settings,
updateFunc,
t,
loadDocs,
changed = false,
}) => {
/**
* A wrapper for {@see MenuItem} to handle design option-specific business
* @param {Object} options.config the config for the item
* @param {Object} options.settings core settings
* @param {Object} options.rest the rest of the props
*/
export const DesignOption = ({ config, settings, ...rest }) => {
const type = optionType(config)
const Input = inputs[type]
const Value = values[type]
@ -76,22 +89,26 @@ export const DesignOption = ({
return (
<MenuItem
{...{
name,
config,
current,
updateFunc,
t,
changed,
loadDocs,
...rest,
Input,
Value,
allowOverride,
passProps: { settings },
}}
/>
)
}
/**
* The design options menu
* @param {String} options.design the name of the design
* @param {Object} options.patternConfig the configuration from the pattern
* @param {Object} options.settings core settings
* @param {Object} options.update settings and ui update functions
* @param {String} options.language the menu language
* @param {Object} options.account the user account data
* @param {Boolean|React.component} options.DynamicDocs A docs component
*/
export const DesignOptions = ({
design,
patternConfig,

View file

@ -1,5 +1,7 @@
import { formatMm, formatPercentage } from 'shared/utils.mjs'
import { ListValue, HighlightedValue, PlainValue } from '../shared/values'
import { ListValue, HighlightedValue, PlainValue, BoolValue } from '../shared/values'
/** Displays the current percentatge value, and the absolute value if configured */
export const PctOptionValue = ({ config, current, settings, changed }) => {
const val = changed ? current : config.pct / 100
@ -11,34 +13,30 @@ export const PctOptionValue = ({ config, current, settings, changed }) => {
)
}
export const BoolOptionValue = ({ config, current, t, changed }) => (
<ListValue
{...{
current: current === undefined ? current : Number(current),
t,
config: { ...config, dflt: Number(config.bool) },
changed,
}}
/>
)
/** Displays a boolean value */
export const BoolOptionValue = BoolValue
/** Displays a count value*/
export const CountOptionValue = ({ config, current, changed }) => (
<PlainValue {...{ current, changed, dflt: config.count }} />
)
export const ListOptionValue = ({ name, config, current, t, changed }) => {
const translate = config.doNotTranslate ? (input) => input : (input) => t(`${name}.o.${input}`)
const value = translate(changed ? current : config.dflt)
return <HighlightedValue changed={changed}> {value} </HighlightedValue>
}
/** Displays a list option value */
export const ListOptionValue = (props) => (
<ListValue {...props} t={(input) => props.t(`${props.name}.o.${input}`)} />
)
/** Displays a degree value */
export const DegOptionValue = ({ config, current, changed }) => (
<HighlightedValue changed={changed}> {changed ? current : config.deg}&deg;</HighlightedValue>
)
/** Displays the MmOptions are not supported */
export const MmOptionValue = () => (
<span className="text-error">FIXME: No MmOptionvalue implemented</span>
<span className="text-error">FIXME: No Mm Options are not supported</span>
)
/** Displays that constant values are not implemented in the front end */
export const ConstantOptionValue = () => (
<span className="text-error">FIXME: No ConstantOptionvalue implemented</span>
)

View file

@ -6,6 +6,13 @@ import { HelpIcon } from 'shared/components/icons.mjs'
import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
import { ModalContext } from 'shared/context/modal-context.mjs'
/**
* get a loadDocs method for a menu
* @param {DynamicDocs} DynamicDocs the docs component to use
* @param {Function} getDocsPath a function that accepts an item name and returns a path to its documentation
* @param {string} language the language to get documentation in
* @return {Function | false} an event handler that loads does into a modal
*/
export const useDocsLoader = (DynamicDocs, getDocsPath, language) => {
const { setModal } = useContext(ModalContext)
return DynamicDocs
@ -23,6 +30,25 @@ export const useDocsLoader = (DynamicDocs, getDocsPath, language) => {
: false
}
/**
* A component for a collapsible sidebar menu in workbench views
* @param {Function} options.updateFunc a function the menu's inputs will use to update their values
* @param {String[]} options.ns namespaces used by this menu
* @param {React.Component} options.Icon the menu's icon
* @param {String} options.name the translation key for the menu's title
* @param {Object} options.config the structure of the menu's options
* @param {Number} options.control the user's control level setting
* @param {Object} options.inputs a map of input components to use, keyed by option name
* @param {Object} options.values a map of value components to use, keyed by option name
* @param {Object} options.currentValues a map of the values of the menu's options
* @param {Object} options.passProps any additional properties to pass the the inputs
* @param {DynamicDocs | Boolean} DynamicDocs the docs component to use for loading documentation
* @param {Function} getDocsPath a function that accepts an item name and returns a path to its documentation
* @param {string} language the language to use for the menu
* @param {Object} emojis a map of the emojis to use, keyed by option name
* @param {React.component} Item the component to use for menu items
* @return {[type]} [description]
*/
export const WorkbenchMenu = ({
updateFunc,
ns,
@ -41,10 +67,13 @@ export const WorkbenchMenu = ({
Item,
children,
}) => {
// get translation for the menu
const { t } = useTranslation(ns)
// get a documentation loader
const loadDocs = useDocsLoader(DynamicDocs, getDocsPath, language)
// get the appropriate buttons for the menu
const openButtons = []
if (loadDocs)
openButtons.push(

View file

@ -8,6 +8,16 @@ import {
} from 'shared/utils.mjs'
import { ChoiceButton } from 'shared/components/choice-button.mjs'
/*******************************************************************************************
* This file contains the base components to be used by inputs in menus in the workbench
* For the purposes of our menus, we have two main types:
* Sliders for changing numbers
* Lists for changing everything else
*
* Inputs that deal with more specific use cases should wrap one of the above base inputs
*******************************************************************************************/
/** A component that shows a number input to edit a value */
const EditCount = (props) => (
<div className="form-control mb-2 w-full">
<label className="label">
@ -29,6 +39,15 @@ const EditCount = (props) => (
</div>
)
/**
* A hook to get the change handler for an input.
* Also sets the reset function on a parent component
* @param {Number|String|Boolean} options.dflt the default value for the input
* @param {Function} options.updateFunc the onChange
* @param {string} options.name the name of the property being changed
* @param {Function} options.setReset the setReset function passed from the parent component
* @return {ret.handleChange} the change handler for the input
*/
const useSharedHandlers = ({ dflt, updateFunc, name, setReset }) => {
const reset = useCallback(() => updateFunc(name), [updateFunc, name])
@ -49,6 +68,16 @@ const useSharedHandlers = ({ dflt, updateFunc, name, setReset }) => {
return { handleChange, reset }
}
/**
* An input for selecting and item from a list
* @param {String} options.name the name of the property this input changes
* @param {Object} options.config configuration for the input
* @param {String|Number} options.current the current value of the input
* @param {Function} options.updateFunc the function called by the event handler to update the value
* @param {Boolean} options.compact include descriptions with the list items?
* @param {Function} options.t translation function
* @param {Function} options.setReset a setter for the reset function on the parent component
*/
export const ListInput = ({ name, config, current, updateFunc, compact = false, t, setReset }) => {
const { handleChange } = useSharedHandlers({
dflt: config.dflt,
@ -78,6 +107,7 @@ export const ListInput = ({ name, config, current, updateFunc, compact = false,
)
}
/** A boolean version of {@see ListInput} that sets up the necessary configuration */
export const BoolInput = (props) => {
const boolConfig = {
list: [0, 1],
@ -96,6 +126,18 @@ export const BoolInput = (props) => {
return <ListInput {...props} config={boolConfig} />
}
/**
* An input component that uses a slider to change a number value
* @param {String} options.name the name of the property being changed by the input
* @param {Object} options.config configuration for the input
* @param {Number} options.current the current value of the input
* @param {Function} options.updateFunc the function called by the event handler to update the value
* @param {Function} options.t translation function
* @param {Boolean} options.override open the text input to allow override of the slider?
* @param {String} options.suffix a suffix to append to value labels
* @param {Function} options.valFormatter a function that accepts a value and formats it for display as a label
* @param {Function} options.setReset a setter for the reset function on the parent component
*/
export const SliderInput = ({
name,
config,
@ -107,6 +149,7 @@ export const SliderInput = ({
valFormatter = (val) => val,
setReset,
children,
changed,
}) => {
const { max, min } = config
const { handleChange } = useSharedHandlers({
@ -117,7 +160,7 @@ export const SliderInput = ({
setReset,
})
let currentOrDefault = current === undefined ? config.dflt : current
let currentOrDefault = changed ? current : config.dflt
return (
<>
@ -153,7 +196,7 @@ export const SliderInput = ({
</div>
<input
type="range"
{...{ min, max, value: currentOrDefault, step: config.step }}
{...{ min, max, value: currentOrDefault, step: config.step || 0.1 }}
onChange={(evt) => handleChange(evt.target.value)}
className={`
range range-sm mt-1
@ -165,12 +208,10 @@ export const SliderInput = ({
)
}
export const PctInput = ({ config, settings, current, updateFunc, type = 'pct', ...rest }) => {
const suffix = type === 'deg' ? '°' : '%'
const factor = type === 'deg' ? 1 : 100
let pctCurrent = typeof current === 'undefined' ? config.dflt : current * factor
const valFormatter = (val) => round(val)
/** A {@see SliderInput} to handle percentage values */
export const PctInput = ({ current, changed, updateFunc, ...rest }) => {
const factor = 100
let pctCurrent = changed ? current * factor : current
const pctUpdateFunc = useCallback(
(path, newVal) => updateFunc(path, newVal === undefined ? undefined : newVal / factor),
[updateFunc, factor]
@ -180,28 +221,18 @@ export const PctInput = ({ config, settings, current, updateFunc, type = 'pct',
<SliderInput
{...{
...rest,
config: {
...config,
step: 0.1,
},
current: pctCurrent,
updateFunc: pctUpdateFunc,
suffix,
valFormatter,
suffix: '%',
valFormatter: round,
changed,
}}
>
<div className="flex flex-row justify-around">
<span className={current === config.dflt ? 'text-secondary' : 'text-accent'}>
{config.toAbs && settings.measurements
? formatMm(config.toAbs(current / factor, settings))
: ' '}
</span>
</div>
</SliderInput>
/>
)
}
export const DegInput = (props) => <PctInput {...props} type="deg" />
/** A {@see SliderInput} to handle degree values */
export const DegInput = (props) => <SliderInput {...props} suffix="°" valFormatter={round} />
export const MmInput = (props) => {
const { units, updateFunc, current } = props
@ -227,4 +258,5 @@ export const MmInput = (props) => {
)
}
/** A placeholder for an input to handle constant values */
export const ConstantInput = () => <p>FIXME: Constant options are not implemented (yet)</p>

View file

@ -2,13 +2,26 @@ import { ClearIcon, HelpIcon, EditIcon } from 'shared/components/icons.mjs'
import { Collapse } from 'shared/components/collapse.mjs'
import { useState, useMemo } from 'react'
/**
* Check to see if a value is different from its default
* @param {Number|String|Boolean} current the current value
* @param {Object} config configuration containing a dflt key
* @return {Boolean} was the value changed?
*/
export const wasChanged = (current, config) => {
if (typeof current === 'undefined') return false
if (current === config.dflt) return false
return true
}
/**
* A generic component to present the title of a menu item
* @param {String} options.name the name of the item, to act as its translation key
* @param {Function} options.t the translation function
* @param {String|React.Component} options.current a the current value, or a Value component to display it
* @param {Boolean} options.open is the menu item open?
* @param {String} options.emoji the emoji icon of the menu item
*/
export const ItemTitle = ({ name, t, current = null, open = false, emoji = '' }) => (
<div className={`flex flex-row gap-1 items-center w-full ${open ? '' : 'justify-between'}`}>
<span className="font-medium">
@ -21,7 +34,25 @@ export const ItemTitle = ({ name, t, current = null, open = false, emoji = '' })
</div>
)
/** @type {String} class to apply to buttons on open menu items */
const openButtonClass = 'btn btn-xs btn-ghost px-0'
/**
* A generic component for handling a menu item.
* Wraps the given input in a {@see Collapse} with the appropriate buttons
* @param {String} options.name the name of the item, for using as a key
* @param {Object} options.config the configuration for the input
* @param {Sting|Boolean|Number} options.current the current value of the item
* @param {Function} options.updateFunc the function that will be called by event handlers to update the value
* @param {Function} options.t the translation function
* @param {Object} options.passProps props to pass to the Input component
* @param {Boolean} changed has the value changed from default?
* @param {Function} loadDocs a function to load documentation for the item into a modal
* @param {React.Component} Input the input component this menu item will use
* @param {React.Component} Value a value display component this menu item will use
* @param {Boolean} allowOverride all a text input to be used to override the given input component
* @param {Number} control the user-defined control level
*/
export const MenuItem = ({
name,
config,
@ -36,9 +67,12 @@ export const MenuItem = ({
allowOverride = false,
control = Infinity,
}) => {
// state for knowing whether the override input should be shown
const [override, setOverride] = useState(false)
// store the reset function in state because the Input may set a custom one
const [reset, setReset] = useState(() => () => updateFunc(name))
// generate properties to pass to the Input
const drillProps = useMemo(
() => ({
name,
@ -48,14 +82,16 @@ export const MenuItem = ({
t,
changed,
override,
setReset,
setReset, // allow setting of the reset function
...passProps,
}),
[name, config, current, updateFunc, t, changed, override, setReset, passProps]
)
// don't render if this item is more advanced than the user has chosen to see
if (config.control && config.control > control) return null
// get buttons for open and closed states
const buttons = []
const openButtons = []
if (loadDocs)
@ -93,6 +129,7 @@ export const MenuItem = ({
openButtons.push(<ResetButton open key="clear" />)
}
// props to pass to the ItemTitle
const titleProps = { name, t, current: <Value {...drillProps} />, emoji: config.emoji }
return (
@ -108,6 +145,24 @@ export const MenuItem = ({
)
}
/**
* A component for recursively displaying groups of menu items.
* Accepts any object where menu item configurations are keyed by name
* Items that are group headings are expected to have an isGroup: true property
* @param {Boolean} options.collapsible Should this group be collapsible (use false for the top level of a menu)
* @param {Number} options.control the user-defined control level
* @param {String} options.name the name of the group or item
* @param {Object} options.currentValues a map of current values for items in the group, keyed by name
* @param {Object} structure the configuration for the group.
* @param {React.Component} Item the component to use for menu items
* @param {Object} values a map of Value display components to be used by menu items in the group
* @param {Object} inputs a map of Input components to be used by menu items in the group
* @param {Function} loadDocs a function to load item documentation into a modal
* @param {Object} passProps properties to pass to Inputs within menu items
* @param {Object} emojis a map of emojis to use as icons for groups or items
* @param {Function} updateFunc the function called by change handlers on inputs within menu items
* @param {Function} t translation function
*/
export const MenuItemGroup = ({
collapsible = true,
control,
@ -123,9 +178,13 @@ export const MenuItemGroup = ({
updateFunc,
t,
}) => {
// map the entries in the structure
const content = Object.entries(structure).map(([itemName, item]) => {
if (itemName === 'isMenu' || item === false) return null
if (!item.isMenu)
// if it's the isGroup property, or it is false, it shouldn't be shown
if (itemName === 'isGroup' || item === false) return null
// if the item is not a menu, it's an Item
if (!item.isGroup)
return (
<Item
key={itemName}
@ -145,6 +204,7 @@ export const MenuItemGroup = ({
/>
)
// otherwise, it's a group
return (
<MenuItemGroup
key={itemName}
@ -167,21 +227,26 @@ export const MenuItemGroup = ({
)
})
const titleProps = {
name,
t,
emoji: emojis[name] || emojis.dflt,
// if it should be wrapped in a collapsible
if (collapsible) {
// props to give to the group title
const titleProps = {
name,
t,
emoji: emojis[name] || emojis.dflt,
}
return (
<Collapse
bottom
color="secondary"
title={<ItemTitle {...titleProps} />}
openTitle={<ItemTitle open {...titleProps} />}
>
{content}
</Collapse>
)
}
return collapsible ? (
<Collapse
bottom
color="secondary"
title={<ItemTitle {...titleProps} />}
openTitle={<ItemTitle open {...titleProps} />}
>
{content}
</Collapse>
) : (
content
)
//otherwise just return the content
return content
}

View file

@ -1,26 +1,55 @@
import { formatMm, formatFraction128 } from 'shared/utils.mjs'
/*********************************************************************************************************
* This file contains the base components to be used for displaying values in menu titles in the workbench
* Values that deal with more specific use cases should wrap one of the below components
*********************************************************************************************************/
/** The basis of it all. Handles the changed/unchanged styling for the wrapped value */
export const HighlightedValue = ({ changed, children }) => (
<span className={changed ? 'text-info' : ''}> {children} </span>
)
/**
* A wrapper for displaying the correct value based on whether or not the value has changed
* @param {Number|String|Boolean} options.current the current value, if it has been changed
* @param {Number|String|Boolean} options.dflt the default value
* @param {Boolean} options.changed has the value been changed?
*/
export const PlainValue = ({ current, dflt, changed }) => (
<HighlightedValue changed={changed}> {changed ? current : dflt} </HighlightedValue>
)
/**
* Displays the correct, translated value for a list
* @param {String|Boolean} options.current the current value, if it has been changed
* @param {Function} options.t a translation function
* @param {Object} options.config the item config
* @param {Boolean} options.changed has the value been changed?
*/
export const ListValue = ({ current, t, config, changed }) => {
// get the values
const val = changed ? current : config.dflt
// key will be based on a few factors
let key
// are valueTitles configured?
if (config.valueTitles) key = config.valueTitles[val]
// if not, is the value a string
else if (typeof val === 'string') key = val
// otherwise stringify booleans
else if (val) key = 'yes'
else key = 'no'
return <HighlightedValue changed={changed}>{t(key)}</HighlightedValue>
const translated = config.doNotTranslate ? key : t(key)
return <HighlightedValue changed={changed}>{translated}</HighlightedValue>
}
/** Displays the corrent, translated value for a boolean */
export const BoolValue = ListValue
/** Displays a formated mm value based on the current units */
export const MmValue = ({ current, config, units, changed }) => (
<HighlightedValue changed={changed}>
<span

View file

@ -149,6 +149,7 @@ export const getCrumbs = (app, slug = false) => {
return crumbs
}
/** convert a millimeter value to a Number value in the given units */
export const measurementAsUnits = (mmValue, units = 'metric') =>
mmValue / (units === 'imperial' ? 25.4 : 10)
@ -199,7 +200,7 @@ export const optionsMenuStructure = (options) => {
if (typeof option === 'object') {
option.dflt = option.dflt || option[optionType(option)]
if (option.menu) {
set(menu, `${option.menu}.isMenu`, true)
set(menu, `${option.menu}.isGroup`, true)
set(menu, `${option.menu}.${option.name}`, option)
} else if (typeof option.menu === 'undefined') {
console.log(