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",
"collectionName": "homepage",
"info": {
@ -7,12 +6,16 @@
},
"options": {
"increments": true,
"timestamps": ["created_at", "updated_at"]
"timestamps": [
"created_at",
"updated_at"
]
},
"attributes": {
"title": {
"type": "string",
"required": true
}
}
},
"kind": "singleType"
}

View File

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

View File

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

View File

@ -14,35 +14,32 @@ const BooleanBox = ({ label, name, onChange, options, value }) => {
<Div>
<Label htmlFor={name}>{label}</Label>
<Wrapper>
{options.map(option => {
return (
<Enumeration
key={option.value}
id={option.value.toString()}
className="option-input"
checked={option.value === value}
name={name}
onChange={onChange}
type="radio"
value={option.value}
/>
);
})}
{options.map(option => {
return (
<EnumerationWrapper
className="option"
key={option.value}
htmlFor={option.value.toString()}
>
<span className="option__indicator" />
<span className="option__title">
{formatMessage({ id: option.headerId })}
</span>
<p>{formatMessage({ id: option.descriptionId })}</p>
</EnumerationWrapper>
);
})}
{options.map(option => (
<Enumeration
{...option}
key={option.value}
id={option.value.toString()}
className="option-input"
checked={option.value === value}
name={name}
onChange={onChange}
type="radio"
value={option.value}
/>
))}
{options.map(option => (
<EnumerationWrapper
className="option"
key={option.value}
htmlFor={option.value.toString()}
>
<span className="option__indicator" />
<span className="option__title">
{formatMessage({ id: option.headerId })}
</span>
<p>{formatMessage({ id: option.descriptionId })}</p>
</EnumerationWrapper>
))}
</Wrapper>
</Div>
);

View File

@ -159,7 +159,11 @@ function List({
label: isInDevelopmentMode
? formatMessage({
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`,
})
: null,

View File

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

View File

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

View File

@ -40,6 +40,7 @@ import { NAVLINKS, INITIAL_STATE_DATA } from './utils/staticData';
import init from './init';
import reducer, { initialState } from './reducer';
import CustomButton from './CustomButton';
import canEditContentType from './utils/canEditContentType';
/* eslint-disable indent */
/* eslint-disable react/no-array-index-key */
@ -63,6 +64,7 @@ const FormModal = () => {
deleteCategory,
deleteData,
editCategory,
submitData,
modifiedData: allDataSchema,
nestedComponents,
setModifiedData,
@ -125,6 +127,7 @@ const FormModal = () => {
actionType,
attributeName,
attributeType,
contentTypeKind,
dynamicZoneTarget,
forTarget,
modalType,
@ -158,7 +161,6 @@ const FormModal = () => {
header_info_name_5,
header_info_category_5,
headerId,
contentTypeKind,
});
// Reset all the modification when opening the edit category modal
@ -217,7 +219,7 @@ const FormModal = () => {
state.modalType !== 'contentType' &&
actionType === 'edit'
) {
const { name, collectionName } = get(
const { name, collectionName, kind } = get(
allDataSchema,
[...pathToSchema, 'schema'],
{ name: null, collectionName: null }
@ -228,6 +230,7 @@ const FormModal = () => {
data: {
name,
collectionName,
kind,
},
});
}
@ -601,9 +604,14 @@ const FormModal = () => {
uid
);
} else {
updateSchema(modifiedData, state.modalType);
// Close the modal
push({ search: '' });
if (canEditContentType(allDataSchema, modifiedData)) {
push({ search: '' });
submitData(modifiedData);
} else {
strapi.notification.error(
'notification.contentType.relations.conflict'
);
}
return;
}
@ -1346,13 +1354,13 @@ const FormModal = () => {
</ModalForm>
{!isPickingAttribute && (
<ModalFooter>
<section>
<section style={{ alignItems: 'center' }}>
<Button type="button" color="cancel" onClick={handleToggle}>
{formatMessage({
id: 'components.popUpWarning.button.cancel',
})}
</Button>
<div style={{ margin: 'auto 0' }}>
<div>
{isCreatingAttribute && !isInFirstComponentStep && (
<Button
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'))
.required(errorsTrads.required),
collectionName: yup.string(),
kind: yup.string().oneOf(['singleType', 'collectionType']),
});
},
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 };
},
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));
};
const label = get(modifiedData, [firstMainDataPath, 'schema', 'name'], '');
const kind = get(modifiedData, [firstMainDataPath, 'schema', 'kind'], '');
const headerProps = {
actions: isInDevelopmentMode
@ -217,9 +218,14 @@ const ListView = () => {
onClick: async () => {
await wait();
if (firstMainDataPath === 'contentType') {
const contentType = kind || firstMainDataPath;
if (contentType === 'collectionType') {
emitEvent('willEditNameOfContentType');
}
if (contentType === 'singleType') {
emitEvent('willEditNameOfSingleType');
}
push({
search: makeSearch({
@ -230,7 +236,10 @@ const ListView = () => {
targetUid,
header_label_1: label,
header_icon_isCustom_1: false,
header_icon_name_1: firstMainDataPath,
header_icon_name_1:
contentType === 'singleType'
? contentType
: firstMainDataPath,
headerId: getTrad('modalForm.header-edit'),
}),
});

View File

@ -33,8 +33,8 @@
"button.attributes.add.another": "Add another field",
"button.component.add": "Add a component",
"button.component.create": "Create new component",
"button.model.create": "Create new collection-type",
"button.single-types.create": "Create new single-type",
"button.model.create": "Create new collection type",
"button.single-types.create": "Create new single type",
"component.repeatable": "(repeatable)",
"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",
@ -95,7 +95,8 @@
"form.button.add-field": "Add another field",
"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.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.configure-component": "Configure the component",
"form.button.configure-view": "Configure the view",
@ -104,6 +105,8 @@
"form.button.finish": "Finish",
"form.button.save": "Save",
"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",
"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",

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.settings.default": "Valeur par défault",
"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.configure-view": "Configurer la vue",
"form.button.continue": "Continuer",
"form.button.save": "Sauvegarder",
"form.button.finish": "Terminer",
"form.button.delete": "Supprimer",
"from": "de",
"menu.section.components.name.plural": "Composants",
"menu.section.components.name.singular": "Composant",
"menu.section.models.name.plural": "Modèles",
"menu.section.models.name.singular": "Modèle",
"menu.section.models.name.plural": "Collections",
"menu.section.models.name.singular": "Collection",
"menu.section.single-types.name.plural": "Single Types",
"menu.section.single-types.name.singular": "Single Type",
"modalForm.singleType.header-create": "Créer un single type",

View File

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