Merge branch 'add-media-ctb' of github.com:strapi/strapi into fix-ctb

This commit is contained in:
cyril lopez 2018-02-28 17:44:12 +01:00
commit 1d85c459fa
20 changed files with 161 additions and 79 deletions

View File

@ -88,7 +88,8 @@ module.exports = function(strapi) {
const loadedModel = _.assign({
tableName: definition.collectionName,
hasTimestamps: _.get(definition, 'options.timestamps') === true,
idAttribute: _.get(definition, 'options.idAttribute', 'id')
idAttribute: _.get(definition, 'options.idAttribute', 'id'),
associations: []
}, definition.options);
if (_.isString(_.get(connection, 'options.pivot_prefix'))) {

View File

@ -95,6 +95,7 @@ module.exports = function (strapi) {
It allows us to make Upload.find().populate('related')
instead of Upload.find().populate('related.item')
*/
const morphAssociations = definition.associations.filter(association => association.nature.toLowerCase().indexOf('morph') !== -1);
if (morphAssociations.length > 0) {
@ -220,6 +221,7 @@ module.exports = function (strapi) {
// Add some informations about ORM & client connection
definition.orm = 'mongoose';
definition.client = _.get(strapi.config.connections[definition.connection], 'client');
definition.associations = [];
// Register the final model for Mongoose.
definition.loadedModel = _.cloneDeep(definition.attributes);

View File

@ -22,6 +22,7 @@ import IcoText from '../../assets/images/icon_text.png';
import styles from './styles.scss';
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/no-autofocus */
const asset = {
'boolean': IcoBoolean,
'date': IcoDate,
@ -35,10 +36,16 @@ const asset = {
'text': IcoText,
};
function AttributeCard({ attribute, handleClick }) {
function AttributeCard({ attribute, autoFocus, handleClick, tabIndex }) {
return (
<div className="col-md-6">
<div className={styles.attributeCardContainer} onClick={() => handleClick(attribute.type)}>
<button
autoFocus={autoFocus}
className={styles.attributeCardContainer}
onClick={() => handleClick(attribute.type)}
type="button"
tabIndex={tabIndex + 1}
>
<div className={styles.attributeCard}>
<img src={asset[attribute.type]} alt="ico" />
<FormattedMessage id={`content-type-builder.popUpForm.attributes.${attribute.type}.name`}>
@ -46,14 +53,21 @@ function AttributeCard({ attribute, handleClick }) {
</FormattedMessage>
<FormattedMessage id={attribute.description} />
</div>
</div>
</button>
</div>
);
}
AttributeCard.defaultProps = {
autoFocus: false,
tabIndex: 0,
};
AttributeCard.propTypes = {
attribute: PropTypes.object.isRequired,
autoFocus: PropTypes.bool,
handleClick: PropTypes.func.isRequired,
tabIndex: PropTypes.number,
};
export default AttributeCard;

View File

@ -1,22 +1,22 @@
.attributeCardContainer { /* stylelint-disable */
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
width: 100% !important;
height: 4rem;
padding: 0 1rem 0 1rem;
margin-top: .6rem;
margin-bottom: .8rem;
padding: 0 1rem 0 1rem;
border: 1px solid #E3E9F3;
align-items: center;
justify-content: space-between;
border-radius: 0.25rem;
line-height: 4rem;
border: 1px solid #E3E9F3;
background: #FFFFFF;
line-height: 4rem;
box-shadow: 1px 1px 1px rgba(104, 118,145, 0.05);
&:hover, &:active {
cursor: pointer;
&:hover, &:active, &:focus {
background: #F7F7F7;
> div:after{
outline: 0;
> div:after {
color: #0097F6;
}
}
@ -24,25 +24,24 @@
.attributeCard {
font-size: 1.3rem;
&:after{
content: '\f05d';
position: absolute;
top: 7px;
right: 26px;
color: #E3E9F3;
font-size: 1.4rem;
font-family: 'FontAwesome';
-webkit-font-smoothing: antialiased;
}
&:hover{
background: none;
}
&:after{
position: absolute;
top: 7px; right: 26px;
content: '\f05d';
font-size: 1.4rem;
font-family: 'FontAwesome';
color: #E3E9F3;
-webkit-font-smoothing: antialiased;
}
> img{
display: inline-block;
width: 35px;
height: 20px;
width: 35px;
margin-top: -3px;
margin-right: 10px;
}
@ -57,11 +56,11 @@
}
.attributeType {
color: #323740 !important;
margin-right: 1.2rem;
font-style: normal !important;
color: #323740 !important;
text-transform: capitalize;
font-size: 1.3rem !important;
font-weight: 500 !important;
text-transform: capitalize;
font-style: normal !important;
-webkit-font-smoothing: subpixel-antialiased !important;
}

View File

@ -42,6 +42,8 @@ class AttributeRow extends React.Component { // eslint-disable-line react/prefer
'decimal': IcoNumber,
'email': IcoEmail,
'password': IcoPassword,
// TODO add Enumeration icon
'enumeration': IcoJson,
};
this.state = {
showWarning: false,
@ -90,7 +92,7 @@ class AttributeRow extends React.Component { // eslint-disable-line react/prefer
const relationStyle = !this.props.row.params.type ? styles.relation : '';
const icons = isNotEditable ? [{ icoType: 'lock' }] : [{ icoType: 'pencil', onClick: this.handleEdit }, { icoType: 'trash', onClick: () => this.setState({ showWarning: !this.state.showWarning }) }];
const editableStyle = isNotEditable ? '' : styles.editable;
return (
<li
className={`${styles.attributeRow} ${editableStyle} ${relationStyle}`}

View File

@ -251,6 +251,7 @@ function setAttributeFormData(hash) {
unique: false,
maxLength: false,
minLength: false,
multiple: false,
min: false,
max: false,
}),

View File

@ -108,6 +108,10 @@
"type": "password",
"description": "content-type-builder.popUpForm.attributes.password.description"
},
{
"type": "media",
"description": "content-type-builder.popUpForm.attributes.media.description"
},
{
"type": "relation",
"description": "content-type-builder.popUpForm.attributes.relation.description"
@ -716,6 +720,17 @@
"validations": {
"required": true
}
},
{
"label": {
"id": "content-type-builder.form.attribute.item.media.multiple"
},
"name": "params.multiple",
"type": "checkbox",
"value": false,
"validations": {
"required": true
}
}
]
},

View File

@ -460,8 +460,10 @@ export class Form extends React.Component { // eslint-disable-line react/prefer-
<AttributeCard
key={key}
attribute={attribute}
autoFocus={key === 0}
routePath={this.props.routePath}
handleClick={this.goToAttributeTypeView}
tabIndex={key}
/>
))
)

View File

@ -120,7 +120,7 @@ export function modelFetch(modelName) {
export function modelFetchSucceeded(data) {
const model = data;
const defaultKeys = ['required', 'unique', 'type', 'key', 'target', 'nature', 'targetColumnName', 'columnName'];
const defaultKeys = ['required', 'unique', 'type', 'key', 'target', 'nature', 'targetColumnName', 'columnName', 'multiple'];
forEach(model.model.attributes, (attribute, index) => {
map(attribute.params, (value, key) => {

View File

@ -173,6 +173,10 @@ export class ModelPage extends React.Component { // eslint-disable-line react/pr
handleEditAttribute = (attributeName) => {
const index = findIndex(this.props.modelPage.model.attributes, ['name', attributeName]);
const attribute = this.props.modelPage.model.attributes[index];
if (attribute.params.type === 'enumeration') {
return strapi.notification.info('content-type-builder.notification.info.enumeration');
}
const settingsType = attribute.params.type ? 'baseSettings' : 'defineRelation';
const parallelAttributeIndex = findIndex(this.props.modelPage.model.attributes, ['name', attribute.params.key]);
const hasParallelAttribute = settingsType === 'defineRelation' && parallelAttributeIndex !== -1 ? `::${parallelAttributeIndex}` : '';

View File

@ -100,7 +100,7 @@ export function* submitChanges(action) {
set(body.attributes[index].params, 'plugin', true);
}
if (!value) {
if (!value && key !== 'multiple') {
const paramsKey = includes(key, 'Value') ? replace(key,'Value', '') : key;
unset(body.attributes[index].params, paramsKey);
}
@ -116,6 +116,7 @@ export function* submitChanges(action) {
const baseUrl = '/content-type-builder/models/';
const requestUrl = method === 'POST' ? baseUrl : `${baseUrl}${body.name}`;
const opts = { method, body };
const response = yield call(request, requestUrl, opts, true);
if (response.ok) {

View File

@ -13,21 +13,22 @@
"attribute.email": "E-Mail",
"attribute.password": "Passwort",
"attribute.relation": "Beziehung",
"attribute.enumeration": "Enumeration",
"contentType.temporaryDisplay": "(Nicht gespeichert)",
"from": "aus",
"home.contentTypeBuilder.name": "Content-Typen",
"home.contentTypeBuilder.description": "Verwalte deine Content-Typen.",
"home.emptyContentType.title": "Es sind keine Content-Typen verfügbar",
"home.emptyContentType.description": "Lege deinen ersten Content-Typ an, Daten deiner API abrufen zu können.",
"home.emptyAttributes.title": "Es gibt noch keine Felder",
"home.emptyAttributes.description": "Füge deinem Content-Typen das erste Feld hinzu",
"button.contentType.create": "Lege einen Content-Typ an",
"button.contentType.add": "Neuer Content-Typ",
"button.attributes.add": "Neues Feld",
"error.validation.required": "Dieser Wert ist erforderlich.",
"error.validation.regex": "Dieser Wert entspricht nicht dem RegEx.",
"error.validation.max": "Dieser Wert ist zu hoch.",
@ -39,11 +40,12 @@
"error.attribute.key.taken": "Dieser Wert existiert bereits",
"error.attribute.sameKeyAndName": "Darf nicht gleich sein",
"error.validation.minSupMax": "Darf nicht höher sein",
"form.attribute.item.textarea.name": "Name",
"form.attribute.item.number.name": "Name",
"form.attribute.item.date.name": "Name",
"form.attribute.item.media.name": "Name",
"form.attribute.item.media.multiple": "Allow multiple files",
"form.attribute.item.json.name": "Name",
"form.attribute.item.boolean.name": "Name",
"form.attribute.item.string.name": "Name",
@ -63,11 +65,11 @@
"form.attribute.item.number.type.integer": "integer (ex: 10)",
"form.attribute.item.number.type.float": "float (ex: 3.33333333)",
"form.attribute.item.number.type.decimal": "decimal (ex: 2.22)",
"form.button.cancel": "Abbrechen",
"form.button.continue": "Weiter",
"form.button.save": "Speichern",
"form.contentType.item.connections": "Verbindung",
"form.contentType.item.name": "Name",
"form.contentType.item.name.description": "Der Name des Content-Typs sollte Singular sein. {link}",
@ -76,7 +78,7 @@
"form.contentType.item.description.placeholder": "Beschreibe deinen Content-Typ",
"form.contentType.item.collectionName": "Name des Dokuments in der Datenbank",
"form.contentType.item.collectionName.inputDescription": "Nützlich, wenn Content-Typ und Datenbankname unterschiedlich sind",
"menu.section.contentTypeBuilder.name.plural": "Content-Typen",
"menu.section.contentTypeBuilder.name.singular": "Content-Typ",
"menu.section.documentation.name": "Dokumentation",
@ -84,7 +86,7 @@
"menu.section.documentation.guideLink": "Anleitung.",
"menu.section.documentation.tutorial": "Schau dir unser",
"menu.section.documentation.tutorialLink": "Tutorial an.",
"modelPage.contentHeader.emptyDescription.description": "Dieser Content-Typ hat keine Beschreibung",
"modelPage.contentType.list.title.plural": "Felder",
"modelPage.contentType.list.title.singular": "Feld",
@ -92,17 +94,18 @@
"modelPage.contentType.list.relationShipTitle.plural": "Beziehungen",
"modelPage.contentType.list.relationShipTitle.singular": "Beziehung",
"modelPage.attribute.relationWith": "Beziehung mit",
"noTableWarning.description": "Vergiss nicht, die Tabelle `{modelName}` in deiner Datenbank zu erstellen",
"noTableWarning.infos": "Mehr Informationen",
"notification.error.message": "Ein Fehler ist aufgetreten",
"notification.info.contentType.creating.notSaved": "Bitte speichere zuerst diesen Content-Typ bevor du einen neuen anlegst",
"notification.info.optimized": "Dieses Plugin ist auf deinen localStorage optimiert",
"notification.success.message.contentType.edit": "Der Content-Typ wurde aktualisiert",
"notification.success.message.contentType.create": "Der Content-Typ wurde angelegt",
"notification.success.contentTypeDeleted": "Der Content-Typ wurde gelöscht",
"notification.info.enumeration": "This field is not editable for the moment...😮",
"popUpForm.attributes.string.description": "Titel, Namen, Namenslisten",
"popUpForm.attributes.text.description": "Beschreibungen, Paragraphen, Artikel",
"popUpForm.attributes.boolean.description": "Ja/Nein, 1 oder 0, Wahr/Falsch",
@ -113,7 +116,7 @@
"popUpForm.attributes.relation.description": "Bezieht sich auf einen Content-Typ",
"popUpForm.attributes.email.description": "E-Mail-Adressen von Benutzern",
"popUpForm.attributes.password.description": "Passwörter von Benutzers",
"popUpForm.attributes.string.name": "String",
"popUpForm.attributes.text.name": "Text",
"popUpForm.attributes.boolean.name": "Boolean",
@ -130,31 +133,30 @@
"popUpForm.create.contentType.header.title": "Neuer Content-Typ",
"popUpForm.choose.attributes.header.title": "Neues Feld hinzufügen",
"popUpForm.edit.contentType.header.title": "Content-Typen bearbeiten",
"popUpForm.navContainer.relation": "Beziehung definieren",
"popUpForm.navContainer.base": "Grundeinstellungen",
"popUpForm.navContainer.advanced": "Fortgeschrittene Einstellungen",
"popUpRelation.title": "Beziehung",
"popUpWarning.button.cancel": "Abbrechen",
"popUpWarning.button.confirm": "Bestätigen",
"popUpWarning.title": "Bitte bestätigen",
"popUpWarning.bodyMessage.contentType.delete": "Bist du sicher, dass du diesen Content-Typ löschen willst?",
"popUpWarning.bodyMessage.attribute.delete": "Bist du sicher, dass du dieses Feld löschen willst?",
"table.contentType.title.plural": "Content-Typen sind verfügbar",
"table.contentType.title.singular": "Content-Typ ist verfügbar",
"table.contentType.head.name": "Name",
"table.contentType.head.description": "Beschreibung",
"table.contentType.head.fields": "Felder",
"relation.oneToOne": "hat ein(en)",
"relation.oneToMany": "gehört zu vielen",
"relation.manyToOne": "hat viele",
"relation.manyToMany": "hat und gehört zu vielen",
"relation.attributeName.placeholder": "z.B.: Autor, Kategorie"
}

View File

@ -13,6 +13,7 @@
"attribute.email": "Email",
"attribute.password": "Password",
"attribute.relation": "Relation",
"attribute.enumeration": "Enumeration",
"contentType.temporaryDisplay": "(Not saved)",
"from": "from",
@ -44,6 +45,7 @@
"form.attribute.item.number.name": "Name",
"form.attribute.item.date.name": "Name",
"form.attribute.item.media.name": "Name",
"form.attribute.item.media.multiple": "Allow multiple files",
"form.attribute.item.json.name": "Name",
"form.attribute.item.boolean.name": "Name",
"form.attribute.item.string.name": "Name",
@ -102,6 +104,7 @@
"notification.success.message.contentType.edit": "Your Content Type has been updated",
"notification.success.message.contentType.create": "Your Content Type has been created",
"notification.success.contentTypeDeleted": "The Content Type has been deleted",
"notification.info.enumeration": "This field is not editable for the moment...😮",
"popUpForm.attributes.string.description": "Titles, names, paragraphs, list of names",
"popUpForm.attributes.text.description": "Descriptions, text paragraphs, articles ",

View File

@ -13,6 +13,7 @@
"attribute.password": "Mot de passe",
"attribute.email": "Email",
"attribute.relation": "Relation",
"attribute.enumeration": "Enumération",
"contentType.temporaryDisplay": "(Non sauvegardé)",
@ -48,6 +49,7 @@
"form.attribute.item.number.name": "Nom",
"form.attribute.item.json.name": "Nom",
"form.attribute.item.media.name": "Nom",
"form.attribute.item.media.multiple": "Peut avoir plusierus fichiers",
"form.attribute.item.string.name": "Nom",
"form.attribute.item.settings.name": "Paramètres",
"form.attribute.item.requiredField": "Champ obligatoire",
@ -105,6 +107,7 @@
"notification.success.message.contentType.edit": "Votre modèle a bien été modifié",
"notification.success.message.contentType.create": "Votre modèle a bien été créée",
"notification.success.contentTypeDeleted": "Le modèle a bien été supprimé.",
"notification.info.enumeration": "Ce champ n'est pas modifiable pour le moment...😮",
"popUpForm.attributes.string.description": "Titres, noms,...",
"popUpForm.attributes.text.description": "Descriptions, paragraphes texte, articles ",

View File

@ -13,6 +13,7 @@
"attribute.email": "Email",
"attribute.password": "Hasło",
"attribute.relation": "Relacja",
"attribute.enumeration": "Enumeration",
"contentType.temporaryDisplay": "(Nie zapisany)",
"from": "z",
@ -44,6 +45,7 @@
"form.attribute.item.number.name": "Nazwa",
"form.attribute.item.date.name": "Nazwa",
"form.attribute.item.media.name": "Nazwa",
"form.attribute.item.media.multiple": "Allow multiple files",
"form.attribute.item.json.name": "Nazwa",
"form.attribute.item.boolean.name": "Nazwa",
"form.attribute.item.string.name": "Nazwa",
@ -102,6 +104,7 @@
"notification.success.message.contentType.edit": "Model został zmieniony",
"notification.success.message.contentType.create": "Model został utworzony",
"notification.success.contentTypeDeleted": "Model został usunięty",
"notification.info.enumeration": "This field is not editable for the moment...😮",
"popUpForm.attributes.string.description": "Tytuły, nazwy, paragrafy, lista nazwisk",
"popUpForm.attributes.text.description": "Opisy, paragrafy, artykuły ",

View File

@ -13,6 +13,7 @@
"attribute.email": "E-posta",
"attribute.password": "Parola",
"attribute.relation": "İlişki",
"attribute.enumeration": "Enumeration",
"contentType.temporaryDisplay": "(Kaydedilmedi)",
"from": "kimden",
@ -44,6 +45,7 @@
"form.attribute.item.number.name": "İsim",
"form.attribute.item.date.name": "İsim",
"form.attribute.item.media.name": "İsim",
"form.attribute.item.media.multiple": "Allow multiple files",
"form.attribute.item.json.name": "İsim",
"form.attribute.item.boolean.name": "İsim",
"form.attribute.item.string.name": "İsim",
@ -102,6 +104,7 @@
"notification.success.message.contentType.edit": "İçerik Türünüz güncellendi",
"notification.success.message.contentType.create": "İçerik Türünüz oluşturuldu",
"notification.success.contentTypeDeleted": "İçerik Türü silindi",
"notification.info.enumeration": "This field is not editable for the moment...😮",
"popUpForm.attributes.string.description": "Başlıklar, adlar, paragraflar, isim listesi",
"popUpForm.attributes.text.description": "Tanımlar, metin paragrafları, makaleler ",

View File

@ -44,18 +44,23 @@ module.exports = {
const model = source ? _.get(strapi.plugins, [source, 'models', name]) : _.get(strapi.models, name);
// const model = _.get(strapi.models, name);
const attributes = [];
_.forEach(model.attributes, (params, name) => {
const relation = _.find(model.associations, { alias: name });
if (relation) {
params = _.omit(params, ['collection', 'model', 'via']);
params.target = relation.model || relation.collection;
params.key = relation.via;
params.nature = relation.nature;
params.targetColumnName = _.get((params.plugin ? strapi.plugins[params.plugin].models : strapi.models )[params.target].attributes[params.key], 'columnName', '');
if (relation && !_.isArray(_.get(relation, 'related'))) {
if (params.plugin === 'upload' && relation.model || relation.collection === 'file') {
params = {
type: 'media',
multiple: params.collection ? true : false
};
} else {
params = _.omit(params, ['collection', 'model', 'via']);
params.target = relation.model || relation.collection;
params.key = relation.via;
params.nature = relation.nature;
params.targetColumnName = _.get((params.plugin ? strapi.plugins[params.plugin].models : strapi.models )[params.target].attributes[params.key], 'columnName', '');
}
}
attributes.push({
@ -144,6 +149,14 @@ module.exports = {
_.forEach(attributesConfigurable, attribute => {
if (_.has(attribute, 'params.type')) {
attrs[attribute.name] = attribute.params;
if (attribute.params.type === 'media') {
attrs[attribute.name] = {
[attribute.params.multiple ? 'collection' : 'model']: 'file',
via: 'related',
plugin: 'upload'
}
}
} else if (_.has(attribute, 'params.target')) {
const relation = attribute.params;
const attr = {

View File

@ -33,6 +33,11 @@
"type": "string",
"configurable": false,
"required": true
},
"related": {
"collection": "*",
"filter": "field",
"configurable": false
}
}
}

View File

@ -38,6 +38,10 @@
"via": "users",
"plugin": "users-permissions",
"configurable": false
},
"products": {
"collection": "product",
"via": "manager"
}
}
}
}

View File

@ -137,24 +137,28 @@ module.exports = {
// We have to find if they are a model linked to this key
_.forIn(_.omit(models, currentModelName || ''), model => {
_.forIn(model.attributes, attribute => {
if (attribute.hasOwnProperty('via') && attribute.via === key && attribute.hasOwnProperty('collection') && attribute.collection !== '*') {
types.other = 'collection';
Object.keys(model.attributes)
.filter(key => key === association.via)
.forEach(attr => {
const attribute = model.attributes[attr];
// Break loop
return false;
} else if (attribute.hasOwnProperty('model') && attribute.model !== '*') {
types.other = 'model';
if (attribute.hasOwnProperty('via') && attribute.via === key && attribute.hasOwnProperty('collection') && attribute.collection !== '*') {
types.other = 'collection';
// Break loop
return false;
} else if (attribute.hasOwnProperty('collection') || attribute.hasOwnProperty('model')) {
types.other = 'morphTo';
// Break loop
return false;
} else if (attribute.hasOwnProperty('model') && attribute.model !== '*') {
types.other = 'model';
// Break loop
return false;
}
});
// Break loop
return false;
} else if (attribute.hasOwnProperty('collection') || attribute.hasOwnProperty('model')) {
types.other = 'morphTo';
// Break loop
return false;
}
});
});
} else if (association.hasOwnProperty('model')) {
types.current = 'model';
@ -230,12 +234,12 @@ module.exports = {
nature: 'oneMorphToOne',
verbose: 'belongsToMorph'
};
} else if (types.current === 'morphTo' && types.other === 'model') {
} else if (types.current === 'morphTo' && (types.other === 'model' || association.hasOwnProperty('model'))) {
return {
nature: 'manyMorphToOne',
verbose: 'belongsToManyMorph'
};
} else if (types.current === 'morphTo' && types.other === 'collection') {
} else if (types.current === 'morphTo' && (types.other === 'collection' || association.hasOwnProperty('collection'))) {
return {
nature: 'manyMorphToMany',
verbose: 'belongsToManyMorph'
@ -358,6 +362,7 @@ module.exports = {
Object.keys(strapi.plugins[current].models).forEach((entity) => {
Object.keys(strapi.plugins[current].models[entity].attributes).forEach((attribute) => {
const attr = strapi.plugins[current].models[entity].attributes[attribute];
if (
(attr.collection || attr.model || '').toLowerCase() === model.toLowerCase() &&
strapi.plugins[current].models[entity].globalId !== definition.globalId