Merge pull request #5286 from strapi/single-types/uid

add Uid attributes in CTB
This commit is contained in:
ELABBASSI Hicham 2020-02-25 09:05:24 +01:00 committed by GitHub
commit da7ebced8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 490 additions and 594 deletions

View File

@ -7,12 +7,7 @@ import {
useGlobalContext, useGlobalContext,
PopUpWarning, PopUpWarning,
} from 'strapi-helper-plugin'; } from 'strapi-helper-plugin';
import { import { useHistory, useLocation, useRouteMatch, Redirect } from 'react-router-dom';
useHistory,
useLocation,
useRouteMatch,
Redirect,
} from 'react-router-dom';
import DataManagerContext from '../../contexts/DataManagerContext'; import DataManagerContext from '../../contexts/DataManagerContext';
import getTrad from '../../utils/getTrad'; import getTrad from '../../utils/getTrad';
import makeUnique from '../../utils/makeUnique'; import makeUnique from '../../utils/makeUnique';
@ -55,21 +50,17 @@ const DataManagerProvider = ({ allIcons, children }) => {
} = reducerState.toJS(); } = reducerState.toJS();
const { pathname } = useLocation(); const { pathname } = useLocation();
const { push } = useHistory(); const { push } = useHistory();
const contentTypeMatch = useRouteMatch( const contentTypeMatch = useRouteMatch(`/plugins/${pluginId}/content-types/:uid`);
`/plugins/${pluginId}/content-types/:uid`
);
const componentMatch = useRouteMatch( const componentMatch = useRouteMatch(
`/plugins/${pluginId}/component-categories/:categoryUid/:componentUid` `/plugins/${pluginId}/component-categories/:categoryUid/:componentUid`
); );
const formatMessageRef = useRef(); const formatMessageRef = useRef();
formatMessageRef.current = formatMessage; formatMessageRef.current = formatMessage;
const isInDevelopmentMode = const isInDevelopmentMode = currentEnvironment === 'development' && autoReload;
currentEnvironment === 'development' && autoReload;
const isInContentTypeView = contentTypeMatch !== null; const isInContentTypeView = contentTypeMatch !== null;
const firstKeyToMainSchema = isInContentTypeView const firstKeyToMainSchema = isInContentTypeView ? 'contentType' : 'component';
? 'contentType'
: 'component';
const currentUid = isInContentTypeView const currentUid = isInContentTypeView
? get(contentTypeMatch, 'params.uid', null) ? get(contentTypeMatch, 'params.uid', null)
: get(componentMatch, 'params.componentUid', null); : get(componentMatch, 'params.componentUid', null);
@ -80,10 +71,7 @@ const DataManagerProvider = ({ allIcons, children }) => {
getDataRef.current = async () => { getDataRef.current = async () => {
try { try {
const [ const [{ data: componentsArray }, { data: contentTypesArray }] = await Promise.all(
{ data: componentsArray },
{ data: contentTypesArray },
] = await Promise.all(
['components', 'content-types'].map(endPoint => { ['components', 'content-types'].map(endPoint => {
return request(`/${pluginId}/${endPoint}`, { return request(`/${pluginId}/${endPoint}`, {
method: 'GET', method: 'GET',
@ -118,11 +106,11 @@ const DataManagerProvider = ({ allIcons, children }) => {
useEffect(() => { useEffect(() => {
// We need to set the modifiedData after the data has been retrieved // We need to set the modifiedData after the data has been retrieved
// and also on pathname change // and also on pathname change
if (!isLoading) { if (!isLoading && currentUid) {
setModifiedData(); setModifiedData();
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoading, pathname]); }, [isLoading, pathname, currentUid]);
useEffect(() => { useEffect(() => {
if (currentEnvironment === 'development' && !autoReload) { if (currentEnvironment === 'development' && !autoReload) {
@ -135,8 +123,7 @@ const DataManagerProvider = ({ allIcons, children }) => {
}, [autoReload, currentEnvironment]); }, [autoReload, currentEnvironment]);
const didModifiedComponents = const didModifiedComponents =
getCreatedAndModifiedComponents(modifiedData.components || {}, components) getCreatedAndModifiedComponents(modifiedData.components || {}, components).length > 0;
.length > 0;
const addAttribute = ( const addAttribute = (
attributeToSet, attributeToSet,
@ -158,10 +145,7 @@ const DataManagerProvider = ({ allIcons, children }) => {
}); });
}; };
const addCreatedComponentToDynamicZone = ( const addCreatedComponentToDynamicZone = (dynamicZoneTarget, componentsToAdd) => {
dynamicZoneTarget,
componentsToAdd
) => {
dispatch({ dispatch({
type: 'ADD_CREATED_COMPONENT_TO_DYNAMIC_ZONE', type: 'ADD_CREATED_COMPONENT_TO_DYNAMIC_ZONE',
dynamicZoneTarget, dynamicZoneTarget,
@ -181,10 +165,7 @@ const DataManagerProvider = ({ allIcons, children }) => {
componentCategory, componentCategory,
shouldAddComponentToData = false shouldAddComponentToData = false
) => { ) => {
const type = const type = schemaType === 'contentType' ? 'CREATE_SCHEMA' : 'CREATE_COMPONENT_SCHEMA';
schemaType === 'contentType'
? 'CREATE_SCHEMA'
: 'CREATE_COMPONENT_SCHEMA';
dispatch({ dispatch({
type, type,
@ -204,15 +185,9 @@ const DataManagerProvider = ({ allIcons, children }) => {
}); });
}; };
const removeAttribute = ( const removeAttribute = (mainDataKey, attributeToRemoveName, componentUid = '') => {
mainDataKey,
attributeToRemoveName,
componentUid = ''
) => {
const type = const type =
mainDataKey === 'components' mainDataKey === 'components' ? 'REMOVE_FIELD_FROM_DISPLAYED_COMPONENT' : 'REMOVE_FIELD';
? 'REMOVE_FIELD_FROM_DISPLAYED_COMPONENT'
: 'REMOVE_FIELD';
if (mainDataKey === 'contentType') { if (mainDataKey === 'contentType') {
emitEvent('willDeleteFieldOfContentType'); emitEvent('willDeleteFieldOfContentType');
@ -253,17 +228,11 @@ const DataManagerProvider = ({ allIcons, children }) => {
const deleteData = async () => { const deleteData = async () => {
try { try {
const requestURL = `/${pluginId}/${endPoint}/${currentUid}`; const requestURL = `/${pluginId}/${endPoint}/${currentUid}`;
const isTemporary = get( const isTemporary = get(modifiedData, [firstKeyToMainSchema, 'isTemporary'], false);
modifiedData,
[firstKeyToMainSchema, 'isTemporary'],
false
);
const userConfirm = window.confirm( const userConfirm = window.confirm(
formatMessage({ formatMessage({
id: getTrad( id: getTrad(
`popUpWarning.bodyMessage.${ `popUpWarning.bodyMessage.${isInContentTypeView ? 'contentType' : 'component'}.delete`
isInContentTypeView ? 'contentType' : 'component'
}.delete`
), ),
}) })
); );
@ -338,9 +307,7 @@ const DataManagerProvider = ({ allIcons, children }) => {
const getAllNestedComponents = () => { const getAllNestedComponents = () => {
const appNestedCompo = retrieveNestedComponents(components); const appNestedCompo = retrieveNestedComponents(components);
const editingDataNestedCompos = retrieveNestedComponents( const editingDataNestedCompos = retrieveNestedComponents(modifiedData.components || {});
modifiedData.components || {}
);
return makeUnique([...editingDataNestedCompos, ...appNestedCompo]); return makeUnique([...editingDataNestedCompos, ...appNestedCompo]);
}; };
@ -370,10 +337,7 @@ const DataManagerProvider = ({ allIcons, children }) => {
isInContentTypeView isInContentTypeView
); );
const dataShape = orderAllDataAttributesWithImmutable( const dataShape = orderAllDataAttributesWithImmutable(newSchemaToSet, isInContentTypeView);
newSchemaToSet,
isInContentTypeView
);
// This prevents from losing the created content type or component when clicking on the link from the left menu // This prevents from losing the created content type or component when clicking on the link from the left menu
const hasJustCreatedSchema = const hasJustCreatedSchema =
@ -401,11 +365,7 @@ const DataManagerProvider = ({ allIcons, children }) => {
const submitData = async additionalContentTypeData => { const submitData = async additionalContentTypeData => {
try { try {
const isCreating = get( const isCreating = get(modifiedData, [firstKeyToMainSchema, 'isTemporary'], false);
modifiedData,
[firstKeyToMainSchema, 'isTemporary'],
false
);
const body = { const body = {
components: getComponentsToPost( components: getComponentsToPost(
modifiedData.components, modifiedData.components,
@ -505,14 +465,11 @@ const DataManagerProvider = ({ allIcons, children }) => {
value={{ value={{
addAttribute, addAttribute,
addCreatedComponentToDynamicZone, addCreatedComponentToDynamicZone,
allComponentsCategories: retrieveSpecificInfoFromComponents( allComponentsCategories: retrieveSpecificInfoFromComponents(components, ['category']),
components, allComponentsIconAlreadyTaken: retrieveSpecificInfoFromComponents(components, [
['category'] 'schema',
), 'icon',
allComponentsIconAlreadyTaken: retrieveSpecificInfoFromComponents( ]),
components,
['schema', 'icon']
),
allIcons, allIcons,
changeDynamicZoneComponents, changeDynamicZoneComponents,
components, components,

View File

@ -34,8 +34,7 @@ const addComponentsToState = (state, componentToAddUid, objToUpdate) => {
const isTemporaryComponent = componentToAdd.get('isTemporary'); const isTemporaryComponent = componentToAdd.get('isTemporary');
const componentToAddSchema = componentToAdd.getIn(['schema', 'attributes']); const componentToAddSchema = componentToAdd.getIn(['schema', 'attributes']);
const hasComponentAlreadyBeenAdded = const hasComponentAlreadyBeenAdded =
state.getIn(['modifiedData', 'components', componentToAddUid]) !== state.getIn(['modifiedData', 'components', componentToAddUid]) !== undefined;
undefined;
// created components are already in the modifiedData.components // created components are already in the modifiedData.components
// We don't add them because all modifications will be lost // We don't add them because all modifications will be lost
@ -52,17 +51,13 @@ const addComponentsToState = (state, componentToAddUid, objToUpdate) => {
// We need to add the nested components to the modifiedData.components as well // We need to add the nested components to the modifiedData.components as well
nestedComponents.forEach(componentUid => { nestedComponents.forEach(componentUid => {
const isTemporary = const isTemporary = state.getIn(['components', componentUid, 'isTemporary']) || false;
state.getIn(['components', componentUid, 'isTemporary']) || false;
const hasNestedComponentAlreadyBeenAdded = const hasNestedComponentAlreadyBeenAdded =
state.getIn(['modifiedData', 'components', componentUid]) !== undefined; state.getIn(['modifiedData', 'components', componentUid]) !== undefined;
// Same logic here otherwise we will lose the modifications added to the components // Same logic here otherwise we will lose the modifications added to the components
if (!isTemporary && !hasNestedComponentAlreadyBeenAdded) { if (!isTemporary && !hasNestedComponentAlreadyBeenAdded) {
newObj = newObj.set( newObj = newObj.set(componentUid, state.getIn(['components', componentUid]));
componentUid,
state.getIn(['components', componentUid])
);
} }
}); });
@ -84,52 +79,42 @@ const reducer = (state, action) => {
: [forTarget, targetUid]; : [forTarget, targetUid];
return state return state
.updateIn( .updateIn(['modifiedData', ...pathToDataToEdit, 'schema', 'attributes', name], () => {
['modifiedData', ...pathToDataToEdit, 'schema', 'attributes', name], return fromJS(rest);
() => { })
return fromJS(rest); .updateIn(['modifiedData', ...pathToDataToEdit, 'schema', 'attributes'], obj => {
const type = get(rest, 'type', 'relation');
const target = get(rest, 'target', null);
const nature = get(rest, 'nature', null);
const currentUid = state.getIn(['modifiedData', ...pathToDataToEdit, 'uid']);
// When the user in creating a relation with the same content type we need to create another attribute
// that is the opposite of the created one
if (
type === 'relation' &&
nature !== 'oneWay' &&
nature !== 'manyWay' &&
target === currentUid
) {
const oppositeAttribute = {
nature: getOppositeNature(nature),
target,
unique: rest.unique,
// Leave this if we allow the required on the relation
// required: rest.required,
dominant: nature === 'manyToMany' ? !rest.dominant : null,
targetAttribute: name,
columnName: rest.targetColumnName,
targetColumnName: rest.columnName,
};
return obj.update(rest.targetAttribute, () => {
return fromJS(oppositeAttribute);
});
} }
)
.updateIn(
['modifiedData', ...pathToDataToEdit, 'schema', 'attributes'],
obj => {
const type = get(rest, 'type', 'relation');
const target = get(rest, 'target', null);
const nature = get(rest, 'nature', null);
const currentUid = state.getIn([
'modifiedData',
...pathToDataToEdit,
'uid',
]);
// When the user in creating a relation with the same content type we need to create another attribute return obj;
// that is the opposite of the created one })
if (
type === 'relation' &&
nature !== 'oneWay' &&
nature !== 'manyWay' &&
target === currentUid
) {
const oppositeAttribute = {
nature: getOppositeNature(nature),
target,
unique: rest.unique,
// Leave this if we allow the required on the relation
// required: rest.required,
dominant: nature === 'manyToMany' ? !rest.dominant : null,
targetAttribute: name,
columnName: rest.targetColumnName,
targetColumnName: rest.columnName,
};
return obj.update(rest.targetAttribute, () => {
return fromJS(oppositeAttribute);
});
}
return obj;
}
)
.updateIn(['modifiedData', 'components'], existingCompos => { .updateIn(['modifiedData', 'components'], existingCompos => {
if (action.shouldAddComponentToData) { if (action.shouldAddComponentToData) {
return addComponentsToState(state, rest.component, existingCompos); return addComponentsToState(state, rest.component, existingCompos);
@ -142,14 +127,7 @@ const reducer = (state, action) => {
const { dynamicZoneTarget, componentsToAdd } = action; const { dynamicZoneTarget, componentsToAdd } = action;
return state.updateIn( return state.updateIn(
[ ['modifiedData', 'contentType', 'schema', 'attributes', dynamicZoneTarget, 'components'],
'modifiedData',
'contentType',
'schema',
'attributes',
dynamicZoneTarget,
'components',
],
list => { list => {
return list.concat(componentsToAdd); return list.concat(componentsToAdd);
} }
@ -165,14 +143,7 @@ const reducer = (state, action) => {
return state return state
.updateIn( .updateIn(
[ ['modifiedData', 'contentType', 'schema', 'attributes', dynamicZoneTarget, 'components'],
'modifiedData',
'contentType',
'schema',
'attributes',
dynamicZoneTarget,
'components',
],
list => { list => {
return fromJS(makeUnique([...list.toJS(), ...newComponents])); return fromJS(makeUnique([...list.toJS(), ...newComponents]));
} }
@ -196,9 +167,7 @@ const reducer = (state, action) => {
}, },
}; };
return state.updateIn(['contentTypes', action.uid], () => return state.updateIn(['contentTypes', action.uid], () => fromJS(newSchema));
fromJS(newSchema)
);
} }
case 'CREATE_COMPONENT_SCHEMA': { case 'CREATE_COMPONENT_SCHEMA': {
const newSchema = { const newSchema = {
@ -214,14 +183,10 @@ const reducer = (state, action) => {
if (action.shouldAddComponentToData) { if (action.shouldAddComponentToData) {
return state return state
.updateIn(['components', action.uid], () => fromJS(newSchema)) .updateIn(['components', action.uid], () => fromJS(newSchema))
.updateIn(['modifiedData', 'components', action.uid], () => .updateIn(['modifiedData', 'components', action.uid], () => fromJS(newSchema));
fromJS(newSchema)
);
} }
return state.updateIn(['components', action.uid], () => return state.updateIn(['components', action.uid], () => fromJS(newSchema));
fromJS(newSchema)
);
} }
case 'DELETE_NOT_SAVED_TYPE': { case 'DELETE_NOT_SAVED_TYPE': {
// Doing so will also reset the modified and the initial data // Doing so will also reset the modified and the initial data
@ -243,144 +208,129 @@ const reducer = (state, action) => {
? [forTarget] ? [forTarget]
: [forTarget, targetUid]; : [forTarget, targetUid];
return newState.updateIn( return newState.updateIn(['modifiedData', ...pathToDataToEdit, 'schema'], obj => {
['modifiedData', ...pathToDataToEdit, 'schema'], let oppositeAttributeNameToRemove = null;
obj => { let oppositeAttributeNameToUpdate = null;
let oppositeAttributeNameToRemove = null; let oppositeAttributeNameToCreateBecauseOfNatureChange = null;
let oppositeAttributeNameToUpdate = null; let oppositeAttributeToCreate = null;
let oppositeAttributeNameToCreateBecauseOfNatureChange = null;
let oppositeAttributeToCreate = null;
const newObj = OrderedMap( const newObj = OrderedMap(
obj obj
.get('attributes') .get('attributes')
.keySeq() .keySeq()
.reduce((acc, current) => { .reduce((acc, current) => {
const isEditingCurrentAttribute = const isEditingCurrentAttribute = current === initialAttributeName;
current === initialAttributeName;
if (isEditingCurrentAttribute) { if (isEditingCurrentAttribute) {
const currentUid = state.getIn([ const currentUid = state.getIn(['modifiedData', ...pathToDataToEdit, 'uid']);
'modifiedData', const isEditingRelation = has(initialAttribute, 'nature');
...pathToDataToEdit, const didChangeTargetRelation = initialAttribute.target !== rest.target;
'uid', const didCreateInternalRelation = rest.target === currentUid;
]); const nature = rest.nature;
const isEditingRelation = has(initialAttribute, 'nature'); const initialNature = initialAttribute.nature;
const didChangeTargetRelation = const hadInternalRelation = initialAttribute.target === currentUid;
initialAttribute.target !== rest.target; const didChangeRelationNature = initialAttribute.nature !== nature;
const didCreateInternalRelation = rest.target === currentUid; const shouldRemoveOppositeAttributeBecauseOfTargetChange =
const nature = rest.nature; didChangeTargetRelation &&
const initialNature = initialAttribute.nature; !didCreateInternalRelation &&
const hadInternalRelation = hadInternalRelation &&
initialAttribute.target === currentUid; isEditingRelation;
const didChangeRelationNature = const shouldRemoveOppositeAttributeBecauseOfNatureChange =
initialAttribute.nature !== nature; didChangeRelationNature &&
const shouldRemoveOppositeAttributeBecauseOfTargetChange = hadInternalRelation &&
didChangeTargetRelation && ['oneWay', 'manyWay'].includes(nature) &&
!didCreateInternalRelation && isEditingRelation;
hadInternalRelation && const shouldUpdateOppositeAttributeBecauseOfNatureChange =
isEditingRelation; !ONE_SIDE_RELATIONS.includes(initialNature) &&
const shouldRemoveOppositeAttributeBecauseOfNatureChange = !ONE_SIDE_RELATIONS.includes(nature) &&
didChangeRelationNature && hadInternalRelation &&
hadInternalRelation && didCreateInternalRelation &&
['oneWay', 'manyWay'].includes(nature) && isEditingRelation;
isEditingRelation; const shouldCreateOppositeAttributeBecauseOfNatureChange =
const shouldUpdateOppositeAttributeBecauseOfNatureChange = ONE_SIDE_RELATIONS.includes(initialNature) &&
!ONE_SIDE_RELATIONS.includes(initialNature) && !ONE_SIDE_RELATIONS.includes(nature) &&
!ONE_SIDE_RELATIONS.includes(nature) && hadInternalRelation &&
hadInternalRelation && didCreateInternalRelation &&
didCreateInternalRelation && isEditingRelation;
isEditingRelation; const shouldCreateOppositeAttributeBecauseOfTargetChange =
const shouldCreateOppositeAttributeBecauseOfNatureChange = didChangeTargetRelation &&
ONE_SIDE_RELATIONS.includes(initialNature) && didCreateInternalRelation &&
!ONE_SIDE_RELATIONS.includes(nature) && !ONE_SIDE_RELATIONS.includes(nature);
hadInternalRelation &&
didCreateInternalRelation &&
isEditingRelation;
const shouldCreateOppositeAttributeBecauseOfTargetChange =
didChangeTargetRelation &&
didCreateInternalRelation &&
!ONE_SIDE_RELATIONS.includes(nature);
// Update the opposite attribute name so it is removed at the end of the loop // Update the opposite attribute name so it is removed at the end of the loop
if (
shouldRemoveOppositeAttributeBecauseOfTargetChange ||
shouldRemoveOppositeAttributeBecauseOfNatureChange
) {
oppositeAttributeNameToRemove = initialAttribute.targetAttribute;
}
// Set the opposite attribute that will be updated when the loop attribute matches the name
if (
shouldUpdateOppositeAttributeBecauseOfNatureChange ||
shouldCreateOppositeAttributeBecauseOfNatureChange ||
shouldCreateOppositeAttributeBecauseOfTargetChange
) {
oppositeAttributeNameToUpdate = initialAttribute.targetAttribute;
oppositeAttributeNameToCreateBecauseOfNatureChange = rest.targetAttribute;
oppositeAttributeToCreate = {
nature: getOppositeNature(rest.nature),
target: rest.target,
unique: rest.unique,
// Leave this if we allow the required on the relation
// required: rest.required,
dominant: rest.nature === 'manyToMany' ? !rest.dominant : null,
targetAttribute: name,
columnName: rest.targetColumnName,
targetColumnName: rest.columnName,
};
// First update the current attribute with the value
acc[name] = fromJS(rest);
// Then (if needed) create the opposite attribute the case is changing the relation from
// We do it here so keep the order of the attributes
// oneWay || manyWay to something another relation
if ( if (
shouldRemoveOppositeAttributeBecauseOfTargetChange ||
shouldRemoveOppositeAttributeBecauseOfNatureChange
) {
oppositeAttributeNameToRemove =
initialAttribute.targetAttribute;
}
// Set the opposite attribute that will be updated when the loop attribute matches the name
if (
shouldUpdateOppositeAttributeBecauseOfNatureChange ||
shouldCreateOppositeAttributeBecauseOfNatureChange || shouldCreateOppositeAttributeBecauseOfNatureChange ||
shouldCreateOppositeAttributeBecauseOfTargetChange shouldCreateOppositeAttributeBecauseOfTargetChange
) { ) {
oppositeAttributeNameToUpdate = acc[oppositeAttributeNameToCreateBecauseOfNatureChange] = fromJS(
initialAttribute.targetAttribute; oppositeAttributeToCreate
oppositeAttributeNameToCreateBecauseOfNatureChange = );
rest.targetAttribute;
oppositeAttributeToCreate = { oppositeAttributeToCreate = null;
nature: getOppositeNature(rest.nature), oppositeAttributeNameToCreateBecauseOfNatureChange = null;
target: rest.target,
unique: rest.unique,
// Leave this if we allow the required on the relation
// required: rest.required,
dominant:
rest.nature === 'manyToMany' ? !rest.dominant : null,
targetAttribute: name,
columnName: rest.targetColumnName,
targetColumnName: rest.columnName,
};
// First update the current attribute with the value
acc[name] = fromJS(rest);
// Then (if needed) create the opposite attribute the case is changing the relation from
// We do it here so keep the order of the attributes
// oneWay || manyWay to something another relation
if (
shouldCreateOppositeAttributeBecauseOfNatureChange ||
shouldCreateOppositeAttributeBecauseOfTargetChange
) {
acc[
oppositeAttributeNameToCreateBecauseOfNatureChange
] = fromJS(oppositeAttributeToCreate);
oppositeAttributeToCreate = null;
oppositeAttributeNameToCreateBecauseOfNatureChange = null;
}
return acc;
} }
acc[name] = fromJS(rest); return acc;
} else if (current === oppositeAttributeNameToUpdate) {
acc[
oppositeAttributeNameToCreateBecauseOfNatureChange
] = fromJS(oppositeAttributeToCreate);
} else {
acc[current] = obj.getIn(['attributes', current]);
} }
return acc; acc[name] = fromJS(rest);
}, {}) } else if (current === oppositeAttributeNameToUpdate) {
); acc[oppositeAttributeNameToCreateBecauseOfNatureChange] = fromJS(
oppositeAttributeToCreate
);
} else {
acc[current] = obj.getIn(['attributes', current]);
}
let updatedObj; return acc;
}, {})
);
// Remove the opposite attribute let updatedObj;
if (oppositeAttributeNameToRemove !== null) {
updatedObj = newObj.remove(oppositeAttributeNameToRemove);
} else {
updatedObj = newObj;
}
return obj.set('attributes', updatedObj); // Remove the opposite attribute
if (oppositeAttributeNameToRemove !== null) {
updatedObj = newObj.remove(oppositeAttributeNameToRemove);
} else {
updatedObj = newObj;
} }
);
return obj.set('attributes', updatedObj);
});
} }
case 'GET_DATA_SUCCEEDED': { case 'GET_DATA_SUCCEEDED': {
@ -418,35 +368,18 @@ const reducer = (state, action) => {
]); ]);
case 'REMOVE_FIELD': { case 'REMOVE_FIELD': {
const { mainDataKey, attributeToRemoveName } = action; const { mainDataKey, attributeToRemoveName } = action;
const pathToAttributes = [ const pathToAttributes = ['modifiedData', mainDataKey, 'schema', 'attributes'];
'modifiedData', const pathToAttributeToRemove = [...pathToAttributes, attributeToRemoveName];
mainDataKey,
'schema',
'attributes',
];
const pathToAttributeToRemove = [
...pathToAttributes,
attributeToRemoveName,
];
const attributeToRemoveData = state.getIn(pathToAttributeToRemove); const attributeToRemoveData = state.getIn(pathToAttributeToRemove);
const isRemovingRelationAttribute = const isRemovingRelationAttribute = attributeToRemoveData.get('nature') !== undefined;
attributeToRemoveData.get('nature') !== undefined;
// Only content types can have relations with themselves since // Only content types can have relations with themselves since
// components can only have oneWay or manyWay relations // components can only have oneWay or manyWay relations
const canTheAttributeToRemoveHaveARelationWithItself = const canTheAttributeToRemoveHaveARelationWithItself = mainDataKey === 'contentType';
mainDataKey === 'contentType';
if ( if (isRemovingRelationAttribute && canTheAttributeToRemoveHaveARelationWithItself) {
isRemovingRelationAttribute && const { target, nature, targetAttribute } = attributeToRemoveData.toJS();
canTheAttributeToRemoveHaveARelationWithItself
) {
const {
target,
nature,
targetAttribute,
} = attributeToRemoveData.toJS();
const uid = state.getIn(['modifiedData', 'contentType', 'uid']); const uid = state.getIn(['modifiedData', 'contentType', 'uid']);
const shouldRemoveOppositeAttribute = const shouldRemoveOppositeAttribute =
target === uid && !ONE_SIDE_RELATIONS.includes(nature); target === uid && !ONE_SIDE_RELATIONS.includes(nature);
@ -458,7 +391,15 @@ const reducer = (state, action) => {
} }
} }
return state.removeIn(pathToAttributeToRemove); return state.removeIn(pathToAttributeToRemove).updateIn([...pathToAttributes], attributes => {
return attributes.keySeq().reduce((acc, current) => {
if (acc.getIn([current, 'targetField']) === attributeToRemoveName) {
return acc.removeIn([current, 'targetField']);
}
return acc;
}, attributes);
});
} }
case 'SET_MODIFIED_DATA': { case 'SET_MODIFIED_DATA': {
let newState = state let newState = state
@ -502,9 +443,7 @@ const reducer = (state, action) => {
if (schemaType === 'component') { if (schemaType === 'component') {
newState = newState.updateIn(['components'], obj => { newState = newState.updateIn(['components'], obj => {
return obj.update(uid, () => return obj.update(uid, () => newState.getIn(['modifiedData', 'component']));
newState.getIn(['modifiedData', 'component'])
);
}); });
} }

View File

@ -417,13 +417,7 @@ const data = {
collectionName: '', collectionName: '',
attributes: { attributes: {
price_range: { price_range: {
enum: [ enum: ['very_cheap', 'cheap', 'average', 'expensive', 'very_expensive'],
'very_cheap',
'cheap',
'average',
'expensive',
'very_expensive',
],
type: 'enumeration', type: 'enumeration',
}, },
closing_period: { closing_period: {
@ -479,6 +473,17 @@ const data = {
}, },
}, },
}, },
'application::homepage.homepage': {
uid: 'application::homepage.homepage',
schema: {
name: 'homepage',
attributes: {
title: { type: 'string' },
description: { type: 'string' },
homepageuidfield: { type: 'uid', targetField: 'description' },
},
},
},
}, },
}; };

View File

@ -17,10 +17,7 @@ describe('CTB | containers | DataManagerProvider | reducer | REMOVE_FIELD', () =
const state = initialState const state = initialState
.set('contentTypes', fromJS(testData.contentTypes)) .set('contentTypes', fromJS(testData.contentTypes))
.set('initialContentTypes', fromJS(testData.contentTypes)) .set('initialContentTypes', fromJS(testData.contentTypes))
.setIn( .setIn(['modifiedData', 'contentType'], fromJS(testData.contentTypes[contentTypeUID]))
['modifiedData', 'contentType'],
fromJS(testData.contentTypes[contentTypeUID])
)
.setIn(['modifiedData', 'components'], fromJS({})); .setIn(['modifiedData', 'components'], fromJS({}));
const expected = state.removeIn([ const expected = state.removeIn([
@ -49,10 +46,7 @@ describe('CTB | containers | DataManagerProvider | reducer | REMOVE_FIELD', () =
const state = initialState const state = initialState
.set('contentTypes', fromJS(testData.contentTypes)) .set('contentTypes', fromJS(testData.contentTypes))
.set('initialContentTypes', fromJS(testData.contentTypes)) .set('initialContentTypes', fromJS(testData.contentTypes))
.setIn( .setIn(['modifiedData', 'contentType'], fromJS(testData.contentTypes[contentTypeUID]))
['modifiedData', 'contentType'],
fromJS(testData.contentTypes[contentTypeUID])
)
.setIn(['modifiedData', 'components'], fromJS({})); .setIn(['modifiedData', 'components'], fromJS({}));
const expected = state.removeIn([ const expected = state.removeIn([
@ -163,10 +157,7 @@ describe('CTB | containers | DataManagerProvider | reducer | REMOVE_FIELD', () =
.setIn(['contentTypes', contentTypeUID], fromJS(contentType)) .setIn(['contentTypes', contentTypeUID], fromJS(contentType))
.setIn(['modifiedData', 'contentType'], fromJS(contentType)); .setIn(['modifiedData', 'contentType'], fromJS(contentType));
const expected = state.setIn( const expected = state.setIn(['modifiedData', 'contentType'], fromJS(expectedContentType));
['modifiedData', 'contentType'],
fromJS(expectedContentType)
);
expect(reducer(state, action)).toEqual(expected); expect(reducer(state, action)).toEqual(expected);
}); });
@ -257,10 +248,7 @@ describe('CTB | containers | DataManagerProvider | reducer | REMOVE_FIELD', () =
.setIn(['contentTypes', contentTypeUID], fromJS(contentType)) .setIn(['contentTypes', contentTypeUID], fromJS(contentType))
.setIn(['modifiedData', 'contentType'], fromJS(contentType)); .setIn(['modifiedData', 'contentType'], fromJS(contentType));
const expected = state.setIn( const expected = state.setIn(['modifiedData', 'contentType'], fromJS(expectedContentType));
['modifiedData', 'contentType'],
fromJS(expectedContentType)
);
expect(reducer(state, action)).toEqual(expected); expect(reducer(state, action)).toEqual(expected);
expect( expect(
@ -271,4 +259,35 @@ describe('CTB | containers | DataManagerProvider | reducer | REMOVE_FIELD', () =
).toEqual(expected); ).toEqual(expected);
}); });
}); });
describe('Removing a field that is targeted by a UID field', () => {
it('Should remove the attribute correctly and remove the targetField from the UID field', () => {
const contentTypeUID = 'application::homepage.homepage';
const attributeToRemoveName = 'description';
const action = {
type: 'REMOVE_FIELD',
mainDataKey: 'contentType',
attributeToRemoveName,
componentUid: '',
};
const state = initialState
.set('contentTypes', fromJS(testData.contentTypes))
.set('initialContentTypes', fromJS(testData.contentTypes))
.setIn(['modifiedData', 'contentType'], fromJS(testData.contentTypes[contentTypeUID]))
.setIn(['modifiedData', 'components'], fromJS({}));
const expected = state
.removeIn(['modifiedData', 'contentType', 'schema', 'attributes', attributeToRemoveName])
.removeIn([
'modifiedData',
'contentType',
'schema',
'attributes',
'homepageuidfield',
'targetField',
]);
expect(reducer(state, action)).toEqual(expected);
});
});
}); });

View File

@ -327,6 +327,7 @@ const FormModal = () => {
const isOpen = !isEmpty(search); const isOpen = !isEmpty(search);
const isPickingAttribute = state.modalType === 'chooseAttribute'; const isPickingAttribute = state.modalType === 'chooseAttribute';
const uid = createUid(modifiedData.name || ''); const uid = createUid(modifiedData.name || '');
const attributes = get(allDataSchema, [...state.pathToSchema, 'schema', 'attributes'], null);
const checkFormValidity = async () => { const checkFormValidity = async () => {
let schema; let schema;
@ -394,7 +395,6 @@ const FormModal = () => {
} }
); );
} }
schema = forms[state.modalType].schema( schema = forms[state.modalType].schema(
get(allDataSchema, state.pathToSchema, {}), get(allDataSchema, state.pathToSchema, {}),
type, type,
@ -1090,180 +1090,184 @@ const FormModal = () => {
</div> </div>
); );
}) })
: form(modifiedData, state.attributeType, state.step, state.actionType).items.map( : form(
(row, index) => { modifiedData,
return ( state.attributeType,
<div className="row" key={index} style={{ marginBottom: 4 }}> state.step,
{row.map((input, i) => { state.actionType,
// The divider type is used mainly the advanced tab attributes
// It is the one responsible for displaying the settings label ).items.map((row, index) => {
if (input.type === 'divider') { return (
return ( <div className="row" key={index} style={{ marginBottom: 4 }}>
<div {row.map((input, i) => {
className="col-12" // The divider type is used mainly the advanced tab
style={{ // It is the one responsible for displaying the settings label
marginBottom: '1.4rem', if (input.type === 'divider') {
marginTop: -2,
fontWeight: 500,
}}
key="divider"
>
<FormattedMessage
id={getTrad('form.attribute.item.settings.name')}
/>
</div>
);
}
// The spacer type is used mainly to align the icon picker...
if (input.type === 'spacer') {
return <div key="spacer" style={{ height: 11 }} />;
}
// The spacer type is used mainly to align the icon picker...
if (input.type === 'spacer-small') {
return <div key="spacer" style={{ height: 4 }} />;
}
if (input.type === 'spacer-medium') {
return <div key="spacer" style={{ height: 8 }} />;
}
// This type is used in the addComponentToDynamicZone modal when selecting the option add an existing component
// It pushes select the components to the right
if (input.type === 'pushRight') {
return <div key={`${index}.${i}`} className={`col-${input.size}`} />;
}
if (input.type === 'relation') {
return (
<RelationForm
key="relation"
mainBoxHeader={get(headers, [0, 'label'], '')}
modifiedData={modifiedData}
naturePickerType={state.forTarget}
onChange={handleChange}
errors={formErrors}
/>
);
}
// Retrieve the error for a specific input
const errorId = get(
formErrors,
[
...input.name
.split('.')
// The filter here is used when creating a component
// in the component step 1 modal
// Since the component info is stored in the
// componentToCreate object we can access the error
// By removing the key
.filter(key => key !== 'componentToCreate'),
'id',
],
null
);
const retrievedValue = get(modifiedData, input.name, '');
let value;
// Condition for the boolean default value
// The radio input doesn't accept false, true or null as value
// So we pass them as string
// This way the data stays accurate and we don't have to operate
// any data mutation
if (input.name === 'default' && state.attributeType === 'boolean') {
value = toString(retrievedValue);
// Same here for the enum
} else if (input.name === 'enum' && Array.isArray(retrievedValue)) {
value = retrievedValue.join('\n');
} else if (input.name === 'uid') {
value = input.value;
} else {
value = retrievedValue;
}
// The addon input is not present in @buffetjs so we are using the old lib
// for the moment that's why we don't want them be passed to buffet
// like the other created inputs
if (input.type === 'addon') {
return (
<InputsIndex
key={input.name}
{...input}
type="string"
onChange={handleChange}
value={value}
style={{ marginTop: 8, marginBottom: 11 }}
/>
);
}
return ( return (
<div className={`col-${input.size || 6}`} key={input.name}> <div
<Inputs className="col-12"
{...input} style={{
modifiedData={modifiedData} marginBottom: '1.4rem',
addComponentsToDynamicZone={handleClickAddComponentsToDynamicZone} marginTop: -2,
customInputs={{ fontWeight: 500,
componentIconPicker: ComponentIconPicker, }}
componentSelect: WrapperSelect, key="divider"
creatableSelect: WrapperSelect, >
customCheckboxWithChildren: CustomCheckbox, <FormattedMessage
booleanBox: BooleanBox, id={getTrad('form.attribute.item.settings.name')}
}}
isCreating={isCreating}
// Props for the componentSelect
isCreatingComponentWhileAddingAField={
isCreatingComponentWhileAddingAField
}
// Props for the componentSelect
// Since the component is created after adding it to a type
// its name and category can't be retrieved from the data manager
componentCategoryNeededForAddingAfieldWhileCreatingAComponent={get(
componentToCreate,
'category',
null
)}
// Props for the componentSelect same explanation
componentNameNeededForAddingAfieldWhileCreatingAComponent={get(
componentToCreate,
'name',
null
)}
isAddingAComponentToAnotherComponent={
state.forTarget === 'components' ||
state.forTarget === 'component'
}
value={value}
error={isEmpty(errorId) ? null : formatMessage({ id: errorId })}
onChange={handleChange}
onBlur={() => {}}
description={
get(input, 'description.id', null)
? formatMessage(input.description)
: input.description
}
placeholder={
get(input, 'placeholder.id', null)
? formatMessage(input.placeholder)
: input.placeholder
}
label={
get(input, 'label.id', null)
? formatMessage(input.label)
: input.label
}
/> />
</div> </div>
); );
})} }
</div>
); // The spacer type is used mainly to align the icon picker...
} if (input.type === 'spacer') {
)} return <div key="spacer" style={{ height: 11 }} />;
}
// The spacer type is used mainly to align the icon picker...
if (input.type === 'spacer-small') {
return <div key="spacer" style={{ height: 4 }} />;
}
if (input.type === 'spacer-medium') {
return <div key="spacer" style={{ height: 8 }} />;
}
// This type is used in the addComponentToDynamicZone modal when selecting the option add an existing component
// It pushes select the components to the right
if (input.type === 'pushRight') {
return <div key={`${index}.${i}`} className={`col-${input.size}`} />;
}
if (input.type === 'relation') {
return (
<RelationForm
key="relation"
mainBoxHeader={get(headers, [0, 'label'], '')}
modifiedData={modifiedData}
naturePickerType={state.forTarget}
onChange={handleChange}
errors={formErrors}
/>
);
}
// Retrieve the error for a specific input
const errorId = get(
formErrors,
[
...input.name
.split('.')
// The filter here is used when creating a component
// in the component step 1 modal
// Since the component info is stored in the
// componentToCreate object we can access the error
// By removing the key
.filter(key => key !== 'componentToCreate'),
'id',
],
null
);
const retrievedValue = get(modifiedData, input.name, '');
let value;
// Condition for the boolean default value
// The radio input doesn't accept false, true or null as value
// So we pass them as string
// This way the data stays accurate and we don't have to operate
// any data mutation
if (input.name === 'default' && state.attributeType === 'boolean') {
value = toString(retrievedValue);
// Same here for the enum
} else if (input.name === 'enum' && Array.isArray(retrievedValue)) {
value = retrievedValue.join('\n');
} else if (input.name === 'uid') {
value = input.value;
} else {
value = retrievedValue;
}
// The addon input is not present in @buffetjs so we are using the old lib
// for the moment that's why we don't want them be passed to buffet
// like the other created inputs
if (input.type === 'addon') {
return (
<InputsIndex
key={input.name}
{...input}
type="string"
onChange={handleChange}
value={value}
style={{ marginTop: 8, marginBottom: 11 }}
/>
);
}
return (
<div className={`col-${input.size || 6}`} key={input.name}>
<Inputs
{...input}
modifiedData={modifiedData}
addComponentsToDynamicZone={handleClickAddComponentsToDynamicZone}
customInputs={{
componentIconPicker: ComponentIconPicker,
componentSelect: WrapperSelect,
creatableSelect: WrapperSelect,
customCheckboxWithChildren: CustomCheckbox,
booleanBox: BooleanBox,
}}
isCreating={isCreating}
// Props for the componentSelect
isCreatingComponentWhileAddingAField={
isCreatingComponentWhileAddingAField
}
// Props for the componentSelect
// Since the component is created after adding it to a type
// its name and category can't be retrieved from the data manager
componentCategoryNeededForAddingAfieldWhileCreatingAComponent={get(
componentToCreate,
'category',
null
)}
// Props for the componentSelect same explanation
componentNameNeededForAddingAfieldWhileCreatingAComponent={get(
componentToCreate,
'name',
null
)}
isAddingAComponentToAnotherComponent={
state.forTarget === 'components' ||
state.forTarget === 'component'
}
value={value}
error={isEmpty(errorId) ? null : formatMessage({ id: errorId })}
onChange={handleChange}
onBlur={() => {}}
description={
get(input, 'description.id', null)
? formatMessage(input.description)
: input.description
}
placeholder={
get(input, 'placeholder.id', null)
? formatMessage(input.placeholder)
: input.placeholder
}
label={
get(input, 'label.id', null)
? formatMessage(input.label)
: input.label
}
/>
</div>
);
})}
</div>
);
})}
</div> </div>
</ModalBody> </ModalBody>
</ModalForm> </ModalForm>

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { Fragment } from 'react';
import * as yup from 'yup'; import * as yup from 'yup';
import { get, isEmpty, toLower, trim, toNumber } from 'lodash'; import { get, isEmpty, toLower, trim, toNumber } from 'lodash';
import { translatedErrors as errorsTrads } from 'strapi-helper-plugin'; import { translatedErrors as errorsTrads } from 'strapi-helper-plugin';
@ -8,22 +8,14 @@ import getTrad from '../../../utils/getTrad';
import { createComponentUid, createUid, nameToSlug } from './createUid'; import { createComponentUid, createUid, nameToSlug } from './createUid';
import componentForm from './componentForm'; import componentForm from './componentForm';
import fields from './staticFields'; import fields from './staticFields';
import { import { NAME_REGEX, ENUM_REGEX, CATEGORY_NAME_REGEX } from './attributesRegexes';
NAME_REGEX,
ENUM_REGEX,
CATEGORY_NAME_REGEX,
} from './attributesRegexes';
import RESERVED_NAMES from './reservedNames'; import RESERVED_NAMES from './reservedNames';
/* eslint-disable indent */ /* eslint-disable indent */
/* eslint-disable prefer-arrow-callback */ /* eslint-disable prefer-arrow-callback */
yup.addMethod(yup.mixed, 'defined', function() { yup.addMethod(yup.mixed, 'defined', function() {
return this.test( return this.test('defined', errorsTrads.required, value => value !== undefined);
'defined',
errorsTrads.required,
value => value !== undefined
);
}); });
yup.addMethod(yup.string, 'unique', function( yup.addMethod(yup.string, 'unique', function(
@ -83,12 +75,7 @@ yup.addMethod(yup.array, 'matchesEnumRegex', function(message) {
}); });
}); });
const ATTRIBUTES_THAT_DONT_HAVE_MIN_MAX_SETTINGS = [ const ATTRIBUTES_THAT_DONT_HAVE_MIN_MAX_SETTINGS = ['boolean', 'date', 'enumeration', 'media'];
'boolean',
'date',
'enumeration',
'media',
];
const forms = { const forms = {
attribute: { attribute: {
@ -193,10 +180,7 @@ const forms = {
.integer() .integer()
.when('maxLength', (maxLength, schema) => { .when('maxLength', (maxLength, schema) => {
if (maxLength) { if (maxLength) {
return schema.max( return schema.max(maxLength, getTrad('error.validation.minSupMax'));
maxLength,
getTrad('error.validation.minSupMax')
);
} }
return schema; return schema;
@ -232,10 +216,7 @@ const forms = {
.of(yup.string()) .of(yup.string())
.min(1, errorsTrads.min) .min(1, errorsTrads.min)
.matchesEnumRegex(errorsTrads.regex) .matchesEnumRegex(errorsTrads.regex)
.hasNotEmptyValues( .hasNotEmptyValues('Empty strings are not allowed', dataToValidate.enum),
'Empty strings are not allowed',
dataToValidate.enum
),
enumName: yup.string().nullable(), enumName: yup.string().nullable(),
}); });
case 'number': case 'number':
@ -256,10 +237,7 @@ const forms = {
.matches(/^\d*$/) .matches(/^\d*$/)
.when('max', (max, schema) => { .when('max', (max, schema) => {
if (max) { if (max) {
return schema.isInferior( return schema.isInferior(getTrad('error.validation.minSupMax'), max);
getTrad('error.validation.minSupMax'),
max
);
} }
return schema; return schema;
@ -275,9 +253,7 @@ const forms = {
let defaultType = yup.number(); let defaultType = yup.number();
if (dataToValidate.type === 'integer') { if (dataToValidate.type === 'integer') {
defaultType = yup defaultType = yup.number().integer('component.Input.error.validation.integer');
.number()
.integer('component.Input.error.validation.integer');
} }
return yup.object().shape({ return yup.object().shape({
@ -344,8 +320,7 @@ const forms = {
}, },
{ {
autoFocus: false, autoFocus: false,
disabled: disabled: targetAttributeValue === null || targetAttributeValue === '-',
targetAttributeValue === null || targetAttributeValue === '-',
name: 'targetColumnName', name: 'targetColumnName',
label: '', label: '',
type: 'addon', type: 'addon',
@ -366,12 +341,7 @@ const forms = {
[fields.required], [fields.required],
[fields.unique], [fields.unique],
]; ];
const dynamiczoneItems = [ const dynamiczoneItems = [[fields.required], [fields.divider], [fields.max], [fields.min]];
[fields.required],
[fields.divider],
[fields.max],
[fields.min],
];
if (type === 'component') { if (type === 'component') {
if (step === '1') { if (step === '1') {
@ -386,11 +356,10 @@ const forms = {
}; };
} }
const items = defaultItems.slice(); let items = defaultItems.slice();
if (type === 'number' && data.type !== 'biginteger') { if (type === 'number' && data.type !== 'biginteger') {
const step = const step = data.type === 'decimal' || data.type === 'float' ? 'any' : '1';
data.type === 'decimal' || data.type === 'float' ? 'any' : '1';
items.splice(0, 1, [ items.splice(0, 1, [
{ {
@ -441,10 +410,7 @@ const forms = {
].concat( ].concat(
data.enum data.enum
? data.enum ? data.enum
.filter( .filter((val, index) => data.enum.indexOf(val) === index && !isEmpty(val))
(val, index) =>
data.enum.indexOf(val) === index && !isEmpty(val)
)
.map(val => ( .map(val => (
<option key={val} value={val}> <option key={val} value={val}>
{val} {val}
@ -463,9 +429,7 @@ const forms = {
type: 'text', type: 'text',
validations: {}, validations: {},
description: { description: {
id: getTrad( id: getTrad('form.attribute.item.enumeration.graphql.description'),
'form.attribute.item.enumeration.graphql.description'
),
}, },
}, },
]); ]);
@ -473,13 +437,20 @@ const forms = {
items.splice(0, 1, [ items.splice(0, 1, [
{ {
...fields.default, ...fields.default,
// type: data.type || 'date',
type: 'date', type: 'date',
value: null, value: null,
withDefaultValue: false, withDefaultValue: false,
disabled: data.type !== 'date', disabled: data.type !== 'date',
}, },
]); ]);
} else if (type === 'uid') {
const uidItems = [
[{ ...fields.default, disabled: Boolean(data.targetField), type: 'text' }],
[fields.divider],
[fields.required],
];
items = uidItems;
} }
if (!ATTRIBUTES_THAT_DONT_HAVE_MIN_MAX_SETTINGS.includes(type)) { if (!ATTRIBUTES_THAT_DONT_HAVE_MIN_MAX_SETTINGS.includes(type)) {
@ -490,11 +461,7 @@ const forms = {
name: type === 'number' ? 'max' : 'maxLength', name: type === 'number' ? 'max' : 'maxLength',
type: 'customCheckboxWithChildren', type: 'customCheckboxWithChildren',
label: { label: {
id: getTrad( id: getTrad(`form.attribute.item.maximum${type === 'number' ? '' : 'Length'}`),
`form.attribute.item.maximum${
type === 'number' ? '' : 'Length'
}`
),
}, },
validations: {}, validations: {},
@ -506,11 +473,7 @@ const forms = {
name: type === 'number' ? 'min' : 'minLength', name: type === 'number' ? 'min' : 'minLength',
type: 'customCheckboxWithChildren', type: 'customCheckboxWithChildren',
label: { label: {
id: getTrad( id: getTrad(`form.attribute.item.minimum${type === 'number' ? '' : 'Length'}`),
`form.attribute.item.minimum${
type === 'number' ? '' : 'Length'
}`
),
}, },
validations: {}, validations: {},
}, },
@ -534,7 +497,7 @@ const forms = {
items, items,
}; };
}, },
base(data, type, step) { base(data, type, step, actionType, attributes) {
if (type === 'relation') { if (type === 'relation') {
return { return {
items: [ items: [
@ -552,9 +515,7 @@ const forms = {
if (type === 'component' && step === '1') { if (type === 'component' && step === '1') {
const itemsToConcat = const itemsToConcat =
data.createComponent === true data.createComponent === true
? [[{ type: 'spacer' }]].concat( ? [[{ type: 'spacer' }]].concat(componentForm.base('componentToCreate.'))
componentForm.base('componentToCreate.')
)
: [[{ type: 'spacer' }]]; : [[{ type: 'spacer' }]];
return { return {
@ -581,19 +542,13 @@ const forms = {
size: 12, size: 12,
options: [ options: [
{ {
headerId: getTrad( headerId: getTrad('form.attribute.component.option.repeatable'),
'form.attribute.component.option.repeatable' descriptionId: getTrad('form.attribute.component.option.repeatable.description'),
),
descriptionId: getTrad(
'form.attribute.component.option.repeatable.description'
),
value: true, value: true,
}, },
{ {
headerId: getTrad('form.attribute.component.option.single'), headerId: getTrad('form.attribute.component.option.single'),
descriptionId: getTrad( descriptionId: getTrad('form.attribute.component.option.single.description'),
'form.attribute.component.option.single.description'
),
value: false, value: false,
}, },
], ],
@ -615,9 +570,7 @@ const forms = {
options: [ options: [
{ {
headerId: getTrad( headerId: getTrad(
`form.attribute.${type}.option.${ `form.attribute.${type}.option.${type === 'text' ? 'short-text' : 'multiple'}`
type === 'text' ? 'short-text' : 'multiple'
}`
), ),
descriptionId: getTrad( descriptionId: getTrad(
`form.attribute.${type}.option.${ `form.attribute.${type}.option.${
@ -628,9 +581,7 @@ const forms = {
}, },
{ {
headerId: getTrad( headerId: getTrad(
`form.attribute.${type}.option.${ `form.attribute.${type}.option.${type === 'text' ? 'long-text' : 'single'}`
type === 'text' ? 'long-text' : 'single'
}`
), ),
descriptionId: getTrad( descriptionId: getTrad(
`form.attribute.${type}.option.${ `form.attribute.${type}.option.${
@ -751,6 +702,35 @@ const forms = {
]); ]);
} }
if (type === 'uid') {
const options = Object.keys(attributes)
.filter(key => attributes[key].type === 'string')
.map(key => ({ id: key, value: key }));
items[0].push({
label: {
id: getTrad('modalForm.attribute.target-field'),
},
name: 'targetField',
type: 'select',
options: [{ id: getTrad('none'), value: '' }, ...options].map((option, index) => (
// eslint-disable-next-line react/no-array-index-key
<Fragment key={index}>
{index === 0 ? (
<FormattedMessage id={option.id}>
{msg => <option value={option.value}>{msg}</option>}
</FormattedMessage>
) : (
<option value={option.value}>{option.value}</option>
)}
</Fragment>
)),
validations: {
required: true,
},
});
}
return { return {
items, items,
}; };
@ -814,15 +794,11 @@ const forms = {
type: 'booleanBox', type: 'booleanBox',
size: 12, size: 12,
onChangeCallback: () => onChangeCallback: () =>
strapi.notification.info( strapi.notification.info(getTrad('contentType.kind.change.warning')),
getTrad('contentType.kind.change.warning')
),
options: [ options: [
{ {
headerId: getTrad('menu.section.models.name.singular'), headerId: getTrad('menu.section.models.name.singular'),
descriptionId: getTrad( descriptionId: getTrad('form.button.collection-type.description'),
'form.button.collection-type.description'
),
value: 'collectionType', value: 'collectionType',
}, },
{ {
@ -860,12 +836,7 @@ const forms = {
}, },
}, },
component: { component: {
schema( schema(alreadyTakenAttributes, componentCategory, isEditing = false, compoUid = null) {
alreadyTakenAttributes,
componentCategory,
isEditing = false,
compoUid = null
) {
const takenNames = isEditing const takenNames = isEditing
? alreadyTakenAttributes.filter(uid => uid !== compoUid) ? alreadyTakenAttributes.filter(uid => uid !== compoUid)
: alreadyTakenAttributes; : alreadyTakenAttributes;
@ -873,12 +844,7 @@ const forms = {
return yup.object().shape({ return yup.object().shape({
name: yup name: yup
.string() .string()
.unique( .unique(errorsTrads.unique, takenNames, createComponentUid, componentCategory)
errorsTrads.unique,
takenNames,
createComponentUid,
componentCategory
)
.isAllowed(getTrad('error.contentTypeName.reserved-name')) .isAllowed(getTrad('error.contentTypeName.reserved-name'))
.required(errorsTrads.required), .required(errorsTrads.required),
category: yup category: yup
@ -911,9 +877,7 @@ const forms = {
const isCreatingComponent = get(data, 'createComponent', false); const isCreatingComponent = get(data, 'createComponent', false);
const itemsToConcat = isCreatingComponent const itemsToConcat = isCreatingComponent
? [[{ type: 'spacer' }]].concat( ? [[{ type: 'spacer' }]].concat(componentForm.base('componentToCreate.'))
componentForm.base('componentToCreate.')
)
: [ : [
[{ type: 'spacer' }], [{ type: 'spacer' }],
[ [

View File

@ -257,8 +257,9 @@ const ListView = () => {
}; };
const goToCMSettingsPage = () => { const goToCMSettingsPage = () => {
const endPoint = isInContentTypeView const endPoint = isInContentTypeView
? `/plugins/content-manager/${targetUid}/ctm-configurations/edit-settings/content-types` ? `/plugins/content-manager/${contentTypeKind}/${targetUid}/ctm-configurations/edit-settings/content-types`
: `/plugins/content-manager/ctm-configurations/edit-settings/components/${targetUid}/`; : `/plugins/content-manager/ctm-configurations/edit-settings/components/${targetUid}/`;
push(endPoint); push(endPoint);
}; };

View File

@ -28,7 +28,7 @@
"attribute.text": "Text", "attribute.text": "Text",
"attribute.text.description": "Krátký nebo delší text", "attribute.text.description": "Krátký nebo delší text",
"attribute.time": "Čas", "attribute.time": "Čas",
"attribute.uid": "Uuid", "attribute.uid": "Uid",
"attribute.uid.description": "Unikátní identifikátor", "attribute.uid.description": "Unikátní identifikátor",
"button.attributes.add.another": "Přidat další pole", "button.attributes.add.another": "Přidat další pole",
"button.component.add": "Přidat komponent", "button.component.add": "Přidat komponent",

View File

@ -28,8 +28,9 @@
"attribute.text.description": "Small or long text like title or description", "attribute.text.description": "Small or long text like title or description",
"attribute.text": "Text", "attribute.text": "Text",
"attribute.time": "Time", "attribute.time": "Time",
"attribute.timestamp": "Timestamp",
"attribute.uid.description": "Unique identifier", "attribute.uid.description": "Unique identifier",
"attribute.uid": "Uuid", "attribute.uid": "Uid",
"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",
@ -118,6 +119,7 @@
"menu.section.models.name.singular": "Collection Type", "menu.section.models.name.singular": "Collection Type",
"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.attribute.target-field": "Attached field",
"modalForm.attribute.form.base.name.description": "No space is allowed for the name of the attribute", "modalForm.attribute.form.base.name.description": "No space is allowed for the name of the attribute",
"modalForm.attribute.form.base.name": "Name", "modalForm.attribute.form.base.name": "Name",
"modalForm.attribute.text.type-selection": "Type", "modalForm.attribute.text.type-selection": "Type",
@ -139,6 +141,7 @@
"modalForm.sub-header.chooseAttribute.contentType": "Select a field for your collection type", "modalForm.sub-header.chooseAttribute.contentType": "Select a field for your collection type",
"modelPage.attribute.relationWith": "Relation with", "modelPage.attribute.relationWith": "Relation with",
"modelPage.contentHeader.emptyDescription.description": "There is no description", "modelPage.contentHeader.emptyDescription.description": "There is no description",
"none": "None",
"notification.info.creating.notSaved": "Please save your work before creating a new collection type or component", "notification.info.creating.notSaved": "Please save your work before creating a new collection type or component",
"notification.info.autoreaload-disable": "The autoReload feature is required to use this plugin. Start your server with `strapi develop`", "notification.info.autoreaload-disable": "The autoReload feature is required to use this plugin. Start your server with `strapi develop`",
"plugin.description.long": "Modelize the data structure of your API. Create new fields and relations in just a minute. The files are automatically created and updated in your project.", "plugin.description.long": "Modelize the data structure of your API. Create new fields and relations in just a minute. The files are automatically created and updated in your project.",

View File

@ -50,7 +50,10 @@
"menu.section.models.name.singular": "Collection", "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.attribute.target-field": "Champ associé",
"modalForm.attribute.target-field.none": "Aucun",
"modalForm.singleType.header-create": "Créer un single type", "modalForm.singleType.header-create": "Créer un single type",
"none": "Aucun",
"button.single-types.create": "Créer un single type", "button.single-types.create": "Créer un single type",
"modelPage.attribute.relationWith": "Relation avec", "modelPage.attribute.relationWith": "Relation avec",
"modelPage.contentHeader.emptyDescription.description": "Il n'y a pas de description", "modelPage.contentHeader.emptyDescription.description": "Il n'y a pas de description",

View File

@ -68,7 +68,7 @@
"attribute.richtext.description": "Edytor tekstu z możliwością formatowania", "attribute.richtext.description": "Edytor tekstu z możliwością formatowania",
"attribute.text.description": "Krótki lub długi tekst jak tytuł lub opis", "attribute.text.description": "Krótki lub długi tekst jak tytuł lub opis",
"attribute.time": "Czas", "attribute.time": "Czas",
"attribute.uid": "Uuid", "attribute.uid": "Uid",
"attribute.uid.description": "Unikalny identyfikator", "attribute.uid.description": "Unikalny identyfikator",
"button.attributes.add.another": "Dodaj kolejne pole", "button.attributes.add.another": "Dodaj kolejne pole",
"button.component.add": "Dodaj komponent", "button.component.add": "Dodaj komponent",

View File

@ -28,7 +28,7 @@
"attribute.text": "Text", "attribute.text": "Text",
"attribute.text.description": "Простой текст для заголовка или описания", "attribute.text.description": "Простой текст для заголовка или описания",
"attribute.time": "Time", "attribute.time": "Time",
"attribute.uid": "Uuid", "attribute.uid": "Uid",
"attribute.uid.description": "Уникальный идентификатор", "attribute.uid.description": "Уникальный идентификатор",
"button.attributes.add.another": "Ещё поле", "button.attributes.add.another": "Ещё поле",
"button.component.add": "Добавить компонент", "button.component.add": "Добавить компонент",

View File

@ -28,7 +28,7 @@
"attribute.text": "Text", "attribute.text": "Text",
"attribute.text.description": "Krátky alebo dlhší text", "attribute.text.description": "Krátky alebo dlhší text",
"attribute.time": "Čas", "attribute.time": "Čas",
"attribute.uid": "Uuid", "attribute.uid": "Uid",
"attribute.uid.description": "Unikátny identifikátor", "attribute.uid.description": "Unikátny identifikátor",
"button.attributes.add.another": "Pridať ďalšie políčko", "button.attributes.add.another": "Pridať ďalšie políčko",
"button.component.add": "Pridať komponent", "button.component.add": "Pridať komponent",

View File

@ -28,7 +28,7 @@
"attribute.text": "文本", "attribute.text": "文本",
"attribute.text.description": "较短或较长的文字,例如标题或说明", "attribute.text.description": "较短或较长的文字,例如标题或说明",
"attribute.time": "时间", "attribute.time": "时间",
"attribute.uid": "Uuid", "attribute.uid": "Uid",
"attribute.uid.description": "唯一标识符", "attribute.uid.description": "唯一标识符",
"button.attributes.add.another": "添加一个新字段", "button.attributes.add.another": "添加一个新字段",
"button.component.add": "添加组件", "button.component.add": "添加组件",

View File

@ -5,6 +5,7 @@ const getAttributeDisplayedType = type => {
case 'date': case 'date':
case 'datetime': case 'datetime':
case 'time': case 'time':
case 'timestamp':
displayedType = 'date'; displayedType = 'date';
break; break;
case 'integer': case 'integer':