mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Merge pull request #4265 from BookStackApp/image_manager_responsive
Enhanced Responsive Image Manager
This commit is contained in:
commit
242d23788d
@ -28,4 +28,12 @@ class ApiDocsController extends ApiController
|
||||
|
||||
return response()->json($docs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the API docs page.
|
||||
*/
|
||||
public function redirect()
|
||||
{
|
||||
return redirect('/api/docs');
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ class GalleryImageController extends Controller
|
||||
$uploadedToFilter = $request->get('uploaded_to', null);
|
||||
$parentTypeFilter = $request->get('filter_type', null);
|
||||
|
||||
$imgData = $this->imageRepo->getEntityFiltered('gallery', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm);
|
||||
$imgData = $this->imageRepo->getEntityFiltered('gallery', $parentTypeFilter, $page, 30, $uploadedToFilter, $searchTerm);
|
||||
|
||||
return view('pages.parts.image-manager-list', [
|
||||
'images' => $imgData['images'],
|
||||
|
@ -14,13 +14,10 @@ use Illuminate\Validation\ValidationException;
|
||||
|
||||
class ImageController extends Controller
|
||||
{
|
||||
protected ImageRepo $imageRepo;
|
||||
protected ImageService $imageService;
|
||||
|
||||
public function __construct(ImageRepo $imageRepo, ImageService $imageService)
|
||||
{
|
||||
$this->imageRepo = $imageRepo;
|
||||
$this->imageService = $imageService;
|
||||
public function __construct(
|
||||
protected ImageRepo $imageRepo,
|
||||
protected ImageService $imageService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
@ -65,6 +62,29 @@ class ImageController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the file for an existing image.
|
||||
*/
|
||||
public function updateFile(Request $request, string $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'file' => ['required', 'file', ...$this->getImageValidationRules()],
|
||||
]);
|
||||
|
||||
$image = $this->imageRepo->getById($id);
|
||||
$this->checkImagePermission($image);
|
||||
$this->checkOwnablePermission('image-update', $image);
|
||||
$file = $request->file('file');
|
||||
|
||||
try {
|
||||
$this->imageRepo->updateImageFile($image, $file);
|
||||
} catch (ImageUploadException $exception) {
|
||||
return $this->jsonError($exception->getMessage());
|
||||
}
|
||||
|
||||
return response('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the form for editing the given image.
|
||||
*
|
||||
|
@ -30,6 +30,7 @@ class ImageGalleryApiController extends ApiController
|
||||
],
|
||||
'update' => [
|
||||
'name' => ['string', 'max:180'],
|
||||
'image' => ['file', ...$this->getImageValidationRules()],
|
||||
]
|
||||
];
|
||||
}
|
||||
@ -89,7 +90,8 @@ class ImageGalleryApiController extends ApiController
|
||||
|
||||
/**
|
||||
* Update the details of an existing image in the system.
|
||||
* Only allows updating of the image name at this time.
|
||||
* Since "image" is expected to be a file, this needs to be a 'multipart/form-data' type request if providing a
|
||||
* new image file. Updated image files should be of the same file type as the original image.
|
||||
*/
|
||||
public function update(Request $request, string $id)
|
||||
{
|
||||
@ -99,6 +101,9 @@ class ImageGalleryApiController extends ApiController
|
||||
$this->checkOwnablePermission('image-update', $image);
|
||||
|
||||
$this->imageRepo->updateImageDetails($image, $data);
|
||||
if (isset($data['image'])) {
|
||||
$this->imageRepo->updateImageFile($image, $data['image']);
|
||||
}
|
||||
|
||||
return response()->json($this->formatForSingleResponse($image));
|
||||
}
|
||||
|
@ -11,16 +11,10 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class ImageRepo
|
||||
{
|
||||
protected ImageService $imageService;
|
||||
protected PermissionApplicator $permissions;
|
||||
|
||||
/**
|
||||
* ImageRepo constructor.
|
||||
*/
|
||||
public function __construct(ImageService $imageService, PermissionApplicator $permissions)
|
||||
{
|
||||
$this->imageService = $imageService;
|
||||
$this->permissions = $permissions;
|
||||
public function __construct(
|
||||
protected ImageService $imageService,
|
||||
protected PermissionApplicator $permissions
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
@ -164,12 +158,30 @@ class ImageRepo
|
||||
public function updateImageDetails(Image $image, $updateDetails): Image
|
||||
{
|
||||
$image->fill($updateDetails);
|
||||
$image->updated_by = user()->id;
|
||||
$image->save();
|
||||
$this->loadThumbs($image);
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the image file of an existing image in the system.
|
||||
* @throws ImageUploadException
|
||||
*/
|
||||
public function updateImageFile(Image $image, UploadedFile $file): void
|
||||
{
|
||||
if ($file->getClientOriginalExtension() !== pathinfo($image->path, PATHINFO_EXTENSION)) {
|
||||
throw new ImageUploadException(trans('errors.image_upload_replace_type'));
|
||||
}
|
||||
|
||||
$image->refresh();
|
||||
$image->updated_by = user()->id;
|
||||
$image->save();
|
||||
$this->imageService->replaceExistingFromUpload($image->path, $image->type, $file);
|
||||
$this->loadThumbs($image, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys an Image object along with its revisions, files and thumbnails.
|
||||
*
|
||||
@ -202,11 +214,11 @@ class ImageRepo
|
||||
/**
|
||||
* Load thumbnails onto an image object.
|
||||
*/
|
||||
public function loadThumbs(Image $image): void
|
||||
public function loadThumbs(Image $image, bool $forceCreate = false): void
|
||||
{
|
||||
$image->setAttribute('thumbs', [
|
||||
'gallery' => $this->getThumbnail($image, 150, 150, false),
|
||||
'display' => $this->getThumbnail($image, 1680, null, true),
|
||||
'gallery' => $this->getThumbnail($image, 150, 150, false, $forceCreate),
|
||||
'display' => $this->getThumbnail($image, 1680, null, true, $forceCreate),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -215,10 +227,10 @@ class ImageRepo
|
||||
* If $keepRatio is true only the width will be used.
|
||||
* Checks the cache then storage to avoid creating / accessing the filesystem on every check.
|
||||
*/
|
||||
protected function getThumbnail(Image $image, ?int $width, ?int $height, bool $keepRatio): ?string
|
||||
protected function getThumbnail(Image $image, ?int $width, ?int $height, bool $keepRatio, bool $forceCreate): ?string
|
||||
{
|
||||
try {
|
||||
return $this->imageService->getThumbnail($image, $width, $height, $keepRatio);
|
||||
return $this->imageService->getThumbnail($image, $width, $height, $keepRatio, $forceCreate);
|
||||
} catch (Exception $exception) {
|
||||
return null;
|
||||
}
|
||||
|
@ -194,6 +194,14 @@ class ImageService
|
||||
return $image;
|
||||
}
|
||||
|
||||
public function replaceExistingFromUpload(string $path, string $type, UploadedFile $file): void
|
||||
{
|
||||
$imageData = file_get_contents($file->getRealPath());
|
||||
$storage = $this->getStorageDisk($type);
|
||||
$adjustedPath = $this->adjustPathForStorageDisk($path, $type);
|
||||
$storage->put($adjustedPath, $imageData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save image data for the given path in the public space, if possible,
|
||||
* for the provided storage mechanism.
|
||||
@ -262,7 +270,7 @@ class ImageService
|
||||
* @throws Exception
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function getThumbnail(Image $image, ?int $width, ?int $height, bool $keepRatio = false): string
|
||||
public function getThumbnail(Image $image, ?int $width, ?int $height, bool $keepRatio = false, bool $forceCreate = false): string
|
||||
{
|
||||
// Do not resize GIF images where we're not cropping
|
||||
if ($keepRatio && $this->isGif($image)) {
|
||||
@ -277,13 +285,13 @@ class ImageService
|
||||
|
||||
// Return path if in cache
|
||||
$cachedThumbPath = $this->cache->get($thumbCacheKey);
|
||||
if ($cachedThumbPath) {
|
||||
if ($cachedThumbPath && !$forceCreate) {
|
||||
return $this->getPublicUrl($cachedThumbPath);
|
||||
}
|
||||
|
||||
// If thumbnail has already been generated, serve that and cache path
|
||||
$storage = $this->getStorageDisk($image->type);
|
||||
if ($storage->exists($this->adjustPathForStorageDisk($thumbFilePath, $image->type))) {
|
||||
if (!$forceCreate && $storage->exists($this->adjustPathForStorageDisk($thumbFilePath, $image->type))) {
|
||||
$this->cache->put($thumbCacheKey, $thumbFilePath, 60 * 60 * 72);
|
||||
|
||||
return $this->getPublicUrl($thumbFilePath);
|
||||
|
@ -6,6 +6,7 @@ return [
|
||||
|
||||
// Buttons
|
||||
'cancel' => 'Cancel',
|
||||
'close' => 'Close',
|
||||
'confirm' => 'Confirm',
|
||||
'back' => 'Back',
|
||||
'save' => 'Save',
|
||||
|
@ -6,6 +6,8 @@ return [
|
||||
|
||||
// Image Manager
|
||||
'image_select' => 'Image Select',
|
||||
'image_list' => 'Image List',
|
||||
'image_details' => 'Image Details',
|
||||
'image_upload' => 'Upload Image',
|
||||
'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
|
||||
'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
|
||||
@ -15,6 +17,9 @@ return [
|
||||
'image_page_title' => 'View images uploaded to this page',
|
||||
'image_search_hint' => 'Search by image name',
|
||||
'image_uploaded' => 'Uploaded :uploadedDate',
|
||||
'image_uploaded_by' => 'Uploaded by :userName',
|
||||
'image_uploaded_to' => 'Uploaded to :pageLink',
|
||||
'image_updated' => 'Updated :updateDate',
|
||||
'image_load_more' => 'Load More',
|
||||
'image_image_name' => 'Image Name',
|
||||
'image_delete_used' => 'This image is used in the pages below.',
|
||||
@ -27,6 +32,8 @@ return [
|
||||
'image_upload_success' => 'Image uploaded successfully',
|
||||
'image_update_success' => 'Image details successfully updated',
|
||||
'image_delete_success' => 'Image successfully deleted',
|
||||
'image_replace' => 'Replace Image',
|
||||
'image_replace_success' => 'Image file successfully updated',
|
||||
|
||||
// Code Editor
|
||||
'code_editor' => 'Edit Code',
|
||||
|
@ -49,6 +49,7 @@ return [
|
||||
// Drawing & Images
|
||||
'image_upload_error' => 'An error occurred uploading the image',
|
||||
'image_upload_type_error' => 'The image type being uploaded is invalid',
|
||||
'image_upload_replace_type' => 'Image file replacements must be of the same type',
|
||||
'drawing_data_not_found' => 'Drawing data could not be loaded. The drawing file might no longer exist or you may not have permission to access it.',
|
||||
|
||||
// Attachments
|
||||
|
@ -15,6 +15,7 @@ export class Dropzone extends Component {
|
||||
this.isActive = true;
|
||||
|
||||
this.url = this.$opts.url;
|
||||
this.method = (this.$opts.method || 'post').toUpperCase();
|
||||
this.successMessage = this.$opts.successMessage;
|
||||
this.errorMessage = this.$opts.errorMessage;
|
||||
this.uploadLimitMb = Number(this.$opts.uploadLimit);
|
||||
@ -167,6 +168,9 @@ export class Dropzone extends Component {
|
||||
startXhrForUpload(upload) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', upload.file, upload.file.name);
|
||||
if (this.method !== 'POST') {
|
||||
formData.append('_method', this.method);
|
||||
}
|
||||
const component = this;
|
||||
|
||||
const req = window.$http.createXMLHttpRequest('POST', this.url, {
|
||||
|
@ -23,6 +23,7 @@ export class ImageManager extends Component {
|
||||
this.formContainer = this.$refs.formContainer;
|
||||
this.formContainerPlaceholder = this.$refs.formContainerPlaceholder;
|
||||
this.dropzoneContainer = this.$refs.dropzoneContainer;
|
||||
this.loadMore = this.$refs.loadMore;
|
||||
|
||||
// Instance data
|
||||
this.type = 'gallery';
|
||||
@ -40,6 +41,7 @@ export class ImageManager extends Component {
|
||||
}
|
||||
|
||||
setupListeners() {
|
||||
// Filter tab click
|
||||
onSelect(this.filterTabs, e => {
|
||||
this.resetAll();
|
||||
this.filter = e.target.dataset.filter;
|
||||
@ -47,32 +49,33 @@ export class ImageManager extends Component {
|
||||
this.loadGallery();
|
||||
});
|
||||
|
||||
// Search submit
|
||||
this.searchForm.addEventListener('submit', event => {
|
||||
this.resetListView();
|
||||
this.loadGallery();
|
||||
this.cancelSearch.toggleAttribute('hidden', !this.searchInput.value);
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
// Cancel search button
|
||||
onSelect(this.cancelSearch, () => {
|
||||
this.resetListView();
|
||||
this.resetSearchView();
|
||||
this.loadGallery();
|
||||
});
|
||||
|
||||
onChildEvent(this.listContainer, '.load-more button', 'click', async event => {
|
||||
const wrapper = event.target.closest('.load-more');
|
||||
showLoading(wrapper);
|
||||
this.page += 1;
|
||||
await this.loadGallery();
|
||||
wrapper.remove();
|
||||
});
|
||||
// Load more button click
|
||||
onChildEvent(this.container, '.load-more button', 'click', this.runLoadMore.bind(this));
|
||||
|
||||
// Select image event
|
||||
this.listContainer.addEventListener('event-emit-select-image', this.onImageSelectEvent.bind(this));
|
||||
|
||||
// Image load error handling
|
||||
this.listContainer.addEventListener('error', event => {
|
||||
event.target.src = window.baseUrl('loading_error.png');
|
||||
}, true);
|
||||
|
||||
// Footer select button click
|
||||
onSelect(this.selectButton, () => {
|
||||
if (this.callback) {
|
||||
this.callback(this.lastSelected);
|
||||
@ -80,17 +83,39 @@ export class ImageManager extends Component {
|
||||
this.hide();
|
||||
});
|
||||
|
||||
// Delete button click
|
||||
onChildEvent(this.formContainer, '#image-manager-delete', 'click', () => {
|
||||
if (this.lastSelected) {
|
||||
this.loadImageEditForm(this.lastSelected.id, true);
|
||||
}
|
||||
});
|
||||
|
||||
// Edit form submit
|
||||
this.formContainer.addEventListener('ajax-form-success', () => {
|
||||
this.refreshGallery();
|
||||
this.resetEditForm();
|
||||
});
|
||||
|
||||
// Image upload success
|
||||
this.container.addEventListener('dropzone-upload-success', this.refreshGallery.bind(this));
|
||||
|
||||
// Auto load-more on scroll
|
||||
const scrollZone = this.listContainer.parentElement;
|
||||
let scrollEvents = [];
|
||||
scrollZone.addEventListener('wheel', event => {
|
||||
const scrollOffset = Math.ceil(scrollZone.scrollHeight - scrollZone.scrollTop);
|
||||
const bottomedOut = scrollOffset === scrollZone.clientHeight;
|
||||
if (!bottomedOut || event.deltaY < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const secondAgo = Date.now() - 1000;
|
||||
scrollEvents.push(Date.now());
|
||||
scrollEvents = scrollEvents.filter(d => d >= secondAgo);
|
||||
if (scrollEvents.length > 5 && this.canLoadMore()) {
|
||||
this.runLoadMore();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
show(callback, type = 'gallery') {
|
||||
@ -145,6 +170,14 @@ export class ImageManager extends Component {
|
||||
addReturnedHtmlElementsToList(html) {
|
||||
const el = document.createElement('div');
|
||||
el.innerHTML = html;
|
||||
|
||||
const loadMore = el.querySelector('.load-more');
|
||||
if (loadMore) {
|
||||
loadMore.remove();
|
||||
this.loadMore.innerHTML = loadMore.innerHTML;
|
||||
}
|
||||
this.loadMore.toggleAttribute('hidden', !loadMore);
|
||||
|
||||
window.$components.init(el);
|
||||
for (const child of [...el.children]) {
|
||||
this.listContainer.appendChild(child);
|
||||
@ -169,6 +202,7 @@ export class ImageManager extends Component {
|
||||
|
||||
resetSearchView() {
|
||||
this.searchInput.value = '';
|
||||
this.cancelSearch.toggleAttribute('hidden', true);
|
||||
}
|
||||
|
||||
resetEditForm() {
|
||||
@ -224,4 +258,14 @@ export class ImageManager extends Component {
|
||||
window.$components.init(this.formContainer);
|
||||
}
|
||||
|
||||
runLoadMore() {
|
||||
showLoading(this.loadMore);
|
||||
this.page += 1;
|
||||
this.loadGallery();
|
||||
}
|
||||
|
||||
canLoadMore() {
|
||||
return this.loadMore.querySelector('button') && !this.loadMore.hasAttribute('hidden');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -21,15 +21,23 @@ export class Tabs extends Component {
|
||||
|
||||
setup() {
|
||||
this.container = this.$el;
|
||||
this.tabs = Array.from(this.container.querySelectorAll('[role="tab"]'));
|
||||
this.panels = Array.from(this.container.querySelectorAll('[role="tabpanel"]'));
|
||||
this.tabList = this.container.querySelector('[role="tablist"]');
|
||||
this.tabs = Array.from(this.tabList.querySelectorAll('[role="tab"]'));
|
||||
this.panels = Array.from(this.container.querySelectorAll(':scope > [role="tabpanel"], :scope > * > [role="tabpanel"]'));
|
||||
this.activeUnder = this.$opts.activeUnder ? Number(this.$opts.activeUnder) : 10000;
|
||||
this.active = null;
|
||||
|
||||
this.container.addEventListener('click', event => {
|
||||
const button = event.target.closest('[role="tab"]');
|
||||
if (button) {
|
||||
this.show(button.getAttribute('aria-controls'));
|
||||
const tab = event.target.closest('[role="tab"]');
|
||||
if (tab && this.tabs.includes(tab)) {
|
||||
this.show(tab.getAttribute('aria-controls'));
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('resize', this.updateActiveState.bind(this), {
|
||||
passive: true,
|
||||
});
|
||||
this.updateActiveState();
|
||||
}
|
||||
|
||||
show(sectionId) {
|
||||
@ -46,4 +54,35 @@ export class Tabs extends Component {
|
||||
this.$emit('change', {showing: sectionId});
|
||||
}
|
||||
|
||||
updateActiveState() {
|
||||
const active = window.innerWidth < this.activeUnder;
|
||||
if (active === this.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (active) {
|
||||
this.activate();
|
||||
} else {
|
||||
this.deactivate();
|
||||
}
|
||||
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
activate() {
|
||||
const panelToShow = this.panels.find(p => !p.hasAttribute('hidden')) || this.panels[0];
|
||||
this.show(panelToShow.id);
|
||||
this.tabList.toggleAttribute('hidden', false);
|
||||
}
|
||||
|
||||
deactivate() {
|
||||
for (const panel of this.panels) {
|
||||
panel.removeAttribute('hidden');
|
||||
}
|
||||
for (const tab of this.tabs) {
|
||||
tab.setAttribute('aria-selected', 'false');
|
||||
}
|
||||
this.tabList.toggleAttribute('hidden', true);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
.anim.fadeIn {
|
||||
opacity: 0;
|
||||
animation-name: fadeIn;
|
||||
animation-duration: 180ms;
|
||||
animation-duration: 120ms;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
@ -200,10 +200,6 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.image-manager-body {
|
||||
min-height: 70vh;
|
||||
}
|
||||
|
||||
.dropzone-overlay {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
@ -347,43 +343,99 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.image-manager-body {
|
||||
min-height: 70vh;
|
||||
}
|
||||
.image-manager-filter-bar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
@include lightDark(background-color, rgba(255, 255, 255, 0.85), rgba(80, 80, 80, 0.85));
|
||||
}
|
||||
.image-manager-filter-bar-bg {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: .15;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.image-manager-filters {
|
||||
box-shadow: $bs-med;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
border-bottom: 0 !important;
|
||||
@include whenDark {
|
||||
border: 1px solid #000 !important;
|
||||
}
|
||||
button {
|
||||
line-height: 0;
|
||||
@include lightDark(background-color, #FFF, #333);
|
||||
}
|
||||
svg {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.image-manager-list {
|
||||
padding: 3px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat( auto-fit, minmax(140px, 1fr) );
|
||||
gap: 3px;
|
||||
z-index: 3;
|
||||
> div {
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.image-manager-list .image {
|
||||
display: block;
|
||||
position: relative;
|
||||
border-radius: 0;
|
||||
float: left;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
text-align: start;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
width: math.div(100%, 6);
|
||||
height: auto;
|
||||
aspect-ratio: 1;
|
||||
@include lightDark(border-color, #ddd, #000);
|
||||
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
|
||||
transition: all cubic-bezier(.4, 0, 1, 1) 160ms;
|
||||
transition: all linear 80ms;
|
||||
overflow: hidden;
|
||||
&.selected {
|
||||
transform: scale3d(0.92, 0.92, 0.92);
|
||||
outline: currentColor 2px solid;
|
||||
background-color: var(--color-primary-light);
|
||||
outline: currentColor 3px solid;
|
||||
border-radius: 3px;
|
||||
transform: scale3d(0.95, 0.95, 0.95);
|
||||
}
|
||||
img {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
height: auto;
|
||||
}
|
||||
.image-meta {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
color: #EEE;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
font-size: 10px;
|
||||
padding: 3px 4px;
|
||||
pointer-events: none;
|
||||
transition: opacity ease-in-out 80ms;
|
||||
span {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@include smaller-than($xl) {
|
||||
width: math.div(100%, 4);
|
||||
&.selected .image-meta,
|
||||
&:hover .image-meta,
|
||||
&:focus .image-meta {
|
||||
opacity: 1;
|
||||
}
|
||||
@include smaller-than($m) {
|
||||
.image-meta {
|
||||
@ -393,7 +445,6 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
||||
}
|
||||
|
||||
.image-manager .load-more {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: $-s $-m;
|
||||
clear: both;
|
||||
@ -408,6 +459,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
||||
|
||||
.image-manager-sidebar {
|
||||
width: 300px;
|
||||
margin: 0 auto;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
border-inline-start: 1px solid #DDD;
|
||||
@ -433,16 +485,17 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-manager-list {
|
||||
overflow-y: scroll;
|
||||
flex: 1;
|
||||
@include smaller-than($m) {
|
||||
.image-manager-sidebar {
|
||||
border-inline-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.image-manager-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow-y: scroll;
|
||||
.container {
|
||||
width: 100%;
|
||||
}
|
||||
@ -451,18 +504,14 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
||||
}
|
||||
}
|
||||
|
||||
.image-manager [role="tablist"] button[role="tab"] {
|
||||
border-right: 1px solid #DDD;
|
||||
@include lightDark(border-color, #DDD, #000);
|
||||
.tab-container.bordered [role="tablist"] button[role="tab"] {
|
||||
border-inline-end: 1px solid #DDD;
|
||||
@include lightDark(border-inline-end-color, #DDD, #000);
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
border-inline-end: none;
|
||||
}
|
||||
}
|
||||
|
||||
.image-manager-header {
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.tab-container [role="tablist"] {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
@ -473,8 +522,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
||||
margin-bottom: $-m;
|
||||
}
|
||||
|
||||
.tab-container [role="tablist"] button[role="tab"],
|
||||
.image-manager [role="tablist"] button[role="tab"] {
|
||||
.tab-container [role="tablist"] button[role="tab"] {
|
||||
display: inline-block;
|
||||
padding: $-s;
|
||||
@include lightDark(color, rgba(0, 0, 0, .5), rgba(255, 255, 255, .5));
|
||||
@ -484,11 +532,16 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
||||
&[aria-selected="true"] {
|
||||
color: var(--color-link) !important;
|
||||
border-bottom-color: var(--color-link) !important;
|
||||
outline: 0 !important;
|
||||
}
|
||||
&:hover, &:focus {
|
||||
@include lightDark(color, rgba(0, 0, 0, .8), rgba(255, 255, 255, .8));
|
||||
@include lightDark(border-bottom-color, rgba(0, 0, 0, .2), rgba(255, 255, 255, .2));
|
||||
}
|
||||
&:focus {
|
||||
outline: 1px dotted var(--color-primary);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
}
|
||||
.tab-container [role="tablist"].controls-card {
|
||||
margin-bottom: 0;
|
||||
|
@ -467,6 +467,58 @@ div[editor-type="markdown"] .title-input.page-title input[type="text"] {
|
||||
}
|
||||
}
|
||||
|
||||
.contained-search-box {
|
||||
display: flex;
|
||||
height: 38px;
|
||||
z-index: -1;
|
||||
&.floating {
|
||||
box-shadow: $bs-med;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
@include whenDark {
|
||||
border: 1px solid #000;
|
||||
}
|
||||
}
|
||||
input, button {
|
||||
height: 100%;
|
||||
border-radius: 0;
|
||||
border: 1px solid #ddd;
|
||||
@include lightDark(border-color, #ddd, #000);
|
||||
margin-inline-start: -1px;
|
||||
&:last-child {
|
||||
border-inline-end: 0;
|
||||
}
|
||||
}
|
||||
input {
|
||||
border: 0;
|
||||
flex: 5;
|
||||
padding: $-xs $-s;
|
||||
&:focus, &:active {
|
||||
outline: 1px dotted var(--color-primary);
|
||||
outline-offset: -2px;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
button {
|
||||
border: 0;
|
||||
width: 48px;
|
||||
border-inline-start: 1px solid #DDD;
|
||||
background-color: #FFF;
|
||||
@include lightDark(background-color, #FFF, #333);
|
||||
@include lightDark(color, #444, #AAA);
|
||||
}
|
||||
button:focus {
|
||||
outline: 1px dotted var(--color-primary);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
svg {
|
||||
margin: 0;
|
||||
}
|
||||
@include smaller-than($s) {
|
||||
width: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
.outline > input {
|
||||
border: 0;
|
||||
border-bottom: 2px solid #DDD;
|
||||
|
@ -298,6 +298,10 @@ body.flexbox {
|
||||
}
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/**
|
||||
* Border radiuses
|
||||
*/
|
||||
|
@ -674,6 +674,10 @@ ul.pagination {
|
||||
text-align: start !important;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
&.anchor-left {
|
||||
inset-inline-end: auto;
|
||||
inset-inline-start: 0;
|
||||
}
|
||||
&.wide {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
@ -117,44 +117,6 @@ $loadingSize: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.contained-search-box {
|
||||
display: flex;
|
||||
height: 38px;
|
||||
z-index: -1;
|
||||
input, button {
|
||||
height: 100%;
|
||||
border-radius: 0;
|
||||
border: 1px solid #ddd;
|
||||
@include lightDark(border-color, #ddd, #000);
|
||||
margin-inline-start: -1px;
|
||||
&:last-child {
|
||||
border-inline-end: 0;
|
||||
}
|
||||
}
|
||||
input {
|
||||
flex: 5;
|
||||
padding: $-xs $-s;
|
||||
&:focus, &:active {
|
||||
outline: 1px dotted var(--color-primary);
|
||||
outline-offset: -2px;
|
||||
border: 1px solid #ddd;
|
||||
@include lightDark(border-color, #ddd, #000);
|
||||
}
|
||||
}
|
||||
button {
|
||||
width: 60px;
|
||||
}
|
||||
button.primary-background {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
button i {
|
||||
padding: 0;
|
||||
}
|
||||
svg {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.entity-selector {
|
||||
border: 1px solid #DDD;
|
||||
@include lightDark(border-color, #ddd, #111);
|
||||
|
@ -1,4 +1,14 @@
|
||||
<div class="image-manager-details">
|
||||
<div component="dropzone"
|
||||
option:dropzone:url="{{ url("/images/{$image->id}/file") }}"
|
||||
option:dropzone:method="PUT"
|
||||
option:dropzone:success-message="{{ trans('components.image_update_success') }}"
|
||||
option:dropzone:upload-limit="{{ config('app.upload_limit') }}"
|
||||
option:dropzone:upload-limit-message="{{ trans('errors.server_upload_limit') }}"
|
||||
option:dropzone:zone-text="{{ trans('entities.attachments_dropzone') }}"
|
||||
option:dropzone:file-accept="image/*"
|
||||
class="image-manager-details">
|
||||
|
||||
<div refs="dropzone@status-area dropzone@drop-target"></div>
|
||||
|
||||
<form component="ajax-form"
|
||||
option:ajax-form:success-message="{{ trans('components.image_update_success') }}"
|
||||
@ -18,23 +28,33 @@
|
||||
<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>
|
||||
@if(userCan('image-delete', $image))
|
||||
<button type="button"
|
||||
id="image-manager-delete"
|
||||
title="{{ trans('common.delete') }}"
|
||||
class="button icon outline">@icon('delete')</button>
|
||||
@endif
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="flex-container-row justify-space-between gap-m">
|
||||
@if(userCan('image-delete', $image) || userCan('image-update', $image))
|
||||
<div component="dropdown"
|
||||
class="dropdown-container">
|
||||
<button refs="dropdown@toggle" type="button" class="button icon outline">@icon('more')</button>
|
||||
<div refs="dropdown@menu" class="dropdown-menu anchor-left">
|
||||
@if(userCan('image-delete', $image))
|
||||
<button type="button"
|
||||
id="image-manager-delete"
|
||||
class="text-item">{{ trans('common.delete') }}</button>
|
||||
@endif
|
||||
@if(userCan('image-update', $image))
|
||||
<button type="button"
|
||||
id="image-manager-replace"
|
||||
refs="dropzone@select-button"
|
||||
class="text-item">{{ trans('components.image_replace') }}</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<button type="submit"
|
||||
class="button icon outline">{{ trans('common.save') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@if(!is_null($dependantPages))
|
||||
<hr>
|
||||
@if(count($dependantPages) > 0)
|
||||
<p class="text-neg mb-xs mt-m">{{ trans('components.image_delete_used') }}</p>
|
||||
<ul class="text-neg">
|
||||
@ -60,4 +80,27 @@
|
||||
</form>
|
||||
@endif
|
||||
|
||||
<div class="text-muted text-small">
|
||||
<hr class="my-m">
|
||||
<div title="{{ $image->created_at->format('Y-m-d H:i:s') }}">
|
||||
@icon('star') {{ trans('components.image_uploaded', ['uploadedDate' => $image->created_at->diffForHumans()]) }}
|
||||
</div>
|
||||
@if($image->created_at->valueOf() !== $image->updated_at->valueOf())
|
||||
<div title="{{ $image->updated_at->format('Y-m-d H:i:s') }}">
|
||||
@icon('edit') {{ trans('components.image_updated', ['updateDate' => $image->updated_at->diffForHumans()]) }}
|
||||
</div>
|
||||
@endif
|
||||
@if($image->createdBy)
|
||||
<div>@icon('user') {{ trans('components.image_uploaded_by', ['userName' => $image->createdBy->name]) }}</div>
|
||||
@endif
|
||||
@if(($page = $image->getPage()) && userCan('view', $page))
|
||||
<div>
|
||||
@icon('page')
|
||||
{!! trans('components.image_uploaded_to', [
|
||||
'pageLink' => '<a class="text-page" href="' . e($page->getUrl()) . '" target="_blank">' . e($page->name) . '</a>'
|
||||
]) !!}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
@ -1,23 +1,26 @@
|
||||
@foreach($images as $index => $image)
|
||||
<div>
|
||||
<div component="event-emit-select"
|
||||
<button component="event-emit-select"
|
||||
option:event-emit-select:name="image"
|
||||
option:event-emit-select:data="{{ json_encode($image) }}"
|
||||
class="image anim fadeIn text-link"
|
||||
style="animation-delay: {{ $index > 26 ? '160ms' : ($index * 25) . 'ms' }};">
|
||||
style="animation-delay: {{ min($index * 10, 260) . 'ms' }};">
|
||||
<img src="{{ $image->thumbs['gallery'] }}"
|
||||
alt="{{ $image->name }}"
|
||||
role="none"
|
||||
width="150"
|
||||
height="150"
|
||||
loading="lazy"
|
||||
title="{{ $image->name }}">
|
||||
loading="lazy">
|
||||
<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>
|
||||
<span class="date">{{ trans('components.image_uploaded', ['uploadedDate' => $image->created_at->format('Y-m-d')]) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
@endforeach
|
||||
@if(count($images) === 0)
|
||||
<p class="m-m text-bigger italic text-muted">{{ trans('common.no_items') }}</p>
|
||||
@endif
|
||||
@if($hasMore)
|
||||
<div class="load-more">
|
||||
<button type="button" class="button small outline">{{ trans('components.image_load_more') }}</button>
|
||||
|
@ -21,60 +21,98 @@
|
||||
<span>@icon('upload')</span>
|
||||
<span>{{ trans('components.image_upload') }}</span>
|
||||
</button>
|
||||
<button refs="popup@hide" type="button" class="popup-header-close">@icon('close')</button>
|
||||
<button refs="popup@hide"
|
||||
type="button"
|
||||
title="{{ trans('common.close') }}"
|
||||
class="popup-header-close">@icon('close')</button>
|
||||
</div>
|
||||
|
||||
<div refs="dropzone@drop-target" class="flex-fill image-manager-body">
|
||||
|
||||
<div class="image-manager-content">
|
||||
<div role="tablist" class="image-manager-header grid third no-gap">
|
||||
<button refs="image-manager@filterTabs"
|
||||
data-filter="all"
|
||||
role="tab"
|
||||
<div component="tabs"
|
||||
option:tabs:active-under="880"
|
||||
refs="dropzone@drop-target"
|
||||
class="flex-container-column image-manager-body">
|
||||
<div class="tab-container">
|
||||
<div role="tablist" class="hide-over-m mb-none">
|
||||
<button id="image-manager-list-tab"
|
||||
aria-selected="true"
|
||||
type="button" class="tab-item" title="{{ trans('components.image_all_title') }}">@icon('images') {{ trans('components.image_all') }}</button>
|
||||
<button refs="image-manager@filterTabs"
|
||||
data-filter="book"
|
||||
role="tab"
|
||||
aria-selected="false"
|
||||
type="button" class="tab-item" title="{{ trans('components.image_book_title') }}">@icon('book', ['class' => 'svg-icon']) {{ trans('entities.book') }}</button>
|
||||
<button refs="image-manager@filterTabs"
|
||||
data-filter="page"
|
||||
role="tab"
|
||||
aria-selected="false"
|
||||
type="button" class="tab-item" title="{{ trans('components.image_page_title') }}">@icon('page', ['class' => 'svg-icon']) {{ trans('entities.page') }}</button>
|
||||
</div>
|
||||
<div>
|
||||
<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 refs="image-manager@listContainer" class="image-manager-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="image-manager-sidebar flex-container-column">
|
||||
|
||||
<div refs="image-manager@dropzoneContainer">
|
||||
<div refs="dropzone@status-area"></div>
|
||||
</div>
|
||||
|
||||
<div refs="image-manager@form-container-placeholder" class="p-m text-small text-muted">
|
||||
<p>{{ trans('components.image_intro') }}</p>
|
||||
<p refs="image-manager@upload-hint">{{ trans('components.image_intro_upload') }}</p>
|
||||
</div>
|
||||
|
||||
<div refs="image-manager@formContainer" class="inner flex">
|
||||
aria-controls="image-manager-list"
|
||||
role="tab">{{ trans('components.image_list') }}</button>
|
||||
<button id="image-manager-info-tab"
|
||||
aria-selected="true"
|
||||
aria-controls="image-manager-info"
|
||||
role="tab">{{ trans('components.image_details') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-container-row flex-fill flex">
|
||||
<div id="image-manager-list"
|
||||
tabindex="0"
|
||||
role="tabpanel"
|
||||
aria-labelledby="image-manager-list-tab"
|
||||
class="image-manager-content">
|
||||
<div class="image-manager-filter-bar flex-container-row wrap justify-space-between">
|
||||
<div class="primary-background image-manager-filter-bar-bg"></div>
|
||||
<div>
|
||||
<form refs="image-manager@searchForm" role="search" class="contained-search-box floating mx-m my-s">
|
||||
<input refs="image-manager@searchInput"
|
||||
placeholder="{{ trans('components.image_search_hint') }}"
|
||||
type="search">
|
||||
<button refs="image-manager@cancelSearch"
|
||||
title="{{ trans('common.search_clear') }}"
|
||||
type="button"
|
||||
hidden="hidden"
|
||||
class="cancel">@icon('close')</button>
|
||||
<button type="submit"
|
||||
title="{{ trans('common.search') }}">@icon('search')</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-container bordered mx-m my-s">
|
||||
<div role="tablist" class="image-manager-filters flex-container-row mb-none">
|
||||
<button refs="image-manager@filterTabs"
|
||||
data-filter="all"
|
||||
role="tab"
|
||||
aria-selected="true"
|
||||
type="button"
|
||||
title="{{ trans('components.image_all_title') }}">@icon('images')</button>
|
||||
<button refs="image-manager@filterTabs"
|
||||
data-filter="book"
|
||||
role="tab"
|
||||
aria-selected="false"
|
||||
type="button"
|
||||
title="{{ trans('components.image_book_title') }}">@icon('book', ['class' => 'svg-icon'])</button>
|
||||
<button refs="image-manager@filterTabs"
|
||||
data-filter="page"
|
||||
role="tab"
|
||||
aria-selected="false"
|
||||
type="button"
|
||||
title="{{ trans('components.image_page_title') }}">@icon('page', ['class' => 'svg-icon'])</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div refs="image-manager@listContainer" class="image-manager-list"></div>
|
||||
<div refs="image-manager@loadMore" class="load-more" hidden>
|
||||
<button type="button" class="button small outline">Load More</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="image-manager-info"
|
||||
tabindex="0"
|
||||
role="tabpanel"
|
||||
aria-labelledby="image-manager-info-tab"
|
||||
class="image-manager-sidebar flex-container-column">
|
||||
|
||||
<div refs="image-manager@dropzoneContainer">
|
||||
<div refs="dropzone@status-area"></div>
|
||||
</div>
|
||||
|
||||
<div refs="image-manager@form-container-placeholder" class="p-m text-small text-muted">
|
||||
<p>{{ trans('components.image_intro') }}</p>
|
||||
<p refs="image-manager@upload-hint">{{ trans('components.image_intro_upload') }}</p>
|
||||
</div>
|
||||
|
||||
<div refs="image-manager@formContainer" class="inner flex">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="popup-footer">
|
||||
|
@ -29,7 +29,7 @@ Route::middleware('auth')->group(function () {
|
||||
->where('path', '.*$');
|
||||
|
||||
// API docs routes
|
||||
Route::redirect('/api', '/api/docs');
|
||||
Route::get('/api', [ApiDocsController::class, 'redirect']);
|
||||
Route::get('/api/docs', [ApiDocsController::class, 'display']);
|
||||
|
||||
Route::get('/pages/recently-updated', [EntityControllers\PageController::class, 'showRecentlyUpdated']);
|
||||
@ -140,6 +140,7 @@ Route::middleware('auth')->group(function () {
|
||||
Route::get('/images/drawio/base64/{id}', [UploadControllers\DrawioImageController::class, 'getAsBase64']);
|
||||
Route::post('/images/drawio', [UploadControllers\DrawioImageController::class, 'create']);
|
||||
Route::get('/images/edit/{id}', [UploadControllers\ImageController::class, 'edit']);
|
||||
Route::put('/images/{id}/file', [UploadControllers\ImageController::class, 'updateFile']);
|
||||
Route::put('/images/{id}', [UploadControllers\ImageController::class, 'update']);
|
||||
Route::delete('/images/{id}', [UploadControllers\ImageController::class, 'destroy']);
|
||||
|
||||
|
@ -295,7 +295,24 @@ class ImageGalleryApiTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_update_endpoint_requires_image_delete_permission()
|
||||
public function test_update_existing_image_file()
|
||||
{
|
||||
$this->actingAsApiAdmin();
|
||||
$imagePage = $this->entities->page();
|
||||
$data = $this->files->uploadGalleryImageToPage($this, $imagePage);
|
||||
$image = Image::findOrFail($data['response']->id);
|
||||
|
||||
$this->assertFileEquals($this->files->testFilePath('test-image.png'), public_path($data['path']));
|
||||
|
||||
$resp = $this->call('PUT', $this->baseEndpoint . "/{$image->id}", [], [], [
|
||||
'image' => $this->files->uploadedImage('my-cool-image.png', 'compressed.png'),
|
||||
]);
|
||||
|
||||
$resp->assertStatus(200);
|
||||
$this->assertFileEquals($this->files->testFilePath('compressed.png'), public_path($data['path']));
|
||||
}
|
||||
|
||||
public function test_update_endpoint_requires_image_update_permission()
|
||||
{
|
||||
$user = $this->users->editor();
|
||||
$this->actingAsForApi($user);
|
||||
|
@ -92,6 +92,45 @@ class ImageTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_image_file_update()
|
||||
{
|
||||
$page = $this->entities->page();
|
||||
$this->asEditor();
|
||||
|
||||
$imgDetails = $this->files->uploadGalleryImageToPage($this, $page);
|
||||
$relPath = $imgDetails['path'];
|
||||
|
||||
$newUpload = $this->files->uploadedImage('updated-image.png', 'compressed.png');
|
||||
$this->assertFileEquals($this->files->testFilePath('test-image.png'), public_path($relPath));
|
||||
|
||||
$imageId = $imgDetails['response']->id;
|
||||
$this->call('PUT', "/images/{$imageId}/file", [], [], ['file' => $newUpload])
|
||||
->assertOk();
|
||||
|
||||
$this->assertFileEquals($this->files->testFilePath('compressed.png'), public_path($relPath));
|
||||
|
||||
$this->files->deleteAtRelativePath($relPath);
|
||||
}
|
||||
|
||||
public function test_image_file_update_does_not_allow_change_in_image_extension()
|
||||
{
|
||||
$page = $this->entities->page();
|
||||
$this->asEditor();
|
||||
|
||||
$imgDetails = $this->files->uploadGalleryImageToPage($this, $page);
|
||||
$relPath = $imgDetails['path'];
|
||||
$newUpload = $this->files->uploadedImage('updated-image.jpg', 'compressed.png');
|
||||
|
||||
$imageId = $imgDetails['response']->id;
|
||||
$this->call('PUT', "/images/{$imageId}/file", [], [], ['file' => $newUpload])
|
||||
->assertJson([
|
||||
"message" => "Image file replacements must be of the same type",
|
||||
"status" => "error",
|
||||
]);
|
||||
|
||||
$this->files->deleteAtRelativePath($relPath);
|
||||
}
|
||||
|
||||
public function test_gallery_get_list_format()
|
||||
{
|
||||
$this->asEditor();
|
||||
@ -493,15 +532,15 @@ class ImageTest extends TestCase
|
||||
$image = Image::first();
|
||||
|
||||
$resp = $this->get("/images/edit/{$image->id}");
|
||||
$this->withHtml($resp)->assertElementExists('button#image-manager-delete[title="Delete"]');
|
||||
$this->withHtml($resp)->assertElementExists('button#image-manager-delete');
|
||||
|
||||
$resp = $this->actingAs($viewer)->get("/images/edit/{$image->id}");
|
||||
$this->withHtml($resp)->assertElementNotExists('button#image-manager-delete[title="Delete"]');
|
||||
$this->withHtml($resp)->assertElementNotExists('button#image-manager-delete');
|
||||
|
||||
$this->permissions->grantUserRolePermissions($viewer, ['image-delete-all']);
|
||||
|
||||
$resp = $this->actingAs($viewer)->get("/images/edit/{$image->id}");
|
||||
$this->withHtml($resp)->assertElementExists('button#image-manager-delete[title="Delete"]');
|
||||
$this->withHtml($resp)->assertElementExists('button#image-manager-delete');
|
||||
|
||||
$this->files->deleteAtRelativePath($relPath);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user