document new shared menus
This commit is contained in:
parent
a8891d6091
commit
b975355f45
9 changed files with 285 additions and 84 deletions
|
@ -58,6 +58,15 @@ const inputs = {
|
||||||
|
|
||||||
export const ns = ['core-settings', 'modal']
|
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 = ({
|
export const CoreSettings = ({
|
||||||
update,
|
update,
|
||||||
settings,
|
settings,
|
||||||
|
|
|
@ -10,19 +10,29 @@ export const PaperlessSettingInput = ListInput
|
||||||
|
|
||||||
export const MarginSettingInput = MmInput
|
export const MarginSettingInput = MmInput
|
||||||
export const ScaleSettingInput = SliderInput
|
export const ScaleSettingInput = SliderInput
|
||||||
|
|
||||||
|
/** an input for the 'only' setting. toggles individual parts*/
|
||||||
export const OnlySettingInput = (props) => {
|
export const OnlySettingInput = (props) => {
|
||||||
const { config, updateFunc, current } = props
|
const { config, updateFunc, current } = props
|
||||||
|
|
||||||
|
// set up choice titles
|
||||||
config.choiceTitles = {}
|
config.choiceTitles = {}
|
||||||
config.list.forEach((p) => (config.choiceTitles[p] = p))
|
config.list.forEach((p) => (config.choiceTitles[p] = p))
|
||||||
|
|
||||||
|
// make an update function that toggles the parts
|
||||||
const onlyUpdateFunc = useCallback(
|
const onlyUpdateFunc = useCallback(
|
||||||
(path, part) => {
|
(path, part) => {
|
||||||
|
// if there's no part being set, it's a reset
|
||||||
if (part === undefined) return updateFunc(path, part)
|
if (part === undefined) return updateFunc(path, part)
|
||||||
|
|
||||||
|
// add or remove the part from the set
|
||||||
let newParts = new Set(current || [])
|
let newParts = new Set(current || [])
|
||||||
if (newParts.has(part)) newParts.delete(part)
|
if (newParts.has(part)) newParts.delete(part)
|
||||||
else newParts.add(part)
|
else newParts.add(part)
|
||||||
|
|
||||||
|
// if the set is now empty, reset
|
||||||
if (newParts.size < 1) newParts = undefined
|
if (newParts.size < 1) newParts = undefined
|
||||||
|
// otherwise use the new set
|
||||||
else newParts = [...newParts]
|
else newParts = [...newParts]
|
||||||
|
|
||||||
updateFunc(path, newParts)
|
updateFunc(path, newParts)
|
||||||
|
@ -33,11 +43,16 @@ export const OnlySettingInput = (props) => {
|
||||||
return <ListInput {...props} updateFunc={onlyUpdateFunc} />
|
return <ListInput {...props} updateFunc={onlyUpdateFunc} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** An input for the samm setting */
|
||||||
export const SaMmSettingInput = (props) => {
|
export const SaMmSettingInput = (props) => {
|
||||||
const { updateFunc, units, config } = props
|
const { updateFunc, units, config } = props
|
||||||
|
|
||||||
|
// the update function to switch the 'sa' setting along with samm
|
||||||
const mmUpdateFunc = useCallback(
|
const mmUpdateFunc = useCallback(
|
||||||
(_path, newCurrent) => {
|
(_path, newCurrent) => {
|
||||||
|
// convert to millimeters if there's a value
|
||||||
newCurrent = newCurrent === undefined ? measurementAsMm(config.dflt, units) : newCurrent
|
newCurrent = newCurrent === undefined ? measurementAsMm(config.dflt, units) : newCurrent
|
||||||
|
// update both values to match
|
||||||
updateFunc([
|
updateFunc([
|
||||||
[['samm'], newCurrent],
|
[['samm'], newCurrent],
|
||||||
[['sa'], newCurrent],
|
[['sa'], newCurrent],
|
||||||
|
@ -56,14 +71,20 @@ export const SaMmSettingInput = (props) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** An input for the sabool setting */
|
||||||
export const SaBoolSettingInput = (props) => {
|
export const SaBoolSettingInput = (props) => {
|
||||||
const { updateFunc, samm } = props
|
const { updateFunc, samm } = props
|
||||||
|
|
||||||
|
// the update function to toggle the 'sa' setting based on 'sabool'
|
||||||
const saUpdateFunc = useCallback(
|
const saUpdateFunc = useCallback(
|
||||||
(_path, newCurrent) =>
|
(_path, newCurrent) => {
|
||||||
updateFunc([
|
updateFunc([
|
||||||
|
// update sabool to the new current
|
||||||
[['sabool'], newCurrent],
|
[['sabool'], newCurrent],
|
||||||
|
// set sa based on whether there's a current value or not
|
||||||
[['sa'], newCurrent ? samm : undefined],
|
[['sa'], newCurrent ? samm : undefined],
|
||||||
]),
|
])
|
||||||
|
},
|
||||||
[updateFunc, samm]
|
[updateFunc, samm]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Components
|
// Components
|
||||||
import { OptionsIcon } from 'shared/components/icons.mjs'
|
import { OptionsIcon } from 'shared/components/icons.mjs'
|
||||||
import { optionsMenuStructure } from 'shared/utils.mjs'
|
import { optionsMenuStructure } from 'shared/utils.mjs'
|
||||||
import { optionType } from 'shared/utils.mjs'
|
import { optionType, formatMm } from 'shared/utils.mjs'
|
||||||
import {
|
import {
|
||||||
BoolInput,
|
BoolInput,
|
||||||
ConstantInput,
|
ConstantInput,
|
||||||
|
@ -24,6 +24,22 @@ import { MenuItem } from '../shared/menu-item.mjs'
|
||||||
|
|
||||||
export const ns = ['design-options']
|
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
|
// Facilitate lookup of the input component
|
||||||
const inputs = {
|
const inputs = {
|
||||||
bool: BoolInput,
|
bool: BoolInput,
|
||||||
|
@ -32,7 +48,7 @@ const inputs = {
|
||||||
deg: DegInput,
|
deg: DegInput,
|
||||||
list: ListInput,
|
list: ListInput,
|
||||||
mm: () => <span>FIXME: Mm options are deprecated. Please report this </span>,
|
mm: () => <span>FIXME: Mm options are deprecated. Please report this </span>,
|
||||||
pct: PctInput,
|
pct: PctOptionInput,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Facilitate lookup of the value component
|
// Facilitate lookup of the value component
|
||||||
|
@ -55,16 +71,13 @@ const emojis = {
|
||||||
groupDflt: '📁',
|
groupDflt: '📁',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DesignOption = ({
|
/**
|
||||||
name,
|
* A wrapper for {@see MenuItem} to handle design option-specific business
|
||||||
current,
|
* @param {Object} options.config the config for the item
|
||||||
config,
|
* @param {Object} options.settings core settings
|
||||||
settings,
|
* @param {Object} options.rest the rest of the props
|
||||||
updateFunc,
|
*/
|
||||||
t,
|
export const DesignOption = ({ config, settings, ...rest }) => {
|
||||||
loadDocs,
|
|
||||||
changed = false,
|
|
||||||
}) => {
|
|
||||||
const type = optionType(config)
|
const type = optionType(config)
|
||||||
const Input = inputs[type]
|
const Input = inputs[type]
|
||||||
const Value = values[type]
|
const Value = values[type]
|
||||||
|
@ -76,22 +89,26 @@ export const DesignOption = ({
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
{...{
|
{...{
|
||||||
name,
|
|
||||||
config,
|
config,
|
||||||
current,
|
...rest,
|
||||||
updateFunc,
|
|
||||||
t,
|
|
||||||
changed,
|
|
||||||
loadDocs,
|
|
||||||
Input,
|
Input,
|
||||||
Value,
|
Value,
|
||||||
allowOverride,
|
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 = ({
|
export const DesignOptions = ({
|
||||||
design,
|
design,
|
||||||
patternConfig,
|
patternConfig,
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { formatMm, formatPercentage } from 'shared/utils.mjs'
|
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 }) => {
|
export const PctOptionValue = ({ config, current, settings, changed }) => {
|
||||||
const val = changed ? current : config.pct / 100
|
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 }) => (
|
/** Displays a boolean value */
|
||||||
<ListValue
|
export const BoolOptionValue = BoolValue
|
||||||
{...{
|
|
||||||
current: current === undefined ? current : Number(current),
|
|
||||||
t,
|
|
||||||
config: { ...config, dflt: Number(config.bool) },
|
|
||||||
changed,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
|
/** Displays a count value*/
|
||||||
export const CountOptionValue = ({ config, current, changed }) => (
|
export const CountOptionValue = ({ config, current, changed }) => (
|
||||||
<PlainValue {...{ current, changed, dflt: config.count }} />
|
<PlainValue {...{ current, changed, dflt: config.count }} />
|
||||||
)
|
)
|
||||||
|
|
||||||
export const ListOptionValue = ({ name, config, current, t, changed }) => {
|
/** Displays a list option value */
|
||||||
const translate = config.doNotTranslate ? (input) => input : (input) => t(`${name}.o.${input}`)
|
export const ListOptionValue = (props) => (
|
||||||
const value = translate(changed ? current : config.dflt)
|
<ListValue {...props} t={(input) => props.t(`${props.name}.o.${input}`)} />
|
||||||
return <HighlightedValue changed={changed}> {value} </HighlightedValue>
|
)
|
||||||
}
|
|
||||||
|
|
||||||
|
/** Displays a degree value */
|
||||||
export const DegOptionValue = ({ config, current, changed }) => (
|
export const DegOptionValue = ({ config, current, changed }) => (
|
||||||
<HighlightedValue changed={changed}> {changed ? current : config.deg}°</HighlightedValue>
|
<HighlightedValue changed={changed}> {changed ? current : config.deg}°</HighlightedValue>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** Displays the MmOptions are not supported */
|
||||||
export const MmOptionValue = () => (
|
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 = () => (
|
export const ConstantOptionValue = () => (
|
||||||
<span className="text-error">FIXME: No ConstantOptionvalue implemented</span>
|
<span className="text-error">FIXME: No ConstantOptionvalue implemented</span>
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,6 +6,13 @@ import { HelpIcon } from 'shared/components/icons.mjs'
|
||||||
import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
|
import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
|
||||||
import { ModalContext } from 'shared/context/modal-context.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) => {
|
export const useDocsLoader = (DynamicDocs, getDocsPath, language) => {
|
||||||
const { setModal } = useContext(ModalContext)
|
const { setModal } = useContext(ModalContext)
|
||||||
return DynamicDocs
|
return DynamicDocs
|
||||||
|
@ -23,6 +30,25 @@ export const useDocsLoader = (DynamicDocs, getDocsPath, language) => {
|
||||||
: false
|
: 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 = ({
|
export const WorkbenchMenu = ({
|
||||||
updateFunc,
|
updateFunc,
|
||||||
ns,
|
ns,
|
||||||
|
@ -41,10 +67,13 @@ export const WorkbenchMenu = ({
|
||||||
Item,
|
Item,
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
|
// get translation for the menu
|
||||||
const { t } = useTranslation(ns)
|
const { t } = useTranslation(ns)
|
||||||
|
|
||||||
|
// get a documentation loader
|
||||||
const loadDocs = useDocsLoader(DynamicDocs, getDocsPath, language)
|
const loadDocs = useDocsLoader(DynamicDocs, getDocsPath, language)
|
||||||
|
|
||||||
|
// get the appropriate buttons for the menu
|
||||||
const openButtons = []
|
const openButtons = []
|
||||||
if (loadDocs)
|
if (loadDocs)
|
||||||
openButtons.push(
|
openButtons.push(
|
||||||
|
|
|
@ -8,6 +8,16 @@ import {
|
||||||
} from 'shared/utils.mjs'
|
} from 'shared/utils.mjs'
|
||||||
import { ChoiceButton } from 'shared/components/choice-button.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) => (
|
const EditCount = (props) => (
|
||||||
<div className="form-control mb-2 w-full">
|
<div className="form-control mb-2 w-full">
|
||||||
<label className="label">
|
<label className="label">
|
||||||
|
@ -29,6 +39,15 @@ const EditCount = (props) => (
|
||||||
</div>
|
</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 useSharedHandlers = ({ dflt, updateFunc, name, setReset }) => {
|
||||||
const reset = useCallback(() => updateFunc(name), [updateFunc, name])
|
const reset = useCallback(() => updateFunc(name), [updateFunc, name])
|
||||||
|
|
||||||
|
@ -49,6 +68,16 @@ const useSharedHandlers = ({ dflt, updateFunc, name, setReset }) => {
|
||||||
return { handleChange, reset }
|
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 }) => {
|
export const ListInput = ({ name, config, current, updateFunc, compact = false, t, setReset }) => {
|
||||||
const { handleChange } = useSharedHandlers({
|
const { handleChange } = useSharedHandlers({
|
||||||
dflt: config.dflt,
|
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) => {
|
export const BoolInput = (props) => {
|
||||||
const boolConfig = {
|
const boolConfig = {
|
||||||
list: [0, 1],
|
list: [0, 1],
|
||||||
|
@ -96,6 +126,18 @@ export const BoolInput = (props) => {
|
||||||
return <ListInput {...props} config={boolConfig} />
|
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 = ({
|
export const SliderInput = ({
|
||||||
name,
|
name,
|
||||||
config,
|
config,
|
||||||
|
@ -107,6 +149,7 @@ export const SliderInput = ({
|
||||||
valFormatter = (val) => val,
|
valFormatter = (val) => val,
|
||||||
setReset,
|
setReset,
|
||||||
children,
|
children,
|
||||||
|
changed,
|
||||||
}) => {
|
}) => {
|
||||||
const { max, min } = config
|
const { max, min } = config
|
||||||
const { handleChange } = useSharedHandlers({
|
const { handleChange } = useSharedHandlers({
|
||||||
|
@ -117,7 +160,7 @@ export const SliderInput = ({
|
||||||
setReset,
|
setReset,
|
||||||
})
|
})
|
||||||
|
|
||||||
let currentOrDefault = current === undefined ? config.dflt : current
|
let currentOrDefault = changed ? current : config.dflt
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -153,7 +196,7 @@ export const SliderInput = ({
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
{...{ min, max, value: currentOrDefault, step: config.step }}
|
{...{ min, max, value: currentOrDefault, step: config.step || 0.1 }}
|
||||||
onChange={(evt) => handleChange(evt.target.value)}
|
onChange={(evt) => handleChange(evt.target.value)}
|
||||||
className={`
|
className={`
|
||||||
range range-sm mt-1
|
range range-sm mt-1
|
||||||
|
@ -165,12 +208,10 @@ export const SliderInput = ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PctInput = ({ config, settings, current, updateFunc, type = 'pct', ...rest }) => {
|
/** A {@see SliderInput} to handle percentage values */
|
||||||
const suffix = type === 'deg' ? '°' : '%'
|
export const PctInput = ({ current, changed, updateFunc, ...rest }) => {
|
||||||
const factor = type === 'deg' ? 1 : 100
|
const factor = 100
|
||||||
let pctCurrent = typeof current === 'undefined' ? config.dflt : current * factor
|
let pctCurrent = changed ? current * factor : current
|
||||||
|
|
||||||
const valFormatter = (val) => round(val)
|
|
||||||
const pctUpdateFunc = useCallback(
|
const pctUpdateFunc = useCallback(
|
||||||
(path, newVal) => updateFunc(path, newVal === undefined ? undefined : newVal / factor),
|
(path, newVal) => updateFunc(path, newVal === undefined ? undefined : newVal / factor),
|
||||||
[updateFunc, factor]
|
[updateFunc, factor]
|
||||||
|
@ -180,28 +221,18 @@ export const PctInput = ({ config, settings, current, updateFunc, type = 'pct',
|
||||||
<SliderInput
|
<SliderInput
|
||||||
{...{
|
{...{
|
||||||
...rest,
|
...rest,
|
||||||
config: {
|
|
||||||
...config,
|
|
||||||
step: 0.1,
|
|
||||||
},
|
|
||||||
current: pctCurrent,
|
current: pctCurrent,
|
||||||
updateFunc: pctUpdateFunc,
|
updateFunc: pctUpdateFunc,
|
||||||
suffix,
|
suffix: '%',
|
||||||
valFormatter,
|
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) => {
|
export const MmInput = (props) => {
|
||||||
const { units, updateFunc, current } = 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>
|
export const ConstantInput = () => <p>FIXME: Constant options are not implemented (yet)</p>
|
||||||
|
|
|
@ -2,13 +2,26 @@ import { ClearIcon, HelpIcon, EditIcon } from 'shared/components/icons.mjs'
|
||||||
import { Collapse } from 'shared/components/collapse.mjs'
|
import { Collapse } from 'shared/components/collapse.mjs'
|
||||||
import { useState, useMemo } from 'react'
|
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) => {
|
export const wasChanged = (current, config) => {
|
||||||
if (typeof current === 'undefined') return false
|
if (typeof current === 'undefined') return false
|
||||||
if (current === config.dflt) return false
|
if (current === config.dflt) return false
|
||||||
|
|
||||||
return true
|
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 = '' }) => (
|
export const ItemTitle = ({ name, t, current = null, open = false, emoji = '' }) => (
|
||||||
<div className={`flex flex-row gap-1 items-center w-full ${open ? '' : 'justify-between'}`}>
|
<div className={`flex flex-row gap-1 items-center w-full ${open ? '' : 'justify-between'}`}>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
|
@ -21,7 +34,25 @@ export const ItemTitle = ({ name, t, current = null, open = false, emoji = '' })
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** @type {String} class to apply to buttons on open menu items */
|
||||||
const openButtonClass = 'btn btn-xs btn-ghost px-0'
|
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 = ({
|
export const MenuItem = ({
|
||||||
name,
|
name,
|
||||||
config,
|
config,
|
||||||
|
@ -36,9 +67,12 @@ export const MenuItem = ({
|
||||||
allowOverride = false,
|
allowOverride = false,
|
||||||
control = Infinity,
|
control = Infinity,
|
||||||
}) => {
|
}) => {
|
||||||
|
// state for knowing whether the override input should be shown
|
||||||
const [override, setOverride] = useState(false)
|
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))
|
const [reset, setReset] = useState(() => () => updateFunc(name))
|
||||||
|
|
||||||
|
// generate properties to pass to the Input
|
||||||
const drillProps = useMemo(
|
const drillProps = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
name,
|
name,
|
||||||
|
@ -48,14 +82,16 @@ export const MenuItem = ({
|
||||||
t,
|
t,
|
||||||
changed,
|
changed,
|
||||||
override,
|
override,
|
||||||
setReset,
|
setReset, // allow setting of the reset function
|
||||||
...passProps,
|
...passProps,
|
||||||
}),
|
}),
|
||||||
[name, config, current, updateFunc, t, changed, override, setReset, 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
|
if (config.control && config.control > control) return null
|
||||||
|
|
||||||
|
// get buttons for open and closed states
|
||||||
const buttons = []
|
const buttons = []
|
||||||
const openButtons = []
|
const openButtons = []
|
||||||
if (loadDocs)
|
if (loadDocs)
|
||||||
|
@ -93,6 +129,7 @@ export const MenuItem = ({
|
||||||
openButtons.push(<ResetButton open key="clear" />)
|
openButtons.push(<ResetButton open key="clear" />)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// props to pass to the ItemTitle
|
||||||
const titleProps = { name, t, current: <Value {...drillProps} />, emoji: config.emoji }
|
const titleProps = { name, t, current: <Value {...drillProps} />, emoji: config.emoji }
|
||||||
|
|
||||||
return (
|
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 = ({
|
export const MenuItemGroup = ({
|
||||||
collapsible = true,
|
collapsible = true,
|
||||||
control,
|
control,
|
||||||
|
@ -123,9 +178,13 @@ export const MenuItemGroup = ({
|
||||||
updateFunc,
|
updateFunc,
|
||||||
t,
|
t,
|
||||||
}) => {
|
}) => {
|
||||||
|
// map the entries in the structure
|
||||||
const content = Object.entries(structure).map(([itemName, item]) => {
|
const content = Object.entries(structure).map(([itemName, item]) => {
|
||||||
if (itemName === 'isMenu' || item === false) return null
|
// if it's the isGroup property, or it is false, it shouldn't be shown
|
||||||
if (!item.isMenu)
|
if (itemName === 'isGroup' || item === false) return null
|
||||||
|
|
||||||
|
// if the item is not a menu, it's an Item
|
||||||
|
if (!item.isGroup)
|
||||||
return (
|
return (
|
||||||
<Item
|
<Item
|
||||||
key={itemName}
|
key={itemName}
|
||||||
|
@ -145,6 +204,7 @@ export const MenuItemGroup = ({
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// otherwise, it's a group
|
||||||
return (
|
return (
|
||||||
<MenuItemGroup
|
<MenuItemGroup
|
||||||
key={itemName}
|
key={itemName}
|
||||||
|
@ -167,21 +227,26 @@ export const MenuItemGroup = ({
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const titleProps = {
|
// if it should be wrapped in a collapsible
|
||||||
name,
|
if (collapsible) {
|
||||||
t,
|
// props to give to the group title
|
||||||
emoji: emojis[name] || emojis.dflt,
|
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
|
//otherwise just return the content
|
||||||
bottom
|
return content
|
||||||
color="secondary"
|
|
||||||
title={<ItemTitle {...titleProps} />}
|
|
||||||
openTitle={<ItemTitle open {...titleProps} />}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</Collapse>
|
|
||||||
) : (
|
|
||||||
content
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,55 @@
|
||||||
import { formatMm, formatFraction128 } from 'shared/utils.mjs'
|
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 }) => (
|
export const HighlightedValue = ({ changed, children }) => (
|
||||||
<span className={changed ? 'text-info' : ''}> {children} </span>
|
<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 }) => (
|
export const PlainValue = ({ current, dflt, changed }) => (
|
||||||
<HighlightedValue changed={changed}> {changed ? current : dflt} </HighlightedValue>
|
<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 }) => {
|
export const ListValue = ({ current, t, config, changed }) => {
|
||||||
|
// get the values
|
||||||
const val = changed ? current : config.dflt
|
const val = changed ? current : config.dflt
|
||||||
|
|
||||||
|
// key will be based on a few factors
|
||||||
let key
|
let key
|
||||||
|
// are valueTitles configured?
|
||||||
if (config.valueTitles) key = config.valueTitles[val]
|
if (config.valueTitles) key = config.valueTitles[val]
|
||||||
|
// if not, is the value a string
|
||||||
else if (typeof val === 'string') key = val
|
else if (typeof val === 'string') key = val
|
||||||
|
// otherwise stringify booleans
|
||||||
else if (val) key = 'yes'
|
else if (val) key = 'yes'
|
||||||
else key = 'no'
|
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
|
export const BoolValue = ListValue
|
||||||
|
|
||||||
|
/** Displays a formated mm value based on the current units */
|
||||||
export const MmValue = ({ current, config, units, changed }) => (
|
export const MmValue = ({ current, config, units, changed }) => (
|
||||||
<HighlightedValue changed={changed}>
|
<HighlightedValue changed={changed}>
|
||||||
<span
|
<span
|
||||||
|
|
|
@ -149,6 +149,7 @@ export const getCrumbs = (app, slug = false) => {
|
||||||
return crumbs
|
return crumbs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** convert a millimeter value to a Number value in the given units */
|
||||||
export const measurementAsUnits = (mmValue, units = 'metric') =>
|
export const measurementAsUnits = (mmValue, units = 'metric') =>
|
||||||
mmValue / (units === 'imperial' ? 25.4 : 10)
|
mmValue / (units === 'imperial' ? 25.4 : 10)
|
||||||
|
|
||||||
|
@ -199,7 +200,7 @@ export const optionsMenuStructure = (options) => {
|
||||||
if (typeof option === 'object') {
|
if (typeof option === 'object') {
|
||||||
option.dflt = option.dflt || option[optionType(option)]
|
option.dflt = option.dflt || option[optionType(option)]
|
||||||
if (option.menu) {
|
if (option.menu) {
|
||||||
set(menu, `${option.menu}.isMenu`, true)
|
set(menu, `${option.menu}.isGroup`, true)
|
||||||
set(menu, `${option.menu}.${option.name}`, option)
|
set(menu, `${option.menu}.${option.name}`, option)
|
||||||
} else if (typeof option.menu === 'undefined') {
|
} else if (typeof option.menu === 'undefined') {
|
||||||
console.log(
|
console.log(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue