Merge pull request #11024 from strapi/cm/edit-inputs

Cm/edit inputs
This commit is contained in:
cyril lopez 2021-09-21 15:24:46 +02:00 committed by GitHub
commit 1cb6275a16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 336 additions and 136 deletions

View File

@ -115,30 +115,6 @@
"component": "blog.test-como", "component": "blog.test-como",
"required": false, "required": false,
"min": 2 "min": 2
},
"temp_one_to_one": {
"type": "relation",
"relation": "oneToOne",
"target": "api::temp.temp",
"inversedBy": "one_to_one"
},
"temp": {
"type": "relation",
"relation": "manyToOne",
"target": "api::temp.temp",
"inversedBy": "one_to_many"
},
"temps": {
"type": "relation",
"relation": "oneToMany",
"target": "api::temp.temp",
"mappedBy": "many_to_one"
},
"many_to_many_temp": {
"type": "relation",
"relation": "manyToMany",
"target": "api::temp.temp",
"inversedBy": "many_to_many"
} }
} }
} }

View File

@ -9,7 +9,7 @@
"description": "" "description": ""
}, },
"options": { "options": {
"draftAndPublish": true "draftAndPublish": false
}, },
"pluginOptions": {}, "pluginOptions": {},
"attributes": { "attributes": {
@ -17,7 +17,7 @@
"type": "string" "type": "string"
}, },
"long": { "long": {
"type": "string" "type": "text"
}, },
"email": { "email": {
"type": "email" "type": "email"
@ -28,7 +28,7 @@
"password": { "password": {
"type": "password" "type": "password"
}, },
"number": { "number_int": {
"type": "integer" "type": "integer"
}, },
"enum": { "enum": {
@ -77,39 +77,33 @@
"uid": { "uid": {
"type": "uid" "type": "uid"
}, },
"one_way": { "number_big": {
"type": "relation", "type": "biginteger"
"relation": "oneToOne",
"target": "api::address.address"
}, },
"one_to_one": { "number_dec": {
"type": "relation", "type": "decimal"
"relation": "oneToOne",
"target": "api::address.address",
"inversedBy": "temp_one_to_one"
}, },
"one_to_many": { "number_float": {
"type": "relation", "type": "float"
"relation": "oneToMany",
"target": "api::address.address",
"mappedBy": "temp"
}, },
"many_to_one": { "enum_req_def": {
"type": "relation", "type": "enumeration",
"relation": "manyToOne", "enum": [
"target": "api::address.address", "un",
"inversedBy": "temps" "deux",
"trois"
],
"required": true,
"default": "un"
}, },
"many_to_many": { "enum_req": {
"type": "relation", "type": "enumeration",
"relation": "manyToMany", "enum": [
"target": "api::address.address", "un",
"inversedBy": "many_to_many_temp" "deux",
}, "trois"
"many_way": { ],
"type": "relation", "required": true
"relation": "oneToMany",
"target": "api::address.address"
} }
} }
} }

View File

@ -93,6 +93,7 @@ const EditViewDataManagerProvider = ({
const errorsInForm = Object.keys(formErrors); const errorsInForm = Object.keys(formErrors);
// TODO check if working with DZ, components... // TODO check if working with DZ, components...
// TODO use querySelector querySelectorAll('[data-strapi-field-error]')
if (errorsInForm.length > 0) { if (errorsInForm.length > 0) {
const firstError = errorsInForm[0]; const firstError = errorsInForm[0];
const el = document.getElementById(firstError); const el = document.getElementById(firstError);

View File

@ -1,4 +1,6 @@
import { get, isArray, isObject } from 'lodash'; import get from 'lodash/get';
import isArray from 'lodash/isArray';
import isObject from 'lodash/isObject';
/* eslint-disable indent */ /* eslint-disable indent */
@ -23,13 +25,23 @@ const cleanData = (retrievedData, currentSchema, componentsSchema) => {
} }
break; break;
case 'date': // TODO
cleanedData = // case 'date':
value && value._isAMomentObject === true ? value.format('YYYY-MM-DD') : value; // cleanedData =
break; // value && value._isAMomentObject === true ? value.format('YYYY-MM-DD') : value;
case 'datetime': // break;
cleanedData = value && value._isAMomentObject === true ? value.toISOString() : value; // case 'datetime':
// cleanedData = value && value._isAMomentObject === true ? value.toISOString() : value;
// break;
case 'time': {
cleanedData = value;
if (value && value.split(':').length < 3) {
cleanedData = `${value}:00`;
}
break; break;
}
case 'media': case 'media':
if (getOtherInfos(schema, [current, 'multiple']) === true) { if (getOtherInfos(schema, [current, 'multiple']) === true) {
cleanedData = value ? value.filter(file => !(file instanceof File)) : null; cleanedData = value ? value.filter(file => !(file instanceof File)) : null;

View File

@ -1,16 +1,26 @@
/** /**
* *
* Input * GenericInput
* This is a temp file move it to the helper plugin when ready * This is a temp file move it to the helper plugin when ready
*/ */
import React from 'react'; import React, { useState } from 'react';
import { useIntl } from 'react-intl';
import { ToggleInput } from '@strapi/parts/ToggleInput';
import { TextInput } from '@strapi/parts/TextInput';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import cloneDeep from 'lodash/cloneDeep';
import { formatISO } from 'date-fns';
import { DatePicker } from '@strapi/parts/DatePicker';
import { NumberInput } from '@strapi/parts/NumberInput';
import { Select, Option } from '@strapi/parts/Select';
import { Textarea } from '@strapi/parts/Textarea';
import { TextInput } from '@strapi/parts/TextInput';
import { TimePicker } from '@strapi/parts/TimePicker';
import { ToggleInput } from '@strapi/parts/ToggleInput';
import Hide from '@strapi/icons/Hide';
import Show from '@strapi/icons/Show';
const Input = ({ const GenericInput = ({
autoComplete,
customInputs, customInputs,
description, description,
disabled, disabled,
@ -19,12 +29,15 @@ const Input = ({
error, error,
name, name,
onChange, onChange,
options,
placeholder, placeholder,
step,
type, type,
value, value,
...rest ...rest
}) => { }) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const [showPassword, setShowPassword] = useState(false);
const CustomInput = customInputs ? customInputs[type] : null; const CustomInput = customInputs ? customInputs[type] : null;
@ -46,10 +59,12 @@ const Input = ({
); );
} }
const label = formatMessage( const label = intlLabel.id
? formatMessage(
{ id: intlLabel.id, defaultMessage: intlLabel.defaultMessage }, { id: intlLabel.id, defaultMessage: intlLabel.defaultMessage },
{ ...intlLabel.values } { ...intlLabel.values }
); )
: name;
const hint = description const hint = description
? formatMessage( ? formatMessage(
@ -58,7 +73,17 @@ const Input = ({
) )
: ''; : '';
if (type === 'bool') { const formattedPlaceholder = placeholder
? formatMessage(
{ id: placeholder.id, defaultMessage: placeholder.defaultMessage },
{ ...placeholder.values }
)
: '';
const errorMessage = error ? formatMessage({ id: error, defaultMessage: error }) : '';
switch (type) {
case 'bool': {
return ( return (
<ToggleInput <ToggleInput
checked={value || false} checked={value || false}
@ -81,18 +106,54 @@ const Input = ({
/> />
); );
} }
case 'date': {
return (
<DatePicker
clearLabel={formatMessage({ id: 'clearLabel', defaultMessage: 'Clear' })}
disabled={disabled}
error={errorMessage}
label={label}
labelAction={labelAction}
id={name}
hint={hint}
name={name}
onChange={date => {
const formattedDate = formatISO(cloneDeep(date), { representation: 'date' });
const formattedPlaceholder = placeholder onChange({ target: { name, value: formattedDate, type } });
? formatMessage( }}
{ id: placeholder.id, defaultMessage: placeholder.defaultMessage }, onClear={() => onChange({ target: { name, value: '', type } })}
{ ...placeholder.values } placeholder={formattedPlaceholder}
) selectedDate={value ? new Date(value) : null}
: ''; selectedDateLabel={formattedDate => `Date picker, current is ${formattedDate}`}
/>
const errorMessage = error ? formatMessage({ id: error, defaultMessage: error }) : ''; );
}
case 'number': {
return (
<NumberInput
disabled={disabled}
error={errorMessage}
label={label}
labelAction={labelAction}
id={name}
hint={hint}
name={name}
onValueChange={value => {
onChange({ target: { name, value, type } });
}}
placeholder={formattedPlaceholder}
step={step}
value={value || undefined}
/>
);
}
case 'email':
case 'text':
case 'string': {
return ( return (
<TextInput <TextInput
autoComplete={autoComplete}
disabled={disabled} disabled={disabled}
error={errorMessage} error={errorMessage}
label={label} label={label}
@ -106,19 +167,143 @@ const Input = ({
value={value || ''} value={value || ''}
/> />
); );
}
case 'password': {
return (
<TextInput
autoComplete={autoComplete}
disabled={disabled}
error={errorMessage}
endAction={
<button
aria-label={formatMessage({
id: 'Auth.form.password.show-password',
defaultMessage: 'Show password',
})}
onClick={() => {
setShowPassword(prev => !prev);
}}
style={{
border: 'none',
padding: 0,
background: 'transparent',
}}
type="button"
>
{showPassword ? <Show /> : <Hide />}
</button>
}
label={label}
labelAction={labelAction}
id={name}
hint={hint}
name={name}
onChange={onChange}
placeholder={formattedPlaceholder}
type={showPassword ? 'text' : 'password'}
value={value || ''}
/>
);
}
case 'select': {
return (
<Select
disabled={disabled}
error={errorMessage}
label={label}
labelAction={labelAction}
id={name}
hint={hint}
name={name}
onChange={value => {
onChange({ target: { name, value: value === '' ? null : value, type: 'select' } });
}}
placeholder={formattedPlaceholder}
value={value || ''}
>
{options.map(({ metadatas: { intlLabel, disabled, hidden }, key, value }) => {
return (
<Option key={key} value={value} disabled={disabled} hidden={hidden}>
{formatMessage(intlLabel)}
</Option>
);
})}
</Select>
);
}
case 'textarea': {
return (
<Textarea
disabled={disabled}
error={errorMessage}
label={label}
labelAction={labelAction}
id={name}
hint={hint}
name={name}
onChange={onChange}
placeholder={formattedPlaceholder}
type={type}
value={value || ''}
>
{value}
</Textarea>
);
}
case 'time': {
let time = value;
// The backend send a value which has the following format: '00:45:00.000'
// or the time picker only supports hours & minutes so we need to mutate the value
if (value && value.split(':').length > 2) {
time = time.split(':');
time.pop();
time = time.join(':');
}
return (
<TimePicker
clearLabel={formatMessage({ id: 'clearLabel', defaultMessage: 'Clear' })}
disabled={disabled}
error={errorMessage}
label={label}
labelAction={labelAction}
id={name}
hint={hint}
name={name}
onChange={time => {
onChange({ target: { name, value: `${time}`, type } });
}}
onClear={() => {
onChange({ target: { name, value: null, type } });
}}
placeholder={formattedPlaceholder}
step={step}
value={time}
/>
);
}
default: {
return <div>{type} is not supported</div>;
}
}
}; };
Input.defaultProps = { GenericInput.defaultProps = {
autoComplete: undefined,
customInputs: null, customInputs: null,
description: null, description: null,
disabled: false, disabled: false,
error: '', error: '',
labelAction: undefined, labelAction: undefined,
placeholder: null, placeholder: null,
options: [],
step: 1,
value: '', value: '',
}; };
Input.propTypes = { GenericInput.propTypes = {
autoComplete: PropTypes.string,
customInputs: PropTypes.object, customInputs: PropTypes.object,
description: PropTypes.shape({ description: PropTypes.shape({
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
@ -135,13 +320,28 @@ Input.propTypes = {
labelAction: PropTypes.element, labelAction: PropTypes.element,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
options: PropTypes.arrayOf(
PropTypes.shape({
metadatas: PropTypes.shape({
intlLabel: PropTypes.shape({
id: PropTypes.string.isRequired,
defaultMessage: PropTypes.string.isRequired,
}).isRequired,
disabled: PropTypes.bool,
hidden: PropTypes.bool,
}).isRequired,
key: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
}).isRequired
),
placeholder: PropTypes.shape({ placeholder: PropTypes.shape({
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
defaultMessage: PropTypes.string.isRequired, defaultMessage: PropTypes.string.isRequired,
values: PropTypes.object, values: PropTypes.object,
}), }),
step: PropTypes.number,
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
value: PropTypes.any, value: PropTypes.any,
}; };
export default Input; export default GenericInput;

View File

@ -166,7 +166,7 @@ function Inputs({
isRequired, isRequired,
]); ]);
const { label, description, visible } = metadatas; const { label, description, placeholder, visible } = metadatas;
if (visible === false) { if (visible === false) {
return null; return null;
@ -218,8 +218,10 @@ function Inputs({
// wysiwyg: WysiwygWithErrors, // wysiwyg: WysiwygWithErrors,
// uid: InputUID, // uid: InputUID,
// ...fields, // ...fields,
json: () => <div>TODO json</div>,
media: () => <div>TODO media</div>, media: () => <div>TODO media</div>,
uid: () => <div>TODO uid</div>, uid: () => <div>TODO uid</div>,
wysiwyg: () => <div>TODO wysiwyg</div>,
}} }}
multiple={fieldSchema.multiple || false} multiple={fieldSchema.multiple || false}
attribute={fieldSchema} attribute={fieldSchema}
@ -227,9 +229,10 @@ function Inputs({
onBlur={onBlur} onBlur={onBlur}
onChange={onChange} onChange={onChange}
options={options} options={options}
placeholder={placeholder ? { id: placeholder, defaultMessage: placeholder } : null}
step={step} step={step}
type={inputType} type={inputType}
validations={validations} // validations={validations}
value={inputValue} value={inputValue}
withDefaultValue={false} withDefaultValue={false}
/> />

View File

@ -1,19 +1,32 @@
import React from 'react'; const generateOptions = (options, isRequired = false) => {
import { FormattedMessage } from 'react-intl'; return [
{
const generateOptions = (options, isRequired = false) => [ metadatas: {
<FormattedMessage id="components.InputSelect.option.placeholder" key="__enum_option_null"> intlLabel: {
{msg => ( id: 'components.InputSelect.option.placeholder',
<option disabled={isRequired} hidden={isRequired} value=""> defaultMessage: 'Choose here',
{msg} },
</option> disabled: isRequired,
)} hidden: isRequired,
</FormattedMessage>, },
...options.map(v => ( key: '__enum_option_null',
<option key={v} value={v}> value: '',
{v} },
</option> ...options.map(option => {
)), return {
metadatas: {
intlLabel: {
id: option,
defaultMessage: option,
},
hidden: false,
disabled: false,
},
key: option,
value: option,
};
}),
]; ];
};
export default generateOptions; export default generateOptions;

View File

@ -156,7 +156,7 @@ const EditView = ({
{grid.map( {grid.map(
({ fieldSchema, labelAction, metadatas, name, size }) => { ({ fieldSchema, labelAction, metadatas, name, size }) => {
return ( return (
<GridItem col={size} key={name}> <GridItem col={size} key={name} s={12} xs={12}>
<Inputs <Inputs
fieldSchema={fieldSchema} fieldSchema={fieldSchema}
keys={name} keys={name}

View File

@ -7,7 +7,7 @@ import { TimePicker } from '@strapi/parts/TimePicker';
import { Select, Option } from '@strapi/parts/Select'; import { Select, Option } from '@strapi/parts/Select';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { formatISO } from 'date-fns'; import { formatISO } from 'date-fns';
import { cloneDeep } from 'lodash'; import cloneDeep from 'lodash/cloneDeep';
const Inputs = ({ label, onChange, options, type, value }) => { const Inputs = ({ label, onChange, options, type, value }) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();

View File

@ -110,6 +110,7 @@ const CMEditViewLocalePicker = ({
<Option <Option
value={value.value} value={value.value}
disabled disabled
hidden
startIcon={hasDraftAndPublishEnabled ? <Bullet status={currentLocaleStatus} /> : null} startIcon={hasDraftAndPublishEnabled ? <Bullet status={currentLocaleStatus} /> : null}
> >
{value.label} {value.label}