Dan Brown

Started work on attachments

Created base models and started user-facing controls.
1 +<?php namespace BookStack\Exceptions;
2 +
3 +
4 +class FileUploadException extends PrettyException {}
...\ No newline at end of file ...\ No newline at end of file
1 +<?php namespace BookStack;
2 +
3 +
4 +class File extends Ownable
5 +{
6 + protected $fillable = ['name', 'order'];
7 +
8 + /**
9 + * Get the page this file was uploaded to.
10 + * @return mixed
11 + */
12 + public function page()
13 + {
14 + return $this->belongsTo(Page::class, 'uploaded_to');
15 + }
16 +
17 +
18 +}
1 +<?php
2 +
3 +namespace BookStack\Http\Controllers;
4 +
5 +use BookStack\Exceptions\FileUploadException;
6 +use BookStack\File;
7 +use BookStack\Page;
8 +use BookStack\Repos\PageRepo;
9 +use BookStack\Services\FileService;
10 +use Illuminate\Http\Request;
11 +
12 +use BookStack\Http\Requests;
13 +
14 +class FileController extends Controller
15 +{
16 + protected $fileService;
17 + protected $file;
18 + protected $pageRepo;
19 +
20 + /**
21 + * FileController constructor.
22 + * @param FileService $fileService
23 + * @param File $file
24 + * @param PageRepo $pageRepo
25 + */
26 + public function __construct(FileService $fileService, File $file, PageRepo $pageRepo)
27 + {
28 + $this->fileService = $fileService;
29 + $this->file = $file;
30 + $this->pageRepo = $pageRepo;
31 + }
32 +
33 +
34 + /**
35 + * Endpoint at which files are uploaded to.
36 + * @param Request $request
37 + */
38 + public function upload(Request $request)
39 + {
40 + // TODO - Add file upload permission check
41 + // TODO - ensure user has permission to edit relevant page.
42 + // TODO - ensure uploads are deleted on page delete.
43 +
44 + $this->validate($request, [
45 + 'uploaded_to' => 'required|integer|exists:pages,id'
46 + ]);
47 +
48 + $uploadedFile = $request->file('file');
49 + $pageId = $request->get('uploaded_to');
50 +
51 + try {
52 + $file = $this->fileService->saveNewUpload($uploadedFile, $pageId);
53 + } catch (FileUploadException $e) {
54 + return response($e->getMessage(), 500);
55 + }
56 +
57 + return response()->json($file);
58 + }
59 +
60 + /**
61 + * Get the files for a specific page.
62 + * @param $pageId
63 + * @return mixed
64 + */
65 + public function getFilesForPage($pageId)
66 + {
67 + // TODO - check view permission on page?
68 + $page = $this->pageRepo->getById($pageId);
69 + return response()->json($page->files);
70 + }
71 +
72 + /**
73 + * Update the file sorting.
74 + * @param $pageId
75 + * @param Request $request
76 + * @return mixed
77 + */
78 + public function sortFilesForPage($pageId, Request $request)
79 + {
80 + $this->validate($request, [
81 + 'files' => 'required|array',
82 + 'files.*.id' => 'required|integer',
83 + ]);
84 + $page = $this->pageRepo->getById($pageId);
85 + $files = $request->get('files');
86 + $this->fileService->updateFileOrderWithinPage($files, $pageId);
87 + return response()->json(['message' => 'File order updated']);
88 + }
89 +
90 +
91 +}
...@@ -55,6 +55,15 @@ class Page extends Entity ...@@ -55,6 +55,15 @@ class Page extends Entity
55 } 55 }
56 56
57 /** 57 /**
58 + * Get the files attached to this page.
59 + * @return \Illuminate\Database\Eloquent\Relations\HasMany
60 + */
61 + public function files()
62 + {
63 + return $this->hasMany(File::class, 'uploaded_to')->orderBy('order', 'asc');
64 + }
65 +
66 + /**
58 * Get the url for this page. 67 * Get the url for this page.
59 * @param string|bool $path 68 * @param string|bool $path
60 * @return string 69 * @return string
......
...@@ -48,7 +48,7 @@ class PageRepo extends EntityRepo ...@@ -48,7 +48,7 @@ class PageRepo extends EntityRepo
48 * Get a page via a specific ID. 48 * Get a page via a specific ID.
49 * @param $id 49 * @param $id
50 * @param bool $allowDrafts 50 * @param bool $allowDrafts
51 - * @return mixed 51 + * @return Page
52 */ 52 */
53 public function getById($id, $allowDrafts = false) 53 public function getById($id, $allowDrafts = false)
54 { 54 {
......
1 +<?php namespace BookStack\Services;
2 +
3 +
4 +use BookStack\Exceptions\FileUploadException;
5 +use BookStack\File;
6 +use Exception;
7 +use Illuminate\Support\Collection;
8 +use Symfony\Component\HttpFoundation\File\UploadedFile;
9 +
10 +class FileService extends UploadService
11 +{
12 +
13 + /**
14 + * Store a new file upon user upload.
15 + * @param UploadedFile $uploadedFile
16 + * @param int $page_id
17 + * @return File
18 + * @throws FileUploadException
19 + */
20 + public function saveNewUpload(UploadedFile $uploadedFile, $page_id)
21 + {
22 + $fileName = $uploadedFile->getClientOriginalName();
23 + $fileData = file_get_contents($uploadedFile->getRealPath());
24 +
25 + $storage = $this->getStorage();
26 + $fileBasePath = 'uploads/files/' . Date('Y-m-M') . '/';
27 + $storageBasePath = $this->getStorageBasePath() . $fileBasePath;
28 +
29 + $uploadFileName = $fileName;
30 + while ($storage->exists($storageBasePath . $uploadFileName)) {
31 + $uploadFileName = str_random(3) . $uploadFileName;
32 + }
33 +
34 + $filePath = $fileBasePath . $uploadFileName;
35 + $fileStoragePath = $this->getStorageBasePath() . $filePath;
36 +
37 + try {
38 + $storage->put($fileStoragePath, $fileData);
39 + } catch (Exception $e) {
40 + throw new FileUploadException('File path ' . $fileStoragePath . ' could not be uploaded to. Ensure it is writable to the server.');
41 + }
42 +
43 + $largestExistingOrder = File::where('uploaded_to', '=', $page_id)->max('order');
44 +
45 + $file = File::forceCreate([
46 + 'name' => $fileName,
47 + 'path' => $filePath,
48 + 'uploaded_to' => $page_id,
49 + 'created_by' => user()->id,
50 + 'updated_by' => user()->id,
51 + 'order' => $largestExistingOrder + 1
52 + ]);
53 +
54 + return $file;
55 + }
56 +
57 + /**
58 + * Get the file storage base path, amended for storage type.
59 + * This allows us to keep a generic path in the database.
60 + * @return string
61 + */
62 + private function getStorageBasePath()
63 + {
64 + return $this->isLocal() ? 'storage/' : '';
65 + }
66 +
67 + /**
68 + * Updates the file ordering for a listing of attached files.
69 + * @param array $fileList
70 + * @param $pageId
71 + */
72 + public function updateFileOrderWithinPage($fileList, $pageId)
73 + {
74 + foreach ($fileList as $index => $file) {
75 + File::where('uploaded_to', '=', $pageId)->where('id', '=', $file['id'])->update(['order' => $index]);
76 + }
77 + }
78 +
79 +}
...\ No newline at end of file ...\ No newline at end of file
...@@ -9,20 +9,13 @@ use Intervention\Image\ImageManager; ...@@ -9,20 +9,13 @@ use Intervention\Image\ImageManager;
9 use Illuminate\Contracts\Filesystem\Factory as FileSystem; 9 use Illuminate\Contracts\Filesystem\Factory as FileSystem;
10 use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance; 10 use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
11 use Illuminate\Contracts\Cache\Repository as Cache; 11 use Illuminate\Contracts\Cache\Repository as Cache;
12 -use Setting;
13 use Symfony\Component\HttpFoundation\File\UploadedFile; 12 use Symfony\Component\HttpFoundation\File\UploadedFile;
14 13
15 -class ImageService 14 +class ImageService extends UploadService
16 { 15 {
17 16
18 protected $imageTool; 17 protected $imageTool;
19 - protected $fileSystem;
20 protected $cache; 18 protected $cache;
21 -
22 - /**
23 - * @var FileSystemInstance
24 - */
25 - protected $storageInstance;
26 protected $storageUrl; 19 protected $storageUrl;
27 20
28 /** 21 /**
...@@ -34,8 +27,8 @@ class ImageService ...@@ -34,8 +27,8 @@ class ImageService
34 public function __construct(ImageManager $imageTool, FileSystem $fileSystem, Cache $cache) 27 public function __construct(ImageManager $imageTool, FileSystem $fileSystem, Cache $cache)
35 { 28 {
36 $this->imageTool = $imageTool; 29 $this->imageTool = $imageTool;
37 - $this->fileSystem = $fileSystem;
38 $this->cache = $cache; 30 $this->cache = $cache;
31 + parent::__construct($fileSystem);
39 } 32 }
40 33
41 /** 34 /**
...@@ -88,6 +81,9 @@ class ImageService ...@@ -88,6 +81,9 @@ class ImageService
88 if ($secureUploads) $imageName = str_random(16) . '-' . $imageName; 81 if ($secureUploads) $imageName = str_random(16) . '-' . $imageName;
89 82
90 $imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/'; 83 $imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/';
84 +
85 + if ($this->isLocal()) $imagePath = '/public' . $imagePath;
86 +
91 while ($storage->exists($imagePath . $imageName)) { 87 while ($storage->exists($imagePath . $imageName)) {
92 $imageName = str_random(3) . $imageName; 88 $imageName = str_random(3) . $imageName;
93 } 89 }
...@@ -100,6 +96,8 @@ class ImageService ...@@ -100,6 +96,8 @@ class ImageService
100 throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.'); 96 throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.');
101 } 97 }
102 98
99 + if ($this->isLocal()) $fullPath = str_replace_first('/public', '', $fullPath);
100 +
103 $imageDetails = [ 101 $imageDetails = [
104 'name' => $imageName, 102 'name' => $imageName,
105 'path' => $fullPath, 103 'path' => $fullPath,
...@@ -120,6 +118,16 @@ class ImageService ...@@ -120,6 +118,16 @@ class ImageService
120 } 118 }
121 119
122 /** 120 /**
121 + * Get the storage path, Dependant of storage type.
122 + * @param Image $image
123 + * @return mixed|string
124 + */
125 + protected function getPath(Image $image)
126 + {
127 + return ($this->isLocal()) ? ('public/' . $image->path) : $image->path;
128 + }
129 +
130 + /**
123 * Get the thumbnail for an image. 131 * Get the thumbnail for an image.
124 * If $keepRatio is true only the width will be used. 132 * If $keepRatio is true only the width will be used.
125 * Checks the cache then storage to avoid creating / accessing the filesystem on every check. 133 * Checks the cache then storage to avoid creating / accessing the filesystem on every check.
...@@ -135,7 +143,8 @@ class ImageService ...@@ -135,7 +143,8 @@ class ImageService
135 public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false) 143 public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
136 { 144 {
137 $thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/'; 145 $thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/';
138 - $thumbFilePath = dirname($image->path) . $thumbDirName . basename($image->path); 146 + $imagePath = $this->getPath($image);
147 + $thumbFilePath = dirname($imagePath) . $thumbDirName . basename($imagePath);
139 148
140 if ($this->cache->has('images-' . $image->id . '-' . $thumbFilePath) && $this->cache->get('images-' . $thumbFilePath)) { 149 if ($this->cache->has('images-' . $image->id . '-' . $thumbFilePath) && $this->cache->get('images-' . $thumbFilePath)) {
141 return $this->getPublicUrl($thumbFilePath); 150 return $this->getPublicUrl($thumbFilePath);
...@@ -148,7 +157,7 @@ class ImageService ...@@ -148,7 +157,7 @@ class ImageService
148 } 157 }
149 158
150 try { 159 try {
151 - $thumb = $this->imageTool->make($storage->get($image->path)); 160 + $thumb = $this->imageTool->make($storage->get($imagePath));
152 } catch (Exception $e) { 161 } catch (Exception $e) {
153 if ($e instanceof \ErrorException || $e instanceof NotSupportedException) { 162 if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
154 throw new ImageUploadException('The server cannot create thumbnails. Please check you have the GD PHP extension installed.'); 163 throw new ImageUploadException('The server cannot create thumbnails. Please check you have the GD PHP extension installed.');
...@@ -183,8 +192,8 @@ class ImageService ...@@ -183,8 +192,8 @@ class ImageService
183 { 192 {
184 $storage = $this->getStorage(); 193 $storage = $this->getStorage();
185 194
186 - $imageFolder = dirname($image->path); 195 + $imageFolder = dirname($this->getPath($image));
187 - $imageFileName = basename($image->path); 196 + $imageFileName = basename($this->getPath($image));
188 $allImages = collect($storage->allFiles($imageFolder)); 197 $allImages = collect($storage->allFiles($imageFolder));
189 198
190 $imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) { 199 $imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) {
...@@ -223,34 +232,8 @@ class ImageService ...@@ -223,34 +232,8 @@ class ImageService
223 } 232 }
224 233
225 /** 234 /**
226 - * Get the storage that will be used for storing images.
227 - * @return FileSystemInstance
228 - */
229 - private function getStorage()
230 - {
231 - if ($this->storageInstance !== null) return $this->storageInstance;
232 -
233 - $storageType = config('filesystems.default');
234 - $this->storageInstance = $this->fileSystem->disk($storageType);
235 -
236 - return $this->storageInstance;
237 - }
238 -
239 - /**
240 - * Check whether or not a folder is empty.
241 - * @param $path
242 - * @return int
243 - */
244 - private function isFolderEmpty($path)
245 - {
246 - $files = $this->getStorage()->files($path);
247 - $folders = $this->getStorage()->directories($path);
248 - return count($files) === 0 && count($folders) === 0;
249 - }
250 -
251 - /**
252 * Gets a public facing url for an image by checking relevant environment variables. 235 * Gets a public facing url for an image by checking relevant environment variables.
253 - * @param $filePath 236 + * @param string $filePath
254 * @return string 237 * @return string
255 */ 238 */
256 private function getPublicUrl($filePath) 239 private function getPublicUrl($filePath)
...@@ -273,6 +256,8 @@ class ImageService ...@@ -273,6 +256,8 @@ class ImageService
273 $this->storageUrl = $storageUrl; 256 $this->storageUrl = $storageUrl;
274 } 257 }
275 258
259 + if ($this->isLocal()) $filePath = str_replace_first('public/', '', $filePath);
260 +
276 return ($this->storageUrl == false ? rtrim(baseUrl(''), '/') : rtrim($this->storageUrl, '/')) . $filePath; 261 return ($this->storageUrl == false ? rtrim(baseUrl(''), '/') : rtrim($this->storageUrl, '/')) . $filePath;
277 } 262 }
278 263
......
1 +<?php namespace BookStack\Services;
2 +
3 +use Illuminate\Contracts\Filesystem\Factory as FileSystem;
4 +use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
5 +
6 +class UploadService
7 +{
8 +
9 + /**
10 + * @var FileSystem
11 + */
12 + protected $fileSystem;
13 +
14 + /**
15 + * @var FileSystemInstance
16 + */
17 + protected $storageInstance;
18 +
19 +
20 + /**
21 + * FileService constructor.
22 + * @param $fileSystem
23 + */
24 + public function __construct(FileSystem $fileSystem)
25 + {
26 + $this->fileSystem = $fileSystem;
27 + }
28 +
29 + /**
30 + * Get the storage that will be used for storing images.
31 + * @return FileSystemInstance
32 + */
33 + protected function getStorage()
34 + {
35 + if ($this->storageInstance !== null) return $this->storageInstance;
36 +
37 + $storageType = config('filesystems.default');
38 + $this->storageInstance = $this->fileSystem->disk($storageType);
39 +
40 + return $this->storageInstance;
41 + }
42 +
43 +
44 + /**
45 + * Check whether or not a folder is empty.
46 + * @param $path
47 + * @return bool
48 + */
49 + protected function isFolderEmpty($path)
50 + {
51 + $files = $this->getStorage()->files($path);
52 + $folders = $this->getStorage()->directories($path);
53 + return (count($files) === 0 && count($folders) === 0);
54 + }
55 +
56 + /**
57 + * Check if using a local filesystem.
58 + * @return bool
59 + */
60 + protected function isLocal()
61 + {
62 + return strtolower(config('filesystems.default')) === 'local';
63 + }
64 +}
...\ No newline at end of file ...\ No newline at end of file
...@@ -56,7 +56,7 @@ return [ ...@@ -56,7 +56,7 @@ return [
56 56
57 'local' => [ 57 'local' => [
58 'driver' => 'local', 58 'driver' => 'local',
59 - 'root' => public_path(), 59 + 'root' => base_path(),
60 ], 60 ],
61 61
62 'ftp' => [ 62 'ftp' => [
......
1 +<?php
2 +
3 +use Illuminate\Support\Facades\Schema;
4 +use Illuminate\Database\Schema\Blueprint;
5 +use Illuminate\Database\Migrations\Migration;
6 +
7 +class CreateFilesTable extends Migration
8 +{
9 + /**
10 + * Run the migrations.
11 + *
12 + * @return void
13 + */
14 + public function up()
15 + {
16 + Schema::create('files', function (Blueprint $table) {
17 + $table->increments('id');
18 + $table->string('name');
19 + $table->string('path');
20 + $table->integer('uploaded_to');
21 +
22 + $table->boolean('external');
23 + $table->integer('order');
24 +
25 + $table->integer('created_by');
26 + $table->integer('updated_by');
27 +
28 + $table->index('uploaded_to');
29 + $table->timestamps();
30 + });
31 + }
32 +
33 + /**
34 + * Reverse the migrations.
35 + *
36 + * @return void
37 + */
38 + public function down()
39 + {
40 + Schema::dropIfExists('files');
41 + }
42 +}
...@@ -460,7 +460,7 @@ module.exports = function (ngApp, events) { ...@@ -460,7 +460,7 @@ module.exports = function (ngApp, events) {
460 * Get all tags for the current book and add into scope. 460 * Get all tags for the current book and add into scope.
461 */ 461 */
462 function getTags() { 462 function getTags() {
463 - let url = window.baseUrl('/ajax/tags/get/page/' + pageId); 463 + let url = window.baseUrl(`/ajax/tags/get/page/${pageId}`);
464 $http.get(url).then((responseData) => { 464 $http.get(url).then((responseData) => {
465 $scope.tags = responseData.data; 465 $scope.tags = responseData.data;
466 addEmptyTag(); 466 addEmptyTag();
...@@ -529,6 +529,74 @@ module.exports = function (ngApp, events) { ...@@ -529,6 +529,74 @@ module.exports = function (ngApp, events) {
529 529
530 }]); 530 }]);
531 531
532 +
533 + ngApp.controller('PageAttachmentController', ['$scope', '$http', '$attrs',
534 + function ($scope, $http, $attrs) {
535 +
536 + const pageId = $scope.uploadedTo = $attrs.pageId;
537 + let currentOrder = '';
538 + $scope.files = [];
539 +
540 + // Angular-UI-Sort options
541 + $scope.sortOptions = {
542 + handle: '.handle',
543 + items: '> tr',
544 + containment: "parent",
545 + axis: "y",
546 + stop: sortUpdate,
547 + };
548 +
549 + /**
550 + * Event listener for sort changes.
551 + * Updates the file ordering on the server.
552 + * @param event
553 + * @param ui
554 + */
555 + function sortUpdate(event, ui) {
556 + let newOrder = $scope.files.map(file => {return file.id}).join(':');
557 + if (newOrder === currentOrder) return;
558 +
559 + currentOrder = newOrder;
560 + $http.put(`/files/sort/page/${pageId}`, {files: $scope.files}).then(resp => {
561 + events.emit('success', resp.data.message);
562 + });
563 + }
564 +
565 + /**
566 + * Used by dropzone to get the endpoint to upload to.
567 + * @returns {string}
568 + */
569 + $scope.getUploadUrl = function () {
570 + return window.baseUrl('/files/upload');
571 + };
572 +
573 + /**
574 + * Get files for the current page from the server.
575 + */
576 + function getFiles() {
577 + let url = window.baseUrl(`/files/get/page/${pageId}`)
578 + $http.get(url).then(responseData => {
579 + $scope.files = responseData.data;
580 + currentOrder = responseData.data.map(file => {return file.id}).join(':');
581 + });
582 + }
583 + getFiles();
584 +
585 + /**
586 + * Runs on file upload, Adds an file to local file list
587 + * and shows a success message to the user.
588 + * @param file
589 + * @param data
590 + */
591 + $scope.uploadSuccess = function (file, data) {
592 + $scope.$apply(() => {
593 + $scope.files.unshift(data);
594 + });
595 + events.emit('success', 'File uploaded');
596 + };
597 +
598 + }]);
599 +
532 }; 600 };
533 601
534 602
......
...@@ -43,10 +43,6 @@ ...@@ -43,10 +43,6 @@
43 } 43 }
44 } 44 }
45 45
46 -//body.ie .popup-body {
47 -// min-height: 100%;
48 -//}
49 -
50 .corner-button { 46 .corner-button {
51 position: absolute; 47 position: absolute;
52 top: 0; 48 top: 0;
...@@ -82,7 +78,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { ...@@ -82,7 +78,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
82 min-height: 70vh; 78 min-height: 70vh;
83 } 79 }
84 80
85 -#image-manager .dropzone-container { 81 +.dropzone-container {
86 position: relative; 82 position: relative;
87 border: 3px dashed #DDD; 83 border: 3px dashed #DDD;
88 } 84 }
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
4 <div class="tabs primary-background-light"> 4 <div class="tabs primary-background-light">
5 <span toolbox-toggle><i class="zmdi zmdi-caret-left-circle"></i></span> 5 <span toolbox-toggle><i class="zmdi zmdi-caret-left-circle"></i></span>
6 <span tab-button="tags" title="Page Tags" class="active"><i class="zmdi zmdi-tag"></i></span> 6 <span tab-button="tags" title="Page Tags" class="active"><i class="zmdi zmdi-tag"></i></span>
7 + <span tab-button="files" title="Attachments"><i class="zmdi zmdi-attachment"></i></span>
7 </div> 8 </div>
8 9
9 <div tab-content="tags" ng-controller="PageTagController" page-id="{{ $page->id or 0 }}"> 10 <div tab-content="tags" ng-controller="PageTagController" page-id="{{ $page->id or 0 }}">
...@@ -34,4 +35,22 @@ ...@@ -34,4 +35,22 @@
34 </div> 35 </div>
35 </div> 36 </div>
36 37
38 + <div tab-content="files" ng-controller="PageAttachmentController" page-id="{{ $page->id or 0 }}">
39 + <h4>Attached Files</h4>
40 + <div class="padded files">
41 + <p class="muted small">Upload some files to display on your page. This are visible in the page sidebar.</p>
42 + <drop-zone upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone>
43 +
44 + <table class="no-style" tag-autosuggestions style="width: 100%;">
45 + <tbody ui-sortable="sortOptions" ng-model="files" >
46 + <tr ng-repeat="file in files track by $index">
47 + <td width="20" ><i class="handle zmdi zmdi-menu"></i></td>
48 + <td ng-bind="file.name"></td>
49 + <td width="10" class="text-center text-neg" style="padding: 0;"><i class="zmdi zmdi-close"></i></td>
50 + </tr>
51 + </tbody>
52 + </table>
53 + </div>
54 + </div>
55 +
37 </div> 56 </div>
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -87,6 +87,11 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -87,6 +87,11 @@ Route::group(['middleware' => 'auth'], function () {
87 Route::delete('/{imageId}', 'ImageController@destroy'); 87 Route::delete('/{imageId}', 'ImageController@destroy');
88 }); 88 });
89 89
90 + // File routes
91 + Route::post('/files/upload', 'FileController@upload');
92 + Route::get('/files/get/page/{pageId}', 'FileController@getFilesForPage');
93 + Route::put('/files/sort/page/{pageId}', 'FileController@sortFilesForPage');
94 +
90 // AJAX routes 95 // AJAX routes
91 Route::put('/ajax/page/{id}/save-draft', 'PageController@saveDraft'); 96 Route::put('/ajax/page/{id}/save-draft', 'PageController@saveDraft');
92 Route::get('/ajax/page/{id}', 'PageController@getPageAjax'); 97 Route::get('/ajax/page/{id}', 'PageController@getPageAjax');
......