feat: Added test view
This commit is contained in:
parent
0dba8dcac0
commit
a97f9b6e8a
8 changed files with 470 additions and 2 deletions
|
@ -41,6 +41,7 @@ import { DesignOptionsMenu } from './menus/DesignOptionsMenu.mjs'
|
|||
import { CoreSettingsMenu } from './menus/CoreSettingsMenu.mjs'
|
||||
import { UiPreferencesMenu } from './menus/UiPreferencesMenu.mjs'
|
||||
import { LayoutSettingsMenu } from './menus/LayoutMenu.mjs'
|
||||
import { TestOptionsMenu, TestMeasurementsMenu } from './menus/TestMenu.mjs'
|
||||
import { FlagsAccordionEntries } from './Flag.mjs'
|
||||
import { UndoStep } from './views/UndosView.mjs'
|
||||
|
||||
|
@ -85,6 +86,54 @@ export const HeaderMenuDraftView = (props) => {
|
|||
)
|
||||
}
|
||||
|
||||
export const HeaderMenuTestView = (props) => {
|
||||
const i18n = useDesignTranslation(props.Design.designConfig.data.id)
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderMenuTestViewDesignOptions {...props} i18n={i18n} />
|
||||
<HeaderMenuTestViewDesignMeasurements {...props} />
|
||||
<HeaderMenuTestIcons {...props} i18n={i18n} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const HeaderMenuTestViewDesignOptions = (props) => {
|
||||
return (
|
||||
<HeaderMenuDropdown
|
||||
{...props}
|
||||
id="designOptions"
|
||||
tooltip="These options are specific to this design. You can use them to customize your pattern in a variety of ways."
|
||||
toggle={
|
||||
<>
|
||||
<HeaderMenuIcon name="options" extraClasses="tw-text-secondary" />
|
||||
<span className="tw-hidden lg:tw-inline">Test Options</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TestOptionsMenu {...props} />
|
||||
</HeaderMenuDropdown>
|
||||
)
|
||||
}
|
||||
|
||||
export const HeaderMenuTestViewDesignMeasurements = (props) => {
|
||||
return (
|
||||
<HeaderMenuDropdown
|
||||
{...props}
|
||||
id="designMeasurements"
|
||||
tooltip="These options are specific to this design. You can use them to customize your pattern in a variety of ways."
|
||||
toggle={
|
||||
<>
|
||||
<HeaderMenuIcon name="options" extraClasses="tw-text-secondary" />
|
||||
<span className="tw-hidden lg:tw-inline">Test Measurements</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TestMeasurementsMenu {...props} />
|
||||
</HeaderMenuDropdown>
|
||||
)
|
||||
}
|
||||
|
||||
export const HeaderMenuDropdown = (props) => {
|
||||
const { tooltip, toggle, open, setOpen, id, end = false } = props
|
||||
/*
|
||||
|
@ -381,6 +430,24 @@ export const HeaderMenuUndoIcons = (props) => {
|
|||
)
|
||||
}
|
||||
|
||||
export const HeaderMenuTestIcons = (props) => {
|
||||
const { update, state, Design } = props
|
||||
const Button = HeaderMenuButton
|
||||
const size = 'tw-w-5 tw-h-5'
|
||||
const undos = state._?.undos && state._.undos.length > 0 ? state._.undos : false
|
||||
|
||||
return (
|
||||
<div className="tw-flex tw-flex-row tw-flex-wrap tw-items-center tw-justify-center tw-px-0.5 lg:tw-px-1">
|
||||
<Button
|
||||
updateHandler={() => update.settings('sample', undefined)}
|
||||
tooltip="Clear the test so you can select another"
|
||||
>
|
||||
Clear Test
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const HeaderMenuSaveIcons = (props) => {
|
||||
const { update, state } = props
|
||||
const backend = useBackend()
|
||||
|
@ -498,7 +565,9 @@ export const HeaderMenuViewMenu = (props) => {
|
|||
toggle={
|
||||
<>
|
||||
<HeaderMenuIcon name="right" stroke={3} extraClasses="tw-text-secondary tw-rotate-90" />
|
||||
<span className="tw-hidden lg:tw-inline">Views</span>
|
||||
<span className="tw-hidden lg:tw-inline">
|
||||
{viewLabels[state.view] ? viewLabels[state.view].t : 'Views'}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
@ -611,6 +680,7 @@ export const HeaderMenuLayoutViewIcons = (props) => {
|
|||
|
||||
const headerMenus = {
|
||||
draft: HeaderMenuDraftView,
|
||||
test: HeaderMenuTestView,
|
||||
layout: HeaderMenuLayoutView,
|
||||
//HeaderMenuDraftViewDesignOptions,
|
||||
//HeaderMenuDraftViewCoreSettings,
|
||||
|
|
|
@ -174,6 +174,7 @@ export const MenuItemGroup = ({
|
|||
i18n,
|
||||
}) => {
|
||||
if (!Item) Item = MenuItem
|
||||
console.log(structure)
|
||||
|
||||
// map the entries in the structure
|
||||
const content = Object.entries(structure).map(([itemName, item]) => {
|
||||
|
@ -285,3 +286,79 @@ export const MenuItemTitle = ({ name, current = null, open = false, emoji = '',
|
|||
<span className="tw-font-bold">{current}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
/**
|
||||
* A component for recursively displaying groups of menu buttons.
|
||||
*
|
||||
* This is a lot simpler than the options menu structure
|
||||
*
|
||||
* @param {object} props - All the React props
|
||||
* @param {object} props.structure - The menu structure
|
||||
* @param {React.Component} props.Button - The component to use for menu items
|
||||
* @param {freesewing.Design} props.Design - The FreeSewing design
|
||||
* @param {object} props.i18n - The translations object
|
||||
*/
|
||||
export const MenuButtonGroup = ({ structure, Button = false, Design, Icon, i18n }) => {
|
||||
if (!Button) return null
|
||||
/*
|
||||
* Create structure, push groups to the end
|
||||
*/
|
||||
const content = []
|
||||
for (const [itemName, item] of Object.entries(structure)) {
|
||||
if (item && !item.isGroup && !['isGroup', 'title'].includes(itemName))
|
||||
content.push(<Button key={itemName} {...{ name: itemName }} />)
|
||||
}
|
||||
|
||||
for (const [itemName, item] of Object.entries(structure)) {
|
||||
if (item && item.isGroup) {
|
||||
const ItemIcon = item.icon
|
||||
? item.icon
|
||||
: item.isGroup
|
||||
? GroupIcon
|
||||
: Icon
|
||||
? Icon
|
||||
: () => <span role="img">fixme-icon</span>
|
||||
const Value = item.isGroup
|
||||
? () => (
|
||||
<div className="tw-flex tw-flex-row tw-gap-2 tw-items-center tw-font-medium">
|
||||
{Object.keys(item).filter((i) => i !== 'isGroup').length}
|
||||
<OptionsIcon className="tw-w-5 tw-h-5" />
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
|
||||
content.push([
|
||||
<div
|
||||
className="tw-flex tw-flex-row tw-items-center tw-justify-between tw-w-full tw-pl-0 tw-pr-4 tw-py-2"
|
||||
key="a"
|
||||
>
|
||||
<div className="tw-flex tw-flex-row tw-items-center tw-gap-4 tw-w-full">
|
||||
<ItemIcon />
|
||||
<span className="tw-font-medium tw-capitalize">
|
||||
{item.title ? item.title : getItemLabel(i18n, itemName)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="tw-font-bold">
|
||||
<Value config={item} Design={Design} />
|
||||
</div>
|
||||
</div>,
|
||||
<MenuButtonGroup
|
||||
key={itemName}
|
||||
{...{
|
||||
structure: item,
|
||||
Icon,
|
||||
Button,
|
||||
Design,
|
||||
i18n,
|
||||
}}
|
||||
/>,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tw-flex tw-flex-col tw-gap-0.5 tw-ml-4">
|
||||
{content.filter((item) => item !== null)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
106
packages/react/components/Editor/components/menus/TestMenu.mjs
Normal file
106
packages/react/components/Editor/components/menus/TestMenu.mjs
Normal file
|
@ -0,0 +1,106 @@
|
|||
// Dependencies
|
||||
import { menuDesignOptionsStructure } from '../../lib/index.mjs'
|
||||
import { measurements as measurementsTranslations } from '@freesewing/i18n'
|
||||
import { orderBy } from '@freesewing/utils'
|
||||
// Hooks
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
// Components
|
||||
import { MenuButtonGroup } from './Container.mjs'
|
||||
import { BeakerIcon, OptionsIcon } from '@freesewing/react/components/Icon'
|
||||
|
||||
/**
|
||||
* The test design options menu
|
||||
*
|
||||
* @param {object} props.Design - An object holding the Design instance
|
||||
* @param {Object} props.state - Object holding state
|
||||
* @param {Object} props.i18n - Object holding translations loaded from the design
|
||||
* @param {Object} props.update - Object holding state handlers
|
||||
*/
|
||||
export const TestOptionsMenu = ({ Design, state, i18n, update }) => {
|
||||
const structure = useMemo(
|
||||
() =>
|
||||
menuDesignOptionsStructure(
|
||||
Design.designConfig.data.id,
|
||||
Design.patternConfig.options,
|
||||
state.settings
|
||||
),
|
||||
[Design.designConfig.data.id, Design.patternConfig, state.settings]
|
||||
)
|
||||
|
||||
return (
|
||||
<MenuButtonGroup
|
||||
{...{
|
||||
structure,
|
||||
ux: state.ui.ux,
|
||||
Icon: OptionsIcon,
|
||||
Button: (props) => <SampleOptionButton {...{ i18n, update }} {...props} />,
|
||||
name: 'Design Options',
|
||||
isDesignOptionsGroup: true,
|
||||
state,
|
||||
Design,
|
||||
i18n,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The test measurements options menu
|
||||
*
|
||||
* @param {object} props.Design - An object holding the Design instance
|
||||
* @param {Object} props.state - Object holding state
|
||||
* @param {Object} props.update - Object holding state handlers
|
||||
*/
|
||||
export const TestMeasurementsMenu = ({ Design, state, update }) => {
|
||||
const structure = {}
|
||||
if (Design.patternConfig.measurements.length > 0)
|
||||
structure.required = { isGroup: true, title: 'Required Measurements' }
|
||||
for (const m of Design.patternConfig.measurements) {
|
||||
structure.required[m] = { isGroup: false, name: m, title: m }
|
||||
}
|
||||
if (Design.patternConfig.optionalMeasurements.length > 0)
|
||||
structure.optional = { isGroup: true, title: 'Optional Measurements' }
|
||||
for (const m of Design.patternConfig.optionalMeasurements) {
|
||||
structure.optional[m] = { isGroup: false, name: m, title: m }
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuButtonGroup
|
||||
{...{
|
||||
structure,
|
||||
Icon: OptionsIcon,
|
||||
Button: (props) => <SampleMeasurementButton {...{ update }} {...props} />,
|
||||
name: 'Design Measurments',
|
||||
isDesignOptionsGroup: true,
|
||||
state,
|
||||
Design,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const SampleOptionButton = ({ name, i18n, update }) => (
|
||||
<button
|
||||
className={
|
||||
'tw-daisy-btn tw-daisy-btn-outline tw-daisy-btn-sm tw-mx-2 ' +
|
||||
'tw-daisy-btn-secondary tw-flex tw-flex-row tw-items-center tw-justify-between'
|
||||
}
|
||||
onClick={() => update.settings('sample', { type: 'option', option: name })}
|
||||
>
|
||||
<BeakerIcon className="tw-w-5 tw-h-5" />
|
||||
<span>{i18n.en.o[name].t}</span>
|
||||
</button>
|
||||
)
|
||||
|
||||
const SampleMeasurementButton = ({ name, i18n, update }) => (
|
||||
<button
|
||||
className={
|
||||
'tw-daisy-btn tw-daisy-btn-outline tw-daisy-btn-sm tw-mx-2 ' +
|
||||
'tw-daisy-btn-secondary tw-flex tw-flex-row tw-items-center tw-justify-between'
|
||||
}
|
||||
onClick={() => update.settings('sample', { type: 'option', option: name })}
|
||||
>
|
||||
<BeakerIcon className="tw-w-5 tw-h-5" />
|
||||
<span>{measurementsTranslations[name]}</span>
|
||||
</button>
|
||||
)
|
|
@ -5,7 +5,6 @@ import { draft, missingMeasurements } from '../../lib/index.mjs'
|
|||
import { Null } from '../Null.mjs'
|
||||
import { ZoomablePattern } from '../ZoomablePattern.mjs'
|
||||
import { PatternLayout } from '../PatternLayout.mjs'
|
||||
import { DraftMenu } from '../menus/DraftMenu.mjs'
|
||||
|
||||
/**
|
||||
* The draft view allows users to tweak their pattern
|
||||
|
|
181
packages/react/components/Editor/components/views/TestView.mjs
Normal file
181
packages/react/components/Editor/components/views/TestView.mjs
Normal file
|
@ -0,0 +1,181 @@
|
|||
// Dependencies
|
||||
import React, { useMemo, useCallback } from 'react'
|
||||
import { sample, missingMeasurements, menuDesignOptionsStructure } from '../../lib/index.mjs'
|
||||
import { measurements as measurementsTranslations } from '@freesewing/i18n'
|
||||
import { orderBy } from '@freesewing/utils'
|
||||
// Components
|
||||
import { Null } from '../Null.mjs'
|
||||
import { ZoomablePattern } from '../ZoomablePattern.mjs'
|
||||
import { PatternLayout } from '../PatternLayout.mjs'
|
||||
import { HeaderMenu } from '../HeaderMenu.mjs'
|
||||
import { H1, H3, H4, H5 } from '@freesewing/react/components/Heading'
|
||||
|
||||
/**
|
||||
* The test view allows users to test options and measurements
|
||||
*
|
||||
* @param (object) props - All the props
|
||||
* @param {function} props.config - The editor configuration
|
||||
* @param {function} props.Design - The design constructor
|
||||
* @param {array} props.missingMeasurements - List of missing measurements for the current design
|
||||
* @param {object} props.state - The ViewWrapper state object
|
||||
* @param {object} props.state.settings - The current settings
|
||||
* @param {object} props.update - Helper object for updating the ViewWrapper state
|
||||
* @return {function} DraftView - React component
|
||||
*/
|
||||
export const TestView = ({ Design, state, update, config }) => {
|
||||
/*
|
||||
* Don't trust that we have all measurements
|
||||
*
|
||||
* We do not need to change the view here. That is done in the central
|
||||
* ViewWrapper componenet. However, checking the measurements against
|
||||
* the design takes a brief moment, so this component will typically
|
||||
* render before that happens, and if measurements are missing it will
|
||||
* throw and error.
|
||||
*
|
||||
* So when measurements are missing, we just return here and the view
|
||||
* will switch on the next render loop.
|
||||
*/
|
||||
if (missingMeasurements(state)) return <Null />
|
||||
|
||||
const { settings } = state
|
||||
if (settings.sample) {
|
||||
const { pattern } = sample(Design, settings)
|
||||
const renderProps = pattern.getRenderProps()
|
||||
const output = (
|
||||
<ZoomablePattern
|
||||
renderProps={renderProps}
|
||||
patternLocale={state.locale || 'en'}
|
||||
rotate={state.ui.rotate}
|
||||
/>
|
||||
)
|
||||
|
||||
return <PatternLayout {...{ update, Design, output, state, pattern, config }} />
|
||||
}
|
||||
|
||||
// Translated measurements
|
||||
const trm = orderBy(
|
||||
Design.patternConfig.measurements.map((m) => ({ m, t: measurementsTranslations[m] })),
|
||||
't',
|
||||
'ASC'
|
||||
)
|
||||
const tom = orderBy(
|
||||
Design.patternConfig.optionalMeasurements.map((m) => ({ m, t: measurementsTranslations[m] })),
|
||||
't',
|
||||
'ASC'
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderMenu state={state} {...{ config, update, Design }} />
|
||||
<div className="tw-m-auto tw-mt-8 tw-max-w-4xl tw-px-4 tw-mb-8">
|
||||
<H1>Test Pattern</H1>
|
||||
<div className="tw-grid tw-grid-cols-1 lg:tw-grid-cols-2 lg:tw-gap-4">
|
||||
<div className="tw-flex tw-flex-col tw-gap-4">
|
||||
<H3>Test Design Options</H3>
|
||||
<SampleOptionsMenu {...{ Design, state, update }} />
|
||||
</div>
|
||||
{trm.length > 0 ? (
|
||||
<div className="tw-flex tw-flex-col tw-gap-4">
|
||||
<H3>Test Measurements</H3>
|
||||
<H4>Required Measurements</H4>
|
||||
<div className="">
|
||||
{trm.map(({ t, m }) => (
|
||||
<button
|
||||
key={m}
|
||||
className="tw-my-0.5 tw-block tw-daisy-btn tw-daisy-btn-primary tw-daisy-btn-outline tw-daisy-btn-xs"
|
||||
onClick={() =>
|
||||
update.settings(['sample'], { type: 'measurement', measurement: m })
|
||||
}
|
||||
>
|
||||
{t}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{tom.length > 0 ? (
|
||||
<div className="tw-flex tw-flex-col tw-gap-4">
|
||||
<H4>Optional Measurements</H4>
|
||||
<div className="">
|
||||
{tom.map(({ t, m }) => (
|
||||
<button
|
||||
key={m}
|
||||
className="tw-my-0.5 tw-block tw-daisy-btn tw-daisy-btn-primary tw-daisy-btn-outline tw-daisy-btn-xs"
|
||||
onClick={() =>
|
||||
update.settings(['sample'], { type: 'measurement', measurement: m })
|
||||
}
|
||||
>
|
||||
{t}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const SampleOptionsMenu = ({ Design, state, update }) => {
|
||||
const structure = useMemo(
|
||||
() =>
|
||||
menuDesignOptionsStructure(
|
||||
Design.designConfig.data.id,
|
||||
Design.patternConfig.options,
|
||||
state.settings
|
||||
),
|
||||
[Design.designConfig.data.id, Design.patternConfig, state.settings]
|
||||
)
|
||||
|
||||
return <SampleOptionsSubMenu structure={structure} update={update} />
|
||||
}
|
||||
|
||||
const SampleOptionsSubMenu = ({ structure, update, level = 1 }) => {
|
||||
const output = []
|
||||
/*
|
||||
* Show entries alphabetic, but force groups last, and advanced last among them
|
||||
*/
|
||||
const titles = Object.keys(structure)
|
||||
.filter((key) => key !== 'isGroup')
|
||||
.sort()
|
||||
const order = [
|
||||
...titles.filter((key) => key !== 'advanced'),
|
||||
...titles.filter((key) => key === 'advanced'),
|
||||
]
|
||||
// Non-groups first
|
||||
for (const name of order) {
|
||||
const struct = structure[name]
|
||||
if (!struct.isGroup)
|
||||
output.push(
|
||||
<button
|
||||
key={name}
|
||||
className="tw-my-0.5 tw-block tw-daisy-btn tw-daisy-btn-primary tw-daisy-btn-outline tw-daisy-btn-xs"
|
||||
onClick={() => update.settings(['sample'], { type: 'option', option: name })}
|
||||
>
|
||||
{struct.title}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
// Then groups
|
||||
for (const name of order) {
|
||||
const struct = structure[name]
|
||||
if (struct.isGroup) {
|
||||
output.push(
|
||||
<H5 key={name}>
|
||||
<span className="tw-capitalize">{name}</span>
|
||||
</H5>
|
||||
)
|
||||
output.push(
|
||||
<SampleOptionsSubMenu
|
||||
structure={struct}
|
||||
update={update}
|
||||
level={level + 1}
|
||||
key={name + 's'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return <div className="tw-pl-2 tw-border-l-2">{output}</div>
|
||||
}
|
|
@ -10,6 +10,7 @@ import { UndosView } from './UndosView.mjs'
|
|||
import { LayoutView } from './LayoutView.mjs'
|
||||
import { DocsView } from './DocsView.mjs'
|
||||
import { LogView } from './LogView.mjs'
|
||||
import { TestView } from './TestView.mjs'
|
||||
import { EditSettingsView } from './EditSettingsView.mjs'
|
||||
import { ErrorIcon } from '@freesewing/react/components/Icon'
|
||||
import {
|
||||
|
@ -67,6 +68,7 @@ export const View = (props) => {
|
|||
if (view === 'editSettings') return <EditSettingsView {...props} />
|
||||
if (view === 'inspect') return <InspectView {...props} />
|
||||
if (view === 'logs') return <LogView {...props} />
|
||||
if (view === 'test') return <TestView {...props} />
|
||||
|
||||
return <h1 className="tw-ext-center tw-my-12">No view component for view {props.view}</h1>
|
||||
}
|
||||
|
|
|
@ -46,6 +46,37 @@ export function draft(Design, settings, plugins = []) {
|
|||
return data
|
||||
}
|
||||
|
||||
/*
|
||||
* This method samples a pattern option
|
||||
*
|
||||
* @param {function} Design - The Design constructor
|
||||
* @param {object} settings - The settings for the pattern
|
||||
* @param {array} plugins - Any (extra) plugins to load into the pattern
|
||||
* @return {object} data - The drafted pattern, along with errors and failure data
|
||||
*/
|
||||
export function sample(Design, settings, plugins = []) {
|
||||
const pattern = new Design(settings)
|
||||
for (const plugin of plugins) pattern.use(plugin)
|
||||
const data = {
|
||||
// The pattern
|
||||
pattern,
|
||||
// Any errors logged by the pattern
|
||||
errors: [],
|
||||
// If the pattern fails to draft, this will hold the error
|
||||
failure: false,
|
||||
}
|
||||
// Draft the pattern or die trying
|
||||
try {
|
||||
data.pattern.sample()
|
||||
data.errors.push(...data.pattern.store.logs.error)
|
||||
for (const store of data.pattern.setStores) data.errors.push(...store.logs.error)
|
||||
} catch (error) {
|
||||
data.failure = error
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export function flattenFlags(flags) {
|
||||
const all = {}
|
||||
for (const type of defaultConfig.flagTypes) {
|
||||
|
|
|
@ -29,6 +29,7 @@ import {
|
|||
notEmpty,
|
||||
nsMerge,
|
||||
objUpdate,
|
||||
sample,
|
||||
settingsValueIsCustom,
|
||||
settingsValueCustomOrDefault,
|
||||
statePrefixPath,
|
||||
|
@ -89,6 +90,7 @@ export {
|
|||
notEmpty,
|
||||
nsMerge,
|
||||
objUpdate,
|
||||
sample,
|
||||
settingsValueIsCustom,
|
||||
settingsValueCustomOrDefault,
|
||||
statePrefixPath,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue