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..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
@@ -1,18 +1,9 @@
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 }
- )
- : '';
-
- if (!hint || error) {
+export const Hint = ({ id, error, name, hint }) => {
+ if (hint.length === 0 || 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/InputUID/index.js b/packages/core/admin/admin/src/content-manager/components/InputUID/index.js
index ea443a1382..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,
- description,
+ hint,
disabled,
error,
intlLabel,
@@ -54,13 +54,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 },
@@ -251,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({
@@ -273,16 +261,17 @@ InputUID.propTypes = {
values: PropTypes.object,
}),
required: PropTypes.bool,
+ hint: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
};
InputUID.defaultProps = {
- description: undefined,
disabled: false,
error: undefined,
labelAction: undefined,
placeholder: undefined,
value: '',
required: false,
+ hint: '',
};
export default InputUID;
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 284fff154d..80e8ffdc46 100644
--- a/packages/core/admin/admin/src/translations/en.json
+++ b/packages/core/admin/admin/src/translations/en.json
@@ -699,6 +699,9 @@
"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}}",
"content-manager.global.displayedFields": "Displayed Fields",
"content-manager.groups": "Groups",
"content-manager.groups.numbered": "Groups ({number})",
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);
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..aa5e1910a1 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,7 +25,9 @@ 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,
@@ -43,9 +46,16 @@ const GenericInput = ({
type,
value: defaultValue,
isNullable,
+ attribute,
...rest
}) => {
const { formatMessage } = useIntl();
+
+ const { hint } = useFieldHint({
+ description,
+ fieldSchema: attribute,
+ type: attribute?.type || type,
+ });
const [showPassword, setShowPassword] = useState(false);
const CustomInput = customInputs ? customInputs[type] : null;
@@ -91,7 +101,9 @@ const GenericInput = ({
return (
`Date picker, current is ${formattedDate}`}
- selectButtonTitle={formatMessage({ id: 'selectButtonTitle', defaultMessage: 'Select' })}
+ selectButtonTitle={formatMessage({
+ id: 'selectButtonTitle',
+ defaultMessage: 'Select',
+ })}
/>
);
}
@@ -451,6 +459,7 @@ GenericInput.defaultProps = {
options: [],
step: 1,
value: undefined,
+ attribute: null,
};
GenericInput.propTypes = {
@@ -461,6 +470,7 @@ GenericInput.propTypes = {
defaultMessage: PropTypes.string.isRequired,
values: PropTypes.object,
}),
+ attribute: PropTypes.object,
disabled: PropTypes.bool,
error: PropTypes.oneOfType([
PropTypes.string,
diff --git a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js
new file mode 100644
index 0000000000..a5db9e2b9d
--- /dev/null
+++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/index.js
@@ -0,0 +1,75 @@
+import React from 'react';
+import { useIntl } from 'react-intl';
+import { getFieldUnits, getMinMax } from './utils';
+
+/**
+ * @description
+ * A hook for generating the hint for a field
+ * @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, fieldSchema, type }) => {
+ const { formatMessage } = useIntl();
+
+ /**
+ * @returns {String}
+ */
+ const buildDescription = () =>
+ description?.id
+ ? formatMessage(
+ { id: description.id, defaultMessage: description.defaultMessage },
+ { ...description.values }
+ )
+ : '';
+
+ /**
+ * @returns {''|Array}
+ */
+ 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;
+
+ 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 { hint: buildHint() };
+};
+
+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..e27656d44e
--- /dev/null
+++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/tests/useFieldHint.test.js
@@ -0,0 +1,110 @@
+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 knownDescription = { 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('descriptions', () => {
+ test('generates a known description', async () => {
+ const { result } = await setup({
+ description: knownDescription,
+ });
+
+ expect(result.current.hint).toEqual('response');
+ });
+
+ test('fails to generate an unknown description', async () => {
+ const { result } = await setup({
+ description: {},
+ });
+
+ expect(result.current.hint).toEqual('');
+ });
+ });
+
+ describe('minimum/maximum limits', () => {
+ test('generates a minimum limit', async () => {
+ const minimum = 1;
+ const fieldSchema = { min: minimum };
+
+ const { result } = await setup({
+ fieldSchema,
+ });
+
+ expect(result.current.hint.length).toEqual(3);
+
+ expect(result.current.hint[0]).toEqual(`min. ${minimum} character`);
+ expect(result.current.hint[2]).toEqual('');
+ });
+
+ test('generates a maximum limit', async () => {
+ const maximum = 5;
+ const fieldSchema = { max: maximum };
+
+ const { result } = await setup({
+ fieldSchema,
+ });
+
+ expect(result.current.hint.length).toEqual(3);
+
+ 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({
+ fieldSchema,
+ });
+
+ expect(result.current.hint.length).toEqual(3);
+
+ 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({});
+
+ expect(result.current.hint).toEqual('');
+ });
+
+ 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,
+ fieldSchema,
+ });
+
+ expect(result.current.hint.length).toEqual(3);
+
+ 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..8a129af6b2
--- /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.character.unit',
+ defaultMessage: '{maxValue, plural, one { character} other { characters}}',
+ },
+ values: {
+ maxValue,
+ },
+ };
+};
+
+export default getFieldUnits;
diff --git a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/getMinMax.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/getMinMax.js
new file mode 100644
index 0000000000..453092c4ba
--- /dev/null
+++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/getMinMax.js
@@ -0,0 +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 } )
+ * => { maximum: number; minimum: number } }
+ */
+const getMinMax = (fieldSchema) => {
+ if (!fieldSchema) {
+ return { maximum: undefined, minimum: undefined };
+ }
+
+ const { minLength, maxLength, max, min } = fieldSchema;
+
+ let minimum;
+ let maximum;
+
+ const parsedMin = parseInt(min, 10);
+ const parsedMinLength = parseInt(minLength, 10);
+
+ if (!Number.isNaN(parsedMin)) {
+ minimum = parsedMin;
+ } else if (!Number.isNaN(parsedMinLength)) {
+ minimum = parsedMinLength;
+ }
+
+ const parsedMax = parseInt(max, 10);
+ const parsedMaxLength = parseInt(maxLength, 10);
+
+ if (!Number.isNaN(parsedMax)) {
+ maximum = parsedMax;
+ } else if (!Number.isNaN(parsedMaxLength)) {
+ maximum = parsedMaxLength;
+ }
+
+ return { maximum, minimum };
+};
+
+export default getMinMax;
diff --git a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/index.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/index.js
new file mode 100644
index 0000000000..9cd686e859
--- /dev/null
+++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/index.js
@@ -0,0 +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..2921f28fd0
--- /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.character.unit',
+ defaultMessage: '{maxValue, plural, one { character} other { characters}}',
+ },
+ values: {
+ maxValue: 5,
+ },
+ });
+ });
+ });
+});
diff --git a/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/tests/getMinMax.test.js b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/tests/getMinMax.test.js
new file mode 100644
index 0000000000..ea36389cc3
--- /dev/null
+++ b/packages/core/helper-plugin/lib/src/hooks/useFieldHint/utils/tests/getMinMax.test.js
@@ -0,0 +1,50 @@
+import getMinMax from '../getMinMax';
+
+describe('Content Manager | Inputs | Utils', () => {
+ describe('getMinMax', () => {
+ it('ignores a blank schema', () => {
+ 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', () => {
+ const notANumber = 'NOT_A_NUMBER';
+ const fieldSchema = {
+ min: notANumber,
+ max: notANumber,
+ minLength: notANumber,
+ maxLength: notANumber,
+ };
+ expect(getMinMax(fieldSchema)).toEqual({ maximum: undefined, minimum: undefined });
+ });
+
+ it('correctly parses integer values from strings', () => {
+ const fieldSchema = {
+ min: '2',
+ max: '5',
+ };
+ expect(getMinMax(fieldSchema)).toEqual({ maximum: 5, minimum: 2 });
+ });
+
+ it('returns based on minLength and maxLength values', () => {
+ const fieldSchema = {
+ minLength: 10,
+ maxLength: 20,
+ };
+
+ expect(getMinMax(fieldSchema)).toEqual({ maximum: 20, minimum: 10 });
+ });
+
+ it('returns based on min and max values', () => {
+ const fieldSchema = {
+ min: 10,
+ max: 20,
+ };
+
+ expect(getMinMax(fieldSchema)).toEqual({ maximum: 20, minimum: 10 });
+ });
+ });
+});