Merge branch 'develop' into trunk
This commit is contained in:
commit
7219701f4b
9 changed files with 265 additions and 46 deletions
|
|
@ -98,7 +98,8 @@ class UrlController extends Controller
|
|||
'%s.%s',
|
||||
Str::uuid(),
|
||||
new \SplFileInfo(asset($url->logo))->getExtension()
|
||||
)) :
|
||||
)
|
||||
) :
|
||||
$url->logo;
|
||||
|
||||
$url->saveOrFail();
|
||||
|
|
@ -156,16 +157,23 @@ class UrlController extends Controller
|
|||
{
|
||||
return DB::transaction(function () use ($request, $url) {
|
||||
$oldUrl = $url->value;
|
||||
$url->fill($request->validated());
|
||||
$data = $request->validated();
|
||||
|
||||
// Reassign the logo in image settings to save only the path.
|
||||
// Do it after the first saveOrFail to get the real logo path, not tmp.
|
||||
$settings = json_decode($request->validated('image_settings'));
|
||||
|
||||
if (empty($settings->image)) {
|
||||
$url->logo = null;
|
||||
}
|
||||
|
||||
$url->fill($data);
|
||||
$url->saveOrFail();
|
||||
|
||||
if ($oldUrl !== $url->value) {
|
||||
$this->saveUrlHistory($url, $oldUrl);
|
||||
}
|
||||
|
||||
// Reassign the logo in image settings to save only the path.
|
||||
// Do it after the first saveOrFail to get the real logo path, not tmp.
|
||||
$settings = json_decode($request->validated('image_settings'));
|
||||
$settings->image = $url->logo;
|
||||
$url->image_settings = $settings;
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class SecureHeaders
|
|||
"default-src 'none';",
|
||||
"manifest-src 'self';",
|
||||
// * CKEDITOR Specific https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/csp.html
|
||||
"connect-src 'self' blob: " . implode(' ', [
|
||||
"connect-src 'self' blob: data: " . implode(' ', [
|
||||
// * VITE DEV
|
||||
config('app.debug') ? "wss://{$host}:4443" : '',
|
||||
config('app.debug') ? "https://{$host}:4443" : '',
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Http\Requests\Bo\Urls;
|
||||
|
||||
use App\Enums\Users\RoleEnum;
|
||||
use App\Traits\Requests\HasPictureWithSvgSupport;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
|
@ -11,6 +12,7 @@ use Uwil\LaravelCommon\Traits\Requests\HasPicture;
|
|||
class StoreUrlRequest extends FormRequest
|
||||
{
|
||||
use HasPicture;
|
||||
use HasPictureWithSvgSupport;
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
|
|
@ -58,7 +60,7 @@ class StoreUrlRequest extends FormRequest
|
|||
|
||||
return \array_merge(
|
||||
$rules,
|
||||
$this->pictureRules('logo', 100, 100, 500, 500, true),
|
||||
$this->pictureRulesWithSvgSupport('logo', 100, 100, 500, 500, true),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class UpdateUrlRequest extends StoreUrlRequest
|
|||
|
||||
return \array_merge(
|
||||
$rules,
|
||||
$this->pictureRules('logo', 100, 100, 500, 500, true),
|
||||
$this->pictureRulesWithSvgSupport('logo', 100, 100, 500, 500, true),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
175
app/Traits/Requests/HasPictureWithSvgSupport.php
Normal file
175
app/Traits/Requests/HasPictureWithSvgSupport.php
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
<?php
|
||||
|
||||
namespace App\Traits\Requests;
|
||||
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
|
||||
trait HasPictureWithSvgSupport
|
||||
{
|
||||
/**
|
||||
* Minimum width
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
private $minWidth = 400;
|
||||
|
||||
/**
|
||||
* Maximum width
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
private $maxWidth = 1800;
|
||||
|
||||
/**
|
||||
* Minimum height
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
private $minHeight = 300;
|
||||
|
||||
/**
|
||||
* Maximum height
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
private $maxHeight = 1800;
|
||||
|
||||
|
||||
/**
|
||||
* Image rules
|
||||
*
|
||||
* @param string $name
|
||||
* @param integer|null $minWidth
|
||||
* @param integer|null $minHeight
|
||||
* @param integer|null $maxWidth
|
||||
* @param integer|null $maxHeight
|
||||
* @param boolean $nullable
|
||||
* @param integer|null $size
|
||||
* @param string|null $requiredWith
|
||||
* @param string|null $requiredWithout
|
||||
* @return array
|
||||
*/
|
||||
public function pictureRulesWithSvgSupport(
|
||||
string $name = 'picture',
|
||||
?int $minWidth = null,
|
||||
?int $minHeight = null,
|
||||
?int $maxWidth = null,
|
||||
?int $maxHeight = null,
|
||||
bool $nullable = false,
|
||||
?int $size = null,
|
||||
?string $requiredWith = null,
|
||||
?string $requiredWithout = null,
|
||||
): array {
|
||||
$dimensions = (object)[
|
||||
'minWidth' => $minWidth ?? $this->minWidth,
|
||||
'minHeight' => $minHeight ?? $this->minHeight,
|
||||
'maxWidth' => $maxWidth ?? $this->maxWidth,
|
||||
'maxHeight' => $maxHeight ?? $this->maxHeight
|
||||
];
|
||||
return [
|
||||
$name => [
|
||||
!$nullable ? 'required' : 'nullable',
|
||||
(strlen($requiredWith)) ? 'required_with:' . $requiredWith : '',
|
||||
(strlen($requiredWithout)) ? 'required_without:' . $requiredWithout : '',
|
||||
($size) ? 'max:' . $size : '',
|
||||
function (string $attribute, $value, callable $fail) use ($dimensions) {
|
||||
self::validatePicture($attribute, $value, $fail, $dimensions);
|
||||
}
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate picture
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param string|null $value
|
||||
* @param callable $fail
|
||||
* @param object $sizes
|
||||
* @return void
|
||||
*/
|
||||
public static function validatePicture(string $attribute, string|null $value, callable $fail, object $sizes): void
|
||||
{
|
||||
if (!($useStorage = Storage::exists($value)) && !File::exists($value)) {
|
||||
$fail(trans(':attribute n\'existe pas dans le système de fichier', [
|
||||
'attribute' => $attribute
|
||||
]));
|
||||
return;
|
||||
}
|
||||
$fileFullPath = self::getUploadedFileFullPath($value, $useStorage);
|
||||
if (
|
||||
!\in_array(self::getUploadedFileMimeType($value, $useStorage), [
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/svg+xml'
|
||||
])
|
||||
) {
|
||||
$fail(trans(':attribute doit être une image de type \'image/jpeg,image/jpg,image/png,image/gif\',image/svg\'', [
|
||||
'attribute' => $attribute
|
||||
]));
|
||||
return;
|
||||
}
|
||||
|
||||
if (self::getUploadedFileMimeType($value, $useStorage) !== "image/svg+xml") {
|
||||
if (!($dimensions = \getimagesize($fileFullPath))) {
|
||||
$fail(trans(':attribute impossible de valider les dimensions', [
|
||||
'attribute' => $attribute
|
||||
]));
|
||||
} else {
|
||||
$width = intval($dimensions[0]);
|
||||
$height = intval($dimensions[1]);
|
||||
|
||||
$xMessage = trans(
|
||||
":attribute la largeur doit être comprise entre {$sizes->minWidth}px et {$sizes->maxWidth}px"
|
||||
);
|
||||
if ($sizes->minWidth === $sizes->maxWidth) {
|
||||
$xMessage = trans(":attribute la largeur doit être de {$sizes->minWidth}px");
|
||||
}
|
||||
$yMessage = trans(
|
||||
":attribute la hauteur doit être comprise entre {$sizes->minHeight}px et {$sizes->maxHeight}px"
|
||||
);
|
||||
if ($sizes->minHeight === $sizes->maxHeight) {
|
||||
$yMessage = trans(":attribute la hauteur doit être de {$sizes->minHeight}px");
|
||||
}
|
||||
|
||||
if (($width < $sizes->minWidth) || ($width > $sizes->maxWidth)) {
|
||||
$fail($xMessage);
|
||||
}
|
||||
if (($height < $sizes->minHeight) || ($height > $sizes->maxHeight)) {
|
||||
$fail($yMessage);
|
||||
}
|
||||
} //end if
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file full path
|
||||
*
|
||||
* @param string $value
|
||||
* @param boolean $useStorage
|
||||
* @return string
|
||||
*/
|
||||
private static function getUploadedFileFullPath(string $value, bool $useStorage): string
|
||||
{
|
||||
/** @var \Illuminate\Filesystem\FilesystemAdapter */
|
||||
$disk = Storage::disk();
|
||||
return !$useStorage ? (realpath($value) ?: $value) : $disk->path($value);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the file mimetype
|
||||
*
|
||||
* @param string $value
|
||||
* @param boolean $useStorage
|
||||
* @return string|false
|
||||
*/
|
||||
private static function getUploadedFileMimeType(string $value, bool $useStorage)
|
||||
{
|
||||
return !$useStorage ? File::mimeType($value) : Storage::mimeType($value);
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ return [
|
|||
'validate' => 'Validate',
|
||||
'preview' => 'Preview',
|
||||
'remove_picture' => 'Remove picture',
|
||||
'svg_not_croppable' => 'SVGs do not require cropping'
|
||||
],
|
||||
'WordCounterComponent' => [
|
||||
'words' => 'Words',
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ return [
|
|||
'validate' => 'Valider',
|
||||
'preview' => 'Prévisualisation',
|
||||
'remove_picture' => 'Supprimer l\'image',
|
||||
'svg_not_croppable' => 'Les SVG ne nécessitent pas de recadrage'
|
||||
],
|
||||
'WordCounterComponent' => [
|
||||
'words' => 'Mots',
|
||||
|
|
|
|||
|
|
@ -32,15 +32,15 @@
|
|||
type="file"
|
||||
class="d-none"
|
||||
:name="parameters.name"
|
||||
accept=".jpg,.jpeg,.gif,.png"
|
||||
accept=".jpg,.jpeg,.gif,.png,.svg"
|
||||
:aria-describedby="`Help${parameters.id}`"
|
||||
>
|
||||
<input
|
||||
v-if="typeof fileOutput === 'string'"
|
||||
v-if="typeof fileOutput === 'string' || fileOutput === null"
|
||||
class="d-none"
|
||||
:name="parameters.name"
|
||||
type="text"
|
||||
:value="fileOutput"
|
||||
:value="typeof fileOutput === 'string' ? fileOutput : ''"
|
||||
>
|
||||
<!-- END OUTPUT HTML INPUT -->
|
||||
<!-- FILENAME DISPLAY -->
|
||||
|
|
@ -77,22 +77,24 @@
|
|||
>
|
||||
<FontAwesomeIcon icon="fa-solid fa-eye" />
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
@click.prevent="prepareImageEditor()"
|
||||
:disabled="!hasPicture"
|
||||
:class="`btn ${!hasCroppedPicture ? 'btn-primary' : 'btn-success'}`"
|
||||
type="button"
|
||||
data-bs-toggle="modal"
|
||||
:data-bs-target="`#Modal${parameters.id}`"
|
||||
</button>
|
||||
<span
|
||||
data-bs-toggle="tooltip"
|
||||
:title="isCurrentFileSvg
|
||||
? trans.methods.__('vue.ImageInputComponent.svg_not_croppable')
|
||||
: trans.methods.__('vue.ImageInputComponent.resize_source_picture')"
|
||||
>
|
||||
<span
|
||||
data-bs-toggle="tooltip"
|
||||
:title="trans.methods.__('vue.ImageInputComponent.resize_source_picture')"
|
||||
<button
|
||||
@click.prevent="prepareImageEditor()"
|
||||
:disabled="!hasPicture || isCurrentFileSvg"
|
||||
:class="`btn ${!hasCroppedPicture ? 'btn-primary' : 'btn-success'}`"
|
||||
type="button"
|
||||
data-bs-toggle="modal"
|
||||
:data-bs-target="`#Modal${parameters.id}`"
|
||||
>
|
||||
<FontAwesomeIcon icon="fa-solid fa-crop" />
|
||||
</span>
|
||||
</button>
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
v-if="hasCroppedPicture"
|
||||
class="input-group-text text-success"
|
||||
|
|
@ -427,6 +429,7 @@ import Cropper from "cropperjs";
|
|||
import { computed, defineOptions, nextTick, onMounted, reactive, ref, useAttrs, watch } from "vue";
|
||||
import { Tooltips } from "./../../modules/bsTooltips";
|
||||
import trans from "./../../modules/trans";
|
||||
import { Tooltip } from "bootstrap";
|
||||
|
||||
/** If string the it is an absolute file path. */
|
||||
type FileOutputValue = File|string|null;
|
||||
|
|
@ -555,6 +558,14 @@ const parameters = reactive<{
|
|||
},
|
||||
});
|
||||
|
||||
const isCurrentFileSvg = ref<boolean>(false);
|
||||
|
||||
/** Detect SVG by mime type or extension */
|
||||
function isSvgFile(file: File): boolean {
|
||||
return (
|
||||
file.type === "image/svg+xml" || file.name.toLowerCase().endsWith(".svg")
|
||||
);
|
||||
}
|
||||
/** Internal file we are working on. */
|
||||
const fileParameters = reactive<{
|
||||
file: File|null,
|
||||
|
|
@ -666,7 +677,10 @@ watch(previousPropValue, () => {
|
|||
}
|
||||
cropperReset();
|
||||
hasCroppedPicture.value = false;
|
||||
fetchPictureFromPath(fileParameters.filePath)!.then((file) => useThis(file));
|
||||
const fetchPromise = fetchPictureFromPath(fileParameters.filePath);
|
||||
if (fetchPromise) {
|
||||
fetchPromise.then((file) => useThis(file));
|
||||
}
|
||||
});
|
||||
/** Crop parameters changes. */
|
||||
watch(cropperParameters, (newValue: CropperParameters) => {
|
||||
|
|
@ -700,6 +714,11 @@ watch(fileDataUrl, (newValue) => {
|
|||
}
|
||||
fileParameters.dimensions = {...parameters.dimensions};
|
||||
});
|
||||
watch(isCurrentFileSvg, () => {
|
||||
nextTick(() => {
|
||||
tooltips.value?.refreshTooltips();
|
||||
});
|
||||
});
|
||||
|
||||
// * METHODS
|
||||
function assignFromProps(): void {
|
||||
|
|
@ -777,10 +796,12 @@ function chooseAFile(): void {
|
|||
/** Input file has changed. */
|
||||
function pictureInputHasChanged(): void {
|
||||
if (inputFileImage?.value?.files && inputFileImage!.value!.files[0]) {
|
||||
const file = inputFileImage.value.files[0];
|
||||
cropperReset();
|
||||
hasCroppedPicture.value = false;
|
||||
useThis(inputFileImage!.value!.files[0]);
|
||||
fileOutput.value = inputFileImage!.value!.files[0];
|
||||
isCurrentFileSvg.value = isSvgFile(file);
|
||||
useThis(file);
|
||||
fileOutput.value = file;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -793,17 +814,19 @@ function fetchPictureFromPath(filePath: string): Promise<File>|void {
|
|||
const prefixedFilePath: string = filePath.replace(/^\/|^/, "/"),
|
||||
stringFileName: string = prefixedFilePath.substring(prefixedFilePath.lastIndexOf("/")).replace(/^\/|^/, "");
|
||||
|
||||
fetch(prefixedFilePath)
|
||||
.then((response) => response.blob())
|
||||
.then((blob: Blob) => {
|
||||
const file = new File([blob], stringFileName, {
|
||||
type: "image/png",
|
||||
lastModified: new Date().getTime(),
|
||||
});
|
||||
setFileAsDataUrl(file);
|
||||
accept(file);
|
||||
}).catch(() => deny("Failed to fetch picture"));
|
||||
}));
|
||||
fetch(prefixedFilePath)
|
||||
.then((response) => response.blob())
|
||||
.then((blob: Blob) => {
|
||||
const isSvg = stringFileName.toLowerCase().endsWith(".svg");
|
||||
isCurrentFileSvg.value = isSvg;
|
||||
const file = new File([blob], stringFileName, {
|
||||
type: isSvg ? "image/svg+xml" : blob.type || "image/png",
|
||||
lastModified: new Date().getTime(),
|
||||
});
|
||||
setFileAsDataUrl(file);
|
||||
accept(file);
|
||||
}).catch(() => deny("Failed to fetch picture"));
|
||||
}));
|
||||
}
|
||||
|
||||
/** Persist a File added from input or while component init. */
|
||||
|
|
@ -868,6 +891,7 @@ function resetComponent(): void {
|
|||
fileDataUrl.value = null;
|
||||
fileOutput.value = null;
|
||||
inputFileImage.value!.value = "";
|
||||
isCurrentFileSvg.value = false;
|
||||
fileParameters.fileName = trans.methods.__("vue.ImageInputComponent.choose_a_file");
|
||||
fileParameters.filePath = "";
|
||||
fileParameters.file = null;
|
||||
|
|
|
|||
|
|
@ -366,15 +366,14 @@
|
|||
|
||||
<template v-if="imageLoaded">
|
||||
<ImageInputComponent
|
||||
v-model="options.image"
|
||||
:id="'logo'"
|
||||
:name="'logo'"
|
||||
:helper="'L\'image qui va servir de vignette au produit, doit être de 500px (largeur) par 500px (hauteur) maximum'"
|
||||
:width="100"
|
||||
:height="100"
|
||||
:required="false"
|
||||
:value="options.image"
|
||||
@image-file="updatePicture()"
|
||||
:value="logoStoredPath"
|
||||
@image-file="updatePicture"
|
||||
/>
|
||||
</template>
|
||||
<input
|
||||
|
|
@ -428,8 +427,10 @@ export default defineComponent({
|
|||
this.options = data.settings;
|
||||
}
|
||||
if(data.logo !== "") {
|
||||
this.logoStoredPath = data.logo;
|
||||
this.options.image = "/" + data.logo;
|
||||
} else {
|
||||
this.logoStoredPath = "";
|
||||
this.options.image = "";
|
||||
}
|
||||
}
|
||||
|
|
@ -444,15 +445,20 @@ export default defineComponent({
|
|||
},
|
||||
methods: {
|
||||
download() {
|
||||
this.qrCode.update(this.options);
|
||||
this.qrCode.download({ extension: this.extension as FileExtension });
|
||||
},
|
||||
updatePicture() {
|
||||
const el = document.getElementById("PreviewHelplogo")?.parentElement?.children[1] as HTMLElement;
|
||||
if (el instanceof HTMLElement) {
|
||||
this.options.image = el.getAttribute("src") as string;
|
||||
updatePicture(file: File | null) {
|
||||
if (!file) {
|
||||
this.options.image = "";
|
||||
this.qrCode.update(this.options);
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
this.options.image = String(reader.result);
|
||||
this.qrCode.update(this.options);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
},
|
||||
changeColor(color: string) {
|
||||
if (!this.isTransparent) {
|
||||
|
|
@ -483,6 +489,7 @@ export default defineComponent({
|
|||
urlInput: HTMLInputElement,
|
||||
urlInputValue: string,
|
||||
imageLoaded: boolean,
|
||||
logoStoredPath: string,
|
||||
options: {
|
||||
type: DrawType;
|
||||
width: number;
|
||||
|
|
@ -528,6 +535,7 @@ export default defineComponent({
|
|||
urlInput: document.getElementById(this.urlInputId) as HTMLInputElement,
|
||||
urlInputValue: "",
|
||||
imageLoaded: false,
|
||||
logoStoredPath: "",
|
||||
options: {
|
||||
width: 300,
|
||||
height: 300,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue