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

View File

@ -34,8 +34,7 @@ const addComponentsToState = (state, componentToAddUid, objToUpdate) => {
const isTemporaryComponent = componentToAdd.get('isTemporary');
const componentToAddSchema = componentToAdd.getIn(['schema', 'attributes']);
const hasComponentAlreadyBeenAdded =
state.getIn(['modifiedData', 'components', componentToAddUid]) !==
undefined;
state.getIn(['modifiedData', 'components', componentToAddUid]) !== undefined;
// created components are already in the modifiedData.components
// 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
nestedComponents.forEach(componentUid => {
const isTemporary =
state.getIn(['components', componentUid, 'isTemporary']) || false;
const isTemporary = state.getIn(['components', componentUid, 'isTemporary']) || false;
const hasNestedComponentAlreadyBeenAdded =
state.getIn(['modifiedData', 'components', componentUid]) !== undefined;
// Same logic here otherwise we will lose the modifications added to the components
if (!isTemporary && !hasNestedComponentAlreadyBeenAdded) {
newObj = newObj.set(
componentUid,
state.getIn(['components', componentUid])
);
newObj = newObj.set(componentUid, state.getIn(['components', componentUid]));
}
});
@ -84,52 +79,42 @@ const reducer = (state, action) => {
: [forTarget, targetUid];
return state
.updateIn(
['modifiedData', ...pathToDataToEdit, 'schema', 'attributes', name],
() => {
return fromJS(rest);
.updateIn(['modifiedData', ...pathToDataToEdit, 'schema', 'attributes', name], () => {
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
// 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;
}
)
return obj;
})
.updateIn(['modifiedData', 'components'], existingCompos => {
if (action.shouldAddComponentToData) {
return addComponentsToState(state, rest.component, existingCompos);
@ -142,14 +127,7 @@ const reducer = (state, action) => {
const { dynamicZoneTarget, componentsToAdd } = action;
return state.updateIn(
[
'modifiedData',
'contentType',
'schema',
'attributes',
dynamicZoneTarget,
'components',
],
['modifiedData', 'contentType', 'schema', 'attributes', dynamicZoneTarget, 'components'],
list => {
return list.concat(componentsToAdd);
}
@ -165,14 +143,7 @@ const reducer = (state, action) => {
return state
.updateIn(
[
'modifiedData',
'contentType',
'schema',
'attributes',
dynamicZoneTarget,
'components',
],
['modifiedData', 'contentType', 'schema', 'attributes', dynamicZoneTarget, 'components'],
list => {
return fromJS(makeUnique([...list.toJS(), ...newComponents]));
}
@ -196,9 +167,7 @@ const reducer = (state, action) => {
},
};
return state.updateIn(['contentTypes', action.uid], () =>
fromJS(newSchema)
);
return state.updateIn(['contentTypes', action.uid], () => fromJS(newSchema));
}
case 'CREATE_COMPONENT_SCHEMA': {
const newSchema = {
@ -214,14 +183,10 @@ const reducer = (state, action) => {
if (action.shouldAddComponentToData) {
return state
.updateIn(['components', action.uid], () => fromJS(newSchema))
.updateIn(['modifiedData', 'components', action.uid], () =>
fromJS(newSchema)
);
.updateIn(['modifiedData', 'components', action.uid], () => fromJS(newSchema));
}
return state.updateIn(['components', action.uid], () =>
fromJS(newSchema)
);
return state.updateIn(['components', action.uid], () => fromJS(newSchema));
}
case 'DELETE_NOT_SAVED_TYPE': {
// Doing so will also reset the modified and the initial data
@ -243,144 +208,129 @@ const reducer = (state, action) => {
? [forTarget]
: [forTarget, targetUid];
return newState.updateIn(
['modifiedData', ...pathToDataToEdit, 'schema'],
obj => {
let oppositeAttributeNameToRemove = null;
let oppositeAttributeNameToUpdate = null;
let oppositeAttributeNameToCreateBecauseOfNatureChange = null;
let oppositeAttributeToCreate = null;
return newState.updateIn(['modifiedData', ...pathToDataToEdit, 'schema'], obj => {
let oppositeAttributeNameToRemove = null;
let oppositeAttributeNameToUpdate = null;
let oppositeAttributeNameToCreateBecauseOfNatureChange = null;
let oppositeAttributeToCreate = null;
const newObj = OrderedMap(
obj
.get('attributes')
.keySeq()
.reduce((acc, current) => {
const isEditingCurrentAttribute =
current === initialAttributeName;
const newObj = OrderedMap(
obj
.get('attributes')
.keySeq()
.reduce((acc, current) => {
const isEditingCurrentAttribute = current === initialAttributeName;
if (isEditingCurrentAttribute) {
const currentUid = state.getIn([
'modifiedData',
...pathToDataToEdit,
'uid',
]);
const isEditingRelation = has(initialAttribute, 'nature');
const didChangeTargetRelation =
initialAttribute.target !== rest.target;
const didCreateInternalRelation = rest.target === currentUid;
const nature = rest.nature;
const initialNature = initialAttribute.nature;
const hadInternalRelation =
initialAttribute.target === currentUid;
const didChangeRelationNature =
initialAttribute.nature !== nature;
const shouldRemoveOppositeAttributeBecauseOfTargetChange =
didChangeTargetRelation &&
!didCreateInternalRelation &&
hadInternalRelation &&
isEditingRelation;
const shouldRemoveOppositeAttributeBecauseOfNatureChange =
didChangeRelationNature &&
hadInternalRelation &&
['oneWay', 'manyWay'].includes(nature) &&
isEditingRelation;
const shouldUpdateOppositeAttributeBecauseOfNatureChange =
!ONE_SIDE_RELATIONS.includes(initialNature) &&
!ONE_SIDE_RELATIONS.includes(nature) &&
hadInternalRelation &&
didCreateInternalRelation &&
isEditingRelation;
const shouldCreateOppositeAttributeBecauseOfNatureChange =
ONE_SIDE_RELATIONS.includes(initialNature) &&
!ONE_SIDE_RELATIONS.includes(nature) &&
hadInternalRelation &&
didCreateInternalRelation &&
isEditingRelation;
const shouldCreateOppositeAttributeBecauseOfTargetChange =
didChangeTargetRelation &&
didCreateInternalRelation &&
!ONE_SIDE_RELATIONS.includes(nature);
if (isEditingCurrentAttribute) {
const currentUid = state.getIn(['modifiedData', ...pathToDataToEdit, 'uid']);
const isEditingRelation = has(initialAttribute, 'nature');
const didChangeTargetRelation = initialAttribute.target !== rest.target;
const didCreateInternalRelation = rest.target === currentUid;
const nature = rest.nature;
const initialNature = initialAttribute.nature;
const hadInternalRelation = initialAttribute.target === currentUid;
const didChangeRelationNature = initialAttribute.nature !== nature;
const shouldRemoveOppositeAttributeBecauseOfTargetChange =
didChangeTargetRelation &&
!didCreateInternalRelation &&
hadInternalRelation &&
isEditingRelation;
const shouldRemoveOppositeAttributeBecauseOfNatureChange =
didChangeRelationNature &&
hadInternalRelation &&
['oneWay', 'manyWay'].includes(nature) &&
isEditingRelation;
const shouldUpdateOppositeAttributeBecauseOfNatureChange =
!ONE_SIDE_RELATIONS.includes(initialNature) &&
!ONE_SIDE_RELATIONS.includes(nature) &&
hadInternalRelation &&
didCreateInternalRelation &&
isEditingRelation;
const shouldCreateOppositeAttributeBecauseOfNatureChange =
ONE_SIDE_RELATIONS.includes(initialNature) &&
!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 (
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;
acc[oppositeAttributeNameToCreateBecauseOfNatureChange] = fromJS(
oppositeAttributeToCreate
);
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 (
shouldCreateOppositeAttributeBecauseOfNatureChange ||
shouldCreateOppositeAttributeBecauseOfTargetChange
) {
acc[
oppositeAttributeNameToCreateBecauseOfNatureChange
] = fromJS(oppositeAttributeToCreate);
oppositeAttributeToCreate = null;
oppositeAttributeNameToCreateBecauseOfNatureChange = null;
}
return acc;
oppositeAttributeToCreate = null;
oppositeAttributeNameToCreateBecauseOfNatureChange = null;
}
acc[name] = fromJS(rest);
} else if (current === oppositeAttributeNameToUpdate) {
acc[
oppositeAttributeNameToCreateBecauseOfNatureChange
] = fromJS(oppositeAttributeToCreate);
} else {
acc[current] = obj.getIn(['attributes', current]);
return acc;
}
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
if (oppositeAttributeNameToRemove !== null) {
updatedObj = newObj.remove(oppositeAttributeNameToRemove);
} else {
updatedObj = newObj;
}
let updatedObj;
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': {
@ -418,35 +368,18 @@ const reducer = (state, action) => {
]);
case 'REMOVE_FIELD': {
const { mainDataKey, attributeToRemoveName } = action;
const pathToAttributes = [
'modifiedData',
mainDataKey,
'schema',
'attributes',
];
const pathToAttributeToRemove = [
...pathToAttributes,
attributeToRemoveName,
];
const pathToAttributes = ['modifiedData', mainDataKey, 'schema', 'attributes'];
const pathToAttributeToRemove = [...pathToAttributes, attributeToRemoveName];
const attributeToRemoveData = state.getIn(pathToAttributeToRemove);
const isRemovingRelationAttribute =
attributeToRemoveData.get('nature') !== undefined;
const isRemovingRelationAttribute = attributeToRemoveData.get('nature') !== undefined;
// Only content types can have relations with themselves since
// components can only have oneWay or manyWay relations
const canTheAttributeToRemoveHaveARelationWithItself =
mainDataKey === 'contentType';
const canTheAttributeToRemoveHaveARelationWithItself = mainDataKey === 'contentType';
if (
isRemovingRelationAttribute &&
canTheAttributeToRemoveHaveARelationWithItself
) {
const {
target,
nature,
targetAttribute,
} = attributeToRemoveData.toJS();
if (isRemovingRelationAttribute && canTheAttributeToRemoveHaveARelationWithItself) {
const { target, nature, targetAttribute } = attributeToRemoveData.toJS();
const uid = state.getIn(['modifiedData', 'contentType', 'uid']);
const shouldRemoveOppositeAttribute =
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': {
let newState = state
@ -502,9 +443,7 @@ const reducer = (state, action) => {
if (schemaType === 'component') {
newState = newState.updateIn(['components'], obj => {
return obj.update(uid, () =>
newState.getIn(['modifiedData', 'component'])
);
return obj.update(uid, () => newState.getIn(['modifiedData', 'component']));
});
}

View File

@ -417,13 +417,7 @@ const data = {
collectionName: '',
attributes: {
price_range: {
enum: [
'very_cheap',
'cheap',
'average',
'expensive',
'very_expensive',
],
enum: ['very_cheap', 'cheap', 'average', 'expensive', 'very_expensive'],
type: 'enumeration',
},
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
.set('contentTypes', fromJS(testData.contentTypes))
.set('initialContentTypes', fromJS(testData.contentTypes))
.setIn(
['modifiedData', 'contentType'],
fromJS(testData.contentTypes[contentTypeUID])
)
.setIn(['modifiedData', 'contentType'], fromJS(testData.contentTypes[contentTypeUID]))
.setIn(['modifiedData', 'components'], fromJS({}));
const expected = state.removeIn([
@ -49,10 +46,7 @@ describe('CTB | containers | DataManagerProvider | reducer | REMOVE_FIELD', () =
const state = initialState
.set('contentTypes', fromJS(testData.contentTypes))
.set('initialContentTypes', fromJS(testData.contentTypes))
.setIn(
['modifiedData', 'contentType'],
fromJS(testData.contentTypes[contentTypeUID])
)
.setIn(['modifiedData', 'contentType'], fromJS(testData.contentTypes[contentTypeUID]))
.setIn(['modifiedData', 'components'], fromJS({}));
const expected = state.removeIn([
@ -163,10 +157,7 @@ describe('CTB | containers | DataManagerProvider | reducer | REMOVE_FIELD', () =
.setIn(['contentTypes', contentTypeUID], fromJS(contentType))
.setIn(['modifiedData', 'contentType'], fromJS(contentType));
const expected = state.setIn(
['modifiedData', 'contentType'],
fromJS(expectedContentType)
);
const expected = state.setIn(['modifiedData', 'contentType'], fromJS(expectedContentType));
expect(reducer(state, action)).toEqual(expected);
});
@ -257,10 +248,7 @@ describe('CTB | containers | DataManagerProvider | reducer | REMOVE_FIELD', () =
.setIn(['contentTypes', contentTypeUID], fromJS(contentType))
.setIn(['modifiedData', 'contentType'], fromJS(contentType));
const expected = state.setIn(
['modifiedData', 'contentType'],
fromJS(expectedContentType)
);
const expected = state.setIn(['modifiedData', 'contentType'], fromJS(expectedContentType));
expect(reducer(state, action)).toEqual(expected);
expect(
@ -271,4 +259,35 @@ describe('CTB | containers | DataManagerProvider | reducer | REMOVE_FIELD', () =
).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 isPickingAttribute = state.modalType === 'chooseAttribute';
const uid = createUid(modifiedData.name || '');
const attributes = get(allDataSchema, [...state.pathToSchema, 'schema', 'attributes'], null);
const checkFormValidity = async () => {
let schema;
@ -394,7 +395,6 @@ const FormModal = () => {
}
);
}
schema = forms[state.modalType].schema(
get(allDataSchema, state.pathToSchema, {}),
type,
@ -1090,180 +1090,184 @@ const FormModal = () => {
</div>
);
})
: form(modifiedData, state.attributeType, state.step, state.actionType).items.map(
(row, index) => {
return (
<div className="row" key={index} style={{ marginBottom: 4 }}>
{row.map((input, i) => {
// The divider type is used mainly the advanced tab
// It is the one responsible for displaying the settings label
if (input.type === 'divider') {
return (
<div
className="col-12"
style={{
marginBottom: '1.4rem',
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 }}
/>
);
}
: form(
modifiedData,
state.attributeType,
state.step,
state.actionType,
attributes
).items.map((row, index) => {
return (
<div className="row" key={index} style={{ marginBottom: 4 }}>
{row.map((input, i) => {
// The divider type is used mainly the advanced tab
// It is the one responsible for displaying the settings label
if (input.type === 'divider') {
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
className="col-12"
style={{
marginBottom: '1.4rem',
marginTop: -2,
fontWeight: 500,
}}
key="divider"
>
<FormattedMessage
id={getTrad('form.attribute.item.settings.name')}
/>
</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>
</ModalBody>
</ModalForm>

View File

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

View File

@ -257,8 +257,9 @@ const ListView = () => {
};
const goToCMSettingsPage = () => {
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}/`;
push(endPoint);
};

View File

@ -28,7 +28,7 @@
"attribute.text": "Text",
"attribute.text.description": "Krátký nebo delší text",
"attribute.time": "Čas",
"attribute.uid": "Uuid",
"attribute.uid": "Uid",
"attribute.uid.description": "Unikátní identifikátor",
"button.attributes.add.another": "Přidat další pole",
"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": "Text",
"attribute.time": "Time",
"attribute.timestamp": "Timestamp",
"attribute.uid.description": "Unique identifier",
"attribute.uid": "Uuid",
"attribute.uid": "Uid",
"button.attributes.add.another": "Add another field",
"button.component.add": "Add a component",
"button.component.create": "Create new component",
@ -118,6 +119,7 @@
"menu.section.models.name.singular": "Collection Type",
"menu.section.single-types.name.plural": "Single Types",
"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": "Name",
"modalForm.attribute.text.type-selection": "Type",
@ -139,6 +141,7 @@
"modalForm.sub-header.chooseAttribute.contentType": "Select a field for your collection type",
"modelPage.attribute.relationWith": "Relation with",
"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.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.",

View File

@ -50,7 +50,10 @@
"menu.section.models.name.singular": "Collection",
"menu.section.single-types.name.plural": "Single Types",
"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",
"none": "Aucun",
"button.single-types.create": "Créer un single type",
"modelPage.attribute.relationWith": "Relation avec",
"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.text.description": "Krótki lub długi tekst jak tytuł lub opis",
"attribute.time": "Czas",
"attribute.uid": "Uuid",
"attribute.uid": "Uid",
"attribute.uid.description": "Unikalny identyfikator",
"button.attributes.add.another": "Dodaj kolejne pole",
"button.component.add": "Dodaj komponent",

View File

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

View File

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

View File

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

View File

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