From 673c74ddfc59677b55d0d7438038342f8d138569 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 9 Oct 2016 18:58:22 +0100 Subject: [PATCH 1/7] Started work on attachments Created base models and started user-facing controls. --- app/Exceptions/FileUploadException.php | 4 + app/File.php | 18 ++++ app/Http/Controllers/FileController.php | 91 +++++++++++++++++++ app/Page.php | 9 ++ app/Repos/PageRepo.php | 2 +- app/Services/FileService.php | 79 ++++++++++++++++ app/Services/ImageService.php | 65 +++++-------- app/Services/UploadService.php | 64 +++++++++++++ config/filesystems.php | 2 +- .../2016_10_09_142037_create_files_table.php | 42 +++++++++ resources/assets/js/controllers.js | 70 +++++++++++++- resources/assets/sass/_components.scss | 6 +- resources/views/pages/form-toolbox.blade.php | 19 ++++ routes/web.php | 5 + storage/uploads/files/.gitignore | 2 + 15 files changed, 430 insertions(+), 48 deletions(-) create mode 100644 app/Exceptions/FileUploadException.php create mode 100644 app/File.php create mode 100644 app/Http/Controllers/FileController.php create mode 100644 app/Services/FileService.php create mode 100644 app/Services/UploadService.php create mode 100644 database/migrations/2016_10_09_142037_create_files_table.php create mode 100755 storage/uploads/files/.gitignore diff --git a/app/Exceptions/FileUploadException.php b/app/Exceptions/FileUploadException.php new file mode 100644 index 000000000..af976072a --- /dev/null +++ b/app/Exceptions/FileUploadException.php @@ -0,0 +1,4 @@ +belongsTo(Page::class, 'uploaded_to'); + } + + +} diff --git a/app/Http/Controllers/FileController.php b/app/Http/Controllers/FileController.php new file mode 100644 index 000000000..b97112c1c --- /dev/null +++ b/app/Http/Controllers/FileController.php @@ -0,0 +1,91 @@ +fileService = $fileService; + $this->file = $file; + $this->pageRepo = $pageRepo; + } + + + /** + * Endpoint at which files are uploaded to. + * @param Request $request + */ + public function upload(Request $request) + { + // TODO - Add file upload permission check + // TODO - ensure user has permission to edit relevant page. + // TODO - ensure uploads are deleted on page delete. + + $this->validate($request, [ + 'uploaded_to' => 'required|integer|exists:pages,id' + ]); + + $uploadedFile = $request->file('file'); + $pageId = $request->get('uploaded_to'); + + try { + $file = $this->fileService->saveNewUpload($uploadedFile, $pageId); + } catch (FileUploadException $e) { + return response($e->getMessage(), 500); + } + + return response()->json($file); + } + + /** + * Get the files for a specific page. + * @param $pageId + * @return mixed + */ + public function getFilesForPage($pageId) + { + // TODO - check view permission on page? + $page = $this->pageRepo->getById($pageId); + return response()->json($page->files); + } + + /** + * Update the file sorting. + * @param $pageId + * @param Request $request + * @return mixed + */ + public function sortFilesForPage($pageId, Request $request) + { + $this->validate($request, [ + 'files' => 'required|array', + 'files.*.id' => 'required|integer', + ]); + $page = $this->pageRepo->getById($pageId); + $files = $request->get('files'); + $this->fileService->updateFileOrderWithinPage($files, $pageId); + return response()->json(['message' => 'File order updated']); + } + + +} diff --git a/app/Page.php b/app/Page.php index 1961a4f7f..94bcce2b5 100644 --- a/app/Page.php +++ b/app/Page.php @@ -54,6 +54,15 @@ class Page extends Entity return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc'); } + /** + * Get the files attached to this page. + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function files() + { + return $this->hasMany(File::class, 'uploaded_to')->orderBy('order', 'asc'); + } + /** * Get the url for this page. * @param string|bool $path diff --git a/app/Repos/PageRepo.php b/app/Repos/PageRepo.php index 537dd9bd0..dc7bdb403 100644 --- a/app/Repos/PageRepo.php +++ b/app/Repos/PageRepo.php @@ -48,7 +48,7 @@ class PageRepo extends EntityRepo * Get a page via a specific ID. * @param $id * @param bool $allowDrafts - * @return mixed + * @return Page */ public function getById($id, $allowDrafts = false) { diff --git a/app/Services/FileService.php b/app/Services/FileService.php new file mode 100644 index 000000000..689fa0600 --- /dev/null +++ b/app/Services/FileService.php @@ -0,0 +1,79 @@ +getClientOriginalName(); + $fileData = file_get_contents($uploadedFile->getRealPath()); + + $storage = $this->getStorage(); + $fileBasePath = 'uploads/files/' . Date('Y-m-M') . '/'; + $storageBasePath = $this->getStorageBasePath() . $fileBasePath; + + $uploadFileName = $fileName; + while ($storage->exists($storageBasePath . $uploadFileName)) { + $uploadFileName = str_random(3) . $uploadFileName; + } + + $filePath = $fileBasePath . $uploadFileName; + $fileStoragePath = $this->getStorageBasePath() . $filePath; + + try { + $storage->put($fileStoragePath, $fileData); + } catch (Exception $e) { + throw new FileUploadException('File path ' . $fileStoragePath . ' could not be uploaded to. Ensure it is writable to the server.'); + } + + $largestExistingOrder = File::where('uploaded_to', '=', $page_id)->max('order'); + + $file = File::forceCreate([ + 'name' => $fileName, + 'path' => $filePath, + 'uploaded_to' => $page_id, + 'created_by' => user()->id, + 'updated_by' => user()->id, + 'order' => $largestExistingOrder + 1 + ]); + + return $file; + } + + /** + * Get the file storage base path, amended for storage type. + * This allows us to keep a generic path in the database. + * @return string + */ + private function getStorageBasePath() + { + return $this->isLocal() ? 'storage/' : ''; + } + + /** + * Updates the file ordering for a listing of attached files. + * @param array $fileList + * @param $pageId + */ + public function updateFileOrderWithinPage($fileList, $pageId) + { + foreach ($fileList as $index => $file) { + File::where('uploaded_to', '=', $pageId)->where('id', '=', $file['id'])->update(['order' => $index]); + } + } + +} \ No newline at end of file diff --git a/app/Services/ImageService.php b/app/Services/ImageService.php index a56626246..dfe2cf453 100644 --- a/app/Services/ImageService.php +++ b/app/Services/ImageService.php @@ -9,20 +9,13 @@ use Intervention\Image\ImageManager; use Illuminate\Contracts\Filesystem\Factory as FileSystem; use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance; use Illuminate\Contracts\Cache\Repository as Cache; -use Setting; use Symfony\Component\HttpFoundation\File\UploadedFile; -class ImageService +class ImageService extends UploadService { protected $imageTool; - protected $fileSystem; protected $cache; - - /** - * @var FileSystemInstance - */ - protected $storageInstance; protected $storageUrl; /** @@ -34,8 +27,8 @@ class ImageService public function __construct(ImageManager $imageTool, FileSystem $fileSystem, Cache $cache) { $this->imageTool = $imageTool; - $this->fileSystem = $fileSystem; $this->cache = $cache; + parent::__construct($fileSystem); } /** @@ -88,6 +81,9 @@ class ImageService if ($secureUploads) $imageName = str_random(16) . '-' . $imageName; $imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/'; + + if ($this->isLocal()) $imagePath = '/public' . $imagePath; + while ($storage->exists($imagePath . $imageName)) { $imageName = str_random(3) . $imageName; } @@ -100,6 +96,8 @@ class ImageService throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.'); } + if ($this->isLocal()) $fullPath = str_replace_first('/public', '', $fullPath); + $imageDetails = [ 'name' => $imageName, 'path' => $fullPath, @@ -119,6 +117,16 @@ class ImageService return $image; } + /** + * Get the storage path, Dependant of storage type. + * @param Image $image + * @return mixed|string + */ + protected function getPath(Image $image) + { + return ($this->isLocal()) ? ('public/' . $image->path) : $image->path; + } + /** * Get the thumbnail for an image. * If $keepRatio is true only the width will be used. @@ -135,7 +143,8 @@ class ImageService public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false) { $thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/'; - $thumbFilePath = dirname($image->path) . $thumbDirName . basename($image->path); + $imagePath = $this->getPath($image); + $thumbFilePath = dirname($imagePath) . $thumbDirName . basename($imagePath); if ($this->cache->has('images-' . $image->id . '-' . $thumbFilePath) && $this->cache->get('images-' . $thumbFilePath)) { return $this->getPublicUrl($thumbFilePath); @@ -148,7 +157,7 @@ class ImageService } try { - $thumb = $this->imageTool->make($storage->get($image->path)); + $thumb = $this->imageTool->make($storage->get($imagePath)); } catch (Exception $e) { if ($e instanceof \ErrorException || $e instanceof NotSupportedException) { throw new ImageUploadException('The server cannot create thumbnails. Please check you have the GD PHP extension installed.'); @@ -183,8 +192,8 @@ class ImageService { $storage = $this->getStorage(); - $imageFolder = dirname($image->path); - $imageFileName = basename($image->path); + $imageFolder = dirname($this->getPath($image)); + $imageFileName = basename($this->getPath($image)); $allImages = collect($storage->allFiles($imageFolder)); $imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) { @@ -222,35 +231,9 @@ class ImageService return $image; } - /** - * Get the storage that will be used for storing images. - * @return FileSystemInstance - */ - private function getStorage() - { - if ($this->storageInstance !== null) return $this->storageInstance; - - $storageType = config('filesystems.default'); - $this->storageInstance = $this->fileSystem->disk($storageType); - - return $this->storageInstance; - } - - /** - * Check whether or not a folder is empty. - * @param $path - * @return int - */ - private function isFolderEmpty($path) - { - $files = $this->getStorage()->files($path); - $folders = $this->getStorage()->directories($path); - return count($files) === 0 && count($folders) === 0; - } - /** * Gets a public facing url for an image by checking relevant environment variables. - * @param $filePath + * @param string $filePath * @return string */ private function getPublicUrl($filePath) @@ -273,6 +256,8 @@ class ImageService $this->storageUrl = $storageUrl; } + if ($this->isLocal()) $filePath = str_replace_first('public/', '', $filePath); + return ($this->storageUrl == false ? rtrim(baseUrl(''), '/') : rtrim($this->storageUrl, '/')) . $filePath; } diff --git a/app/Services/UploadService.php b/app/Services/UploadService.php new file mode 100644 index 000000000..44d4bb4f7 --- /dev/null +++ b/app/Services/UploadService.php @@ -0,0 +1,64 @@ +fileSystem = $fileSystem; + } + + /** + * Get the storage that will be used for storing images. + * @return FileSystemInstance + */ + protected function getStorage() + { + if ($this->storageInstance !== null) return $this->storageInstance; + + $storageType = config('filesystems.default'); + $this->storageInstance = $this->fileSystem->disk($storageType); + + return $this->storageInstance; + } + + + /** + * Check whether or not a folder is empty. + * @param $path + * @return bool + */ + protected function isFolderEmpty($path) + { + $files = $this->getStorage()->files($path); + $folders = $this->getStorage()->directories($path); + return (count($files) === 0 && count($folders) === 0); + } + + /** + * Check if using a local filesystem. + * @return bool + */ + protected function isLocal() + { + return strtolower(config('filesystems.default')) === 'local'; + } +} \ No newline at end of file diff --git a/config/filesystems.php b/config/filesystems.php index dbcb03db1..836f68d3d 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -56,7 +56,7 @@ return [ 'local' => [ 'driver' => 'local', - 'root' => public_path(), + 'root' => base_path(), ], 'ftp' => [ diff --git a/database/migrations/2016_10_09_142037_create_files_table.php b/database/migrations/2016_10_09_142037_create_files_table.php new file mode 100644 index 000000000..4eaa86aeb --- /dev/null +++ b/database/migrations/2016_10_09_142037_create_files_table.php @@ -0,0 +1,42 @@ +increments('id'); + $table->string('name'); + $table->string('path'); + $table->integer('uploaded_to'); + + $table->boolean('external'); + $table->integer('order'); + + $table->integer('created_by'); + $table->integer('updated_by'); + + $table->index('uploaded_to'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('files'); + } +} diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js index a64bdfa8c..52477a4ad 100644 --- a/resources/assets/js/controllers.js +++ b/resources/assets/js/controllers.js @@ -460,7 +460,7 @@ module.exports = function (ngApp, events) { * Get all tags for the current book and add into scope. */ function getTags() { - let url = window.baseUrl('/ajax/tags/get/page/' + pageId); + let url = window.baseUrl(`/ajax/tags/get/page/${pageId}`); $http.get(url).then((responseData) => { $scope.tags = responseData.data; addEmptyTag(); @@ -529,6 +529,74 @@ module.exports = function (ngApp, events) { }]); + + ngApp.controller('PageAttachmentController', ['$scope', '$http', '$attrs', + function ($scope, $http, $attrs) { + + const pageId = $scope.uploadedTo = $attrs.pageId; + let currentOrder = ''; + $scope.files = []; + + // Angular-UI-Sort options + $scope.sortOptions = { + handle: '.handle', + items: '> tr', + containment: "parent", + axis: "y", + stop: sortUpdate, + }; + + /** + * Event listener for sort changes. + * Updates the file ordering on the server. + * @param event + * @param ui + */ + function sortUpdate(event, ui) { + let newOrder = $scope.files.map(file => {return file.id}).join(':'); + if (newOrder === currentOrder) return; + + currentOrder = newOrder; + $http.put(`/files/sort/page/${pageId}`, {files: $scope.files}).then(resp => { + events.emit('success', resp.data.message); + }); + } + + /** + * Used by dropzone to get the endpoint to upload to. + * @returns {string} + */ + $scope.getUploadUrl = function () { + return window.baseUrl('/files/upload'); + }; + + /** + * Get files for the current page from the server. + */ + function getFiles() { + let url = window.baseUrl(`/files/get/page/${pageId}`) + $http.get(url).then(responseData => { + $scope.files = responseData.data; + currentOrder = responseData.data.map(file => {return file.id}).join(':'); + }); + } + getFiles(); + + /** + * Runs on file upload, Adds an file to local file list + * and shows a success message to the user. + * @param file + * @param data + */ + $scope.uploadSuccess = function (file, data) { + $scope.$apply(() => { + $scope.files.unshift(data); + }); + events.emit('success', 'File uploaded'); + }; + + }]); + }; diff --git a/resources/assets/sass/_components.scss b/resources/assets/sass/_components.scss index ccb69b44e..7de42d43c 100644 --- a/resources/assets/sass/_components.scss +++ b/resources/assets/sass/_components.scss @@ -43,10 +43,6 @@ } } -//body.ie .popup-body { -// min-height: 100%; -//} - .corner-button { position: absolute; top: 0; @@ -82,7 +78,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { min-height: 70vh; } -#image-manager .dropzone-container { +.dropzone-container { position: relative; border: 3px dashed #DDD; } diff --git a/resources/views/pages/form-toolbox.blade.php b/resources/views/pages/form-toolbox.blade.php index a03a208b6..3a344b651 100644 --- a/resources/views/pages/form-toolbox.blade.php +++ b/resources/views/pages/form-toolbox.blade.php @@ -4,6 +4,7 @@
+
@@ -34,4 +35,22 @@
+
+

Attached Files

+
+

Upload some files to display on your page. This are visible in the page sidebar.

+ + + + + + + + + + +
+
+
+ \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 19541734b..7e05af483 100644 --- a/routes/web.php +++ b/routes/web.php @@ -87,6 +87,11 @@ Route::group(['middleware' => 'auth'], function () { Route::delete('/{imageId}', 'ImageController@destroy'); }); + // File routes + Route::post('/files/upload', 'FileController@upload'); + Route::get('/files/get/page/{pageId}', 'FileController@getFilesForPage'); + Route::put('/files/sort/page/{pageId}', 'FileController@sortFilesForPage'); + // AJAX routes Route::put('/ajax/page/{id}/save-draft', 'PageController@saveDraft'); Route::get('/ajax/page/{id}', 'PageController@getPageAjax'); diff --git a/storage/uploads/files/.gitignore b/storage/uploads/files/.gitignore new file mode 100755 index 000000000..d6b7ef32c --- /dev/null +++ b/storage/uploads/files/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore From ac0b29fb6d05d6e943419b91fdbc09a59e20c89f Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 10 Oct 2016 20:30:27 +0100 Subject: [PATCH 2/7] Added view, deletion and permissions for files --- app/File.php | 10 +++- app/Http/Controllers/FileController.php | 51 +++++++++++++++---- app/Services/FileService.php | 30 +++++++++++ .../2016_10_09_142037_create_files_table.php | 32 ++++++++++++ resources/assets/js/controllers.js | 17 +++++-- resources/views/pages/form-toolbox.blade.php | 2 +- .../views/pages/sidebar-tree-list.blade.php | 11 +++- resources/views/settings/roles/form.blade.php | 13 +++++ routes/web.php | 6 ++- 9 files changed, 152 insertions(+), 20 deletions(-) diff --git a/app/File.php b/app/File.php index ebfa0a296..055f217bd 100644 --- a/app/File.php +++ b/app/File.php @@ -7,12 +7,20 @@ class File extends Ownable /** * Get the page this file was uploaded to. - * @return mixed + * @return Page */ public function page() { return $this->belongsTo(Page::class, 'uploaded_to'); } + /** + * Get the url of this file. + * @return string + */ + public function getUrl() + { + return '/files/' . $this->id; + } } diff --git a/app/Http/Controllers/FileController.php b/app/Http/Controllers/FileController.php index b97112c1c..e09fb98c6 100644 --- a/app/Http/Controllers/FileController.php +++ b/app/Http/Controllers/FileController.php @@ -1,10 +1,7 @@ -validate($request, [ 'uploaded_to' => 'required|integer|exists:pages,id' ]); - $uploadedFile = $request->file('file'); $pageId = $request->get('uploaded_to'); + $page = $this->pageRepo->getById($pageId); + + $this->checkPermission('file-create-all'); + $this->checkOwnablePermission('page-update', $page); + + $uploadedFile = $request->file('file'); try { $file = $this->fileService->saveNewUpload($uploadedFile, $pageId); @@ -62,10 +61,10 @@ class FileController extends Controller * @param $pageId * @return mixed */ - public function getFilesForPage($pageId) + public function listForPage($pageId) { - // TODO - check view permission on page? $page = $this->pageRepo->getById($pageId); + $this->checkOwnablePermission('page-view', $page); return response()->json($page->files); } @@ -75,17 +74,47 @@ class FileController extends Controller * @param Request $request * @return mixed */ - public function sortFilesForPage($pageId, Request $request) + public function sortForPage($pageId, Request $request) { $this->validate($request, [ 'files' => 'required|array', 'files.*.id' => 'required|integer', ]); $page = $this->pageRepo->getById($pageId); + $this->checkOwnablePermission('page-update', $page); + $files = $request->get('files'); $this->fileService->updateFileOrderWithinPage($files, $pageId); return response()->json(['message' => 'File order updated']); } + /** + * Get a file from storage. + * @param $fileId + */ + public function get($fileId) + { + $file = $this->file->findOrFail($fileId); + $page = $this->pageRepo->getById($file->uploaded_to); + $this->checkOwnablePermission('page-view', $page); + $fileContents = $this->fileService->getFile($file); + return response($fileContents, 200, [ + 'Content-Type' => 'application/octet-stream', + 'Content-Disposition' => 'attachment; filename="'. $file->name .'"' + ]); + } + + /** + * Delete a specific file in the system. + * @param $fileId + * @return mixed + */ + public function delete($fileId) + { + $file = $this->file->findOrFail($fileId); + $this->checkOwnablePermission($file, 'file-delete'); + $this->fileService->deleteFile($file); + return response()->json(['message' => 'File deleted']); + } } diff --git a/app/Services/FileService.php b/app/Services/FileService.php index 689fa0600..7429f0e64 100644 --- a/app/Services/FileService.php +++ b/app/Services/FileService.php @@ -4,12 +4,24 @@ use BookStack\Exceptions\FileUploadException; use BookStack\File; use Exception; +use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Support\Collection; use Symfony\Component\HttpFoundation\File\UploadedFile; class FileService extends UploadService { + /** + * Get a file from storage. + * @param File $file + * @return string + */ + public function getFile(File $file) + { + $filePath = $this->getStorageBasePath() . $file->path; + return $this->getStorage()->get($filePath); + } + /** * Store a new file upon user upload. * @param UploadedFile $uploadedFile @@ -76,4 +88,22 @@ class FileService extends UploadService } } + /** + * Delete a file and any empty folders the deletion leaves. + * @param File $file + */ + public function deleteFile(File $file) + { + $storedFilePath = $this->getStorageBasePath() . $file->path; + $storage = $this->getStorage(); + $dirPath = dirname($storedFilePath); + + $storage->delete($storedFilePath); + if (count($storage->allFiles($dirPath)) === 0) { + $storage->deleteDirectory($dirPath); + } + + $file->delete(); + } + } \ No newline at end of file diff --git a/database/migrations/2016_10_09_142037_create_files_table.php b/database/migrations/2016_10_09_142037_create_files_table.php index 4eaa86aeb..57ddd1202 100644 --- a/database/migrations/2016_10_09_142037_create_files_table.php +++ b/database/migrations/2016_10_09_142037_create_files_table.php @@ -28,6 +28,26 @@ class CreateFilesTable extends Migration $table->index('uploaded_to'); $table->timestamps(); }); + + // Get roles with permissions we need to change + $adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id; + + // Create & attach new entity permissions + $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own']; + $entity = 'File'; + foreach ($ops as $op) { + $permissionId = DB::table('role_permissions')->insertGetId([ + 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)), + 'display_name' => $op . ' ' . $entity . 's', + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + ]); + DB::table('permission_role')->insert([ + 'role_id' => $adminRoleId, + 'permission_id' => $permissionId + ]); + } + } /** @@ -38,5 +58,17 @@ class CreateFilesTable extends Migration public function down() { Schema::dropIfExists('files'); + + // Get roles with permissions we need to change + $adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id; + + // Create & attach new entity permissions + $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own']; + $entity = 'File'; + foreach ($ops as $op) { + $permName = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)); + $permission = DB::table('role_permissions')->where('name', '=', $permName)->get(); + DB::table('permission_role')->where('permission_id', '=', $permission->id)->delete(); + } } } diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js index 52477a4ad..b5353e7d9 100644 --- a/resources/assets/js/controllers.js +++ b/resources/assets/js/controllers.js @@ -575,9 +575,9 @@ module.exports = function (ngApp, events) { */ function getFiles() { let url = window.baseUrl(`/files/get/page/${pageId}`) - $http.get(url).then(responseData => { - $scope.files = responseData.data; - currentOrder = responseData.data.map(file => {return file.id}).join(':'); + $http.get(url).then(resp => { + $scope.files = resp.data; + currentOrder = resp.data.map(file => {return file.id}).join(':'); }); } getFiles(); @@ -595,6 +595,17 @@ module.exports = function (ngApp, events) { events.emit('success', 'File uploaded'); }; + /** + * Delete a file from the server and, on success, the local listing. + * @param file + */ + $scope.deleteFile = function(file) { + $http.delete(`/files/${file.id}`).then(resp => { + events.emit('success', resp.data.message); + $scope.files.splice($scope.files.indexOf(file), 1); + }); + }; + }]); }; diff --git a/resources/views/pages/form-toolbox.blade.php b/resources/views/pages/form-toolbox.blade.php index 3a344b651..9ebf223f0 100644 --- a/resources/views/pages/form-toolbox.blade.php +++ b/resources/views/pages/form-toolbox.blade.php @@ -46,7 +46,7 @@ - + diff --git a/resources/views/pages/sidebar-tree-list.blade.php b/resources/views/pages/sidebar-tree-list.blade.php index 5fcec8731..fa9cc84aa 100644 --- a/resources/views/pages/sidebar-tree-list.blade.php +++ b/resources/views/pages/sidebar-tree-list.blade.php @@ -1,6 +1,15 @@
+ @if ($page->files->count() > 0) +
Attachments
+ @foreach($page->files as $file) + + @endforeach + @endif + @if (isset($pageNav) && $pageNav)
Page Navigation
- - @endif
Book Navigation
diff --git a/resources/views/settings/roles/form.blade.php b/resources/views/settings/roles/form.blade.php index 5e653f8de..4f1987c03 100644 --- a/resources/views/settings/roles/form.blade.php +++ b/resources/views/settings/roles/form.blade.php @@ -106,6 +106,19 @@ + + Attached
Files + @include('settings/roles/checkbox', ['permission' => 'file-create-all']) + Controlled by the asset they are uploaded to + + + + + + + + +
diff --git a/routes/web.php b/routes/web.php index 7e05af483..514f82f99 100644 --- a/routes/web.php +++ b/routes/web.php @@ -88,9 +88,11 @@ Route::group(['middleware' => 'auth'], function () { }); // File routes + Route::get('/files/{id}', 'FileController@get'); Route::post('/files/upload', 'FileController@upload'); - Route::get('/files/get/page/{pageId}', 'FileController@getFilesForPage'); - Route::put('/files/sort/page/{pageId}', 'FileController@sortFilesForPage'); + Route::get('/files/get/page/{pageId}', 'FileController@listForPage'); + Route::put('/files/sort/page/{pageId}', 'FileController@sortForPage'); + Route::delete('/files/{id}', 'FileController@delete'); // AJAX routes Route::put('/ajax/page/{id}/save-draft', 'PageController@saveDraft'); From 89509b487af222ae9c9bc6c58c04b0790cc29b09 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 10 Oct 2016 21:13:18 +0100 Subject: [PATCH 3/7] Added attachment creation from link/name --- app/Http/Controllers/Controller.php | 11 ++++-- app/Http/Controllers/FileController.php | 39 +++++++++++++++++-- app/Services/FileService.php | 26 +++++++++++++ resources/assets/js/controllers.js | 12 ++++++ resources/views/pages/form-toolbox.blade.php | 13 +++++++ .../views/pages/sidebar-tree-list.blade.php | 2 +- routes/web.php | 1 + 7 files changed, 96 insertions(+), 8 deletions(-) diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 2dabc417b..ac430065a 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -71,8 +71,13 @@ abstract class Controller extends BaseController */ protected function showPermissionError() { - Session::flash('error', trans('errors.permission')); - $response = request()->wantsJson() ? response()->json(['error' => trans('errors.permissionJson')], 403) : redirect('/'); + if (request()->wantsJson()) { + $response = response()->json(['error' => trans('errors.permissionJson')], 403); + } else { + $response = redirect('/'); + session()->flash('error', trans('errors.permission')); + } + throw new HttpResponseException($response); } @@ -83,7 +88,7 @@ abstract class Controller extends BaseController */ protected function checkPermission($permissionName) { - if (!$this->currentUser || !$this->currentUser->can($permissionName)) { + if (!user() || !user()->can($permissionName)) { $this->showPermissionError(); } return true; diff --git a/app/Http/Controllers/FileController.php b/app/Http/Controllers/FileController.php index e09fb98c6..9486298b2 100644 --- a/app/Http/Controllers/FileController.php +++ b/app/Http/Controllers/FileController.php @@ -36,7 +36,8 @@ class FileController extends Controller { // TODO - ensure uploads are deleted on page delete. $this->validate($request, [ - 'uploaded_to' => 'required|integer|exists:pages,id' + 'uploaded_to' => 'required|integer|exists:pages,id', + 'file' => 'required|file' ]); $pageId = $request->get('uploaded_to'); @@ -56,6 +57,32 @@ class FileController extends Controller return response()->json($file); } + /** + * Attach a link to a page as a file. + * @param Request $request + * @return mixed + */ + public function attachLink(Request $request) + { + $this->validate($request, [ + 'uploaded_to' => 'required|integer|exists:pages,id', + 'name' => 'string', + 'link' => 'url' + ]); + + $pageId = $request->get('uploaded_to'); + $page = $this->pageRepo->getById($pageId); + + $this->checkPermission('file-create-all'); + $this->checkOwnablePermission('page-update', $page); + + $fileName = $request->get('name'); + $link = $request->get('link'); + $file = $this->fileService->saveNewFromLink($fileName, $link, $pageId); + + return response()->json($file); + } + /** * Get the files for a specific page. * @param $pageId @@ -85,7 +112,7 @@ class FileController extends Controller $files = $request->get('files'); $this->fileService->updateFileOrderWithinPage($files, $pageId); - return response()->json(['message' => 'File order updated']); + return response()->json(['message' => 'Attachment order updated']); } /** @@ -98,6 +125,10 @@ class FileController extends Controller $page = $this->pageRepo->getById($file->uploaded_to); $this->checkOwnablePermission('page-view', $page); + if ($file->external) { + return redirect($file->path); + } + $fileContents = $this->fileService->getFile($file); return response($fileContents, 200, [ 'Content-Type' => 'application/octet-stream', @@ -113,8 +144,8 @@ class FileController extends Controller public function delete($fileId) { $file = $this->file->findOrFail($fileId); - $this->checkOwnablePermission($file, 'file-delete'); + $this->checkOwnablePermission('file-delete', $file); $this->fileService->deleteFile($file); - return response()->json(['message' => 'File deleted']); + return response()->json(['message' => 'Attachment deleted']); } } diff --git a/app/Services/FileService.php b/app/Services/FileService.php index 7429f0e64..3674209a8 100644 --- a/app/Services/FileService.php +++ b/app/Services/FileService.php @@ -66,6 +66,27 @@ class FileService extends UploadService return $file; } + /** + * Save a new File attachment from a given link and name. + * @param string $name + * @param string $link + * @param int $page_id + * @return File + */ + public function saveNewFromLink($name, $link, $page_id) + { + $largestExistingOrder = File::where('uploaded_to', '=', $page_id)->max('order'); + return File::forceCreate([ + 'name' => $name, + 'path' => $link, + 'external' => true, + 'uploaded_to' => $page_id, + 'created_by' => user()->id, + 'updated_by' => user()->id, + 'order' => $largestExistingOrder + 1 + ]); + } + /** * Get the file storage base path, amended for storage type. * This allows us to keep a generic path in the database. @@ -94,6 +115,11 @@ class FileService extends UploadService */ public function deleteFile(File $file) { + if ($file->external) { + $file->delete(); + return; + } + $storedFilePath = $this->getStorageBasePath() . $file->path; $storage = $this->getStorage(); $dirPath = dirname($storedFilePath); diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js index b5353e7d9..bc2d43fc8 100644 --- a/resources/assets/js/controllers.js +++ b/resources/assets/js/controllers.js @@ -606,6 +606,18 @@ module.exports = function (ngApp, events) { }); }; + $scope.attachLinkSubmit = function(fileName, fileLink) { + $http.post('/files/link', { + uploaded_to: pageId, + name: fileName, + link: fileLink + }).then(resp => { + $scope.files.unshift(resp.data); + events.emit('success', 'Link attached'); + }); + $scope.fileName = $scope.fileLink = ''; + }; + }]); }; diff --git a/resources/views/pages/form-toolbox.blade.php b/resources/views/pages/form-toolbox.blade.php index 9ebf223f0..e1481f500 100644 --- a/resources/views/pages/form-toolbox.blade.php +++ b/resources/views/pages/form-toolbox.blade.php @@ -41,6 +41,19 @@

Upload some files to display on your page. This are visible in the page sidebar.

+
+ +
+ + +
+
+ + +
+ + + diff --git a/resources/views/pages/sidebar-tree-list.blade.php b/resources/views/pages/sidebar-tree-list.blade.php index fa9cc84aa..bf0e5761a 100644 --- a/resources/views/pages/sidebar-tree-list.blade.php +++ b/resources/views/pages/sidebar-tree-list.blade.php @@ -5,7 +5,7 @@
Attachments
@foreach($page->files as $file) @endforeach @endif diff --git a/routes/web.php b/routes/web.php index 514f82f99..a55eb224f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -90,6 +90,7 @@ Route::group(['middleware' => 'auth'], function () { // File routes Route::get('/files/{id}', 'FileController@get'); Route::post('/files/upload', 'FileController@upload'); + Route::post('/files/link', 'FileController@attachLink'); Route::get('/files/get/page/{pageId}', 'FileController@listForPage'); Route::put('/files/sort/page/{pageId}', 'FileController@sortForPage'); Route::delete('/files/{id}', 'FileController@delete'); From 867fc8be64ec41c41c5646499c1aa34c1e817d53 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 11 Oct 2016 20:39:11 +0100 Subject: [PATCH 4/7] Added basic attachment editing functionality --- app/Http/Controllers/FileController.php | 68 +++++++++++- app/Services/FileService.php | 110 +++++++++++++++---- resources/assets/js/controllers.js | 109 +++++++++++++++--- resources/assets/js/directives.js | 1 + resources/assets/sass/_pages.scss | 15 --- resources/views/pages/form-toolbox.blade.php | 81 +++++++++----- routes/web.php | 2 + 7 files changed, 307 insertions(+), 79 deletions(-) diff --git a/app/Http/Controllers/FileController.php b/app/Http/Controllers/FileController.php index 9486298b2..4cdcf66dc 100644 --- a/app/Http/Controllers/FileController.php +++ b/app/Http/Controllers/FileController.php @@ -57,6 +57,70 @@ class FileController extends Controller return response()->json($file); } + /** + * Update an uploaded file. + * @param int $fileId + * @param Request $request + * @return mixed + */ + public function uploadUpdate($fileId, Request $request) + { + $this->validate($request, [ + 'uploaded_to' => 'required|integer|exists:pages,id', + 'file' => 'required|file' + ]); + + $pageId = $request->get('uploaded_to'); + $page = $this->pageRepo->getById($pageId); + $file = $this->file->findOrFail($fileId); + + $this->checkOwnablePermission('page-update', $page); + $this->checkOwnablePermission('file-create', $file); + + if (intval($pageId) !== intval($file->uploaded_to)) { + return $this->jsonError('Page mismatch during attached file update'); + } + + $uploadedFile = $request->file('file'); + + try { + $file = $this->fileService->saveUpdatedUpload($uploadedFile, $file); + } catch (FileUploadException $e) { + return response($e->getMessage(), 500); + } + + return response()->json($file); + } + + /** + * Update the details of an existing file. + * @param $fileId + * @param Request $request + * @return File|mixed + */ + public function update($fileId, Request $request) + { + $this->validate($request, [ + 'uploaded_to' => 'required|integer|exists:pages,id', + 'name' => 'string|max:255', + 'link' => 'url' + ]); + + $pageId = $request->get('uploaded_to'); + $page = $this->pageRepo->getById($pageId); + $file = $this->file->findOrFail($fileId); + + $this->checkOwnablePermission('page-update', $page); + $this->checkOwnablePermission('file-create', $file); + + if (intval($pageId) !== intval($file->uploaded_to)) { + return $this->jsonError('Page mismatch during attachment update'); + } + + $file = $this->fileService->updateFile($file, $request->all()); + return $file; + } + /** * Attach a link to a page as a file. * @param Request $request @@ -66,8 +130,8 @@ class FileController extends Controller { $this->validate($request, [ 'uploaded_to' => 'required|integer|exists:pages,id', - 'name' => 'string', - 'link' => 'url' + 'name' => 'string|max:255', + 'link' => 'url|max:255' ]); $pageId = $request->get('uploaded_to'); diff --git a/app/Services/FileService.php b/app/Services/FileService.php index 3674209a8..a04d84001 100644 --- a/app/Services/FileService.php +++ b/app/Services/FileService.php @@ -32,26 +32,7 @@ class FileService extends UploadService public function saveNewUpload(UploadedFile $uploadedFile, $page_id) { $fileName = $uploadedFile->getClientOriginalName(); - $fileData = file_get_contents($uploadedFile->getRealPath()); - - $storage = $this->getStorage(); - $fileBasePath = 'uploads/files/' . Date('Y-m-M') . '/'; - $storageBasePath = $this->getStorageBasePath() . $fileBasePath; - - $uploadFileName = $fileName; - while ($storage->exists($storageBasePath . $uploadFileName)) { - $uploadFileName = str_random(3) . $uploadFileName; - } - - $filePath = $fileBasePath . $uploadFileName; - $fileStoragePath = $this->getStorageBasePath() . $filePath; - - try { - $storage->put($fileStoragePath, $fileData); - } catch (Exception $e) { - throw new FileUploadException('File path ' . $fileStoragePath . ' could not be uploaded to. Ensure it is writable to the server.'); - } - + $filePath = $this->putFileInStorage($fileName, $uploadedFile); $largestExistingOrder = File::where('uploaded_to', '=', $page_id)->max('order'); $file = File::forceCreate([ @@ -66,6 +47,30 @@ class FileService extends UploadService return $file; } + /** + * Store a upload, saving to a file and deleting any existing uploads + * attached to that file. + * @param UploadedFile $uploadedFile + * @param File $file + * @return File + * @throws FileUploadException + */ + public function saveUpdatedUpload(UploadedFile $uploadedFile, File $file) + { + if (!$file->external) { + $this->deleteFileInStorage($file); + } + + $fileName = $uploadedFile->getClientOriginalName(); + $filePath = $this->putFileInStorage($fileName, $uploadedFile); + + $file->name = $fileName; + $file->path = $filePath; + $file->external = false; + $file->save(); + return $file; + } + /** * Save a new File attachment from a given link and name. * @param string $name @@ -109,8 +114,29 @@ class FileService extends UploadService } } + /** - * Delete a file and any empty folders the deletion leaves. + * Update the details of a file. + * @param File $file + * @param $requestData + * @return File + */ + public function updateFile(File $file, $requestData) + { + $file->name = $requestData['name']; + if (isset($requestData['link']) && trim($requestData['link']) !== '') { + $file->path = $requestData['link']; + if (!$file->external) { + $this->deleteFileInStorage($file); + $file->external = true; + } + } + $file->save(); + return $file; + } + + /** + * Delete a File from the database and storage. * @param File $file */ public function deleteFile(File $file) @@ -120,6 +146,17 @@ class FileService extends UploadService return; } + $this->deleteFileInStorage($file); + $file->delete(); + } + + /** + * Delete a file from the filesystem it sits on. + * Cleans any empty leftover folders. + * @param File $file + */ + protected function deleteFileInStorage(File $file) + { $storedFilePath = $this->getStorageBasePath() . $file->path; $storage = $this->getStorage(); $dirPath = dirname($storedFilePath); @@ -128,8 +165,37 @@ class FileService extends UploadService if (count($storage->allFiles($dirPath)) === 0) { $storage->deleteDirectory($dirPath); } + } - $file->delete(); + /** + * Store a file in storage with the given filename + * @param $fileName + * @param UploadedFile $uploadedFile + * @return string + * @throws FileUploadException + */ + protected function putFileInStorage($fileName, UploadedFile $uploadedFile) + { + $fileData = file_get_contents($uploadedFile->getRealPath()); + + $storage = $this->getStorage(); + $fileBasePath = 'uploads/files/' . Date('Y-m-M') . '/'; + $storageBasePath = $this->getStorageBasePath() . $fileBasePath; + + $uploadFileName = $fileName; + while ($storage->exists($storageBasePath . $uploadFileName)) { + $uploadFileName = str_random(3) . $uploadFileName; + } + + $filePath = $fileBasePath . $uploadFileName; + $fileStoragePath = $this->getStorageBasePath() . $filePath; + + try { + $storage->put($fileStoragePath, $fileData); + } catch (Exception $e) { + throw new FileUploadException('File path ' . $fileStoragePath . ' could not be uploaded to. Ensure it is writable to the server.'); + } + return $filePath; } } \ No newline at end of file diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js index bc2d43fc8..404668768 100644 --- a/resources/assets/js/controllers.js +++ b/resources/assets/js/controllers.js @@ -536,6 +536,14 @@ module.exports = function (ngApp, events) { const pageId = $scope.uploadedTo = $attrs.pageId; let currentOrder = ''; $scope.files = []; + $scope.editFile = false; + $scope.file = getCleanFile(); + + function getCleanFile() { + return { + page_id: pageId + }; + } // Angular-UI-Sort options $scope.sortOptions = { @@ -559,15 +567,16 @@ module.exports = function (ngApp, events) { currentOrder = newOrder; $http.put(`/files/sort/page/${pageId}`, {files: $scope.files}).then(resp => { events.emit('success', resp.data.message); - }); + }, checkError); } /** * Used by dropzone to get the endpoint to upload to. * @returns {string} */ - $scope.getUploadUrl = function () { - return window.baseUrl('/files/upload'); + $scope.getUploadUrl = function (file) { + let suffix = (typeof file !== 'undefined') ? `/${file.id}` : ''; + return window.baseUrl(`/files/upload${suffix}`); }; /** @@ -578,7 +587,7 @@ module.exports = function (ngApp, events) { $http.get(url).then(resp => { $scope.files = resp.data; currentOrder = resp.data.map(file => {return file.id}).join(':'); - }); + }, checkError); } getFiles(); @@ -595,6 +604,24 @@ module.exports = function (ngApp, events) { events.emit('success', 'File uploaded'); }; + /** + * Upload and overwrite an existing file. + * @param file + * @param data + */ + $scope.uploadSuccessUpdate = function (file, data) { + $scope.$apply(() => { + let search = filesIndexOf(data); + if (search !== -1) $scope.files[search] = file; + + if ($scope.editFile) { + $scope.editFile = data; + data.link = ''; + } + }); + events.emit('success', 'File updated'); + }; + /** * Delete a file from the server and, on success, the local listing. * @param file @@ -603,21 +630,77 @@ module.exports = function (ngApp, events) { $http.delete(`/files/${file.id}`).then(resp => { events.emit('success', resp.data.message); $scope.files.splice($scope.files.indexOf(file), 1); - }); + }, checkError); }; - $scope.attachLinkSubmit = function(fileName, fileLink) { - $http.post('/files/link', { - uploaded_to: pageId, - name: fileName, - link: fileLink - }).then(resp => { + /** + * Attach a link to a page. + * @param fileName + * @param fileLink + */ + $scope.attachLinkSubmit = function(file) { + $http.post('/files/link', file).then(resp => { $scope.files.unshift(resp.data); events.emit('success', 'Link attached'); - }); - $scope.fileName = $scope.fileLink = ''; + $scope.file = getCleanFile(); + }, checkError); }; + /** + * Start the edit mode for a file. + * @param fileId + */ + $scope.startEdit = function(file) { + $scope.editFile = angular.copy(file); + if (!file.external) $scope.editFile.link = ''; + }; + + /** + * Cancel edit mode + */ + $scope.cancelEdit = function() { + $scope.editFile = false; + }; + + /** + * Update the name and link of a file. + * @param file + */ + $scope.updateFile = function(file) { + $http.put(`/files/${file.id}`, file).then(resp => { + let search = filesIndexOf(resp.data); + if (search !== -1) $scope.files[search] = file; + + if ($scope.editFile && !file.external) { + $scope.editFile.link = ''; + } + events.emit('success', 'Attachment details updated'); + }); + }; + + /** + * Search the local files via another file object. + * Used to search via object copies. + * @param file + * @returns int + */ + function filesIndexOf(file) { + for (let i = 0; i < $scope.files.length; i++) { + if ($scope.files[i].id == file.id) return file.id; + } + return -1; + } + + /** + * Check for an error response in a ajax request. + * @param response + */ + function checkError(response) { + if (typeof response.data !== 'undefined' && typeof response.data.error !== 'undefined') { + events.emit('error', response.data.error); + } + } + }]); }; diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index 933bbf5ff..82cb128f3 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -116,6 +116,7 @@ module.exports = function (ngApp, events) { uploadedTo: '@' }, link: function (scope, element, attrs) { + if (attrs.placeholder) element[0].querySelector('.dz-message').textContent = attrs.placeholder; var dropZone = new DropZone(element[0].querySelector('.dropzone-container'), { url: scope.uploadUrl, init: function () { diff --git a/resources/assets/sass/_pages.scss b/resources/assets/sass/_pages.scss index e608295a8..1f79c38c8 100755 --- a/resources/assets/sass/_pages.scss +++ b/resources/assets/sass/_pages.scss @@ -228,21 +228,6 @@ padding-top: $-s; position: relative; } - button.pos { - position: absolute; - bottom: 0; - display: block; - width: 100%; - padding: $-s; - height: 45px; - border: 0; - margin: 0; - box-shadow: none; - border-radius: 0; - &:hover{ - box-shadow: none; - } - } .handle { user-select: none; cursor: move; diff --git a/resources/views/pages/form-toolbox.blade.php b/resources/views/pages/form-toolbox.blade.php index e1481f500..e6b761c28 100644 --- a/resources/views/pages/form-toolbox.blade.php +++ b/resources/views/pages/form-toolbox.blade.php @@ -4,7 +4,9 @@
- + @if(userCan('file-create-all')) + + @endif
@@ -35,35 +37,60 @@
-
-

Attached Files

-
-

Upload some files to display on your page. This are visible in the page sidebar.

- + @if(userCan('file-create-all')) +
+

Attached Files

+
-
+
+

Upload some files to display on your page. This are visible in the page sidebar.

+ + +
+ +
+ + +
+
+ + +
+ + + +
+ + + + + + + + +
+ + +
+
Edit File
+
+ + +
+
+ +
+
+ + +
+ + + +
-
- -
-
- - -
- - - - - - - - - - - -
- + @endif \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index a55eb224f..45957ac62 100644 --- a/routes/web.php +++ b/routes/web.php @@ -90,7 +90,9 @@ Route::group(['middleware' => 'auth'], function () { // File routes Route::get('/files/{id}', 'FileController@get'); Route::post('/files/upload', 'FileController@upload'); + Route::post('/files/upload/{id}', 'FileController@uploadUpdate'); Route::post('/files/link', 'FileController@attachLink'); + Route::put('/files/{id}', 'FileController@update'); Route::get('/files/get/page/{pageId}', 'FileController@listForPage'); Route::put('/files/sort/page/{pageId}', 'FileController@sortForPage'); Route::delete('/files/{id}', 'FileController@delete'); From 7ee695d74a4278b6ecc9a5e1f5537a971d835366 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 23 Oct 2016 13:36:45 +0100 Subject: [PATCH 5/7] File upload deletion complete & added extension handling Also fixed issue with file editing on JS side --- app/File.php | 10 ++++++++++ app/Http/Controllers/FileController.php | 2 +- app/Repos/PageRepo.php | 8 ++++++++ app/Services/FileService.php | 3 +++ .../2016_10_09_142037_create_files_table.php | 7 ++----- resources/assets/js/controllers.js | 3 ++- 6 files changed, 26 insertions(+), 7 deletions(-) diff --git a/app/File.php b/app/File.php index 055f217bd..152350c70 100644 --- a/app/File.php +++ b/app/File.php @@ -5,6 +5,16 @@ class File extends Ownable { protected $fillable = ['name', 'order']; + /** + * Get the downloadable file name for this upload. + * @return mixed|string + */ + public function getFileName() + { + if (str_contains($this->name, '.')) return $this->name; + return $this->name . '.' . $this->extension; + } + /** * Get the page this file was uploaded to. * @return Page diff --git a/app/Http/Controllers/FileController.php b/app/Http/Controllers/FileController.php index 4cdcf66dc..2518d6cd3 100644 --- a/app/Http/Controllers/FileController.php +++ b/app/Http/Controllers/FileController.php @@ -196,7 +196,7 @@ class FileController extends Controller $fileContents = $this->fileService->getFile($file); return response($fileContents, 200, [ 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="'. $file->name .'"' + 'Content-Disposition' => 'attachment; filename="'. $file->getFileName() .'"' ]); } diff --git a/app/Repos/PageRepo.php b/app/Repos/PageRepo.php index dc7bdb403..8cd5c35a9 100644 --- a/app/Repos/PageRepo.php +++ b/app/Repos/PageRepo.php @@ -5,6 +5,7 @@ use BookStack\Book; use BookStack\Chapter; use BookStack\Entity; use BookStack\Exceptions\NotFoundException; +use BookStack\Services\FileService; use Carbon\Carbon; use DOMDocument; use DOMXPath; @@ -633,6 +634,13 @@ class PageRepo extends EntityRepo $page->revisions()->delete(); $page->permissions()->delete(); $this->permissionService->deleteJointPermissionsForEntity($page); + + // Delete AttachedFiles + $fileService = app(FileService::class); + foreach ($page->files as $file) { + $fileService->deleteFile($file); + } + $page->delete(); } diff --git a/app/Services/FileService.php b/app/Services/FileService.php index a04d84001..261695e1f 100644 --- a/app/Services/FileService.php +++ b/app/Services/FileService.php @@ -38,6 +38,7 @@ class FileService extends UploadService $file = File::forceCreate([ 'name' => $fileName, 'path' => $filePath, + 'extension' => $uploadedFile->getClientOriginalExtension(), 'uploaded_to' => $page_id, 'created_by' => user()->id, 'updated_by' => user()->id, @@ -67,6 +68,7 @@ class FileService extends UploadService $file->name = $fileName; $file->path = $filePath; $file->external = false; + $file->extension = $uploadedFile->getClientOriginalExtension(); $file->save(); return $file; } @@ -85,6 +87,7 @@ class FileService extends UploadService 'name' => $name, 'path' => $link, 'external' => true, + 'extension' => '', 'uploaded_to' => $page_id, 'created_by' => user()->id, 'updated_by' => user()->id, diff --git a/database/migrations/2016_10_09_142037_create_files_table.php b/database/migrations/2016_10_09_142037_create_files_table.php index 57ddd1202..49433f19c 100644 --- a/database/migrations/2016_10_09_142037_create_files_table.php +++ b/database/migrations/2016_10_09_142037_create_files_table.php @@ -17,6 +17,7 @@ class CreateFilesTable extends Migration $table->increments('id'); $table->string('name'); $table->string('path'); + $table->string('extension', 20); $table->integer('uploaded_to'); $table->boolean('external'); @@ -59,16 +60,12 @@ class CreateFilesTable extends Migration { Schema::dropIfExists('files'); - // Get roles with permissions we need to change - $adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id; - // Create & attach new entity permissions $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own']; $entity = 'File'; foreach ($ops as $op) { $permName = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)); - $permission = DB::table('role_permissions')->where('name', '=', $permName)->get(); - DB::table('permission_role')->where('permission_id', '=', $permission->id)->delete(); + DB::table('role_permissions')->where('name', '=', $permName)->delete(); } } } diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js index 404668768..78b3684d6 100644 --- a/resources/assets/js/controllers.js +++ b/resources/assets/js/controllers.js @@ -674,6 +674,7 @@ module.exports = function (ngApp, events) { if ($scope.editFile && !file.external) { $scope.editFile.link = ''; } + $scope.editFile = false; events.emit('success', 'Attachment details updated'); }); }; @@ -686,7 +687,7 @@ module.exports = function (ngApp, events) { */ function filesIndexOf(file) { for (let i = 0; i < $scope.files.length; i++) { - if ($scope.files[i].id == file.id) return file.id; + if ($scope.files[i].id == file.id) return i; } return -1; } From 91220239e5d545115ae5844fa978eb5541e4d77e Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 23 Oct 2016 15:25:04 +0100 Subject: [PATCH 6/7] Added in attachment tests --- app/Http/Controllers/FileController.php | 1 - resources/assets/js/controllers.js | 1 + .../views/pages/sidebar-tree-list.blade.php | 2 +- tests/AttachmentTest.php | 201 ++++++++++++++++++ tests/ImageTest.php | 2 +- tests/test-data/test-file.txt | 1 + tests/{ => test-data}/test-image.jpg | Bin 7 files changed, 205 insertions(+), 3 deletions(-) create mode 100644 tests/AttachmentTest.php create mode 100644 tests/test-data/test-file.txt rename tests/{ => test-data}/test-image.jpg (100%) diff --git a/app/Http/Controllers/FileController.php b/app/Http/Controllers/FileController.php index 2518d6cd3..88200ae65 100644 --- a/app/Http/Controllers/FileController.php +++ b/app/Http/Controllers/FileController.php @@ -34,7 +34,6 @@ class FileController extends Controller */ public function upload(Request $request) { - // TODO - ensure uploads are deleted on page delete. $this->validate($request, [ 'uploaded_to' => 'required|integer|exists:pages,id', 'file' => 'required|file' diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js index 78b3684d6..f098e0130 100644 --- a/resources/assets/js/controllers.js +++ b/resources/assets/js/controllers.js @@ -639,6 +639,7 @@ module.exports = function (ngApp, events) { * @param fileLink */ $scope.attachLinkSubmit = function(file) { + file.uploaded_to = pageId; $http.post('/files/link', file).then(resp => { $scope.files.unshift(resp.data); events.emit('success', 'Link attached'); diff --git a/resources/views/pages/sidebar-tree-list.blade.php b/resources/views/pages/sidebar-tree-list.blade.php index bf0e5761a..f6b834f07 100644 --- a/resources/views/pages/sidebar-tree-list.blade.php +++ b/resources/views/pages/sidebar-tree-list.blade.php @@ -1,7 +1,7 @@
- @if ($page->files->count() > 0) + @if (isset($page) && $page->files->count() > 0)
Attachments
@foreach($page->files as $file)
diff --git a/tests/AttachmentTest.php b/tests/AttachmentTest.php new file mode 100644 index 000000000..f22faa740 --- /dev/null +++ b/tests/AttachmentTest.php @@ -0,0 +1,201 @@ +getTestFile($name); + return $this->call('POST', '/files/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []); + } + + /** + * Get the expected upload path for a file. + * @param $fileName + * @return string + */ + protected function getUploadPath($fileName) + { + return 'uploads/files/' . Date('Y-m-M') . '/' . $fileName; + } + + /** + * Delete all uploaded files. + * To assist with cleanup. + */ + protected function deleteUploads() + { + $fileService = $this->app->make(\BookStack\Services\FileService::class); + foreach (\BookStack\File::all() as $file) { + $fileService->deleteFile($file); + } + } + + public function test_file_upload() + { + $page = \BookStack\Page::first(); + $this->asAdmin(); + $admin = $this->getAdmin(); + $fileName = 'upload_test_file.txt'; + + $expectedResp = [ + 'name' => $fileName, + 'uploaded_to'=> $page->id, + 'extension' => 'txt', + 'order' => 1, + 'created_by' => $admin->id, + 'updated_by' => $admin->id, + 'path' => $this->getUploadPath($fileName) + ]; + + $this->uploadFile($fileName, $page->id); + $this->assertResponseOk(); + $this->seeJsonContains($expectedResp); + $this->seeInDatabase('files', $expectedResp); + + $this->deleteUploads(); + } + + public function test_file_display_and_access() + { + $page = \BookStack\Page::first(); + $this->asAdmin(); + $admin = $this->getAdmin(); + $fileName = 'upload_test_file.txt'; + + $this->uploadFile($fileName, $page->id); + $this->assertResponseOk(); + $this->visit($page->getUrl()) + ->seeLink($fileName) + ->click($fileName) + ->see('Hi, This is a test file for testing the upload process.'); + + $this->deleteUploads(); + } + + public function test_attaching_link_to_page() + { + $page = \BookStack\Page::first(); + $admin = $this->getAdmin(); + $this->asAdmin(); + + $this->call('POST', 'files/link', [ + 'link' => 'https://example.com', + 'name' => 'Example Attachment Link', + 'uploaded_to' => $page->id, + ]); + + $expectedResp = [ + 'path' => 'https://example.com', + 'name' => 'Example Attachment Link', + 'uploaded_to' => $page->id, + 'created_by' => $admin->id, + 'updated_by' => $admin->id, + 'external' => true, + 'order' => 1, + 'extension' => '' + ]; + + $this->assertResponseOk(); + $this->seeJsonContains($expectedResp); + $this->seeInDatabase('files', $expectedResp); + + $this->visit($page->getUrl())->seeLink('Example Attachment Link') + ->click('Example Attachment Link')->seePageIs('https://example.com'); + + $this->deleteUploads(); + } + + public function test_attachment_updating() + { + $page = \BookStack\Page::first(); + $this->asAdmin(); + + $this->call('POST', 'files/link', [ + 'link' => 'https://example.com', + 'name' => 'Example Attachment Link', + 'uploaded_to' => $page->id, + ]); + + $attachmentId = \BookStack\File::first()->id; + + $this->call('PUT', 'files/' . $attachmentId, [ + 'uploaded_to' => $page->id, + 'name' => 'My new attachment name', + 'link' => 'https://test.example.com' + ]); + + $expectedResp = [ + 'path' => 'https://test.example.com', + 'name' => 'My new attachment name', + 'uploaded_to' => $page->id + ]; + + $this->assertResponseOk(); + $this->seeJsonContains($expectedResp); + $this->seeInDatabase('files', $expectedResp); + + $this->deleteUploads(); + } + + public function test_file_deletion() + { + $page = \BookStack\Page::first(); + $this->asAdmin(); + $fileName = 'deletion_test.txt'; + $this->uploadFile($fileName, $page->id); + + $filePath = base_path('storage/' . $this->getUploadPath($fileName)); + + $this->assertTrue(file_exists($filePath), 'File at path ' . $filePath . ' does not exist'); + + $attachmentId = \BookStack\File::first()->id; + $this->call('DELETE', 'files/' . $attachmentId); + + $this->dontSeeInDatabase('files', [ + 'name' => $fileName + ]); + $this->assertFalse(file_exists($filePath), 'File at path ' . $filePath . ' was not deleted as expected'); + + $this->deleteUploads(); + } + + public function test_attachment_deletion_on_page_deletion() + { + $page = \BookStack\Page::first(); + $this->asAdmin(); + $fileName = 'deletion_test.txt'; + $this->uploadFile($fileName, $page->id); + + $filePath = base_path('storage/' . $this->getUploadPath($fileName)); + + $this->assertTrue(file_exists($filePath), 'File at path ' . $filePath . ' does not exist'); + $this->seeInDatabase('files', [ + 'name' => $fileName + ]); + + $this->call('DELETE', $page->getUrl()); + + $this->dontSeeInDatabase('files', [ + 'name' => $fileName + ]); + $this->assertFalse(file_exists($filePath), 'File at path ' . $filePath . ' was not deleted as expected'); + + $this->deleteUploads(); + } +} diff --git a/tests/ImageTest.php b/tests/ImageTest.php index d9acd4b71..234988ba4 100644 --- a/tests/ImageTest.php +++ b/tests/ImageTest.php @@ -10,7 +10,7 @@ class ImageTest extends TestCase */ protected function getTestImage($fileName) { - return new \Illuminate\Http\UploadedFile(base_path('tests/test-image.jpg'), $fileName, 'image/jpeg', 5238); + return new \Illuminate\Http\UploadedFile(base_path('tests/test-data/test-image.jpg'), $fileName, 'image/jpeg', 5238); } /** diff --git a/tests/test-data/test-file.txt b/tests/test-data/test-file.txt new file mode 100644 index 000000000..4c1f41af9 --- /dev/null +++ b/tests/test-data/test-file.txt @@ -0,0 +1 @@ +Hi, This is a test file for testing the upload process. \ No newline at end of file diff --git a/tests/test-image.jpg b/tests/test-data/test-image.jpg similarity index 100% rename from tests/test-image.jpg rename to tests/test-data/test-image.jpg From 30458405ce55ca84493e197449ad48f64a8c4cd6 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 23 Oct 2016 17:55:48 +0100 Subject: [PATCH 7/7] Page Attachments - Improved UI, Now initially complete Closes #62 --- app/File.php | 2 +- app/Http/Controllers/Controller.php | 22 +++- app/Http/Controllers/FileController.php | 8 +- resources/assets/js/controllers.js | 51 ++++++--- resources/assets/js/directives.js | 65 ++++++++++- resources/assets/sass/_components.scss | 14 +++ resources/assets/sass/_pages.scss | 7 +- resources/assets/sass/_tables.scss | 10 ++ resources/views/pages/form-toolbox.blade.php | 108 ++++++++++++------ .../views/pages/sidebar-tree-list.blade.php | 2 +- .../views/partials/custom-styles.blade.php | 2 +- 11 files changed, 222 insertions(+), 69 deletions(-) diff --git a/app/File.php b/app/File.php index 152350c70..e9b77d2ea 100644 --- a/app/File.php +++ b/app/File.php @@ -30,7 +30,7 @@ class File extends Ownable */ public function getUrl() { - return '/files/' . $this->id; + return baseUrl('/files/' . $this->id); } } diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index ac430065a..2b6c88fe0 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -3,13 +3,11 @@ namespace BookStack\Http\Controllers; use BookStack\Ownable; -use HttpRequestException; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Http\Exception\HttpResponseException; +use Illuminate\Http\Request; use Illuminate\Routing\Controller as BaseController; use Illuminate\Foundation\Validation\ValidatesRequests; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Session; use BookStack\User; abstract class Controller extends BaseController @@ -130,4 +128,22 @@ abstract class Controller extends BaseController return response()->json(['message' => $messageText], $statusCode); } + /** + * Create the response for when a request fails validation. + * + * @param \Illuminate\Http\Request $request + * @param array $errors + * @return \Symfony\Component\HttpFoundation\Response + */ + protected function buildFailedValidationResponse(Request $request, array $errors) + { + if ($request->expectsJson()) { + return response()->json(['validation' => $errors], 422); + } + + return redirect()->to($this->getRedirectUrl()) + ->withInput($request->input()) + ->withErrors($errors, $this->errorBag()); + } + } diff --git a/app/Http/Controllers/FileController.php b/app/Http/Controllers/FileController.php index 88200ae65..668e9ec6c 100644 --- a/app/Http/Controllers/FileController.php +++ b/app/Http/Controllers/FileController.php @@ -101,8 +101,8 @@ class FileController extends Controller { $this->validate($request, [ 'uploaded_to' => 'required|integer|exists:pages,id', - 'name' => 'string|max:255', - 'link' => 'url' + 'name' => 'required|string|min:1|max:255', + 'link' => 'url|min:1|max:255' ]); $pageId = $request->get('uploaded_to'); @@ -129,8 +129,8 @@ class FileController extends Controller { $this->validate($request, [ 'uploaded_to' => 'required|integer|exists:pages,id', - 'name' => 'string|max:255', - 'link' => 'url|max:255' + 'name' => 'required|string|min:1|max:255', + 'link' => 'required|url|min:1|max:255' ]); $pageId = $request->get('uploaded_to'); diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js index f098e0130..99cf6af9d 100644 --- a/resources/assets/js/controllers.js +++ b/resources/assets/js/controllers.js @@ -538,6 +538,10 @@ module.exports = function (ngApp, events) { $scope.files = []; $scope.editFile = false; $scope.file = getCleanFile(); + $scope.errors = { + link: {}, + edit: {} + }; function getCleanFile() { return { @@ -567,7 +571,7 @@ module.exports = function (ngApp, events) { currentOrder = newOrder; $http.put(`/files/sort/page/${pageId}`, {files: $scope.files}).then(resp => { events.emit('success', resp.data.message); - }, checkError); + }, checkError('sort')); } /** @@ -587,7 +591,7 @@ module.exports = function (ngApp, events) { $http.get(url).then(resp => { $scope.files = resp.data; currentOrder = resp.data.map(file => {return file.id}).join(':'); - }, checkError); + }, checkError('get')); } getFiles(); @@ -599,7 +603,7 @@ module.exports = function (ngApp, events) { */ $scope.uploadSuccess = function (file, data) { $scope.$apply(() => { - $scope.files.unshift(data); + $scope.files.push(data); }); events.emit('success', 'File uploaded'); }; @@ -612,10 +616,10 @@ module.exports = function (ngApp, events) { $scope.uploadSuccessUpdate = function (file, data) { $scope.$apply(() => { let search = filesIndexOf(data); - if (search !== -1) $scope.files[search] = file; + if (search !== -1) $scope.files[search] = data; if ($scope.editFile) { - $scope.editFile = data; + $scope.editFile = angular.copy(data); data.link = ''; } }); @@ -627,10 +631,14 @@ module.exports = function (ngApp, events) { * @param file */ $scope.deleteFile = function(file) { + if (!file.deleting) { + file.deleting = true; + return; + } $http.delete(`/files/${file.id}`).then(resp => { events.emit('success', resp.data.message); $scope.files.splice($scope.files.indexOf(file), 1); - }, checkError); + }, checkError('delete')); }; /** @@ -641,10 +649,10 @@ module.exports = function (ngApp, events) { $scope.attachLinkSubmit = function(file) { file.uploaded_to = pageId; $http.post('/files/link', file).then(resp => { - $scope.files.unshift(resp.data); + $scope.files.push(resp.data); events.emit('success', 'Link attached'); $scope.file = getCleanFile(); - }, checkError); + }, checkError('link')); }; /** @@ -652,8 +660,9 @@ module.exports = function (ngApp, events) { * @param fileId */ $scope.startEdit = function(file) { + console.log(file); $scope.editFile = angular.copy(file); - if (!file.external) $scope.editFile.link = ''; + $scope.editFile.link = (file.external) ? file.path : ''; }; /** @@ -670,16 +679,23 @@ module.exports = function (ngApp, events) { $scope.updateFile = function(file) { $http.put(`/files/${file.id}`, file).then(resp => { let search = filesIndexOf(resp.data); - if (search !== -1) $scope.files[search] = file; + if (search !== -1) $scope.files[search] = resp.data; if ($scope.editFile && !file.external) { $scope.editFile.link = ''; } $scope.editFile = false; events.emit('success', 'Attachment details updated'); - }); + }, checkError('edit')); }; + /** + * Get the url of a file. + */ + $scope.getFileUrl = function(file) { + return window.baseUrl('/files/' + file.id); + } + /** * Search the local files via another file object. * Used to search via object copies. @@ -697,9 +713,16 @@ module.exports = function (ngApp, events) { * Check for an error response in a ajax request. * @param response */ - function checkError(response) { - if (typeof response.data !== 'undefined' && typeof response.data.error !== 'undefined') { - events.emit('error', response.data.error); + function checkError(errorGroupName) { + $scope.errors[errorGroupName] = {}; + return function(response) { + if (typeof response.data !== 'undefined' && typeof response.data.error !== 'undefined') { + events.emit('error', response.data.error); + } + if (typeof response.data !== 'undefined' && typeof response.data.validation !== 'undefined') { + $scope.errors[errorGroupName] = response.data.validation; + console.log($scope.errors[errorGroupName]) + } } } diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index 82cb128f3..fa6c2c3be 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -33,6 +33,59 @@ module.exports = function (ngApp, events) { }; }); + /** + * Common tab controls using simple jQuery functions. + */ + ngApp.directive('tabContainer', function() { + return { + restrict: 'A', + link: function (scope, element, attrs) { + const $content = element.find('[tab-content]'); + const $buttons = element.find('[tab-button]'); + + if (attrs.tabContainer) { + let initial = attrs.tabContainer; + $buttons.filter(`[tab-button="${initial}"]`).addClass('selected'); + $content.hide().filter(`[tab-content="${initial}"]`).show(); + } else { + $content.hide().first().show(); + $buttons.first().addClass('selected'); + } + + $buttons.click(function() { + let clickedTab = $(this); + $buttons.removeClass('selected'); + $content.hide(); + let name = clickedTab.addClass('selected').attr('tab-button'); + $content.filter(`[tab-content="${name}"]`).show(); + }); + } + }; + }); + + /** + * Sub form component to allow inner-form sections to act like thier own forms. + */ + ngApp.directive('subForm', function() { + return { + restrict: 'A', + link: function (scope, element, attrs) { + element.on('keypress', e => { + if (e.keyCode === 13) { + submitEvent(e); + } + }); + + element.find('button[type="submit"]').click(submitEvent); + + function submitEvent(e) { + e.preventDefault() + if (attrs.subForm) scope.$eval(attrs.subForm); + } + } + }; + }); + /** * Image Picker @@ -489,8 +542,8 @@ module.exports = function (ngApp, events) { link: function (scope, elem, attrs) { // Get common elements - const $buttons = elem.find('[tab-button]'); - const $content = elem.find('[tab-content]'); + const $buttons = elem.find('[toolbox-tab-button]'); + const $content = elem.find('[toolbox-tab-content]'); const $toggle = elem.find('[toolbox-toggle]'); // Handle toolbox toggle click @@ -502,17 +555,17 @@ module.exports = function (ngApp, events) { function setActive(tabName, openToolbox) { $buttons.removeClass('active'); $content.hide(); - $buttons.filter(`[tab-button="${tabName}"]`).addClass('active'); - $content.filter(`[tab-content="${tabName}"]`).show(); + $buttons.filter(`[toolbox-tab-button="${tabName}"]`).addClass('active'); + $content.filter(`[toolbox-tab-content="${tabName}"]`).show(); if (openToolbox) elem.addClass('open'); } // Set the first tab content active on load - setActive($content.first().attr('tab-content'), false); + setActive($content.first().attr('toolbox-tab-content'), false); // Handle tab button click $buttons.click(function (e) { - let name = $(this).attr('tab-button'); + let name = $(this).attr('toolbox-tab-button'); setActive(name, true); }); } diff --git a/resources/assets/sass/_components.scss b/resources/assets/sass/_components.scss index 7de42d43c..2f9051a52 100644 --- a/resources/assets/sass/_components.scss +++ b/resources/assets/sass/_components.scss @@ -452,3 +452,17 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { border-right: 6px solid transparent; border-bottom: 6px solid $negative; } + + +[tab-container] .nav-tabs { + text-align: left; + border-bottom: 1px solid #DDD; + margin-bottom: $-m; + .tab-item { + padding: $-s; + color: #666; + &.selected { + border-bottom-width: 3px; + } + } +} \ No newline at end of file diff --git a/resources/assets/sass/_pages.scss b/resources/assets/sass/_pages.scss index 1f79c38c8..c7d3e0377 100755 --- a/resources/assets/sass/_pages.scss +++ b/resources/assets/sass/_pages.scss @@ -150,7 +150,6 @@ background-color: #FFF; border: 1px solid #DDD; right: $-xl*2; - z-index: 99; width: 48px; overflow: hidden; align-items: stretch; @@ -201,7 +200,7 @@ color: #444; background-color: rgba(0, 0, 0, 0.1); } - div[tab-content] { + div[toolbox-tab-content] { padding-bottom: 45px; display: flex; flex: 1; @@ -209,7 +208,7 @@ min-height: 0px; overflow-y: scroll; } - div[tab-content] .padded { + div[toolbox-tab-content] .padded { flex: 1; padding-top: 0; } @@ -241,7 +240,7 @@ } } -[tab-content] { +[toolbox-tab-content] { display: none; } diff --git a/resources/assets/sass/_tables.scss b/resources/assets/sass/_tables.scss index 1fc8e11c2..37c61159d 100644 --- a/resources/assets/sass/_tables.scss +++ b/resources/assets/sass/_tables.scss @@ -51,4 +51,14 @@ table.list-table { vertical-align: middle; padding: $-xs; } +} + +table.file-table { + @extend .no-style; + td { + padding: $-xs; + } + .ui-sortable-helper { + display: table; + } } \ No newline at end of file diff --git a/resources/views/pages/form-toolbox.blade.php b/resources/views/pages/form-toolbox.blade.php index e6b761c28..78e485eab 100644 --- a/resources/views/pages/form-toolbox.blade.php +++ b/resources/views/pages/form-toolbox.blade.php @@ -3,13 +3,13 @@
- + @if(userCan('file-create-all')) - + @endif
-
+

Page Tags

Add some tags to better categorise your content.
You can assign a value to a tag for more in-depth organisation.

@@ -38,55 +38,93 @@
@if(userCan('file-create-all')) -
-

Attached Files

+
+

Attachments

-

Upload some files to display on your page. This are visible in the page sidebar.

- +

Upload some files or attach some link to display on your page. This are visible in the page sidebar.

-
+
+ +
+ + + + + + + + + + +
+ +
+ Click delete again to confirm you want to delete this attachment. +
+ Cancel +
+
+

+ No files have been uploaded. +

+
+
+ +
+
+

You can attach a link if you'd prefer not to upload a file. This can be a link to another page or a link to a file in the cloud.

+
+ + +

+
+
+ + +

+
+ -
- - +
-
- - -
- - - - - - - - - - - -
-
+
Edit File
+
+

-
- -
-
- - + +
+ +
+ +
+
+
+
+ + +

+
+
- +
diff --git a/resources/views/pages/sidebar-tree-list.blade.php b/resources/views/pages/sidebar-tree-list.blade.php index f6b834f07..8e7db85ac 100644 --- a/resources/views/pages/sidebar-tree-list.blade.php +++ b/resources/views/pages/sidebar-tree-list.blade.php @@ -5,7 +5,7 @@
Attachments
@foreach($page->files as $file) @endforeach @endif diff --git a/resources/views/partials/custom-styles.blade.php b/resources/views/partials/custom-styles.blade.php index bf7dde1d4..885cc2729 100644 --- a/resources/views/partials/custom-styles.blade.php +++ b/resources/views/partials/custom-styles.blade.php @@ -14,7 +14,7 @@ .nav-tabs a.selected, .nav-tabs .tab-item.selected { border-bottom-color: {{ setting('app-color') }}; } - p.primary:hover, p .primary:hover, span.primary:hover, .text-primary:hover, a, a:hover, a:focus, .text-button, .text-button:hover, .text-button:focus { + .text-primary, p.primary, p .primary, span.primary:hover, .text-primary:hover, a, a:hover, a:focus, .text-button, .text-button:hover, .text-button:focus { color: {{ setting('app-color') }}; } \ No newline at end of file