From 11ab76cc7a5524088792910b39bf45023267444f Mon Sep 17 00:00:00 2001 From: Jamie Howard Date: Thu, 12 Jan 2023 15:13:35 +0000 Subject: [PATCH 01/17] feat(content-manager): display min/max limits with field description --- .../components/Inputs/index.js | 20 ++++++-- .../Inputs/utils/fieldDescription.js | 37 ++++++++++++++ .../components/Inputs/utils/index.js | 1 + .../Inputs/utils/tests/index.test.js | 48 +++++++++++++++++++ .../services/utils/configuration/metadatas.js | 5 +- 5 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 packages/core/admin/admin/src/content-manager/components/Inputs/utils/fieldDescription.js create mode 100644 packages/core/admin/admin/src/content-manager/components/Inputs/utils/tests/index.test.js diff --git a/packages/core/admin/admin/src/content-manager/components/Inputs/index.js b/packages/core/admin/admin/src/content-manager/components/Inputs/index.js index 2e55e1750c..eec636c247 100644 --- a/packages/core/admin/admin/src/content-manager/components/Inputs/index.js +++ b/packages/core/admin/admin/src/content-manager/components/Inputs/index.js @@ -14,6 +14,7 @@ import InputUID from '../InputUID'; import { RelationInputDataManager } from '../RelationInputDataManager'; import { + buildDescription, connect, generateOptions, getInputType, @@ -166,6 +167,8 @@ function Inputs({ ); const { label, description, placeholder, visible } = metadatas; + const { minLength, maxLength } = fieldSchema; + const builtDescription = buildDescription(description, minLength, maxLength); /** * It decides whether using the default `step` accoding to its `inputType` or the one @@ -218,10 +221,11 @@ function Inputs({ {...fieldSchema} componentUid={componentUid} description={ - metadatas.description + builtDescription.id ? formatMessage({ - id: metadatas.description, - defaultMessage: metadatas.description, + id: builtDescription.id, + defaultMessage: builtDescription.defaultMessage, + values: builtDescription.values, }) : undefined } @@ -265,7 +269,15 @@ function Inputs({ intlLabel={{ id: label, defaultMessage: label }} // in case the default value of the boolean is null, attribute.default doesn't exist isNullable={inputType === 'bool' && [null, undefined].includes(fieldSchema.default)} - description={description ? { id: description, defaultMessage: description } : null} + description={ + builtDescription.id + ? { + id: builtDescription.id, + defaultMessage: builtDescription.defaultMessage, + values: builtDescription.values, + } + : null + } disabled={shouldDisableField} error={error} labelAction={labelAction} diff --git a/packages/core/admin/admin/src/content-manager/components/Inputs/utils/fieldDescription.js b/packages/core/admin/admin/src/content-manager/components/Inputs/utils/fieldDescription.js new file mode 100644 index 0000000000..cf5a7f8af4 --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/components/Inputs/utils/fieldDescription.js @@ -0,0 +1,37 @@ +import React from 'react'; + +/** + * Constructs a suitable description, taking into account a fields minimum and + * maximum length + * + * @param {String} description - the fields description + * @param {Number} minLength - the minimum length of the field + * @param {Number} maxLength - the maximum length of the field + * @returns + */ +const buildDescription = (description, minLength, maxLength) => { + const minMaxDescription = []; + + if (minLength) { + minMaxDescription.push(`min. ${minLength}`); + } + if (minLength && maxLength) { + minMaxDescription.push(`/`); + } + if (maxLength) { + minMaxDescription.push(`max. ${maxLength}`); + } + if (minMaxDescription.length > 0) { + minMaxDescription.push(`characters{br}`); + } + + return { + id: description, + defaultMessage: `${minMaxDescription.join(' ')}${description}`, + values: { + br:
, + }, + }; +}; + +export default buildDescription; diff --git a/packages/core/admin/admin/src/content-manager/components/Inputs/utils/index.js b/packages/core/admin/admin/src/content-manager/components/Inputs/utils/index.js index 607ae6336b..fb45371127 100644 --- a/packages/core/admin/admin/src/content-manager/components/Inputs/utils/index.js +++ b/packages/core/admin/admin/src/content-manager/components/Inputs/utils/index.js @@ -1,3 +1,4 @@ +export { default as buildDescription } from './fieldDescription'; export { default as connect } from './connect'; export { default as generateOptions } from './generateOptions'; export { default as getInputType } from './getInputType'; diff --git a/packages/core/admin/admin/src/content-manager/components/Inputs/utils/tests/index.test.js b/packages/core/admin/admin/src/content-manager/components/Inputs/utils/tests/index.test.js new file mode 100644 index 0000000000..fbfdc1a2b0 --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/components/Inputs/utils/tests/index.test.js @@ -0,0 +1,48 @@ +import React from 'react'; +import { buildDescription } from '../index'; + +describe('CONTENT MANAGER | Inputs | Utils', () => { + describe('fieldDescription', () => { + const description = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'; + const values = { br:
}; + + it('correctly generates field description', () => { + const minLength = 2; + const maxLength = 100; + + const result = buildDescription(description, minLength, maxLength); + expect(result).toEqual({ + defaultMessage: `min. ${minLength} / max. ${maxLength} characters{br}${description}`, + id: description, + values, + }); + }); + + describe('correctly ignores omissions', () => { + it('minLength', () => { + const minLength = 0; + const maxLength = 100; + + const result = buildDescription(description, minLength, maxLength); + expect(result).toEqual({ + defaultMessage: `max. ${maxLength} characters{br}${description}`, + id: description, + values, + }); + }); + + it('maxLength', () => { + const minLength = 5; + const maxLength = 0; + + const result = buildDescription(description, minLength, maxLength); + expect(result).toEqual({ + defaultMessage: `min. ${minLength} characters{br}${description}`, + id: description, + values, + }); + }); + }); + }); +}); diff --git a/packages/core/content-manager/server/services/utils/configuration/metadatas.js b/packages/core/content-manager/server/services/utils/configuration/metadatas.js index 5033970694..406055e1b0 100644 --- a/packages/core/content-manager/server/services/utils/configuration/metadatas.js +++ b/packages/core/content-manager/server/services/utils/configuration/metadatas.js @@ -36,8 +36,9 @@ function createDefaultMetadata(schema, name) { editable: true, }; - if (isRelation(schema.attributes[name])) { - const { targetModel } = schema.attributes[name]; + const fieldAttributes = schema.attributes[name]; + if (isRelation(fieldAttributes)) { + const { targetModel } = fieldAttributes; const targetSchema = getTargetSchema(targetModel); From 1eec59973aa78c7252a7ac71194b48327d8a5dea Mon Sep 17 00:00:00 2001 From: Jamie Howard Date: Fri, 13 Jan 2023 13:36:56 +0000 Subject: [PATCH 02/17] feat(content-manager): cover all field types feat(content-manager): construct min/max description separately from main description --- .../components/InputUID/index.js | 17 ++++- .../components/InputUID/tests/index.test.js | 13 ++++ .../components/Inputs/index.js | 32 ++++---- .../Inputs/utils/fieldDescription.js | 37 --------- .../components/Inputs/utils/index.js | 2 +- .../Inputs/utils/minMaxDescription.js | 47 ++++++++++++ .../Inputs/utils/tests/index.test.js | 75 ++++++++++++------- .../lib/src/components/GenericInput/index.js | 31 ++++++-- .../tests/__snapshots__/index.test.js.snap | 17 ++++- 9 files changed, 185 insertions(+), 86 deletions(-) delete mode 100644 packages/core/admin/admin/src/content-manager/components/Inputs/utils/fieldDescription.js create mode 100644 packages/core/admin/admin/src/content-manager/components/Inputs/utils/minMaxDescription.js diff --git a/packages/core/admin/admin/src/content-manager/components/InputUID/index.js b/packages/core/admin/admin/src/content-manager/components/InputUID/index.js index ea443a1382..381ac540f3 100644 --- a/packages/core/admin/admin/src/content-manager/components/InputUID/index.js +++ b/packages/core/admin/admin/src/content-manager/components/InputUID/index.js @@ -24,6 +24,7 @@ const InputUID = ({ attribute, contentTypeUID, description, + minMaxDescription, disabled, error, intlLabel, @@ -68,6 +69,14 @@ const InputUID = ({ ) : ''; + const formattedMinMaxDescription = minMaxDescription + ? formatMessage( + { id: minMaxDescription.id, defaultMessage: minMaxDescription.defaultMessage }, + { ...minMaxDescription.values } + ) + : []; + const combinedHint = [...formattedMinMaxDescription, hint]; + generateUid.current = async (shouldSetInitialValue = false) => { setIsLoading(true); const requestURL = getRequestUrl('uid/generate'); @@ -233,7 +242,7 @@ const InputUID = ({ } - hint={hint} + hint={combinedHint} label={label} labelAction={labelAction} name={name} @@ -256,6 +265,11 @@ InputUID.propTypes = { defaultMessage: PropTypes.string.isRequired, values: PropTypes.object, }), + minMaxDescription: PropTypes.shape({ + id: PropTypes.string.isRequired, + defaultMessage: PropTypes.string.isRequired, + values: PropTypes.object, + }), disabled: PropTypes.bool, error: PropTypes.string, intlLabel: PropTypes.shape({ @@ -277,6 +291,7 @@ InputUID.propTypes = { InputUID.defaultProps = { description: undefined, + minMaxDescription: undefined, disabled: false, error: undefined, labelAction: undefined, diff --git a/packages/core/admin/admin/src/content-manager/components/InputUID/tests/index.test.js b/packages/core/admin/admin/src/content-manager/components/InputUID/tests/index.test.js index 010e169726..77c82ffa63 100644 --- a/packages/core/admin/admin/src/content-manager/components/InputUID/tests/index.test.js +++ b/packages/core/admin/admin/src/content-manager/components/InputUID/tests/index.test.js @@ -105,6 +105,12 @@ describe('', () => { color: #32324d; } + .c12 { + font-size: 0.75rem; + line-height: 1.33; + color: #666687; + } + .c1 > * { margin-top: 0; margin-bottom: 0; @@ -233,6 +239,7 @@ describe('', () => { class="c4 c5" > ', () => { +

+ +

diff --git a/packages/core/admin/admin/src/content-manager/components/Inputs/index.js b/packages/core/admin/admin/src/content-manager/components/Inputs/index.js index eec636c247..eb0a9d5e47 100644 --- a/packages/core/admin/admin/src/content-manager/components/Inputs/index.js +++ b/packages/core/admin/admin/src/content-manager/components/Inputs/index.js @@ -14,7 +14,7 @@ import InputUID from '../InputUID'; import { RelationInputDataManager } from '../RelationInputDataManager'; import { - buildDescription, + buildMinMaxDescription, connect, generateOptions, getInputType, @@ -167,8 +167,6 @@ function Inputs({ ); const { label, description, placeholder, visible } = metadatas; - const { minLength, maxLength } = fieldSchema; - const builtDescription = buildDescription(description, minLength, maxLength); /** * It decides whether using the default `step` accoding to its `inputType` or the one @@ -221,11 +219,10 @@ function Inputs({ {...fieldSchema} componentUid={componentUid} description={ - builtDescription.id + metadatas.description ? formatMessage({ - id: builtDescription.id, - defaultMessage: builtDescription.defaultMessage, - values: builtDescription.values, + id: metadatas.description, + defaultMessage: metadatas.description, }) : undefined } @@ -262,6 +259,14 @@ function Inputs({ ...customFieldInputs, }; + const { minLength, maxLength, max, min } = fieldSchema; + const genericInputType = customFieldUid || inputType; + const minMaxDescription = buildMinMaxDescription( + genericInputType, + min || minLength, + max || maxLength + ); + return ( { - const minMaxDescription = []; - - if (minLength) { - minMaxDescription.push(`min. ${minLength}`); - } - if (minLength && maxLength) { - minMaxDescription.push(`/`); - } - if (maxLength) { - minMaxDescription.push(`max. ${maxLength}`); - } - if (minMaxDescription.length > 0) { - minMaxDescription.push(`characters{br}`); - } - - return { - id: description, - defaultMessage: `${minMaxDescription.join(' ')}${description}`, - values: { - br:
, - }, - }; -}; - -export default buildDescription; diff --git a/packages/core/admin/admin/src/content-manager/components/Inputs/utils/index.js b/packages/core/admin/admin/src/content-manager/components/Inputs/utils/index.js index fb45371127..e2042fe0e3 100644 --- a/packages/core/admin/admin/src/content-manager/components/Inputs/utils/index.js +++ b/packages/core/admin/admin/src/content-manager/components/Inputs/utils/index.js @@ -1,4 +1,4 @@ -export { default as buildDescription } from './fieldDescription'; +export { default as buildMinMaxDescription } from './minMaxDescription'; export { default as connect } from './connect'; export { default as generateOptions } from './generateOptions'; export { default as getInputType } from './getInputType'; diff --git a/packages/core/admin/admin/src/content-manager/components/Inputs/utils/minMaxDescription.js b/packages/core/admin/admin/src/content-manager/components/Inputs/utils/minMaxDescription.js new file mode 100644 index 0000000000..0dc8b1e5af --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/components/Inputs/utils/minMaxDescription.js @@ -0,0 +1,47 @@ +import React from 'react'; + +/** + * Constructs a suitable description, taking into account a fields minimum and + * maximum length + * + * @param {String} type - the type of field + * @param {Number} min - the minimum length or value of the field + * @param {Number} max - the maximum length or value of the field + * @returns {(null|Object)} + */ +const buildMinMaxDescription = (type, min, max) => { + if (!(typeof min === 'number' || typeof max === 'number')) { + return null; + } + if ( + !['number', 'email', 'timestamp', 'text', 'string', 'password', 'textarea', 'uid'].includes( + type + ) + ) { + return null; + } + + const minMaxDescription = []; + + if (min) { + minMaxDescription.push(`min. {min}`); + } + if (max) { + minMaxDescription.push(`max. {max}`); + } + + return { + id: 'content-manager.form.Input.minMaxDescription', + defaultMessage: + minMaxDescription.length === 0 + ? `` + : `${minMaxDescription.join(' / ')}${type === 'number' ? '{br}' : ' characters{br}'}`, + values: { + min, + max, + br:
, + }, + }; +}; + +export default buildMinMaxDescription; diff --git a/packages/core/admin/admin/src/content-manager/components/Inputs/utils/tests/index.test.js b/packages/core/admin/admin/src/content-manager/components/Inputs/utils/tests/index.test.js index fbfdc1a2b0..29243a6402 100644 --- a/packages/core/admin/admin/src/content-manager/components/Inputs/utils/tests/index.test.js +++ b/packages/core/admin/admin/src/content-manager/components/Inputs/utils/tests/index.test.js @@ -1,46 +1,69 @@ import React from 'react'; -import { buildDescription } from '../index'; +import { buildMinMaxDescription } from '../index'; describe('CONTENT MANAGER | Inputs | Utils', () => { describe('fieldDescription', () => { - const description = - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'; - const values = { br:
}; + const id = 'content-manager.form.Input.minMaxDescription'; + const values = (min, max) => ({ br:
, min, max }); - it('correctly generates field description', () => { - const minLength = 2; - const maxLength = 100; + it('ignores unsupported field types', () => { + const result = buildMinMaxDescription('someType'); + expect(result).toBeNull(); + }); - const result = buildDescription(description, minLength, maxLength); - expect(result).toEqual({ - defaultMessage: `min. ${minLength} / max. ${maxLength} characters{br}${description}`, - id: description, - values, + it('expects one of min or max to be a number ', () => { + const result = buildMinMaxDescription('text', null, 'test'); + expect(result).toBeNull(); + }); + + describe('correctly generates field description', () => { + it('text field', () => { + const min = 2; + const max = 100; + + const result = buildMinMaxDescription('text', min, max); + expect(result).toEqual({ + id, + defaultMessage: `min. {min} / max. {max} characters{br}`, + values: values(min, max), + }); + }); + + it('number field', () => { + const min = 2; + const max = 100; + + const result = buildMinMaxDescription('number', min, max); + expect(result).toEqual({ + id, + defaultMessage: `min. {min} / max. {max}{br}`, + values: values(min, max), + }); }); }); describe('correctly ignores omissions', () => { - it('minLength', () => { - const minLength = 0; - const maxLength = 100; + it('min', () => { + const min = 0; + const max = 100; - const result = buildDescription(description, minLength, maxLength); + const result = buildMinMaxDescription('text', min, max); expect(result).toEqual({ - defaultMessage: `max. ${maxLength} characters{br}${description}`, - id: description, - values, + id, + defaultMessage: `max. {max} characters{br}`, + values: values(min, max), }); }); - it('maxLength', () => { - const minLength = 5; - const maxLength = 0; + it('max', () => { + const min = 5; + const max = 0; - const result = buildDescription(description, minLength, maxLength); + const result = buildMinMaxDescription('text', min, max); expect(result).toEqual({ - defaultMessage: `min. ${minLength} characters{br}${description}`, - id: description, - values, + id, + defaultMessage: `min. {min} characters{br}`, + values: values(min, max), }); }); }); diff --git a/packages/core/helper-plugin/lib/src/components/GenericInput/index.js b/packages/core/helper-plugin/lib/src/components/GenericInput/index.js index 1b584b0076..13e41cd36a 100644 --- a/packages/core/helper-plugin/lib/src/components/GenericInput/index.js +++ b/packages/core/helper-plugin/lib/src/components/GenericInput/index.js @@ -30,6 +30,7 @@ const GenericInput = ({ autoComplete, customInputs, description, + minMaxDescription, disabled, intlLabel, labelAction, @@ -92,6 +93,7 @@ const GenericInput = ({ `Date picker, current is ${formattedDate}`} - selectButtonTitle={formatMessage({ id: 'selectButtonTitle', defaultMessage: 'Select' })} + selectButtonTitle={formatMessage({ + id: 'selectButtonTitle', + defaultMessage: 'Select', + })} /> ); } @@ -252,7 +265,7 @@ const GenericInput = ({ label={label} labelAction={labelAction} id={name} - hint={hint} + hint={combinedHint} name={name} onValueChange={(value) => onChange({ target: { name, value, type } })} placeholder={formattedPlaceholder} @@ -271,7 +284,7 @@ const GenericInput = ({ label={label} labelAction={labelAction} id={name} - hint={hint} + hint={combinedHint} name={name} onChange={onChange} placeholder={formattedPlaceholder} @@ -292,7 +305,7 @@ const GenericInput = ({ label={label} labelAction={labelAction} id={name} - hint={hint} + hint={combinedHint} name={name} onChange={onChange} placeholder={formattedPlaceholder} @@ -334,7 +347,7 @@ const GenericInput = ({ label={label} labelAction={labelAction} id={name} - hint={hint} + hint={combinedHint} name={name} onChange={onChange} placeholder={formattedPlaceholder} @@ -377,7 +390,7 @@ const GenericInput = ({ label={label} labelAction={labelAction} id={name} - hint={hint} + hint={combinedHint} name={name} onChange={onChange} required={required} @@ -442,6 +455,7 @@ GenericInput.defaultProps = { autoComplete: undefined, customInputs: null, description: null, + minMaxDescription: null, disabled: false, error: '', isNullable: undefined, @@ -461,6 +475,11 @@ GenericInput.propTypes = { defaultMessage: PropTypes.string.isRequired, values: PropTypes.object, }), + minMaxDescription: PropTypes.shape({ + id: PropTypes.string.isRequired, + defaultMessage: PropTypes.string.isRequired, + values: PropTypes.object, + }), disabled: PropTypes.bool, error: PropTypes.oneOfType([ PropTypes.string, diff --git a/packages/core/helper-plugin/lib/src/components/GenericInput/tests/__snapshots__/index.test.js.snap b/packages/core/helper-plugin/lib/src/components/GenericInput/tests/__snapshots__/index.test.js.snap index 873b1ace29..2ede9e21d9 100644 --- a/packages/core/helper-plugin/lib/src/components/GenericInput/tests/__snapshots__/index.test.js.snap +++ b/packages/core/helper-plugin/lib/src/components/GenericInput/tests/__snapshots__/index.test.js.snap @@ -14,6 +14,12 @@ exports[`GenericInput number renders and matches the snapshot 1`] = ` color: #d02b20; } +.c14 { + font-size: 0.75rem; + line-height: 1.33; + color: #666687; +} + .c9 { padding-right: 12px; padding-left: 8px; @@ -82,7 +88,7 @@ exports[`GenericInput number renders and matches the snapshot 1`] = ` fill: #8e8ea9; } -.c14 { +.c15 { border: 0; -webkit-clip: rect(0 0 0 0); clip: rect(0 0 0 0); @@ -227,6 +233,7 @@ exports[`GenericInput number renders and matches the snapshot 1`] = ` class="c6 c7" > +

+ +

Date: Mon, 16 Jan 2023 16:43:45 +0000 Subject: [PATCH 03/17] feat(helper-plugin): handle hint generation --- .../components/InputUID/index.js | 33 ++----- .../components/InputUID/tests/index.test.js | 13 --- .../components/Inputs/index.js | 20 +--- .../components/Inputs/utils/index.js | 1 - .../Inputs/utils/minMaxDescription.js | 47 ---------- .../Inputs/utils/tests/index.test.js | 71 -------------- .../lib/src/components/GenericInput/index.js | 64 ++++++------- .../tests/__snapshots__/index.test.js.snap | 17 +--- .../lib/src/hooks/useFieldHint/index.js | 90 ++++++++++++++++++ .../useFieldHint/tests/useFieldHint.test.js | 94 +++++++++++++++++++ packages/core/helper-plugin/lib/src/index.js | 1 + 11 files changed, 228 insertions(+), 223 deletions(-) delete mode 100644 packages/core/admin/admin/src/content-manager/components/Inputs/utils/minMaxDescription.js delete mode 100644 packages/core/admin/admin/src/content-manager/components/Inputs/utils/tests/index.test.js create mode 100644 packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js create mode 100644 packages/core/helper-plugin/lib/src/hooks/useFieldHint/tests/useFieldHint.test.js diff --git a/packages/core/admin/admin/src/content-manager/components/InputUID/index.js b/packages/core/admin/admin/src/content-manager/components/InputUID/index.js index 381ac540f3..dd00cfc8b6 100644 --- a/packages/core/admin/admin/src/content-manager/components/InputUID/index.js +++ b/packages/core/admin/admin/src/content-manager/components/InputUID/index.js @@ -1,6 +1,6 @@ import React, { useEffect, useState, useRef } from 'react'; import PropTypes from 'prop-types'; -import { useCMEditViewDataManager } from '@strapi/helper-plugin'; +import { useCMEditViewDataManager, useFieldHint } from '@strapi/helper-plugin'; import { useIntl } from 'react-intl'; import get from 'lodash/get'; import { TextInput } from '@strapi/design-system/TextInput'; @@ -24,7 +24,8 @@ const InputUID = ({ attribute, contentTypeUID, description, - minMaxDescription, + minimum, + maximum, disabled, error, intlLabel, @@ -36,6 +37,7 @@ const InputUID = ({ required, }) => { const { modifiedData, initialData, layout } = useCMEditViewDataManager(); + const { fieldHint } = useFieldHint({ description, minimum, maximum }); const [isLoading, setIsLoading] = useState(false); const [availability, setAvailability] = useState(null); const debouncedValue = useDebounce(value, 300); @@ -55,13 +57,6 @@ const InputUID = ({ ) : name; - const hint = description - ? formatMessage( - { id: description.id, defaultMessage: description.defaultMessage }, - { ...description.values } - ) - : ''; - const formattedPlaceholder = placeholder ? formatMessage( { id: placeholder.id, defaultMessage: placeholder.defaultMessage }, @@ -69,14 +64,6 @@ const InputUID = ({ ) : ''; - const formattedMinMaxDescription = minMaxDescription - ? formatMessage( - { id: minMaxDescription.id, defaultMessage: minMaxDescription.defaultMessage }, - { ...minMaxDescription.values } - ) - : []; - const combinedHint = [...formattedMinMaxDescription, hint]; - generateUid.current = async (shouldSetInitialValue = false) => { setIsLoading(true); const requestURL = getRequestUrl('uid/generate'); @@ -242,7 +229,7 @@ const InputUID = ({ } - hint={combinedHint} + hint={fieldHint} label={label} labelAction={labelAction} name={name} @@ -265,11 +252,6 @@ InputUID.propTypes = { defaultMessage: PropTypes.string.isRequired, values: PropTypes.object, }), - minMaxDescription: PropTypes.shape({ - id: PropTypes.string.isRequired, - defaultMessage: PropTypes.string.isRequired, - values: PropTypes.object, - }), disabled: PropTypes.bool, error: PropTypes.string, intlLabel: PropTypes.shape({ @@ -287,17 +269,20 @@ InputUID.propTypes = { values: PropTypes.object, }), required: PropTypes.bool, + minimum: PropTypes.number, + maximum: PropTypes.number, }; InputUID.defaultProps = { description: undefined, - minMaxDescription: undefined, disabled: false, error: undefined, labelAction: undefined, placeholder: undefined, value: '', required: false, + minimum: undefined, + maximum: undefined, }; export default InputUID; diff --git a/packages/core/admin/admin/src/content-manager/components/InputUID/tests/index.test.js b/packages/core/admin/admin/src/content-manager/components/InputUID/tests/index.test.js index 77c82ffa63..010e169726 100644 --- a/packages/core/admin/admin/src/content-manager/components/InputUID/tests/index.test.js +++ b/packages/core/admin/admin/src/content-manager/components/InputUID/tests/index.test.js @@ -105,12 +105,6 @@ describe('', () => { color: #32324d; } - .c12 { - font-size: 0.75rem; - line-height: 1.33; - color: #666687; - } - .c1 > * { margin-top: 0; margin-bottom: 0; @@ -239,7 +233,6 @@ describe('', () => { class="c4 c5" > ', () => {

-

- -

diff --git a/packages/core/admin/admin/src/content-manager/components/Inputs/index.js b/packages/core/admin/admin/src/content-manager/components/Inputs/index.js index eb0a9d5e47..295334bdf9 100644 --- a/packages/core/admin/admin/src/content-manager/components/Inputs/index.js +++ b/packages/core/admin/admin/src/content-manager/components/Inputs/index.js @@ -14,7 +14,6 @@ import InputUID from '../InputUID'; import { RelationInputDataManager } from '../RelationInputDataManager'; import { - buildMinMaxDescription, connect, generateOptions, getInputType, @@ -260,12 +259,6 @@ function Inputs({ }; const { minLength, maxLength, max, min } = fieldSchema; - const genericInputType = customFieldUid || inputType; - const minMaxDescription = buildMinMaxDescription( - genericInputType, - min || minLength, - max || maxLength - ); return ( { - if (!(typeof min === 'number' || typeof max === 'number')) { - return null; - } - if ( - !['number', 'email', 'timestamp', 'text', 'string', 'password', 'textarea', 'uid'].includes( - type - ) - ) { - return null; - } - - const minMaxDescription = []; - - if (min) { - minMaxDescription.push(`min. {min}`); - } - if (max) { - minMaxDescription.push(`max. {max}`); - } - - return { - id: 'content-manager.form.Input.minMaxDescription', - defaultMessage: - minMaxDescription.length === 0 - ? `` - : `${minMaxDescription.join(' / ')}${type === 'number' ? '{br}' : ' characters{br}'}`, - values: { - min, - max, - br:
, - }, - }; -}; - -export default buildMinMaxDescription; diff --git a/packages/core/admin/admin/src/content-manager/components/Inputs/utils/tests/index.test.js b/packages/core/admin/admin/src/content-manager/components/Inputs/utils/tests/index.test.js deleted file mode 100644 index 29243a6402..0000000000 --- a/packages/core/admin/admin/src/content-manager/components/Inputs/utils/tests/index.test.js +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; -import { buildMinMaxDescription } from '../index'; - -describe('CONTENT MANAGER | Inputs | Utils', () => { - describe('fieldDescription', () => { - const id = 'content-manager.form.Input.minMaxDescription'; - const values = (min, max) => ({ br:
, min, max }); - - it('ignores unsupported field types', () => { - const result = buildMinMaxDescription('someType'); - expect(result).toBeNull(); - }); - - it('expects one of min or max to be a number ', () => { - const result = buildMinMaxDescription('text', null, 'test'); - expect(result).toBeNull(); - }); - - describe('correctly generates field description', () => { - it('text field', () => { - const min = 2; - const max = 100; - - const result = buildMinMaxDescription('text', min, max); - expect(result).toEqual({ - id, - defaultMessage: `min. {min} / max. {max} characters{br}`, - values: values(min, max), - }); - }); - - it('number field', () => { - const min = 2; - const max = 100; - - const result = buildMinMaxDescription('number', min, max); - expect(result).toEqual({ - id, - defaultMessage: `min. {min} / max. {max}{br}`, - values: values(min, max), - }); - }); - }); - - describe('correctly ignores omissions', () => { - it('min', () => { - const min = 0; - const max = 100; - - const result = buildMinMaxDescription('text', min, max); - expect(result).toEqual({ - id, - defaultMessage: `max. {max} characters{br}`, - values: values(min, max), - }); - }); - - it('max', () => { - const min = 5; - const max = 0; - - const result = buildMinMaxDescription('text', min, max); - expect(result).toEqual({ - id, - defaultMessage: `min. {min} characters{br}`, - values: values(min, max), - }); - }); - }); - }); -}); diff --git a/packages/core/helper-plugin/lib/src/components/GenericInput/index.js b/packages/core/helper-plugin/lib/src/components/GenericInput/index.js index 13e41cd36a..1c56fd7d97 100644 --- a/packages/core/helper-plugin/lib/src/components/GenericInput/index.js +++ b/packages/core/helper-plugin/lib/src/components/GenericInput/index.js @@ -9,6 +9,7 @@ import PropTypes from 'prop-types'; import parseISO from 'date-fns/parseISO'; import formatISO from 'date-fns/formatISO'; import { useIntl } from 'react-intl'; + import { Checkbox, DatePicker, @@ -24,13 +25,14 @@ import { import { Option } from '@strapi/design-system/Select'; import EyeStriked from '@strapi/icons/EyeStriked'; import Eye from '@strapi/icons/Eye'; + import NotSupported from './NotSupported'; +import useFieldHint from '../../hooks/useFieldHint'; const GenericInput = ({ autoComplete, customInputs, description, - minMaxDescription, disabled, intlLabel, labelAction, @@ -44,9 +46,17 @@ const GenericInput = ({ type, value: defaultValue, isNullable, + minimum, + maximum, ...rest }) => { const { formatMessage } = useIntl(); + const { fieldHint } = useFieldHint({ + isNumber: type === 'number', + description, + minimum, + maximum, + }); const [showPassword, setShowPassword] = useState(false); const CustomInput = customInputs ? customInputs[type] : null; @@ -93,7 +103,8 @@ const GenericInput = ({ { @@ -207,7 +203,7 @@ const GenericInput = ({ label={label} labelAction={labelAction} id={name} - hint={hint} + hint={fieldHint} name={name} onChange={(date) => { const formattedDate = date.toISOString(); @@ -242,7 +238,7 @@ const GenericInput = ({ label={label} labelAction={labelAction} id={name} - hint={hint} + hint={fieldHint} name={name} onChange={(date) => { onChange({ @@ -265,7 +261,7 @@ const GenericInput = ({ label={label} labelAction={labelAction} id={name} - hint={combinedHint} + hint={fieldHint} name={name} onValueChange={(value) => onChange({ target: { name, value, type } })} placeholder={formattedPlaceholder} @@ -284,7 +280,7 @@ const GenericInput = ({ label={label} labelAction={labelAction} id={name} - hint={combinedHint} + hint={fieldHint} name={name} onChange={onChange} placeholder={formattedPlaceholder} @@ -305,7 +301,7 @@ const GenericInput = ({ label={label} labelAction={labelAction} id={name} - hint={combinedHint} + hint={fieldHint} name={name} onChange={onChange} placeholder={formattedPlaceholder} @@ -347,7 +343,7 @@ const GenericInput = ({ label={label} labelAction={labelAction} id={name} - hint={combinedHint} + hint={fieldHint} name={name} onChange={onChange} placeholder={formattedPlaceholder} @@ -365,7 +361,7 @@ const GenericInput = ({ label={label} labelAction={labelAction} id={name} - hint={hint} + hint={fieldHint} name={name} onChange={(value) => onChange({ target: { name, value, type: 'select' } })} placeholder={formattedPlaceholder} @@ -390,7 +386,7 @@ const GenericInput = ({ label={label} labelAction={labelAction} id={name} - hint={combinedHint} + hint={fieldHint} name={name} onChange={onChange} required={required} @@ -421,7 +417,7 @@ const GenericInput = ({ label={label} labelAction={labelAction} id={name} - hint={hint} + hint={fieldHint} name={name} onChange={(time) => { onChange({ target: { name, value: `${time}`, type } }); @@ -442,7 +438,7 @@ const GenericInput = ({ name={name} label={label} labelAction={labelAction} - hint={hint} + hint={fieldHint} error={errorMessage} required={required} /> @@ -455,7 +451,6 @@ GenericInput.defaultProps = { autoComplete: undefined, customInputs: null, description: null, - minMaxDescription: null, disabled: false, error: '', isNullable: undefined, @@ -465,6 +460,8 @@ GenericInput.defaultProps = { options: [], step: 1, value: undefined, + minimum: undefined, + maximum: undefined, }; GenericInput.propTypes = { @@ -475,11 +472,6 @@ GenericInput.propTypes = { defaultMessage: PropTypes.string.isRequired, values: PropTypes.object, }), - minMaxDescription: PropTypes.shape({ - id: PropTypes.string.isRequired, - defaultMessage: PropTypes.string.isRequired, - values: PropTypes.object, - }), disabled: PropTypes.bool, error: PropTypes.oneOfType([ PropTypes.string, @@ -520,6 +512,8 @@ GenericInput.propTypes = { step: PropTypes.number, type: PropTypes.string.isRequired, value: PropTypes.any, + minimum: PropTypes.number, + maximum: PropTypes.number, }; export default GenericInput; diff --git a/packages/core/helper-plugin/lib/src/components/GenericInput/tests/__snapshots__/index.test.js.snap b/packages/core/helper-plugin/lib/src/components/GenericInput/tests/__snapshots__/index.test.js.snap index 2ede9e21d9..873b1ace29 100644 --- a/packages/core/helper-plugin/lib/src/components/GenericInput/tests/__snapshots__/index.test.js.snap +++ b/packages/core/helper-plugin/lib/src/components/GenericInput/tests/__snapshots__/index.test.js.snap @@ -14,12 +14,6 @@ exports[`GenericInput number renders and matches the snapshot 1`] = ` color: #d02b20; } -.c14 { - font-size: 0.75rem; - line-height: 1.33; - color: #666687; -} - .c9 { padding-right: 12px; padding-left: 8px; @@ -88,7 +82,7 @@ exports[`GenericInput number renders and matches the snapshot 1`] = ` fill: #8e8ea9; } -.c15 { +.c14 { border: 0; -webkit-clip: rect(0 0 0 0); clip: rect(0 0 0 0); @@ -233,7 +227,6 @@ exports[`GenericInput number renders and matches the snapshot 1`] = ` class="c6 c7" > -

- -

{ + const { formatMessage } = useIntl(); + + const [fieldHint, setFieldHint] = useState([]); + + /** + * @returns {String} + */ + const buildDescription = () => + description + ? formatMessage( + { id: description.id, defaultMessage: description.defaultMessage }, + { ...description.values } + ) + : ''; + + /** + * Constructs a suitable description of a field's minimum and maximum limits + * @returns {Array} + */ + const buildMinMaxHint = () => { + if (typeof minimum !== 'number' && typeof maximum !== 'number') { + return []; + } + + const minMaxDescription = []; + + if (typeof minimum === 'number' && minimum > 0) { + minMaxDescription.push(`min. {minimum}`); + } + if (typeof maximum === 'number' && maximum > 0) { + minMaxDescription.push(`max. {maximum}`); + } + + let defaultMessage; + + if (minMaxDescription.length === 0) { + defaultMessage = ''; + } else if (isNumber) { + defaultMessage = `${minMaxDescription.join(' / ')}{br}`; + } else { + defaultMessage = `${minMaxDescription.join( + ' / ' + )} {isPlural, select, true {characters} other {character}}{br}`; + } + + return formatMessage( + { + id: `content-manager.form.Input.minMaxDescription${isNumber ? '.number' : ''}`, + defaultMessage, + }, + { + minimum, + maximum, + isPlural: Math.max(minimum || 0, maximum || 0) > 1, + br:
, + } + ); + }; + + useEffect(() => { + const description = buildDescription(); + const minMaxHint = buildMinMaxHint(); + + if (description.length === 0 && minMaxHint.length === 0) { + setFieldHint(''); + + return; + } + setFieldHint([...minMaxHint, description]); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isNumber, description, minimum, maximum]); + + return { fieldHint }; +}; + +export default useFieldHint; diff --git a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/tests/useFieldHint.test.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/tests/useFieldHint.test.js new file mode 100644 index 0000000000..f6aaefaf64 --- /dev/null +++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/tests/useFieldHint.test.js @@ -0,0 +1,94 @@ +import React from 'react'; +import { renderHook, act } from '@testing-library/react-hooks'; +import { IntlProvider } from 'react-intl'; + +import useFieldHint from '../index'; + +const messages = { 'message.id': 'response' }; +const description = { id: 'message.id', defaultMessage: '' }; + +// eslint-disable-next-line react/prop-types +export const IntlWrapper = ({ children }) => ( + + {children} + +); + +function setup(args) { + return new Promise((resolve) => { + act(() => { + resolve(renderHook(() => useFieldHint(args), { wrapper: IntlWrapper })); + }); + }); +} + +describe('useFieldHint', () => { + describe('correctly generates the description', () => { + const minimum = 1; + const maximum = 5; + + test('as a character limit', async () => { + const { result } = await setup({ + description, + minimum, + maximum, + }); + + expect(result.current.fieldHint).toContain('min. 1 / max. 5 characters'); + expect(result.current.fieldHint).toContain('response'); + }); + + test('as a number limit', async () => { + const { result } = await setup({ + description, + minimum, + maximum, + isNumber: true, + }); + + expect(result.current.fieldHint).toContain(`min. ${minimum} / max. ${maximum}`); + expect(result.current.fieldHint).toContain('response'); + }); + }); + + test('ignores 0 minimum values', async () => { + const minimum = 0; + const maximum = 2; + const { result } = await setup({ + description, + minimum, + maximum, + }); + + expect(result.current.fieldHint).toContain(`max. ${maximum} characters`); + expect(result.current.fieldHint).toContain('response'); + }); + + describe('handles plurals correctly', () => { + const minimum = undefined; + const maximum = 1; + test('maximum', async () => { + const { result } = await setup({ + description, + minimum, + maximum, + }); + + expect(result.current.fieldHint).toContain(`max. ${maximum} character`); + expect(result.current.fieldHint).toContain('response'); + }); + + test('minimum', async () => { + const minimum = 1; + const maximum = undefined; + const { result } = await setup({ + description, + minimum, + maximum, + }); + + expect(result.current.fieldHint).toContain(`min. ${minimum} character`); + expect(result.current.fieldHint).toContain('response'); + }); + }); +}); diff --git a/packages/core/helper-plugin/lib/src/index.js b/packages/core/helper-plugin/lib/src/index.js index 2c604b2508..eda9d2d9b2 100644 --- a/packages/core/helper-plugin/lib/src/index.js +++ b/packages/core/helper-plugin/lib/src/index.js @@ -16,6 +16,7 @@ export { default as useAppInfos } from './hooks/useAppInfos'; export { default as useQuery } from './hooks/useQuery'; export { default as useLibrary } from './hooks/useLibrary'; export { default as useCustomFields } from './hooks/useCustomFields'; +export { default as useFieldHint } from './hooks/useFieldHint'; export { default as useNotification } from './hooks/useNotification'; export { default as useStrapiApp } from './hooks/useStrapiApp'; export { default as useTracking } from './hooks/useTracking'; From 89b0ad56e7431cf6665573215d4390f58fb010de Mon Sep 17 00:00:00 2001 From: Jamie Howard Date: Tue, 17 Jan 2023 11:12:00 +0000 Subject: [PATCH 04/17] chore: code cleanup --- .../components/Inputs/index.js | 19 +++++++++++++++++-- .../lib/src/hooks/useFieldHint/index.js | 10 ++++++---- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/core/admin/admin/src/content-manager/components/Inputs/index.js b/packages/core/admin/admin/src/content-manager/components/Inputs/index.js index 295334bdf9..8f67d593a6 100644 --- a/packages/core/admin/admin/src/content-manager/components/Inputs/index.js +++ b/packages/core/admin/admin/src/content-manager/components/Inputs/index.js @@ -260,6 +260,21 @@ function Inputs({ const { minLength, maxLength, max, min } = fieldSchema; + let genericMinimum; + let genericMaximum; + + if (typeof min === 'number') { + genericMinimum = min; + } else if (typeof minLength === 'number') { + genericMinimum = minLength; + } + + if (typeof max === 'number') { + genericMaximum = max; + } else if (typeof maxLength === 'number') { + genericMaximum = maxLength; + } + return ( { const { formatMessage } = useIntl(); @@ -31,16 +30,19 @@ const useFieldHint = ({ description, minimum, maximum, isNumber = false }) => { * @returns {Array} */ const buildMinMaxHint = () => { - if (typeof minimum !== 'number' && typeof maximum !== 'number') { + const minIsNumber = typeof minimum === 'number'; + const maxIsNumber = typeof maximum === 'number'; + + if (!minIsNumber && !maxIsNumber) { return []; } const minMaxDescription = []; - if (typeof minimum === 'number' && minimum > 0) { + if (minIsNumber && minimum > 0) { minMaxDescription.push(`min. {minimum}`); } - if (typeof maximum === 'number' && maximum > 0) { + if (maxIsNumber && maximum > 0) { minMaxDescription.push(`max. {maximum}`); } From c375c6935a5cd08b3da1cb2519e8ff0e81c14bb6 Mon Sep 17 00:00:00 2001 From: Jamie Howard Date: Tue, 17 Jan 2023 13:55:14 +0000 Subject: [PATCH 05/17] fix(helper-lugin): pass field hint to UID component --- .../components/InputUID/index.js | 19 ++++--------------- .../lib/src/components/GenericInput/index.js | 3 +-- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/packages/core/admin/admin/src/content-manager/components/InputUID/index.js b/packages/core/admin/admin/src/content-manager/components/InputUID/index.js index dd00cfc8b6..0cf3f314f7 100644 --- a/packages/core/admin/admin/src/content-manager/components/InputUID/index.js +++ b/packages/core/admin/admin/src/content-manager/components/InputUID/index.js @@ -1,6 +1,6 @@ import React, { useEffect, useState, useRef } from 'react'; import PropTypes from 'prop-types'; -import { useCMEditViewDataManager, useFieldHint } from '@strapi/helper-plugin'; +import { useCMEditViewDataManager } from '@strapi/helper-plugin'; import { useIntl } from 'react-intl'; import get from 'lodash/get'; import { TextInput } from '@strapi/design-system/TextInput'; @@ -23,9 +23,7 @@ import { const InputUID = ({ attribute, contentTypeUID, - description, - minimum, - maximum, + fieldHint, disabled, error, intlLabel, @@ -37,7 +35,6 @@ const InputUID = ({ required, }) => { const { modifiedData, initialData, layout } = useCMEditViewDataManager(); - const { fieldHint } = useFieldHint({ description, minimum, maximum }); const [isLoading, setIsLoading] = useState(false); const [availability, setAvailability] = useState(null); const debouncedValue = useDebounce(value, 300); @@ -247,11 +244,6 @@ InputUID.propTypes = { required: PropTypes.bool, }).isRequired, contentTypeUID: PropTypes.string.isRequired, - description: PropTypes.shape({ - id: PropTypes.string.isRequired, - defaultMessage: PropTypes.string.isRequired, - values: PropTypes.object, - }), disabled: PropTypes.bool, error: PropTypes.string, intlLabel: PropTypes.shape({ @@ -269,20 +261,17 @@ InputUID.propTypes = { values: PropTypes.object, }), required: PropTypes.bool, - minimum: PropTypes.number, - maximum: PropTypes.number, + fieldHint: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), }; InputUID.defaultProps = { - description: undefined, disabled: false, error: undefined, labelAction: undefined, placeholder: undefined, value: '', required: false, - minimum: undefined, - maximum: undefined, + fieldHint: '', }; export default InputUID; diff --git a/packages/core/helper-plugin/lib/src/components/GenericInput/index.js b/packages/core/helper-plugin/lib/src/components/GenericInput/index.js index 1c56fd7d97..5e97c6099b 100644 --- a/packages/core/helper-plugin/lib/src/components/GenericInput/index.js +++ b/packages/core/helper-plugin/lib/src/components/GenericInput/index.js @@ -103,8 +103,7 @@ const GenericInput = ({ Date: Tue, 17 Jan 2023 13:56:29 +0000 Subject: [PATCH 06/17] chore(helper-plugin) --- packages/core/helper-plugin/lib/src/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/helper-plugin/lib/src/index.js b/packages/core/helper-plugin/lib/src/index.js index eda9d2d9b2..2c604b2508 100644 --- a/packages/core/helper-plugin/lib/src/index.js +++ b/packages/core/helper-plugin/lib/src/index.js @@ -16,7 +16,6 @@ export { default as useAppInfos } from './hooks/useAppInfos'; export { default as useQuery } from './hooks/useQuery'; export { default as useLibrary } from './hooks/useLibrary'; export { default as useCustomFields } from './hooks/useCustomFields'; -export { default as useFieldHint } from './hooks/useFieldHint'; export { default as useNotification } from './hooks/useNotification'; export { default as useStrapiApp } from './hooks/useStrapiApp'; export { default as useTracking } from './hooks/useTracking'; From 402617b5f024bfd2bcc1a79ec69ccbc56f94fe20 Mon Sep 17 00:00:00 2001 From: Jamie Howard Date: Tue, 17 Jan 2023 14:53:17 +0000 Subject: [PATCH 07/17] chore: feedback fixes --- .../components/InputUID/index.js | 8 +-- .../components/Inputs/index.js | 22 ++----- .../components/Inputs/utils/getMinMax.js | 27 ++++++++ .../components/Inputs/utils/index.js | 1 + .../lib/src/components/GenericInput/index.js | 31 ++++----- .../GenericInput/utils/fieldUnits.js | 20 ++++++ .../components/GenericInput/utils/index.js | 1 + .../lib/src/hooks/useFieldHint/index.js | 28 ++++----- .../useFieldHint/tests/useFieldHint.test.js | 63 ++----------------- 9 files changed, 90 insertions(+), 111 deletions(-) create mode 100644 packages/core/admin/admin/src/content-manager/components/Inputs/utils/getMinMax.js create mode 100644 packages/core/helper-plugin/lib/src/components/GenericInput/utils/fieldUnits.js create mode 100644 packages/core/helper-plugin/lib/src/components/GenericInput/utils/index.js diff --git a/packages/core/admin/admin/src/content-manager/components/InputUID/index.js b/packages/core/admin/admin/src/content-manager/components/InputUID/index.js index 0cf3f314f7..2c79c74a24 100644 --- a/packages/core/admin/admin/src/content-manager/components/InputUID/index.js +++ b/packages/core/admin/admin/src/content-manager/components/InputUID/index.js @@ -23,7 +23,7 @@ import { const InputUID = ({ attribute, contentTypeUID, - fieldHint, + hint, disabled, error, intlLabel, @@ -226,7 +226,7 @@ const InputUID = ({ } - hint={fieldHint} + hint={hint} label={label} labelAction={labelAction} name={name} @@ -261,7 +261,7 @@ InputUID.propTypes = { values: PropTypes.object, }), required: PropTypes.bool, - fieldHint: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), + hint: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), }; InputUID.defaultProps = { @@ -271,7 +271,7 @@ InputUID.defaultProps = { placeholder: undefined, value: '', required: false, - fieldHint: '', + hint: '', }; export default InputUID; diff --git a/packages/core/admin/admin/src/content-manager/components/Inputs/index.js b/packages/core/admin/admin/src/content-manager/components/Inputs/index.js index 8f67d593a6..7f60b2c4a0 100644 --- a/packages/core/admin/admin/src/content-manager/components/Inputs/index.js +++ b/packages/core/admin/admin/src/content-manager/components/Inputs/index.js @@ -17,6 +17,7 @@ import { connect, generateOptions, getInputType, + getMinMax, getStep, select, VALIDATIONS_TO_OMIT, @@ -258,22 +259,7 @@ function Inputs({ ...customFieldInputs, }; - const { minLength, maxLength, max, min } = fieldSchema; - - let genericMinimum; - let genericMaximum; - - if (typeof min === 'number') { - genericMinimum = min; - } else if (typeof minLength === 'number') { - genericMinimum = minLength; - } - - if (typeof max === 'number') { - genericMaximum = max; - } else if (typeof maxLength === 'number') { - genericMaximum = maxLength; - } + const { inputMaximum, inputMinimum } = getMinMax(fieldSchema); return ( { + const { minLength, maxLength, max, min } = fieldSchema; + + let inputMinimum; + let inputMaximum; + + if (typeof min === 'number') { + inputMinimum = min; + } else if (typeof minLength === 'number') { + inputMinimum = minLength; + } + + if (typeof max === 'number') { + inputMaximum = max; + } else if (typeof maxLength === 'number') { + inputMaximum = maxLength; + } + + return { inputMaximum, inputMinimum }; +}; + +export default getMinMax; diff --git a/packages/core/admin/admin/src/content-manager/components/Inputs/utils/index.js b/packages/core/admin/admin/src/content-manager/components/Inputs/utils/index.js index 607ae6336b..82eb08ec54 100644 --- a/packages/core/admin/admin/src/content-manager/components/Inputs/utils/index.js +++ b/packages/core/admin/admin/src/content-manager/components/Inputs/utils/index.js @@ -3,4 +3,5 @@ export { default as generateOptions } from './generateOptions'; export { default as getInputType } from './getInputType'; export { default as getStep } from './getStep'; export { default as select } from './select'; +export { default as getMinMax } from './getMinMax'; export { default as VALIDATIONS_TO_OMIT } from './VALIDATIONS_TO_OMIT'; diff --git a/packages/core/helper-plugin/lib/src/components/GenericInput/index.js b/packages/core/helper-plugin/lib/src/components/GenericInput/index.js index 5e97c6099b..8a9330438f 100644 --- a/packages/core/helper-plugin/lib/src/components/GenericInput/index.js +++ b/packages/core/helper-plugin/lib/src/components/GenericInput/index.js @@ -28,6 +28,7 @@ import Eye from '@strapi/icons/Eye'; import NotSupported from './NotSupported'; import useFieldHint from '../../hooks/useFieldHint'; +import { getFieldUnits } from './utils'; const GenericInput = ({ autoComplete, @@ -51,11 +52,11 @@ const GenericInput = ({ ...rest }) => { const { formatMessage } = useIntl(); - const { fieldHint } = useFieldHint({ - isNumber: type === 'number', + const { hint } = useFieldHint({ description, minimum, maximum, + units: getFieldUnits(type, minimum, maximum), }); const [showPassword, setShowPassword] = useState(false); @@ -103,7 +104,7 @@ const GenericInput = ({ { @@ -202,7 +203,7 @@ const GenericInput = ({ label={label} labelAction={labelAction} id={name} - hint={fieldHint} + hint={hint} name={name} onChange={(date) => { const formattedDate = date.toISOString(); @@ -237,7 +238,7 @@ const GenericInput = ({ label={label} labelAction={labelAction} id={name} - hint={fieldHint} + hint={hint} name={name} onChange={(date) => { onChange({ @@ -260,7 +261,7 @@ const GenericInput = ({ label={label} labelAction={labelAction} id={name} - hint={fieldHint} + hint={hint} name={name} onValueChange={(value) => onChange({ target: { name, value, type } })} placeholder={formattedPlaceholder} @@ -279,7 +280,7 @@ const GenericInput = ({ label={label} labelAction={labelAction} id={name} - hint={fieldHint} + hint={hint} name={name} onChange={onChange} placeholder={formattedPlaceholder} @@ -300,7 +301,7 @@ const GenericInput = ({ label={label} labelAction={labelAction} id={name} - hint={fieldHint} + hint={hint} name={name} onChange={onChange} placeholder={formattedPlaceholder} @@ -342,7 +343,7 @@ const GenericInput = ({ label={label} labelAction={labelAction} id={name} - hint={fieldHint} + hint={hint} name={name} onChange={onChange} placeholder={formattedPlaceholder} @@ -360,7 +361,7 @@ const GenericInput = ({ label={label} labelAction={labelAction} id={name} - hint={fieldHint} + hint={hint} name={name} onChange={(value) => onChange({ target: { name, value, type: 'select' } })} placeholder={formattedPlaceholder} @@ -385,7 +386,7 @@ const GenericInput = ({ label={label} labelAction={labelAction} id={name} - hint={fieldHint} + hint={hint} name={name} onChange={onChange} required={required} @@ -416,7 +417,7 @@ const GenericInput = ({ label={label} labelAction={labelAction} id={name} - hint={fieldHint} + hint={hint} name={name} onChange={(time) => { onChange({ target: { name, value: `${time}`, type } }); @@ -437,7 +438,7 @@ const GenericInput = ({ name={name} label={label} labelAction={labelAction} - hint={fieldHint} + hint={hint} error={errorMessage} required={required} /> diff --git a/packages/core/helper-plugin/lib/src/components/GenericInput/utils/fieldUnits.js b/packages/core/helper-plugin/lib/src/components/GenericInput/utils/fieldUnits.js new file mode 100644 index 0000000000..06720c18ca --- /dev/null +++ b/packages/core/helper-plugin/lib/src/components/GenericInput/utils/fieldUnits.js @@ -0,0 +1,20 @@ +/** + * @param {String} type + * @param {Number} minimum + * @param {Number} maximum + * @returns + */ +const getFieldUnits = (type, minimum, maximum) => { + if (type === 'number') { + return ''; + } + const plural = Math.max(minimum || 0, maximum || 0) > 1; + + if (plural) { + return 'characters'; + } + + return 'character'; +}; + +export default getFieldUnits; diff --git a/packages/core/helper-plugin/lib/src/components/GenericInput/utils/index.js b/packages/core/helper-plugin/lib/src/components/GenericInput/utils/index.js new file mode 100644 index 0000000000..45a7b8a4c5 --- /dev/null +++ b/packages/core/helper-plugin/lib/src/components/GenericInput/utils/index.js @@ -0,0 +1 @@ +export { default as getFieldUnits } from './fieldUnits'; diff --git a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js index 11a65c4ec9..7a0ea5dd82 100644 --- a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js +++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js @@ -7,12 +7,12 @@ import { useIntl } from 'react-intl'; * @param {Object} description - the description of the field * @param {Number} minimum - the minimum length or value of the field * @param {Number} maximum - the maximum length or value of the field - * @param {Boolean} isNumber - whether this is a number field + * @param {String} units */ -const useFieldHint = ({ description, minimum, maximum, isNumber = false }) => { +const useFieldHint = ({ description, minimum, maximum, units }) => { const { formatMessage } = useIntl(); - const [fieldHint, setFieldHint] = useState([]); + const [hint, setHint] = useState([]); /** * @returns {String} @@ -39,10 +39,10 @@ const useFieldHint = ({ description, minimum, maximum, isNumber = false }) => { const minMaxDescription = []; - if (minIsNumber && minimum > 0) { + if (minIsNumber) { minMaxDescription.push(`min. {minimum}`); } - if (maxIsNumber && maximum > 0) { + if (maxIsNumber) { minMaxDescription.push(`max. {maximum}`); } @@ -50,23 +50,19 @@ const useFieldHint = ({ description, minimum, maximum, isNumber = false }) => { if (minMaxDescription.length === 0) { defaultMessage = ''; - } else if (isNumber) { - defaultMessage = `${minMaxDescription.join(' / ')}{br}`; } else { - defaultMessage = `${minMaxDescription.join( - ' / ' - )} {isPlural, select, true {characters} other {character}}{br}`; + defaultMessage = `${minMaxDescription.join(' / ')} {units}{br}`; } return formatMessage( { - id: `content-manager.form.Input.minMaxDescription${isNumber ? '.number' : ''}`, + id: `content-manager.form.Input.minMaxDescription`, defaultMessage, }, { minimum, maximum, - isPlural: Math.max(minimum || 0, maximum || 0) > 1, + units, br:
, } ); @@ -77,16 +73,16 @@ const useFieldHint = ({ description, minimum, maximum, isNumber = false }) => { const minMaxHint = buildMinMaxHint(); if (description.length === 0 && minMaxHint.length === 0) { - setFieldHint(''); + setHint(''); return; } - setFieldHint([...minMaxHint, description]); + setHint([...minMaxHint, description]); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isNumber, description, minimum, maximum]); + }, [units, description, minimum, maximum]); - return { fieldHint }; + return { hint }; }; export default useFieldHint; diff --git a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/tests/useFieldHint.test.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/tests/useFieldHint.test.js index f6aaefaf64..739155e9db 100644 --- a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/tests/useFieldHint.test.js +++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/tests/useFieldHint.test.js @@ -6,6 +6,7 @@ import useFieldHint from '../index'; const messages = { 'message.id': 'response' }; const description = { id: 'message.id', defaultMessage: '' }; +const units = 'units'; // eslint-disable-next-line react/prop-types export const IntlWrapper = ({ children }) => ( @@ -23,72 +24,18 @@ function setup(args) { } describe('useFieldHint', () => { - describe('correctly generates the description', () => { + test('correctly generates the description', async () => { const minimum = 1; const maximum = 5; - test('as a character limit', async () => { - const { result } = await setup({ - description, - minimum, - maximum, - }); - - expect(result.current.fieldHint).toContain('min. 1 / max. 5 characters'); - expect(result.current.fieldHint).toContain('response'); - }); - - test('as a number limit', async () => { - const { result } = await setup({ - description, - minimum, - maximum, - isNumber: true, - }); - - expect(result.current.fieldHint).toContain(`min. ${minimum} / max. ${maximum}`); - expect(result.current.fieldHint).toContain('response'); - }); - }); - - test('ignores 0 minimum values', async () => { - const minimum = 0; - const maximum = 2; const { result } = await setup({ description, minimum, maximum, + units, }); - expect(result.current.fieldHint).toContain(`max. ${maximum} characters`); - expect(result.current.fieldHint).toContain('response'); - }); - - describe('handles plurals correctly', () => { - const minimum = undefined; - const maximum = 1; - test('maximum', async () => { - const { result } = await setup({ - description, - minimum, - maximum, - }); - - expect(result.current.fieldHint).toContain(`max. ${maximum} character`); - expect(result.current.fieldHint).toContain('response'); - }); - - test('minimum', async () => { - const minimum = 1; - const maximum = undefined; - const { result } = await setup({ - description, - minimum, - maximum, - }); - - expect(result.current.fieldHint).toContain(`min. ${minimum} character`); - expect(result.current.fieldHint).toContain('response'); - }); + expect(result.current.hint).toContain(`min. ${minimum} / max. ${maximum} ${units}`); + expect(result.current.hint).toContain('response'); }); }); From 1b8044726f544173b55ce11faccb488e48ecbfc6 Mon Sep 17 00:00:00 2001 From: Jamie Howard Date: Wed, 18 Jan 2023 09:48:47 +0000 Subject: [PATCH 08/17] chore: docs fix(helper-plugin): useFieldHint dependencies array --- .../components/Inputs/utils/getMinMax.js | 3 +- .../GenericInput/utils/fieldUnits.js | 2 +- .../lib/src/hooks/useFieldHint/index.js | 104 +++++++++--------- 3 files changed, 55 insertions(+), 54 deletions(-) diff --git a/packages/core/admin/admin/src/content-manager/components/Inputs/utils/getMinMax.js b/packages/core/admin/admin/src/content-manager/components/Inputs/utils/getMinMax.js index a2e18d8b70..06a663a397 100644 --- a/packages/core/admin/admin/src/content-manager/components/Inputs/utils/getMinMax.js +++ b/packages/core/admin/admin/src/content-manager/components/Inputs/utils/getMinMax.js @@ -1,7 +1,6 @@ /** * Get the minimum and maximum limits for an input - * @param {Object} fieldSchema - * @returns {Object} + * @type { (fieldSchema: { minLength?: number; maxLength?: number; max?: number; min?: number } ) => { inputMaximum: number; inputMinimum: number } } */ const getMinMax = (fieldSchema) => { const { minLength, maxLength, max, min } = fieldSchema; diff --git a/packages/core/helper-plugin/lib/src/components/GenericInput/utils/fieldUnits.js b/packages/core/helper-plugin/lib/src/components/GenericInput/utils/fieldUnits.js index 06720c18ca..2ee8b75574 100644 --- a/packages/core/helper-plugin/lib/src/components/GenericInput/utils/fieldUnits.js +++ b/packages/core/helper-plugin/lib/src/components/GenericInput/utils/fieldUnits.js @@ -2,7 +2,7 @@ * @param {String} type * @param {Number} minimum * @param {Number} maximum - * @returns + * @returns {'' | 'characters' | 'character'} */ const getFieldUnits = (type, minimum, maximum) => { if (type === 'number') { diff --git a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js index 7a0ea5dd82..ac9b8b5a24 100644 --- a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js +++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import { useIntl } from 'react-intl'; /** @@ -17,70 +17,72 @@ const useFieldHint = ({ description, minimum, maximum, units }) => { /** * @returns {String} */ - const buildDescription = () => - description - ? formatMessage( - { id: description.id, defaultMessage: description.defaultMessage }, - { ...description.values } - ) - : ''; + const buildDescription = useCallback( + (desc) => + desc + ? formatMessage({ id: desc.id, defaultMessage: desc.defaultMessage }, { ...desc.values }) + : '', + [formatMessage] + ); /** * Constructs a suitable description of a field's minimum and maximum limits + * @param {Number} minimum - the minimum length or value of the field + * @param {Number} maximum - the maximum length or value of the field + * @param {String} units * @returns {Array} */ - const buildMinMaxHint = () => { - const minIsNumber = typeof minimum === 'number'; - const maxIsNumber = typeof maximum === 'number'; + const buildMinMaxHint = useCallback( + (min, max, units) => { + const minIsNumber = typeof min === 'number'; + const maxIsNumber = typeof max === 'number'; - if (!minIsNumber && !maxIsNumber) { - return []; - } - - const minMaxDescription = []; - - if (minIsNumber) { - minMaxDescription.push(`min. {minimum}`); - } - if (maxIsNumber) { - minMaxDescription.push(`max. {maximum}`); - } - - let defaultMessage; - - if (minMaxDescription.length === 0) { - defaultMessage = ''; - } else { - defaultMessage = `${minMaxDescription.join(' / ')} {units}{br}`; - } - - return formatMessage( - { - id: `content-manager.form.Input.minMaxDescription`, - defaultMessage, - }, - { - minimum, - maximum, - units, - br:
, + if (!minIsNumber && !maxIsNumber) { + return []; } - ); - }; + const minMaxDescription = []; + + if (minIsNumber) { + minMaxDescription.push(`min. {min}`); + } + if (maxIsNumber) { + minMaxDescription.push(`max. {max}`); + } + let defaultMessage; + + if (minMaxDescription.length === 0) { + defaultMessage = ''; + } else { + defaultMessage = `${minMaxDescription.join(' / ')} {units}{br}`; + } + + return formatMessage( + { + id: `content-manager.form.Input.minMaxDescription`, + defaultMessage, + }, + { + min, + max, + units, + br:
, + } + ); + }, + [formatMessage] + ); useEffect(() => { - const description = buildDescription(); - const minMaxHint = buildMinMaxHint(); + const newDescription = buildDescription(description); + const minMaxHint = buildMinMaxHint(minimum, maximum, units); - if (description.length === 0 && minMaxHint.length === 0) { + if (newDescription.length === 0 && minMaxHint.length === 0) { setHint(''); return; } - setHint([...minMaxHint, description]); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [units, description, minimum, maximum]); + setHint([...minMaxHint, newDescription]); + }, [units, description, minimum, maximum, buildMinMaxHint, buildDescription]); return { hint }; }; From 68984c20e196095c93973dd15420b97348f0eb3f Mon Sep 17 00:00:00 2001 From: Jamie Howard Date: Thu, 19 Jan 2023 15:55:14 +0000 Subject: [PATCH 09/17] fix(helper-plugin): make useFieldHint translatable test(helper-plugin): getFieldUnits feat(I18n): add en.json translations for min/max descriptions feat(content-manager): support min/max descriptions in Wysiwyg and BIGINT inputs test(content-manager): getMinMax --- .../content-manager/components/Hint/index.js | 19 +--- .../components/Inputs/utils/getMinMax.js | 24 +++-- .../Inputs/utils/tests/getMinMax.test.js | 46 ++++++++ .../components/Wysiwyg/index.js | 12 +-- .../core/admin/admin/src/translations/en.json | 4 +- .../lib/src/components/GenericInput/index.js | 2 +- .../utils/{fieldUnits.js => getFieldUnits.js} | 5 +- .../components/GenericInput/utils/index.js | 2 +- .../utils/tests/getFieldUnits.test.js | 21 ++++ .../lib/src/hooks/useFieldHint/index.js | 69 ++++++++---- .../useFieldHint/tests/useFieldHint.test.js | 102 +++++++++++++++++- 11 files changed, 240 insertions(+), 66 deletions(-) create mode 100644 packages/core/admin/admin/src/content-manager/components/Inputs/utils/tests/getMinMax.test.js rename packages/core/helper-plugin/lib/src/components/GenericInput/utils/{fieldUnits.js => getFieldUnits.js} (70%) create mode 100644 packages/core/helper-plugin/lib/src/components/GenericInput/utils/tests/getFieldUnits.test.js diff --git a/packages/core/admin/admin/src/content-manager/components/Hint/index.js b/packages/core/admin/admin/src/content-manager/components/Hint/index.js index 92822a9be9..19f47bf42d 100644 --- a/packages/core/admin/admin/src/content-manager/components/Hint/index.js +++ b/packages/core/admin/admin/src/content-manager/components/Hint/index.js @@ -1,17 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { useIntl } from 'react-intl'; import { Typography } from '@strapi/design-system/Typography'; -export const Hint = ({ id, error, name, description }) => { - const { formatMessage } = useIntl(); - const hint = description - ? formatMessage( - { id: description.id, defaultMessage: description.defaultMessage }, - { ...description.values } - ) - : ''; - +export const Hint = ({ id, error, name, hint }) => { if (!hint || error) { return null; } @@ -25,16 +16,12 @@ export const Hint = ({ id, error, name, description }) => { Hint.defaultProps = { id: undefined, - description: undefined, error: undefined, + hint: '', }; Hint.propTypes = { - description: PropTypes.shape({ - id: PropTypes.string.isRequired, - defaultMessage: PropTypes.string.isRequired, - values: PropTypes.object, - }), + hint: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), error: PropTypes.string, id: PropTypes.string, name: PropTypes.string.isRequired, diff --git a/packages/core/admin/admin/src/content-manager/components/Inputs/utils/getMinMax.js b/packages/core/admin/admin/src/content-manager/components/Inputs/utils/getMinMax.js index 06a663a397..7e9a921c8f 100644 --- a/packages/core/admin/admin/src/content-manager/components/Inputs/utils/getMinMax.js +++ b/packages/core/admin/admin/src/content-manager/components/Inputs/utils/getMinMax.js @@ -1,6 +1,6 @@ /** * Get the minimum and maximum limits for an input - * @type { (fieldSchema: { minLength?: number; maxLength?: number; max?: number; min?: number } ) => { inputMaximum: number; inputMinimum: number } } + * @type { (fieldSchema: { minLength?: number|string; maxLength?: number|string; max?: number|string; min?: number|string } ) => { inputMaximum: number; inputMinimum: number } } */ const getMinMax = (fieldSchema) => { const { minLength, maxLength, max, min } = fieldSchema; @@ -8,16 +8,22 @@ const getMinMax = (fieldSchema) => { let inputMinimum; let inputMaximum; - if (typeof min === 'number') { - inputMinimum = min; - } else if (typeof minLength === 'number') { - inputMinimum = minLength; + const parsedMin = parseInt(min, 10); + const parsedMinLength = parseInt(minLength, 10); + + if (!Number.isNaN(parsedMin)) { + inputMinimum = parsedMin; + } else if (!Number.isNaN(parsedMinLength)) { + inputMinimum = parsedMinLength; } - if (typeof max === 'number') { - inputMaximum = max; - } else if (typeof maxLength === 'number') { - inputMaximum = maxLength; + const parsedMax = parseInt(max, 10); + const parsedMaxLength = parseInt(maxLength, 10); + + if (!Number.isNaN(parsedMax)) { + inputMaximum = parsedMax; + } else if (!Number.isNaN(parsedMaxLength)) { + inputMaximum = parsedMaxLength; } return { inputMaximum, inputMinimum }; diff --git a/packages/core/admin/admin/src/content-manager/components/Inputs/utils/tests/getMinMax.test.js b/packages/core/admin/admin/src/content-manager/components/Inputs/utils/tests/getMinMax.test.js new file mode 100644 index 0000000000..3a554e6424 --- /dev/null +++ b/packages/core/admin/admin/src/content-manager/components/Inputs/utils/tests/getMinMax.test.js @@ -0,0 +1,46 @@ +import getMinMax from '../getMinMax'; + +describe('Content Manager | Inputs | Utils', () => { + describe('getMinMax', () => { + it('ignores a blank schema', () => { + expect(getMinMax({})).toEqual({ inputMaximum: undefined, inputMinimum: undefined }); + }); + + it('ignores values provided as strings that cannot be parsed to integers', () => { + const notANumber = 'NOT_A_NUMBER'; + const fieldSchema = { + min: notANumber, + max: notANumber, + minLength: notANumber, + maxLength: notANumber, + }; + expect(getMinMax(fieldSchema)).toEqual({ inputMaximum: undefined, inputMinimum: undefined }); + }); + + it('correctly parses integer values from strings', () => { + const fieldSchema = { + min: '2', + max: '5', + }; + expect(getMinMax(fieldSchema)).toEqual({ inputMaximum: 5, inputMinimum: 2 }); + }); + + it('returns based on minLength and maxLength values', () => { + const fieldSchema = { + minLength: 10, + maxLength: 20, + }; + + expect(getMinMax(fieldSchema)).toEqual({ inputMaximum: 20, inputMinimum: 10 }); + }); + + it('returns based on min and max values', () => { + const fieldSchema = { + min: 10, + max: 20, + }; + + expect(getMinMax(fieldSchema)).toEqual({ inputMaximum: 20, inputMinimum: 10 }); + }); + }); +}); diff --git a/packages/core/admin/admin/src/content-manager/components/Wysiwyg/index.js b/packages/core/admin/admin/src/content-manager/components/Wysiwyg/index.js index d8fdbe8e3c..cdf094b577 100644 --- a/packages/core/admin/admin/src/content-manager/components/Wysiwyg/index.js +++ b/packages/core/admin/admin/src/content-manager/components/Wysiwyg/index.js @@ -31,7 +31,7 @@ const TypographyAsterisk = styled(Typography)` `; const Wysiwyg = ({ - description, + hint, disabled, error, intlLabel, @@ -167,7 +167,7 @@ const Wysiwyg = ({ {!isExpandMode && } - + {error && ( @@ -186,21 +186,17 @@ const Wysiwyg = ({ }; Wysiwyg.defaultProps = { - description: null, disabled: false, error: '', labelAction: undefined, placeholder: null, required: false, value: '', + hint: '', }; Wysiwyg.propTypes = { - description: PropTypes.shape({ - id: PropTypes.string.isRequired, - defaultMessage: PropTypes.string.isRequired, - values: PropTypes.object, - }), + hint: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), disabled: PropTypes.bool, error: PropTypes.string, intlLabel: PropTypes.shape({ diff --git a/packages/core/admin/admin/src/translations/en.json b/packages/core/admin/admin/src/translations/en.json index e1fc7ac6c7..9d36fc4db7 100644 --- a/packages/core/admin/admin/src/translations/en.json +++ b/packages/core/admin/admin/src/translations/en.json @@ -689,7 +689,9 @@ "content-manager.form.Input.search.field": "Enable search on this field", "content-manager.form.Input.sort.field": "Enable sort on this field", "content-manager.form.Input.sort.order": "Default sort order", - "content-manager.form.Input.wysiwyg": "Display as WYSIWYG", + "content-manager.form.Input.minimum": "min. {min}", + "content-manager.form.Input.maximum": "max. {max}", + "content-manager.form.Input.units": " {units}{br}", "content-manager.global.displayedFields": "Displayed Fields", "content-manager.groups": "Groups", "content-manager.groups.numbered": "Groups ({number})", diff --git a/packages/core/helper-plugin/lib/src/components/GenericInput/index.js b/packages/core/helper-plugin/lib/src/components/GenericInput/index.js index 8a9330438f..5df206c19b 100644 --- a/packages/core/helper-plugin/lib/src/components/GenericInput/index.js +++ b/packages/core/helper-plugin/lib/src/components/GenericInput/index.js @@ -56,7 +56,7 @@ const GenericInput = ({ description, minimum, maximum, - units: getFieldUnits(type, minimum, maximum), + units: getFieldUnits({ type, name, minimum, maximum }), }); const [showPassword, setShowPassword] = useState(false); diff --git a/packages/core/helper-plugin/lib/src/components/GenericInput/utils/fieldUnits.js b/packages/core/helper-plugin/lib/src/components/GenericInput/utils/getFieldUnits.js similarity index 70% rename from packages/core/helper-plugin/lib/src/components/GenericInput/utils/fieldUnits.js rename to packages/core/helper-plugin/lib/src/components/GenericInput/utils/getFieldUnits.js index 2ee8b75574..df58c806bc 100644 --- a/packages/core/helper-plugin/lib/src/components/GenericInput/utils/fieldUnits.js +++ b/packages/core/helper-plugin/lib/src/components/GenericInput/utils/getFieldUnits.js @@ -1,11 +1,12 @@ /** * @param {String} type + * @param {String} name * @param {Number} minimum * @param {Number} maximum * @returns {'' | 'characters' | 'character'} */ -const getFieldUnits = (type, minimum, maximum) => { - if (type === 'number') { +const getFieldUnits = ({ type, name, minimum, maximum }) => { + if (type === 'number' || name === 'BIGINT') { return ''; } const plural = Math.max(minimum || 0, maximum || 0) > 1; diff --git a/packages/core/helper-plugin/lib/src/components/GenericInput/utils/index.js b/packages/core/helper-plugin/lib/src/components/GenericInput/utils/index.js index 45a7b8a4c5..ba7776f292 100644 --- a/packages/core/helper-plugin/lib/src/components/GenericInput/utils/index.js +++ b/packages/core/helper-plugin/lib/src/components/GenericInput/utils/index.js @@ -1 +1 @@ -export { default as getFieldUnits } from './fieldUnits'; +export { default as getFieldUnits } from './getFieldUnits'; diff --git a/packages/core/helper-plugin/lib/src/components/GenericInput/utils/tests/getFieldUnits.test.js b/packages/core/helper-plugin/lib/src/components/GenericInput/utils/tests/getFieldUnits.test.js new file mode 100644 index 0000000000..45ff87052f --- /dev/null +++ b/packages/core/helper-plugin/lib/src/components/GenericInput/utils/tests/getFieldUnits.test.js @@ -0,0 +1,21 @@ +import { getFieldUnits } from '../index'; + +describe('Content Manager | Inputs | Utils', () => { + describe('getFieldUnits', () => { + it('returns for number types', () => { + expect(getFieldUnits({ type: 'number' })).toEqual(''); + }); + + it('returns for BIGINT types', () => { + expect(getFieldUnits({ name: 'BIGINT' })).toEqual(''); + }); + + it('returns "character" for other types when neither minimum or maximum is greater than 1', () => { + expect(getFieldUnits({ type: 'text', minimum: 1, maximum: 1 })).toEqual('character'); + }); + + it('returns "characters" for other types when neither minimum or maximum is greater than 0', () => { + expect(getFieldUnits({ type: 'text', maximum: 2 })).toEqual('characters'); + }); + }); +}); diff --git a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js index ac9b8b5a24..792f78f8ae 100644 --- a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js +++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js @@ -19,7 +19,7 @@ const useFieldHint = ({ description, minimum, maximum, units }) => { */ const buildDescription = useCallback( (desc) => - desc + desc?.id ? formatMessage({ id: desc.id, defaultMessage: desc.defaultMessage }, { ...desc.values }) : '', [formatMessage] @@ -40,34 +40,57 @@ const useFieldHint = ({ description, minimum, maximum, units }) => { if (!minIsNumber && !maxIsNumber) { return []; } - const minMaxDescription = []; + const ret = []; if (minIsNumber) { - minMaxDescription.push(`min. {min}`); + ret.push( + formatMessage( + { + id: 'content-manager.form.Input.minimum', + defaultMessage: 'min. {min}', + }, + { + min, + } + ) + ); + } + if (minIsNumber && maxIsNumber) { + const connector = ' / '; + ret.push( + formatMessage({ + id: connector, + defaultMessage: connector, + }) + ); } if (maxIsNumber) { - minMaxDescription.push(`max. {max}`); + ret.push( + formatMessage( + { + id: 'content-manager.form.Input.maximum', + defaultMessage: 'max. {max}', + }, + { + max, + } + ) + ); } - let defaultMessage; - - if (minMaxDescription.length === 0) { - defaultMessage = ''; - } else { - defaultMessage = `${minMaxDescription.join(' / ')} {units}{br}`; - } - - return formatMessage( - { - id: `content-manager.form.Input.minMaxDescription`, - defaultMessage, - }, - { - min, - max, - units, - br:
, - } + ret.push( + formatMessage( + { + id: 'content-manager.form.Input.units', + defaultMessage: ' {units}{br}', + }, + { + units, + br:
, + } + ) ); + + return ret; }, [formatMessage] ); diff --git a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/tests/useFieldHint.test.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/tests/useFieldHint.test.js index 739155e9db..433251b1e2 100644 --- a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/tests/useFieldHint.test.js +++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/tests/useFieldHint.test.js @@ -5,7 +5,7 @@ import { IntlProvider } from 'react-intl'; import useFieldHint from '../index'; const messages = { 'message.id': 'response' }; -const description = { id: 'message.id', defaultMessage: '' }; +const knownDescription = { id: 'message.id', defaultMessage: '' }; const units = 'units'; // eslint-disable-next-line react/prop-types @@ -24,18 +24,110 @@ function setup(args) { } describe('useFieldHint', () => { - test('correctly generates the description', async () => { + describe('descriptions', () => { + test('generates a known description', async () => { + const { result } = await setup({ + description: knownDescription, + units, + }); + + expect(result.current.hint.length).toEqual(1); + expect(result.current.hint).toContain('response'); + }); + + test('fails to generate an unknown description', async () => { + const { result } = await setup({ + description: {}, + units, + }); + + expect(result.current.hint).toEqual(''); + }); + }); + + describe('minimum/maximum limits', () => { + test('generates nothing if minimum and maximum are undefined', async () => { + const { result } = await setup({ + units, + }); + + expect(result.current.hint).toEqual(''); + }); + + test('generates a minimum limit', async () => { + const minimum = 1; + + const { result } = await setup({ + minimum, + units, + }); + + expect(result.current.hint.length).toEqual(3); + + expect(result.current.hint[0]).toEqual(`min. ${minimum}`); + expect(result.current.hint[1]).toContain(` ${units}`); + expect(result.current.hint[2]).toEqual(``); + }); + + test('generates a minimum/maximum limits', async () => { + const maximum = 5; + + const { result } = await setup({ + maximum, + units, + }); + + expect(result.current.hint.length).toEqual(3); + + expect(result.current.hint[0]).toEqual(`max. ${maximum}`); + expect(result.current.hint[1]).toContain(` ${units}`); + expect(result.current.hint[2]).toEqual(''); + }); + + test('generates a minimum/maximum limits', async () => { + const minimum = 1; + const maximum = 5; + + const { result } = await setup({ + minimum, + maximum, + units, + }); + + expect(result.current.hint.length).toEqual(5); + + expect(result.current.hint).toContain(`min. ${minimum}`); + expect(result.current.hint).toContain(`max. ${maximum}`); + expect(result.current.hint[3]).toContain(` ${units}`); + expect(result.current.hint[4]).toEqual(''); + }); + }); + + test('returns an empty string when there is no description or minimum and maximum limits', async () => { + const { result } = await setup({ + units, + }); + + expect(result.current.hint).toEqual(''); + }); + + test('generates the description and min max hint', async () => { const minimum = 1; const maximum = 5; const { result } = await setup({ - description, + description: knownDescription, minimum, maximum, units, }); - expect(result.current.hint).toContain(`min. ${minimum} / max. ${maximum} ${units}`); - expect(result.current.hint).toContain('response'); + expect(result.current.hint.length).toEqual(5); + + expect(result.current.hint[0]).toEqual(`min. ${minimum}`); + expect(result.current.hint[1]).toEqual(' / '); + expect(result.current.hint[2]).toEqual(`max. ${maximum}`); + expect(result.current.hint[3]).toContain(` ${units}`); + expect(result.current.hint[4]).toEqual('response'); }); }); From 9a47143859e2b9e6b7feb0eb5d3922dc2b2e7c30 Mon Sep 17 00:00:00 2001 From: Jamie Howard Date: Thu, 19 Jan 2023 16:03:48 +0000 Subject: [PATCH 10/17] chore(helper-plugin) --- .../components/GenericInput/utils/tests/getFieldUnits.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/helper-plugin/lib/src/components/GenericInput/utils/tests/getFieldUnits.test.js b/packages/core/helper-plugin/lib/src/components/GenericInput/utils/tests/getFieldUnits.test.js index 45ff87052f..5376ec7cf0 100644 --- a/packages/core/helper-plugin/lib/src/components/GenericInput/utils/tests/getFieldUnits.test.js +++ b/packages/core/helper-plugin/lib/src/components/GenericInput/utils/tests/getFieldUnits.test.js @@ -10,11 +10,11 @@ describe('Content Manager | Inputs | Utils', () => { expect(getFieldUnits({ name: 'BIGINT' })).toEqual(''); }); - it('returns "character" for other types when neither minimum or maximum is greater than 1', () => { + it('returns "character" when neither minimum or maximum is greater than 1', () => { expect(getFieldUnits({ type: 'text', minimum: 1, maximum: 1 })).toEqual('character'); }); - it('returns "characters" for other types when neither minimum or maximum is greater than 0', () => { + it('returns "characters" when either minimum or maximum is greater than 1', () => { expect(getFieldUnits({ type: 'text', maximum: 2 })).toEqual('characters'); }); }); From 7f7b4f212d57b251274ae6bf427c03e984d0b1ed Mon Sep 17 00:00:00 2001 From: Jamie Howard Date: Thu, 19 Jan 2023 16:05:34 +0000 Subject: [PATCH 11/17] chore(content-manager) --- .../admin/admin/src/content-manager/components/Hint/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/admin/admin/src/content-manager/components/Hint/index.js b/packages/core/admin/admin/src/content-manager/components/Hint/index.js index 19f47bf42d..a4d7daaf0e 100644 --- a/packages/core/admin/admin/src/content-manager/components/Hint/index.js +++ b/packages/core/admin/admin/src/content-manager/components/Hint/index.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { Typography } from '@strapi/design-system/Typography'; export const Hint = ({ id, error, name, hint }) => { - if (!hint || error) { + if (hint.length === 0 || error) { return null; } From 98fdd17fcde7d797777fc96241ae1bd3e699adda Mon Sep 17 00:00:00 2001 From: Jamie Howard Date: Thu, 19 Jan 2023 16:09:44 +0000 Subject: [PATCH 12/17] chore(helper-plugin): useFieldHint variable naming --- .../lib/src/hooks/useFieldHint/index.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js index 792f78f8ae..dd62801670 100644 --- a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js +++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js @@ -40,10 +40,10 @@ const useFieldHint = ({ description, minimum, maximum, units }) => { if (!minIsNumber && !maxIsNumber) { return []; } - const ret = []; + const minMaxDescription = []; if (minIsNumber) { - ret.push( + minMaxDescription.push( formatMessage( { id: 'content-manager.form.Input.minimum', @@ -57,7 +57,7 @@ const useFieldHint = ({ description, minimum, maximum, units }) => { } if (minIsNumber && maxIsNumber) { const connector = ' / '; - ret.push( + minMaxDescription.push( formatMessage({ id: connector, defaultMessage: connector, @@ -65,7 +65,7 @@ const useFieldHint = ({ description, minimum, maximum, units }) => { ); } if (maxIsNumber) { - ret.push( + minMaxDescription.push( formatMessage( { id: 'content-manager.form.Input.maximum', @@ -77,7 +77,7 @@ const useFieldHint = ({ description, minimum, maximum, units }) => { ) ); } - ret.push( + minMaxDescription.push( formatMessage( { id: 'content-manager.form.Input.units', @@ -90,7 +90,7 @@ const useFieldHint = ({ description, minimum, maximum, units }) => { ) ); - return ret; + return minMaxDescription; }, [formatMessage] ); From 688baffb9c7f0fcd3ef3062ab48fc9c4a69a7ba3 Mon Sep 17 00:00:00 2001 From: Jamie Howard Date: Fri, 20 Jan 2023 08:44:18 +0000 Subject: [PATCH 13/17] fix(helper-plugin): BigInt mistake --- .../helper-plugin/lib/src/components/GenericInput/index.js | 2 +- .../lib/src/components/GenericInput/utils/getFieldUnits.js | 5 ++--- .../GenericInput/utils/tests/getFieldUnits.test.js | 4 ---- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/core/helper-plugin/lib/src/components/GenericInput/index.js b/packages/core/helper-plugin/lib/src/components/GenericInput/index.js index 5df206c19b..d14bf07fd7 100644 --- a/packages/core/helper-plugin/lib/src/components/GenericInput/index.js +++ b/packages/core/helper-plugin/lib/src/components/GenericInput/index.js @@ -56,7 +56,7 @@ const GenericInput = ({ description, minimum, maximum, - units: getFieldUnits({ type, name, minimum, maximum }), + units: getFieldUnits({ type, minimum, maximum }), }); const [showPassword, setShowPassword] = useState(false); diff --git a/packages/core/helper-plugin/lib/src/components/GenericInput/utils/getFieldUnits.js b/packages/core/helper-plugin/lib/src/components/GenericInput/utils/getFieldUnits.js index df58c806bc..09329db04a 100644 --- a/packages/core/helper-plugin/lib/src/components/GenericInput/utils/getFieldUnits.js +++ b/packages/core/helper-plugin/lib/src/components/GenericInput/utils/getFieldUnits.js @@ -1,12 +1,11 @@ /** * @param {String} type - * @param {String} name * @param {Number} minimum * @param {Number} maximum * @returns {'' | 'characters' | 'character'} */ -const getFieldUnits = ({ type, name, minimum, maximum }) => { - if (type === 'number' || name === 'BIGINT') { +const getFieldUnits = ({ type, minimum, maximum }) => { + if (type === 'number') { return ''; } const plural = Math.max(minimum || 0, maximum || 0) > 1; diff --git a/packages/core/helper-plugin/lib/src/components/GenericInput/utils/tests/getFieldUnits.test.js b/packages/core/helper-plugin/lib/src/components/GenericInput/utils/tests/getFieldUnits.test.js index 5376ec7cf0..4b22ec30cc 100644 --- a/packages/core/helper-plugin/lib/src/components/GenericInput/utils/tests/getFieldUnits.test.js +++ b/packages/core/helper-plugin/lib/src/components/GenericInput/utils/tests/getFieldUnits.test.js @@ -6,10 +6,6 @@ describe('Content Manager | Inputs | Utils', () => { expect(getFieldUnits({ type: 'number' })).toEqual(''); }); - it('returns for BIGINT types', () => { - expect(getFieldUnits({ name: 'BIGINT' })).toEqual(''); - }); - it('returns "character" when neither minimum or maximum is greater than 1', () => { expect(getFieldUnits({ type: 'text', minimum: 1, maximum: 1 })).toEqual('character'); }); From c3fd52c24a938477dcdc90614b7790b90def7733 Mon Sep 17 00:00:00 2001 From: Jamie Howard Date: Fri, 20 Jan 2023 12:41:59 +0000 Subject: [PATCH 14/17] fix(helper-plugin): make units translatable refactor(helper-plugin) fix(helper-plugin):combine translation strings --- .../components/Inputs/index.js | 5 - .../components/Inputs/utils/index.js | 1 - .../core/admin/admin/src/translations/en.json | 6 +- .../lib/src/components/GenericInput/index.js | 17 +-- .../GenericInput/utils/getFieldUnits.js | 20 --- .../utils/tests/getFieldUnits.test.js | 17 --- .../lib/src/hooks/useFieldHint/index.js | 124 +++++++----------- .../useFieldHint/tests/useFieldHint.test.js | 63 +++------ .../hooks/useFieldHint/utils/getFieldUnits.js | 22 ++++ .../hooks/useFieldHint}/utils/getMinMax.js | 22 ++-- .../useFieldHint}/utils/index.js | 1 + .../utils/tests/getFieldUnits.test.js | 29 ++++ .../utils/tests/getMinMax.test.js | 14 +- 13 files changed, 152 insertions(+), 189 deletions(-) delete mode 100644 packages/core/helper-plugin/lib/src/components/GenericInput/utils/getFieldUnits.js delete mode 100644 packages/core/helper-plugin/lib/src/components/GenericInput/utils/tests/getFieldUnits.test.js create mode 100644 packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/getFieldUnits.js rename packages/core/{admin/admin/src/content-manager/components/Inputs => helper-plugin/lib/src/hooks/useFieldHint}/utils/getMinMax.js (55%) rename packages/core/helper-plugin/lib/src/{components/GenericInput => hooks/useFieldHint}/utils/index.js (53%) create mode 100644 packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/tests/getFieldUnits.test.js rename packages/core/{admin/admin/src/content-manager/components/Inputs => helper-plugin/lib/src/hooks/useFieldHint}/utils/tests/getMinMax.test.js (63%) diff --git a/packages/core/admin/admin/src/content-manager/components/Inputs/index.js b/packages/core/admin/admin/src/content-manager/components/Inputs/index.js index 7f60b2c4a0..2e55e1750c 100644 --- a/packages/core/admin/admin/src/content-manager/components/Inputs/index.js +++ b/packages/core/admin/admin/src/content-manager/components/Inputs/index.js @@ -17,7 +17,6 @@ import { connect, generateOptions, getInputType, - getMinMax, getStep, select, VALIDATIONS_TO_OMIT, @@ -259,8 +258,6 @@ function Inputs({ ...customFieldInputs, }; - const { inputMaximum, inputMinimum } = getMinMax(fieldSchema); - return ( { const { formatMessage } = useIntl(); + const { hint } = useFieldHint({ description, - minimum, - maximum, - units: getFieldUnits({ type, minimum, maximum }), + fieldSchema: attribute, + type: attribute?.type || type, }); const [showPassword, setShowPassword] = useState(false); @@ -103,6 +101,7 @@ const GenericInput = ({ return ( { - if (type === 'number') { - return ''; - } - const plural = Math.max(minimum || 0, maximum || 0) > 1; - - if (plural) { - return 'characters'; - } - - return 'character'; -}; - -export default getFieldUnits; diff --git a/packages/core/helper-plugin/lib/src/components/GenericInput/utils/tests/getFieldUnits.test.js b/packages/core/helper-plugin/lib/src/components/GenericInput/utils/tests/getFieldUnits.test.js deleted file mode 100644 index 4b22ec30cc..0000000000 --- a/packages/core/helper-plugin/lib/src/components/GenericInput/utils/tests/getFieldUnits.test.js +++ /dev/null @@ -1,17 +0,0 @@ -import { getFieldUnits } from '../index'; - -describe('Content Manager | Inputs | Utils', () => { - describe('getFieldUnits', () => { - it('returns for number types', () => { - expect(getFieldUnits({ type: 'number' })).toEqual(''); - }); - - it('returns "character" when neither minimum or maximum is greater than 1', () => { - expect(getFieldUnits({ type: 'text', minimum: 1, maximum: 1 })).toEqual('character'); - }); - - it('returns "characters" when either minimum or maximum is greater than 1', () => { - expect(getFieldUnits({ type: 'text', maximum: 2 })).toEqual('characters'); - }); - }); -}); diff --git a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js index dd62801670..22fbbbc8be 100644 --- a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js +++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js @@ -1,17 +1,20 @@ import React, { useEffect, useState, useCallback } from 'react'; import { useIntl } from 'react-intl'; +import { getFieldUnits, getMinMax } from './utils'; /** * @description * A hook for generating the hint for a field - * @param {Object} description - the description of the field - * @param {Number} minimum - the minimum length or value of the field - * @param {Number} maximum - the maximum length or value of the field - * @param {String} units + * @type { + * ({ description: { id: string, defaultMessage: string }, + * type: string, + * fieldSchema: { minLength?: number|string; maxLength?: number|string; max?: number|string; min?: number|string } + * }) + * => { hint: ''|Array } + * } */ -const useFieldHint = ({ description, minimum, maximum, units }) => { +const useFieldHint = ({ description, fieldSchema, type }) => { const { formatMessage } = useIntl(); - const [hint, setHint] = useState([]); /** @@ -26,86 +29,53 @@ const useFieldHint = ({ description, minimum, maximum, units }) => { ); /** - * Constructs a suitable description of a field's minimum and maximum limits - * @param {Number} minimum - the minimum length or value of the field - * @param {Number} maximum - the maximum length or value of the field - * @param {String} units - * @returns {Array} + * @returns {''|Array} */ - const buildMinMaxHint = useCallback( - (min, max, units) => { - const minIsNumber = typeof min === 'number'; - const maxIsNumber = typeof max === 'number'; + const buildHint = useCallback( + (desc) => { + const { maximum, minimum } = getMinMax(fieldSchema); + const units = getFieldUnits({ + type, + minimum, + maximum, + }); - if (!minIsNumber && !maxIsNumber) { - return []; - } - const minMaxDescription = []; + const minIsNumber = typeof minimum === 'number'; + const maxIsNumber = typeof maximum === 'number'; + const hasMinAndMax = maxIsNumber && minIsNumber; + const hasMinOrMax = maxIsNumber || minIsNumber; - if (minIsNumber) { - minMaxDescription.push( - formatMessage( - { - id: 'content-manager.form.Input.minimum', - defaultMessage: 'min. {min}', - }, - { - min, - } - ) - ); + if (!desc?.id && !hasMinOrMax) { + return ''; } - if (minIsNumber && maxIsNumber) { - const connector = ' / '; - minMaxDescription.push( - formatMessage({ - id: connector, - defaultMessage: connector, - }) - ); - } - if (maxIsNumber) { - minMaxDescription.push( - formatMessage( - { - id: 'content-manager.form.Input.maximum', - defaultMessage: 'max. {max}', - }, - { - max, - } - ) - ); - } - minMaxDescription.push( - formatMessage( - { - id: 'content-manager.form.Input.units', - defaultMessage: ' {units}{br}', - }, - { - units, - br:
, - } - ) + + return formatMessage( + { + id: 'content-manager.form.Input.hint.text', + defaultMessage: + '{min, select, undefined {} other {min. {min}}}{hasMinAndMax, select, true { {divider} } other {}}{max, select, undefined {} other {max. {max}}}{hasMinOrMax, select, true { {unit}{br}} other {}}{description}', + }, + { + min: minimum, + max: maximum, + hasMinAndMax, + hasMinOrMax, + divider: formatMessage({ + id: 'content-manager.form.Input.hint.minMaxDivider', + defaultMessage: '/', + }), + unit: units?.message ? formatMessage(units.message, units.values) : '', + br:
, + description: buildDescription(desc), + } ); - - return minMaxDescription; }, - [formatMessage] + [formatMessage, buildDescription, fieldSchema, type] ); useEffect(() => { - const newDescription = buildDescription(description); - const minMaxHint = buildMinMaxHint(minimum, maximum, units); - - if (newDescription.length === 0 && minMaxHint.length === 0) { - setHint(''); - - return; - } - setHint([...minMaxHint, newDescription]); - }, [units, description, minimum, maximum, buildMinMaxHint, buildDescription]); + setHint(buildHint(description)); + }, [description, buildHint]); return { hint }; }; diff --git a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/tests/useFieldHint.test.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/tests/useFieldHint.test.js index 433251b1e2..e27656d44e 100644 --- a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/tests/useFieldHint.test.js +++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/tests/useFieldHint.test.js @@ -6,7 +6,6 @@ import useFieldHint from '../index'; const messages = { 'message.id': 'response' }; const knownDescription = { id: 'message.id', defaultMessage: '' }; -const units = 'units'; // eslint-disable-next-line react/prop-types export const IntlWrapper = ({ children }) => ( @@ -28,17 +27,14 @@ describe('useFieldHint', () => { test('generates a known description', async () => { const { result } = await setup({ description: knownDescription, - units, }); - expect(result.current.hint.length).toEqual(1); - expect(result.current.hint).toContain('response'); + expect(result.current.hint).toEqual('response'); }); test('fails to generate an unknown description', async () => { const { result } = await setup({ description: {}, - units, }); expect(result.current.hint).toEqual(''); @@ -46,67 +42,52 @@ describe('useFieldHint', () => { }); describe('minimum/maximum limits', () => { - test('generates nothing if minimum and maximum are undefined', async () => { - const { result } = await setup({ - units, - }); - - expect(result.current.hint).toEqual(''); - }); - test('generates a minimum limit', async () => { const minimum = 1; + const fieldSchema = { min: minimum }; const { result } = await setup({ - minimum, - units, + fieldSchema, }); expect(result.current.hint.length).toEqual(3); - expect(result.current.hint[0]).toEqual(`min. ${minimum}`); - expect(result.current.hint[1]).toContain(` ${units}`); - expect(result.current.hint[2]).toEqual(``); + expect(result.current.hint[0]).toEqual(`min. ${minimum} character`); + expect(result.current.hint[2]).toEqual(''); }); - test('generates a minimum/maximum limits', async () => { + test('generates a maximum limit', async () => { const maximum = 5; + const fieldSchema = { max: maximum }; const { result } = await setup({ - maximum, - units, + fieldSchema, }); expect(result.current.hint.length).toEqual(3); - expect(result.current.hint[0]).toEqual(`max. ${maximum}`); - expect(result.current.hint[1]).toContain(` ${units}`); + expect(result.current.hint[0]).toEqual(`max. ${maximum} characters`); expect(result.current.hint[2]).toEqual(''); }); test('generates a minimum/maximum limits', async () => { const minimum = 1; const maximum = 5; + const fieldSchema = { minLength: minimum, maxLength: maximum }; const { result } = await setup({ - minimum, - maximum, - units, + fieldSchema, }); - expect(result.current.hint.length).toEqual(5); + expect(result.current.hint.length).toEqual(3); - expect(result.current.hint).toContain(`min. ${minimum}`); - expect(result.current.hint).toContain(`max. ${maximum}`); - expect(result.current.hint[3]).toContain(` ${units}`); - expect(result.current.hint[4]).toEqual(''); + expect(result.current.hint).toContain(`min. ${minimum} / max. ${maximum} characters`); + expect(result.current.hint[2]).toEqual(''); }); }); test('returns an empty string when there is no description or minimum and maximum limits', async () => { - const { result } = await setup({ - units, - }); + const { result } = await setup({}); expect(result.current.hint).toEqual(''); }); @@ -114,20 +95,16 @@ describe('useFieldHint', () => { test('generates the description and min max hint', async () => { const minimum = 1; const maximum = 5; + const fieldSchema = { minLength: minimum, maxLength: maximum }; const { result } = await setup({ description: knownDescription, - minimum, - maximum, - units, + fieldSchema, }); - expect(result.current.hint.length).toEqual(5); + expect(result.current.hint.length).toEqual(3); - expect(result.current.hint[0]).toEqual(`min. ${minimum}`); - expect(result.current.hint[1]).toEqual(' / '); - expect(result.current.hint[2]).toEqual(`max. ${maximum}`); - expect(result.current.hint[3]).toContain(` ${units}`); - expect(result.current.hint[4]).toEqual('response'); + expect(result.current.hint[0]).toEqual(`min. ${minimum} / max. ${maximum} characters`); + expect(result.current.hint[2]).toEqual('response'); }); }); diff --git a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/getFieldUnits.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/getFieldUnits.js new file mode 100644 index 0000000000..65154a1178 --- /dev/null +++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/getFieldUnits.js @@ -0,0 +1,22 @@ +/** + * @type { ({ type?: string; minimum?: number; maximum: number; } ) => { + * message?: {id: string, defaultMessage: string}; values?: {maxValue: number} } } + */ +const getFieldUnits = ({ type, minimum, maximum }) => { + if (['biginteger', 'integer', 'number'].includes(type)) { + return {}; + } + const maxValue = Math.max(minimum || 0, maximum || 0); + + return { + message: { + id: 'content-manager.form.Input.hint.units', + defaultMessage: '{maxValue, plural, one {character} other {characters}}', + }, + values: { + maxValue, + }, + }; +}; + +export default getFieldUnits; diff --git a/packages/core/admin/admin/src/content-manager/components/Inputs/utils/getMinMax.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/getMinMax.js similarity index 55% rename from packages/core/admin/admin/src/content-manager/components/Inputs/utils/getMinMax.js rename to packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/getMinMax.js index 7e9a921c8f..453092c4ba 100644 --- a/packages/core/admin/admin/src/content-manager/components/Inputs/utils/getMinMax.js +++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/getMinMax.js @@ -1,32 +1,38 @@ /** * Get the minimum and maximum limits for an input - * @type { (fieldSchema: { minLength?: number|string; maxLength?: number|string; max?: number|string; min?: number|string } ) => { inputMaximum: number; inputMinimum: number } } + * @type { + * (fieldSchema: { minLength?: number|string; maxLength?: number|string; max?: number|string; min?: number|string } ) + * => { maximum: number; minimum: number } } */ const getMinMax = (fieldSchema) => { + if (!fieldSchema) { + return { maximum: undefined, minimum: undefined }; + } + const { minLength, maxLength, max, min } = fieldSchema; - let inputMinimum; - let inputMaximum; + let minimum; + let maximum; const parsedMin = parseInt(min, 10); const parsedMinLength = parseInt(minLength, 10); if (!Number.isNaN(parsedMin)) { - inputMinimum = parsedMin; + minimum = parsedMin; } else if (!Number.isNaN(parsedMinLength)) { - inputMinimum = parsedMinLength; + minimum = parsedMinLength; } const parsedMax = parseInt(max, 10); const parsedMaxLength = parseInt(maxLength, 10); if (!Number.isNaN(parsedMax)) { - inputMaximum = parsedMax; + maximum = parsedMax; } else if (!Number.isNaN(parsedMaxLength)) { - inputMaximum = parsedMaxLength; + maximum = parsedMaxLength; } - return { inputMaximum, inputMinimum }; + return { maximum, minimum }; }; export default getMinMax; diff --git a/packages/core/helper-plugin/lib/src/components/GenericInput/utils/index.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/index.js similarity index 53% rename from packages/core/helper-plugin/lib/src/components/GenericInput/utils/index.js rename to packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/index.js index ba7776f292..9cd686e859 100644 --- a/packages/core/helper-plugin/lib/src/components/GenericInput/utils/index.js +++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/index.js @@ -1 +1,2 @@ export { default as getFieldUnits } from './getFieldUnits'; +export { default as getMinMax } from './getMinMax'; diff --git a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/tests/getFieldUnits.test.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/tests/getFieldUnits.test.js new file mode 100644 index 0000000000..c3e41447d3 --- /dev/null +++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/tests/getFieldUnits.test.js @@ -0,0 +1,29 @@ +import { getFieldUnits } from '../index'; + +describe('Content Manager | Inputs | Utils', () => { + describe('getFieldUnits', () => { + it('returns for number types', () => { + expect(getFieldUnits({ type: 'number' })).toEqual({}); + }); + + it('returns for biginteger types', () => { + expect(getFieldUnits({ type: 'biginteger' })).toEqual({}); + }); + + it('returns for integer types', () => { + expect(getFieldUnits({ type: 'integer' })).toEqual({}); + }); + + it('correctly returns units translation object', () => { + expect(getFieldUnits({ type: 'text', minimum: 1, maximum: 5 })).toEqual({ + message: { + id: 'content-manager.form.Input.hint.units', + defaultMessage: '{maxValue, plural, one {character} other {characters}}', + }, + values: { + maxValue: 5, + }, + }); + }); + }); +}); diff --git a/packages/core/admin/admin/src/content-manager/components/Inputs/utils/tests/getMinMax.test.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/tests/getMinMax.test.js similarity index 63% rename from packages/core/admin/admin/src/content-manager/components/Inputs/utils/tests/getMinMax.test.js rename to packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/tests/getMinMax.test.js index 3a554e6424..ea36389cc3 100644 --- a/packages/core/admin/admin/src/content-manager/components/Inputs/utils/tests/getMinMax.test.js +++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/tests/getMinMax.test.js @@ -3,7 +3,11 @@ import getMinMax from '../getMinMax'; describe('Content Manager | Inputs | Utils', () => { describe('getMinMax', () => { it('ignores a blank schema', () => { - expect(getMinMax({})).toEqual({ inputMaximum: undefined, inputMinimum: undefined }); + expect(getMinMax({})).toEqual({ maximum: undefined, minimium: undefined }); + }); + + it('ignores a null schema', () => { + expect(getMinMax(null)).toEqual({ maximum: undefined, minimium: undefined }); }); it('ignores values provided as strings that cannot be parsed to integers', () => { @@ -14,7 +18,7 @@ describe('Content Manager | Inputs | Utils', () => { minLength: notANumber, maxLength: notANumber, }; - expect(getMinMax(fieldSchema)).toEqual({ inputMaximum: undefined, inputMinimum: undefined }); + expect(getMinMax(fieldSchema)).toEqual({ maximum: undefined, minimum: undefined }); }); it('correctly parses integer values from strings', () => { @@ -22,7 +26,7 @@ describe('Content Manager | Inputs | Utils', () => { min: '2', max: '5', }; - expect(getMinMax(fieldSchema)).toEqual({ inputMaximum: 5, inputMinimum: 2 }); + expect(getMinMax(fieldSchema)).toEqual({ maximum: 5, minimum: 2 }); }); it('returns based on minLength and maxLength values', () => { @@ -31,7 +35,7 @@ describe('Content Manager | Inputs | Utils', () => { maxLength: 20, }; - expect(getMinMax(fieldSchema)).toEqual({ inputMaximum: 20, inputMinimum: 10 }); + expect(getMinMax(fieldSchema)).toEqual({ maximum: 20, minimum: 10 }); }); it('returns based on min and max values', () => { @@ -40,7 +44,7 @@ describe('Content Manager | Inputs | Utils', () => { max: 20, }; - expect(getMinMax(fieldSchema)).toEqual({ inputMaximum: 20, inputMinimum: 10 }); + expect(getMinMax(fieldSchema)).toEqual({ maximum: 20, minimum: 10 }); }); }); }); From 2acdc52f305c61d64e5950d0ba135bb680966bff Mon Sep 17 00:00:00 2001 From: Jamie Howard Date: Mon, 23 Jan 2023 09:59:26 +0000 Subject: [PATCH 15/17] fix(helper-plugin): simplify translation string --- .../core/admin/admin/src/translations/en.json | 6 +++--- .../lib/src/hooks/useFieldHint/index.js | 18 +++++++++--------- .../hooks/useFieldHint/utils/getFieldUnits.js | 4 ++-- .../utils/tests/getFieldUnits.test.js | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/core/admin/admin/src/translations/en.json b/packages/core/admin/admin/src/translations/en.json index 78ac5e0c58..2c939ab485 100644 --- a/packages/core/admin/admin/src/translations/en.json +++ b/packages/core/admin/admin/src/translations/en.json @@ -698,9 +698,9 @@ "content-manager.form.Input.search.field": "Enable search on this field", "content-manager.form.Input.sort.field": "Enable sort on this field", "content-manager.form.Input.sort.order": "Default sort order", - "content-manager.form.Input.hint.text": "{min, select, undefined {} other {min. {min}}}{hasMinAndMax, select, true { {divider} } other {}}{max, select, undefined {} other {max. {max}}}{hasMinOrMax, select, true { {unit}{br}} other {}}{description}", - "content-manager.form.Input.hint.minMaxDivider": "/", - "content-manager.form.Input.hint.units": "{maxValue, plural, one {character} other {characters}}", + "content-manager.form.Input.hint.text": "{min, select, undefined {} other {min. {min}}}{divider}{max, select, undefined {} other {max. {max}}}{unit}{br}{description}", + "content-manager.form.Input.hint.minMaxDivider": " / ", + "content-manager.form.Input.hint.character.unit": "{maxValue, plural, one { character} other { characters}}", "content-manager.global.displayedFields": "Displayed Fields", "content-manager.groups": "Groups", "content-manager.groups.numbered": "Groups ({number})", diff --git a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js index 22fbbbc8be..1a15b9db45 100644 --- a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js +++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js @@ -53,20 +53,20 @@ const useFieldHint = ({ description, fieldSchema, type }) => { { id: 'content-manager.form.Input.hint.text', defaultMessage: - '{min, select, undefined {} other {min. {min}}}{hasMinAndMax, select, true { {divider} } other {}}{max, select, undefined {} other {max. {max}}}{hasMinOrMax, select, true { {unit}{br}} other {}}{description}', + '{min, select, undefined {} other {min. {min}}}{divider}{max, select, undefined {} other {max. {max}}}{unit}{br}{description}', }, { min: minimum, max: maximum, - hasMinAndMax, - hasMinOrMax, - divider: formatMessage({ - id: 'content-manager.form.Input.hint.minMaxDivider', - defaultMessage: '/', - }), - unit: units?.message ? formatMessage(units.message, units.values) : '', - br:
, description: buildDescription(desc), + unit: units?.message && hasMinOrMax ? formatMessage(units.message, units.values) : null, + divider: hasMinAndMax + ? formatMessage({ + id: 'content-manager.form.Input.hint.minMaxDivider', + defaultMessage: ' / ', + }) + : null, + br: hasMinOrMax ?
: null, } ); }, diff --git a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/getFieldUnits.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/getFieldUnits.js index 65154a1178..8a129af6b2 100644 --- a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/getFieldUnits.js +++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/getFieldUnits.js @@ -10,8 +10,8 @@ const getFieldUnits = ({ type, minimum, maximum }) => { return { message: { - id: 'content-manager.form.Input.hint.units', - defaultMessage: '{maxValue, plural, one {character} other {characters}}', + id: 'content-manager.form.Input.hint.character.unit', + defaultMessage: '{maxValue, plural, one { character} other { characters}}', }, values: { maxValue, diff --git a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/tests/getFieldUnits.test.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/tests/getFieldUnits.test.js index c3e41447d3..2921f28fd0 100644 --- a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/tests/getFieldUnits.test.js +++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/tests/getFieldUnits.test.js @@ -17,8 +17,8 @@ describe('Content Manager | Inputs | Utils', () => { it('correctly returns units translation object', () => { expect(getFieldUnits({ type: 'text', minimum: 1, maximum: 5 })).toEqual({ message: { - id: 'content-manager.form.Input.hint.units', - defaultMessage: '{maxValue, plural, one {character} other {characters}}', + id: 'content-manager.form.Input.hint.character.unit', + defaultMessage: '{maxValue, plural, one { character} other { characters}}', }, values: { maxValue: 5, From a5bf7c1de9f4eb312874aa5fe9437699371cd8b4 Mon Sep 17 00:00:00 2001 From: Jamie Howard Date: Mon, 23 Jan 2023 10:15:01 +0000 Subject: [PATCH 16/17] chore(core) --- packages/core/admin/admin/src/translations/en.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/admin/admin/src/translations/en.json b/packages/core/admin/admin/src/translations/en.json index 2c939ab485..d8085a61bd 100644 --- a/packages/core/admin/admin/src/translations/en.json +++ b/packages/core/admin/admin/src/translations/en.json @@ -698,6 +698,7 @@ "content-manager.form.Input.search.field": "Enable search on this field", "content-manager.form.Input.sort.field": "Enable sort on this field", "content-manager.form.Input.sort.order": "Default sort order", + "content-manager.form.Input.wysiwyg": "Display as WYSIWYG", "content-manager.form.Input.hint.text": "{min, select, undefined {} other {min. {min}}}{divider}{max, select, undefined {} other {max. {max}}}{unit}{br}{description}", "content-manager.form.Input.hint.minMaxDivider": " / ", "content-manager.form.Input.hint.character.unit": "{maxValue, plural, one { character} other { characters}}", From ea95c70ae3a36533de781858caff38a6592ab115 Mon Sep 17 00:00:00 2001 From: Jamie Howard Date: Mon, 23 Jan 2023 14:38:18 +0000 Subject: [PATCH 17/17] refactor(helper-plugin): simplify useFieldHint --- .../lib/src/hooks/useFieldHint/index.js | 96 +++++++++---------- 1 file changed, 44 insertions(+), 52 deletions(-) diff --git a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js index 1a15b9db45..a5db9e2b9d 100644 --- a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js +++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useCallback } from 'react'; +import React from 'react'; import { useIntl } from 'react-intl'; import { getFieldUnits, getMinMax } from './utils'; @@ -15,69 +15,61 @@ import { getFieldUnits, getMinMax } from './utils'; */ const useFieldHint = ({ description, fieldSchema, type }) => { const { formatMessage } = useIntl(); - const [hint, setHint] = useState([]); /** * @returns {String} */ - const buildDescription = useCallback( - (desc) => - desc?.id - ? formatMessage({ id: desc.id, defaultMessage: desc.defaultMessage }, { ...desc.values }) - : '', - [formatMessage] - ); + const buildDescription = () => + description?.id + ? formatMessage( + { id: description.id, defaultMessage: description.defaultMessage }, + { ...description.values } + ) + : ''; /** * @returns {''|Array} */ - const buildHint = useCallback( - (desc) => { - const { maximum, minimum } = getMinMax(fieldSchema); - const units = getFieldUnits({ - type, - minimum, - maximum, - }); + const buildHint = () => { + const { maximum, minimum } = getMinMax(fieldSchema); + const units = getFieldUnits({ + type, + minimum, + maximum, + }); - const minIsNumber = typeof minimum === 'number'; - const maxIsNumber = typeof maximum === 'number'; - const hasMinAndMax = maxIsNumber && minIsNumber; - const hasMinOrMax = maxIsNumber || minIsNumber; + const minIsNumber = typeof minimum === 'number'; + const maxIsNumber = typeof maximum === 'number'; + const hasMinAndMax = maxIsNumber && minIsNumber; + const hasMinOrMax = maxIsNumber || minIsNumber; - if (!desc?.id && !hasMinOrMax) { - return ''; + if (!description?.id && !hasMinOrMax) { + return ''; + } + + return formatMessage( + { + id: 'content-manager.form.Input.hint.text', + defaultMessage: + '{min, select, undefined {} other {min. {min}}}{divider}{max, select, undefined {} other {max. {max}}}{unit}{br}{description}', + }, + { + min: minimum, + max: maximum, + description: buildDescription(), + unit: units?.message && hasMinOrMax ? formatMessage(units.message, units.values) : null, + divider: hasMinAndMax + ? formatMessage({ + id: 'content-manager.form.Input.hint.minMaxDivider', + defaultMessage: ' / ', + }) + : null, + br: hasMinOrMax ?
: null, } + ); + }; - return formatMessage( - { - id: 'content-manager.form.Input.hint.text', - defaultMessage: - '{min, select, undefined {} other {min. {min}}}{divider}{max, select, undefined {} other {max. {max}}}{unit}{br}{description}', - }, - { - min: minimum, - max: maximum, - description: buildDescription(desc), - unit: units?.message && hasMinOrMax ? formatMessage(units.message, units.values) : null, - divider: hasMinAndMax - ? formatMessage({ - id: 'content-manager.form.Input.hint.minMaxDivider', - defaultMessage: ' / ', - }) - : null, - br: hasMinOrMax ?
: null, - } - ); - }, - [formatMessage, buildDescription, fieldSchema, type] - ); - - useEffect(() => { - setHint(buildHint(description)); - }, [description, buildHint]); - - return { hint }; + return { hint: buildHint() }; }; export default useFieldHint;