Merge branch 'custom-fields/add-custom-field-attribute' of github.com:strapi/strapi into add-custom-field-attribute/error-handling

This commit is contained in:
Mark Kaylor 2022-08-10 11:42:29 +02:00
commit b3384fb6e8
19 changed files with 260 additions and 110 deletions

View File

@ -49,10 +49,21 @@ export default {
id: 'color-picker.color.format.label',
defaultMessage: 'Color format',
},
name: 'options.color-picker.format',
name: 'options.format',
type: 'select',
value: 'hex',
options: [
{
key: '__null_reset_value__',
value: '',
metadatas: {
intlLabel: {
id: 'color-picker.color.format.placeholder',
defaultMessage: 'Select a format',
},
hidden: true,
},
},
{
key: 'hex',
value: 'hex',
@ -111,11 +122,9 @@ export default {
},
],
validator: args => ({
'color-picker': yup.object().shape({
format: yup.string().required({
id: 'options.color-picker.format.error',
defaultMessage: 'The color format is required',
}),
format: yup.string().required({
id: 'options.color-picker.format.error',
defaultMessage: 'The color format is required',
}),
}),
},

View File

@ -173,17 +173,19 @@ const DataManagerProvider = ({
});
};
const addCustomFieldAttribute = (
attributeToSet,
forTarget,
targetUid,
isEditing = false,
initialAttribute
) => {
const actionType = isEditing ? EDIT_CUSTOM_FIELD_ATTRIBUTE : ADD_CUSTOM_FIELD_ATTRIBUTE;
const addCustomFieldAttribute = ({ attributeToSet, forTarget, targetUid, initialAttribute }) => {
dispatch({
type: actionType,
type: ADD_CUSTOM_FIELD_ATTRIBUTE,
attributeToSet,
forTarget,
targetUid,
initialAttribute,
});
};
const editCustomFieldAttribute = ({ attributeToSet, forTarget, targetUid, initialAttribute }) => {
dispatch({
type: EDIT_CUSTOM_FIELD_ATTRIBUTE,
attributeToSet,
forTarget,
targetUid,
@ -573,6 +575,7 @@ const DataManagerProvider = ({
deleteCategory,
deleteData,
editCategory,
editCustomFieldAttribute,
isInDevelopmentMode,
initialData,
isInContentTypeView,

View File

@ -36,65 +36,58 @@ const findAttributeIndex = (schema, attributeToFind) => {
return schema.schema.attributes.findIndex(({ name }) => name === attributeToFind);
};
const getAddAttributeUpdate = (action, state) => {
const {
attributeToSet: { name, ...rest },
forTarget,
targetUid,
} = action;
delete rest.createComponent;
const pathToDataToEdit = ['component', 'contentType'].includes(forTarget)
? [forTarget]
: [forTarget, targetUid];
const currentAttributes = get(
state,
['modifiedData', ...pathToDataToEdit, 'schema', 'attributes'],
[]
).slice();
// Add the createdAttribute
const updatedAttributes = [...currentAttributes, { ...rest, name }];
return { pathToDataToEdit, updatedAttributes, attributeToSet: { ...rest, name } };
};
const getEditAttributeUpdate = (action, state) => {
const { forTarget, targetUid, initialAttribute } = action;
const initialAttributeName = initialAttribute.name;
const pathToDataToEdit = ['component', 'contentType'].includes(forTarget)
? [forTarget]
: [forTarget, targetUid];
const initialAttributeIndex = findAttributeIndex(
get(state, ['modifiedData', ...pathToDataToEdit]),
initialAttributeName
);
return { pathToDataToEdit, initialAttributeIndex };
};
const reducer = (state = initialState, action) =>
// eslint-disable-next-line consistent-return
produce(state, draftState => {
switch (action.type) {
case actions.ADD_CUSTOM_FIELD_ATTRIBUTE: {
const { pathToDataToEdit, updatedAttributes } = getAddAttributeUpdate(action, state);
const {
attributeToSet: { name, ...rest },
forTarget,
targetUid,
} = action;
const pathToDataToEdit = ['component', 'contentType'].includes(forTarget)
? [forTarget]
: [forTarget, targetUid];
const currentAttributes = get(
state,
['modifiedData', ...pathToDataToEdit, 'schema', 'attributes'],
[]
).slice();
// Add the createdAttribute
const updatedAttributes = [...currentAttributes, { ...rest, name }];
set(
draftState,
['modifiedData', ...pathToDataToEdit, 'schema', 'attributes'],
updatedAttributes
);
break;
}
case actions.ADD_ATTRIBUTE: {
const {
pathToDataToEdit,
updatedAttributes,
attributeToSet: { name, ...rest },
} = getAddAttributeUpdate(action, state);
forTarget,
targetUid,
} = action;
delete rest.createComponent;
const pathToDataToEdit = ['component', 'contentType'].includes(forTarget)
? [forTarget]
: [forTarget, targetUid];
const currentAttributes = get(
state,
['modifiedData', ...pathToDataToEdit, 'schema', 'attributes'],
[]
).slice();
// Add the createdAttribute
const updatedAttributes = [...currentAttributes, { ...rest, name }];
set(
draftState,
@ -270,9 +263,17 @@ const reducer = (state = initialState, action) =>
break;
}
case actions.EDIT_CUSTOM_FIELD_ATTRIBUTE: {
const { attributeToSet } = action;
const { forTarget, targetUid, initialAttribute, attributeToSet } = action;
const { pathToDataToEdit, initialAttributeIndex } = getEditAttributeUpdate(action, state);
const initialAttributeName = initialAttribute.name;
const pathToDataToEdit = ['component', 'contentType'].includes(forTarget)
? [forTarget]
: [forTarget, targetUid];
const initialAttributeIndex = findAttributeIndex(
get(state, ['modifiedData', ...pathToDataToEdit]),
initialAttributeName
);
set(
draftState,
@ -285,10 +286,20 @@ const reducer = (state = initialState, action) =>
case actions.EDIT_ATTRIBUTE: {
const {
attributeToSet: { name, ...rest },
forTarget,
targetUid,
initialAttribute,
} = action;
const { pathToDataToEdit, initialAttributeIndex } = getEditAttributeUpdate(action, state);
const initialAttributeName = initialAttribute.name;
const pathToDataToEdit = ['component', 'contentType'].includes(forTarget)
? [forTarget]
: [forTarget, targetUid];
const initialAttributeIndex = findAttributeIndex(
get(state, ['modifiedData', ...pathToDataToEdit]),
initialAttributeName
);
const isEditingRelation = rest.type === 'relation';

View File

@ -7,16 +7,9 @@ import { createComponentSchema, componentForm } from '../component';
import { dynamiczoneForm } from '../dynamicZone';
import { nameField } from '../attributes/nameField';
import addItemsToFormSection from './utils/addItemsToFormSection';
import getUsedAttributeNames from './utils/getUsedAttributeNames';
import getTrad from '../../../utils/getTrad';
const getUsedAttributeNames = (attributes, schemaData) => {
return attributes
.filter(({ name }) => {
return name !== schemaData.initialData.name;
})
.map(({ name }) => name);
};
const forms = {
customField: {
schema({
@ -68,7 +61,6 @@ const forms = {
}
if (injectedInputs) {
// TODO: Discuss how to handle settings from other plugins
const extendedSettings = {
sectionTitle: {
id: getTrad('modalForm.custom-fields.advanced.settings.extended'),

View File

@ -15,7 +15,7 @@ const addItemsToFormSection = (formTypeOptions, sections) => {
return;
}
// Otherwise, when no sectionTitle is present or sectionTitle has a value (including null),
// Otherwise, when sectionTitle has a value (including null),
// add the item as a new section
sections.push(item);
});

View File

@ -0,0 +1,15 @@
/**
*
* @param {array} attributes The attributes found on the dataManager's modifiedData object
* @param {object} schemaData The modifiedData and SchemaData objects from the reducer state
* @returns A list of names already being used
*/
const getUsedAttributeNames = (attributes, schemaData) => {
return attributes
.filter(({ name }) => {
return name !== schemaData.initialData.name;
})
.map(({ name }) => name);
};
export default getUsedAttributeNames;

View File

@ -116,6 +116,7 @@ const FormModal = () => {
deleteCategory,
deleteData,
editCategory,
editCustomFieldAttribute,
submitData,
modifiedData: allDataSchema,
nestedComponents,
@ -347,7 +348,6 @@ const FormModal = () => {
ctbFormsAPI,
customFieldValidator: customField.options.validator,
});
// Check for validity for creating a component
// This is happening when the user creates a component "on the fly"
// Since we temporarily store the component info in another object
@ -550,13 +550,18 @@ const FormModal = () => {
// Add/edit a field to a content type
// Add/edit a field to a created component (the end modal is not step 2)
} else if (isCreatingCustomFieldAttribute) {
addCustomFieldAttribute(
{ ...modifiedData, customField: customFieldUid },
const customFieldAttributeUpdate = {
attributeToSet: { ...modifiedData, customField: customFieldUid },
forTarget,
targetUid,
actionType === 'edit',
initialData
);
initialAttribute: initialData,
};
if (actionType === 'edit') {
editCustomFieldAttribute(customFieldAttributeUpdate);
} else {
addCustomFieldAttribute(customFieldAttributeUpdate);
}
if (shouldContinue) {
onNavigateToChooseAttributeModal({
@ -1050,10 +1055,10 @@ const FormModal = () => {
onSubmitCreateContentType={handleSubmit}
onSubmitCreateDz={handleSubmit}
onSubmitEditAttribute={handleSubmit}
onSubmitEditCusomFieldAttribute={handleSubmit}
onSubmitEditCategory={handleSubmit}
onSubmitEditComponent={handleSubmit}
onSubmitEditContentType={handleSubmit}
onSubmitEditCustomFieldAttribute={handleSubmit}
onSubmitEditDz={handleSubmit}
/>
}

View File

@ -0,0 +1,91 @@
import addItemsToFormSections from '../forms/utils/addItemsToFormSection';
describe('addItemsToFormSection', () => {
it('adds items to the default section', () => {
const sections = [{ sectionTitle: null, items: [] }];
const itemsToAdd = [
{
intlLabel: {
id: 'color-picker.color.format.label',
defaultMessage: 'Color format',
},
name: 'options.color-picker.format',
type: 'select',
value: 'hex',
options: [
{
key: 'hex',
value: 'hex',
metadatas: {
intlLabel: {
id: 'color-picker.color.format.hex',
defaultMessage: 'Hexadecimal',
},
},
},
{
key: 'rgba',
value: 'rgba',
metadatas: {
intlLabel: {
id: 'color-picker.color.format.rgba',
defaultMessage: 'RGBA',
},
},
},
],
},
];
addItemsToFormSections(itemsToAdd, sections);
expect(sections.length).toBe(1);
expect(sections[0].items.length).toBe(1);
});
it('adds the item as a new section', () => {
const sections = [{ sectionTitle: null, items: [] }];
const itemsToAdd = [
{
sectionTitle: null,
items: [
{
intlLabel: {
id: 'color-picker.color.format.label',
defaultMessage: 'Color format',
},
name: 'options.color-picker.format',
type: 'select',
value: 'hex',
options: [
{
key: 'hex',
value: 'hex',
metadatas: {
intlLabel: {
id: 'color-picker.color.format.hex',
defaultMessage: 'Hexadecimal',
},
},
},
{
key: 'rgba',
value: 'rgba',
metadatas: {
intlLabel: {
id: 'color-picker.color.format.rgba',
defaultMessage: 'RGBA',
},
},
},
],
},
],
},
];
addItemsToFormSections(itemsToAdd, sections);
expect(sections.length).toBe(2);
});
});

View File

@ -38,10 +38,10 @@ const FormModalEndActions = ({
onSubmitCreateComponent,
onSubmitCreateDz,
onSubmitEditAttribute,
onSubmitEditCusomFieldAttribute,
onSubmitEditCategory,
onSubmitEditComponent,
onSubmitEditContentType,
onSubmitEditCustomFieldAttribute,
onSubmitEditDz,
}) => {
const { formatMessage } = useIntl();
@ -394,7 +394,7 @@ const FormModalEndActions = ({
onClick={e => {
e.preventDefault();
onSubmitEditCusomFieldAttribute(e, true);
onSubmitEditCustomFieldAttribute(e, true);
}}
startIcon={<Plus />}
>
@ -409,7 +409,7 @@ const FormModalEndActions = ({
onClick={e => {
e.preventDefault();
onSubmitEditCusomFieldAttribute(e, false);
onSubmitEditCustomFieldAttribute(e, false);
}}
>
{formatMessage({
@ -455,10 +455,10 @@ FormModalEndActions.propTypes = {
onSubmitCreateComponent: PropTypes.func.isRequired,
onSubmitCreateDz: PropTypes.func.isRequired,
onSubmitEditAttribute: PropTypes.func.isRequired,
onSubmitEditCusomFieldAttribute: PropTypes.func.isRequired,
onSubmitEditCategory: PropTypes.func.isRequired,
onSubmitEditComponent: PropTypes.func.isRequired,
onSubmitEditContentType: PropTypes.func.isRequired,
onSubmitEditCustomFieldAttribute: PropTypes.func.isRequired,
onSubmitEditDz: PropTypes.func.isRequired,
};

View File

@ -14,7 +14,6 @@ const FormModalNavigationProvider = ({ children }) => {
return {
...prevState,
actionType: 'create',
// TODO: Create a new modalType on EXPANSION-245
modalType: 'customField',
attributeType,
customFieldUid,

View File

@ -17,14 +17,6 @@ const FormModalSubHeader = ({
customField,
}) => {
const { formatMessage } = useIntl();
const type =
modalType === 'customField'
? upperFirst(formatMessage(customField.intlLabel))
: upperFirst(
formatMessage({
id: getTrad(`attribute.${attributeType}`),
})
);
return (
<Typography as="h2" variant="beta">
@ -40,7 +32,9 @@ const FormModalSubHeader = ({
defaultMessage: 'Add new field',
},
{
type,
type: upperFirst(
formatMessage(customField?.intlLabel ?? { id: getTrad(`attribute.${attributeType}`) })
),
name: upperFirst(attributeName),
step,
}

View File

@ -88,8 +88,12 @@ const formsAPI = {
return sectionsToAdd;
},
makeCustomFieldValidator(initShape, validator, ...validatorArgs) {
return initShape.shape({ options: yup.object().shape(validator(validatorArgs)) });
makeCustomFieldValidator(attributeShape, validator, ...validatorArgs) {
// When no validator, return the attribute shape
if (!validator) return attributeShape;
// Otherwise extend the shape with the provided validator
return attributeShape.shape({ options: yup.object().shape(validator(validatorArgs)) });
},
makeValidator(target, initShape, ...args) {

View File

@ -4,7 +4,6 @@ const _ = require('lodash');
const { formatAttributes, replaceTemporaryUIDs } = require('../utils/attributes');
const createBuilder = require('./schema-builder');
const convertCustomFieldType = require('./utils/convert-custom-field-type');
/**
* Formats a component attributes
@ -42,11 +41,9 @@ const createComponent = async ({ component, components = [] }) => {
const uidMap = builder.createNewComponentUIDMap(components);
const replaceTmpUIDs = replaceTemporaryUIDs(uidMap);
convertCustomFieldType(component.attributes);
const newComponent = builder.createComponent(replaceTmpUIDs(component));
components.forEach(component => {
convertCustomFieldType(component.attributes);
if (!_.has(component, 'uid')) {
return builder.createComponent(replaceTmpUIDs(component));
}
@ -70,14 +67,12 @@ const editComponent = async (uid, { component, components = [] }) => {
const uidMap = builder.createNewComponentUIDMap(components);
const replaceTmpUIDs = replaceTemporaryUIDs(uidMap);
convertCustomFieldType(component.attributes);
const updatedComponent = builder.editComponent({
uid,
...replaceTmpUIDs(component),
});
components.forEach(component => {
convertCustomFieldType(component.attributes);
if (!_.has(component, 'uid')) {
return builder.createComponent(replaceTmpUIDs(component));
}

View File

@ -8,7 +8,6 @@ const { ApplicationError } = require('@strapi/utils').errors;
const { formatAttributes, replaceTemporaryUIDs } = require('../utils/attributes');
const createBuilder = require('./schema-builder');
const { coreUids, pluginsUids } = require('./constants');
const convertCustomFieldType = require('./utils/convert-custom-field-type');
const isContentTypeVisible = model =>
getOr(true, 'pluginOptions.content-type-builder.visible', model) === true;
@ -85,7 +84,6 @@ const createContentType = async ({ contentType, components = [] }, options = {})
const replaceTmpUIDs = replaceTemporaryUIDs(uidMap);
convertCustomFieldType(contentType.attributes);
const newContentType = builder.createContentType(replaceTmpUIDs(contentType));
// allow components to target the new contentType
@ -101,7 +99,6 @@ const createContentType = async ({ contentType, components = [] }, options = {})
};
components.forEach(component => {
convertCustomFieldType(component.attributes);
const options = replaceTmpUIDs(targetContentType(component));
if (!_.has(component, 'uid')) {
@ -173,15 +170,12 @@ const editContentType = async (uid, { contentType, components = [] }) => {
const uidMap = builder.createNewComponentUIDMap(components);
const replaceTmpUIDs = replaceTemporaryUIDs(uidMap);
convertCustomFieldType(contentType.attributes);
const updatedContentType = builder.editContentType({
uid,
...replaceTmpUIDs(contentType),
});
components.forEach(component => {
convertCustomFieldType(component.attributes);
if (!_.has(component, 'uid')) {
return builder.createComponent(replaceTmpUIDs(component));
}

View File

@ -8,6 +8,7 @@ const { nameToSlug, nameToCollectionName } = require('@strapi/utils');
const { ApplicationError } = require('@strapi/utils').errors;
const { isConfigurable } = require('../../utils/attributes');
const createSchemaHandler = require('./schema-handler');
const convertCustomFieldType = require('./utils/convert-custom-field-type');
module.exports = function createComponentBuilder() {
return {
@ -32,12 +33,15 @@ module.exports = function createComponentBuilder() {
* create a component in the tmpComponent map
*/
createComponent(infos) {
const { attributes } = infos;
const uid = this.createComponentUID(infos);
if (this.components.has(uid)) {
throw new ApplicationError('component.alreadyExists');
}
convertCustomFieldType(attributes);
const handler = createSchemaHandler({
dir: path.join(strapi.dirs.components, nameToSlug(infos.category)),
filename: `${nameToSlug(infos.displayName)}.json`,
@ -72,12 +76,13 @@ module.exports = function createComponentBuilder() {
* create a component in the tmpComponent map
*/
editComponent(infos) {
const { uid } = infos;
const { uid, attributes } = infos;
if (!this.components.has(uid)) {
throw new ApplicationError('component.notFound');
}
convertCustomFieldType(attributes);
const component = this.components.get(uid);
const [, nameUID] = uid.split('.');

View File

@ -8,6 +8,7 @@ const { ApplicationError } = require('@strapi/utils').errors;
const { isRelation, isConfigurable } = require('../../utils/attributes');
const { typeKinds } = require('../constants');
const createSchemaHandler = require('./schema-handler');
const convertCustomFieldType = require('./utils/convert-custom-field-type');
const reuseUnsetPreviousProperties = (newAttribute, oldAttribute) => {
_.defaults(
@ -71,12 +72,15 @@ module.exports = function createComponentBuilder() {
* @returns {object} new content type
*/
createContentType(infos) {
const { attributes } = infos;
const uid = createContentTypeUID(infos);
if (this.contentTypes.has(uid)) {
throw new ApplicationError('contentType.alreadyExists');
}
convertCustomFieldType(attributes);
const contentType = createSchemaHandler({
modelName: infos.singularName,
dir: path.join(strapi.dirs.api, infos.singularName, 'content-types', infos.singularName),
@ -124,12 +128,14 @@ module.exports = function createComponentBuilder() {
},
editContentType(infos) {
const { uid } = infos;
const { uid, attributes } = infos;
if (!this.contentTypes.has(uid)) {
throw new ApplicationError('contentType.notFound');
}
convertCustomFieldType(attributes);
const contentType = this.contentTypes.get(uid);
const oldAttributes = contentType.schema.attributes;

View File

@ -26,6 +26,16 @@ describe('format attributes', () => {
},
},
},
components: {
'default.test': {
attributes: {
color: {
type: 'customField',
customField: 'plugin::mycustomfields.color',
},
},
},
},
};
convertCustomFieldType(global.strapi);
@ -42,6 +52,16 @@ describe('format attributes', () => {
},
},
},
components: {
'default.test': {
attributes: {
color: {
type: 'text',
customField: 'plugin::mycustomfields.color',
},
},
},
},
};
expect(global.strapi).toEqual(expected);

View File

@ -1,7 +1,14 @@
'use strict';
const convertCustomFieldType = strapi => {
const allSchemasAttributes = Object.values(strapi.contentTypes).map(schema => schema.attributes);
const allContentTypeSchemaAttributes = Object.values(strapi.contentTypes).map(
schema => schema.attributes
);
const allComponentSchemaAttributes = Object.values(strapi.components).map(
schema => schema.attributes
);
const allSchemasAttributes = [...allContentTypeSchemaAttributes, ...allComponentSchemaAttributes];
for (const schemaAttrbutes of allSchemasAttributes) {
for (const attribute of Object.values(schemaAttrbutes)) {
if (attribute.type === 'customField') {