Merge pull request #5179 from strapi/single-types/ctb-edit

Edit single type options
This commit is contained in:
cyril lopez 2020-02-06 15:25:32 +01:00 committed by GitHub
commit 7466d64ed8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 224 additions and 53 deletions

View File

@ -1,5 +1,4 @@
{ {
"kind": "singleType",
"connection": "default", "connection": "default",
"collectionName": "homepage", "collectionName": "homepage",
"info": { "info": {
@ -7,12 +6,16 @@
}, },
"options": { "options": {
"increments": true, "increments": true,
"timestamps": ["created_at", "updated_at"] "timestamps": [
"created_at",
"updated_at"
]
}, },
"attributes": { "attributes": {
"title": { "title": {
"type": "string", "type": "string",
"required": true "required": true
} }
} },
"kind": "singleType"
} }

View File

@ -6,7 +6,7 @@ const Wrapper = styled(TransitionGroup)`
top: 72px; top: 72px;
left: 240px; left: 240px;
right: 0; right: 0;
z-index: 1000; z-index: 1100;
list-style: none; list-style: none;
width: 300px; width: 300px;
margin: 0 auto; margin: 0 auto;

View File

@ -256,6 +256,7 @@
"components.Input.error.password.noMatch": "Passwords do not match", "components.Input.error.password.noMatch": "Passwords do not match",
"form.button.done": "Done", "form.button.done": "Done",
"form.button.finish": "Finish", "form.button.finish": "Finish",
"notification.contentType.relations.conflict": "Content type has conflicting relations",
"notification.form.error.fields": "The form contains some errors", "notification.form.error.fields": "The form contains some errors",
"notification.form.success.fields": "Changes saved", "notification.form.success.fields": "Changes saved",
"global.prompt.unsaved": "Are you sure you want to leave this page? All your modifications will be lost" "global.prompt.unsaved": "Are you sure you want to leave this page? All your modifications will be lost"

View File

@ -14,9 +14,9 @@ const BooleanBox = ({ label, name, onChange, options, value }) => {
<Div> <Div>
<Label htmlFor={name}>{label}</Label> <Label htmlFor={name}>{label}</Label>
<Wrapper> <Wrapper>
{options.map(option => { {options.map(option => (
return (
<Enumeration <Enumeration
{...option}
key={option.value} key={option.value}
id={option.value.toString()} id={option.value.toString()}
className="option-input" className="option-input"
@ -26,10 +26,8 @@ const BooleanBox = ({ label, name, onChange, options, value }) => {
type="radio" type="radio"
value={option.value} value={option.value}
/> />
); ))}
})} {options.map(option => (
{options.map(option => {
return (
<EnumerationWrapper <EnumerationWrapper
className="option" className="option"
key={option.value} key={option.value}
@ -41,8 +39,7 @@ const BooleanBox = ({ label, name, onChange, options, value }) => {
</span> </span>
<p>{formatMessage({ id: option.descriptionId })}</p> <p>{formatMessage({ id: option.descriptionId })}</p>
</EnumerationWrapper> </EnumerationWrapper>
); ))}
})}
</Wrapper> </Wrapper>
</Div> </Div>
); );

View File

@ -159,7 +159,11 @@ function List({
label: isInDevelopmentMode label: isInDevelopmentMode
? formatMessage({ ? formatMessage({
id: !isSub id: !isSub
? `${pluginId}.form.button.add.field.to.${editTarget}` ? `${pluginId}.form.button.add.field.to.${
modifiedData.contentType
? modifiedData.contentType.schema.kind
: editTarget
}`
: `${pluginId}.form.button.add.field.to.component`, : `${pluginId}.form.button.add.field.to.component`,
}) })
: null, : null,

View File

@ -399,7 +399,7 @@ const DataManagerProvider = ({ allIcons, children }) => {
return <Redirect to={`/plugins/${pluginId}/content-types/${firstCTUid}`} />; return <Redirect to={`/plugins/${pluginId}/content-types/${firstCTUid}`} />;
} }
const submitData = async () => { const submitData = async additionalContentTypeData => {
try { try {
const isCreating = get( const isCreating = get(
modifiedData, modifiedData,
@ -416,7 +416,10 @@ const DataManagerProvider = ({ allIcons, children }) => {
}; };
if (isInContentTypeView) { if (isInContentTypeView) {
body.contentType = formatMainDataType(modifiedData.contentType); body.contentType = {
...formatMainDataType(modifiedData.contentType),
...additionalContentTypeData,
};
emitEvent('willSaveContentType'); emitEvent('willSaveContentType');
} else { } else {

View File

@ -478,7 +478,7 @@ const reducer = (state, action) => {
} }
case 'UPDATE_SCHEMA': { case 'UPDATE_SCHEMA': {
const { const {
data: { name, collectionName, category, icon }, data: { name, collectionName, category, icon, kind },
schemaType, schemaType,
uid, uid,
} = action; } = action;
@ -493,6 +493,9 @@ const reducer = (state, action) => {
.update('category', () => category) .update('category', () => category)
.updateIn(['schema', 'icon'], () => icon); .updateIn(['schema', 'icon'], () => icon);
} }
if (action.schemaType === 'contentType') {
updatedObj = updatedObj.updateIn(['schema', 'kind'], () => kind);
}
return updatedObj; return updatedObj;
}); });

View File

@ -40,6 +40,7 @@ import { NAVLINKS, INITIAL_STATE_DATA } from './utils/staticData';
import init from './init'; import init from './init';
import reducer, { initialState } from './reducer'; import reducer, { initialState } from './reducer';
import CustomButton from './CustomButton'; import CustomButton from './CustomButton';
import canEditContentType from './utils/canEditContentType';
/* eslint-disable indent */ /* eslint-disable indent */
/* eslint-disable react/no-array-index-key */ /* eslint-disable react/no-array-index-key */
@ -63,6 +64,7 @@ const FormModal = () => {
deleteCategory, deleteCategory,
deleteData, deleteData,
editCategory, editCategory,
submitData,
modifiedData: allDataSchema, modifiedData: allDataSchema,
nestedComponents, nestedComponents,
setModifiedData, setModifiedData,
@ -125,6 +127,7 @@ const FormModal = () => {
actionType, actionType,
attributeName, attributeName,
attributeType, attributeType,
contentTypeKind,
dynamicZoneTarget, dynamicZoneTarget,
forTarget, forTarget,
modalType, modalType,
@ -158,7 +161,6 @@ const FormModal = () => {
header_info_name_5, header_info_name_5,
header_info_category_5, header_info_category_5,
headerId, headerId,
contentTypeKind,
}); });
// Reset all the modification when opening the edit category modal // Reset all the modification when opening the edit category modal
@ -217,7 +219,7 @@ const FormModal = () => {
state.modalType !== 'contentType' && state.modalType !== 'contentType' &&
actionType === 'edit' actionType === 'edit'
) { ) {
const { name, collectionName } = get( const { name, collectionName, kind } = get(
allDataSchema, allDataSchema,
[...pathToSchema, 'schema'], [...pathToSchema, 'schema'],
{ name: null, collectionName: null } { name: null, collectionName: null }
@ -228,6 +230,7 @@ const FormModal = () => {
data: { data: {
name, name,
collectionName, collectionName,
kind,
}, },
}); });
} }
@ -601,9 +604,14 @@ const FormModal = () => {
uid uid
); );
} else { } else {
updateSchema(modifiedData, state.modalType); if (canEditContentType(allDataSchema, modifiedData)) {
// Close the modal
push({ search: '' }); push({ search: '' });
submitData(modifiedData);
} else {
strapi.notification.error(
'notification.contentType.relations.conflict'
);
}
return; return;
} }
@ -1346,13 +1354,13 @@ const FormModal = () => {
</ModalForm> </ModalForm>
{!isPickingAttribute && ( {!isPickingAttribute && (
<ModalFooter> <ModalFooter>
<section> <section style={{ alignItems: 'center' }}>
<Button type="button" color="cancel" onClick={handleToggle}> <Button type="button" color="cancel" onClick={handleToggle}>
{formatMessage({ {formatMessage({
id: 'components.popUpWarning.button.cancel', id: 'components.popUpWarning.button.cancel',
})} })}
</Button> </Button>
<div style={{ margin: 'auto 0' }}> <div>
{isCreatingAttribute && !isInFirstComponentStep && ( {isCreatingAttribute && !isInFirstComponentStep && (
<Button <Button
type={isCreating ? 'button' : 'submit'} type={isCreating ? 'button' : 'submit'}

View File

@ -0,0 +1,23 @@
import { get } from 'lodash';
const canEditContentType = (data, modifiedData) => {
const kind = get(data, ['contentType', 'schema', 'kind'], '');
// if kind isn't modified or content type is a single type, there is no need to check attributes.
if (kind === 'singleType' || kind === modifiedData.kind) {
return true;
}
const contentTypeAttributes = get(
data,
['contentType', 'schema', 'attributes'],
''
);
const relationAttributes = Object.values(contentTypeAttributes).filter(
({ nature }) => nature && !['oneWay', 'manyWay'].includes(nature)
);
return relationAttributes.length === 0;
};
export default canEditContentType;

View File

@ -766,6 +766,7 @@ const forms = {
.isAllowed(getTrad('error.contentTypeName.reserved-name')) .isAllowed(getTrad('error.contentTypeName.reserved-name'))
.required(errorsTrads.required), .required(errorsTrads.required),
collectionName: yup.string(), collectionName: yup.string(),
kind: yup.string().oneOf(['singleType', 'collectionType']),
}); });
}, },
form: { form: {
@ -800,6 +801,32 @@ const forms = {
}); });
} }
if (actionType === 'edit') {
items[0].push({
label: {
id: getTrad('modalForm.attribute.text.type-selection'),
},
name: 'kind',
type: 'booleanBox',
size: 12,
options: [
{
headerId: getTrad('menu.section.models.name.singular'),
descriptionId: getTrad(
'form.button.collection-type.description'
),
value: 'collectionType',
},
{
headerId: getTrad('menu.section.single-types.name.singular'),
descriptionId: getTrad('form.button.single-type.description'),
value: 'singleType',
},
],
validations: {},
});
}
return { items }; return { items };
}, },
advanced() { advanced() {

View File

@ -0,0 +1,44 @@
import canEditContentType from '../canEditContentType';
import rawData from './rawData';
describe('canEditContentType', () => {
it('should allow content type edition if one of attributes is a oneWay or manyWay relation', () => {
const { postContentType } = rawData;
expect(
canEditContentType(postContentType, {
kind: 'singleType',
})
).toBeTruthy();
});
it('should not allow content type edition if one of attributes is not oneWay or manyWay relation', () => {
const { articleContentType } = rawData;
expect(
canEditContentType(articleContentType, {
kind: 'singleType',
})
).toBeFalsy();
});
it('should always allow content type edition if content type is a single type', () => {
const { homeSingleType } = rawData;
expect(
canEditContentType(homeSingleType, {
kind: 'collectionType',
})
).toBeTruthy();
});
it('should always allow content type edition if the kind is not modified', () => {
const { articleContentType } = rawData;
expect(
canEditContentType(articleContentType, {
kind: 'collectionType',
})
).toBeTruthy();
});
});

View File

@ -0,0 +1,43 @@
const data = {
homeSingleType: {
contentType: {
uid: 'plugins::myplugins.home',
schema: {
name: 'plugins::myplugins.home',
kind: 'singleType',
attributes: {
category: { nature: 'oneWay' },
address: { type: 'string' },
},
},
},
},
articleContentType: {
contentType: {
uid: 'plugins::myplugins.article',
schema: {
name: 'plugins::myplugins.article',
kind: 'collectionType',
attributes: {
user: { nature: 'manyToOne' },
title: { type: 'string' },
},
},
},
},
postContentType: {
contentType: {
uid: 'plugins::myplugins.post',
schema: {
name: 'plugins::myplugins.post',
kind: 'collectionType',
attributes: {
user: { nature: 'manyWay' },
title: { type: 'string' },
},
},
},
},
};
export default data;

View File

@ -181,6 +181,7 @@ const ListView = () => {
return new Promise(resolve => setTimeout(resolve, 100)); return new Promise(resolve => setTimeout(resolve, 100));
}; };
const label = get(modifiedData, [firstMainDataPath, 'schema', 'name'], ''); const label = get(modifiedData, [firstMainDataPath, 'schema', 'name'], '');
const kind = get(modifiedData, [firstMainDataPath, 'schema', 'kind'], '');
const headerProps = { const headerProps = {
actions: isInDevelopmentMode actions: isInDevelopmentMode
@ -217,9 +218,14 @@ const ListView = () => {
onClick: async () => { onClick: async () => {
await wait(); await wait();
if (firstMainDataPath === 'contentType') { const contentType = kind || firstMainDataPath;
if (contentType === 'collectionType') {
emitEvent('willEditNameOfContentType'); emitEvent('willEditNameOfContentType');
} }
if (contentType === 'singleType') {
emitEvent('willEditNameOfSingleType');
}
push({ push({
search: makeSearch({ search: makeSearch({
@ -230,7 +236,10 @@ const ListView = () => {
targetUid, targetUid,
header_label_1: label, header_label_1: label,
header_icon_isCustom_1: false, header_icon_isCustom_1: false,
header_icon_name_1: firstMainDataPath, header_icon_name_1:
contentType === 'singleType'
? contentType
: firstMainDataPath,
headerId: getTrad('modalForm.header-edit'), headerId: getTrad('modalForm.header-edit'),
}), }),
}); });

View File

@ -33,8 +33,8 @@
"button.attributes.add.another": "Add another field", "button.attributes.add.another": "Add another field",
"button.component.add": "Add a component", "button.component.add": "Add a component",
"button.component.create": "Create new component", "button.component.create": "Create new component",
"button.model.create": "Create new collection-type", "button.model.create": "Create new collection type",
"button.single-types.create": "Create new single-type", "button.single-types.create": "Create new single type",
"component.repeatable": "(repeatable)", "component.repeatable": "(repeatable)",
"components.componentSelect.no-component-available.with-search": "There is no component matching your search", "components.componentSelect.no-component-available.with-search": "There is no component matching your search",
"components.componentSelect.no-component-available": "You have already added all your components", "components.componentSelect.no-component-available": "You have already added all your components",
@ -95,7 +95,8 @@
"form.button.add-field": "Add another field", "form.button.add-field": "Add another field",
"form.button.add-first-field-to-created-component": "Add first field to the component", "form.button.add-first-field-to-created-component": "Add first field to the component",
"form.button.add.field.to.component": "Add another field to this component", "form.button.add.field.to.component": "Add another field to this component",
"form.button.add.field.to.contentType": "Add another field to this collection-type", "form.button.add.field.to.collectionType": "Add another field to this collection type",
"form.button.add.field.to.singleType": "Add another field to this single type",
"form.button.cancel": "Cancel", "form.button.cancel": "Cancel",
"form.button.configure-component": "Configure the component", "form.button.configure-component": "Configure the component",
"form.button.configure-view": "Configure the view", "form.button.configure-view": "Configure the view",
@ -104,6 +105,8 @@
"form.button.finish": "Finish", "form.button.finish": "Finish",
"form.button.save": "Save", "form.button.save": "Save",
"form.button.select-component": "Select a component", "form.button.select-component": "Select a component",
"form.button.collection-type.description": "Best for multiple instances like articles, products, comments, etc.",
"form.button.single-type.description": "Best for single instance like about us, homepage, etc.",
"from": "from", "from": "from",
"injected-components.content-manager.edit-settings-view.link.components": "Edit the component", "injected-components.content-manager.edit-settings-view.link.components": "Edit the component",
"injected-components.content-manager.edit-settings-view.link.content-types": "Edit the collection type", "injected-components.content-manager.edit-settings-view.link.content-types": "Edit the collection type",

View File

@ -34,16 +34,19 @@
"form.attribute.item.uniqueField.description": "Vous ne pourrez pas créer une entrée s'il existe un champ similaire", "form.attribute.item.uniqueField.description": "Vous ne pourrez pas créer une entrée s'il existe un champ similaire",
"form.attribute.settings.default": "Valeur par défault", "form.attribute.settings.default": "Valeur par défault",
"form.button.add.field.to.component": "Ajouter un nouveau champ à ce composant", "form.button.add.field.to.component": "Ajouter un nouveau champ à ce composant",
"form.button.add.field.to.contentType": "Ajouter un nouveau champ à cette Collection", "form.button.add.field.to.collectionType": "Ajouter un nouveau champ à cette collection",
"form.button.add.field.to.singleType": "Ajouter un nouveau champ à ce single type",
"form.button.cancel": "Annuler", "form.button.cancel": "Annuler",
"form.button.configure-view": "Configurer la vue", "form.button.configure-view": "Configurer la vue",
"form.button.continue": "Continuer", "form.button.continue": "Continuer",
"form.button.save": "Sauvegarder", "form.button.save": "Sauvegarder",
"form.button.finish": "Terminer",
"form.button.delete": "Supprimer",
"from": "de", "from": "de",
"menu.section.components.name.plural": "Composants", "menu.section.components.name.plural": "Composants",
"menu.section.components.name.singular": "Composant", "menu.section.components.name.singular": "Composant",
"menu.section.models.name.plural": "Modèles", "menu.section.models.name.plural": "Collections",
"menu.section.models.name.singular": "Modèle", "menu.section.models.name.singular": "Collection",
"menu.section.single-types.name.plural": "Single Types", "menu.section.single-types.name.plural": "Single Types",
"menu.section.single-types.name.singular": "Single Type", "menu.section.single-types.name.singular": "Single Type",
"modalForm.singleType.header-create": "Créer un single type", "modalForm.singleType.header-create": "Créer un single type",

View File

@ -1,7 +1,7 @@
{ {
"Analytics": "Analytics", "Analytics": "Analytics",
"Content Manager": "Content Manager", "Content Manager": "Content Manager",
"Content Type Builder": "Types Builder", "Content Type Builder": " Content Types Builder",
"Email": "Email", "Email": "Email",
"Files Upload": "Files Upload", "Files Upload": "Files Upload",
"HomePage.notification.newsLetter.success": "Successfully subscribed to the newsletter", "HomePage.notification.newsLetter.success": "Successfully subscribed to the newsletter",