diff --git a/app/Http/Controllers/Bo/SectionController.php b/app/Http/Controllers/Bo/SectionController.php index befb2c8..987b88a 100644 --- a/app/Http/Controllers/Bo/SectionController.php +++ b/app/Http/Controllers/Bo/SectionController.php @@ -7,6 +7,7 @@ use App\Http\Controllers\Controller; use App\Http\Requests\Bo\Sections\UpdateSectionRequest; use App\Models\Section; use App\Models\StaticPage; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; @@ -88,7 +89,7 @@ class SectionController extends Controller * @param \App\Models\Section $section * @return \Illuminate\Http\RedirectResponse */ - public function update(UpdateSectionRequest $request, Section $section): \Illuminate\Http\RedirectResponse + public function update(UpdateSectionRequest $request, Section $section): RedirectResponse { return DB::transaction(function () use ($request, $section) { $section->fill(array_replace_recursive( @@ -109,4 +110,38 @@ class SectionController extends Controller ])); }); } + + /** + * Section add PartPiece + * + * @param \App\Models\Section $section + * @return \Illuminate\Http\RedirectResponse + */ + public function addPartPiece(Section $section): RedirectResponse + { + $section->addPartPiece(); + $section->saveOrFail(); + return response()->redirectToRoute('bo.sections.edit', $section) + ->with('success', trans('Partie ajouté')); + } + + /** + * Section remove PartPiece + * + * @param \App\Models\Section $section + * @param integer $index + * @return \Illuminate\Http\RedirectResponse + */ + public function removePartPiece(Section $section, int $index): RedirectResponse + { + $count = $section->removePartPiece($index); + $section->saveOrFail(); + $response = response()->redirectToRoute('bo.sections.edit', $section); + if ($count) { + $response->with('error', trans('Partie enlevé')); + } else { + $response->with('info', trans('Aucune partie enlevé')); + } + return $response; + } } diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php index eeacc88..1d32a1e 100644 --- a/app/Http/Middleware/RedirectIfAuthenticated.php +++ b/app/Http/Middleware/RedirectIfAuthenticated.php @@ -28,7 +28,7 @@ class RedirectIfAuthenticated return \redirect()->route('fo.home.show'); case 'backend': if (!$request->routeIs('bo.*')) { - continue; + continue 2; } return \redirect()->route('bo.dashboard'); default: diff --git a/app/Http/Requests/Bo/UpdateMiniCrudRequest.php b/app/Http/Requests/Bo/UpdateMiniCrudRequest.php index cc438df..025a1b9 100644 --- a/app/Http/Requests/Bo/UpdateMiniCrudRequest.php +++ b/app/Http/Requests/Bo/UpdateMiniCrudRequest.php @@ -133,15 +133,10 @@ abstract class UpdateMiniCrudRequest extends FormRequest case InputType::url->value: $rules = array_merge($rules, [ "data.$iptName.value" => [ - 'required', 'url', + $this->compileValidate('url', $input), // @phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed function ($attribute, $value, $fail) { - try { - ToolboxHelper::assertNoInternalUrls($attribute, $value); - ToolboxHelper::assertNoInternalBoUrls($attribute, $value); - } catch (ValidationException $e) { - $fail(collect($e->errors())->first()[0]); - } + $this->validateUrl($attribute, $value, $fail); } ] ]); @@ -184,18 +179,32 @@ abstract class UpdateMiniCrudRequest extends FormRequest } } - $test = \collect($inputTypeValidate)->all(); - $test = \collect($inputTypeValidate)->sort(function ($valueA, $valueB) { - $valueA = $this->ruleHasToComeFirst($valueA) ? 0 : 2; - $valueB = $this->ruleHasToComeFirst($valueB) ? 1 : 2; - return $valueA - $valueB; - })->unique()->all(); - return \collect($inputTypeValidate)->unique()->sort(function ($valueA, $valueB) { $valueA = $this->ruleHasToComeFirst($valueA) ? 0 : 2; $valueB = $this->ruleHasToComeFirst($valueB) ? 1 : 2; return $valueA - $valueB; - })->all(); + })->values()->all(); + } + + /** + * Validate URl + * + * @param string $attribute + * @param mixed $url + * @param \Closure $fail + * @return void + */ + private function validateUrl(string $attribute, mixed $url, \Closure $fail): void + { + try { + if (!is_null($url)) { + $url = is_string($url) ? $url : ''; + ToolboxHelper::assertNoInternalUrls($attribute, $url); + ToolboxHelper::assertNoInternalBoUrls($attribute, $url); + } + } catch (ValidationException $e) { + $fail(collect($e->errors())->first()[0]); + } } /** diff --git a/app/Models/Section.php b/app/Models/Section.php index 05ed052..4541cac 100644 --- a/app/Models/Section.php +++ b/app/Models/Section.php @@ -17,7 +17,7 @@ use Illuminate\Support\Str; * @property \App\Enums\Sections\PageSectionEnum $section Slider that contains the slide. * @property string $h2_title Title. * @property string $text WYSIWYG - Content. - * @property string $data Minicrud content. + * @property array $data Minicrud content. * @property-read \Carbon\Carbon $created_at Created date. * @property-read \Carbon\Carbon $updated_at Updated date. * @@ -53,7 +53,7 @@ class Section extends Model */ protected $casts = [ 'section' => PageSectionEnum::class, - 'data' => 'object', + 'data' => 'array', ]; /** @@ -138,7 +138,7 @@ class Section extends Model bool $required = false, string $name = "URL à ouvrir", ): array { - $data["bloc_link"] = [ + $data[sprintf('bloc_%s', Str::of($name)->snake()->ascii()->remove(['\'', '-']))] = [ 'label' => $name, 'inputType' => InputType::url->value, 'value' => $url, @@ -265,6 +265,160 @@ class Section extends Model return $data; } + /** + * Add part piece + * + * @return void + */ + public function addPartPiece(): void + { + $pieceTemplateLinesCount = self::countPartPieces(); + + $pieceToAdd = self::getAddablePartPieceBlock( + $this->section, + $this->countPartPieces() + 1 + ); + + $linesToSkip = 1; + $data = collect($this->data); + $groupedPieces = $data->skip($linesToSkip)->splitIn($data->count() / ($pieceTemplateLinesCount ?: 1)) + ->concat([$pieceToAdd]) + ->sort(function ($aCollect, $bCollect) { + if ($this->section->equals(PageSectionEnum::produce_rhd)) { + return strcmp( + strrev(collect(collect($aCollect)->first())->first()), + strrev(collect(collect($bCollect)->first())->first()) + ); + } + return 0; + }); + $groupedPieces->prepend([$data->keys()->first() => $data->first()]); + $this->data = $groupedPieces->collapse()->all(); + } + + /** + * Remove part piece + * + * @param integer $index + * @return integer Elements count removed. + */ + public function removePartPiece(int $index): int + { + $pieceTemplate = self::getPartPieces(); + $pieceTemplateLinesCount = self::countPartPieces(); + if (!$pieceTemplateLinesCount) { + return 0; + } + $linesToSkip = 1; + $data = collect($this->data); + $groupedPieces = $data->skip($linesToSkip)->splitIn($data->count() / count($pieceTemplate)); + $removedGroup = $groupedPieces->splice($index - 1, 1); + $this->data = $groupedPieces->collapse() + ->prepend($data->first(), $data->keys()->first()) + ->all(); + return $removedGroup->count(); + } + + /** + * Count part pieces + * + * @return integer + */ + public function countPartPieces(): int + { + $pieceTemplate = self::getPartPieces(); + $data = collect($this->data); + $templateKeys = collect($pieceTemplate)->keys(); + $dataCount = $data->count(); + $repetitionCount = 0; + for ($i = 1; $i <= $dataCount; $i++) { + $indexKeys = $templateKeys->map(fn ($key) => str_replace('__index', strval($i), $key)); + if ($indexKeys->every(fn ($key) => $data->has($key))) { + $repetitionCount++; + } + } + return $repetitionCount; + } + + /** + * Get part pieces + * + * @return array + */ + private function getPartPieces(): array + { + $pieceTemplate = self::getAddablePartPieceBlockTemplate($this->section); + return is_null($pieceTemplate) ? [] : $pieceTemplate; + } + + /** + * Addable part piece block + * + * @param \App\Enums\Sections\PageSectionEnum $sectionEnum + * @param integer $index + * @return array + */ + public static function getAddablePartPieceBlock(PageSectionEnum $sectionEnum, int $index = 1): array|null + { + $data = null; + $pieceTemplate = self::getAddablePartPieceBlockTemplate($sectionEnum); + if (!is_null($pieceTemplate)) { + $data = json_decode(Str::of(json_encode($pieceTemplate))->replace('__index', strval($index)), true); + /** @var array $data */ + } + return $data; + } + + /** + * Addable part piece block + * + * @param \App\Enums\Sections\PageSectionEnum $sectionEnum + * @return array + */ + public static function getAddablePartPieceBlockIndexes(PageSectionEnum $sectionEnum): array|null + { + $data = self::getAddablePartPieceBlockTemplate($sectionEnum); + if (!is_null($data)) { + return array_keys($data); + } + return $data; + } + + /** + * @param \App\Enums\Sections\PageSectionEnum $sectionEnum + * @return array|null + */ + private static function getAddablePartPieceBlockTemplate(PageSectionEnum $sectionEnum): array|null + { + $data = null; + switch ($sectionEnum) { + case PageSectionEnum::produce_export: + $data = array_merge( + self::emptyWysiwygBloc( + [], + "accompagnant l'image __index de la partie", + '' + ), + self::emptyPictureBloc([], '', '', '', 100, 100, true, "Image __index de la partie"), + self::emptyLinkBloc([], '', false, "Lien de l'image__index de la partie") + ); + break; + case PageSectionEnum::produce_rhd: + $data = array_merge( + self::emptyWysiwygBloc( + [], + "accompagnant l'image __index de la partie 1", + '' + ), + self::emptyPictureBloc([], '', '', '', 100, 100, true, "Image __index de la partie 1"), + self::emptyLinkBloc([], '', false, "Lien de l'image__index de la partie 1") + ); + break; + default: + } //end switch + return $data; + } + // * RELATIONS /** diff --git a/app/Policies/SectionPolicy.php b/app/Policies/SectionPolicy.php index d1fc9e9..cbbad1b 100644 --- a/app/Policies/SectionPolicy.php +++ b/app/Policies/SectionPolicy.php @@ -3,6 +3,7 @@ namespace App\Policies; use App\Enums\Users\RoleEnum; +use App\Models\Section; use App\Models\User; use App\Policies\Rules\UserStaticRules; @@ -30,4 +31,32 @@ class SectionPolicy // phpcs:enable return UserStaticRules::atLeastRole($authUser, RoleEnum::admin); } + + /** + * Can add part piece + * + * @param \App\Models\User $authUser + * @param \App\Models\Section $section + * @return boolean + * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed + */ + public function addPartPiece(User $authUser, Section $section): bool + { + // phpcs:enable + return Section::getAddablePartPieceBlock($section->section) !== null; + } + + /** + * Can remove part piece + * + * @param \App\Models\User $authUser + * @param \App\Models\Section $section + * @return boolean + * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed + */ + public function removePartPiece(User $authUser, Section $section): bool + { + // phpcs:enable + return Section::getAddablePartPieceBlock($section->section) !== null; + } } diff --git a/app/Traits/Models/MiniCrudModel.php b/app/Traits/Models/MiniCrudModel.php index 4ea4711..60a3b7c 100644 --- a/app/Traits/Models/MiniCrudModel.php +++ b/app/Traits/Models/MiniCrudModel.php @@ -168,7 +168,9 @@ trait MiniCrudModel case InputType::string->value: case InputType::text->value: case InputType::url->value: - ToolboxHelper::assertNoInternalBoUrls('content', $input['value']); + if (!is_null($input['value'])) { + ToolboxHelper::assertNoInternalBoUrls('content', $input['value']); + } break; default: throw new \ErrorException(sprintf( diff --git a/database/assets/schema.png b/database/assets/schema.png new file mode 100644 index 0000000..9d52a7b Binary files /dev/null and b/database/assets/schema.png differ diff --git a/database/migrations/2024_06_20_100530_add_picture_link_to_sections.php b/database/migrations/2024_06_20_100530_add_picture_link_to_sections.php new file mode 100644 index 0000000..1c7f949 --- /dev/null +++ b/database/migrations/2024_06_20_100530_add_picture_link_to_sections.php @@ -0,0 +1,118 @@ +items = array_merge(array_slice($this->items, 0, $target + 1), [$value], array_slice($this->items, $target + 1)); + return; + } + + foreach ($this->items as $key => $old) { + $new[$key] = $old; + if ($key === $target && is_null($new_key)) { + $new[] = $value; + } elseif ($key === $target) { + $new[$new_key] = $value; + } + } + + $this->items = $new; + }); + + $export = Section::query()->where('section', PageSectionEnum::produce_export)->first(); + if ($export) { + $partsAmount = 3; + $data = collect($export->data); + for ($i = 1; $i <= $partsAmount; $i++) { + $bloc = collect(Section::emptyLinkBloc( + [], + '', + false, + "Lien de l'image {$i} de la partie" + ))->first(); + $data->insertAfter( + "bloc_image{$i}_de_la_partie", + $bloc, + "bloc_lien_de_limage{$i}_de_la_partie" + ); + } + $export->data = $data->all(); + $export->saveQuietly(); + } + $rhd = Section::query()->where('section', PageSectionEnum::produce_rhd)->first(); + if ($rhd) { + $getEmptyLinkBlock = fn ($i, $k) => collect(Section::emptyLinkBloc( + [], + '', + false, + "Lien de l'image {$i} de la partie {$k}" + ))->first(); + + $data = collect($rhd->data); + $data->insertAfter( + 'bloc_image1_de_la_partie1', + $getEmptyLinkBlock(1, 1), + 'bloc_lien_de_limage1_de_la_partie1' + ); + $data->insertAfter( + 'bloc_image2_de_la_partie1', + $getEmptyLinkBlock(2, 1), + 'bloc_lien_de_limage2_de_la_partie1' + ); + $data->insertAfter( + 'bloc_image3_de_la_partie1', + $getEmptyLinkBlock(3, 1), + 'bloc_lien_de_limage3_de_la_partie1' + ); + $data->insertAfter( + 'bloc_image1_de_la_partie2', + $getEmptyLinkBlock(1, 2), + 'bloc_lien_de_limage1_de_la_partie2' + ); + $rhd->data = $data->all(); + $rhd->saveQuietly(); + } //end if + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + $export = Section::query()->where('section', PageSectionEnum::produce_export)->first(); + if ($export) { + $partsAmount = 3; + $data = collect($export->data); + for ($i = 1; $i <= $partsAmount; $i++) { + $data->forget("bloc_lien_de_limage{$i}_de_la_partie"); + } + $export->data = $data->all(); + $export->saveQuietly(); + } + $rhd = Section::query()->where('section', PageSectionEnum::produce_rhd)->first(); + if ($rhd) { + $data = collect($rhd->data); + $data->forget('bloc_lien_de_limage1_de_la_partie1'); + $data->forget('bloc_lien_de_limage2_de_la_partie1'); + $data->forget('bloc_lien_de_limage3_de_la_partie1'); + $data->forget('bloc_lien_de_limage1_de_la_partie2'); + $rhd->data = $data->all(); + $rhd->saveQuietly(); + } //end if + } +}; diff --git a/database/migrations/2024_06_21_145915_qa_section.php b/database/migrations/2024_06_21_145915_qa_section.php new file mode 100644 index 0000000..626dfba --- /dev/null +++ b/database/migrations/2024_06_21_145915_qa_section.php @@ -0,0 +1,71 @@ +items = array_merge(array_slice($this->items, 0, $target + 1), [$value], array_slice($this->items, $target + 1)); + return; + } + + foreach ($this->items as $key => $old) { + $new[$key] = $old; + if ($key === $target && is_null($new_key)) { + $new[] = $value; + } elseif ($key === $target) { + $new[$new_key] = $value; + } + } + + $this->items = $new; + }); + + $quality = Section::query()->where('section', PageSectionEnum::produce_quality)->first(); + if ($quality) { + $data = collect($quality->data); + $data->insertAfter( + 'bloc_text_de_la_partie2_content', + collect(Section::emptyPictureBloc([], FileStorageHelper::storeFile( + new Section(), + new \SplFileInfo(\resource_path( + '../database/assets/schema.png' + )) + ), fake()->sentence(), fake()->sentence(), 1208, 374))->first(), + 'bloc_image_de_la_partie2_content' + ); + $quality->data = $data->all(); + $quality->saveQuietly(); + } + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + $rhd = Section::query()->where('section', PageSectionEnum::produce_rhd)->first(); + if ($rhd) { + $data = collect($rhd->data); + $data->forget('bloc_lien_de_limage1_de_la_partie1'); + $rhd->data = $data->all(); + $rhd->saveQuietly(); + } //end if + } +}; diff --git a/database/seeders/Development/SectionSeeder.php b/database/seeders/Development/SectionSeeder.php index 15802f9..8523e3a 100644 --- a/database/seeders/Development/SectionSeeder.php +++ b/database/seeders/Development/SectionSeeder.php @@ -465,10 +465,6 @@ class SectionSeeder extends Seeder '../database/factories/assets/sections/default-picture-100x100.png' )) ), fake()->sentence(), fake()->sentence(), 200, 200); - $data = array_merge( - $data, - $section->emptyWysiwygBloc([], 'accompagnant l\'image', clean(\fake()->randomHtml())) - ); $data = array_merge($data, $section->emptyWysiwygBloc([], 'text', clean(\fake()->randomHtml()))); return $data; } diff --git a/resources/sass/fo/vendor/_ckEditorContent.scss b/resources/sass/fo/vendor/_ckEditorContent.scss index 26e3d02..19bd7f5 100644 --- a/resources/sass/fo/vendor/_ckEditorContent.scss +++ b/resources/sass/fo/vendor/_ckEditorContent.scss @@ -24,6 +24,11 @@ @include media-breakpoint-up(md) { hyphens: initial; } + + h2,h3,h4,.h2,.h3,.h4 { + color: $color-sixt !important; + font-weight: 600 !important; + } } /* diff --git a/resources/ts/bo/base/common.ts b/resources/ts/bo/base/common.ts index 89c7b86..8cd345c 100644 --- a/resources/ts/bo/base/common.ts +++ b/resources/ts/bo/base/common.ts @@ -30,4 +30,28 @@ window.addEventListener("DOMContentLoaded", () => { return false; }); } + + /** + * Popup delete item. + */ + const elementsLinks = document.getElementsByClassName( + "confirmDeleteByLinkTS" + ) as HTMLCollectionOf; + for (const element of elementsLinks) { + element.addEventListener("click", function (e) { + e.preventDefault(); + const el = e.target; + sweetalert.methods.confirm( + "Êtes-vous sûr ?", + "Toutes les données vont être perdu", + el as HTMLElement, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + function (response: SweetAlertResult) { + window.location.href = (el as HTMLAnchorElement).href; + }, + { icon: "warning" } + ); + return false; + }); + } }); diff --git a/resources/ts/bo/components/BelongsToManyDropdownComponent.vue b/resources/ts/bo/components/BelongsToManyDropdownComponent.vue index c4ae5dd..bda8283 100644 --- a/resources/ts/bo/components/BelongsToManyDropdownComponent.vue +++ b/resources/ts/bo/components/BelongsToManyDropdownComponent.vue @@ -67,9 +67,9 @@ import type VueTagsInput from "@sipec/vue3-tags-input"; import { VueTagsInput as VueTagsInputCls } from "@sipec/vue3-tags-input"; import slugify from "slugify"; import { defineComponent } from "vue"; -import parseLaravelErrorMessages from "./../../modules/parseLaravelErrorMessages"; -import route from "./../../modules/route"; -import trans from "./../../modules/trans"; +import parseLaravelErrorMessages from "../../modules/parseLaravelErrorMessages"; +import route from "../../modules/route"; +import trans from "../../modules/trans"; export default defineComponent({ name: "BelongsToManyDropdownComponent", diff --git a/resources/ts/bo/components/ImageInputComponent.vue b/resources/ts/bo/components/ImageInputComponent.vue index f9aae93..23cdf2e 100644 --- a/resources/ts/bo/components/ImageInputComponent.vue +++ b/resources/ts/bo/components/ImageInputComponent.vue @@ -1,138 +1,157 @@ - diff --git a/resources/ts/bo/components/WordCounterComponent.vue b/resources/ts/bo/components/WordCounterComponent.vue index 19abb51..b012275 100644 --- a/resources/ts/bo/components/WordCounterComponent.vue +++ b/resources/ts/bo/components/WordCounterComponent.vue @@ -11,7 +11,7 @@