diff --git a/lib/Alchemy/Phrasea/Controller/Admin/Fields.php b/lib/Alchemy/Phrasea/Controller/Admin/Fields.php index 81e2de8af7..3968909c39 100644 --- a/lib/Alchemy/Phrasea/Controller/Admin/Fields.php +++ b/lib/Alchemy/Phrasea/Controller/Admin/Fields.php @@ -40,6 +40,10 @@ class Fields implements ControllerProviderInterface ->assert('sbas_id', '\d+') ->bind('admin_fields'); + $controllers->put('/{sbas_id}/fields', 'admin.fields.controller:updateFields') + ->assert('sbas_id', '\d+') + ->bind('admin_fields_register'); + $controllers->get('/{sbas_id}/fields', 'admin.fields.controller:listFields') ->assert('sbas_id', '\d+') ->bind('admin_fields_list'); @@ -84,20 +88,94 @@ class Fields implements ControllerProviderInterface return $controllers; } - public function getLanguage(Application $app) { + public function updateFields(Application $app, Request $request, $sbas_id) + { + $json = array( + 'success' => false, + // use to store the updated collection + 'fields' => array(), + 'messages' => array() + ); + + $databox = $app['phraseanet.appbox']->get_databox((int) $sbas_id); + $connection = $databox->get_connection(); + $data = $this->getFieldsJsonFromRequest($app, $request); + + // calculate max position + try { + $stmt = $connection->prepare('SELECT MAX(sorter) as max_position FROM metadatas_structure'); + $stmt->execute(); + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + $stmt->closeCursor(); + $maxPosition = $row['max_position'] + 1; + } catch (\PDOException $e) { + $app->abort(500); + } + + $connection->beginTransaction(); + $i = 0; + foreach ($data as $jsonField) { + try { + $jsonField['sorter'] = $jsonField['sorter'] + $maxPosition; + $field = \databox_field::get_instance($app, $databox, $jsonField['id']); + $this->updateFieldWithData($app, $field, $jsonField); + $field->save(); + $json['fields'][] = $field->toArray(); + $i++; + } catch (\PDOException $e) { + if ($e->errorInfo[1] == 1062) { + $json['messages'][] = _(sprintf('Field name %s already exists', $jsonField['name'])); + } else { + $json['messages'][] = _(sprintf('Field %s could not be saved, please retry or contact an administrator if problem persists', $jsonField['name'])); + } + } catch (\Exception $e) { + if ($e instanceof \Exception_Databox_metadataDescriptionNotFound || $e->getPrevious() instanceof TagUnknown) { + $json['messages'][] = _(sprintf('Provided tag %s is unknown', $jsonField['tag'])); + } else { + $json['messages'][] = _(sprintf('Field %s could not be saved, please retry or contact an administrator if problem persists', $jsonField['name'])); + } + } + } + + if ($i === count($data)) { + // update field position in database, this query forces to update all fields each time + $stmt = $connection->prepare(sprintf('UPDATE metadatas_structure SET sorter = (sorter - %s)', $maxPosition)); + $row = $stmt->execute(); + $stmt->closeCursor(); + + $connection->commit(); + + $json['success'] = true; + $json['messages'][] = _('Fields configuration has been saved'); + + // update field position in array + array_walk($json['fields'], function(&$field) use ($maxPosition) { + $field['sorter'] = $field['sorter'] - $maxPosition; + }); + } else { + $connection->rollback(); + } + + return $app->json($json); + } + + public function getLanguage(Application $app, Request $request) + { return $app->json(array( - 'something_wrong' => _('Something wrong happened, please try again or contact an admin if problem persists'), - 'created_success' => _('%s field has been created with success'), - 'deleted_success' => _('%s field has been deleted with success'), - 'are_you_sure_delete' => _('Do you really want to delete the field %s ?'), - 'validation_blank' => _('Field can not be blank'), + 'something_wrong' => _('Something wrong happened, please try again or contact an admin if problem persists'), + 'deleted_success' => _('%s field has been deleted with success'), + 'are_you_sure_delete' => _('Do you really want to delete the field %s ?'), + 'validation_blank' => _('Field can not be blank'), + 'validation_name_exists' => _('Field name already exists'), + 'validation_tag_invalid' => _('Field source is not valid'), + 'field_error' => _('Field %s contains errors'), )); } - public function displayApp(Application $app, $sbas_id) { + public function displayApp(Application $app, Request $request, $sbas_id) + { return $app['twig']->render('/admin/fields/index.html.twig', array( - 'sbas_id' => $sbas_id, - 'js' => '' + 'sbas_id' => $sbas_id )); } @@ -192,9 +270,9 @@ class Fields implements ControllerProviderInterface $json['field'] = $field->toArray(); } catch (\PDOException $e) { if ($e->errorInfo[1] == 1062) { - $json['message'] = _(sprintf('Field name %s already exists', $data['name'])); + $json['message'] = _(sprintf('Field name %s already exists', $data['name'])); } - } catch (\Exception $e) { + } catch (\Exception $e) { if ($e instanceof \Exception_Databox_metadataDescriptionNotFound || $e->getPrevious() instanceof TagUnknown) { $json['message'] = _(sprintf('Provided tag %s is unknown', $data['tag'])); } @@ -246,11 +324,7 @@ class Fields implements ControllerProviderInterface $app->abort(400, 'Body must contain a valid JSON payload'); } - $required = array( - 'name', 'multi', 'thumbtitle', 'tag', 'business', 'indexable', - 'required', 'separator', 'readonly', 'type', 'tbranch', 'report', - 'vocabulary-type', 'vocabulary-restricted', 'dces-element' - ); + $required = $this->getMandatoryFieldProperties(); foreach ($required as $key) { if (false === array_key_exists($key, $data)) { @@ -261,9 +335,32 @@ class Fields implements ControllerProviderInterface return $data; } + private function getFieldsJsonFromRequest(Application $app, Request $request) + { + $body = $request->getContent(); + $data = @json_decode($body, true); + + if (JSON_ERROR_NONE !== json_last_error()) { + $app->abort(400, 'Body must contain a valid JSON payload'); + } + + $required = $this->getMandatoryFieldProperties(); + + foreach($data as $field) { + foreach ($required as $key) { + if (false === array_key_exists($key, $field)) { + $app->abort(400, sprintf('The entity must contain a key `%s`', $key)); + } + } + } + + return $data; + } + private function updateFieldWithData(Application $app, \databox_field $field, array $data) { $field + ->set_name($data['name']) ->set_thumbtitle($data['thumbtitle']) ->set_tag(\databox_field::loadClassFromTagName($data['tag'])) ->set_business($data['business']) @@ -277,6 +374,10 @@ class Fields implements ControllerProviderInterface ->setVocabularyControl(null) ->setVocabularyRestricted(false); + if (isset($data['sorter'])) { + $field->set_position($data['sorter']); + } + try { $vocabulary = VocabularyController::get($app, $data['vocabulary-type']); $field->setVocabularyControl($vocabulary); @@ -287,11 +388,20 @@ class Fields implements ControllerProviderInterface $dces_element = null; - $class = 'databox_Field_DCES_' . $data['dces-element']; + $class = '\databox_Field_DCES_' . $data['dces-element']; if (class_exists($class)) { $dces_element = new $class(); } $field->set_dces_element($dces_element); } + + private function getMandatoryFieldProperties() + { + return array( + 'name', 'multi', 'thumbtitle', 'tag', 'business', 'indexable', + 'required', 'separator', 'readonly', 'type', 'tbranch', 'report', + 'vocabulary-type', 'vocabulary-restricted', 'dces-element' + ); + } } diff --git a/lib/classes/databox/field.php b/lib/classes/databox/field.php index 347bc9aefd..048dbee215 100644 --- a/lib/classes/databox/field.php +++ b/lib/classes/databox/field.php @@ -350,6 +350,7 @@ class databox_field implements cache_cacheableInterface `report` = :report, `type` = :type, `tbranch` = :tbranch, + `sorter` = :position, `thumbtitle` = :thumbtitle, `VocabularyControlType` = :VocabularyControlType, `RestrictToVocabularyControl` = :RestrictVocab @@ -367,6 +368,7 @@ class databox_field implements cache_cacheableInterface ':report' => $this->report ? '1' : '0', ':type' => $this->type, ':tbranch' => $this->tbranch, + ':position' => $this->position, ':thumbtitle' => $this->thumbtitle, ':VocabularyControlType' => $this->Vocabulary ? $this->Vocabulary->getType() : null, ':RestrictVocab' => $this->Vocabulary ? ($this->VocabularyRestriction ? '1' : '0') : '0', @@ -417,6 +419,7 @@ class databox_field implements cache_cacheableInterface } $meta->setAttribute('thumbtitle', $this->thumbtitle); $meta->setAttribute('meta_id', $this->id); + $meta->setAttribute('sorter', $this->position); $this->delete_data_from_cache(); $this->databox->saveStructure($dom_struct); @@ -798,6 +801,24 @@ class databox_field implements cache_cacheableInterface { return $this->name; } + /** + * + * @return string + */ + public function get_position() + { + return $this->position; + } + /** + * + * @return string + */ + public function set_position($position) + { + $this->position = $position; + + return $this; + } /** * Return true is the field is unknown @@ -827,7 +848,7 @@ class databox_field implements cache_cacheableInterface 'readonly' => $this->readonly, 'multi' => $this->multi, 'indexable' => $this->indexable, - 'dces-element' => $this->dces_element, + 'dces-element' => $this->dces_element ? $this->dces_element->get_label(): null, 'vocabulary-type' => $this->Vocabulary ? $this->Vocabulary->getType() : null, 'vocabulary-restricted' => $this->VocabularyRestriction, ); diff --git a/templates/web/admin/fields/index.html.twig b/templates/web/admin/fields/index.html.twig index a4c3eda609..80c6c171b1 100644 --- a/templates/web/admin/fields/index.html.twig +++ b/templates/web/admin/fields/index.html.twig @@ -1,20 +1,25 @@ +{# include js templates #} {% include 'admin/fields/templates.html.twig' %}
+ {# sbas_id is saved in the dom and used to fetch right models and collections #} -
-
- +
+
+ {# set loading state, this will be removed once backbone application is fully loaded #} + + {% trans %}Loading database documentary fields ...{% endtrans %}
-
+
-
-
-
+
-{# bootstrap admin field application #} + +{# bootstrap admin field backbone application #} diff --git a/templates/web/admin/fields/templates.html.twig b/templates/web/admin/fields/templates.html.twig index 479137c158..78467ce9f5 100644 --- a/templates/web/admin/fields/templates.html.twig +++ b/templates/web/admin/fields/templates.html.twig @@ -3,7 +3,13 @@ <%= msg %> - + + - + + diff --git a/tests/Alchemy/Tests/Phrasea/Controller/Admin/FieldsTest.php b/tests/Alchemy/Tests/Phrasea/Controller/Admin/FieldsTest.php index abfed82d78..8e22f2be75 100644 --- a/tests/Alchemy/Tests/Phrasea/Controller/Admin/FieldsTest.php +++ b/tests/Alchemy/Tests/Phrasea/Controller/Admin/FieldsTest.php @@ -7,6 +7,25 @@ use Alchemy\Phrasea\Vocabulary\Controller as VocabularyController; class ControllerFieldsTest extends \PhraseanetWebTestCaseAuthenticatedAbstract { + public function testRoot() + { + $databoxes = self::$DI['app']['phraseanet.appbox']->get_databoxes(); + $databox = array_shift($databoxes); + + self::$DI['client']->request("GET", "/admin/fields/" . $databox->get_sbas_id()); + + $this->assertTrue(self::$DI['client']->getResponse()->isOk()); + } + + public function testLanguage() + { + self::$DI['client']->request("GET", "/admin/fields/language.json"); + $response = self::$DI['client']->getResponse(); + + $this->assertTrue($response->isOk()); + $this->assertEquals("application/json", $response->headers->get("content-type")); + } + public function testGetTag() { $tag = new ObjectName(); @@ -100,6 +119,99 @@ class ControllerFieldsTest extends \PhraseanetWebTestCaseAuthenticatedAbstract } } + public function testUpdateFields() + { + $databoxes = self::$DI['app']['phraseanet.appbox']->get_databoxes(); + $databox = array_shift($databoxes); + $fieldObjects = array(); + // create two fields + $fields = array( + array( + 'sbas-id' => $databox->get_sbas_id(), + 'name' => 'testfield' . mt_rand(), + 'multi' => true, + 'thumbtitle' => false, + 'tag' => 'XMP:XMP', + 'business' => false, + 'indexable' => true, + 'required' => true, + 'separator' => '=;', + 'readonly' => false, + 'type' => 'string', + 'tbranch' => '', + 'report' => true, + 'dces-element' => null, + 'vocabulary-type' => null, + 'vocabulary-restricted' => false, + ), array( + 'sbas-id' => $databox->get_sbas_id(), + 'name' => 'testfield' . mt_rand(), + 'multi' => true, + 'thumbtitle' => false, + 'tag' => 'XMP:XMP', + 'business' => false, + 'indexable' => true, + 'required' => true, + 'separator' => '=;', + 'readonly' => false, + 'type' => 'string', + 'tbranch' => '', + 'report' => true, + 'dces-element' => null, + 'vocabulary-type' => null, + 'vocabulary-restricted' => false, + )); + + foreach($fields as $fieldData) { + $field = \databox_field::create(self::$DI['app'], $databox, $fieldData['name'], $fieldData['multi']); + $field + ->set_thumbtitle($fieldData['thumbtitle']) + ->set_tag(\databox_field::loadClassFromTagName($fieldData['tag'])) + ->set_business($fieldData['business']) + ->set_indexable($fieldData['indexable']) + ->set_required($fieldData['required']) + ->set_separator($fieldData['separator']) + ->set_readonly($fieldData['readonly']) + ->set_type($fieldData['type']) + ->set_tbranch($fieldData['tbranch']) + ->set_report($fieldData['report']) + ->setVocabularyControl(null) + ->setVocabularyRestricted(false); + $field->save(); + $fieldObjects[] = $field; + } + + // get body + $body = $databox->get_meta_structure()->toArray(); + + // change some body data + $body[count($body) - 2]['business'] = true; + $body[count($body) - 2]['indexable'] = false; + $body[count($body) - 1]['readonly'] = true; + $body[count($body) - 1]['required'] = false; + + self::$DI['client']->request("PUT", sprintf("/admin/fields/%d/fields", $databox->get_sbas_id()), array(), array(), array(), json_encode($body)); + + $response = self::$DI['client']->getResponse()->getContent(); + + $this->assertEquals("application/json", self::$DI['client']->getResponse()->headers->get("content-type")); + + $data = json_decode($response, true); + + $this->assertArrayHasKey('success', $data); + $this->assertArrayHasKey('messages', $data); + $this->assertArrayHasKey('fields', $data); + + // expect last 2 fields from body equals last 2 fields from response + $this->assertEquals(array_splice($body, -2), array_splice($data['fields'], -2)); + + // delete created fields + foreach($fieldObjects as $field) { + $field->delete(); + } + + } + public function testCreateField() { $databoxes = self::$DI['app']['phraseanet.appbox']->get_databoxes(); diff --git a/www/scripts/apps/admin/fields/app.js b/www/scripts/apps/admin/fields/app.js index 1d911ff724..49606f80e0 100644 --- a/www/scripts/apps/admin/fields/app.js +++ b/www/scripts/apps/admin/fields/app.js @@ -1,47 +1,83 @@ define([ - 'jquery', - 'underscore', - 'backbone', - 'i18n', - 'apps/admin/fields/collections/fields', - 'apps/admin/fields/collections/vocabularies', - 'apps/admin/fields/collections/dcFields', - 'apps/admin/fields/views/list' -], function($, _, Backbone, i18n, FieldsCollection, VocabulariesCollection, DcFieldsCollection, FieldListView) { + "jquery", + "underscore", + "backbone", + "i18n", + "apps/admin/fields/collections/fields", + "apps/admin/fields/collections/vocabularies", + "apps/admin/fields/collections/dcFields", + "apps/admin/fields/views/list", + "apps/admin/fields/views/save", + "apps/admin/fields/views/fieldError", + "apps/admin/fields/errors/errorManager" +], function( + $, _, Backbone, i18n, FieldsCollection, VocabulariesCollection, + DcFieldsCollection, FieldListView, SaveView, FieldErrorView, ErrorManager) { var initialize = function() { - window.AdminFieldApp = {}; + AdminFieldApp = { + $window : $(window), + $scope : $("#admin-field-app"), + $top : $(".row-top", this.$scope), + $bottom : $(".row-bottom", this.$scope), + $leftBlock : $(".left-block", this.$bottom), + $rightBlock : $(".right-block", this.$bottom), + resizeListBlock: function () { + var listBlock = $(".list-block", AdminFieldApp.$leftBlock); + listBlock.height(AdminFieldApp.$window.height() - listBlock.offset().top - 10); + }, + resize: function () { + AdminFieldApp.resizeListBlock(); + AdminFieldApp.$rightBlock.height(AdminFieldApp.$window.height() - AdminFieldApp.$rightBlock.offset().top - 10); + } + }; - window.AdminFieldApp.sbas_id = $('input[name=current_sbas_id]').val(); + // bind resize + AdminFieldApp.$window.bind("resize", AdminFieldApp.resize); - var fieldsCollection = new FieldsCollection(null, { - sbas_id : window.AdminFieldApp.sbas_id + // current sbas id + AdminFieldApp.sbas_id = $("input[name=current_sbas_id]", AdminFieldApp.scope).val(); + + // global errors + AdminFieldApp.errorManager = new ErrorManager(); + _.extend(AdminFieldApp.errorManager, Backbone.Events); + + // initiliaze collections + AdminFieldApp.fieldsCollection = new FieldsCollection(null, { + sbas_id : AdminFieldApp.sbas_id }); + AdminFieldApp.vocabularyCollection = new VocabulariesCollection(); + AdminFieldApp.dcFieldsCollection = new DcFieldsCollection(); - var vocabulariesCollection = new VocabulariesCollection(); - var dcFieldsCollection = new DcFieldsCollection(); + // load strings + i18n.init({ resGetPath: "/admin/fields/language.json"}); - // load strings synchronously - i18n.init({ resGetPath: '/admin/fields/language.json', getAsync: false }); - - var requests = [ - fieldsCollection.fetch(), - vocabulariesCollection.fetch(), - dcFieldsCollection.fetch() - ]; - - $.when.apply($, requests).done( + // load all collections + $.when.apply($, [ + AdminFieldApp.fieldsCollection.fetch(), + AdminFieldApp.vocabularyCollection.fetch(), + AdminFieldApp.dcFieldsCollection.fetch() + ]).done( function() { - window.AdminFieldApp.vocabularyCollection = vocabulariesCollection; - window.AdminFieldApp.dcFieldsCollection = dcFieldsCollection; - - window.AdminFieldApp.fieldListView = new FieldListView({ - collection: fieldsCollection, - el: $('.left-block')[0] + // register views + AdminFieldApp.saveView = new SaveView({ + el: $(".save-block", AdminFieldApp.scope) }); + AdminFieldApp.fieldErrorView = new FieldErrorView(); + AdminFieldApp.fieldListView = new FieldListView({ + collection: AdminFieldApp.fieldsCollection, + el: AdminFieldApp.$leftBlock + }); + // render views + AdminFieldApp.saveView.render(); + AdminFieldApp.fieldListView.render(); + + // show bottom + AdminFieldApp.$bottom.removeClass("hidden"); + + AdminFieldApp.$window.trigger("resize"); - window.AdminFieldApp.fieldListView.render(); // click on first item list - _.first(window.AdminFieldApp.fieldListView.itemViews).clickAction().animate(); + _.first(AdminFieldApp.fieldListView.itemViews).clickAction().animate(); } ); }; diff --git a/www/scripts/apps/admin/fields/collections/dcFields.js b/www/scripts/apps/admin/fields/collections/dcFields.js index e4837e013b..f3478d16ff 100644 --- a/www/scripts/apps/admin/fields/collections/dcFields.js +++ b/www/scripts/apps/admin/fields/collections/dcFields.js @@ -1,12 +1,12 @@ define([ - 'underscore', - 'backbone', - 'models/dcField' + "underscore", + "backbone", + "models/dcField" ], function(_, Backbone, DcFieldModel) { var DcFieldCollection = Backbone.Collection.extend({ model: DcFieldModel, url: function() { - return '/admin/fields/dc-fields'; + return "/admin/fields/dc-fields"; }, comparator: function(item) { return item.get("label"); diff --git a/www/scripts/apps/admin/fields/collections/fields.js b/www/scripts/apps/admin/fields/collections/fields.js index 4326571c0f..7e97779abd 100644 --- a/www/scripts/apps/admin/fields/collections/fields.js +++ b/www/scripts/apps/admin/fields/collections/fields.js @@ -1,7 +1,7 @@ define([ - 'underscore', - 'backbone', - 'models/field' + "underscore", + "backbone", + "models/field" ], function(_, Backbone, FieldModel) { var FieldCollection = Backbone.Collection.extend({ initialize: function(models, options) { @@ -12,7 +12,7 @@ define([ }, model: FieldModel, url: function() { - return '/admin/fields/' + this.sbasId + '/fields'; + return "/admin/fields/" + this.sbasId + "/fields"; }, search: function(letters) { if (letters === "") @@ -52,6 +52,10 @@ define([ } return index - 1; + }, + // save all collection + save: function(options) { + return Backbone.sync("update", this, options || {}); } }); diff --git a/www/scripts/apps/admin/fields/collections/vocabularies.js b/www/scripts/apps/admin/fields/collections/vocabularies.js index 2b86d4109d..290348ce41 100644 --- a/www/scripts/apps/admin/fields/collections/vocabularies.js +++ b/www/scripts/apps/admin/fields/collections/vocabularies.js @@ -1,12 +1,12 @@ define([ - 'underscore', - 'backbone', - 'models/vocabulary' + "underscore", + "backbone", + "models/vocabulary" ], function(_, Backbone, VocabularyModel) { var VocabularyCollection = Backbone.Collection.extend({ model: VocabularyModel, url: function() { - return '/admin/fields/vocabularies'; + return "/admin/fields/vocabularies"; }, comparator: function(item) { return item.get("name"); diff --git a/www/scripts/apps/admin/fields/errors/error.js b/www/scripts/apps/admin/fields/errors/error.js new file mode 100644 index 0000000000..97d234a5ce --- /dev/null +++ b/www/scripts/apps/admin/fields/errors/error.js @@ -0,0 +1,13 @@ +define([ + "jquery", + "underscore" +], function($, _) { + + var Error = function (model, fieldId, message) { + this.model = model; + this.fieldId = fieldId; + this.message = message; + }; + + return Error; +}); diff --git a/www/scripts/apps/admin/fields/errors/errorManager.js b/www/scripts/apps/admin/fields/errors/errorManager.js new file mode 100644 index 0000000000..aea7bc4067 --- /dev/null +++ b/www/scripts/apps/admin/fields/errors/errorManager.js @@ -0,0 +1,125 @@ +define([ + "jquery", + "underscore", + "backbone", + "apps/admin/fields/errors/errorModel" +], function($, _, Backbone, ErrorModel) { + + var ErrorManager = function() { + this.errors = {}; + _.extend(this, Backbone.Events); + }; + + ErrorManager.prototype = { + addModelError: function (model) { + return this.errors[model.get("id")] = new ErrorModel(model.get("id")); + }, + getModelError: function (model) { + if (this.containsModelError(model)) { + return this.errors[model.get("id")]; + } + + return null; + }, + removeModelError: function (model) { + if (this.containsModelError(model)) { + delete this.errors[model.get("id")]; + } + }, + containsModelError: function (model) { + return "undefined" !== typeof this.errors[model.get("id")]; + }, + addModelFieldError: function(error) { + if (! error instanceof Error) { + throw "Item must be an error object"; + } + + var model = error.model; + var fieldId = error.fieldId; + + if (!this.containsModelError(model)) { + this.addModelError(model); + } + + this.getModelError(model).add(fieldId, error); + + this.trigger("add-error", error); + + return this; + }, + removeModelFieldError: function(model, fieldId) { + var modelError = this.getModelError(model); + + if (modelError) { + modelError.remove(fieldId); + this.trigger("remove-error", model, fieldId); + + if (modelError.count() === 0) { + this.removeModelError(model); + + if (!this.hasErrors()) { + this.trigger("no-error"); + } + } + } + }, + clearModelFieldErrors: function(model, fieldId) { + var modelError = this.getModelError(model); + + if (modelError) { + modelError.clear(); + this.removeModelError(model); + } + + if (!this.hasErrors()) { + this.trigger("no-error"); + } + }, + containsModelFieldError: function (model, fieldId) { + var modelError = this.getModelError(model); + + if (modelError) { + return modelError.has(fieldId); + } + + return false; + }, + getModelFieldError: function(model, fieldId) { + var modelError = this.getModelError(model); + + if (modelError) { + return modelError.get(fieldId); + } + + return null; + }, + clearAll: function() { + this.errors = {}; + this.trigger("no-error"); + }, + hasErrors: function () { + return !_.isEmpty(this.errors); + }, + count: function () { + var count = 0; + for (var k in this.errors) { + if (this.errors.hasOwnProperty(k)) { + ++count; + } + } + return count; + }, + all: function () { + var errors = []; + _.each(this.errors, function(modelErrors) { + _.each(modelErrors.all(), function(error) { + errors.push(error); + }); + }); + + return errors; + } + }; + + return ErrorManager; +}); diff --git a/www/scripts/apps/admin/fields/errors/errorModel.js b/www/scripts/apps/admin/fields/errors/errorModel.js new file mode 100644 index 0000000000..ebe1399f73 --- /dev/null +++ b/www/scripts/apps/admin/fields/errors/errorModel.js @@ -0,0 +1,50 @@ +define([ + "jquery", + "underscore" +], function($, _) { + var ErrorModel = function(id) { + this.id = id; + this.errors = {}; + }; + + ErrorModel.prototype = { + add: function(id, error) { + if (! error instanceof Error) { + throw "Item must be an error object"; + } + + this.errors[id] = error; + }, + get: function(id) { + if (this.has(id)) { + return this.errors[id]; + } + return null; + }, + has: function (id) { + return "undefined" !== typeof this.errors[id]; + }, + remove: function(id) { + if (this.has(id)) { + delete this.errors[id]; + } + }, + count: function() { + var count = 0; + for (var k in this.errors) { + if (this.errors.hasOwnProperty(k)) { + ++count; + } + } + return count; + }, + clear: function () { + this.errors = {}; + }, + all: function () { + return this.errors; + } + }; + + return ErrorModel; +}); diff --git a/www/scripts/apps/admin/fields/main.js b/www/scripts/apps/admin/fields/main.js index 732974a5c0..2c98838a2f 100644 --- a/www/scripts/apps/admin/fields/main.js +++ b/www/scripts/apps/admin/fields/main.js @@ -1,25 +1,27 @@ +// configure AMD loading require.config({ baseUrl: "/scripts", paths: { - jquery: '../include/minify/f=include/jslibs/jquery-1.7.1', - jqueryui: '../include/jslibs/jquery-ui-1.8.17/js/jquery-ui-1.8.17.custom.min', - underscore: '../assets/underscore-amd/underscore', - backbone: '../assets/backbone-amd/backbone', - twig: '../assets/twig/twig', - i18n: '../assets/i18n/i18next.amd', - bootstrap: '../skins/html5/bootstrap/js/bootstrap.min' + jquery: "../include/minify/f=include/jslibs/jquery-1.7.1", + jqueryui: "../include/jslibs/jquery-ui-1.8.17/js/jquery-ui-1.8.17.custom.min", + underscore: "../assets/underscore-amd/underscore", + backbone: "../assets/backbone-amd/backbone", + twig: "../assets/twig/twig", + i18n: "../assets/i18n/i18next.amd", + bootstrap: "../skins/html5/bootstrap/js/bootstrap.min" }, shim: { twig: { - exports: 'Twig' + exports: "Twig" }, - bootstrap : ['jquery'], + bootstrap : ["jquery"], jqueryui: { - deps: [ 'jquery' ] + deps: [ "jquery" ] } } }); -require(['apps/admin/fields/app'], function(App) { +// launch application +require(["apps/admin/fields/app"], function(App) { App.initialize(); }); diff --git a/www/scripts/apps/admin/fields/views/alert.js b/www/scripts/apps/admin/fields/views/alert.js index 5e9208c21c..1f46e44898 100644 --- a/www/scripts/apps/admin/fields/views/alert.js +++ b/www/scripts/apps/admin/fields/views/alert.js @@ -1,9 +1,10 @@ define([ - 'underscore', - 'backbone', - 'i18n', - 'bootstrap' -], function(_, Backbone, i18n, bootstrap) { + "jquery", + "underscore", + "backbone", + "i18n", + "bootstrap" +], function($, _, Backbone, i18n, bootstrap) { var AlertView = Backbone.View.extend({ tagName: "div", className: "alert", @@ -15,7 +16,7 @@ define([ this.message = options.message || ""; } // remove view when alert is closed - this.$el.bind('closed', function () { + this.$el.bind("closed", function () { self.remove(); }); }, @@ -26,7 +27,7 @@ define([ this.$el.addClass("alert-" + this.alert).html(template).alert(); - $('.block-alert').empty().append(this.$el); + $(".block-alert").empty().append(this.$el); return this; } diff --git a/www/scripts/apps/admin/fields/views/dcField.js b/www/scripts/apps/admin/fields/views/dcField.js index 4b16c49618..972ac71311 100644 --- a/www/scripts/apps/admin/fields/views/dcField.js +++ b/www/scripts/apps/admin/fields/views/dcField.js @@ -1,26 +1,31 @@ define([ - 'underscore', - 'backbone', - 'i18n' -], function( _, Backbone, i18n, bootstrap) { + "jquery", + "underscore", + "backbone", + "i18n" +], function($, _, Backbone, i18n, bootstrap) { var DcFieldsView = Backbone.View.extend({ tagName: "div", className: "input-append", - events: { - "change select": "selectChangedAction" + initialize : function (options) { + this.field = options.field; }, render: function() { var template = _.template($("#dc_fields_template").html(), { - dces_elements: this.collection.toJSON() + dces_elements: this.collection.toJSON(), + field: this.field.toJSON() }); this.$el.html(template); + var index = $("#dces-element", AdminFieldApp.$rightBlock)[0].selectedIndex - 1; + if (index > 0 ) { + $(".dces-help-block", AdminFieldApp.$rightBlock).html( + this.collection.at(index).get("definition") + ); + } + return this; - }, - selectChangedAction: function(e) { - var index = $(e.target)[0].selectedIndex; - this.$el.closest('table').find('.dces-help-block').empty().append(this.collection.at(index).get('definition')); } }); diff --git a/www/scripts/apps/admin/fields/views/edit.js b/www/scripts/apps/admin/fields/views/edit.js index 1597b1406b..1aaa17723c 100644 --- a/www/scripts/apps/admin/fields/views/edit.js +++ b/www/scripts/apps/admin/fields/views/edit.js @@ -1,36 +1,47 @@ define([ - 'underscore', - 'backbone', - 'i18n', - 'apps/admin/fields/views/alert', - 'apps/admin/fields/views/modal', - 'apps/admin/fields/views/dcField', -], function(_, Backbone, i18n, AlertView, ModalView, DcFieldView) { + "jquery", + "underscore", + "backbone", + "i18n", + "apps/admin/fields/views/alert", + "apps/admin/fields/views/modal", + "apps/admin/fields/views/dcField", + "apps/admin/fields/errors/error" +], function($, _, Backbone, i18n, AlertView, ModalView, DcFieldView, Error) { var FieldEditView = Backbone.View.extend({ tagName: "div", className: "field-edit", initialize: function() { - this.model.on('change', this.render, this); - this.model.on('change:name', this.onModelFieldChange, this); - this.model.on('change:tag', this.onModelFieldChange, this); + this.model.on("change", this._onModelChange, this); this.dcFieldsSubView = new DcFieldView({ - collection: window.AdminFieldApp.dcFieldsCollection + collection: AdminFieldApp.dcFieldsCollection, + field: this.model }); }, + updateModel: function(model) { + // unbind event to previous model + this.model.off("change"); + this.model = model; + + return this; + }, render: function() { + var self = this; var template = _.template($("#edit_template").html(), { field: this.model.toJSON(), - vocabularyTypes: window.AdminFieldApp.vocabularyCollection.toJSON() + vocabularyTypes: AdminFieldApp.vocabularyCollection.toJSON(), + modelErrors: AdminFieldApp.errorManager.getModelError(this.model) }); this.$el.empty().html(template); - this.assign({ - '.dc-fields-subview' : this.dcFieldsSubView + this._assign({ + ".dc-fields-subview" : this.dcFieldsSubView }); - $("#tag", this.$el).autocomplete({ + var completer = $("#tag", this.$el).autocomplete({ + minLength: 2, source: function(request, response) { $.ajax({ url: "/admin/fields/tags/search", @@ -47,72 +58,170 @@ define([ })); } }); + }, + close: function(e) { + var fieldTag = $(e.target); + var fieldTagId = fieldTag.attr("id"); + var fieldTagValue = fieldTag.val(); + + // check for format tag + if ("" !== fieldTagValue && false === /[a-z]+:[a-z0-9]+/i.test(fieldTagValue)) { + fieldTag + .closest(".control-group") + .addClass("error") + .find(".help-block") + .empty() + .append(i18n.t("validation_tag_invalid")); + // add error + AdminFieldApp.errorManager.addModelFieldError(new Error( + self.model, fieldTagId, i18n.t("validation_tag_invalid") + )); + } else if (fieldTag.closest(".control-group").hasClass("error")) { + // remove error + AdminFieldApp.errorManager.removeModelFieldError( + self.model, fieldTagId + ); + + fieldTag + .closest(".control-group") + .removeClass("error") + .find(".help-block") + .empty(); + } + + var data = {}; + data[fieldTagId] = fieldTagValue; + self.model.set(data); } - }).val(this.model.get('tag')).autocomplete("widget").addClass("ui-autocomplete-admin-field"); + }); + + completer + .val(this.model.get("tag")) + .autocomplete("widget") + .addClass("ui-autocomplete-admin-field"); + + this.delegateEvents(); return this; }, events: { + "click": "focusAction", "click .delete-field": "deleteAction", + "keyup #name": "changeNameAction", "focusout input[type=text]": "fieldChangedAction", "change input[type=checkbox]": "fieldChangedAction", "change select": "selectionChangedAction" }, + focusAction: function() { + var index = AdminFieldApp.fieldListView.collection.indexOf(this.model); + if (index >= 0) { + AdminFieldApp.fieldListView.itemViews[index].animate(); + } + + return this; + }, + // on input name keyup check for errors + changeNameAction: function(event) { + var self = this; + var fieldName = $(event.target); + var fieldNameId = fieldName.attr("id"); + var fieldNameValue = fieldName.val(); + + // check for duplicate field name + if ("" === fieldNameValue || "undefined" !== typeof AdminFieldApp.fieldListView.collection.find(function(model) { + return model.get("name").toLowerCase() === fieldNameValue.toLowerCase() && self.model.get("id") !== model.get("id"); + })) { + fieldName + .closest(".control-group") + .addClass("error") + .find(".help-block") + .empty() + .append(i18n.t("validation_name_exists")); + // add error + AdminFieldApp.errorManager.addModelFieldError(new Error( + self.model, fieldNameId, i18n.t("" === fieldNameValue ? "validation_blank" : "validation_name_exists") + )); + } else if (fieldName.closest(".control-group").hasClass("error")) { + fieldName + .closest(".control-group") + .removeClass("error") + .find(".help-block") + .empty(); + // remove error + AdminFieldApp.errorManager.removeModelFieldError( + self.model, fieldNameId + ); + } + }, selectionChangedAction: function(e) { var field = $(e.currentTarget); - var value = $("option:selected", field).val(); var data = {}; - data[field.attr('id')] = value; + data[field.attr("id")] = $("option:selected", field).val(); this.model.set(data); + + return this; }, fieldChangedAction: function(e) { var field = $(e.currentTarget); + var fieldId = field.attr("id"); var data = {}; - data[field.attr('id')] = field.is(":checkbox") ? field.is(":checked") : field.val(); + data[fieldId] = field.is(":checkbox") ? field.is(":checked") : field.val(); this.model.set(data); + + return this; }, deleteAction: function() { var self = this; var modalView = new ModalView({ model: this.model, - message: i18n.t("are_you_sure_delete", { postProcess: "sprintf", sprintf: [this.model.get('name')] }) + message: i18n.t("are_you_sure_delete", { + postProcess: "sprintf", + sprintf: [this.model.get("name")] + }) }); - var previousIndex = AdminFieldApp.fieldListView.collection.previousIndex(this.model); - var nextIndex = AdminFieldApp.fieldListView.collection.nextIndex(this.model); - var itemView; - if (previousIndex) { - itemView = AdminFieldApp.fieldListView.itemViews[previousIndex]; - } else if (nextIndex) { - itemView = AdminFieldApp.fieldListView.itemViews[nextIndex]; - } + // get collection index of previous and next model + var previousIndex = AdminFieldApp.fieldListView.collection.previousIndex(this.model); + var nextIndex = AdminFieldApp.fieldListView.collection.nextIndex(this.model); + + // get previous index if exists else next index - 1 as item is being deleted + var index = previousIndex ? previousIndex : (nextIndex ? nextIndex - 1 : -1); modalView.render(); - modalView.on('modal:confirm', function() { + modalView.on("modal:confirm", function() { self.model.destroy({ success: function(model, response) { AdminFieldApp.fieldListView.collection.remove(self.model); + self._selectModelView(index); - if (itemView) { - itemView.clickAction().animate(); - } - - new AlertView({alert: 'info', message: i18n.t("deleted_success", { postProcess: "sprintf", sprintf: [model.get('name')] })}).render(); + new AlertView({alert: "info", message: i18n.t("deleted_success", { + postProcess: "sprintf", + sprintf: [model.get("name")] + }) + }).render(); }, error: function(model, xhr) { - new AlertView({alert: 'error', message: i18n.t("something_wrong")}).render(); + new AlertView({ + alert: "error", message: i18n.t("something_wrong") + }).render(); } }); }); return this; }, - onModelFieldChange: function() { + _onModelChange: function() { AdminFieldApp.fieldListView.collection.remove(this.model, {silent: true}); AdminFieldApp.fieldListView.collection.add(this.model); + + var index = AdminFieldApp.fieldListView.collection.indexOf(this.model); + + this._selectModelView(index); + this.render(); }, - assign: function(selector, view) { + // bind a subview to a DOM element + _assign: function(selector, view) { var selectors; if (_.isObject(selector)) { selectors = selector; @@ -124,6 +233,13 @@ define([ _.each(selectors, function(view, selector) { view.setElement(this.$(selector)).render(); }, this); + }, + // select temView by index in itemList + _selectModelView: function(index) { + // select previous or next itemview + if (index >= 0) { + AdminFieldApp.fieldListView.itemViews[index].clickAction().animate(); + } } }); diff --git a/www/scripts/apps/admin/fields/views/fieldError.js b/www/scripts/apps/admin/fields/views/fieldError.js new file mode 100644 index 0000000000..38137523a8 --- /dev/null +++ b/www/scripts/apps/admin/fields/views/fieldError.js @@ -0,0 +1,38 @@ +define([ + "jquery", + "underscore", + "backbone", + "i18n" +], function($, _, Backbone, i18n) { + var FieldErrorView = Backbone.View.extend({ + initialize: function() { + AdminFieldApp.errorManager.on("add-error", this.render, this); + AdminFieldApp.errorManager.on("remove-error", this.render, this); + }, + render: function() { + var messages = []; + var errors = AdminFieldApp.errorManager.all(); + + _.each(_.groupBy(errors, function(error) { + return error.model.get("name"); + }), function(groupedErrors) { + _.each(groupedErrors, function(error) { + messages.push(i18n.t("field_error", { + postProcess: "sprintf", + sprintf: [error.model.get("name")] + })); + }); + }); + + var template = _.template($("#field_error_template").html(), { + messages: messages + }); + + $(".block-alert").html(template); + + return this; + } + }); + + return FieldErrorView; +}); diff --git a/www/scripts/apps/admin/fields/views/list.js b/www/scripts/apps/admin/fields/views/list.js index 031973913f..8aa8263e9e 100644 --- a/www/scripts/apps/admin/fields/views/list.js +++ b/www/scripts/apps/admin/fields/views/list.js @@ -1,12 +1,13 @@ define([ - 'jqueryui', - 'underscore', - 'backbone', - 'i18n', - 'apps/admin/fields/views/listRow', - 'apps/admin/fields/views/alert', - 'models/field' -], function(jqueryui, _, Backbone, i18n, FieldListRowView, AlertView, FieldModel) { + "jquery", + "jqueryui", + "underscore", + "backbone", + "i18n", + "apps/admin/fields/views/listRow", + "apps/admin/fields/views/alert", + "models/field" +], function($, jqueryui, _, Backbone, i18n, FieldListRowView, AlertView, FieldModel) { var FieldListView = Backbone.View.extend({ events: { "keyup #live_search": "searchAction", @@ -16,14 +17,35 @@ define([ "update-sort": "updateSortAction" }, initialize: function() { - // Store all single rendered views + var self = this; + // store all single rendered views this.itemViews = []; - _.bindAll(this, "render"); // rerender whenever there is a change on the collection this.collection.bind("reset", this.render, this); this.collection.bind("add", this.render, this); this.collection.bind("remove", this.render, this); + + AdminFieldApp.errorManager.on('add-error', function(error) { + var model = error.model; + var itemView = _.find(self.itemViews, function(view) { + return model.get('id') === view.model.get('id'); + }); + + if ('undefined' !== typeof itemView) { + itemView.error(true); + } + }); + + AdminFieldApp.errorManager.on('remove-error', function(model) { + var itemView = _.find(self.itemViews, function(view) { + return model.get('id') === view.model.get('id'); + }); + + if ('undefined' !== typeof itemView) { + itemView.error(false); + } + }); }, render: function() { var template = _.template($("#item_list_view_template").html(), {}); @@ -35,6 +57,7 @@ define([ this._renderList(this.collection); $("#new-source", this.$el).autocomplete({ + minLength: 2, source: function(request, response) { $.ajax({ url: "/admin/fields/tags/search", @@ -54,8 +77,11 @@ define([ } }).autocomplete("widget").addClass("ui-autocomplete-admin-field"); + AdminFieldApp.resizeListBlock(); + return this; }, + // render list by appending single item view, also fill itemViews _renderList: function(fields) { var that = this; @@ -63,45 +89,100 @@ define([ this.itemViews = []; fields.each(function(field) { + var fieldErrors = AdminFieldApp.errorManager.getModelError(field); + var singleView = new FieldListRowView({ model: field, - id: 'field-' + field.get('id') - }); + id: "field-" + field.get("id") + }).error(fieldErrors && fieldErrors.count() > 0); + that.$listEl.append(singleView.render().el); that.itemViews.push(singleView); }); this.$listEl.sortable({ handle: ".handle", + placeholder: "item-list-placeholder", + start: function(event, ui) { + ui.item.addClass("border-bottom"); + + }, stop: function(event, ui) { - ui.item.trigger('drop', ui.item.index()); + ui.firstItemPosition = $("li:first", $(this).sortable('widget')).position().top; + ui.item.trigger("drop", ui); } }); this.$listEl.disableSelection(); - this.$listEl.find('li:last').addClass('last'); + this.$listEl.find("li:last").addClass("last"); return this; }, searchAction: function(event) { this._renderList(this.collection.search($("#live_search", this.$el).val())); + + return this; }, createAction: function(event) { var self = this; + var formErrors = 0; var fieldName = $("#new-name", this.$el); + var fieldNameValue = fieldName.val(); + var fieldTag = $("#new-source", this.$el); + var fieldTagValue = fieldTag.val(); - if ('' == fieldName.val()) { - fieldName.closest('.control-group').addClass('error').find('.help-block').empty().append(i18n.t('validation_blank')); + // check for empty field name + if ("" === fieldNameValue) { + fieldName + .closest(".control-group") + .addClass("error") + .find(".help-block") + .empty() + .append(i18n.t("validation_blank")); + + formErrors++; + } + + // check for duplicate field name + if ("undefined" !== typeof this.collection.find(function(model){ + return model.get("name").toLowerCase() === fieldNameValue.toLowerCase(); + })) { + fieldName + .closest(".control-group") + .addClass("error") + .find(".help-block") + .empty() + .append(i18n.t("validation_name_exists")); + + formErrors++; + } + + // check for format tag + if ("" !== fieldTagValue && false === /[a-z]+:[a-z0-9]+/i.test(fieldTagValue)) { + fieldTag + .closest(".control-group") + .addClass("error") + .find(".help-block") + .empty() + .append(i18n.t("validation_tag_invalid")); + + formErrors++; + } + + if (formErrors > 0 ) { return; } var field = new FieldModel({ "sbas-id": AdminFieldApp.sbas_id, - "name": fieldName.val(), - "tag": $("#new-source", this.$el).val(), - "multi": $("#new-multivalued", this.$el).is(':checked') + "name": fieldNameValue, + "tag": fieldTagValue, + "multi": $("#new-multivalued", this.$el).is(":checked"), + "sorter": this.collection.max(function(model) { + return model.get("sorter"); + }).get("sorter") + 1 }); field.save(null, { @@ -109,36 +190,53 @@ define([ if (response.success) { self.collection.add(field); _.last(self.itemViews).clickAction().animate(); - new AlertView({alert: 'success', message: response.message }).render(); - } else { - new AlertView({alert: 'warning', message: response.message}).render(); } + + new AlertView({ + alert: response.success ? "success" : "error", message: response.message + }).render(); }, error: function(model, xhr, options) { - new AlertView({alert: 'error', message: i18n.t("something_wrong")}).render(); + new AlertView({ + alert: "error", message: i18n.t("something_wrong")} + ).render(); + self.toggleCreateFormAction(); } }); + + return this; }, toggleCreateFormAction: function(event) { - $('.add-field-block', this.$el).toggle(); + var fieldBlock = $(".add-field-block", this.$el); + + fieldBlock.is(":hidden") ? fieldBlock.show() : fieldBlock.hide(); + AdminFieldApp.resizeListBlock(); + + return this; }, - updateSortAction: function(event, model, position) { + updateSortAction: function(event, model, ui) { + var position = ui.item.index(); this.collection.remove(model, {silent: true}); + // reorder all collection model this.collection.each(function(model, index) { var ordinal = index; if (index >= position) ordinal += 1; - model.set('sorter', ordinal); + model.set("sorter", ordinal); }); - model.set('sorter', position); + model.set("sorter", position); this.collection.add(model, {at: position}); - // update edit view + this.itemViews[0].animate(Math.abs(ui.firstItemPosition)); + + // update edit view model AdminFieldApp.fieldEditView.model = this.collection.find(function(el) { - return el.get('id') === AdminFieldApp.fieldEditView.model.get('id'); + return el.get("id") === AdminFieldApp.fieldEditView.model.get("id"); }); + + return this; } }); diff --git a/www/scripts/apps/admin/fields/views/listRow.js b/www/scripts/apps/admin/fields/views/listRow.js index 3195a2e343..a94812ff93 100644 --- a/www/scripts/apps/admin/fields/views/listRow.js +++ b/www/scripts/apps/admin/fields/views/listRow.js @@ -1,15 +1,15 @@ define([ - 'underscore', - 'backbone', - 'apps/admin/fields/views/edit', - 'apps/admin/fields/views/alert' -], function(_, Backbone, FieldEditView, AlertView) { + "jquery", + "underscore", + "backbone", + "apps/admin/fields/views/edit" +], function($, _, Backbone, FieldEditView) { var FieldListRowView = Backbone.View.extend({ tagName: "li", className: "field-row", initialize: function() { // destroy view is model is deleted - this.model.on('destroy', this.remove, this); + this.model.on("destroy", this.remove, this); }, events : { "click .trigger-click": "clickAction", @@ -17,51 +17,71 @@ define([ }, clickAction: function (e) { this.select(); - // first click create view else update model's view - if (typeof AdminFieldApp.fieldEditView === 'undefined') { + // first click create edit view else update model"s view + if (typeof AdminFieldApp.fieldEditView === "undefined") { AdminFieldApp.fieldEditView = new FieldEditView({ - el: $('.right-block')[0], + el: AdminFieldApp.$rightBlock, model: this.model }); } else { - AdminFieldApp.fieldEditView.model = this.model; + AdminFieldApp.fieldEditView.updateModel(this.model).initialize(); } AdminFieldApp.fieldEditView.render(); return this; }, - dropAction: function(event, index) { - this.$el.trigger('update-sort', [this.model, index]); + dropAction: function(event, ui) { + this.$el.trigger("update-sort", [this.model, ui]); + + return this; }, render: function() { var template = _.template($("#list_row_template").html(), { - id: this.model.get('id'), - position: this.model.get('sorter'), - name: this.model.get('name'), - tag: this.model.get('tag') + id: this.model.get("id"), + position: this.model.get("sorter"), + name: this.model.get("name"), + tag: this.model.get("tag") }); this.$el.empty().html(template); - if (AdminFieldApp.fieldEditView && AdminFieldApp.fieldEditView.model.get('id') === this.model.get('id')) { + // highlight view if edit view model match current view model + if (AdminFieldApp.fieldEditView + && AdminFieldApp.fieldEditView.model.get("id") === this.model.get("id")) { this.select(); } + return this; }, - // set selected class + // set selected class to current view select: function () { - $("li", this.$el.closest('ul')).removeClass('selected'); - this.$el.addClass('selected'); + $("li", this.$el.closest("ul")).removeClass("selected"); + this.$el.addClass("selected"); return this; }, - animate: function () { - var offset = this.$el.offset(); + // scroll to current view in item list + animate: function (top) { + top = top || null; + + if (null === top) { + top = $(".field-row").index(this.$el) * this.$el.height(); + } - this.$el.closest('div').animate({ - scrollTop: offset.top - 20 - }); + this.$el.closest("div").scrollTop(top); + + return this; + }, + // add error class to item + error: function (errored) { + if (errored) { + this.$el.addClass("error"); + } else { + this.$el.removeClass("error"); + } + + return this; } }); diff --git a/www/scripts/apps/admin/fields/views/modal.js b/www/scripts/apps/admin/fields/views/modal.js index 5e7c504cb6..d6fc73b99a 100644 --- a/www/scripts/apps/admin/fields/views/modal.js +++ b/www/scripts/apps/admin/fields/views/modal.js @@ -1,19 +1,20 @@ define([ - 'underscore', - 'backbone', - 'i18n', - 'bootstrap' -], function(_, Backbone, i18n, bootstrap) { + "jquery", + "underscore", + "backbone", + "i18n", + "bootstrap" +], function($, _, Backbone, i18n, bootstrap) { var ModalView = Backbone.View.extend({ tagName: "div", className: "modal", events: { - 'click .confirm': 'confirmAction' + "click .confirm": "confirmAction" }, initialize: function (options) { var self = this; // remove view when modal is closed - this.$el.on('hidden', function() { + this.$el.on("hidden", function() { self.remove(); }); @@ -22,8 +23,8 @@ define([ } }, render: function() { - var template = _.template($("#modal_delete_confirm_template").html(), { - msg: this.message || '' + var template = _.template($("#modal_template").html(), { + msg: this.message || "" }); this.$el.html(template).modal(); @@ -31,8 +32,8 @@ define([ return this; }, confirmAction: function () { - this.trigger('modal:confirm'); - this.$el.modal('hide'); + this.trigger("modal:confirm"); + this.$el.modal("hide"); this.remove(); return this; diff --git a/www/scripts/apps/admin/fields/views/save.js b/www/scripts/apps/admin/fields/views/save.js new file mode 100644 index 0000000000..bf993075d4 --- /dev/null +++ b/www/scripts/apps/admin/fields/views/save.js @@ -0,0 +1,95 @@ +define([ + "jquery", + "underscore", + "backbone", + "i18n", + "bootstrap", + "apps/admin/fields/views/alert" +], function($, _, Backbone, i18n, bootstrap, AlertView) { + var SaveView = Backbone.View.extend({ + initialize: function() { + var self = this; + this.previousAttributes = []; + this.$overlay = null; + + AdminFieldApp.errorManager.on("add-error", function(errors) { + self._disableSaveButton(true); + }); + + AdminFieldApp.errorManager.on("no-error", function() { + self._disableSaveButton(false); + }); + }, + events: { + "click button.save-all" : "clickSaveAction" + }, + clickSaveAction: function(event) { + var self = this; + + if (this._isModelDesync()) { + this._loadingState(true); + AdminFieldApp.fieldsCollection.save({ + success: function(response) { + // reset collection with new one + if (response.success) { + AdminFieldApp.fieldsCollection.reset(response.fields); + } + + new AlertView({ + alert: response.success ? "success" : "error", + message: response.messages.join("
") + }).render(); + }, + error: function(model, xhr, options) { + new AlertView({ + alert: "error", message: i18n.t("something_wrong") + }).render(); + } + }).done(function() { + self._loadingState(false); + }); + } + + return this; + }, + render: function () { + var template = _.template($("#save_template").html()); + this.$el.html(template); + + return this; + }, + // check whether model has changed or not + _isModelDesync: function () { + return "undefined" !== typeof AdminFieldApp.fieldsCollection.find(function(model) { + return !_.isEmpty(model.previousAttributes()); + }); + }, + // create a transparent overlay on top of the application + _overlay: function(showOrHide) { + if(showOrHide && !this.$overlay) { + this.$overlay = $("
").addClass("overlay"); + AdminFieldApp.$bottom.append(this.$overlay); + } else if (!showOrHide && this.$overlay) { + this.$overlay.remove(); + this.$overlay = null; + } + }, + _disableSaveButton: function (active) { + $("button.save-all", this.$el).attr("disabled", active); + }, + // put application on loading state (add overlay, add spinner, disable global save button) + _loadingState: function(active) { + if (active) { + $(".save-block", AdminFieldApp.$top).addClass("loading"); + $(".block-alert", AdminFieldApp.$top).empty(); + } else { + $(".save-block", AdminFieldApp.$top).removeClass("loading"); + } + + this._disableSaveButton(active); + this._overlay(active); + } + }); + + return SaveView; +}); diff --git a/www/scripts/models/dcField.js b/www/scripts/models/dcField.js index fa9ab138ba..8c60b959cc 100644 --- a/www/scripts/models/dcField.js +++ b/www/scripts/models/dcField.js @@ -1,10 +1,10 @@ define([ - 'underscore', - 'backbone' + "underscore", + "backbone" ], function(_, Backbone) { var DcFieldModel = Backbone.Model.extend({ urlRoot: function () { - return '/admin/fields/dc-fields'; + return "/admin/fields/dc-fields"; } }); diff --git a/www/scripts/models/field.js b/www/scripts/models/field.js index 2881228e98..1dc22254e5 100644 --- a/www/scripts/models/field.js +++ b/www/scripts/models/field.js @@ -1,6 +1,6 @@ define([ - 'underscore', - 'backbone' + "underscore", + "backbone" ], function(_, Backbone) { var FieldModel = Backbone.Model.extend({ initialize : function(attributes, options) { @@ -9,7 +9,7 @@ define([ } }, urlRoot: function () { - return '/admin/fields/'+ this.get('sbas-id') +'/fields'; + return "/admin/fields/"+ this.get("sbas-id") +"/fields"; }, defaults: { "business": false, diff --git a/www/scripts/models/vocabulary.js b/www/scripts/models/vocabulary.js index 7dcc9439ce..1eb31e0bd1 100644 --- a/www/scripts/models/vocabulary.js +++ b/www/scripts/models/vocabulary.js @@ -1,10 +1,10 @@ define([ - 'underscore', - 'backbone' + "underscore", + "backbone" ], function(_, Backbone) { var VocabularyModel = Backbone.Model.extend({ urlRoot: function () { - return '/admin/fields/vocabularies'; + return "/admin/fields/vocabularies"; } }); diff --git a/www/scripts/tests/baseTest.js b/www/scripts/tests/baseTest.js index ca57e9a75f..660954f6f5 100644 --- a/www/scripts/tests/baseTest.js +++ b/www/scripts/tests/baseTest.js @@ -1,4 +1,4 @@ -define(['chai'], function(shai) { +define(["chai"], function(shai) { window.expect = shai.expect; window.assert = shai.assert; }); diff --git a/www/scripts/tests/main.js b/www/scripts/tests/main.js index e6fec04564..de6ba69700 100644 --- a/www/scripts/tests/main.js +++ b/www/scripts/tests/main.js @@ -1,8 +1,8 @@ require.config({ baseUrl: "../../scripts", paths: { - specs: 'tests/specs', - chai: '../assets/chai/chai' + specs: "tests/specs", + chai: "../assets/chai/chai" }, shim : { shai: { @@ -12,7 +12,7 @@ require.config({ }); mocha.setup({ - ui: 'bdd', + ui: "bdd", ignoreLeaks: true }); diff --git a/www/scripts/tests/specs/admin/fields.js b/www/scripts/tests/specs/admin/fields.js index 643d16d910..5f2a358eef 100644 --- a/www/scripts/tests/specs/admin/fields.js +++ b/www/scripts/tests/specs/admin/fields.js @@ -1,5 +1,5 @@ define(function(require) { - it('should run', function () { + it("should run", function () { expect(true).to.equal(true); }); }); diff --git a/www/skins/admin/css/fields.css b/www/skins/admin/css/fields.css index fee0e093cf..6432ded324 100644 --- a/www/skins/admin/css/fields.css +++ b/www/skins/admin/css/fields.css @@ -1,69 +1,119 @@ +#admin-field-app .row-top { + min-height: 60px; + border-bottom: 1px solid #000; +} + +#admin-field-app .row-bottom { + position: relative; +} + +#admin-field-app .right-block { + border-left: 1px dashed #000; + overflow: auto; + min-height: 200px; +} + #admin-field-app h4 { padding: 10px 0; } -#admin-field-app li { +#admin-field-app .left-block li { background: #FFF; border-top: 1px solid #ccc; border-left: 1px solid #ccc; border-right: 1px solid #ccc; + height: 55px; } -#admin-field-app li table { - table-layout:fixed; - width:100%; +#admin-field-app .left-block li .trigger-click { + cursor: pointer; } -#admin-field-app li .field-name { +#admin-field-app .left-block li.border-bottom{ + border-bottom: 1px solid #ccc; +} + +#admin-field-app .left-block li table { + table-layout: fixed; + width: 100%; +} + +#admin-field-app .left-block li .field-name { font-weight: bold; font-size: 16px; color: #666; } -#admin-field-app li .handle { +#admin-field-app .left-block li .handle { width: 10%; - vertical-align:middle; - text-align:center; + vertical-align: middle; + text-align: center; cursor: move; } -#admin-field-app li .trigger-click { - padding:10px; +#admin-field-app .left-block li .trigger-click { + padding: 10px; } -#admin-field-app li .position { +#admin-field-app .left-block li .position { width: 10%; - vertical-align:bottom; - text-align: center + vertical-align: bottom; + text-align: center; } -#admin-field-app li .chip { - width:10%; +#admin-field-app .left-block li .chip { + width: 10%; } -#admin-field-app li .handle, #admin-field-app li .position { +#admin-field-app .left-block li .handle, #admin-field-app li .position { color: #ccc; border-right: 1px solid #ccc; } -#admin-field-app li.last { +#admin-field-app .left-block li.last { border-bottom: 1px solid #ccc; } -#admin-field-app li.selected { +#admin-field-app .left-block li.selected { border-top-color: #0080FF; background: #FFF; color: #000; } -#admin-field-app li.selected + li { +#admin-field-app .left-block li.last.selected { + border-bottom-color: #0080FF; +} + +#admin-field-app .left-block li.last.selected.error { + border-bottom-color: #9d261d; +} + +#admin-field-app .left-block li.selected + li { border-top-color: #0080FF; } -#admin-field-app li.selected .field-name { +#admin-field-app .left-block li.selected.error { + border-top-color: #9d261d; +} + +#admin-field-app .left-block li.selected.error + li { + border-top-color: #9d261d; +} + +#admin-field-app .left-block li.selected .field-name { color: #0080FF; } +#admin-field-app .left-block li.error .field-name { + color: #9d261d; +} + +#admin-field-app .item-list-placeholder { + border-top: 1px solid #ccc; + background-color: red; + height: 70px; +} + #admin-field-app .add-field-block .control-label { width: 80px; text-align: left; @@ -85,13 +135,42 @@ margin: 20px 0; } -#admin-field-app #collection-fields { - height:450px; +#admin-field-app .list-block { + height: 450px; + min-height: 130px; overflow: auto; + position: relative; } #admin-field-app .edit-block { - padding: 5px 20px + padding: 5px 20px; +} + +#admin-field-app .edit-block table { + table-layout: fixed; + width: 100%; +} + +#admin-field-app .edit-block table label { + margin: 0px; + width: auto; +} + +#admin-field-app .edit-block table td:first-child { + width: 130px; +} + +#admin-field-app .edit-block td { + height: 25px; +} + +#admin-field-app .edit-block .dces-help-block { + height: auto; +} + +#admin-field-app .info { + color: #aaa; + padding: 10px; } #admin-field-app .edit-block .edit-order { @@ -103,17 +182,38 @@ } #admin-field-app .edit-block input#name { - font-size:28px; - color:#0080FF; - height:42px; - line-height:42px; - font-weight:bold + font-size: 28px; + color: #0080FF; + height: 42px; + line-height: 42px; + font-weight: bold } +#admin-field-app .overlay { + zoom: 1; + filter: alpha(opacity=50); + opacity: 0.5; + background: #fff; + position: absolute; + width: 100%; + height: 100%; + z-index: 2000; +} + +#admin-field-app .list-field-error li { + color: #9d261d; +} + +#admin-field-app .save-block.loading { + background: url('/skins/icons/loaderFFF.gif') #fff no-repeat center right; +} + +/* jquery ui autocomplete style */ .ui-autocomplete-admin-field { list-style-type: none; - overflow-y: scroll; - height: 180px; + overflow-y: auto; + overflow-x: hidden; + max-height: 180px; background: #FFF; max-width: 300px; -webkit-box-shadow: 0 10px 6px -6px #777; @@ -121,10 +221,25 @@ box-shadow: 0 10px 6px -6px #777; } -.ui-autocomplete-admin-field li{ - padding: 3px; +.ui-autocomplete-admin-field li { + padding: 5px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ui-autocomplete-admin-field li:hover { + padding: 5px; + background: #ccc; } .ui-autocomplete-admin-field li a { text-decoration: none; } + +.ui-autocomplete-admin-field li a:hover { + text-decoration: none; + background: none; + color: #000; + border: none; +}