652 lines
20 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-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-07-16 15:19:28 +02:00
import init, { setDefaultForm } 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 createYupSchema from './utils/schema';
import {
2019-07-30 16:18:11 +02:00
getMediaAttributes,
cleanData,
mapDataKeysToFilesToUpload,
2019-07-30 17:25:18 +02:00
} from './utils/formatData';
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-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-07-30 17:25:18 +02:00
const submitAbortController = new AbortController();
const submitSignal = submitAbortController.signal;
2019-07-12 18:38:29 +02:00
const groups = Object.keys(attributes).reduce((acc, current) => {
2019-07-16 15:19:28 +02:00
const { group, repeatable, type, min } = get(attributes, [current], {
2019-07-12 18:38:29 +02:00
group: '',
type: '',
repeatable,
});
if (type === 'group') {
2019-07-16 15:19:28 +02:00
acc.push({ key: current, group, repeatable, isOpen: !repeatable, min });
2019-07-12 18:38:29 +02:00
}
return acc;
}, []);
const groupLayoutsToGet = groups
.filter(
(current, index) =>
groups.findIndex(el => el.group === current.group) === index
)
.map(({ group }) => group);
2019-07-12 15:39:18 +02:00
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-18 14:34:25 +02:00
// Cancel requests
const abortControllerFetchData = new AbortController();
const abortControllerLayouts = new AbortController();
const signalFetchData = abortControllerFetchData.signal;
const signalFetchLayouts = abortControllerLayouts.signal;
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',
signal: signalFetchLayouts,
})
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
// Retrieve all the default values for the repeatables and init the form
const defaultGroupValues = groups.reduce((acc, current) => {
const defaultForm = setDefaultForm(
get(groupLayouts, [current.group, 'schema', 'attributes'], {})
);
const arr = [];
if (current.min && current.repeatable === true) {
for (let i = 0; i < current.min; i++) {
arr.push({ ...defaultForm, _temp__id: i });
}
}
acc[current.key] = {
toSet: arr,
defaultRepeatable: defaultForm,
};
if (current.repeatable === false) {
acc[current.key] = {
toSet: defaultForm,
defaultRepeatable: defaultForm,
};
}
return acc;
}, {});
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 },
signal: signalFetchData,
});
dispatch({
type: 'GET_DATA_SUCCEEDED',
data,
});
fetchGroupLayouts();
} catch (err) {
if (err.code !== 20) {
strapi.notification.error(`${pluginId}.error.record.fetch`);
}
}
};
2019-07-12 18:38:29 +02:00
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-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 () => {
abortControllerFetchData.abort();
abortControllerLayouts.abort();
2019-07-30 17:25:18 +02:00
submitAbortController.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-07-11 16:53:00 +02:00
/**
* Retrieve external links from injected components
* @type {Array} List of external links to display
*/
const retrieveLinksContainerComponent = () => {
const componentToInject = Object.keys(plugins).reduce((acc, current) => {
// Retrieve injected compos from plugin
// if compo can be injected in left.links area push the compo in the array
const currentPlugin = plugins[current];
const injectedComponents = get(currentPlugin, 'injectedComponents', []);
const compos = injectedComponents
.filter(compo => {
return (
compo.plugin === `${pluginId}.editPage` &&
compo.area === 'right.links'
);
})
.map(compo => {
const Component = compo.component;
return (
<Component
currentEnvironment={currentEnvironment}
getModelName={() => slug}
getSource={() => source}
getContentTypeBuilderBaseUrl={() =>
'/plugins/content-type-builder/models/'
}
{...compo.props}
key={compo.key}
onClick={() => {
emitEvent('willEditContentTypeFromEditView');
}}
/>
);
});
return [...acc, ...compos];
}, []);
return componentToInject;
};
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 = { 'X-Forwarded-Host': 'strapi' };
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,
signal: submitSignal,
},
false,
false
);
2019-07-30 17:41:14 +02:00
emitEvent('didSaveEntry');
redirectToPreviousPage();
} catch (err) {
console.log('send data error', err);
emitEvent('didNotSaveEntry', { error: err });
// TODO handle errors from the API
strapi.notification.error(
`${pluginId}.containers.EditView.notification.errors`
);
}
2019-07-18 16:53:12 +02:00
} catch (err) {
2019-07-30 17:25:18 +02:00
setIsSubmitting(false);
2019-07-30 16:18:11 +02:00
console.log({ err });
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;
}, {});
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-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('.'),
});
}}
onChange={({ target: { name, value } }) => {
dispatch({
type: 'ON_CHANGE',
keys: name.split('.'),
value,
});
}}
onRemove={keys => {
dispatch({
type: 'REMOVE_RELATION',
keys,
});
}}
pathname={pathname}
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,
style: isSubmitting
? { marginRight: '18px', flexGrow: 2 }
: { flexGrow: 2 },
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-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 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-07-15 13:00:31 +02:00
addField={keys => {
dispatch({
type: 'ADD_FIELD_TO_GROUP',
keys: keys.split('.'),
});
}}
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-07-13 10:04:25 +02:00
onChange={({ target: { name, value } }) => {
dispatch({
type: 'ON_CHANGE',
keys: name.split('.'),
value,
});
}}
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-07-12 15:44:23 +02:00
onChange={({ target: { name, value } }) => {
dispatch({
type: 'ON_CHANGE',
keys: name.split('.'),
value,
});
}}
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');
}}
/>
{retrieveLinksContainerComponent()}
</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);