diff --git a/application/Espo/Controllers/EntityManager.php b/application/Espo/Controllers/EntityManager.php index 858c1f55d5..6e89ce0ab9 100644 --- a/application/Espo/Controllers/EntityManager.php +++ b/application/Espo/Controllers/EntityManager.php @@ -131,5 +131,97 @@ class EntityManager extends \Espo\Core\Controllers\Base return true; } + + public function actionCreateLink($params, $data, $request) + { + if (!$request->isPost()) { + throw new BadRequest(); + } + + $paramList = [ + 'entity', + 'entityForeign', + 'link', + 'linkForeign', + 'label', + 'labelForeign', + 'linkType' + ]; + + $d = array(); + foreach ($paramList as $item) { + if (empty($data[$item])) { + throw new BadRequest(); + } + $d[$item] = filter_var($data[$item], \FILTER_SANITIZE_STRING); + } + + $result = $this->getContainer()->get('entityManagerUtil')->createLink($d); + + if ($result) { + $this->getContainer()->get('dataManager')->rebuild(); + } else { + throw new Error(); + } + + return true; + } + + public function actionUpdateLink($params, $data, $request) + { + if (!$request->isPost()) { + throw new BadRequest(); + } + + $paramList = [ + 'entity', + 'entityForeign', + 'link', + 'linkForeign', + 'label', + 'labelForeign' + ]; + + $d = array(); + foreach ($paramList as $item) { + $d[$item] = filter_var($data[$item], \FILTER_SANITIZE_STRING); + } + + $result = $this->getContainer()->get('entityManagerUtil')->updateLink($d); + + if ($result) { + $this->getContainer()->get('dataManager')->clearCache(); + } else { + throw new Error(); + } + + return true; + } + + public function actionRemoveLink($params, $data, $request) + { + if (!$request->isPost()) { + throw new BadRequest(); + } + + $paramList = [ + 'entity', + 'link', + ]; + $d = array(); + foreach ($paramList as $item) { + $d[$item] = filter_var($data[$item], \FILTER_SANITIZE_STRING); + } + + $result = $this->getContainer()->get('entityManagerUtil')->deleteLink($d); + + if ($result) { + $this->getContainer()->get('dataManager')->clearCache(); + } else { + throw new Error(); + } + + return true; + } } diff --git a/application/Espo/Core/Utils/EntityManager.php b/application/Espo/Core/Utils/EntityManager.php index 8bef32b90e..12bf537fb9 100644 --- a/application/Espo/Core/Utils/EntityManager.php +++ b/application/Espo/Core/Utils/EntityManager.php @@ -221,4 +221,191 @@ class EntityManager { return $this->getMetadata()->get('scopes.' . $name . '.isCustom'); } + + public function createLink(array $params) + { + $linkType = $params['linkType']; + + $entity = $params['entity']; + $link = $params['link']; + $entityForeign = $params['entityForeign']; + $linkForeign = $params['linkForeign']; + + $label = $params['label']; + $labelForeign = $params['labelForeign']; + + if (empty($linkType)) { + throw new Error(); + } + if (empty($entity) || empty($entityForeign)) { + throw new Error(); + } + if (empty($entityForeign) || empty($linkForeign)) { + throw new Error(); + } + + if ($this->getMetadata()->get('entityDefs.' . $entity . '.links.' . $link)) { + throw new Conflict('Link ['.$entity.'::'.$link.'] already exists.'); + } + if ($this->getMetadata()->get('entityDefs.' . $entityForeign . '.links.' . $linkForeign)) { + throw new Conflict('Link ['.$entityForeign.'::'.$linkForeign.'] already exists.'); + } + + switch ($linkType) { + case 'oneToMany': + if ($this->getMetadata()->get('entityDefs.' . $entityForeign . '.field.' . $linkForeign)) { + throw new Conflict('Field ['.$entityForeign.'::'.$linkForeign.'] already exists.'); + } + $dataLeft = array( + 'links' => array( + $link => array( + 'type' => 'hasMany', + 'foreign' => $linkForeign, + 'entity' => $entityForeign, + 'isCustom' => true + ) + ) + ); + $dataRight = array( + 'fields' => array( + $linkForeign => array( + 'type' => 'link' + ) + ), + 'links' => array( + $linkForeign => array( + 'type' => 'belongsTo', + 'foreign' => $link, + 'entity' => $entity, + 'isCustom' => true + ) + ) + ); + break; + case 'manyToOne': + if ($this->getMetadata()->get('entityDefs.' . $entity . '.field.' . $link)) { + throw new Conflict('Field ['.$entity.'::'.$link.'] already exists.'); + } + $dataLeft = array( + 'fields' => array( + $link => array( + 'type' => 'link' + ) + ), + 'links' => array( + $link => array( + 'type' => 'belongsTo', + 'foreign' => $linkForeign, + 'entity' => $entityForeign, + 'isCustom' => true + ) + ) + ); + $dataRight = array( + 'links' => array( + $linkForeign => array( + 'type' => 'hasMany', + 'foreign' => $link, + 'entity' => $entity, + 'isCustom' => true + ) + ) + ); + break; + case 'manyToMany': + $dataLeft = array( + 'links' => array( + $link => array( + 'type' => 'hasMany', + 'foreign' => $linkForeign, + 'entity' => $entityForeign, + 'isCustom' => true + ) + ) + ); + $dataRight = array( + 'links' => array( + $linkForeign => array( + 'type' => 'hasMany', + 'foreign' => $link, + 'entity' => $entity, + 'isCustom' => true + ) + ) + ); + break; + } + + $this->getMetadata()->set('entityDefs', $entity, $dataLeft); + $this->getMetadata()->set('entityDefs', $entityForeign, $dataRight); + $this->getMetadata()->save(); + + $this->getLanguage()->set($entity, 'fields', $link, $label); + $this->getLanguage()->set($entity, 'links', $link, $label); + $this->getLanguage()->set($entityForeign, 'fields', $linkForeign, $labelForeign); + $this->getLanguage()->set($entityForeign, 'links', $linkForeign, $labelForeign); + + $this->getLanguage()->save(); + + return true; + } + + public function updateLink(array $params) + { + $entity = $params['entity']; + $link = $params['link']; + $entityForeign = $params['entityForeign']; + $linkForeign = $params['linkForeign']; + + $label = $params['label']; + $labelForeign = $params['labelForeign']; + + if (empty($entity) || empty($entityForeign)) { + throw new Error(); + } + if (empty($entityForeign) || empty($linkForeign)) { + throw new Error(); + } + + $this->getLanguage()->set($entity, 'fields', $link, $label); + $this->getLanguage()->set($entity, 'links', $link, $label); + $this->getLanguage()->set($entityForeign, 'fields', $linkForeign, $labelForeign); + $this->getLanguage()->set($entityForeign, 'links', $linkForeign, $labelForeign); + + $this->getLanguage()->save(); + + return true; + } + + public function deleteLink(array $params) + { + $entity = $params['entity']; + $link = $params['link']; + + if (!$this->getMetadata()->get("entityDefs.{$entity}.links.{$link}.isCustom")) { + throw new Error(); + } + + $entityForeign = $this->getMetadata()->get("entityDefs.{$entity}.links.{$link}.entity"); + $linkForeign = $this->getMetadata()->get("entityDefs.{$entity}.links.{$link}.foreign"); + + if (empty($entity) || empty($entityForeign)) { + throw new Error(); + } + if (empty($entityForeign) || empty($linkForeign)) { + throw new Error(); + } + + $this->getMetadata()->delete('entityDefs', $entity, array( + 'fields.' . $link, + 'links.' . $link + )); + $this->getMetadata()->delete('entityDefs', $entityForeign, array( + 'fields.' . $linkForeign, + 'links.' . $linkForeign + )); + $this->getMetadata()->save(); + + return true; + } } diff --git a/application/Espo/Resources/i18n/en_US/EntityManager.json b/application/Espo/Resources/i18n/en_US/EntityManager.json index 19bf5e6503..0374a09879 100644 --- a/application/Espo/Resources/i18n/en_US/EntityManager.json +++ b/application/Espo/Resources/i18n/en_US/EntityManager.json @@ -25,10 +25,13 @@ "linkType": { "manyToMany": "Many-to-Many", "oneToMany": "One-to-Many", - "manyToOne": "Many-to-One" + "manyToOne": "Many-to-One", + "parentToChildren": "Parent-to-Children", + "childrenToParent": "Children-to-Parent" } }, "messages": { - "entityCreated": "Entity has been created" + "entityCreated": "Entity has been created", + "linkAlreadyExists": "Conflict: link already exists." } } diff --git a/frontend/client/res/templates/admin/link-manager/index.tpl b/frontend/client/res/templates/admin/link-manager/index.tpl index a61c02709c..8cf3a1c044 100644 --- a/frontend/client/res/templates/admin/link-manager/index.tpl +++ b/frontend/client/res/templates/admin/link-manager/index.tpl @@ -16,8 +16,15 @@
| + {{translate 'No Data'}} + | +||
| {{translate entity category='scopeNames'}} | @@ -36,15 +43,13 @@ {{translate entityForeign category='scopeNames'}}- {{#if isCustom}} - + {{translate 'Edit'}} - {{/if}} | {{#if isCustom}} - + {{translate 'Remove'}} {{/if}} diff --git a/frontend/client/src/utils.js b/frontend/client/src/utils.js index b9655a9b44..926049232a 100644 --- a/frontend/client/src/utils.js +++ b/frontend/client/src/utils.js @@ -114,6 +114,13 @@ return Espo.Utils.convert(string, 'c-h').split('.').join('-'); }, + lowerCaseFirst: function (string) { + if (string == null) { + return string; + } + return string.charAt(0).toLowerCase() + string.slice(1); + }, + upperCaseFirst: function (string) { if (string == null) { return string; diff --git a/frontend/client/src/views/admin/link-manager/index.js b/frontend/client/src/views/admin/link-manager/index.js index 6a9f8b41a0..0299feebf8 100644 --- a/frontend/client/src/views/admin/link-manager/index.js +++ b/frontend/client/src/views/admin/link-manager/index.js @@ -52,6 +52,34 @@ Espo.define('Views.Admin.LinkManager.Index', 'View', function (Dep) { } }, + computeRelationshipType: function (type, foreignType) { + if (type == 'hasMany') { + if (foreignType == 'hasMany') { + return 'manyToMany'; + } else if (foreignType == 'belongsTo') { + return 'oneToMany'; + } else { + return; + } + } else if (type == 'belongsTo') { + if (foreignType == 'hasMany') { + return 'manyToOne'; + } else { + return; + } + } else if (type == 'belongsToParent') { + if (foreignType == 'hasChildren') { + return 'childrenToParent' + } + return; + } else if (type == 'hasChildren') { + if (foreignType == 'belongsToParent') { + return 'parentToChildren' + } + return; + } + }, + setupLinkData: function () { this.linkDataList = []; @@ -68,25 +96,9 @@ Espo.define('Views.Admin.LinkManager.Index', 'View', function (Dep) { var foreignType = this.getMetadata().get('entityDefs.' + d.entity + '.links.' + d.foreign + '.type'); - var type; + var type = this.computeRelationshipType(d.type, foreignType); - if (d.type == 'hasMany') { - if (foreignType == 'hasMany') { - type = 'manyToMany'; - } else if (foreignType == 'belongsTo') { - type = 'oneToMany'; - } else { - return; - } - } else if (d.type == 'belongsTo') { - if (foreignType == 'hasMany') { - type = 'manyToOne'; - } else { - return; - } - } else { - return; - } + if (!type) return; this.linkDataList.push({ link: link, @@ -140,12 +152,12 @@ Espo.define('Views.Admin.LinkManager.Index', 'View', function (Dep) { }.bind(this)); }, - removeEntity: function (link) { + removeLink: function (link) { $.ajax({ url: 'EntityManager/action/removeLink', type: 'POST', data: JSON.stringify({ - scope: this.scope, + entity: this.scope, link: link }) }).done(function () { diff --git a/frontend/client/src/views/admin/link-manager/modals/edit.js b/frontend/client/src/views/admin/link-manager/modals/edit.js index 52d78c1425..a43cd25f3b 100644 --- a/frontend/client/src/views/admin/link-manager/modals/edit.js +++ b/frontend/client/src/views/admin/link-manager/modals/edit.js @@ -19,7 +19,7 @@ * along with EspoCRM. If not, see http://www.gnu.org/licenses/. ************************************************************************/ -Espo.define('Views.Admin.LinkManager.Modals.Edit', 'Views.Modal', function (Dep) { +Espo.define('Views.Admin.LinkManager.Modals.Edit', ['Views.Modal', 'Views.Admin.LinkManager.Index'], function (Dep, Index) { return Dep.extend({ @@ -50,11 +50,13 @@ Espo.define('Views.Admin.LinkManager.Modals.Edit', 'Views.Modal', function (Dep) var scope = this.scope = this.options.scope; var link = this.link = this.options.link || false; + var entity = scope; + var isNew = this.isNew = (false == link); var header = 'Create Link'; - if (link) { - header = 'Edit Entity'; + if (!isNew) { + header = 'Edit Link'; } this.header = this.translate(header, 'labels', 'Admin'); @@ -62,11 +64,28 @@ Espo.define('Views.Admin.LinkManager.Modals.Edit', 'Views.Modal', function (Dep) var model = this.model = new Espo.Model(); model.name = 'EntityManager'; + this.model.set('entity', scope); - if (scope) { - this.model.set('entity', scope); + if (!isNew) { + var entityForeign = this.getMetadata().get('entityDefs.' + scope + '.links.' + link + '.entity'); + var linkForeign = this.getMetadata().get('entityDefs.' + scope + '.links.' + link + '.foreign'); + var label = this.getLanguage().translate(link, 'links', scope); + var labelForeign = this.getLanguage().translate(linkForeign, 'links', entityForeign); + + var type = this.getMetadata().get('entityDefs.' + entity + '.links.' + link + '.type'); + var foreignType = this.getMetadata().get('entityDefs.' + entityForeign + '.links.' + linkForeign + '.type'); + + var linkType = Index.prototype.computeRelationshipType.call(this, type, foreignType); + + this.model.set('linkType', linkType); + this.model.set('entityForeign', entityForeign); + this.model.set('link', link); + this.model.set('linkForeign', linkForeign); + this.model.set('label', label); + this.model.set('labelForeign', labelForeign); } + var scopes = this.getMetadata().get('scopes') || null; var entityList = (Object.keys(scopes) || []).filter(function (item) { var d = scopes[item]; @@ -77,6 +96,8 @@ Espo.define('Views.Admin.LinkManager.Modals.Edit', 'Views.Modal', function (Dep) return t1.localeCompare(t2); }.bind(this)); + entityList.unshift(''); + this.createView('entity', 'Fields.Varchar', { model: model, @@ -109,7 +130,7 @@ Espo.define('Views.Admin.LinkManager.Modals.Edit', 'Views.Modal', function (Dep) name: 'linkType', params: { required: true, - options: ['', 'oneToMany', 'ManyToOne', 'ManyToMany'] + options: ['', 'oneToMany', 'manyToOne', 'manyToMany'] } }, readOnly: !isNew @@ -163,22 +184,66 @@ Espo.define('Views.Admin.LinkManager.Modals.Edit', 'Views.Modal', function (Dep) }); }, + toPlural: function (string) { + if (string.slice(-1) == 'y') { + return string.substr(0, string.length - 1) + 'ies'; + } else { + return string + 's'; + } + + }, + populateFields: function () { var entityForeign = this.model.get('entityForeign'); + var linkType = this.model.get('linkType'); - name = entityForeign.charAt(0).toUpperCase() + name.slice(1); + if (!entityForeign || !linkType) { + this.model.set('link', ''); + this.model.set('linkForeign', ''); - this.model.set('labelSingular', name); - this.model.set('labelPlural', name + 's') ; - if (name) { - name = name.replace(/\-/g, ' ').replace(/_/g, ' ').replace(/[^\w\s]/gi, '').replace(/ (.)/g, function (match, g) { + this.model.set('label', ''); + this.model.set('labelForeign', ''); + return; + } + + var link; + var linkForeign; + + switch (linkType) { + case 'oneToMany': + linkForeign = Espo.Utils.lowerCaseFirst(this.scope); + link = this.toPlural(Espo.Utils.lowerCaseFirst(entityForeign)) + break; + case 'manyToOne': + linkForeign = this.toPlural(Espo.Utils.lowerCaseFirst(this.scope)); + link = Espo.Utils.lowerCaseFirst(entityForeign); + break; + case 'manyToMany': + linkForeign = this.toPlural(Espo.Utils.lowerCaseFirst(this.scope)); + link = this.toPlural(Espo.Utils.lowerCaseFirst(entityForeign)); + break; + } + + this.model.set('link', link); + this.model.set('linkForeign', linkForeign); + + this.model.set('label', Espo.Utils.upperCaseFirst(link)); + this.model.set('labelForeign', Espo.Utils.upperCaseFirst(linkForeign)); + + return; + }, + + handleLinkChange: function (field) { + var value = this.model.get(field); + if (value) { + value = value.replace(/\-/g, ' ').replace(/_/g, ' ').replace(/[^\w\s]/gi, '').replace(/ (.)/g, function (match, g) { return g.toUpperCase(); }).replace(' ', ''); - if (name.length) { - name = name.charAt(0).toUpperCase() + name.slice(1); + if (value.length) { + value = Espo.Utils.lowerCaseFirst(value); } } - this.model.set('name', name); + this.model.set(field, value); }, afterRender: function () { @@ -188,6 +253,13 @@ Espo.define('Views.Admin.LinkManager.Modals.Edit', 'Views.Modal', function (Dep) this.getView('entityForeign').on('change', function (m) { this.populateFields(); }, this); + + this.getView('link').on('change', function (m) { + this.handleLinkChange('link'); + }, this); + this.getView('linkForeign').on('change', function (m) { + this.handleLinkChange('linkForeign'); + }, this); }, save: function () { @@ -221,23 +293,34 @@ Espo.define('Views.Admin.LinkManager.Modals.Edit', 'Views.Modal', function (Dep) this.$el.find('button[data-name="save"]').addClass('disabled'); var url = 'EntityManager/action/createLink'; - if (this.scope) { + if (!this.isNew) { url = 'EntityManager/action/updateLink'; } + var entity = this.scope; + var entityForeign = this.model.get('entityForeign'); + var link = this.model.get('link'); + var linkForeign = this.model.get('linkForeign'); + var label = this.model.get('label'); + var labelForeign = this.model.get('labelForeign'); + $.ajax({ url: url, type: 'POST', data: JSON.stringify({ - scope: this.scope, - entityForeign: this.model.get('entityForeign'), - link: this.model.get('link'), - linkForeign: this.model.get('linkForeign'), - labelSingular: this.model.get('label'), - labelPlural: this.model.get('labelForeign'), - linkType: this.model.get('linkType'), + entity: entity, + entityForeign: entityForeign, + link: link, + linkForeign: linkForeign, + label: label, + labelForeign: labelForeign, + linkType: this.model.get('linkType') }), - error: function () { + error: function (x) { + if (x.status == 409) { + Espo.Ui.error(this.translate('linkAlreadyExists', 'messages', 'EntityManager')); + x.errorIsHandled = true; + } this.$el.find('button[data-name="save"]').removeClass('disabled'); }.bind(this) }).done(function () { @@ -246,9 +329,16 @@ Espo.define('Views.Admin.LinkManager.Modals.Edit', 'Views.Modal', function (Dep) } else { Espo.Ui.success(this.translate('Created')); } - /*var global = ((this.getLanguage().data || {}) || {}).Global; - (global.scopeNames || {})[name] = this.model.get('labelSingular'); - (global.scopeNamesPlural || {})[name] = this.model.get('labelPlural');*/ + + var data; + + data = ((this.getLanguage().data || {}) || {})[entity]; + (data.fields || {})[link] = label; + (data.links || {})[link] = label; + + data = ((this.getLanguage().data || {}) || {})[entityForeign]; + (data.fields || {})[linkForeign] = labelForeign; + (data.links || {})[linkForeign] = labelForeign; this.getMetadata().load(function () { this.trigger('after:save'); |