From b975355f453639e9af56f9b5faf27be6f8ff1e1f Mon Sep 17 00:00:00 2001 From: Enoch Riese Date: Wed, 31 May 2023 17:42:16 -0500 Subject: [PATCH] document new shared menus --- .../workbench/menus/core-settings/index.mjs | 9 ++ .../workbench/menus/core-settings/inputs.mjs | 25 ++++- .../workbench/menus/design-options/index.mjs | 55 +++++---- .../workbench/menus/design-options/values.mjs | 32 +++--- .../workbench/menus/shared/index.mjs | 29 +++++ .../workbench/menus/shared/inputs.mjs | 80 +++++++++---- .../workbench/menus/shared/menu-item.mjs | 105 ++++++++++++++---- .../workbench/menus/shared/values.mjs | 31 +++++- sites/shared/utils.mjs | 3 +- 9 files changed, 285 insertions(+), 84 deletions(-) diff --git a/sites/shared/components/workbench/menus/core-settings/index.mjs b/sites/shared/components/workbench/menus/core-settings/index.mjs index 090b841dacb..7d25b98db45 100644 --- a/sites/shared/components/workbench/menus/core-settings/index.mjs +++ b/sites/shared/components/workbench/menus/core-settings/index.mjs @@ -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, diff --git a/sites/shared/components/workbench/menus/core-settings/inputs.mjs b/sites/shared/components/workbench/menus/core-settings/inputs.mjs index c6c23bb2ade..bce7ed9f503 100644 --- a/sites/shared/components/workbench/menus/core-settings/inputs.mjs +++ b/sites/shared/components/workbench/menus/core-settings/inputs.mjs @@ -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 } +/** 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] ) diff --git a/sites/shared/components/workbench/menus/design-options/index.mjs b/sites/shared/components/workbench/menus/design-options/index.mjs index 4fe0b61f816..02b5a620b62 100644 --- a/sites/shared/components/workbench/menus/design-options/index.mjs +++ b/sites/shared/components/workbench/menus/design-options/index.mjs @@ -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 ( + +
+ + {config.toAbs && settings.measurements + ? formatMm(config.toAbs(currentOrDefault, settings)) + : ' '} + +
+
+ ) +} + // Facilitate lookup of the input component const inputs = { bool: BoolInput, @@ -32,7 +48,7 @@ const inputs = { deg: DegInput, list: ListInput, mm: () => FIXME: Mm options are deprecated. Please report this , - 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 ( ) } +/** + * 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, diff --git a/sites/shared/components/workbench/menus/design-options/values.mjs b/sites/shared/components/workbench/menus/design-options/values.mjs index 9d6d313008f..0275187a0e2 100644 --- a/sites/shared/components/workbench/menus/design-options/values.mjs +++ b/sites/shared/components/workbench/menus/design-options/values.mjs @@ -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 }) => ( - -) +/** Displays a boolean value */ +export const BoolOptionValue = BoolValue +/** Displays a count value*/ export const CountOptionValue = ({ config, current, changed }) => ( ) -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 {value} -} +/** Displays a list option value */ +export const ListOptionValue = (props) => ( + props.t(`${props.name}.o.${input}`)} /> +) +/** Displays a degree value */ export const DegOptionValue = ({ config, current, changed }) => ( {changed ? current : config.deg}° ) +/** Displays the MmOptions are not supported */ export const MmOptionValue = () => ( - FIXME: No MmOptionvalue implemented + FIXME: No Mm Options are not supported ) + +/** Displays that constant values are not implemented in the front end */ export const ConstantOptionValue = () => ( FIXME: No ConstantOptionvalue implemented ) diff --git a/sites/shared/components/workbench/menus/shared/index.mjs b/sites/shared/components/workbench/menus/shared/index.mjs index e4947c0802e..e9a23179018 100644 --- a/sites/shared/components/workbench/menus/shared/index.mjs +++ b/sites/shared/components/workbench/menus/shared/index.mjs @@ -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( diff --git a/sites/shared/components/workbench/menus/shared/inputs.mjs b/sites/shared/components/workbench/menus/shared/inputs.mjs index 37f1e2e734a..ec498e41fcb 100644 --- a/sites/shared/components/workbench/menus/shared/inputs.mjs +++ b/sites/shared/components/workbench/menus/shared/inputs.mjs @@ -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) => (
) +/** + * 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 } +/** + * 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 = ({ 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', -
- - {config.toAbs && settings.measurements - ? formatMm(config.toAbs(current / factor, settings)) - : ' '} - -
-
+ /> ) } -export const DegInput = (props) => +/** A {@see SliderInput} to handle degree values */ +export const DegInput = (props) => 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 = () =>

FIXME: Constant options are not implemented (yet)

diff --git a/sites/shared/components/workbench/menus/shared/menu-item.mjs b/sites/shared/components/workbench/menus/shared/menu-item.mjs index 4fccf34ff31..81c4284c26d 100644 --- a/sites/shared/components/workbench/menus/shared/menu-item.mjs +++ b/sites/shared/components/workbench/menus/shared/menu-item.mjs @@ -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 = '' }) => (
@@ -21,7 +34,25 @@ export const ItemTitle = ({ name, t, current = null, open = false, emoji = '' })
) +/** @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() } + // props to pass to the ItemTitle const titleProps = { name, t, current: , 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 ( ) + // otherwise, it's a group return ( } + openTitle={} + > + {content} + + ) } - return collapsible ? ( - } - openTitle={} - > - {content} - - ) : ( - content - ) + + //otherwise just return the content + return content } diff --git a/sites/shared/components/workbench/menus/shared/values.mjs b/sites/shared/components/workbench/menus/shared/values.mjs index 1fd456deba6..0abd6964fd6 100644 --- a/sites/shared/components/workbench/menus/shared/values.mjs +++ b/sites/shared/components/workbench/menus/shared/values.mjs @@ -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 }) => ( {children} ) +/** + * 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 }) => ( {changed ? current : dflt} ) +/** + * 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 {t(key)} + const translated = config.doNotTranslate ? key : t(key) + + return {translated} } +/** 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 }) => ( { 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(