616 lines
19 KiB
JavaScript
Raw Normal View History

import React, { memo, useEffect, useState, useReducer } from 'react';
2019-07-11 11:35:18 +02:00
import PropTypes from 'prop-types';
import { cloneDeep, get } from 'lodash';
2019-07-11 11:35:18 +02:00
import {
BackHeader,
2019-07-11 11:35:18 +02:00
getQueryParameters,
LoadingIndicatorPage,
2019-07-11 16:53:00 +02:00
LiLink,
2019-07-11 11:35:18 +02:00
PluginHeader,
PopUpWarning,
2019-09-06 16:01:36 +02:00
getYupInnerErrors,
2019-07-11 11:35:18 +02:00
request,
templateObject,
} from 'strapi-helper-plugin';
import pluginId from '../../pluginId';
2019-07-17 17:39:43 +02:00
import { EditViewProvider } from '../../contexts/EditView';
2019-07-11 11:35:18 +02:00
import Container from '../../components/Container';
2019-07-17 17:39:43 +02:00
import Group from '../../components/Group';
import Inputs from '../../components/Inputs';
import SelectWrapper from '../../components/SelectWrapper';
2019-08-22 17:15:15 +02:00
import createYupSchema from './utils/schema';
import setDefaultForm from './utils/createDefaultForm';
import getInjectedComponents from './utils/getComponents';
import init from './init';
2019-07-11 11:35:18 +02:00
import reducer, { initialState } from './reducer';
2019-07-17 17:39:43 +02:00
import { LinkWrapper, MainWrapper, SubWrapper } from './components';
2019-07-30 17:25:18 +02:00
import {
2019-07-30 16:18:11 +02:00
getMediaAttributes,
cleanData,
mapDataKeysToFilesToUpload,
2019-07-30 17:25:18 +02:00
} from './utils/formatData';
2019-08-22 17:15:15 +02:00
import {
getDefaultGroupValues,
retrieveDisplayedGroups,
retrieveGroupLayoutsToFetch,
} from './utils/groups';
2019-07-11 11:35:18 +02:00
const getRequestUrl = path => `/${pluginId}/explorer/${path}`;
function EditView({
2019-07-11 16:53:00 +02:00
currentEnvironment,
emitEvent,
2019-07-11 11:35:18 +02:00
layouts,
2019-07-16 18:53:41 +02:00
location: { pathname, search },
history: { push },
2019-07-11 11:35:18 +02:00
match: {
params: { slug, id },
},
2019-07-11 16:53:00 +02:00
plugins,
2019-07-11 11:35:18 +02:00
}) {
2019-08-22 17:15:15 +02:00
const abortController = new AbortController();
const { signal } = abortController;
2019-07-12 15:39:18 +02:00
const layout = get(layouts, [slug], {});
const isCreatingEntry = id === 'create';
2019-07-12 18:38:29 +02:00
const attributes = get(layout, ['schema', 'attributes'], {});
2019-08-22 17:15:15 +02:00
const groups = retrieveDisplayedGroups(attributes);
const groupLayoutsToGet = retrieveGroupLayoutsToFetch(groups);
// States
const [showWarningCancel, setWarningCancel] = useState(false);
const [showWarningDelete, setWarningDelete] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
2019-07-11 11:35:18 +02:00
const [reducerState, dispatch] = useReducer(reducer, initialState, () =>
2019-07-12 15:39:18 +02:00
init(initialState, layout, isCreatingEntry)
2019-07-11 11:35:18 +02:00
);
2019-07-17 17:39:43 +02:00
2019-07-12 15:39:18 +02:00
const state = reducerState.toJS();
2019-07-12 18:38:29 +02:00
const {
2019-07-18 16:53:12 +02:00
didCheckErrors,
errors,
2019-07-12 18:38:29 +02:00
groupLayoutsData,
initialData,
modifiedData,
isLoading,
isLoadingForLayouts,
} = state;
2019-07-11 11:35:18 +02:00
const source = getQueryParameters(search, 'source');
2019-07-12 18:38:29 +02:00
const shouldShowLoader =
isLoadingForLayouts || (!isCreatingEntry && isLoading);
2019-07-11 11:35:18 +02:00
useEffect(() => {
2019-07-12 18:38:29 +02:00
const fetchGroupLayouts = async () => {
try {
const data = await Promise.all(
groupLayoutsToGet.map(uid =>
2019-07-24 11:10:29 +02:00
request(`/${pluginId}/groups/${uid}`, {
2019-07-18 14:34:25 +02:00
method: 'GET',
2019-08-22 17:15:15 +02:00
signal,
2019-07-18 14:34:25 +02:00
})
2019-07-12 18:38:29 +02:00
)
);
2019-07-24 11:10:29 +02:00
2019-07-12 18:38:29 +02:00
const groupLayouts = data.reduce((acc, current) => {
2019-07-24 18:24:23 +02:00
acc[current.data.uid] = current.data;
2019-07-12 18:38:29 +02:00
return acc;
}, {});
2019-07-16 15:19:28 +02:00
2019-08-22 17:15:15 +02:00
// Retrieve all the default values for the repeatables and init the form
const defaultGroupValues = getDefaultGroupValues(groups, groupLayouts);
2019-07-16 15:19:28 +02:00
2019-07-12 18:38:29 +02:00
dispatch({
type: 'GET_GROUP_LAYOUTS_SUCCEEDED',
groupLayouts,
2019-07-16 15:19:28 +02:00
defaultGroupValues,
isCreatingEntry,
2019-07-12 18:38:29 +02:00
});
} catch (err) {
// TODO ADD A TRAD
2019-07-18 14:34:25 +02:00
if (err.code !== 20) {
strapi.notification.error('notification.error');
}
2019-07-12 18:38:29 +02:00
}
};
2019-07-18 18:14:29 +02:00
const fetchData = async () => {
try {
const data = await request(getRequestUrl(`${slug}/${id}`), {
method: 'GET',
params: { source },
2019-08-22 17:15:15 +02:00
signal,
2019-07-18 18:14:29 +02:00
});
dispatch({
type: 'GET_DATA_SUCCEEDED',
data,
2019-08-26 17:29:46 +02:00
defaultForm: setDefaultForm(get(layout, ['schema', 'attributes'])),
2019-07-18 18:14:29 +02:00
});
fetchGroupLayouts();
} catch (err) {
if (err.code !== 20) {
strapi.notification.error(`${pluginId}.error.record.fetch`);
}
}
};
2019-07-12 18:38:29 +02:00
2019-09-16 17:47:43 +02:00
// Force state to be cleared when navigation from one entry to another
dispatch({ type: 'RESET_PROPS' });
2019-07-11 11:35:18 +02:00
if (!isCreatingEntry) {
fetchData();
2019-07-17 18:22:33 +02:00
} else {
dispatch({
type: 'INIT',
data: setDefaultForm(get(layout, ['schema', 'attributes'])),
2019-08-26 17:29:46 +02:00
defaultForm: setDefaultForm(get(layout, ['schema', 'attributes'])),
2019-07-17 18:22:33 +02:00
});
2019-07-18 18:14:29 +02:00
fetchGroupLayouts();
2019-07-11 11:35:18 +02:00
}
2019-07-17 18:22:33 +02:00
2019-07-18 14:34:25 +02:00
return () => {
2019-08-22 17:15:15 +02:00
abortController.abort();
2019-07-18 14:34:25 +02:00
};
2019-07-12 18:38:29 +02:00
// eslint-disable-next-line react-hooks/exhaustive-deps
2019-07-17 18:22:33 +02:00
}, [id, isCreatingEntry, slug, source, pathname]);
2019-07-11 11:35:18 +02:00
if (shouldShowLoader) {
return <LoadingIndicatorPage />;
}
const toggleWarningCancel = () => setWarningCancel(prevState => !prevState);
const toggleWarningDelete = () => setWarningDelete(prevState => !prevState);
2019-07-16 18:53:41 +02:00
const redirectURL = search
.split('redirectUrl=')
.filter((_, index) => index !== 0)
.join('');
const redirectToPreviousPage = () => push(redirectURL);
const handleConfirmDelete = async () => {
toggleWarningDelete();
setIsSubmitting(true);
try {
await request(getRequestUrl(`${slug}/${id}`), {
method: 'DELETE',
params: { source },
});
strapi.notification.success(`${pluginId}.success.record.delete`);
redirectToPreviousPage();
} catch (err) {
setIsSubmitting(false);
strapi.notification.error(`${pluginId}.error.record.delete`);
}
};
2019-07-18 16:53:12 +02:00
const displayedFieldNameInHeader = get(
layout,
['settings', 'mainField'],
'id'
);
2019-07-11 11:35:18 +02:00
const pluginHeaderTitle = isCreatingEntry
? { id: `${pluginId}.containers.Edit.pluginHeader.title.new` }
: templateObject({ mainField: displayedFieldNameInHeader }, initialData)
.mainField;
2019-07-16 18:53:41 +02:00
const displayedRelations = get(layout, ['layouts', 'editRelations'], []);
const hasRelations = displayedRelations.length > 0;
2019-07-12 14:15:56 +02:00
const fields = get(layout, ['layouts', 'edit'], []);
2019-08-21 18:05:11 +02:00
const checkFormErrors = async () => {
const schema = createYupSchema(layout, { groups: groupLayoutsData });
let errors = {};
try {
// Validate the form using yup
await schema.validate(modifiedData, { abortEarly: false });
} catch (err) {
2019-09-06 16:01:36 +02:00
errors = getYupInnerErrors(err);
2019-08-21 18:05:11 +02:00
}
2019-09-06 16:01:36 +02:00
2019-08-21 18:05:11 +02:00
dispatch({
type: 'SET_ERRORS',
errors,
});
};
2019-09-13 14:46:31 +02:00
const handleChange = ({ target: { name, value, type } }) => {
let inputValue = value;
// Empty string is not a valid date,
// Set the date to null when it's empty
if (type === 'date' && value === '') {
inputValue = null;
}
dispatch({
type: 'ON_CHANGE',
keys: name.split('.'),
value: inputValue,
});
};
2019-07-18 16:53:12 +02:00
const handleSubmit = async e => {
e.preventDefault();
2019-07-19 09:32:36 +02:00
const schema = createYupSchema(layout, { groups: groupLayoutsData });
2019-07-18 16:53:12 +02:00
try {
2019-07-30 17:41:14 +02:00
// Validate the form using yup
2019-07-18 16:53:12 +02:00
await schema.validate(modifiedData, { abortEarly: false });
2019-07-30 17:41:14 +02:00
// Set the loading state in the plugin header
2019-07-30 17:25:18 +02:00
setIsSubmitting(true);
2019-07-30 17:41:14 +02:00
emitEvent('willSaveEntry');
// Create an object containing all the paths of the media fields
2019-07-30 16:18:11 +02:00
const filesMap = getMediaAttributes(layout, groupLayoutsData);
// Create an object that maps the keys with the related files to upload
const filesToUpload = mapDataKeysToFilesToUpload(filesMap, modifiedData);
2019-07-30 16:18:11 +02:00
const cleanedData = cleanData(
cloneDeep(modifiedData),
layout,
groupLayoutsData
);
2019-07-30 16:18:11 +02:00
const formData = new FormData();
2019-07-30 16:18:11 +02:00
formData.append('data', JSON.stringify(cleanedData));
2019-07-30 16:18:11 +02:00
Object.keys(filesToUpload).forEach(key => {
const files = filesToUpload[key];
2019-07-30 16:18:11 +02:00
files.forEach(file => {
formData.append(`files.${key}`, file);
});
});
2019-07-30 16:18:11 +02:00
// Change the request helper default headers so we can pass a FormData
const headers = {};
2019-07-30 16:18:11 +02:00
const method = isCreatingEntry ? 'POST' : 'PUT';
const endPoint = isCreatingEntry ? slug : `${slug}/${id}`;
2019-07-30 17:41:14 +02:00
try {
// Time to actually send the data
await request(
getRequestUrl(endPoint),
{
method,
headers,
params: { source },
body: formData,
2019-08-22 17:15:15 +02:00
signal,
},
false,
false
);
2019-07-30 17:41:14 +02:00
emitEvent('didSaveEntry');
redirectToPreviousPage();
} catch (err) {
2019-08-14 10:30:58 +02:00
const error = get(
err,
['response', 'payload', 'message', '0', 'messages', '0', 'id'],
'SERVER ERROR'
2019-07-30 17:41:14 +02:00
);
2019-08-14 10:30:58 +02:00
setIsSubmitting(false);
emitEvent('didNotSaveEntry', { error: err });
strapi.notification.error(error);
2019-07-30 17:41:14 +02:00
}
2019-07-18 16:53:12 +02:00
} catch (err) {
2019-07-30 17:25:18 +02:00
setIsSubmitting(false);
2019-07-18 16:53:12 +02:00
const errors = get(err, 'inner', []).reduce((acc, curr) => {
acc[
curr.path
.split('[')
.join('.')
.split(']')
.join('')
] = [{ id: curr.message }];
return acc;
}, {});
2019-09-13 14:46:31 +02:00
2019-07-18 16:53:12 +02:00
dispatch({
type: 'SET_ERRORS',
errors,
});
2019-07-19 09:32:36 +02:00
strapi.notification.error(
`${pluginId}.containers.EditView.notification.errors`
);
2019-07-18 16:53:12 +02:00
}
};
2019-07-11 11:35:18 +02:00
return (
2019-07-17 17:39:43 +02:00
<EditViewProvider
addRelation={({ target: { name, value } }) => {
dispatch({
type: 'ADD_RELATION',
keys: name.split('.'),
value,
});
}}
2019-08-21 18:05:11 +02:00
checkFormErrors={checkFormErrors}
2019-07-18 16:53:12 +02:00
didCheckErrors={didCheckErrors}
errors={errors}
2019-07-17 17:39:43 +02:00
moveRelation={(dragIndex, overIndex, name) => {
dispatch({
type: 'MOVE_FIELD',
dragIndex,
overIndex,
keys: name.split('.'),
});
}}
2019-09-13 14:46:31 +02:00
onChange={handleChange}
2019-07-17 17:39:43 +02:00
onRemove={keys => {
dispatch({
type: 'REMOVE_RELATION',
keys,
});
}}
pathname={pathname}
2019-08-09 15:46:23 +02:00
resetErrors={() => {
dispatch({
type: 'SET_ERRORS',
errors: {},
});
}}
2019-08-21 17:35:13 +02:00
resetGroupData={groupName => {
dispatch({
type: 'RESET_GROUP_DATA',
groupName,
});
}}
2019-07-17 17:39:43 +02:00
search={search}
>
<BackHeader onClick={() => redirectToPreviousPage()} />
<Container className="container-fluid">
<form onSubmit={handleSubmit}>
<PluginHeader
actions={[
{
label: `${pluginId}.containers.Edit.reset`,
kind: 'secondary',
onClick: () => {
toggleWarningCancel();
},
type: 'button',
disabled: isSubmitting, // TODO STATE WHEN SUBMITING
},
{
kind: 'primary',
label: `${pluginId}.containers.Edit.submit`,
type: 'submit',
loader: isSubmitting,
2019-09-03 21:15:32 +02:00
style: isSubmitting ? { marginRight: '18px' } : {},
disabled: isSubmitting, // TODO STATE WHEN SUBMITING
},
]}
subActions={
isCreatingEntry
? []
: [
{
label: 'app.utils.delete',
kind: 'delete',
onClick: () => {
toggleWarningDelete();
},
type: 'button',
disabled: isSubmitting, // TODO STATE WHEN SUBMITING
},
]
}
title={pluginHeaderTitle}
/>
2019-07-11 16:53:00 +02:00
<div className="row">
2019-07-24 18:24:23 +02:00
<div className="col-md-12 col-lg-9">
2019-07-12 14:15:56 +02:00
<MainWrapper>
{fields.map((fieldsRow, key) => {
2019-08-13 16:41:15 +02:00
if (fieldsRow.length === 0) {
return null;
}
2019-07-12 18:38:29 +02:00
const [{ name }] = fieldsRow;
const group = get(layout, ['schema', 'attributes', name], {});
2019-07-24 10:16:44 +02:00
const groupMetas = get(
layout,
['metadatas', name, 'edit'],
{}
);
2019-07-13 10:04:25 +02:00
const groupValue = get(
modifiedData,
[name],
group.repeatable ? [] : {}
);
2019-07-12 18:38:29 +02:00
if (fieldsRow.length === 1 && group.type === 'group') {
2019-08-09 16:59:28 +02:00
// Array containing all the keys with of the error object created by YUP
// It is used only to know if whether or not we need to apply an orange border to the n+1 field item
2019-08-09 13:23:39 +02:00
const groupErrorKeys = Object.keys(errors)
.filter(errorKey => errorKey.includes(name))
.map(errorKey =>
errorKey
.split('.')
.slice(0, 2)
.join('.')
);
2019-07-12 18:38:29 +02:00
return (
<Group
{...group}
2019-07-24 10:16:44 +02:00
{...groupMetas}
2019-08-21 17:35:13 +02:00
addField={(keys, isRepeatable = true) => {
2019-07-15 13:00:31 +02:00
dispatch({
type: 'ADD_FIELD_TO_GROUP',
keys: keys.split('.'),
2019-08-21 17:35:13 +02:00
isRepeatable,
2019-07-15 13:00:31 +02:00
});
}}
2019-08-09 13:23:39 +02:00
groupErrorKeys={groupErrorKeys}
2019-07-13 10:04:25 +02:00
groupValue={groupValue}
key={key}
isRepeatable={group.repeatable || false}
2019-07-13 10:04:25 +02:00
name={name}
modifiedData={modifiedData}
2019-07-15 17:29:13 +02:00
moveGroupField={(dragIndex, overIndex, name) => {
dispatch({
type: 'MOVE_FIELD',
2019-07-15 17:29:13 +02:00
dragIndex,
overIndex,
keys: name.split('.'),
});
}}
2019-09-13 14:46:31 +02:00
onChange={handleChange}
layout={get(groupLayoutsData, group.group, {})}
2019-07-17 16:14:00 +02:00
pathname={pathname}
removeField={(keys, shouldAddEmptyField) => {
2019-07-15 13:00:31 +02:00
dispatch({
type: 'ON_REMOVE_FIELD',
keys: keys.split('.'),
shouldAddEmptyField,
2019-07-15 13:00:31 +02:00
});
}}
2019-07-12 18:38:29 +02:00
/>
);
}
2019-07-12 14:15:56 +02:00
return (
<div key={key} className="row">
2019-07-17 12:06:19 +02:00
{fieldsRow.map(({ name }, index) => {
2019-07-12 14:15:56 +02:00
return (
2019-07-12 15:39:18 +02:00
<Inputs
2019-07-17 12:06:19 +02:00
autoFocus={key === 0 && index === 0}
2019-07-18 16:53:12 +02:00
didCheckErrors={didCheckErrors}
errors={errors}
2019-07-12 15:39:18 +02:00
key={name}
keys={name}
layout={layout}
modifiedData={modifiedData}
2019-07-12 15:39:18 +02:00
name={name}
2019-09-13 14:46:31 +02:00
onChange={handleChange}
2019-07-12 15:39:18 +02:00
/>
2019-07-12 14:15:56 +02:00
);
})}
</div>
);
})}
</MainWrapper>
</div>
<div className="col-md-12 col-lg-3">
{hasRelations && (
<SubWrapper
2019-07-29 17:11:53 +02:00
style={{ padding: '0 20px 1px', marginBottom: '26px' }}
2019-07-12 14:15:56 +02:00
>
2019-07-22 11:41:27 +02:00
<div style={{ paddingTop: '22px' }}>
2019-07-16 18:53:41 +02:00
{displayedRelations.map(relationName => {
const relation = get(
layout,
['schema', 'attributes', relationName],
{}
);
const relationMetas = get(
layout,
2019-07-24 10:16:44 +02:00
['metadatas', relationName, 'edit'],
2019-07-16 18:53:41 +02:00
{}
);
const value = get(modifiedData, [relationName], null);
return (
2019-07-17 12:06:19 +02:00
<SelectWrapper
2019-07-16 18:53:41 +02:00
{...relation}
{...relationMetas}
2019-07-17 12:06:19 +02:00
key={relationName}
2019-07-16 18:53:41 +02:00
name={relationName}
2019-07-24 10:16:44 +02:00
relationsType={relation.relationType}
2019-07-16 18:53:41 +02:00
value={value}
/>
);
})}
2019-07-15 13:00:31 +02:00
</div>
2019-07-12 14:15:56 +02:00
</SubWrapper>
)}
2019-07-11 16:53:00 +02:00
<LinkWrapper>
<ul>
<LiLink
message={{
id: `${pluginId}.containers.Edit.Link.Layout`,
}}
icon="layout"
key={`${pluginId}.link`}
2019-07-31 16:29:04 +02:00
url={`/plugins/${pluginId}/ctm-configurations/models/${slug}/edit-settings${
source !== pluginId ? `?source=${source}` : ''
}`}
2019-07-11 16:53:00 +02:00
onClick={() => {
emitEvent('willEditContentTypeLayoutFromEditView');
}}
/>
2019-08-22 17:15:15 +02:00
{getInjectedComponents(
'right.links',
plugins,
currentEnvironment,
slug,
source,
emitEvent
)}
2019-07-11 16:53:00 +02:00
</ul>
</LinkWrapper>
</div>
</div>
</form>
<PopUpWarning
isOpen={showWarningCancel}
toggleModal={toggleWarningCancel}
content={{
title: `${pluginId}.popUpWarning.title`,
message: `${pluginId}.popUpWarning.warning.cancelAllSettings`,
cancel: `${pluginId}.popUpWarning.button.cancel`,
confirm: `${pluginId}.popUpWarning.button.confirm`,
}}
popUpWarningType="danger"
onConfirm={() => {
dispatch({
type: 'RESET_FORM',
});
toggleWarningCancel();
}}
/>
<PopUpWarning
isOpen={showWarningDelete}
toggleModal={toggleWarningDelete}
content={{
title: `${pluginId}.popUpWarning.title`,
message: `${pluginId}.popUpWarning.bodyMessage.contentType.delete`,
cancel: `${pluginId}.popUpWarning.button.cancel`,
confirm: `${pluginId}.popUpWarning.button.confirm`,
}}
popUpWarningType="danger"
onConfirm={handleConfirmDelete}
2019-07-11 11:35:18 +02:00
/>
</Container>
2019-07-17 17:39:43 +02:00
</EditViewProvider>
2019-07-11 11:35:18 +02:00
);
2019-07-10 09:31:26 +02:00
}
2019-07-11 11:35:18 +02:00
EditView.propTypes = {
2019-07-11 16:53:00 +02:00
currentEnvironment: PropTypes.string.isRequired,
emitEvent: PropTypes.func.isRequired,
history: PropTypes.shape({
push: PropTypes.func.isRequired,
}),
2019-07-11 11:35:18 +02:00
layouts: PropTypes.object,
location: PropTypes.shape({
2019-07-19 17:42:47 +02:00
pathname: PropTypes.string,
2019-07-11 11:35:18 +02:00
search: PropTypes.string,
}),
match: PropTypes.shape({
params: PropTypes.shape({
id: PropTypes.string.isRequired,
slug: PropTypes.string.isRequired,
}),
}),
2019-07-11 16:53:00 +02:00
plugins: PropTypes.object,
2019-07-11 11:35:18 +02:00
};
export default memo(EditView);