diff --git a/frontend/components/template/CheckboxInput.tsx b/frontend/components/template/CheckboxInput.tsx index dec1138cecda4da53c49e73c8f40df364ae2a65a..364f8373fcdc1e91a835090946e6974413dd8d46 100644 --- a/frontend/components/template/CheckboxInput.tsx +++ b/frontend/components/template/CheckboxInput.tsx @@ -6,21 +6,30 @@ type CheckboxInput = { field: CutterField; flagged?: boolean; className?: string; + + truthy?: string | boolean; + falsy?: string | boolean; }; -const CheckboxInput: FC<CheckboxInput> = ({ field, flagged = false, className }) => { +const CheckboxInput: FC<CheckboxInput> = ({ + field, + flagged = false, + className, + truthy = true, + falsy = false, +}) => { + const classes = clsx('rounded input mt-0 mb-1 mr-2', flagged && 'border-warning', className); + return ( <div> + <input type="hidden" name={field.name} value={falsy.toString()} /> <input - className={clsx( - 'rounded input mt-0 mb-1 mr-2', - flagged && 'border-warning', - className - )} + className={classes} type="checkbox" name={field.name} id={field.name} - // TODO: type assertion due to api typing not being good enough - defaultChecked={field.default as boolean} + // TODO: type assertion due to generator issues + defaultChecked={field.default === truthy} + value={truthy.toString()} /> <label htmlFor={field.name}>{field.prompt ?? field.name}</label> </div> diff --git a/frontend/components/template/Form.tsx b/frontend/components/template/Form.tsx index cd4e8da48b3ecf681f70321d9e89abae31e399ee..21168537ca06661f461dd6b6b0bf5c1e643e3bd6 100644 --- a/frontend/components/template/Form.tsx +++ b/frontend/components/template/Form.tsx @@ -1,8 +1,7 @@ -import { BLANK_FIELD, LegalField, SelectField, StringField } from 'lib/template'; +import { BLANK_FIELD } from 'lib/template'; import React, { FC } from 'react'; import SelectInput from './SelectInput'; import TextInput from './TextInput'; -import Badge from 'components/Badge'; import { CutterField } from 'lib/client'; import CheckboxInput from 'components/template/CheckboxInput'; import ErrorBox from 'components/ErrorBox'; @@ -14,8 +13,6 @@ const Formfield: FC<FormFieldProps> = ({ field, flagged }) => { {field.default === BLANK_FIELD && <div>{field.prompt ?? field.name}</div>} {field.default != BLANK_FIELD && ( <> - <label htmlFor={field.name}>{field.prompt ?? field.name}</label>{' '} - {flagged && <Badge type="warning">Missing</Badge>} {field.type === 'text' ? ( <TextInput field={field} flagged={flagged} className="mt-1" /> ) : field.type === 'select' ? ( diff --git a/frontend/components/template/SelectInput.tsx b/frontend/components/template/SelectInput.tsx index 4ea8eb50ee6919ec5182afe2408b9722dd9d2a0b..6c20da551d136ece558bcb1a7efb8d9a51a8062e 100644 --- a/frontend/components/template/SelectInput.tsx +++ b/frontend/components/template/SelectInput.tsx @@ -1,6 +1,9 @@ import { FC } from 'react'; import clsx from 'clsx'; import { CutterField } from 'lib/client'; +import CheckboxInput from 'components/template/CheckboxInput'; +import { attemptDetermineYesNoOptions } from 'components/template/yesOrNo'; +import Badge from 'components/Badge'; type SelectInputProps = { field: CutterField; @@ -8,18 +11,35 @@ type SelectInputProps = { className?: string; }; const SelectInput: FC<SelectInputProps> = ({ field, flagged = false, className }) => { + const yesNoOptions = field.options != null && attemptDetermineYesNoOptions(field.options); + + if (yesNoOptions !== false) { + return ( + <CheckboxInput + field={field} + className="mt-1" + truthy={yesNoOptions.truthy} + falsy={yesNoOptions.falsy} + /> + ); + } + return ( - <select - name={field.name} - id={field.name} - className={clsx('rounded', flagged && 'border-warning', className)} - > - {field.options?.map((option) => ( - <option value={option.name} key={option.name}> - {option.prompt ?? option.name} - </option> - ))} - </select> + <> + <label htmlFor={field.name}>{field.prompt ?? field.name}</label>{' '} + {flagged && <Badge type="warning">Missing</Badge>} + <select + name={field.name} + id={field.name} + className={clsx('rounded', flagged && 'border-warning', className)} + > + {field.options?.map((option) => ( + <option value={option.name} key={option.name}> + {option.prompt ?? option.name} + </option> + ))} + </select> + </> ); }; diff --git a/frontend/components/template/TextInput.tsx b/frontend/components/template/TextInput.tsx index 06f0901f2e766340d1dc00ad4e5d4fca03fb6f27..12f3f445639e0e9627f1c73316c4d5b6aed4f986 100644 --- a/frontend/components/template/TextInput.tsx +++ b/frontend/components/template/TextInput.tsx @@ -1,6 +1,7 @@ import { FC } from 'react'; import clsx from 'clsx'; import { CutterField } from 'lib/client'; +import Badge from 'components/Badge'; type TextInputProps = { field: CutterField; @@ -9,14 +10,18 @@ type TextInputProps = { }; const TextInput: FC<TextInputProps> = ({ field, flagged = false, className }) => { return ( - <input - className={clsx('rounded input', flagged && 'border-warning', className)} - type="text" - name={field.name} - id={field.name} - // TODO: type assertion due to api typing not being good enough - placeholder={field.default as string} - /> + <> + <label htmlFor={field.name}>{field.prompt ?? field.name}</label>{' '} + {flagged && <Badge type="warning">Missing</Badge>} + <input + className={clsx('rounded input', flagged && 'border-warning', className)} + type="text" + name={field.name} + id={field.name} + // TODO: type assertion due to api typing not being good enough + placeholder={field.default as string} + /> + </> ); }; diff --git a/frontend/components/template/yesOrNo.ts b/frontend/components/template/yesOrNo.ts new file mode 100644 index 0000000000000000000000000000000000000000..a25117ae423327b87a0a7f1ed5c9eecef186bf5f --- /dev/null +++ b/frontend/components/template/yesOrNo.ts @@ -0,0 +1,42 @@ +// referencing defaults in https://cookiecutter.readthedocs.io/en/2.3.0/cookiecutter.html#cookiecutter.prompt.YesNoPrompt +import { CutterOption } from 'lib/client'; + +export const TRUTHY_DEFAULTS = ['1', 'true', 't', 'yes', 'y', 'on']; +export const FALSY_DEFAULTS = ['0', 'false', 'f', 'no', 'n', 'off']; + +export const attemptDetermineYesNoOptions = ( + options: CutterOption[] +): + | { + truthy: string; + falsy: string; + } + | false => { + if (options.length !== 2) { + return false; + } + + // do not use a checkbox if the select options have a label because context may ge tlost + if (options.some((o) => o.prompt != null)) { + return false; + } + + // ensure alphabetical order in checks! + const [first, second] = options.map((o) => o.name).sort(); + + if ( + (first === '0' && second === '1') || + (first === 'false' && second === 'true') || + (first === 'f' && second === 't') || + (first === 'no' && second === 'yes') || + (first === 'n' && second === 'y') || + (first === 'off' && second === 'on') + ) { + return { + truthy: second, + falsy: first, + }; + } + + return false; +};