Instead of vue based.
$parentTypeFilter = $request->get('filter_type', null);
$imgData = $this->imageRepo->getEntityFiltered('drawio', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm);
- return response()->json($imgData);
+ return view('components.image-manager-list', [
+ 'images' => $imgData['images'],
+ 'hasMore' => $imgData['has_more'],
+ ]);
}
/**
if ($imageData === null) {
return $this->jsonError("Image data could not be found");
}
+
return response()->json([
'content' => base64_encode($imageData)
]);
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
use BookStack\Http\Controllers\Controller;
+use Illuminate\Validation\ValidationException;
class GalleryImageController extends Controller
{
/**
* GalleryImageController constructor.
- * @param ImageRepo $imageRepo
*/
public function __construct(ImageRepo $imageRepo)
{
/**
* Get a list of gallery images, in a list.
* Can be paged and filtered by entity.
- * @param Request $request
- * @return \Illuminate\Http\JsonResponse
*/
public function list(Request $request)
{
$parentTypeFilter = $request->get('filter_type', null);
$imgData = $this->imageRepo->getEntityFiltered('gallery', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm);
- return response()->json($imgData);
+ return view('components.image-manager-list', [
+ 'images' => $imgData['images'],
+ 'hasMore' => $imgData['has_more'],
+ ]);
}
/**
* Store a new gallery image in the system.
- * @param Request $request
- * @return Illuminate\Http\JsonResponse
- * @throws \Exception
+ * @throws ValidationException
*/
public function create(Request $request)
{
use BookStack\Repos\PageRepo;
use BookStack\Uploads\Image;
use BookStack\Uploads\ImageRepo;
+use Exception;
use Illuminate\Filesystem\Filesystem as File;
+use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
+use Illuminate\Validation\ValidationException;
class ImageController extends Controller
{
/**
* ImageController constructor.
- * @param Image $image
- * @param File $file
- * @param ImageRepo $imageRepo
*/
public function __construct(Image $image, File $file, ImageRepo $imageRepo)
{
/**
* Provide an image file from storage.
- * @param string $path
- * @return mixed
*/
public function showImage(string $path)
{
/**
* Update image details
- * @param Request $request
- * @param integer $id
- * @return \Illuminate\Http\JsonResponse
* @throws ImageUploadException
- * @throws \Exception
+ * @throws ValidationException
*/
- public function update(Request $request, $id)
+ public function update(Request $request, string $id)
{
$this->validate($request, [
'name' => 'required|min:2|string'
$this->checkOwnablePermission('image-update', $image);
$image = $this->imageRepo->updateImageDetails($image, $request->all());
- return response()->json($image);
+
+ $this->imageRepo->loadThumbs($image);
+ return view('components.image-manager-form', [
+ 'image' => $image,
+ 'dependantPages' => null,
+ ]);
}
/**
- * Show the usage of an image on pages.
+ * Get the form for editing the given image.
+ * @throws Exception
*/
- public function usage(int $id)
+ public function edit(Request $request, string $id)
{
$image = $this->imageRepo->getById($id);
$this->checkImagePermission($image);
- $pages = Page::visible()->where('html', 'like', '%' . $image->url . '%')->get(['id', 'name', 'slug', 'book_id']);
- foreach ($pages as $page) {
- $page->url = $page->getUrl();
- $page->html = '';
- $page->text = '';
+ if ($request->has('delete')) {
+ $dependantPages = $this->imageRepo->getPagesUsingImage($image);
}
- $result = count($pages) > 0 ? $pages : false;
- return response()->json($result);
+ $this->imageRepo->loadThumbs($image);
+ return view('components.image-manager-form', [
+ 'image' => $image,
+ 'dependantPages' => $dependantPages ?? null,
+ ]);
}
/**
* Deletes an image and all thumbnail/image files
- * @param int $id
- * @return \Illuminate\Http\JsonResponse
- * @throws \Exception
+ * @throws Exception
*/
- public function destroy($id)
+ public function destroy(string $id)
{
$image = $this->imageRepo->getById($id);
$this->checkOwnablePermission('image-delete', $image);
$this->checkImagePermission($image);
$this->imageRepo->destroyImage($image);
- return response()->json(trans('components.images_deleted'));
+ return response('');
}
/**
* Check related page permission and ensure type is drawio or gallery.
- * @param Image $image
*/
protected function checkImagePermission(Image $image)
{
* Load thumbnails onto an image object.
* @throws Exception
*/
- protected function loadThumbs(Image $image)
+ public function loadThumbs(Image $image)
{
$image->thumbs = [
'gallery' => $this->getThumbnail($image, 150, 150, false),
return null;
}
}
+
+ /**
+ * Get the user visible pages using the given image.
+ */
+ public function getPagesUsingImage(Image $image): array
+ {
+ $pages = Page::visible()
+ ->where('html', 'like', '%' . $image->url . '%')
+ ->get(['id', 'name', 'slug', 'book_id']);
+
+ foreach ($pages as $page) {
+ $page->url = $page->getUrl();
+ }
+
+ return $pages->all();
+ }
}
* Will handle button clicks or input enter press events and submit
* the data over ajax. Will always expect a partial HTML view to be returned.
* Fires an 'ajax-form-success' event when submitted successfully.
+ *
+ * Will handle a real form if that's what the component is added to
+ * otherwise will act as a fake form element.
+ *
* @extends {Component}
*/
class AjaxForm {
setup() {
this.container = this.$el;
+ this.responseContainer = this.container;
this.url = this.$opts.url;
this.method = this.$opts.method || 'post';
this.successMessage = this.$opts.successMessage;
this.submitButtons = this.$manyRefs.submit || [];
+ if (this.$opts.responseContainer) {
+ this.responseContainer = this.container.closest(this.$opts.responseContainer);
+ }
+
this.setupListeners();
}
setupListeners() {
+
+ if (this.container.tagName === 'FORM') {
+ this.container.addEventListener('submit', this.submitRealForm.bind(this));
+ return;
+ }
+
onEnterPress(this.container, event => {
- this.submit();
+ this.submitFakeForm();
event.preventDefault();
});
- this.submitButtons.forEach(button => onSelect(button, this.submit.bind(this)));
+ this.submitButtons.forEach(button => onSelect(button, this.submitFakeForm.bind(this)));
}
- async submit() {
+ submitFakeForm() {
const fd = new FormData();
const inputs = this.container.querySelectorAll(`[name]`);
- console.log(inputs);
for (const input of inputs) {
fd.append(input.getAttribute('name'), input.value);
}
+ this.submit(fd);
+ }
+
+ submitRealForm(event) {
+ event.preventDefault();
+ const fd = new FormData(this.container);
+ this.submit(fd);
+ }
+
+ async submit(formData) {
+ this.responseContainer.style.opacity = '0.7';
+ this.responseContainer.style.pointerEvents = 'none';
- this.container.style.opacity = '0.7';
- this.container.style.pointerEvents = 'none';
try {
- const resp = await window.$http[this.method.toLowerCase()](this.url, fd);
- this.container.innerHTML = resp.data;
- this.$emit('success', {formData: fd});
+ const resp = await window.$http[this.method.toLowerCase()](this.url, formData);
+ this.$emit('success', {formData});
+ this.responseContainer.innerHTML = resp.data;
if (this.successMessage) {
window.$events.emit('success', this.successMessage);
}
} catch (err) {
- this.container.innerHTML = err.data;
+ this.responseContainer.innerHTML = err.data;
}
- window.components.init(this.container);
- this.container.style.opacity = null;
- this.container.style.pointerEvents = null;
+ window.components.init(this.responseContainer);
+ this.responseContainer.style.opacity = null;
+ this.responseContainer.style.pointerEvents = null;
}
}
}
onSuccess(file, data) {
- this.container.dispatchEvent(new Event('dropzone'))
this.$emit('success', {file, data});
if (this.successMessage) {
--- /dev/null
+import {onChildEvent, onSelect, removeLoading, showLoading} from "../services/dom";
+
+/**
+ * ImageManager
+ * @extends {Component}
+ */
+class ImageManager {
+
+ setup() {
+
+ // Options
+ this.uploadedTo = this.$opts.uploadedTo;
+
+ // Element References
+ this.container = this.$el;
+ this.popupEl = this.$refs.popup;
+ this.searchForm = this.$refs.searchForm;
+ this.searchInput = this.$refs.searchInput;
+ this.cancelSearch = this.$refs.cancelSearch;
+ this.listContainer = this.$refs.listContainer;
+ this.filterTabs = this.$manyRefs.filterTabs;
+ this.selectButton = this.$refs.selectButton;
+ this.formContainer = this.$refs.formContainer;
+ this.dropzoneContainer = this.$refs.dropzoneContainer;
+
+ // Instance data
+ this.type = 'gallery';
+ this.lastSelected = {};
+ this.lastSelectedTime = 0;
+ this.resetState = () => {
+ this.callback = null;
+ this.hasData = false;
+ this.page = 1;
+ this.filter = 'all';
+ };
+ this.resetState();
+
+ this.setupListeners();
+
+ window.ImageManager = this;
+ }
+
+ setupListeners() {
+ onSelect(this.filterTabs, e => {
+ this.resetAll();
+ this.filter = e.target.dataset.filter;
+ this.setActiveFilterTab(this.filter);
+ this.loadGallery();
+ });
+
+ this.searchForm.addEventListener('submit', event => {
+ this.resetListView();
+ this.loadGallery();
+ event.preventDefault();
+ });
+
+ onSelect(this.cancelSearch, event => {
+ this.resetListView();
+ this.resetSearchView();
+ this.loadGallery();
+ this.cancelSearch.classList.remove('active');
+ });
+
+ this.searchInput.addEventListener('input', event => {
+ this.cancelSearch.classList.toggle('active', this.searchInput.value.trim());
+ });
+
+ onChildEvent(this.listContainer, '.load-more', 'click', async event => {
+ showLoading(event.target);
+ this.page++;
+ await this.loadGallery();
+ event.target.remove();
+ });
+
+ this.listContainer.addEventListener('event-emit-select-image', this.onImageSelectEvent.bind(this));
+
+ onSelect(this.selectButton, () => {
+ if (this.callback) {
+ this.callback(this.lastSelected);
+ }
+ this.hide();
+ });
+
+ onChildEvent(this.formContainer, '#image-manager-delete', 'click', event => {
+ if (this.lastSelected) {
+ this.loadImageEditForm(this.lastSelected.id, true);
+ }
+ });
+
+ this.formContainer.addEventListener('ajax-form-success', this.refreshGallery.bind(this));
+ this.container.addEventListener('dropzone-success', this.refreshGallery.bind(this));
+ }
+
+ show(callback, type = 'gallery') {
+ this.resetAll();
+
+ this.callback = callback;
+ this.type = type;
+ this.popupEl.components.popup.show();
+ this.dropzoneContainer.classList.toggle('hidden', type !== 'gallery');
+
+ if (!this.hasData) {
+ this.loadGallery();
+ this.hasData = true;
+ }
+ }
+
+ hide() {
+ this.popupEl.components.popup.hide();
+ }
+
+ async loadGallery() {
+ const params = {
+ page: this.page,
+ search: this.searchInput.value || null,
+ uploaded_to: this.uploadedTo,
+ filter_type: this.filter === 'all' ? null : this.filter,
+ };
+
+ const {data: html} = await window.$http.get(`images/${this.type}`, params);
+ this.addReturnedHtmlElementsToList(html);
+ removeLoading(this.listContainer);
+ }
+
+ addReturnedHtmlElementsToList(html) {
+ const el = document.createElement('div');
+ el.innerHTML = html;
+ window.components.init(el);
+ for (const child of [...el.children]) {
+ this.listContainer.appendChild(child);
+ }
+ }
+
+ setActiveFilterTab(filterName) {
+ this.filterTabs.forEach(t => t.classList.remove('selected'));
+ const activeTab = this.filterTabs.find(t => t.dataset.filter === filterName);
+ if (activeTab) {
+ activeTab.classList.add('selected');
+ }
+ }
+
+ resetAll() {
+ this.resetState();
+ this.resetListView();
+ this.resetSearchView();
+ this.formContainer.innerHTML = '';
+ this.setActiveFilterTab('all');
+ }
+
+ resetSearchView() {
+ this.searchInput.value = '';
+ }
+
+ resetListView() {
+ showLoading(this.listContainer);
+ this.page = 1;
+ }
+
+ refreshGallery() {
+ this.resetListView();
+ this.loadGallery();
+ }
+
+ onImageSelectEvent(event) {
+ const image = JSON.parse(event.detail.data);
+ const isDblClick = ((image && image.id === this.lastSelected.id)
+ && Date.now() - this.lastSelectedTime < 400);
+ const alreadySelected = event.target.classList.contains('selected');
+ [...this.listContainer.querySelectorAll('.selected')].forEach(el => {
+ el.classList.remove('selected');
+ });
+
+ if (!alreadySelected) {
+ event.target.classList.add('selected');
+ this.loadImageEditForm(image.id);
+ }
+ this.selectButton.classList.toggle('hidden', alreadySelected);
+
+ if (isDblClick && this.callback) {
+ this.callback(image);
+ this.hide();
+ }
+
+ this.lastSelected = image;
+ this.lastSelectedTime = Date.now();
+ }
+
+ async loadImageEditForm(imageId, requestDelete = false) {
+ if (!requestDelete) {
+ this.formContainer.innerHTML = '';
+ }
+
+ const params = requestDelete ? {delete: true} : {};
+ const {data: formHtml} = await window.$http.get(`/images/edit/${imageId}`, params);
+ this.formContainer.innerHTML = formHtml;
+ window.components.init(this.formContainer);
+ }
+
+}
+
+export default ImageManager;
\ No newline at end of file
*/
export function showLoading(element) {
element.innerHTML = `<div class="loading-container"><div></div><div></div><div></div></div>`;
+}
+
+/**
+ * Remove any loading indicators within the given element.
+ * @param {Element} element
+ */
+export function removeLoading(element) {
+ const loadingEls = element.querySelectorAll('.loading-container');
+ for (const el of loadingEls) {
+ el.remove();
+ }
}
\ No newline at end of file
+++ /dev/null
-import * as Dates from "../services/dates";
-import dropzone from "./components/dropzone";
-
-let page = 1;
-let previousClickTime = 0;
-let previousClickImage = 0;
-let dataLoaded = false;
-let callback = false;
-let baseUrl = '';
-
-let preSearchImages = [];
-let preSearchHasMore = false;
-
-const data = {
- images: [],
-
- imageType: false,
- uploadedTo: false,
-
- selectedImage: false,
- dependantPages: false,
- showing: false,
- filter: null,
- hasMore: false,
- searching: false,
- searchTerm: '',
-
- imageUpdateSuccess: false,
- imageDeleteSuccess: false,
- deleteConfirm: false,
-};
-
-const methods = {
-
- show(providedCallback, imageType = null) {
- callback = providedCallback;
- this.showing = true;
- this.$el.children[0].components.popup.show();
-
- // Get initial images if they have not yet been loaded in.
- if (dataLoaded && imageType === this.imageType) return;
- if (imageType) {
- this.imageType = imageType;
- this.resetState();
- }
- this.fetchData();
- dataLoaded = true;
- },
-
- hide() {
- if (this.$refs.dropzone) {
- this.$refs.dropzone.onClose();
- }
- this.showing = false;
- this.selectedImage = false;
- this.$el.children[0].components.popup.hide();
- },
-
- async fetchData() {
- const params = {
- page,
- search: this.searching ? this.searchTerm : null,
- uploaded_to: this.uploadedTo || null,
- filter_type: this.filter,
- };
-
- const {data} = await this.$http.get(baseUrl, params);
- this.images = this.images.concat(data.images);
- this.hasMore = data.has_more;
- page++;
- },
-
- setFilterType(filterType) {
- this.filter = filterType;
- this.resetState();
- this.fetchData();
- },
-
- resetState() {
- this.cancelSearch();
- this.resetListView();
- this.deleteConfirm = false;
- baseUrl = window.baseUrl(`/images/${this.imageType}`);
- },
-
- resetListView() {
- this.images = [];
- this.hasMore = false;
- page = 1;
- },
-
- searchImages() {
- if (this.searchTerm === '') return this.cancelSearch();
-
- // Cache current settings for later
- if (!this.searching) {
- preSearchImages = this.images;
- preSearchHasMore = this.hasMore;
- }
-
- this.searching = true;
- this.resetListView();
- this.fetchData();
- },
-
- cancelSearch() {
- if (!this.searching) return;
- this.searching = false;
- this.searchTerm = '';
- this.images = preSearchImages;
- this.hasMore = preSearchHasMore;
- },
-
- imageSelect(image) {
- const dblClickTime = 300;
- const currentTime = Date.now();
- const timeDiff = currentTime - previousClickTime;
- const isDblClick = timeDiff < dblClickTime && image.id === previousClickImage;
-
- if (isDblClick) {
- this.callbackAndHide(image);
- } else {
- this.selectedImage = image;
- this.deleteConfirm = false;
- this.dependantPages = false;
- }
-
- previousClickTime = currentTime;
- previousClickImage = image.id;
- },
-
- callbackAndHide(imageResult) {
- if (callback) callback(imageResult);
- this.hide();
- },
-
- async saveImageDetails() {
- let url = window.baseUrl(`/images/${this.selectedImage.id}`);
- try {
- await this.$http.put(url, this.selectedImage)
- } catch (error) {
- if (error.response.status === 422) {
- let errors = error.response.data;
- let message = '';
- Object.keys(errors).forEach((key) => {
- message += errors[key].join('\n');
- });
- this.$events.emit('error', message);
- }
- }
- },
-
- async deleteImage() {
-
- if (!this.deleteConfirm) {
- const url = window.baseUrl(`/images/usage/${this.selectedImage.id}`);
- try {
- const {data} = await this.$http.get(url);
- this.dependantPages = data;
- } catch (error) {
- console.error(error);
- }
- this.deleteConfirm = true;
- return;
- }
-
- const url = window.baseUrl(`/images/${this.selectedImage.id}`);
- await this.$http.delete(url);
- this.images.splice(this.images.indexOf(this.selectedImage), 1);
- this.selectedImage = false;
- this.$events.emit('success', trans('components.image_delete_success'));
- this.deleteConfirm = false;
- },
-
- getDate(stringDate) {
- return Dates.formatDateTime(new Date(stringDate));
- },
-
- uploadSuccess(event) {
- this.images.unshift(event.data);
- this.$events.emit('success', trans('components.image_upload_success'));
- },
-};
-
-const computed = {
- uploadUrl() {
- return window.baseUrl(`/images/${this.imageType}`);
- }
-};
-
-function mounted() {
- window.ImageManager = this;
- this.imageType = this.$el.getAttribute('image-type');
- this.uploadedTo = this.$el.getAttribute('uploaded-to');
- baseUrl = window.baseUrl('/images/' + this.imageType)
-}
-
-export default {
- mounted,
- methods,
- data,
- computed,
- components: {dropzone},
-};
return document.getElementById(id) !== null;
}
-import imageManager from "./image-manager";
-
let vueMapping = {
- 'image-manager': imageManager,
};
window.vues = {};
'copy' => 'Copy',
'reply' => 'Reply',
'delete' => 'Delete',
+ 'delete_confirm' => 'Confirm Deletion',
'search' => 'Search',
'search_clear' => 'Clear Search',
'reset' => 'Reset',
'image_load_more' => 'Load More',
'image_image_name' => 'Image Name',
'image_delete_used' => 'This image is used in the pages below.',
- 'image_delete_confirm' => 'Click delete again to confirm you want to delete this image.',
+ 'image_delete_confirm_text' => 'Are you sure you want to delete this image?',
'image_select_image' => 'Select Image',
'image_dropzone' => 'Drop images or click here to upload',
'images_deleted' => 'Images Deleted',
fill: currentColor !important;
}
+.text-white {
+ color: #fff;
+ fill: currentColor !important;
+}
+
/*
* Entity text colors
*/
transition: all cubic-bezier(.4, 0, 1, 1) 160ms;
overflow: hidden;
&.selected {
- //transform: scale3d(0.92, 0.92, 0.92);
- border: 4px solid #FFF;
- overflow: hidden;
- border-radius: 8px;
- box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2);
+ transform: scale3d(0.92, 0.92, 0.92);
+ outline: currentColor 2px solid;
}
img {
width: 100%;
}
}
-#image-manager .load-more {
+.image-manager .load-more {
display: block;
text-align: center;
@include lightDark(background-color, #EEE, #444);
font-style: italic;
}
+.image-manager .loading-container {
+ text-align: center;
+}
+
.image-manager-sidebar {
width: 300px;
overflow-y: auto;
border-inline-start: 1px solid #DDD;
@include lightDark(border-color, #ddd, #000);
.inner {
+ min-height: auto;
padding: $-m;
}
img {
}
}
+.image-manager .corner-button {
+ margin: 0;
+ border-radius: 0;
+ padding: $-m;
+}
+
// Dropzone
/*
* The MIT License
*/
.dz-message {
font-size: 1em;
- line-height: 2.35;
+ line-height: 2.85;
font-style: italic;
color: #888;
text-align: center;
display: inline-block;
@include lightDark(color, #666, #999);
cursor: pointer;
+ border-right: 1px solid rgba(0, 0, 0, 0.1);
+ border-bottom: 2px solid transparent;
&.selected {
border-bottom: 2px solid var(--color-primary);
}
+ &:last-child {
+ border-right: 0;
+ }
}
}
position: relative;
}
+.flex-container-column {
+ display: flex;
+ flex-direction: column;
+}
+
.flex {
min-height: 0;
flex: 1;
.contained-search-box {
display: flex;
+ height: 38px;
input, button {
border-radius: 0;
+ border: 1px solid #ddd;
@include lightDark(border-color, #ddd, #000);
margin-inline-start: -1px;
}
background-color: $negative;
color: #EEE;
}
+ svg {
+ margin: 0;
+ }
}
.entity-selector {
--- /dev/null
+<div class="image-manager-details">
+
+ <form component="ajax-form"
+ option:ajax-form:success-message="{{ trans('components.image_update_success') }}"
+ option:ajax-form:method="put"
+ option:ajax-form:response-container=".image-manager-details"
+ option:ajax-form:url="{{ url('images/' . $image->id) }}">
+
+ <div class="image-manager-viewer">
+ <a href="{{ $image->url }}" target="_blank" class="block">
+ <img src="{{ $image->thumbs['display'] }}"
+ alt="{{ $image->name }}"
+ class="anim fadeIn"
+ title="{{ $image->name }}">
+ </a>
+ </div>
+ <div class="form-group stretch-inputs">
+ <label for="name">{{ trans('components.image_image_name') }}</label>
+ <input id="name" class="input-base" type="text" name="name" value="{{ $image->name }}">
+ </div>
+ <div class="grid half">
+ <div>
+ <button type="button"
+ id="image-manager-delete"
+ title="{{ trans('common.delete') }}"
+ class="button icon outline">@icon('delete')</button>
+ </div>
+ <div class="text-right">
+ <button type="submit"
+ class="button icon outline">{{ trans('common.save') }}</button>
+ </div>
+ </div>
+ </form>
+
+ @if(!is_null($dependantPages))
+ @if(count($dependantPages) > 0)
+ <p class="text-neg mb-xs mt-m">{{ trans('components.image_delete_used') }}</p>
+ <ul class="text-neg">
+ @foreach($dependantPages as $page)
+ <li>
+ <a href="{{ $page->url }}"
+ target="_blank"
+ class="text-neg">{{ $page->name }}</a>
+ </li>
+ @endforeach
+ </ul>
+ @endif
+ <p class="text-neg mb-xs">{{ trans('components.image_delete_confirm_text') }}</p>
+ <form component="ajax-form"
+ option:ajax-form:success-message="{{ trans('components.image_delete_success') }}"
+ option:ajax-form:method="delete"
+ option:ajax-form:response-container=".image-manager-details"
+ option:ajax-form:url="{{ url('images/' . $image->id) }}">
+ <button type="submit" class="button neg">
+ {{ trans('common.delete_confirm') }}
+ </button>
+ </form>
+ @endif
+
+</div>
\ No newline at end of file
--- /dev/null
+@foreach($images as $index => $image)
+<div>
+ <div component="event-emit-select"
+ option:event-emit-select:name="image"
+ option:event-emit-select:data="{{ json_encode($image) }}"
+ class="image anim fadeIn text-primary"
+ style="animation-delay: {{ $index > 26 ? '160ms' : ($index * 25) . 'ms' }};">
+ <img src="{{ $image->thumbs['gallery'] }}"
+ alt="{{ $image->name }}"
+ width="150"
+ height="150"
+ loading="lazy"
+ title="{{ $image->name }}">
+ <div class="image-meta">
+ <span class="name">{{ $image->name }}</span>
+ <span class="date">{{ trans('components.image_uploaded', ['uploadedDate' => $image->created_at->format('Y-m-d H:i:s')]) }}</span>
+ </div>
+ </div>
+</div>
+@endforeach
+@if($hasMore)
+ <div class="load-more">{{ trans('components.image_load_more') }}</div>
+@endif
\ No newline at end of file
-<div id="image-manager" image-type="{{ $imageType }}" uploaded-to="{{ $uploaded_to ?? 0 }}">
+<div component="image-manager"
+ option:image-manager:uploaded-to="{{ $uploaded_to ?? 0 }}"
+ class="image-manager">
- @exposeTranslations([
- 'components.image_delete_success',
- 'components.image_upload_success',
- 'errors.server_upload_limit',
- 'components.image_upload_remove',
- 'components.file_upload_timeout',
- ])
-
- <div component="popup" class="popup-background" v-cloak @click="hide">
- <div class="popup-body" tabindex="-1" @click.stop>
+ <div component="popup"
+ refs="image-manager@popup"
+ class="popup-background">
+ <div class="popup-body" tabindex="-1">
<div class="popup-header primary-background">
<div class="popup-title">{{ trans('components.image_select') }}</div>
- <button class="popup-header-close" @click="hide()">x</button>
+ <button refs="popup@hide" type="button" class="popup-header-close">x</button>
</div>
<div class="flex-fill image-manager-body">
<div class="image-manager-content">
- <div v-if="imageType === 'gallery' || imageType === 'drawio'" class="image-manager-header primary-background-light nav-tabs grid third">
- <div class="tab-item" title="{{ trans('components.image_all_title') }}" :class="{selected: !filter}" @click="setFilterType(null)">@icon('images') {{ trans('components.image_all') }}</div>
- <div class="tab-item" title="{{ trans('components.image_book_title') }}" :class="{selected: (filter=='book')}" @click="setFilterType('book')">@icon('book', ['class' => 'text-book svg-icon']) {{ trans('entities.book') }}</div>
- <div class="tab-item" title="{{ trans('components.image_page_title') }}" :class="{selected: (filter=='page')}" @click="setFilterType('page')">@icon('page', ['class' => 'text-page svg-icon']) {{ trans('entities.page') }}</div>
+ <div class="image-manager-header primary-background-light nav-tabs grid third no-gap">
+ <button refs="image-manager@filterTabs"
+ data-filter="all"
+ type="button" class="tab-item selected" title="{{ trans('components.image_all_title') }}">@icon('images') {{ trans('components.image_all') }}</button>
+ <button refs="image-manager@filterTabs"
+ data-filter="book"
+ type="button" class="tab-item" title="{{ trans('components.image_book_title') }}">@icon('book', ['class' => 'text-book svg-icon']) {{ trans('entities.book') }}</button>
+ <button refs="image-manager@filterTabs"
+ data-filter="page"
+ type="button" class="tab-item" title="{{ trans('components.image_page_title') }}">@icon('page', ['class' => 'text-page svg-icon']) {{ trans('entities.page') }}</button>
</div>
<div>
- <form @submit.prevent="searchImages" class="contained-search-box">
- <input placeholder="{{ trans('components.image_search_hint') }}" v-model="searchTerm" type="text">
- <button :class="{active: searching}" title="{{ trans('common.search_clear') }}" type="button" @click="cancelSearch()" class="text-button cancel">@icon('close')</button>
- <button title="{{ trans('common.search') }}" class="text-button">@icon('search')</button>
+ <form refs="image-manager@searchForm" class="contained-search-box">
+ <input refs="image-manager@searchInput"
+ placeholder="{{ trans('components.image_search_hint') }}"
+ type="text">
+ <button refs="image-manager@cancelSearch"
+ title="{{ trans('common.search_clear') }}"
+ type="button"
+ class="cancel">@icon('close')</button>
+ <button type="submit" class="primary-background text-white"
+ title="{{ trans('common.search') }}">@icon('search')</button>
</form>
</div>
- <div class="image-manager-list">
- <div v-if="images.length > 0" v-for="(image, idx) in images">
- <div class="image anim fadeIn" :style="{animationDelay: (idx > 26) ? '160ms' : ((idx * 25) + 'ms')}"
- :class="{selected: (image==selectedImage)}" @click="imageSelect(image)">
- <img :src="image.thumbs.gallery" :alt="image.title" :title="image.name">
- <div class="image-meta">
- <span class="name" v-text="image.name"></span>
- <span class="date">{{ trans('components.image_uploaded', ['uploadedDate' => "{{ getDate(image.created_at) }" . "}"]) }}</span>
- </div>
- </div>
- </div>
- <div class="load-more" v-show="hasMore" @click="fetchData">{{ trans('components.image_load_more') }}</div>
- </div>
+ <div refs="image-manager@listContainer" class="image-manager-list"></div>
</div>
- <div class="image-manager-sidebar">
-
- <dropzone v-if="imageType !== 'drawio'" ref="dropzone" placeholder="{{ trans('components.image_dropzone') }}" :upload-url="uploadUrl" :uploaded-to="uploadedTo" @success="uploadSuccess"></dropzone>
-
- <div class="inner">
-
- <div class="image-manager-details anim fadeIn" v-if="selectedImage">
-
- <form @submit.prevent="saveImageDetails">
- <div class="image-manager-viewer">
- <a :href="selectedImage.url" target="_blank" style="display: block;">
- <img :src="selectedImage.thumbs.display" :alt="selectedImage.name"
- :title="selectedImage.name">
- </a>
- </div>
- <div class="form-group">
- <label for="name">{{ trans('components.image_image_name') }}</label>
- <input id="name" class="input-base" name="name" v-model="selectedImage.name">
- </div>
- </form>
-
- <div class="clearfix">
- <div class="float left">
- <button type="button" class="button icon outline" @click="deleteImage">@icon('delete')</button>
-
- </div>
- <button class="button anim fadeIn float right" v-show="selectedImage" @click="callbackAndHide(selectedImage)">
- {{ trans('components.image_select_image') }}
- </button>
- <div class="clearfix"></div>
- <div v-show="dependantPages">
- <p class="text-neg text-small">
- {{ trans('components.image_delete_used') }}
- </p>
- <ul class="text-neg">
- <li v-for="page in dependantPages">
- <a :href="page.url" target="_blank" class="text-neg" v-text="page.name"></a>
- </li>
- </ul>
- </div>
- <div v-show="deleteConfirm" class="text-neg text-small">
- {{ trans('components.image_delete_confirm') }}
- </div>
- </div>
-
- </div>
+ <div class="image-manager-sidebar flex-container-column">
+ <div refs="image-manager@dropzoneContainer">
+ @include('components.dropzone', [
+ 'placeholder' => trans('components.image_dropzone'),
+ 'successMessage' => trans('components.image_upload_success'),
+ 'url' => url('/images/gallery?' . http_build_query(['uploaded_to' => $uploaded_to ?? 0]))
+ ])
+ </div>
+ <div refs="image-manager@formContainer" class="inner flex"></div>
- </div>
+ <button refs="image-manager@selectButton" type="button" class="hidden button corner-button">
+ {{ trans('components.image_select_image') }}
+ </button>
</div>
</div>
</form>
</div>
- @include('components.image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id])
+ @include('components.image-manager', ['uploaded_to' => $page->id])
@include('components.code-editor')
@include('components.entity-selector-popup')
-
@stop
\ No newline at end of file
</div>
- @include('components.image-manager', ['imageType' => 'system'])
@include('components.entity-selector-popup', ['entityTypes' => 'page'])
@stop
Route::get('/user/{userId}', 'UserController@showProfilePage');
// Image routes
- Route::group(['prefix' => 'images'], function () {
-
- // Gallery
- Route::get('/gallery', 'Images\GalleryImageController@list');
- Route::post('/gallery', 'Images\GalleryImageController@create');
-
- // Drawio
- Route::get('/drawio', 'Images\DrawioImageController@list');
- Route::get('/drawio/base64/{id}', 'Images\DrawioImageController@getAsBase64');
- Route::post('/drawio', 'Images\DrawioImageController@create');
-
- // Shared gallery & draw.io endpoint
- Route::get('/usage/{id}', 'Images\ImageController@usage');
- Route::put('/{id}', 'Images\ImageController@update');
- Route::delete('/{id}', 'Images\ImageController@destroy');
- });
+ Route::get('/images/gallery', 'Images\GalleryImageController@list');
+ Route::post('/images/gallery', 'Images\GalleryImageController@create');
+ Route::get('/images/drawio', 'Images\DrawioImageController@list');
+ Route::get('/images/drawio/base64/{id}', 'Images\DrawioImageController@getAsBase64');
+ Route::post('/images/drawio', 'Images\DrawioImageController@create');
+ Route::get('/images/edit/{id}', 'Images\ImageController@edit');
+ Route::put('/images/{id}', 'Images\ImageController@update');
+ Route::delete('/images/{id}', 'Images\ImageController@destroy');
// Attachments routes
Route::get('/attachments/{id}', 'AttachmentController@get');
$newName = Str::random();
$update = $this->put('/images/' . $image->id, ['name' => $newName]);
$update->assertSuccessful();
- $update->assertJson([
- 'id' => $image->id,
- 'name' => $newName,
- 'type' => 'gallery',
- ]);
+ $update->assertSee($newName);
$this->deleteImage($imgDetails['path']);
$imgDetails = $this->uploadGalleryImage();
$image = Image::query()->first();
- $emptyJson = ['images' => [], 'has_more' => false];
- $resultJson = [
- 'images' => [
- [
- 'id' => $image->id,
- 'name' => $imgDetails['name'],
- ]
- ],
- 'has_more' => false,
- ];
-
$pageId = $imgDetails['page']->id;
$firstPageRequest = $this->get("/images/gallery?page=1&uploaded_to={$pageId}");
- $firstPageRequest->assertSuccessful()->assertJson($resultJson);
+ $firstPageRequest->assertSuccessful()->assertElementExists('div');
+ $firstPageRequest->assertSuccessful()->assertSeeText($image->name);
$secondPageRequest = $this->get("/images/gallery?page=2&uploaded_to={$pageId}");
- $secondPageRequest->assertSuccessful()->assertExactJson($emptyJson);
+ $secondPageRequest->assertSuccessful()->assertElementNotExists('div');
$namePartial = substr($imgDetails['name'], 0, 3);
$searchHitRequest = $this->get("/images/gallery?page=1&uploaded_to={$pageId}&search={$namePartial}");
- $searchHitRequest->assertSuccessful()->assertJson($resultJson);
+ $searchHitRequest->assertSuccessful()->assertSee($imgDetails['name']);
$namePartial = Str::random(16);
- $searchHitRequest = $this->get("/images/gallery?page=1&uploaded_to={$pageId}&search={$namePartial}");
- $searchHitRequest->assertSuccessful()->assertExactJson($emptyJson);
+ $searchFailRequest = $this->get("/images/gallery?page=1&uploaded_to={$pageId}&search={$namePartial}");
+ $searchFailRequest->assertSuccessful()->assertDontSee($imgDetails['name']);
+ $searchFailRequest->assertSuccessful()->assertElementNotExists('div');
}
public function test_image_usage()
$page->html = '<img src="'.$image->url.'">';
$page->save();
- $usage = $this->get('/images/usage/' . $image->id);
+ $usage = $this->get('/images/edit/' . $image->id . '?delete=true');
$usage->assertSuccessful();
- $usage->assertJson([
- [
- 'id' => $page->id,
- 'name' => $page->name
- ]
- ]);
+ $usage->assertSeeText($page->name);
+ $usage->assertSee($page->getUrl());
$this->deleteImage($imgDetails['path']);
}