diff --git a/app/Api/ApiDocsController.php b/app/Api/ApiDocsController.php index 020c8902f..382ec15eb 100644 --- a/app/Api/ApiDocsController.php +++ b/app/Api/ApiDocsController.php @@ -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'); + } } diff --git a/app/Uploads/Controllers/GalleryImageController.php b/app/Uploads/Controllers/GalleryImageController.php index fb74d6203..33d3dd74c 100644 --- a/app/Uploads/Controllers/GalleryImageController.php +++ b/app/Uploads/Controllers/GalleryImageController.php @@ -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'], diff --git a/app/Uploads/Controllers/ImageController.php b/app/Uploads/Controllers/ImageController.php index fea0713a2..2c611c515 100644 --- a/app/Uploads/Controllers/ImageController.php +++ b/app/Uploads/Controllers/ImageController.php @@ -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. * diff --git a/app/Uploads/Controllers/ImageGalleryApiController.php b/app/Uploads/Controllers/ImageGalleryApiController.php index 1614b6445..4fca6a4dd 100644 --- a/app/Uploads/Controllers/ImageGalleryApiController.php +++ b/app/Uploads/Controllers/ImageGalleryApiController.php @@ -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)); } diff --git a/app/Uploads/ImageRepo.php b/app/Uploads/ImageRepo.php index 2d35d96ff..cdd5485ac 100644 --- a/app/Uploads/ImageRepo.php +++ b/app/Uploads/ImageRepo.php @@ -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; } diff --git a/app/Uploads/ImageService.php b/app/Uploads/ImageService.php index 5458779e9..66596a57f 100644 --- a/app/Uploads/ImageService.php +++ b/app/Uploads/ImageService.php @@ -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); diff --git a/lang/en/common.php b/lang/en/common.php index c74dcc907..de7937b2b 100644 --- a/lang/en/common.php +++ b/lang/en/common.php @@ -6,6 +6,7 @@ return [ // Buttons 'cancel' => 'Cancel', + 'close' => 'Close', 'confirm' => 'Confirm', 'back' => 'Back', 'save' => 'Save', diff --git a/lang/en/components.php b/lang/en/components.php index cd5dca251..8a105096b 100644 --- a/lang/en/components.php +++ b/lang/en/components.php @@ -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', diff --git a/lang/en/errors.php b/lang/en/errors.php index 6991f96e4..b03fb8c35 100644 --- a/lang/en/errors.php +++ b/lang/en/errors.php @@ -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 diff --git a/resources/js/components/dropzone.js b/resources/js/components/dropzone.js index 2b8b35081..1cac09b4a 100644 --- a/resources/js/components/dropzone.js +++ b/resources/js/components/dropzone.js @@ -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, { diff --git a/resources/js/components/image-manager.js b/resources/js/components/image-manager.js index b81782364..78abcf30d 100644 --- a/resources/js/components/image-manager.js +++ b/resources/js/components/image-manager.js @@ -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'); + } + } diff --git a/resources/js/components/tabs.js b/resources/js/components/tabs.js index 560dc6273..f0fc058ce 100644 --- a/resources/js/components/tabs.js +++ b/resources/js/components/tabs.js @@ -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); + } + } diff --git a/resources/sass/_animations.scss b/resources/sass/_animations.scss index eb9f4e767..f1aa3139b 100644 --- a/resources/sass/_animations.scss +++ b/resources/sass/_animations.scss @@ -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; } diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss index 5ba1286c0..1521e6eaa 100644 --- a/resources/sass/_components.scss +++ b/resources/sass/_components.scss @@ -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; diff --git a/resources/sass/_forms.scss b/resources/sass/_forms.scss index 37f8f1bfc..5276bb566 100644 --- a/resources/sass/_forms.scss +++ b/resources/sass/_forms.scss @@ -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; diff --git a/resources/sass/_layout.scss b/resources/sass/_layout.scss index 541978a65..11889da17 100644 --- a/resources/sass/_layout.scss +++ b/resources/sass/_layout.scss @@ -298,6 +298,10 @@ body.flexbox { } } +[hidden] { + display: none !important; +} + /** * Border radiuses */ diff --git a/resources/sass/_lists.scss b/resources/sass/_lists.scss index 33e500d6a..ad0803e71 100644 --- a/resources/sass/_lists.scss +++ b/resources/sass/_lists.scss @@ -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; } diff --git a/resources/sass/styles.scss b/resources/sass/styles.scss index 2ed680646..9a8e5b36d 100644 --- a/resources/sass/styles.scss +++ b/resources/sass/styles.scss @@ -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); diff --git a/resources/views/pages/parts/image-manager-form.blade.php b/resources/views/pages/parts/image-manager-form.blade.php index aa21e31bb..75750ef2f 100644 --- a/resources/views/pages/parts/image-manager-form.blade.php +++ b/resources/views/pages/parts/image-manager-form.blade.php @@ -1,4 +1,14 @@ -
+
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"> + +
{{ trans('components.image_image_name') }}
-
-
- @if(userCan('image-delete', $image)) - - @endif -
-
+
+ @if(userCan('image-delete', $image) || userCan('image-update', $image)) + + @endif -
@if(!is_null($dependantPages)) +
@if(count($dependantPages) > 0)

{{ trans('components.image_delete_used') }}

\ No newline at end of file diff --git a/resources/views/pages/parts/image-manager-list.blade.php b/resources/views/pages/parts/image-manager-list.blade.php index ccf79fb6d..7e660c747 100644 --- a/resources/views/pages/parts/image-manager-list.blade.php +++ b/resources/views/pages/parts/image-manager-list.blade.php @@ -1,23 +1,26 @@ @foreach($images as $index => $image)
- +
@endforeach +@if(count($images) === 0) +

{{ trans('common.no_items') }}

+@endif @if($hasMore)
diff --git a/resources/views/pages/parts/image-manager.blade.php b/resources/views/pages/parts/image-manager.blade.php index da64d681c..3050f5c5b 100644 --- a/resources/views/pages/parts/image-manager.blade.php +++ b/resources/views/pages/parts/image-manager.blade.php @@ -21,60 +21,98 @@ @icon('upload') {{ trans('components.image_upload') }} - +
-
- -
-
- - - -
-
- -
-
-
- -
- -
-
-
- -
-

{{ trans('components.image_intro') }}

-

{{ trans('components.image_intro_upload') }}

-
- -
+ aria-controls="image-manager-list" + role="tab">{{ trans('components.image_list') }} +
+
+
+
+
+
+ +
+
+
+ + + +
+
+
+
+ +
+
+ +
+
+
+ +
+

{{ trans('components.image_intro') }}

+

{{ trans('components.image_intro_upload') }}

+
+ +
+
+
+