1
0
Fork 0
freesewing/packages/react/components/Editor/components/views/TestView.mjs
2025-06-01 17:04:47 +02:00

214 lines
7.5 KiB
JavaScript

// Dependencies
import React, { useMemo } from 'react'
import {
sample,
missingMeasurements,
menuDesignOptionsStructure,
bundlePatternTranslations,
} from '../../lib/index.mjs'
import { measurements as measurementsTranslations } from '@freesewing/i18n'
import { orderBy } from '@freesewing/utils'
// Components
import { Null } from '@freesewing/react/components/Null'
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'
import { OptionsIcon, MeasurementsIcon } from '@freesewing/react/components/Icon'
/**
* 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) {
/*
* When testing/sampling one design, and then switching the editor to a different design,
* we run the risk that settings.sample holds invalid configuration. Like testing an unused
* measurement, or non-existing option.
* So here we try to check for that and only test/sample if things make sense
*/
if (
(settings.sample.type === 'option' &&
Object.keys(Design.patternConfig.options).includes(settings.sample.option)) ||
(settings.sample.type === 'measurement' &&
[
...Design.patternConfig.measurements,
...Design.patternConfig.optionalMeasurements,
].includes(settings.sample.measurement)) ||
settings.sample.type === 'models'
) {
const { pattern } = sample(Design, settings)
const renderProps = pattern.getRenderProps()
const strings = bundlePatternTranslations(pattern.designConfig.data.id)
const output = (
<ZoomablePattern
renderProps={renderProps}
patternLocale={state.locale || 'en'}
rotate={state.ui.rotate}
strings={strings}
/>
)
return <PatternLayout {...{ update, Design, output, state, pattern, config }} />
} else console.log('Not sampling', settings.sample)
}
// 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'
)
const btnClasses =
'tw:my-0.5 tw:block tw:daisy-btn tw:daisy-btn-primary tw:daisy-btn-outline tw:daisy-btn-xs tw:flex tw:flex-row tw:items-center tw:justify-between tw:w-full tw:max-w-64'
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 tw:lg:grid-cols-2 tw:lg: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={btnClasses}
onClick={() =>
update.settings(['sample'], { type: 'measurement', measurement: m })
}
>
<MeasurementsIcon className="tw:w-4 tw:h-4" />
<span>{t}</span>
</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={btnClasses}
onClick={() =>
update.settings(['sample'], { type: 'measurement', measurement: m })
}
>
<MeasurementsIcon className="tw:w-4 tw:h-4" />
<span>{t}</span>
</button>
))}
</div>
</div>
) : null}
</div>
) : null}
</div>
</div>
</>
)
}
const SampleOptionsMenu = ({ Design, state, update }) => {
const { settings = {} } = state // Guard against undefined settings
const structure = useMemo(
() =>
menuDesignOptionsStructure(
Design.designConfig.data.id,
Design.patternConfig.options,
settings
),
[Design.designConfig.data.id, Design.patternConfig, settings]
)
return <SampleOptionsSubMenu structure={structure} update={update} />
}
const SampleOptionsSubMenu = ({ structure, update, level = 1 }) => {
const output = []
const btnClasses =
'tw:my-0.5 tw:block tw:daisy-btn tw:daisy-btn-primary tw:daisy-btn-outline tw:daisy-btn-xs tw:flex tw:flex-row tw:items-center tw:justify-between tw:w-full tw:max-w-64'
/*
* 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={btnClasses}
onClick={() => update.settings(['sample'], { type: 'option', option: name })}
>
<OptionsIcon className="tw:w-4 tw:h-4" />
<span>{struct.title}</span>
</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>
}