Dan Brown

Renamed attribute to tags & continued interface

Also fixed page create route broken in last commit
...@@ -55,12 +55,12 @@ class Entity extends Ownable ...@@ -55,12 +55,12 @@ class Entity extends Ownable
55 } 55 }
56 56
57 /** 57 /**
58 - * Get the Attribute models that have been user assigned to this entity. 58 + * Get the Tag models that have been user assigned to this entity.
59 * @return \Illuminate\Database\Eloquent\Relations\MorphMany 59 * @return \Illuminate\Database\Eloquent\Relations\MorphMany
60 */ 60 */
61 - public function attributes() 61 + public function tags()
62 { 62 {
63 - return $this->morphMany(Attribute::class, 'entity'); 63 + return $this->morphMany(Tag::class, 'entity');
64 } 64 }
65 65
66 /** 66 /**
......
1 <?php namespace BookStack\Http\Controllers; 1 <?php namespace BookStack\Http\Controllers;
2 2
3 -use BookStack\Repos\AttributeRepo; 3 +use BookStack\Repos\TagRepo;
4 use Illuminate\Http\Request; 4 use Illuminate\Http\Request;
5 use BookStack\Http\Requests; 5 use BookStack\Http\Requests;
6 6
7 -class AttributeController extends Controller 7 +class TagController extends Controller
8 { 8 {
9 9
10 - protected $attributeRepo; 10 + protected $tagRepo;
11 11
12 /** 12 /**
13 - * AttributeController constructor. 13 + * TagController constructor.
14 - * @param $attributeRepo 14 + * @param $tagRepo
15 */ 15 */
16 - public function __construct(AttributeRepo $attributeRepo) 16 + public function __construct(TagRepo $tagRepo)
17 { 17 {
18 - $this->attributeRepo = $attributeRepo; 18 + $this->tagRepo = $tagRepo;
19 } 19 }
20 20
21 /** 21 /**
22 - * Get all the Attributes for a particular entity 22 + * Get all the Tags for a particular entity
23 * @param $entityType 23 * @param $entityType
24 * @param $entityId 24 * @param $entityId
25 */ 25 */
26 public function getForEntity($entityType, $entityId) 26 public function getForEntity($entityType, $entityId)
27 { 27 {
28 - $attributes = $this->attributeRepo->getForEntity($entityType, $entityId); 28 + $tags = $this->tagRepo->getForEntity($entityType, $entityId);
29 - return response()->json($attributes); 29 + return response()->json($tags);
30 } 30 }
31 31
32 /** 32 /**
33 - * Update the attributes for a particular entity. 33 + * Update the tags for a particular entity.
34 * @param $entityType 34 * @param $entityType
35 * @param $entityId 35 * @param $entityId
36 * @param Request $request 36 * @param Request $request
...@@ -38,25 +38,25 @@ class AttributeController extends Controller ...@@ -38,25 +38,25 @@ class AttributeController extends Controller
38 */ 38 */
39 public function updateForEntity($entityType, $entityId, Request $request) 39 public function updateForEntity($entityType, $entityId, Request $request)
40 { 40 {
41 - $entity = $this->attributeRepo->getEntity($entityType, $entityId, 'update'); 41 + $entity = $this->tagRepo->getEntity($entityType, $entityId, 'update');
42 if ($entity === null) return $this->jsonError("Entity not found", 404); 42 if ($entity === null) return $this->jsonError("Entity not found", 404);
43 43
44 - $inputAttributes = $request->input('attributes'); 44 + $inputTags = $request->input('tags');
45 - $attributes = $this->attributeRepo->saveAttributesToEntity($entity, $inputAttributes); 45 + $tags = $this->tagRepo->saveTagsToEntity($entity, $inputTags);
46 return response()->json([ 46 return response()->json([
47 - 'attributes' => $attributes, 47 + 'tags' => $tags,
48 - 'message' => 'Attributes successfully updated' 48 + 'message' => 'Tags successfully updated'
49 ]); 49 ]);
50 } 50 }
51 51
52 /** 52 /**
53 - * Get attribute name suggestions from a given search term. 53 + * Get tag name suggestions from a given search term.
54 * @param Request $request 54 * @param Request $request
55 */ 55 */
56 public function getNameSuggestions(Request $request) 56 public function getNameSuggestions(Request $request)
57 { 57 {
58 $searchTerm = $request->get('search'); 58 $searchTerm = $request->get('search');
59 - $suggestions = $this->attributeRepo->getNameSuggestions($searchTerm); 59 + $suggestions = $this->tagRepo->getNameSuggestions($searchTerm);
60 return response()->json($suggestions); 60 return response()->json($suggestions);
61 } 61 }
62 62
......
...@@ -28,7 +28,7 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -28,7 +28,7 @@ Route::group(['middleware' => 'auth'], function () {
28 // Pages 28 // Pages
29 Route::get('/{bookSlug}/page/create', 'PageController@create'); 29 Route::get('/{bookSlug}/page/create', 'PageController@create');
30 Route::get('/{bookSlug}/draft/{pageId}', 'PageController@editDraft'); 30 Route::get('/{bookSlug}/draft/{pageId}', 'PageController@editDraft');
31 - Route::post('/{bookSlug}/page/{pageId}', 'PageController@store'); 31 + Route::post('/{bookSlug}/draft/{pageId}', 'PageController@store');
32 Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show'); 32 Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show');
33 Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageController@exportPdf'); 33 Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageController@exportPdf');
34 Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageController@exportHtml'); 34 Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageController@exportHtml');
...@@ -85,11 +85,11 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -85,11 +85,11 @@ Route::group(['middleware' => 'auth'], function () {
85 Route::get('/ajax/page/{id}', 'PageController@getPageAjax'); 85 Route::get('/ajax/page/{id}', 'PageController@getPageAjax');
86 Route::delete('/ajax/page/{id}', 'PageController@ajaxDestroy'); 86 Route::delete('/ajax/page/{id}', 'PageController@ajaxDestroy');
87 87
88 - // Attribute routes (AJAX) 88 + // Tag routes (AJAX)
89 - Route::group(['prefix' => 'ajax/attributes'], function() { 89 + Route::group(['prefix' => 'ajax/tags'], function() {
90 - Route::get('/get/{entityType}/{entityId}', 'AttributeController@getForEntity'); 90 + Route::get('/get/{entityType}/{entityId}', 'TagController@getForEntity');
91 - Route::get('/suggest', 'AttributeController@getNameSuggestions'); 91 + Route::get('/suggest', 'TagController@getNameSuggestions');
92 - Route::post('/update/{entityType}/{entityId}', 'AttributeController@updateForEntity'); 92 + Route::post('/update/{entityType}/{entityId}', 'TagController@updateForEntity');
93 }); 93 });
94 94
95 // Links 95 // Links
......
...@@ -582,7 +582,7 @@ class PageRepo extends EntityRepo ...@@ -582,7 +582,7 @@ class PageRepo extends EntityRepo
582 { 582 {
583 Activity::removeEntity($page); 583 Activity::removeEntity($page);
584 $page->views()->delete(); 584 $page->views()->delete();
585 - $page->attributes()->delete(); 585 + $page->tags()->delete();
586 $page->revisions()->delete(); 586 $page->revisions()->delete();
587 $page->permissions()->delete(); 587 $page->permissions()->delete();
588 $this->permissionService->deleteJointPermissionsForEntity($page); 588 $this->permissionService->deleteJointPermissionsForEntity($page);
......
1 <?php namespace BookStack\Repos; 1 <?php namespace BookStack\Repos;
2 2
3 -use BookStack\Attribute; 3 +use BookStack\Tag;
4 use BookStack\Entity; 4 use BookStack\Entity;
5 use BookStack\Services\PermissionService; 5 use BookStack\Services\PermissionService;
6 6
7 /** 7 /**
8 - * Class AttributeRepo 8 + * Class TagRepo
9 * @package BookStack\Repos 9 * @package BookStack\Repos
10 */ 10 */
11 -class AttributeRepo 11 +class TagRepo
12 { 12 {
13 13
14 - protected $attribute; 14 + protected $tag;
15 protected $entity; 15 protected $entity;
16 protected $permissionService; 16 protected $permissionService;
17 17
18 /** 18 /**
19 - * AttributeRepo constructor. 19 + * TagRepo constructor.
20 - * @param Attribute $attr 20 + * @param Tag $attr
21 * @param Entity $ent 21 * @param Entity $ent
22 * @param PermissionService $ps 22 * @param PermissionService $ps
23 */ 23 */
24 - public function __construct(Attribute $attr, Entity $ent, PermissionService $ps) 24 + public function __construct(Tag $attr, Entity $ent, PermissionService $ps)
25 { 25 {
26 - $this->attribute = $attr; 26 + $this->tag = $attr;
27 $this->entity = $ent; 27 $this->entity = $ent;
28 $this->permissionService = $ps; 28 $this->permissionService = $ps;
29 } 29 }
...@@ -37,13 +37,13 @@ class AttributeRepo ...@@ -37,13 +37,13 @@ class AttributeRepo
37 public function getEntity($entityType, $entityId, $action = 'view') 37 public function getEntity($entityType, $entityId, $action = 'view')
38 { 38 {
39 $entityInstance = $this->entity->getEntityInstance($entityType); 39 $entityInstance = $this->entity->getEntityInstance($entityType);
40 - $searchQuery = $entityInstance->where('id', '=', $entityId)->with('attributes'); 40 + $searchQuery = $entityInstance->where('id', '=', $entityId)->with('tags');
41 $searchQuery = $this->permissionService->enforceEntityRestrictions($searchQuery, $action); 41 $searchQuery = $this->permissionService->enforceEntityRestrictions($searchQuery, $action);
42 return $searchQuery->first(); 42 return $searchQuery->first();
43 } 43 }
44 44
45 /** 45 /**
46 - * Get all attributes for a particular entity. 46 + * Get all tags for a particular entity.
47 * @param string $entityType 47 * @param string $entityType
48 * @param int $entityId 48 * @param int $entityId
49 * @return mixed 49 * @return mixed
...@@ -53,42 +53,42 @@ class AttributeRepo ...@@ -53,42 +53,42 @@ class AttributeRepo
53 $entity = $this->getEntity($entityType, $entityId); 53 $entity = $this->getEntity($entityType, $entityId);
54 if ($entity === null) return collect(); 54 if ($entity === null) return collect();
55 55
56 - return $entity->attributes; 56 + return $entity->tags;
57 } 57 }
58 58
59 /** 59 /**
60 - * Get attribute name suggestions from scanning existing attribute names. 60 + * Get tag name suggestions from scanning existing tag names.
61 * @param $searchTerm 61 * @param $searchTerm
62 * @return array 62 * @return array
63 */ 63 */
64 public function getNameSuggestions($searchTerm) 64 public function getNameSuggestions($searchTerm)
65 { 65 {
66 if ($searchTerm === '') return []; 66 if ($searchTerm === '') return [];
67 - $query = $this->attribute->where('name', 'LIKE', $searchTerm . '%')->groupBy('name')->orderBy('name', 'desc'); 67 + $query = $this->tag->where('name', 'LIKE', $searchTerm . '%')->groupBy('name')->orderBy('name', 'desc');
68 - $query = $this->permissionService->filterRestrictedEntityRelations($query, 'attributes', 'entity_id', 'entity_type'); 68 + $query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
69 return $query->get(['name'])->pluck('name'); 69 return $query->get(['name'])->pluck('name');
70 } 70 }
71 71
72 /** 72 /**
73 - * Save an array of attributes to an entity 73 + * Save an array of tags to an entity
74 * @param Entity $entity 74 * @param Entity $entity
75 - * @param array $attributes 75 + * @param array $tags
76 * @return array|\Illuminate\Database\Eloquent\Collection 76 * @return array|\Illuminate\Database\Eloquent\Collection
77 */ 77 */
78 - public function saveAttributesToEntity(Entity $entity, $attributes = []) 78 + public function saveTagsToEntity(Entity $entity, $tags = [])
79 { 79 {
80 - $entity->attributes()->delete(); 80 + $entity->tags()->delete();
81 - $newAttributes = []; 81 + $newTags = [];
82 - foreach ($attributes as $attribute) { 82 + foreach ($tags as $tag) {
83 - if (trim($attribute['name']) === '') continue; 83 + if (trim($tag['name']) === '') continue;
84 - $newAttributes[] = $this->newInstanceFromInput($attribute); 84 + $newTags[] = $this->newInstanceFromInput($tag);
85 } 85 }
86 86
87 - return $entity->attributes()->saveMany($newAttributes); 87 + return $entity->tags()->saveMany($newTags);
88 } 88 }
89 89
90 /** 90 /**
91 - * Create a new Attribute instance from user input. 91 + * Create a new Tag instance from user input.
92 * @param $input 92 * @param $input
93 * @return static 93 * @return static
94 */ 94 */
...@@ -98,7 +98,7 @@ class AttributeRepo ...@@ -98,7 +98,7 @@ class AttributeRepo
98 $value = isset($input['value']) ? trim($input['value']) : ''; 98 $value = isset($input['value']) ? trim($input['value']) : '';
99 // Any other modification or cleanup required can go here 99 // Any other modification or cleanup required can go here
100 $values = ['name' => $name, 'value' => $value]; 100 $values = ['name' => $name, 'value' => $value];
101 - return $this->attribute->newInstance($values); 101 + return $this->tag->newInstance($values);
102 } 102 }
103 103
104 } 104 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -4,12 +4,12 @@ ...@@ -4,12 +4,12 @@
4 * Class Attribute 4 * Class Attribute
5 * @package BookStack 5 * @package BookStack
6 */ 6 */
7 -class Attribute extends Model 7 +class Tag extends Model
8 { 8 {
9 protected $fillable = ['name', 'value', 'order']; 9 protected $fillable = ['name', 'value', 'order'];
10 10
11 /** 11 /**
12 - * Get the entity that this attribute belongs to 12 + * Get the entity that this tag belongs to
13 * @return \Illuminate\Database\Eloquent\Relations\MorphTo 13 * @return \Illuminate\Database\Eloquent\Relations\MorphTo
14 */ 14 */
15 public function entity() 15 public function entity()
......
...@@ -54,7 +54,7 @@ $factory->define(BookStack\Role::class, function ($faker) { ...@@ -54,7 +54,7 @@ $factory->define(BookStack\Role::class, function ($faker) {
54 ]; 54 ];
55 }); 55 });
56 56
57 -$factory->define(BookStack\Attribute::class, function ($faker) { 57 +$factory->define(BookStack\Tag::class, function ($faker) {
58 return [ 58 return [
59 'name' => $faker->city, 59 'name' => $faker->city,
60 'value' => $faker->sentence(3) 60 'value' => $faker->sentence(3)
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
3 use Illuminate\Database\Schema\Blueprint; 3 use Illuminate\Database\Schema\Blueprint;
4 use Illuminate\Database\Migrations\Migration; 4 use Illuminate\Database\Migrations\Migration;
5 5
6 -class CreateAttributesTable extends Migration 6 +class CreateTagsTable extends Migration
7 { 7 {
8 /** 8 /**
9 * Run the migrations. 9 * Run the migrations.
...@@ -12,7 +12,7 @@ class CreateAttributesTable extends Migration ...@@ -12,7 +12,7 @@ class CreateAttributesTable extends Migration
12 */ 12 */
13 public function up() 13 public function up()
14 { 14 {
15 - Schema::create('attributes', function (Blueprint $table) { 15 + Schema::create('tags', function (Blueprint $table) {
16 $table->increments('id'); 16 $table->increments('id');
17 $table->integer('entity_id'); 17 $table->integer('entity_id');
18 $table->string('entity_type', 100); 18 $table->string('entity_type', 100);
...@@ -35,6 +35,6 @@ class CreateAttributesTable extends Migration ...@@ -35,6 +35,6 @@ class CreateAttributesTable extends Migration
35 */ 35 */
36 public function down() 36 public function down()
37 { 37 {
38 - Schema::drop('attributes'); 38 + Schema::drop('tags');
39 } 39 }
40 } 40 }
......
...@@ -400,75 +400,75 @@ module.exports = function (ngApp, events) { ...@@ -400,75 +400,75 @@ module.exports = function (ngApp, events) {
400 400
401 }]); 401 }]);
402 402
403 - ngApp.controller('PageAttributeController', ['$scope', '$http', '$attrs', 403 + ngApp.controller('PageTagController', ['$scope', '$http', '$attrs',
404 function ($scope, $http, $attrs) { 404 function ($scope, $http, $attrs) {
405 405
406 const pageId = Number($attrs.pageId); 406 const pageId = Number($attrs.pageId);
407 - $scope.attributes = []; 407 + $scope.tags = [];
408 408
409 /** 409 /**
410 - * Push an empty attribute to the end of the scope attributes. 410 + * Push an empty tag to the end of the scope tags.
411 */ 411 */
412 - function addEmptyAttribute() { 412 + function addEmptyTag() {
413 - $scope.attributes.push({ 413 + $scope.tags.push({
414 name: '', 414 name: '',
415 value: '' 415 value: ''
416 }); 416 });
417 } 417 }
418 418
419 /** 419 /**
420 - * Get all attributes for the current book and add into scope. 420 + * Get all tags for the current book and add into scope.
421 */ 421 */
422 - function getAttributes() { 422 + function getTags() {
423 - $http.get('/ajax/attributes/get/page/' + pageId).then((responseData) => { 423 + $http.get('/ajax/tags/get/page/' + pageId).then((responseData) => {
424 - $scope.attributes = responseData.data; 424 + $scope.tags = responseData.data;
425 - addEmptyAttribute(); 425 + addEmptyTag();
426 }); 426 });
427 } 427 }
428 - getAttributes(); 428 + getTags();
429 429
430 /** 430 /**
431 - * Set the order property on all attributes. 431 + * Set the order property on all tags.
432 */ 432 */
433 - function setAttributeOrder() { 433 + function setTagOrder() {
434 - for (let i = 0; i < $scope.attributes.length; i++) { 434 + for (let i = 0; i < $scope.tags.length; i++) {
435 - $scope.attributes[i].order = i; 435 + $scope.tags[i].order = i;
436 } 436 }
437 } 437 }
438 438
439 /** 439 /**
440 - * When an attribute changes check if another empty editable 440 + * When an tag changes check if another empty editable
441 * field needs to be added onto the end. 441 * field needs to be added onto the end.
442 - * @param attribute 442 + * @param tag
443 */ 443 */
444 - $scope.attributeChange = function(attribute) { 444 + $scope.tagChange = function(tag) {
445 - let cPos = $scope.attributes.indexOf(attribute); 445 + let cPos = $scope.tags.indexOf(tag);
446 - if (cPos !== $scope.attributes.length-1) return; 446 + if (cPos !== $scope.tags.length-1) return;
447 447
448 - if (attribute.name !== '' || attribute.value !== '') { 448 + if (tag.name !== '' || tag.value !== '') {
449 - addEmptyAttribute(); 449 + addEmptyTag();
450 } 450 }
451 }; 451 };
452 452
453 /** 453 /**
454 - * When an attribute field loses focus check the attribute to see if its 454 + * When an tag field loses focus check the tag to see if its
455 * empty and therefore could be removed from the list. 455 * empty and therefore could be removed from the list.
456 - * @param attribute 456 + * @param tag
457 */ 457 */
458 - $scope.attributeBlur = function(attribute) { 458 + $scope.tagBlur = function(tag) {
459 - let isLast = $scope.attributes.length - 1 === $scope.attributes.indexOf(attribute); 459 + let isLast = $scope.tags.length - 1 === $scope.tags.indexOf(tag);
460 - if (attribute.name === '' && attribute.value === '' && !isLast) { 460 + if (tag.name === '' && tag.value === '' && !isLast) {
461 - let cPos = $scope.attributes.indexOf(attribute); 461 + let cPos = $scope.tags.indexOf(tag);
462 - $scope.attributes.splice(cPos, 1); 462 + $scope.tags.splice(cPos, 1);
463 } 463 }
464 }; 464 };
465 465
466 - $scope.saveAttributes = function() { 466 + $scope.saveTags = function() {
467 - setAttributeOrder(); 467 + setTagOrder();
468 - let postData = {attributes: $scope.attributes}; 468 + let postData = {tags: $scope.tags};
469 - $http.post('/ajax/attributes/update/page/' + pageId, postData).then((responseData) => { 469 + $http.post('/ajax/tags/update/page/' + pageId, postData).then((responseData) => {
470 - $scope.attributes = responseData.data.attributes; 470 + $scope.tags = responseData.data.tags;
471 - addEmptyAttribute(); 471 + addEmptyTag();
472 events.emit('success', responseData.data.message); 472 events.emit('success', responseData.data.message);
473 }) 473 })
474 }; 474 };
......
...@@ -239,6 +239,17 @@ div[editor-type="markdown"] .title-input.page-title input[type="text"] { ...@@ -239,6 +239,17 @@ div[editor-type="markdown"] .title-input.page-title input[type="text"] {
239 } 239 }
240 } 240 }
241 241
242 +input.outline {
243 + border: 0;
244 + border-bottom: 2px solid #DDD;
245 + border-radius: 0;
246 + &:focus, &:active {
247 + border: 0;
248 + border-bottom: 2px solid #AAA;
249 + outline: 0;
250 + }
251 +}
252 +
242 #login-form label[for="remember"] { 253 #login-form label[for="remember"] {
243 margin: 0; 254 margin: 0;
244 } 255 }
......
...@@ -26,6 +26,13 @@ table { ...@@ -26,6 +26,13 @@ table {
26 } 26 }
27 } 27 }
28 28
29 +table.no-style {
30 + td {
31 + border: 0;
32 + padding: 0;
33 + }
34 +}
35 +
29 table.list-table { 36 table.list-table {
30 margin: 0 -$-xs; 37 margin: 0 -$-xs;
31 td { 38 td {
......
...@@ -208,11 +208,70 @@ $btt-size: 40px; ...@@ -208,11 +208,70 @@ $btt-size: 40px;
208 background-color: #FFF; 208 background-color: #FFF;
209 border: 1px solid #BBB; 209 border: 1px solid #BBB;
210 border-radius: 3px; 210 border-radius: 3px;
211 - padding: $-l;
212 position: fixed; 211 position: fixed;
213 right: $-xl*2; 212 right: $-xl*2;
214 top: 100px; 213 top: 100px;
215 z-index: 99; 214 z-index: 99;
216 height: 800px; 215 height: 800px;
216 + width: 480px;
217 overflow-y: scroll; 217 overflow-y: scroll;
218 + display: flex;
219 + align-items: stretch;
220 + flex-direction: row;
221 + > div {
222 + flex: 1;
223 + position: relative;
224 + }
225 + .tabs {
226 + display: block;
227 + border-right: 1px solid #DDD;
228 + width: 54px;
229 + flex: 0;
230 + }
231 + .tabs i {
232 + padding: 0;
233 + margin: 0;
234 + }
235 + .tabs [tab-button] {
236 + display: block;
237 + cursor: pointer;
238 + color: #666;
239 + padding: $-m;
240 + border-bottom: 1px solid rgba(255, 255, 255, 0.3);
241 + &.active {
242 + color: #444;
243 + background-color: rgba(0, 0, 0, 0.1);
244 + }
245 + }
246 + div[tab-content] .padded {
247 + padding: 0 $-m;
248 + }
249 + h4 {
250 + font-size: 24px;
251 + margin: $-m 0 0 0;
252 + padding: 0 $-m;
253 + }
254 + .tags input {
255 + max-width: 100%;
256 + width: 100%;
257 + min-width: 50px;
258 + }
259 + .tags td {
260 + padding-right: $-s;
261 + padding-top: $-s;
262 + }
263 + button.pos {
264 + position: absolute;
265 + bottom: 0;
266 + display: block;
267 + width: 100%;
268 + padding: $-s;
269 + border: 0;
270 + margin: 0;
271 + box-shadow: none;
272 + border-radius: 0;
273 + &:hover{
274 + box-shadow: none;
275 + }
276 + }
218 } 277 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -16,17 +16,7 @@ ...@@ -16,17 +16,7 @@
16 @include('pages/form', ['model' => $page]) 16 @include('pages/form', ['model' => $page])
17 </form> 17 </form>
18 18
19 - <div class="floating-toolbox" ng-controller="PageAttributeController" page-id="{{ $page->id or 0 }}"> 19 + @include('pages/form-toolbox')
20 - <form ng-submit="saveAttributes()">
21 - <table>
22 - <tr ng-repeat="attribute in attributes">
23 - <td><input type="text" ng-model="attribute.name" ng-change="attributeChange(attribute)" ng-blur="attributeBlur(attribute)" placeholder="Attribute Name"></td>
24 - <td><input type="text" ng-model="attribute.value" ng-change="attributeChange(attribute)" ng-blur="attributeBlur(attribute)" placeholder="Value"></td>
25 - </tr>
26 - </table>
27 - <button class="button pos" type="submit">Save attributes</button>
28 - </form>
29 - </div>
30 20
31 </div> 21 </div>
32 @include('partials/image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id]) 22 @include('partials/image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id])
......
1 +<div class="floating-toolbox">
2 + <div class="tabs primary-background-light">
3 + <span tab-button class="active"><i class="zmdi zmdi-tag"></i></span>
4 + <span tab-button><i class="zmdi zmdi-wrench"></i></span>
5 + </div>
6 + <div tab-content ng-controller="PageTagController" page-id="{{ $page->id or 0 }}">
7 + <form ng-submit="saveTags()" >
8 + <h4>Page Tags</h4>
9 + <div class="padded tags">
10 + <table class="no-style" style="width: 100%;">
11 + <tr ng-repeat="tag in tags">
12 + <td><input class="outline" type="text" ng-model="tag.name" ng-change="tagChange(tag)" ng-blur="tagBlur(tag)" placeholder="Tag"></td>
13 + <td><input class="outline" type="text" ng-model="tag.value" ng-change="tagChange(tag)" ng-blur="tagBlur(tag)" placeholder="Tag Value (Optional)"></td>
14 + </tr>
15 + </table>
16 + </div>
17 + <button class="button pos" type="submit">Save Tags</button>
18 + </form>
19 + </div>
20 +</div>
...\ No newline at end of file ...\ No newline at end of file
1 @if(Setting::get('app-color')) 1 @if(Setting::get('app-color'))
2 <style> 2 <style>
3 - header, #back-to-top { 3 + header, #back-to-top, .primary-background {
4 background-color: {{ Setting::get('app-color') }}; 4 background-color: {{ Setting::get('app-color') }};
5 } 5 }
6 - .faded-small { 6 + .faded-small, .primary-background-light {
7 background-color: {{ Setting::get('app-color-light') }}; 7 background-color: {{ Setting::get('app-color-light') }};
8 } 8 }
9 .button-base, .button, input[type="button"], input[type="submit"] { 9 .button-base, .button, input[type="button"], input[type="submit"] {
......
1 <?php namespace Entity; 1 <?php namespace Entity;
2 2
3 -use BookStack\Attribute; 3 +use BookStack\Tag;
4 use BookStack\Page; 4 use BookStack\Page;
5 use BookStack\Services\PermissionService; 5 use BookStack\Services\PermissionService;
6 6
7 -class AttributeTests extends \TestCase 7 +class TagTests extends \TestCase
8 { 8 {
9 9
10 - protected $defaultAttrCount = 20; 10 + protected $defaultTagCount = 20;
11 11
12 /** 12 /**
13 - * Get an instance of a page that has many attributes. 13 + * Get an instance of a page that has many tags.
14 - * @param Attribute[]|bool $attributes 14 + * @param Tag[]|bool $tags
15 * @return mixed 15 * @return mixed
16 */ 16 */
17 - protected function getPageWithAttributes($attributes = false) 17 + protected function getPageWithTags($tags = false)
18 { 18 {
19 $page = Page::first(); 19 $page = Page::first();
20 20
21 - if (!$attributes) { 21 + if (!$tags) {
22 - $attributes = factory(Attribute::class, $this->defaultAttrCount)->make(); 22 + $tags = factory(Tag::class, $this->defaultTagCount)->make();
23 } 23 }
24 24
25 - $page->attributes()->saveMany($attributes); 25 + $page->tags()->saveMany($tags);
26 return $page; 26 return $page;
27 } 27 }
28 28
29 - public function test_get_page_attributes() 29 + public function test_get_page_tags()
30 { 30 {
31 - $page = $this->getPageWithAttributes(); 31 + $page = $this->getPageWithTags();
32 32
33 - // Add some other attributes to check they don't interfere 33 + // Add some other tags to check they don't interfere
34 - factory(Attribute::class, $this->defaultAttrCount)->create(); 34 + factory(Tag::class, $this->defaultTagCount)->create();
35 35
36 - $this->asAdmin()->get("/ajax/attributes/get/page/" . $page->id) 36 + $this->asAdmin()->get("/ajax/tags/get/page/" . $page->id)
37 ->shouldReturnJson(); 37 ->shouldReturnJson();
38 38
39 $json = json_decode($this->response->getContent()); 39 $json = json_decode($this->response->getContent());
40 - $this->assertTrue(count($json) === $this->defaultAttrCount, "Returned JSON item count is not as expected"); 40 + $this->assertTrue(count($json) === $this->defaultTagCount, "Returned JSON item count is not as expected");
41 } 41 }
42 42
43 - public function test_attribute_name_suggestions() 43 + public function test_tag_name_suggestions()
44 { 44 {
45 - // Create some attributes with similar names to test with 45 + // Create some tags with similar names to test with
46 $attrs = collect(); 46 $attrs = collect();
47 - $attrs = $attrs->merge(factory(Attribute::class, 5)->make(['name' => 'country'])); 47 + $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'country']));
48 - $attrs = $attrs->merge(factory(Attribute::class, 5)->make(['name' => 'color'])); 48 + $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'color']));
49 - $attrs = $attrs->merge(factory(Attribute::class, 5)->make(['name' => 'city'])); 49 + $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'city']));
50 - $attrs = $attrs->merge(factory(Attribute::class, 5)->make(['name' => 'county'])); 50 + $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'county']));
51 - $attrs = $attrs->merge(factory(Attribute::class, 5)->make(['name' => 'planet'])); 51 + $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'planet']));
52 - $attrs = $attrs->merge(factory(Attribute::class, 5)->make(['name' => 'plans'])); 52 + $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'plans']));
53 - $page = $this->getPageWithAttributes($attrs); 53 + $page = $this->getPageWithTags($attrs);
54 - 54 +
55 - $this->asAdmin()->get('/ajax/attributes/suggest?search=dog')->seeJsonEquals([]); 55 + $this->asAdmin()->get('/ajax/tags/suggest?search=dog')->seeJsonEquals([]);
56 - $this->get('/ajax/attributes/suggest?search=co')->seeJsonEquals(['color', 'country', 'county']); 56 + $this->get('/ajax/tags/suggest?search=co')->seeJsonEquals(['color', 'country', 'county']);
57 - $this->get('/ajax/attributes/suggest?search=cou')->seeJsonEquals(['country', 'county']); 57 + $this->get('/ajax/tags/suggest?search=cou')->seeJsonEquals(['country', 'county']);
58 - $this->get('/ajax/attributes/suggest?search=pla')->seeJsonEquals(['planet', 'plans']); 58 + $this->get('/ajax/tags/suggest?search=pla')->seeJsonEquals(['planet', 'plans']);
59 } 59 }
60 60
61 - public function test_entity_permissions_effect_attribute_suggestions() 61 + public function test_entity_permissions_effect_tag_suggestions()
62 { 62 {
63 $permissionService = $this->app->make(PermissionService::class); 63 $permissionService = $this->app->make(PermissionService::class);
64 64
65 - // Create some attributes with similar names to test with and save to a page 65 + // Create some tags with similar names to test with and save to a page
66 $attrs = collect(); 66 $attrs = collect();
67 - $attrs = $attrs->merge(factory(Attribute::class, 5)->make(['name' => 'country'])); 67 + $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'country']));
68 - $attrs = $attrs->merge(factory(Attribute::class, 5)->make(['name' => 'color'])); 68 + $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'color']));
69 - $page = $this->getPageWithAttributes($attrs); 69 + $page = $this->getPageWithTags($attrs);
70 70
71 - $this->asAdmin()->get('/ajax/attributes/suggest?search=co')->seeJsonEquals(['color', 'country']); 71 + $this->asAdmin()->get('/ajax/tags/suggest?search=co')->seeJsonEquals(['color', 'country']);
72 - $this->asEditor()->get('/ajax/attributes/suggest?search=co')->seeJsonEquals(['color', 'country']); 72 + $this->asEditor()->get('/ajax/tags/suggest?search=co')->seeJsonEquals(['color', 'country']);
73 73
74 // Set restricted permission the page 74 // Set restricted permission the page
75 $page->restricted = true; 75 $page->restricted = true;
76 $page->save(); 76 $page->save();
77 $permissionService->buildJointPermissionsForEntity($page); 77 $permissionService->buildJointPermissionsForEntity($page);
78 78
79 - $this->asAdmin()->get('/ajax/attributes/suggest?search=co')->seeJsonEquals(['color', 'country']); 79 + $this->asAdmin()->get('/ajax/tags/suggest?search=co')->seeJsonEquals(['color', 'country']);
80 - $this->asEditor()->get('/ajax/attributes/suggest?search=co')->seeJsonEquals([]); 80 + $this->asEditor()->get('/ajax/tags/suggest?search=co')->seeJsonEquals([]);
81 } 81 }
82 82
83 - public function test_entity_attribute_updating() 83 + public function test_entity_tag_updating()
84 { 84 {
85 - $page = $this->getPageWithAttributes(); 85 + $page = $this->getPageWithTags();
86 86
87 $testJsonData = [ 87 $testJsonData = [
88 ['name' => 'color', 'value' => 'red'], 88 ['name' => 'color', 'value' => 'red'],
...@@ -98,23 +98,23 @@ class AttributeTests extends \TestCase ...@@ -98,23 +98,23 @@ class AttributeTests extends \TestCase
98 ]; 98 ];
99 99
100 // Do update request 100 // Do update request
101 - $this->asAdmin()->json("POST", "/ajax/attributes/update/page/" . $page->id, ['attributes' => $testJsonData]); 101 + $this->asAdmin()->json("POST", "/ajax/tags/update/page/" . $page->id, ['tags' => $testJsonData]);
102 $updateData = json_decode($this->response->getContent()); 102 $updateData = json_decode($this->response->getContent());
103 // Check data is correct 103 // Check data is correct
104 $testDataCorrect = true; 104 $testDataCorrect = true;
105 - foreach ($updateData->attributes as $data) { 105 + foreach ($updateData->tags as $data) {
106 $testItem = ['name' => $data->name, 'value' => $data->value]; 106 $testItem = ['name' => $data->name, 'value' => $data->value];
107 if (!in_array($testItem, $testResponseJsonData)) $testDataCorrect = false; 107 if (!in_array($testItem, $testResponseJsonData)) $testDataCorrect = false;
108 } 108 }
109 $testMessage = "Expected data was not found in the response.\nExpected Data: %s\nRecieved Data: %s"; 109 $testMessage = "Expected data was not found in the response.\nExpected Data: %s\nRecieved Data: %s";
110 $this->assertTrue($testDataCorrect, sprintf($testMessage, json_encode($testResponseJsonData), json_encode($updateData))); 110 $this->assertTrue($testDataCorrect, sprintf($testMessage, json_encode($testResponseJsonData), json_encode($updateData)));
111 - $this->assertTrue(isset($updateData->message), "No message returned in attribute update response"); 111 + $this->assertTrue(isset($updateData->message), "No message returned in tag update response");
112 112
113 // Do get request 113 // Do get request
114 - $this->asAdmin()->get("/ajax/attributes/get/page/" . $page->id); 114 + $this->asAdmin()->get("/ajax/tags/get/page/" . $page->id);
115 $getResponseData = json_decode($this->response->getContent()); 115 $getResponseData = json_decode($this->response->getContent());
116 // Check counts 116 // Check counts
117 - $this->assertTrue(count($getResponseData) === count($testJsonData), "The received attribute count is incorrect"); 117 + $this->assertTrue(count($getResponseData) === count($testJsonData), "The received tag count is incorrect");
118 // Check data is correct 118 // Check data is correct
119 $testDataCorrect = true; 119 $testDataCorrect = true;
120 foreach ($getResponseData as $data) { 120 foreach ($getResponseData as $data) {
......