Bump with base project last

This commit is contained in:
Jérémy Munsch 2024-02-09 15:20:52 +01:00
parent 493a76af2d
commit 4792caeabe
No known key found for this signature in database
GPG key ID: 18361AF3BFEC0FEC
24 changed files with 927 additions and 538 deletions

View file

@ -143,8 +143,11 @@ class UserController extends Controller
*/
public function destroy(User $user): \Illuminate\Http\RedirectResponse
{
/** @var array<string,string> */
$previousQueries = [];
\parse_str(\parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries);
if ($user->deleteOrFail()) {
return redirect()->route('bo.users.index')
return redirect()->route('bo.users.index', $previousQueries)
->with('success', __('crud.messages.has_been_deleted', [
'model' => Str::ucfirst(__('models.classes.user'))
]));

View file

@ -480,7 +480,7 @@ class ToolboxHelper
$xMessage = trans(":attribute la largeur doit être de {$sizes->minWidth}px");
}
$yMessage = trans(
":attribute la hauteur doit être comprise entre {$sizes->minWidth}px et {$sizes->maxWidth}px"
":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");

View file

@ -6,7 +6,7 @@ use App\Enums\ActivityLogs\ActivityLogsEventEnum;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
/**
* @property string $model Target model.
@ -98,22 +98,67 @@ class ActivityLog extends Model
{
$modelClassName = $activity->model_class;
/** Get type of each fields of the target model */
/** @var \Illuminate\Database\Eloquent\Model|null $targetModel */
$targetModel = $activity->model_class::where(
(new $modelClassName())->getRouteKeyName(),
$activity->model_id
)->first();
if ($targetModel != null) {
// phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
if (\is_null($targetModel)) {
return [];
}
$targetModelTypes = collect($targetModel->toArray())
->map(function ($field, $fieldIndex) use ($targetModel) {
return Schema::getColumnType($targetModel->getTable(), $fieldIndex);
})->toArray();
// phpcs:enable
->map(fn ($value) => self::getValueType($value))
->toArray();
/** Return old, new and type of values changed */
return ($eventEnum === ActivityLogsEventEnum::updated) ?
array_intersect_key($targetModelTypes, $model->getChanges()) : [];
}
return [];
/**
* Get a value type as a string
* ! Use this for display purpose only
*
* @param mixed $value
* @return string
* @throws \RuntimeException If type is unhlanded.
* @phpcs:disable Generic.Metrics.CyclomaticComplexity.MaxExceeded
*/
private static function getValueType(mixed $value): string
{
// phpcs:enable
$type = \gettype($value);
switch ($type) {
// phpcs:ignore PSR2.ControlStructures.SwitchDeclaration.TerminatingComment
case 'string':
return match (true) {
Str::length(\strip_tags($value)) !== Str::length($value) => 'html',
Str::startsWith($value, 'storage/modelfiles') => 'file',
Str::isUuid($value) => 'uuid',
Str::isUlid($value) => 'ulid',
Str::isUrl($value) => 'url',
Str::isMatch('/^[0-9A-Za-z_-]{10}[048AEIMQUYcgkosw]$/', $value) => 'youtube_video_id',
\is_numeric($value) => 'numeric',
Str::isJson($value) and
(Str::startsWith($value, '[') or Str::startsWith($value, '{')) => 'json',
default => 'string'
};
case 'NULL':
case 'object':
case 'array':
case 'integer':
case 'boolean':
return $type;
case 'double':
return is_float($value) ? 'float' : 'double';
case 'resource (closed)':
case 'resource':
throw new \RuntimeException('Laravel model properties should not contain resources !');
case 'unknown type':
default:
throw new \RuntimeException('Unhandled Type ' . \gettype($value));
} //end switch
}
// * RELATIONS

View file

@ -0,0 +1,43 @@
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Str;
/**
* Validate a vimeo ID regex
*
* @package App\Rules
*/
class IsValidVimeoId implements ValidationRule
{
/**
* Run the validation rule.
*
* @param string $attribute
* @param mixed $value
* @param \Closure(string):\Illuminate\Translation\PotentiallyTranslatedString $fail
* @return void
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceBeforeLastUsed
* @phpcs:disable Squiz.Commenting.FunctionComment.IncorrectTypeHint
*/
public function validate(string $attribute, mixed $value, \Closure $fail): void
{
// phpcs:enable
if (!self::validates($value)) {
$fail(trans('validation.vimeo_id'));
}
}
/**
* Validate a value
*
* @param mixed $value
* @return boolean
*/
public static function validates(mixed $value): bool
{
return Str::of($value)->isMatch('/^[0-9]{8,10}$/');
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Str;
/**
* Validate a youtube ID regex
*
* @url https://webapps.stackexchange.com/questions/54443/format-for-id-of-youtube-video
* @package App\Rules
*/
class IsValidYoutubeId implements ValidationRule
{
/**
* Run the validation rule.
*
* @param string $attribute
* @param mixed $value
* @param \Closure(string):\Illuminate\Translation\PotentiallyTranslatedString $fail
* @return void
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceBeforeLastUsed
* @phpcs:disable Squiz.Commenting.FunctionComment.IncorrectTypeHint
*/
public function validate(string $attribute, mixed $value, \Closure $fail): void
{
// phpcs:enable
if (!self::validates($value)) {
$fail(trans('validation.youtube_id'));
}
}
/**
* Validate a value
*
* @param mixed $value
* @return boolean
*/
public static function validates(mixed $value): bool
{
return Str::of($value)->isMatch('/^[0-9A-Za-z_-]{10}[048AEIMQUYcgkosw]$/');
}
}

View file

@ -3,7 +3,7 @@
namespace App\Traits\Controllers;
use App\Http\Requests\Bo\UpdateChangeOrderRequest;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
@ -14,22 +14,31 @@ trait ChangesModelOrder
* Change order.
*
* @param \App\Http\Requests\Bo\UpdateChangeOrderRequest $request
* @param \Illuminate\Database\Eloquent\Model $model
* @param mixed ...$params
* @return \Illuminate\Http\RedirectResponse
* @throws \RuntimeException Si le nom du parametre de la route ne correspond pas au model utilisé.
* @throws \RuntimeException Si le nom du parametre de la route ne correspond pas au params utilisé.
*/
public function changeOrder(UpdateChangeOrderRequest $request, Model $model): \Illuminate\Http\RedirectResponse
public function changeOrder(UpdateChangeOrderRequest $request, mixed ...$params): \Illuminate\Http\RedirectResponse
{
$params = collect(request()->route()->parameterNames);
$className = Str::ucfirst(Str::lower($params->get($params->count() - 2)));
$className = "\\App\\Models\\$className";
$paramNames = collect(request()->route()->parameterNames);
$className = Str::of($paramNames->get($paramNames->count() - 2))->lower()->studly();
$className = "\\App\\Models\\{$className}";
$model = Arr::get($params, $paramNames->count() - 2);
if (!class_exists($className)) {
throw new \RuntimeException("The $className class doesn't exist.");
throw new \RuntimeException("The {$className} class doesn't exist.");
}
if (!\is_object($model)) {
throw new \RuntimeException("The second last param isn't an object.");
}
if (get_class($model) !== (string)Str::of($className)->replaceStart('\\', '')) {
throw new \RuntimeException("The {$className} doesn't match second last binded param.");
}
/** @var \Illuminate\Database\Eloquent\Model|null */
$model = $className::where($model->getRouteKeyName(), $model->getRouteKey())->first();
$model = $className::query()->where($model->getRouteKeyName(), $model->getRouteKey())->first();
if (!$model) {
return back()->with('error', trans('crud.messages.order_not_changed'));

View file

@ -89,7 +89,7 @@ trait BaseEnum
/**
* Get enums.
*
* @return array
* @return array<self>
*/
public static function toArray(): array
{

View file

@ -89,6 +89,9 @@ trait HasPicture
* @param integer|null $minHeight
* @param integer|null $maxWidth
* @param integer|null $maxHeight
* @param integer $limitMin
* @param integer $limitMax
* @param boolean $nullable
* @return array
*/
public function picturesRules(
@ -96,16 +99,19 @@ trait HasPicture
?int $minWidth = null,
?int $minHeight = null,
?int $maxWidth = null,
?int $maxHeight = null
?int $maxHeight = null,
int $limitMin = 1,
int $limitMax = 5,
bool $nullable = false
): array {
$sizes = (object)[
'minWidth' => $minWidth ?? $this->minWidth,
'minHeight' => $minHeight ?? $this->minHeight,
'maxWidth' => $maxWidth ?? $this->maxWidth,
'maxHeight' => $maxHeight ?? $this->maxHeight
'maxHeight' => $maxHeight ?? $this->maxHeight,
];
return [
$name => 'sometimes|array',
$name => [!$nullable ? 'required' : 'nullable', 'array', "between:{$limitMin},{$limitMax}"],
"{$name}Validate" => "required_with:{$name}|array",
"{$name}Validate.*" => [
'required',

View file

@ -0,0 +1,30 @@
<?php
use Database\Seeders\MandatorySeeder;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Artisan;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Artisan::call('db:seed', [
'class' => MandatorySeeder::class
]);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
// Do nothing.
}
};

View file

@ -14,8 +14,8 @@ class DatabaseSeeder extends Seeder
public function run()
{
$this->call([
UserSeeder::class,
StaticPageSeeder::class
MandatorySeeder::class,
DevelopmentSeeder::class,
]);
}
}

View file

@ -1,6 +1,6 @@
<?php
namespace Database\Seeders;
namespace Database\Seeders\Development;
use App\Models\User;
use Illuminate\Database\Seeder;

View file

@ -0,0 +1,21 @@
<?php
namespace Database\Seeders;
use Database\Seeders\Development\UserSeeder;
use Illuminate\Database\Seeder;
class DevelopmentSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run(): void
{
$this->call([
UserSeeder::class
]);
}
}

View file

@ -1,6 +1,6 @@
<?php
namespace Database\Seeders;
namespace Database\Seeders\Mandatory;
use App\Enums\StaticPageType;
use App\Models\StaticPage;

View file

@ -0,0 +1,21 @@
<?php
namespace Database\Seeders;
use Database\Seeders\Mandatory\StaticPageSeeder;
use Illuminate\Database\Seeder;
class MandatorySeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run(): void
{
$this->call([
StaticPageSeeder::class
]);
}
}

938
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -27,7 +27,7 @@
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.36.2",
"@vitejs/plugin-basic-ssl": "^1.0.1",
"@vitejs/plugin-vue": "latest",
"@vitejs/plugin-vue": "^4.6.2",
"@vue/compiler-sfc": "latest",
"@vue/eslint-config-typescript": "latest",
"@vue/tsconfig": "^0.4.0",

View file

@ -214,6 +214,10 @@ return [
'username' => 'username',
'year' => 'year',
],
'youtube_id' => ':attribute must be a valid Youtube video ID',
'vimeo_id' => ':attribute must be a valid Vimeo video ID',
'custom' => [
'of_the' => [
'male' => 'of the ',
@ -229,5 +233,8 @@ return [
'static_page_type' => 'static page type',
'seo_title' => 'SEO title',
'seo_description' => 'SEO description',
'video_id' => 'ID of the video',
'youtube_video_id' => 'ID od the youtube video',
'vimeo_video_id' => 'ID of the vimeo video',
],
];

View file

@ -55,7 +55,7 @@ return [
'minicrud' => [
'visibility_label' => 'Visibilité',
'visibility_helper' => 'Vérifier qu\'il est visible.',
'visibility_helper' => 'Cocher pour rendre visible.',
'enum_select' => 'Sélectionnez :enum dans la liste.',
'associated_enum' => 'Associé :enum',
'dimensions_must_be' => 'Doit être :widthpx (largeur) par :heightpx (hauteur).',

View file

@ -214,6 +214,10 @@ return [
'username' => 'nom d\'utilisateur',
'year' => 'année',
],
'youtube_id' => ':attribute doit être un ID de vidéo Youtube valide',
'vimeo_id' => ':attribute doit être un ID de vidéo Vimeo valide',
'custom' => [
'of_the' => [
'male' => 'du ',
@ -229,5 +233,8 @@ return [
'static_page_type' => 'type de page',
'seo_title' => 'titre SEO',
'seo_description' => 'description SEO',
'video_id' => 'ID de la vidéo',
'youtube_video_id' => 'ID de la vidéo youtube',
'vimeo_video_id' => 'ID de la vidéo viméo',
],
];

View file

@ -1,5 +1,8 @@
<template>
<div class="container image-input-vue p-0">
<div
ref="imageInput"
class="container image-input-vue p-0"
>
<div class="row">
<div :class="`col-12 ${intPreview ? 'col-md-6' : ''} form-group`">
<label
@ -400,7 +403,7 @@
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import Cropper from "cropperjs";
import { defineComponent } from "vue";
import bsTooltips from "./../../modules/bsTooltips";
import { Tooltips } from "./../../modules/bsTooltips";
import trans from "./../../modules/trans";
const UNITS = [
@ -418,7 +421,7 @@ export default defineComponent({
components: {
FontAwesomeIcon,
},
mixins: [trans, bsTooltips],
mixins: [trans],
emits: {
browseFile: (intId: string) => {
intId;
@ -431,7 +434,11 @@ export default defineComponent({
},
mounted() {
const json = String(this.$attrs.json ?? "");
this.initTooltips();
this.tooltips = Tooltips.make({
type: "dom",
elements: (this.$refs.imageInput as HTMLDivElement)
.querySelectorAll("[data-bs-toggle=\"tooltip\"]")
});
if (json.length) {
// * Init uppon Laravel Blade
@ -546,6 +553,7 @@ export default defineComponent({
};
intOriginalFileName: string;
intPreviewStyle: string;
tooltips: Tooltips | null;
} {
return {
intId: "",
@ -575,6 +583,7 @@ export default defineComponent({
},
intOriginalFileName: "",
intPreviewStyle: "",
tooltips: null
};
},
computed: {
@ -618,6 +627,7 @@ export default defineComponent({
if (this.intBrowseEvent) {
this.intHasImage = true;
}
this.tooltips?.refreshTooltips();
},
value: {
deep: true,

View file

@ -1,26 +1,36 @@
<template>
<button
ref="passwordVisibility"
@click.prevent="toggleVisibility()"
@keydown="keydown"
:class="`btn btn-primary rounded-0 rounded-end password-visibility-${inputId} h-100`"
:aria-pressed="visible"
>
<span
class="text-bg-primary reveal d-flex justify-content-center align-items-center h-100"
:class="{
'text-bg-primary reveal d-flex justify-content-center align-items-center h-100': true,
'd-none': visible
}"
type="button"
:title="visible ? __('vue.PasswordVisibility.hide') : __('vue.PasswordVisibility.show')"
data-bs="tooltip"
:title="__('vue.PasswordVisibility.hide')"
data-bs-toggle="tooltip"
>
<FontAwesomeIcon
v-if="visible"
icon="fas fa-eye-slash"
:aria-hidden="true"
/>
</span>
<span
ref="passwordVisibilitySpan"
:class="{
'text-bg-primary reveal d-flex justify-content-center align-items-center h-100': true,
'd-none': !visible
}"
type="button"
:title="__('vue.PasswordVisibility.show')"
data-bs-toggle="tooltip"
>
<FontAwesomeIcon
v-else
icon="fas fa-eye"
:aria-hidden="true"
/>
@ -30,8 +40,8 @@
<script lang="ts">
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { Tooltip } from "bootstrap";
import { defineComponent } from "vue";
import { Tooltips } from "./../../modules/bsTooltips";
import trans from "./../../modules/trans";
export default defineComponent({
@ -42,13 +52,13 @@ export default defineComponent({
inputId: string;
input: HTMLInputElement;
visible: boolean;
tooltipList: HTMLButtonElement[];
tooltips: Tooltips|null;
} {
return {
inputId: "",
input: document.createElement("input"),
visible: false,
tooltipList: [],
tooltips: null,
};
},
computed: {},
@ -68,7 +78,10 @@ export default defineComponent({
}
this.input = this.input as HTMLInputElement;
this.$nextTick(() => {
this.updateBootstrapTooltip();
this.tooltips = Tooltips.make({
type: "dom",
elements: (this.$refs.passwordVisibility as HTMLDivElement).querySelectorAll("[data-bs-toggle=\"tooltip\"]")
});
});
},
methods: {
@ -96,34 +109,6 @@ export default defineComponent({
this.input.type = "password";
this.visible = false;
}
this.$nextTick(() => {
this.updateBootstrapTooltip();
});
},
/**
* Update Bootstrap tooltips.
*/
updateBootstrapTooltip() {
let newTooltipList = [].slice.call(
document.querySelectorAll(
".password-visibility-" + this.inputId + " [data-bs=\"tooltip\"]"
)
) as HTMLButtonElement[];
let tmp = newTooltipList.filter((x) => !this.tooltipList.includes(x));
tmp.map((tooltip) => {
return new Tooltip(tooltip);
});
this.closeBootstrapTooltip();
this.tooltipList = newTooltipList;
},
/**
* Close all Bootstrap tooltips.
*/
closeBootstrapTooltip() {
this.tooltipList.forEach((tooltip) => {
tooltip.blur();
Tooltip.getInstance(tooltip)?.hide();
});
},
},
});

View file

@ -1,6 +1,6 @@
import axios from "axios";
import * as bootstrap from "bootstrap";
import bsTooltips from "./../../modules/bsTooltips";
import { Tooltips } from "./../../modules/bsTooltips";
/**
* We'll load the axios HTTP library which allows us to easily issue requests
@ -18,4 +18,4 @@ axios.defaults.headers.common = {
window.axios = axios;
window.bootstrap = bootstrap;
bsTooltips.methods.initTooltips();
Tooltips.make();

View file

@ -1,6 +1,6 @@
import axios from "axios";
import * as bootstrap from "bootstrap";
import bsTooltips from "./../../modules/bsTooltips";
import { Tooltips } from "./../../modules/bsTooltips";
/**
* We'll load the axios HTTP library which allows us to easily issue requests
@ -18,4 +18,4 @@ axios.defaults.headers.common = {
window.axios = axios;
window.bootstrap = bootstrap;
bsTooltips.methods.initTooltips();
Tooltips.make();

View file

@ -1,26 +1,114 @@
import type * as bootstrap from "bootstrap";
import { Tooltip } from "bootstrap";
let tooltips: Array<bootstrap.Tooltip> = [];
export default {
methods: {
initTooltips() {
// Disable tooltips on reboot.
tooltips.forEach((tooltip) => {
tooltip.disable();
tooltip.hide();
});
tooltips = [].slice
.call(document.querySelectorAll("[data-bs-toggle=\"tooltip\"]"))
.map(function (tooltipTriggerEl) {
return new window.bootstrap.Tooltip(tooltipTriggerEl, {
customClass: "large-tooltip",
type ConstructorTypeParent = "parent";
type ConstructorTypeDom = "dom";
interface ParentSelectorTooltips {
type: ConstructorTypeParent
/** The parent element that contains Dom elements for which tooltips shall be created. */
parentSelector: string;
}
interface DomProvidedTooltips {
type: ConstructorTypeDom;
/** Dom elements for which tooltips shall be created. */
elements: HTMLElement | NodeListOf<HTMLElement>;
}
interface TooltipsOptions {
/** Tooltip options. */
options?: Partial<Tooltip.Options>|null|undefined;
}
export type TooltipsConstructor = TooltipsOptions & (ParentSelectorTooltips|DomProvidedTooltips);
export class Tooltips {
/** BS tooltips instances */
private tooltips: Array<Tooltip> = [];
/** The parent element that contains Dom elements for which tooltips shall be created. */
private parentSelector: string | null = null;
/** Dom elements for which tooltips shall be created. */
private elements: Array<HTMLElement> = [];
/** Tooltip default options. */
private static defaultOptions: Partial<Tooltip.Options> = {
delay: {
show: 1250,
hide: 0,
},
};
/** Tooltip options. */
private options: Partial<Tooltip.Options> = {};
/**
* @param parentSelector
* @param options Tooltip options.
*/
constructor(params: TooltipsConstructor) {
if (params.type === "parent") {
this.parentSelectorConstructor(params);
}
if (params.type === "dom") {
this.domPrividedTooltipsContructor(params);
}
}
public static make(params: TooltipsConstructor = { type: "parent", parentSelector: "body"}
) {
const fullParams = { ...{ type: "parent", options: {} }, ...params };
return new Tooltips(fullParams);
}
private parentSelectorConstructor({ parentSelector, options }: TooltipsOptions & ParentSelectorTooltips) {
this.parentSelector = parentSelector;
this.options = { ...Tooltips.defaultOptions, ...options };
this.initTooltips();
}
private domPrividedTooltipsContructor({ elements, options }: TooltipsOptions & DomProvidedTooltips) {
if (elements instanceof HTMLElement) {
this.elements = [elements];
} else {
this.elements = Array.from(elements);
}
this.options = { ...Tooltips.defaultOptions, ...options };
this.initTooltips();
}
/**
* Close all Bootstrap tooltips and reinstanciate them.
*/
public refreshTooltips(): void {
this.closeBootstrapTooltip();
this.initTooltips();
}
/**
* Close all Bootstrap tooltips for ever.
*/
public closeBootstrapTooltip(): void {
this.tooltips.forEach((tooltip) => {
tooltip.hide();
// * Prevent pending tooltips beeing shown after we asked to hide them
tooltip.disable();
});
}
/**
* Initialize tooltips
*/
private initTooltips() {
this.closeBootstrapTooltip();
/** data-bs-toggle tooltips elements */
const elements = this.parentSelector ? [].slice
.call(document.querySelectorAll(`${this.parentSelector} [data-bs-toggle="tooltip"]`)) :
this.elements;
this.tooltips = elements.map((tooltipTriggerEl) => {
return new Tooltip(tooltipTriggerEl, this.options);
});
},
},
};
}
}