From 1fa079b46626b5cbb3d1b055542e4a98b4b0ca0a Mon Sep 17 00:00:00 2001
From: Dan Brown <ssddanbrown@googlemail.com>
Date: Thu, 12 May 2016 23:12:05 +0100
Subject: [PATCH] Started the page attributes interface

---
 app/Attribute.php                             |  2 +-
 app/Http/Controllers/AttributeController.php  | 11 +--
 app/Http/Controllers/PageController.php       |  2 +-
 app/Repos/AttributeRepo.php                   |  1 +
 ...6_05_06_185215_create_attributes_table.php |  2 +
 resources/assets/js/controllers.js            | 94 ++++++++++++++++++-
 resources/assets/sass/styles.scss             | 14 +++
 resources/views/pages/create.blade.php        | 17 ----
 resources/views/pages/edit.blade.php          | 17 +++-
 resources/views/pages/form.blade.php          |  1 +
 tests/Entity/AttributeTests.php               | 25 +++--
 11 files changed, 152 insertions(+), 34 deletions(-)
 delete mode 100644 resources/views/pages/create.blade.php

diff --git a/app/Attribute.php b/app/Attribute.php
index 62dc62be7..3fe201a42 100644
--- a/app/Attribute.php
+++ b/app/Attribute.php
@@ -6,7 +6,7 @@
  */
 class Attribute extends Model
 {
-    protected $fillable = ['name', 'value'];
+    protected $fillable = ['name', 'value', 'order'];
 
     /**
      * Get the entity that this attribute belongs to
diff --git a/app/Http/Controllers/AttributeController.php b/app/Http/Controllers/AttributeController.php
index d7282696a..6e6913722 100644
--- a/app/Http/Controllers/AttributeController.php
+++ b/app/Http/Controllers/AttributeController.php
@@ -38,18 +38,15 @@ class AttributeController extends Controller
      */
     public function updateForEntity($entityType, $entityId, Request $request)
     {
-
-        $this->validate($request, [
-            'attributes.*.name' => 'required|min:3|max:250',
-            'attributes.*.value' => 'max:250'
-        ]);
-
         $entity = $this->attributeRepo->getEntity($entityType, $entityId, 'update');
         if ($entity === null) return $this->jsonError("Entity not found", 404);
 
         $inputAttributes = $request->input('attributes');
         $attributes = $this->attributeRepo->saveAttributesToEntity($entity, $inputAttributes);
-        return response()->json($attributes);
+        return response()->json([
+            'attributes' => $attributes,
+            'message' => 'Attributes successfully updated'
+        ]);
     }
 
     /**
diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php
index 19e5632a4..da9273743 100644
--- a/app/Http/Controllers/PageController.php
+++ b/app/Http/Controllers/PageController.php
@@ -72,7 +72,7 @@ class PageController extends Controller
         $this->checkOwnablePermission('page-create', $book);
         $this->setPageTitle('Edit Page Draft');
 
-        return view('pages/create', ['draft' => $draft, 'book' => $book]);
+        return view('pages/edit', ['page' => $draft, 'book' => $book, 'isDraft' => true]);
     }
 
     /**
diff --git a/app/Repos/AttributeRepo.php b/app/Repos/AttributeRepo.php
index 7318b253b..fb742b2d8 100644
--- a/app/Repos/AttributeRepo.php
+++ b/app/Repos/AttributeRepo.php
@@ -80,6 +80,7 @@ class AttributeRepo
         $entity->attributes()->delete();
         $newAttributes = [];
         foreach ($attributes as $attribute) {
+            if (trim($attribute['name']) === '') continue;
             $newAttributes[] = $this->newInstanceFromInput($attribute);
         }
 
diff --git a/database/migrations/2016_05_06_185215_create_attributes_table.php b/database/migrations/2016_05_06_185215_create_attributes_table.php
index df3e55421..b6412037c 100644
--- a/database/migrations/2016_05_06_185215_create_attributes_table.php
+++ b/database/migrations/2016_05_06_185215_create_attributes_table.php
@@ -18,10 +18,12 @@ class CreateAttributesTable extends Migration
             $table->string('entity_type', 100);
             $table->string('name');
             $table->string('value');
+            $table->integer('order');
             $table->timestamps();
 
             $table->index('name');
             $table->index('value');
+            $table->index('order');
             $table->index(['entity_id', 'entity_type']);
         });
     }
diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js
index 8b3d952be..e73efaac9 100644
--- a/resources/assets/js/controllers.js
+++ b/resources/assets/js/controllers.js
@@ -400,4 +400,96 @@ module.exports = function (ngApp, events) {
 
     }]);
 
-};
\ No newline at end of file
+    ngApp.controller('PageAttributeController', ['$scope', '$http', '$attrs',
+        function ($scope, $http, $attrs) {
+
+            const pageId = Number($attrs.pageId);
+            $scope.attributes = [];
+
+            /**
+             * Push an empty attribute to the end of the scope attributes.
+             */
+            function addEmptyAttribute() {
+                $scope.attributes.push({
+                    name: '',
+                    value: ''
+                });
+            }
+
+            /**
+             * Get all attributes for the current book and add into scope.
+             */
+            function getAttributes() {
+                $http.get('/ajax/attributes/get/page/' + pageId).then((responseData) => {
+                    $scope.attributes = responseData.data;
+                    addEmptyAttribute();
+                });
+            }
+            getAttributes();
+
+            /**
+             * Set the order property on all attributes.
+             */
+            function setAttributeOrder() {
+                for (let i = 0; i < $scope.attributes.length; i++) {
+                    $scope.attributes[i].order = i;
+                }
+            }
+
+            /**
+             * When an attribute changes check if another empty editable
+             * field needs to be added onto the end.
+             * @param attribute
+             */
+            $scope.attributeChange = function(attribute) {
+                let cPos = $scope.attributes.indexOf(attribute);
+                if (cPos !== $scope.attributes.length-1) return;
+
+                if (attribute.name !== '' || attribute.value !== '') {
+                    addEmptyAttribute();
+                }
+            };
+
+            /**
+             * When an attribute field loses focus check the attribute to see if its
+             * empty and therefore could be removed from the list.
+             * @param attribute
+             */
+            $scope.attributeBlur = function(attribute) {
+                let isLast = $scope.attributes.length - 1 === $scope.attributes.indexOf(attribute);
+                if (attribute.name === '' && attribute.value === '' && !isLast) {
+                    let cPos = $scope.attributes.indexOf(attribute);
+                    $scope.attributes.splice(cPos, 1);
+                }
+            };
+
+            $scope.saveAttributes = function() {
+                setAttributeOrder();
+                let postData = {attributes: $scope.attributes};
+                $http.post('/ajax/attributes/update/page/' + pageId, postData).then((responseData) => {
+                    $scope.attributes = responseData.data.attributes;
+                    addEmptyAttribute();
+                    events.emit('success', responseData.data.message);
+                })
+            };
+
+        }]);
+
+};
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/assets/sass/styles.scss b/resources/assets/sass/styles.scss
index d8453b9ed..4b6b1cc46 100644
--- a/resources/assets/sass/styles.scss
+++ b/resources/assets/sass/styles.scss
@@ -201,4 +201,18 @@ $btt-size: 40px;
     background-color: $negative;
     color: #EEE;
   }
+}
+
+// Attribute form
+.floating-toolbox {
+  background-color: #FFF;
+  border: 1px solid #BBB;
+  border-radius: 3px;
+  padding: $-l;
+  position: fixed;
+  right: $-xl*2;
+  top: 100px;
+  z-index: 99;
+  height: 800px;
+  overflow-y: scroll;
 }
\ No newline at end of file
diff --git a/resources/views/pages/create.blade.php b/resources/views/pages/create.blade.php
deleted file mode 100644
index 2c6403e48..000000000
--- a/resources/views/pages/create.blade.php
+++ /dev/null
@@ -1,17 +0,0 @@
-@extends('base')
-
-@section('head')
-    <script src="/libs/tinymce/tinymce.min.js?ver=4.3.7"></script>
-@stop
-
-@section('body-class', 'flexbox')
-
-@section('content')
-
-    <div class="flex-fill flex">
-        <form action="{{$book->getUrl() . '/page/' . $draft->id}}" method="POST" class="flex flex-fill">
-            @include('pages/form', ['model' => $draft])
-        </form>
-    </div>
-    @include('partials/image-manager', ['imageType' => 'gallery', 'uploaded_to' => $draft->id])
-@stop
\ No newline at end of file
diff --git a/resources/views/pages/edit.blade.php b/resources/views/pages/edit.blade.php
index 0ad06fc53..a536ad23e 100644
--- a/resources/views/pages/edit.blade.php
+++ b/resources/views/pages/edit.blade.php
@@ -10,9 +10,24 @@
 
     <div class="flex-fill flex">
         <form action="{{$page->getUrl()}}" data-page-id="{{ $page->id }}" method="POST" class="flex flex-fill">
-            <input type="hidden" name="_method" value="PUT">
+            @if(!isset($isDraft))
+                <input type="hidden" name="_method" value="PUT">
+            @endif
             @include('pages/form', ['model' => $page])
         </form>
+
+        <div class="floating-toolbox" ng-controller="PageAttributeController" page-id="{{ $page->id or 0 }}">
+            <form ng-submit="saveAttributes()">
+                <table>
+                    <tr ng-repeat="attribute in attributes">
+                        <td><input type="text" ng-model="attribute.name" ng-change="attributeChange(attribute)" ng-blur="attributeBlur(attribute)" placeholder="Attribute Name"></td>
+                        <td><input type="text" ng-model="attribute.value" ng-change="attributeChange(attribute)" ng-blur="attributeBlur(attribute)" placeholder="Value"></td>
+                    </tr>
+                </table>
+                <button class="button pos" type="submit">Save attributes</button>
+            </form>
+        </div>
+
     </div>
     @include('partials/image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id])
 
diff --git a/resources/views/pages/form.blade.php b/resources/views/pages/form.blade.php
index 7ce9dbfe5..aa05a9014 100644
--- a/resources/views/pages/form.blade.php
+++ b/resources/views/pages/form.blade.php
@@ -41,6 +41,7 @@
             @include('form/text', ['name' => 'name', 'placeholder' => 'Page Title'])
         </div>
     </div>
+
     <div class="edit-area flex-fill flex">
         @if(setting('app-editor') === 'wysiwyg')
             <textarea id="html-editor" tinymce="editorOptions" mce-change="editorChange" mce-model="editContent"  name="html" rows="5"
diff --git a/tests/Entity/AttributeTests.php b/tests/Entity/AttributeTests.php
index 11b66b9e7..808766005 100644
--- a/tests/Entity/AttributeTests.php
+++ b/tests/Entity/AttributeTests.php
@@ -97,19 +97,32 @@ class AttributeTests extends \TestCase
             ['name' => 'country', 'value' => 'England'],
         ];
 
+        // Do update request
         $this->asAdmin()->json("POST", "/ajax/attributes/update/page/" . $page->id, ['attributes' => $testJsonData]);
-        $this->asAdmin()->get("/ajax/attributes/get/page/" . $page->id);
-        $jsonData = json_decode($this->response->getContent());
-        // Check counts
-        $this->assertTrue(count($jsonData) === count($testJsonData), "The received attribute count is incorrect");
+        $updateData = json_decode($this->response->getContent());
         // Check data is correct
         $testDataCorrect = true;
-        foreach ($jsonData as $data) {
+        foreach ($updateData->attributes as $data) {
             $testItem = ['name' => $data->name, 'value' => $data->value];
             if (!in_array($testItem, $testResponseJsonData)) $testDataCorrect = false;
         }
         $testMessage = "Expected data was not found in the response.\nExpected Data: %s\nRecieved Data: %s";
-        $this->assertTrue($testDataCorrect, sprintf($testMessage, json_encode($testResponseJsonData), json_encode($jsonData)));
+        $this->assertTrue($testDataCorrect, sprintf($testMessage, json_encode($testResponseJsonData), json_encode($updateData)));
+        $this->assertTrue(isset($updateData->message), "No message returned in attribute update response");
+
+        // Do get request
+        $this->asAdmin()->get("/ajax/attributes/get/page/" . $page->id);
+        $getResponseData = json_decode($this->response->getContent());
+        // Check counts
+        $this->assertTrue(count($getResponseData) === count($testJsonData), "The received attribute count is incorrect");
+        // Check data is correct
+        $testDataCorrect = true;
+        foreach ($getResponseData as $data) {
+            $testItem = ['name' => $data->name, 'value' => $data->value];
+            if (!in_array($testItem, $testResponseJsonData)) $testDataCorrect = false;
+        }
+        $testMessage = "Expected data was not found in the response.\nExpected Data: %s\nRecieved Data: %s";
+        $this->assertTrue($testDataCorrect, sprintf($testMessage, json_encode($testResponseJsonData), json_encode($getResponseData)));
     }
 
 }