Migrated attachment manager to vue

This commit is contained in:
Dan Brown 2017-08-19 13:55:56 +01:00
parent a04b31866d
commit afc66b3c3d
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
9 changed files with 231 additions and 401 deletions

View File

@ -145,202 +145,6 @@ 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 = [];
$scope.editFile = false;
$scope.file = getCleanFile();
$scope.errors = {
link: {},
edit: {}
};
function getCleanFile() {
return {
page_id: pageId
};
}
// 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(window.baseUrl(`/attachments/sort/page/${pageId}`), {files: $scope.files}).then(resp => {
events.emit('success', resp.data.message);
}, checkError('sort'));
}
/**
* Used by dropzone to get the endpoint to upload to.
* @returns {string}
*/
$scope.getUploadUrl = function (file) {
let suffix = (typeof file !== 'undefined') ? `/${file.id}` : '';
return window.baseUrl(`/attachments/upload${suffix}`);
};
/**
* Get files for the current page from the server.
*/
function getFiles() {
let url = window.baseUrl(`/attachments/get/page/${pageId}`);
$http.get(url).then(resp => {
$scope.files = resp.data;
currentOrder = resp.data.map(file => {return file.id}).join(':');
}, checkError('get'));
}
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.push(data);
});
events.emit('success', trans('entities.attachments_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] = data;
if ($scope.editFile) {
$scope.editFile = angular.copy(data);
data.link = '';
}
});
events.emit('success', trans('entities.attachments_file_updated'));
};
/**
* Delete a file from the server and, on success, the local listing.
* @param file
*/
$scope.deleteFile = function(file) {
if (!file.deleting) {
file.deleting = true;
return;
}
$http.delete(window.baseUrl(`/attachments/${file.id}`)).then(resp => {
events.emit('success', resp.data.message);
$scope.files.splice($scope.files.indexOf(file), 1);
}, checkError('delete'));
};
/**
* Attach a link to a page.
* @param file
*/
$scope.attachLinkSubmit = function(file) {
file.uploaded_to = pageId;
$http.post(window.baseUrl('/attachments/link'), file).then(resp => {
$scope.files.push(resp.data);
events.emit('success', trans('entities.attachments_link_attached'));
$scope.file = getCleanFile();
}, checkError('link'));
};
/**
* Start the edit mode for a file.
* @param file
*/
$scope.startEdit = function(file) {
$scope.editFile = angular.copy(file);
$scope.editFile.link = (file.external) ? file.path : '';
};
/**
* 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(window.baseUrl(`/attachments/${file.id}`), file).then(resp => {
let search = filesIndexOf(resp.data);
if (search !== -1) $scope.files[search] = resp.data;
if ($scope.editFile && !file.external) {
$scope.editFile.link = '';
}
$scope.editFile = false;
events.emit('success', trans('entities.attachments_updated_success'));
}, checkError('edit'));
};
/**
* Get the url of a file.
*/
$scope.getFileUrl = function(file) {
return window.baseUrl('/attachments/' + file.id);
};
/**
* 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 i;
}
return -1;
}
/**
* Check for an error response in a ajax request.
* @param errorGroupName
*/
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])
}
}
}
}]);
// Controller used to reply to and add new comments
ngApp.controller('CommentReplyController', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) {
const MarkdownIt = require("markdown-it");

View File

@ -1,119 +1,10 @@
"use strict";
const DropZone = require("dropzone");
const MarkdownIt = require("markdown-it");
const mdTasksLists = require('markdown-it-task-lists');
const code = require('./code');
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 their 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);
}
}
};
});
/**
* DropZone
* Used for uploading images
*/
ngApp.directive('dropZone', [function () {
return {
restrict: 'E',
template: `
<div class="dropzone-container">
<div class="dz-message">{{message}}</div>
</div>
`,
scope: {
uploadUrl: '@',
eventSuccess: '=',
eventError: '=',
uploadedTo: '@',
},
link: function (scope, element, attrs) {
scope.message = attrs.message;
if (attrs.placeholder) element[0].querySelector('.dz-message').textContent = attrs.placeholder;
let dropZone = new DropZone(element[0].querySelector('.dropzone-container'), {
url: scope.uploadUrl,
init: function () {
let dz = this;
dz.on('sending', function (file, xhr, data) {
let token = window.document.querySelector('meta[name=token]').getAttribute('content');
data.append('_token', token);
let uploadedTo = typeof scope.uploadedTo === 'undefined' ? 0 : scope.uploadedTo;
data.append('uploaded_to', uploadedTo);
});
if (typeof scope.eventSuccess !== 'undefined') dz.on('success', scope.eventSuccess);
dz.on('success', function (file, data) {
$(file.previewElement).fadeOut(400, function () {
dz.removeFile(file);
});
});
if (typeof scope.eventError !== 'undefined') dz.on('error', scope.eventError);
dz.on('error', function (file, errorMessage, xhr) {
console.log(errorMessage);
console.log(xhr);
function setMessage(message) {
$(file.previewElement).find('[data-dz-errormessage]').text(message);
}
if (xhr.status === 413) setMessage(trans('errors.server_upload_limit'));
if (errorMessage.file) setMessage(errorMessage.file[0]);
});
}
});
}
};
}]);
/**
* TinyMCE
* An angular wrapper around the tinyMCE editor.

View File

@ -9,34 +9,6 @@ window.baseUrl = function(path) {
return basePath + '/' + path;
};
const Vue = require("vue");
const axios = require("axios");
let axiosInstance = axios.create({
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name=token]').getAttribute('content'),
'baseURL': window.baseUrl('')
}
});
window.$http = axiosInstance;
Vue.prototype.$http = axiosInstance;
// AngularJS - Create application and load components
const angular = require("angular");
require("angular-resource");
require("angular-animate");
require("angular-sanitize");
require("angular-ui-sortable");
let ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
// Translation setup
// Creates a global function with name 'trans' to be used in the same way as Laravel's translation system
const Translations = require("./translations");
let translator = new Translations(window.translations);
window.trans = translator.get.bind(translator);
// Global Event System
class EventManager {
constructor() {
@ -61,8 +33,45 @@ class EventManager {
}
window.Events = new EventManager();
const Vue = require("vue");
const axios = require("axios");
let axiosInstance = axios.create({
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name=token]').getAttribute('content'),
'baseURL': window.baseUrl('')
}
});
axiosInstance.interceptors.request.use(resp => {
return resp;
}, err => {
if (typeof err.response === "undefined" || typeof err.response.data === "undefined") return Promise.reject(err);
if (typeof err.response.data.error !== "undefined") window.Events.emit('error', err.response.data.error);
if (typeof err.response.data.message !== "undefined") window.Events.emit('error', err.response.data.message);
});
window.$http = axiosInstance;
Vue.prototype.$http = axiosInstance;
Vue.prototype.$events = window.Events;
// AngularJS - Create application and load components
const angular = require("angular");
require("angular-resource");
require("angular-animate");
require("angular-sanitize");
require("angular-ui-sortable");
let ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
// Translation setup
// Creates a global function with name 'trans' to be used in the same way as Laravel's translation system
const Translations = require("./translations");
let translator = new Translations(window.translations);
window.trans = translator.get.bind(translator);
require("./vues/vues");
require("./components");

View File

@ -0,0 +1,138 @@
const draggable = require('vuedraggable');
const dropzone = require('./components/dropzone');
function mounted() {
this.pageId = this.$el.getAttribute('page-id');
this.file = this.newFile();
this.$http.get(window.baseUrl(`/attachments/get/page/${this.pageId}`)).then(resp => {
this.files = resp.data;
}).catch(err => {
this.checkValidationErrors('get', err);
});
}
let data = {
pageId: null,
files: [],
fileToEdit: null,
file: {},
tab: 'list',
editTab: 'file',
errors: {link: {}, edit: {}, delete: {}}
};
const components = {dropzone, draggable};
let methods = {
newFile() {
return {page_id: this.pageId};
},
getFileUrl(file) {
return window.baseUrl(`/attachments/${file.id}`);
},
fileSortUpdate() {
this.$http.put(window.baseUrl(`/attachments/sort/page/${this.pageId}`), {files: this.files}).then(resp => {
this.$events.emit('success', resp.data.message);
}).catch(err => {
this.checkValidationErrors('sort', err);
});
},
startEdit(file) {
this.fileToEdit = Object.assign({}, file);
this.fileToEdit.link = file.external ? file.path : '';
this.editTab = file.external ? 'link' : 'file';
},
deleteFile(file) {
if (!file.deleting) return file.deleting = true;
this.$http.delete(window.baseUrl(`/attachments/${file.id}`)).then(resp => {
this.$events.emit('success', resp.data.message);
this.files.splice(this.files.indexOf(file), 1);
}).catch(err => {
this.checkValidationErrors('delete', err)
});
},
uploadSuccess(upload) {
this.files.push(upload.data);
this.$events.emit('success', trans('entities.attachments_file_uploaded'));
},
uploadSuccessUpdate(upload) {
let fileIndex = this.filesIndex(upload.data);
if (fileIndex === -1) {
this.files.push(upload.data)
} else {
this.files.splice(fileIndex, 1, upload.data);
}
if (this.fileToEdit && this.fileToEdit.id === upload.data.id) {
this.fileToEdit = Object.assign({}, upload.data);
}
this.$events.emit('success', trans('entities.attachments_file_updated'));
},
checkValidationErrors(groupName, err) {
console.error(err);
if (typeof err.response.data === "undefined" && typeof err.response.data.validation === "undefined") return;
this.errors[groupName] = err.response.data.validation;
console.log(this.errors[groupName]);
},
getUploadUrl(file) {
let url = window.baseUrl(`/attachments/upload`);
if (typeof file !== 'undefined') url += `/${file.id}`;
return url;
},
cancelEdit() {
this.fileToEdit = null;
},
attachNewLink(file) {
file.uploaded_to = this.pageId;
this.$http.post(window.baseUrl('/attachments/link'), file).then(resp => {
this.files.push(resp.data);
this.file = this.newFile();
this.$events.emit('success', trans('entities.attachments_link_attached'));
}).catch(err => {
this.checkValidationErrors('link', err);
});
},
updateFile(file) {
$http.put(window.baseUrl(`/attachments/${file.id}`), file).then(resp => {
let search = this.filesIndex(resp.data);
if (search === -1) {
this.files.push(resp.data);
} else {
this.files.splice(search, 1, resp.data);
}
if (this.fileToEdit && !file.external) this.fileToEdit.link = '';
this.fileToEdit = false;
this.$events.emit('success', trans('entities.attachments_updated_success'));
}).catch(err => {
this.checkValidationErrors('edit', err);
});
},
filesIndex(file) {
for (let i = 0, len = this.files.length; i < len; i++) {
if (this.files[i].id === file.id) return i;
}
return -1;
}
};
module.exports = {
data, methods, mounted, components,
};

View File

@ -127,8 +127,6 @@ const methods = {
message += errors[key].join('\n');
});
this.$events.emit('error', message);
} else if (error.response.status === 403) {
this.$events.emit('error', error.response.data.error);
}
});
},
@ -144,8 +142,6 @@ const methods = {
}).catch(error=> {
if (error.response.status === 400) {
this.dependantPages = error.response.data;
} else if (error.response.status === 403) {
this.$events.emit('error', error.response.data.error);
}
});
},

View File

@ -10,14 +10,15 @@ let vueMapping = {
'code-editor': require('./code-editor'),
'image-manager': require('./image-manager'),
'tag-manager': require('./tag-manager'),
'attachment-manager': require('./attachment-manager'),
};
window.vues = {};
Object.keys(vueMapping).forEach(id => {
if (exists(id)) {
let config = vueMapping[id];
config.el = '#' + id;
window.vues[id] = new Vue(config);
}
});
let ids = Object.keys(vueMapping);
for (let i = 0, len = ids.length; i < len; i++) {
if (!exists(ids[i])) continue;
let config = vueMapping[ids[i]];
config.el = '#' + ids[i];
window.vues[ids[i]] = new Vue(config);
}

View File

@ -512,7 +512,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
}
[tab-container] .nav-tabs {
.tab-container .nav-tabs {
text-align: left;
border-bottom: 1px solid #DDD;
margin-bottom: $-m;

View File

@ -59,18 +59,9 @@ table.list-table {
}
}
table.file-table {
@extend .no-style;
td {
padding: $-xs;
}
.ui-sortable-helper {
display: table;
}
}
.fake-table {
display: table;
width: 100%;
> div {
display: table-row-group;
}

View File

@ -14,8 +14,8 @@
<div class="padded tags">
<p class="muted small">{!! nl2br(e(trans('entities.tags_explain'))) !!}</p>
<draggable class="fake-table no-style tag-table" :options="{handle: '.handle'}" :list="tags" element="div" style="width: 100%;">
<transition-group name="test" tag="div">
<draggable class="fake-table no-style tag-table" :options="{handle: '.handle'}" :list="tags" element="div">
<transition-group tag="div">
<div v-for="(tag, i) in tags" :key="tag.key">
<div width="20" class="handle" ><i class="zmdi zmdi-menu"></i></div>
<div>
@ -47,93 +47,93 @@
</div>
@if(userCan('attachment-create-all'))
<div toolbox-tab-content="files" ng-controller="PageAttachmentController" page-id="{{ $page->id or 0 }}">
<div toolbox-tab-content="files" id="attachment-manager" page-id="{{ $page->id or 0 }}">
<h4>{{ trans('entities.attachments') }}</h4>
<div class="padded files">
<div id="file-list" ng-show="!editFile">
<div id="file-list" v-show="!fileToEdit">
<p class="muted small">{{ trans('entities.attachments_explain') }} <span class="secondary">{{ trans('entities.attachments_explain_instant_save') }}</span></p>
<div tab-container>
<div class="tab-container">
<div class="nav-tabs">
<div tab-button="list" class="tab-item">{{ trans('entities.attachments_items') }}</div>
<div tab-button="file" class="tab-item">{{ trans('entities.attachments_upload') }}</div>
<div tab-button="link" class="tab-item">{{ trans('entities.attachments_link') }}</div>
<div @click="tab = 'list'" :class="{selected: tab === 'list'}" class="tab-item">{{ trans('entities.attachments_items') }}</div>
<div @click="tab = 'file'" :class="{selected: tab === 'file'}" class="tab-item">{{ trans('entities.attachments_upload') }}</div>
<div @click="tab = 'link'" :class="{selected: tab === 'link'}" class="tab-item">{{ trans('entities.attachments_link') }}</div>
</div>
<div tab-content="list">
<table class="file-table" style="width: 100%;">
<tbody ui-sortable="sortOptions" ng-model="files" >
<tr ng-repeat="file in files track by $index">
<td width="20" ><i class="handle zmdi zmdi-menu"></i></td>
<td>
<a ng-href="@{{getFileUrl(file)}}" target="_blank" ng-bind="file.name"></a>
<div ng-if="file.deleting">
<div v-show="tab === 'list'">
<draggable class="fake-table no-style " style="width: 100%;" :options="{handle: '.handle'}" @change="fileSortUpdate" :list="files" element="div">
<transition-group tag="div">
<div v-for="(file, index) in files" :key="file.id">
<div width="20" ><i class="handle zmdi zmdi-menu"></i></div>
<div>
<a :href="getFileUrl(file)" target="_blank" v-text="file.name"></a>
<div v-if="file.deleting">
<span class="neg small">{{ trans('entities.attachments_delete_confirm') }}</span>
<br>
<span class="text-primary small" ng-click="file.deleting=false;">{{ trans('common.cancel') }}</span>
<span class="text-primary small" @click="file.deleting = false;">{{ trans('common.cancel') }}</span>
</div>
</td>
<td width="10" ng-click="startEdit(file)" class="text-center text-primary" style="padding: 0;"><i class="zmdi zmdi-edit"></i></td>
<td width="5"></td>
<td width="10" ng-click="deleteFile(file)" class="text-center text-neg" style="padding: 0;"><i class="zmdi zmdi-close"></i></td>
</tr>
</tbody>
</table>
<p class="small muted" ng-if="files.length == 0">
</div>
<div width="10" @click="startEdit(file)" class="text-center text-primary" style="padding: 0;"><i class="zmdi zmdi-edit"></i></div>
<div width="5"></div>
<div width="10" @click="deleteFile(file)" class="text-center text-neg" style="padding: 0;"><i class="zmdi zmdi-close"></i></div>
</div>
</transition-group>
</draggable>
<p class="small muted" v-if="files.length === 0">
{{ trans('entities.attachments_no_files') }}
</p>
</div>
<div tab-content="file">
<drop-zone message="{{ trans('entities.attachments_dropzone') }}" upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone>
<div v-show="tab === 'file'">
<dropzone placeholder="{{ trans('entities.attachments_dropzone') }}" :upload-url="getUploadUrl()" :uploaded-to="pageId" @success="uploadSuccess"></dropzone>
</div>
<div tab-content="link" sub-form="attachLinkSubmit(file)">
<div v-show="tab === 'link'" @keypress.enter.prevent="attachNewLink(file)">
<p class="muted small">{{ trans('entities.attachments_explain_link') }}</p>
<div class="form-group">
<label for="attachment-via-link">{{ trans('entities.attachments_link_name') }}</label>
<input placeholder="{{ trans('entities.attachments_link_name') }}" ng-model="file.name">
<p class="small neg" ng-repeat="error in errors.link.name" ng-bind="error"></p>
<input type="text" placeholder="{{ trans('entities.attachments_link_name') }}" v-model="file.name">
<p class="small neg" v-for="error in errors.link.name" v-text="error"></p>
</div>
<div class="form-group">
<label for="attachment-via-link">{{ trans('entities.attachments_link_url') }}</label>
<input placeholder="{{ trans('entities.attachments_link_url_hint') }}" ng-model="file.link">
<p class="small neg" ng-repeat="error in errors.link.link" ng-bind="error"></p>
<input type="text" placeholder="{{ trans('entities.attachments_link_url_hint') }}" v-model="file.link">
<p class="small neg" v-for="error in errors.link.link" v-text="error"></p>
</div>
<button class="button pos">{{ trans('entities.attach') }}</button>
<button @click.prevent="attachNewLink(file)" class="button pos">{{ trans('entities.attach') }}</button>
</div>
</div>
</div>
<div id="file-edit" ng-if="editFile" sub-form="updateFile(editFile)">
<div id="file-edit" v-if="fileToEdit" @keypress.enter.prevent="updateFile(fileToEdit)">
<h5>{{ trans('entities.attachments_edit_file') }}</h5>
<div class="form-group">
<label for="attachment-name-edit">{{ trans('entities.attachments_edit_file_name') }}</label>
<input type="text" id="attachment-name-edit" placeholder="{{ trans('entities.attachments_edit_file_name') }}" ng-model="editFile.name">
<p class="small neg" ng-repeat="error in errors.edit.name" ng-bind="error"></p>
<input type="text" id="attachment-name-edit" placeholder="{{ trans('entities.attachments_edit_file_name') }}" v-model="fileToEdit.name">
<p class="small neg" v-for="error in errors.edit.name" v-text="error"></p>
</div>
<div tab-container="@{{ editFile.external ? 'link' : 'file' }}">
<div class="tab-container">
<div class="nav-tabs">
<div tab-button="file" class="tab-item">{{ trans('entities.attachments_upload') }}</div>
<div tab-button="link" class="tab-item">{{ trans('entities.attachments_set_link') }}</div>
<div @click="editTab = 'file'" :class="{selected: editTab === 'file'}" class="tab-item">{{ trans('entities.attachments_upload') }}</div>
<div @click="editTab = 'link'" :class="{selected: editTab === 'link'}" class="tab-item">{{ trans('entities.attachments_set_link') }}</div>
</div>
<div tab-content="file">
<drop-zone upload-url="@{{getUploadUrl(editFile)}}" uploaded-to="@{{uploadedTo}}" placeholder="{{ trans('entities.attachments_edit_drop_upload') }}" event-success="uploadSuccessUpdate"></drop-zone>
<div v-if="editTab === 'file'">
<dropzone :upload-url="getUploadUrl(fileToEdit)" :uploaded-to="pageId" placeholder="{{ trans('entities.attachments_edit_drop_upload') }}" @success="uploadSuccessUpdate"></dropzone>
<br>
</div>
<div tab-content="link">
<div v-if="editTab === 'link'">
<div class="form-group">
<label for="attachment-link-edit">{{ trans('entities.attachments_link_url') }}</label>
<input id="attachment-link-edit" placeholder="{{ trans('entities.attachment_link') }}" ng-model="editFile.link">
<p class="small neg" ng-repeat="error in errors.edit.link" ng-bind="error"></p>
<input type="text" id="attachment-link-edit" placeholder="{{ trans('entities.attachment_link') }}" v-model="fileToEdit.link">
<p class="small neg" v-for="error in errors.edit.link" v-text="error"></p>
</div>
</div>
</div>
<button type="button" class="button" ng-click="cancelEdit()">{{ trans('common.back') }}</button>
<button class="button pos">{{ trans('common.save') }}</button>
<button type="button" class="button" @click="cancelEdit">{{ trans('common.back') }}</button>
<button @click.enter.prevent="updateFile(fileToEdit)" class="button pos">{{ trans('common.save') }}</button>
</div>
</div>