mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Started migration of attachment manager from vue
- Created new dropzone component. - Added standard component event system using custom DOM events. - Added tabs component. - Added ajax-delete-row component.
This commit is contained in:
parent
8dc9689c6d
commit
14b6cd1091
@ -152,7 +152,9 @@ class AttachmentController extends Controller
|
||||
{
|
||||
$page = $this->pageRepo->getById($pageId);
|
||||
$this->checkOwnablePermission('page-view', $page);
|
||||
return response()->json($page->attachments);
|
||||
return view('pages.attachment-list', [
|
||||
'attachments' => $page->attachments->all(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -163,14 +165,13 @@ class AttachmentController extends Controller
|
||||
public function sortForPage(Request $request, int $pageId)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'files' => 'required|array',
|
||||
'files.*.id' => 'required|integer',
|
||||
'order' => 'required|array',
|
||||
]);
|
||||
$page = $this->pageRepo->getById($pageId);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
|
||||
$attachments = $request->get('files');
|
||||
$this->attachmentService->updateFileOrderWithinPage($attachments, $pageId);
|
||||
$attachmentOrder = $request->get('order');
|
||||
$this->attachmentService->updateFileOrderWithinPage($attachmentOrder, $pageId);
|
||||
return response()->json(['message' => trans('entities.attachments_order_updated')]);
|
||||
}
|
||||
|
||||
|
@ -30,9 +30,8 @@ class Attachment extends Ownable
|
||||
|
||||
/**
|
||||
* Get the url of this file.
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl()
|
||||
public function getUrl(): string
|
||||
{
|
||||
if ($this->external && strpos($this->path, 'http') !== 0) {
|
||||
return $this->path;
|
||||
|
@ -109,14 +109,14 @@ class AttachmentService extends UploadService
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the file ordering for a listing of attached files.
|
||||
* @param array $attachmentList
|
||||
* @param $pageId
|
||||
* Updates the ordering for a listing of attached files.
|
||||
*/
|
||||
public function updateFileOrderWithinPage($attachmentList, $pageId)
|
||||
public function updateFileOrderWithinPage(array $attachmentOrder, string $pageId)
|
||||
{
|
||||
foreach ($attachmentList as $index => $attachment) {
|
||||
Attachment::where('uploaded_to', '=', $pageId)->where('id', '=', $attachment['id'])->update(['order' => $index]);
|
||||
foreach ($attachmentOrder as $index => $attachmentId) {
|
||||
Attachment::query()->where('uploaded_to', '=', $pageId)
|
||||
->where('id', '=', $attachmentId)
|
||||
->update(['order' => $index]);
|
||||
}
|
||||
}
|
||||
|
||||
|
32
resources/js/components/ajax-delete-row.js
Normal file
32
resources/js/components/ajax-delete-row.js
Normal file
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* AjaxDelete
|
||||
* @extends {Component}
|
||||
*/
|
||||
import {onSelect} from "../services/dom";
|
||||
|
||||
class AjaxDeleteRow {
|
||||
setup() {
|
||||
this.row = this.$el;
|
||||
this.url = this.$opts.url;
|
||||
this.deleteButtons = this.$manyRefs.delete;
|
||||
|
||||
onSelect(this.deleteButtons, this.runDelete.bind(this));
|
||||
}
|
||||
|
||||
runDelete() {
|
||||
this.row.style.opacity = '0.7';
|
||||
this.row.style.pointerEvents = 'none';
|
||||
|
||||
window.$http.delete(this.url).then(resp => {
|
||||
if (typeof resp.data === 'object' && resp.data.message) {
|
||||
window.$events.emit('success', resp.data.message);
|
||||
}
|
||||
this.row.remove();
|
||||
}).catch(err => {
|
||||
this.row.style.opacity = null;
|
||||
this.row.style.pointerEvents = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default AjaxDeleteRow;
|
46
resources/js/components/attachments.js
Normal file
46
resources/js/components/attachments.js
Normal file
@ -0,0 +1,46 @@
|
||||
|
||||
/**
|
||||
* Attachments
|
||||
* @extends {Component}
|
||||
*/
|
||||
class Attachments {
|
||||
|
||||
setup() {
|
||||
this.container = this.$el;
|
||||
this.pageId = this.$opts.pageId;
|
||||
this.editContainer = this.$refs.editContainer;
|
||||
this.mainTabs = this.$refs.mainTabs;
|
||||
this.list = this.$refs.list;
|
||||
|
||||
this.setupListeners();
|
||||
}
|
||||
|
||||
setupListeners() {
|
||||
this.container.addEventListener('dropzone-success', event => {
|
||||
this.mainTabs.components.tabs.show('items');
|
||||
window.$http.get(`/attachments/get/page/${this.pageId}`).then(resp => {
|
||||
this.list.innerHTML = resp.data;
|
||||
window.components.init(this.list);
|
||||
})
|
||||
});
|
||||
|
||||
this.container.addEventListener('sortable-list-sort', event => {
|
||||
this.updateOrder(event.detail.ids);
|
||||
});
|
||||
|
||||
this.editContainer.addEventListener('keypress', event => {
|
||||
if (event.key === 'Enter') {
|
||||
// TODO - Update editing file
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
updateOrder(idOrder) {
|
||||
window.$http.put(`/attachments/sort/page/${this.pageId}`, {order: idOrder}).then(resp => {
|
||||
window.$events.emit('success', resp.data.message);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Attachments;
|
69
resources/js/components/dropzone.js
Normal file
69
resources/js/components/dropzone.js
Normal file
@ -0,0 +1,69 @@
|
||||
import DropZoneLib from "dropzone";
|
||||
import {fadeOut} from "../services/animations";
|
||||
|
||||
/**
|
||||
* Dropzone
|
||||
* @extends {Component}
|
||||
*/
|
||||
class Dropzone {
|
||||
setup() {
|
||||
this.container = this.$el;
|
||||
this.url = this.$opts.url;
|
||||
|
||||
const _this = this;
|
||||
this.dz = new DropZoneLib(this.container, {
|
||||
addRemoveLinks: true,
|
||||
dictRemoveFile: window.trans('components.image_upload_remove'),
|
||||
timeout: Number(window.uploadTimeout) || 60000,
|
||||
maxFilesize: Number(window.uploadLimit) || 256,
|
||||
url: this.url,
|
||||
withCredentials: true,
|
||||
init() {
|
||||
this.dz = this;
|
||||
this.dz.on('sending', _this.onSending.bind(_this));
|
||||
this.dz.on('success', _this.onSuccess.bind(_this));
|
||||
this.dz.on('error', _this.onError.bind(_this));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onSending(file, xhr, data) {
|
||||
|
||||
const token = window.document.querySelector('meta[name=token]').getAttribute('content');
|
||||
data.append('_token', token);
|
||||
|
||||
xhr.ontimeout = function (e) {
|
||||
this.dz.emit('complete', file);
|
||||
this.dz.emit('error', file, window.trans('errors.file_upload_timeout'));
|
||||
}
|
||||
}
|
||||
|
||||
onSuccess(file, data) {
|
||||
this.container.dispatchEvent(new Event('dropzone'))
|
||||
this.$emit('success', {file, data});
|
||||
fadeOut(file.previewElement, 800, () => {
|
||||
this.dz.removeFile(file);
|
||||
});
|
||||
}
|
||||
|
||||
onError(file, errorMessage, xhr) {
|
||||
this.$emit('error', {file, errorMessage, xhr});
|
||||
|
||||
const setMessage = (message) => {
|
||||
const messsageEl = file.previewElement.querySelector('[data-dz-errormessage]');
|
||||
messsageEl.textContent = message;
|
||||
}
|
||||
|
||||
if (xhr && xhr.status === 413) {
|
||||
setMessage(window.trans('errors.server_upload_limit'))
|
||||
} else if (errorMessage.file) {
|
||||
setMessage(errorMessage.file);
|
||||
}
|
||||
}
|
||||
|
||||
removeAll() {
|
||||
this.dz.removeAllFiles(true);
|
||||
}
|
||||
}
|
||||
|
||||
export default Dropzone;
|
@ -40,6 +40,14 @@ function initComponent(name, element) {
|
||||
instance.$refs = allRefs.refs;
|
||||
instance.$manyRefs = allRefs.manyRefs;
|
||||
instance.$opts = parseOpts(name, element);
|
||||
instance.$emit = (eventName, data = {}) => {
|
||||
data.from = instance;
|
||||
const event = new CustomEvent(`${name}-${eventName}`, {
|
||||
bubbles: true,
|
||||
detail: data
|
||||
});
|
||||
instance.$el.dispatchEvent(event);
|
||||
};
|
||||
if (typeof instance.setup === 'function') {
|
||||
instance.setup();
|
||||
}
|
||||
@ -158,4 +166,5 @@ export default initAll;
|
||||
* @property {Object<String, HTMLElement>} $refs
|
||||
* @property {Object<String, HTMLElement[]>} $manyRefs
|
||||
* @property {Object<String, String>} $opts
|
||||
* @property {function(string, Object)} $emit
|
||||
*/
|
@ -9,9 +9,12 @@ class SortableList {
|
||||
this.container = this.$el;
|
||||
this.handleSelector = this.$opts.handleSelector;
|
||||
|
||||
new Sortable(this.container, {
|
||||
const sortable = new Sortable(this.container, {
|
||||
handle: this.handleSelector,
|
||||
animation: 150,
|
||||
onSort: () => {
|
||||
this.$emit('sort', {ids: sortable.toArray()});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
51
resources/js/components/tabs.js
Normal file
51
resources/js/components/tabs.js
Normal file
@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Tabs
|
||||
* Works by matching 'tabToggle<Key>' with 'tabContent<Key>' sections.
|
||||
* @extends {Component}
|
||||
*/
|
||||
import {onSelect} from "../services/dom";
|
||||
|
||||
class Tabs {
|
||||
|
||||
setup() {
|
||||
this.tabContentsByName = {};
|
||||
this.tabButtonsByName = {};
|
||||
this.allContents = [];
|
||||
this.allButtons = [];
|
||||
|
||||
for (const [key, elems] of Object.entries(this.$manyRefs || {})) {
|
||||
if (key.startsWith('toggle')) {
|
||||
const cleanKey = key.replace('toggle', '').toLowerCase();
|
||||
onSelect(elems, e => this.show(cleanKey));
|
||||
this.allButtons.push(...elems);
|
||||
this.tabButtonsByName[cleanKey] = elems;
|
||||
}
|
||||
if (key.startsWith('content')) {
|
||||
const cleanKey = key.replace('content', '').toLowerCase();
|
||||
this.tabContentsByName[cleanKey] = elems;
|
||||
this.allContents.push(...elems);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
show(key) {
|
||||
this.allContents.forEach(c => {
|
||||
c.classList.add('hidden');
|
||||
c.classList.remove('selected');
|
||||
});
|
||||
this.allButtons.forEach(b => b.classList.remove('selected'));
|
||||
|
||||
const contents = this.tabContentsByName[key] || [];
|
||||
const buttons = this.tabButtonsByName[key] || [];
|
||||
if (contents.length > 0) {
|
||||
contents.forEach(c => {
|
||||
c.classList.remove('hidden')
|
||||
c.classList.add('selected')
|
||||
});
|
||||
buttons.forEach(b => b.classList.add('selected'));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Tabs;
|
@ -256,7 +256,7 @@ return [
|
||||
'attachments_upload' => 'Upload File',
|
||||
'attachments_link' => 'Attach Link',
|
||||
'attachments_set_link' => 'Set Link',
|
||||
'attachments_delete_confirm' => 'Click delete again to confirm you want to delete this attachment.',
|
||||
'attachments_delete' => 'Are you sure you want to delete this attachment?',
|
||||
'attachments_dropzone' => 'Drop files or click here to attach a file',
|
||||
'attachments_no_files' => 'No files have been uploaded',
|
||||
'attachments_explain_link' => '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.',
|
||||
|
@ -126,6 +126,10 @@ body.flexbox {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.justify-flex-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Display and float utilities
|
||||
|
@ -66,4 +66,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@include('pages.attachment-manager', ['page' => \BookStack\Entities\Page::first()])
|
||||
|
||||
@stop
|
||||
|
9
resources/views/components/dropzone.blade.php
Normal file
9
resources/views/components/dropzone.blade.php
Normal file
@ -0,0 +1,9 @@
|
||||
{{--
|
||||
@url - URL to upload to.
|
||||
@placeholder - Placeholder text
|
||||
--}}
|
||||
<div component="dropzone"
|
||||
option:dropzone:url="{{ $url }}"
|
||||
class="dropzone-container text-center">
|
||||
<button type="button" class="dz-message">{{ $placeholder }}</button>
|
||||
</div>
|
28
resources/views/pages/attachment-list.blade.php
Normal file
28
resources/views/pages/attachment-list.blade.php
Normal file
@ -0,0 +1,28 @@
|
||||
<div component="sortable-list" option:sortable-list:handle-selector=".handle">
|
||||
@foreach($attachments as $attachment)
|
||||
<div component="ajax-delete-row"
|
||||
option:ajax-delete-row:url="{{ url('/attachments/' . $attachment->id) }}"
|
||||
data-id="{{ $attachment->id }}"
|
||||
class="card drag-card">
|
||||
<div class="handle">@icon('grip')</div>
|
||||
<div class="py-s">
|
||||
<a href="{{ $attachment->getUrl() }}" target="_blank">{{ $attachment->name }}</a>
|
||||
</div>
|
||||
<div class="flex-fill justify-flex-end">
|
||||
<button type="button" class="drag-card-action text-center text-primary">@icon('edit')</button>
|
||||
<div component="dropdown" class="flex-fill relative">
|
||||
<button refs="dropdown@toggle" type="button" class="drag-card-action text-center text-neg">@icon('close')</button>
|
||||
<div refs="dropdown@menu" class="dropdown-menu">
|
||||
<p class="text-neg small px-m mb-xs">{{ trans('entities.attachments_delete') }}</p>
|
||||
<button refs="ajax-delete-row@delete" type="button" class="text-primary small delete">{{ trans('common.confirm') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@if (count($attachments) === 0)
|
||||
<p class="small text-muted">
|
||||
{{ trans('entities.attachments_no_files') }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
@ -1,101 +1,91 @@
|
||||
<div toolbox-tab-content="files" id="attachment-manager" page-id="{{ $page->id ?? 0 }}">
|
||||
<div style="display: block;" toolbox-tab-content="files"
|
||||
component="attachments"
|
||||
option:attachments:page-id="{{ $page->id ?? 0 }}">
|
||||
|
||||
@exposeTranslations([
|
||||
'entities.attachments_file_uploaded',
|
||||
'entities.attachments_file_updated',
|
||||
'entities.attachments_link_attached',
|
||||
'entities.attachments_updated_success',
|
||||
'errors.server_upload_limit',
|
||||
'components.image_upload_remove',
|
||||
'components.file_upload_timeout',
|
||||
'entities.attachments_file_uploaded',
|
||||
'entities.attachments_file_updated',
|
||||
'entities.attachments_link_attached',
|
||||
'entities.attachments_updated_success',
|
||||
'errors.server_upload_limit',
|
||||
'components.image_upload_remove',
|
||||
'components.file_upload_timeout',
|
||||
])
|
||||
|
||||
<h4>{{ trans('entities.attachments') }}</h4>
|
||||
<div class="px-l files">
|
||||
|
||||
<div id="file-list" v-show="!fileToEdit">
|
||||
<div id="file-list">
|
||||
<p class="text-muted small">{{ trans('entities.attachments_explain') }} <span class="text-warn">{{ trans('entities.attachments_explain_instant_save') }}</span></p>
|
||||
|
||||
<div class="tab-container">
|
||||
<div component="tabs" refs="attachments@mainTabs" class="tab-container">
|
||||
<div class="nav-tabs">
|
||||
<button type="button" @click="tab = 'list'" :class="{selected: tab === 'list'}"
|
||||
class="tab-item">{{ trans('entities.attachments_items') }}</button>
|
||||
<button type="button" @click="tab = 'file'" :class="{selected: tab === 'file'}"
|
||||
class="tab-item">{{ trans('entities.attachments_upload') }}</button>
|
||||
<button type="button" @click="tab = 'link'" :class="{selected: tab === 'link'}"
|
||||
class="tab-item">{{ trans('entities.attachments_link') }}</button>
|
||||
<button refs="tabs@toggleItems" type="button" class="selected tab-item">{{ trans('entities.attachments_items') }}</button>
|
||||
<button refs="tabs@toggleUpload" type="button" class="tab-item">{{ trans('entities.attachments_upload') }}</button>
|
||||
<button refs="tabs@toggleLinks" type="button" class="tab-item">{{ trans('entities.attachments_link') }}</button>
|
||||
</div>
|
||||
<div v-show="tab === 'list'">
|
||||
<draggable style="width: 100%;" :options="{handle: '.handle'}" @change="fileSortUpdate" :list="files" element="div">
|
||||
<div v-for="(file, index) in files" :key="file.id" class="card drag-card">
|
||||
<div class="handle">@icon('grip')</div>
|
||||
<div class="py-s">
|
||||
<a :href="getFileUrl(file)" target="_blank" v-text="file.name"></a>
|
||||
<div v-if="file.deleting">
|
||||
<span class="text-neg small">{{ trans('entities.attachments_delete_confirm') }}</span>
|
||||
<br>
|
||||
<button type="button" class="text-primary small" @click="file.deleting = false;">{{ trans('common.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" @click="startEdit(file)" class="drag-card-action text-center text-primary">@icon('edit')</button>
|
||||
<button type="button" @click="deleteFile(file)" class="drag-card-action text-center text-neg">@icon('close')</button>
|
||||
</div>
|
||||
</draggable>
|
||||
<p class="small text-muted" v-if="files.length === 0">
|
||||
{{ trans('entities.attachments_no_files') }}
|
||||
</p>
|
||||
<div refs="tabs@contentItems attachments@list">
|
||||
@include('pages.attachment-list', ['attachments' => $page->attachments->all()])
|
||||
</div>
|
||||
<div v-show="tab === 'file'">
|
||||
<dropzone placeholder="{{ trans('entities.attachments_dropzone') }}" :upload-url="getUploadUrl()" :uploaded-to="pageId" @success="uploadSuccess"></dropzone>
|
||||
<div refs="tabs@contentUpload" class="hiden">
|
||||
@include('components.dropzone', [
|
||||
'placeholder' => trans('entities.attachments_dropzone'),
|
||||
'url' => url('/attachments/upload?uploaded_to=' . $page->id)
|
||||
])
|
||||
</div>
|
||||
<div v-show="tab === 'link'" @keypress.enter.prevent="attachNewLink(file)">
|
||||
<div refs="tabs@contentLinks" class="hidden">
|
||||
<p class="text-muted small">{{ trans('entities.attachments_explain_link') }}</p>
|
||||
<div class="form-group">
|
||||
<label for="attachment-via-link">{{ trans('entities.attachments_link_name') }}</label>
|
||||
<input type="text" placeholder="{{ trans('entities.attachments_link_name') }}" v-model="file.name">
|
||||
<p class="small text-neg" v-for="error in errors.link.name" v-text="error"></p>
|
||||
<label for="attachment_link_name">{{ trans('entities.attachments_link_name') }}</label>
|
||||
<input name="attachment_link_name" id="attachment_link_name" type="text" placeholder="{{ trans('entities.attachments_link_name') }}">
|
||||
<p class="small text-neg"></p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="attachment-via-link">{{ trans('entities.attachments_link_url') }}</label>
|
||||
<input type="text" placeholder="{{ trans('entities.attachments_link_url_hint') }}" v-model="file.link">
|
||||
<p class="small text-neg" v-for="error in errors.link.link" v-text="error"></p>
|
||||
<label for="attachment_link_url">{{ trans('entities.attachments_link_url') }}</label>
|
||||
<input name="attachment_link_url" id="attachment_link_url" type="text" placeholder="{{ trans('entities.attachments_link_url_hint') }}">
|
||||
<p class="small text-neg"></p>
|
||||
</div>
|
||||
<button @click.prevent="attachNewLink(file)" class="button">{{ trans('entities.attach') }}</button>
|
||||
|
||||
<button class="button">{{ trans('entities.attach') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="file-edit" v-if="fileToEdit" @keypress.enter.prevent="updateFile(fileToEdit)">
|
||||
<div refs="attachments@editContainer" class="hidden">
|
||||
<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') }}" v-model="fileToEdit.name">
|
||||
<p class="small text-neg" v-for="error in errors.edit.name" v-text="error"></p>
|
||||
<input type="text" id="attachment-name-edit"
|
||||
name="attachment_name"
|
||||
placeholder="{{ trans('entities.attachments_edit_file_name') }}">
|
||||
<p class="small text-neg"></p>
|
||||
</div>
|
||||
|
||||
<div class="tab-container">
|
||||
<div component="tabs" class="tab-container">
|
||||
<div class="nav-tabs">
|
||||
<button type="button" @click="editTab = 'file'" :class="{selected: editTab === 'file'}" class="tab-item">{{ trans('entities.attachments_upload') }}</button>
|
||||
<button type="button" @click="editTab = 'link'" :class="{selected: editTab === 'link'}" class="tab-item">{{ trans('entities.attachments_set_link') }}</button>
|
||||
<button refs="tabs@toggleFile" type="button" class="tab-item selected">{{ trans('entities.attachments_upload') }}</button>
|
||||
<button refs="tabs@toggleLink" type="button" class="tab-item">{{ trans('entities.attachments_set_link') }}</button>
|
||||
</div>
|
||||
<div v-if="editTab === 'file'">
|
||||
<div refs="tabs@contentFile">
|
||||
@include('components.dropzone', [
|
||||
'placeholder' => trans('entities.attachments_edit_drop_upload'),
|
||||
'url' => url('/attachments')
|
||||
])
|
||||
<dropzone :upload-url="getUploadUrl(fileToEdit)" :uploaded-to="pageId" placeholder="{{ trans('entities.attachments_edit_drop_upload') }}" @success="uploadSuccessUpdate"></dropzone>
|
||||
<br>
|
||||
</div>
|
||||
<div v-if="editTab === 'link'">
|
||||
<div refs="tabs@contentLink" class="hidden">
|
||||
<div class="form-group">
|
||||
<label for="attachment-link-edit">{{ trans('entities.attachments_link_url') }}</label>
|
||||
<input type="text" id="attachment-link-edit" placeholder="{{ trans('entities.attachment_link') }}" v-model="fileToEdit.link">
|
||||
<p class="small text-neg" v-for="error in errors.edit.link" v-text="error"></p>
|
||||
<p class="small text-neg"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="button outline" @click="cancelEdit">{{ trans('common.back') }}</button>
|
||||
<button @click.enter.prevent="updateFile(fileToEdit)" class="button">{{ trans('common.save') }}</button>
|
||||
<button type="button" class="button outline">{{ trans('common.back') }}</button>
|
||||
<button class="button">{{ trans('common.save') }}</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user