Bo dynamic menu ok

This commit is contained in:
Jérémy Munsch 2024-04-16 16:53:38 +02:00
parent 4a8d615387
commit eb2afed1a7
No known key found for this signature in database
GPG key ID: 18361AF3BFEC0FEC
8 changed files with 212 additions and 107 deletions

View file

@ -27,14 +27,24 @@ class UpdateMenuNodeDataRequest extends FormRequest
*/
public function prepareForValidation(): void
{
$this->merge([
'text' => str($this->input('text'))->value(),
'link_target' => $this->boolean('link_target'),
'link_status' => $this->boolean('link_status'),
'picture_status' => $this->boolean('picture_status'),
]);
$this->mergePicture('picture');
$this->mergePicture('background_picture');
if ($this->has('text')) {
$this->merge(['text' => str($this->input('text'))->value()]);
}
if ($this->has('link_target')) {
$this->merge(['link_target' => $this->boolean('link_target')]);
}
if ($this->has('link_status')) {
$this->merge(['link_status' => $this->boolean('link_status')]);
}
if ($this->has('picture_status')) {
$this->merge(['picture_status' => $this->boolean('picture_status')]);
}
if ($this->has('picture')) {
$this->mergePicture('picture');
}
if ($this->has('background_picture')) {
$this->mergePicture('background_picture');
}
}
/**
@ -45,24 +55,22 @@ class UpdateMenuNodeDataRequest extends FormRequest
public function rules(): array
{
$rules = [
'title' => 'required|string|min:4|max:255',
'text' => 'present|string|max:255',
'link_url' => 'string|max:255',
'link_title' => 'required|string|between:3,255',
'link_target' => 'required|boolean',
'link_status' => 'string|boolean',
'picture' => 'string|max:255',
'picture_alt' => 'string|between:3,255',
'picture_title' => 'string|between:3,255',
'picture_status' => 'string|boolean',
'background_picture' => 'string|max:255',
'background_picture_alt' => 'string|between:3,255',
'background_picture_title' => 'string|between:3,255',
'title' => 'sometimes|required|string|min:4|max:255',
'text' => 'sometimes|required|string|max:255',
'link_url' => 'sometimes|required|string|max:255',
'link_title' => 'sometimes|required|string|between:3,255',
'link_target' => 'sometimes|present|boolean',
'link_status' => 'sometimes|present|boolean',
'picture_alt' => 'sometimes|required|string|between:3,255',
'picture_title' => 'sometimes|required|string|between:3,255',
'picture_status' => 'sometimes|required|string|boolean',
'background_picture_alt' => 'sometimes|required|string|between:3,255',
'background_picture_title' => 'sometimes|required|string|between:3,255',
];
return \array_merge(
$rules,
$this->pictureRules('picture', 180, 228, 180, 228),
$this->pictureRules('background_picture', 500, 231, 500, 231),
$this->pictureRules('picture', 180, 228, 180, 228, nullable: false, sometimes: true),
$this->pictureRules('background_picture', 500, 231, 500, 231, nullable: false, sometimes: true),
);
}
}

View file

@ -49,6 +49,7 @@ trait HasPicture
* @param integer|null $size
* @param string|null $requiredWith
* @param string|null $requiredWithout
* @param boolean $sometimes
* @return array
*/
public function pictureRules(
@ -61,6 +62,7 @@ trait HasPicture
?int $size = null,
?string $requiredWith = null,
?string $requiredWithout = null,
bool $sometimes = false,
): array {
$dimensions = (object)[
'minWidth' => $minWidth ?? $this->minWidth,
@ -70,6 +72,7 @@ trait HasPicture
];
return [
$name => [
$sometimes ? 'sometimes' : '',
!$nullable ? 'required' : 'nullable',
(strlen($requiredWith)) ? 'required_with:' . $requiredWith : '',
(strlen($requiredWithout)) ? 'required_without:' . $requiredWithout : '',

View file

@ -425,7 +425,7 @@
<script setup lang="ts">
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import Cropper from "cropperjs";
import { computed, defineEmits, defineOptions, nextTick, onMounted, reactive, ref, useAttrs, watch } from "vue";
import { computed, defineOptions, nextTick, onMounted, reactive, ref, useAttrs, watch } from "vue";
import { Tooltips } from "./../../modules/bsTooltips";
import trans from "./../../modules/trans";
@ -442,8 +442,6 @@ type CropperParameters = {
scaleY: MirrorScale
};
type ValueOf<T> = T[keyof T];
// * CONSTANTS
const UNITS = [
"byte",

View file

@ -1,5 +1,8 @@
<template>
<div class="container-fluid main-menu-component my-3">
<fieldset
class="container-fluid main-menu-component my-3"
:disabled="loading"
>
<div class="row justify-content-start mb-3">
<div class="col">
<span class="h5">{{ trans.methods.__('Réorganisez le menu principal comme vous le souhaitez ou ajoutez un lien.') }}</span>
@ -11,7 +14,7 @@
<div class="col col-xl-4 my-3">
<fieldset
class="border p-4"
:disabled="nodeIndexToEdit !== null || loading"
:disabled="nodeIndexToEdit !== null"
>
<legend>
<span class="fs-5">{{ trans.methods.__('Ordonancement du menu') }}</span>
@ -80,7 +83,8 @@
<MainMenuNodeDataEditor
v-if="nodeDataToEdit !== null && nodeDataToEdit.type !== MenuNodeTypeEnum.link"
:menu-node-data="getNodeData(nodeDataToEdit.type)"
@updated-node-data="(value) => { saveMenuNodeData(value) }"
:errors="menuNodeDataErrors"
@updated-node-data="(prop, value) => nodeDataToEdit ? saveMenuNodeData(nodeDataToEdit.type, prop, value) : null"
/>
</div>
@ -91,7 +95,7 @@
:value="JSON.stringify(nodeList)"
>
</div>
</div>
</fieldset>
</template>
<script lang="ts" setup>
@ -99,6 +103,7 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import axios, { type AxiosResponse } from "axios";
import { computed, defineOptions, onMounted, ref, useAttrs } from "vue";
import Draggable from "vuedraggable";
import type { IMenuNodeDataInputInterface } from "../../interfaces/IMenuNodeDataInputInterface";
import route from "../../modules/route";
import toast from "../../modules/toast";
import trans from "../../modules/trans";
@ -123,6 +128,7 @@ const menuNodeDataList = ref<Array<MenuNodeDataInterface>>([]);
const nodeTypeEnumList = ref<Map<MenuNodeTypeEnum,Enum>>(new Map<MenuNodeTypeEnum,Enum>());
/** If set not null then opens the node editor */
const nodeIndexToEdit = ref<number|null>(null);
const menuNodeDataErrors = ref<LaravelValidationErrors>({});
// * COMPUTED
const nodeToEdit = computed(() => nodeIndexToEdit.value !== null && nodeList.value[nodeIndexToEdit.value] !== undefined ?
@ -180,35 +186,6 @@ function getNodeData(type: MenuNodeTypeEnum): MenuNodeDataInterface
}
return NodeData;
}
async function saveMenuNodeData(menuNodeData: MenuNodeDataInterface): Promise<boolean>
{
return new Promise((resolve, reject) => {
const menuNodeDataListRoute = route.methods.route("bo.menu_node_data.index");
if(!menuNodeDataListRoute) {
throw new Error("Could not load route 'bo.menu_node_data.index'");
}
if(loading.value) {
toast.methods.errorNotify("An action is already occuring");
reject();
return;
}
loading.value = true;
axios.post<boolean>(menuNodeDataListRoute, menuNodeData).then(() => resolve(true))
.catch(() => resolve(false))
.finally(() => { loading.value = false; });
});
}
function getEnum(tsEnum: MenuNodeTypeEnum): Enum {
const enumValue: Enum|undefined = Object.values(nodeTypeEnumList.value).find((value) => value.value === tsEnum);
if (!enumValue) {
throw new Error(`Undefined enumValue for ${tsEnum}`);
}
return enumValue;
}
async function fetchMenuNodeDataList(): Promise<Array<MenuNodeDataInterface>>
{
@ -234,6 +211,61 @@ async function fetchMenuNodeDataList(): Promise<Array<MenuNodeDataInterface>>
}).finally(() => { loading.value = false; });
});
}
async function saveMenuNodeData(
type: MenuNodeTypeEnum,
prop: string,
menuNodeData: ValueOf<IMenuNodeDataInputInterface>
): Promise<boolean> {
return new Promise((resolve, reject) => {
const menuNodeDataListRoute = route.methods.route("bo.menu_node_data.update", {MENU_NODE_DATA: type});
if(!menuNodeDataListRoute) {
reject();
throw new Error("Could not load route 'bo.menu_node_data.update'");
}
if(loading.value) {
toast.methods.errorNotify("An action is already occuring");
reject();
return;
}
loading.value = true;
axios.patch<boolean>(menuNodeDataListRoute, {
[prop]: menuNodeData
}).then(() => {
loading.value = false;
menuNodeDataErrors.value = {};
fetchMenuNodeDataList().then((dataList) => { menuNodeDataList.value = dataList; });
resolve(true);
}).catch((error) => {
let message = "Une erreur est survenue merci de ressayer plus tard";
if (error.response.status === 422) {
menuNodeDataErrors.value = error.response?.data?.errors ?? {};
return;
}
if (error.response) {
console.error(error.response.data);
} else if (error.request) {
console.error(error.request);
} else {
// Something happened in setting up the request that triggered an Error
console.error("Error", error.message);
}
toast.methods.errorNotify(message);
resolve(false);
}).finally(() => { loading.value = false; });
});
}
function getEnum(tsEnum: MenuNodeTypeEnum): Enum {
const enumValue: Enum|undefined = Object.values(nodeTypeEnumList.value).find((value) => value.value === tsEnum);
if (!enumValue) {
throw new Error(`Undefined enumValue for ${tsEnum}`);
}
return enumValue;
}
</script>
<style lang="scss" scoped>

View file

@ -7,33 +7,38 @@
<!-- TITLE -->
<div class="mb-3 row">
<label
for="type"
for="title-text"
class="col-sm-2 col-form-label"
>
{{ trans.methods.__("Titre") }}
</label>
<div class="col-sm-4">
<input
id="title-text"
type="text"
class="form-control"
:value="menuNodeData.title"
@blur="($event) => saveNodeDataInput('title', ($event.target as HTMLInputElement).value)"
>
<small v-if="errors.title" v-for="(message) of errors.title" class="text-danger">"{{ message }}"</small>
</div>
</div>
<!-- TEXT -->
<div class="mb-3 row">
<label
for="type"
for="text-text"
class="col-sm-2 col-form-label"
>
{{ trans.methods.__("Text") }}
</label>
<div class="col-sm-4">
<input
id="text-text"
type="text"
class="form-control"
:value="menuNodeData.text"
@blur="($event) => saveNodeDataInput('text', ($event.target as HTMLInputElement).value)"
>
</div>
</div>
@ -42,59 +47,75 @@
<div class="mb-3 row">
<!-- LINK URL -->
<label
for="type"
for="link-text"
class="col-sm-2 col-form-label p-2"
>
{{ trans.methods.__("Link url") }}
</label>
<div class="col-sm-4 p-2">
<input
id="link-text"
type="text"
class="form-control"
:value="menuNodeData.link_url"
@blur="($event) => saveNodeDataInput('link_url', ($event.target as HTMLInputElement).value)"
>
</div>
<!-- LINK TITLE -->
<label
for="type"
for="link-title-text"
class="col-sm-2 col-form-label p-2"
>
{{ trans.methods.__("Link title") }}
</label>
<div class="col-sm-4 p-2">
<input
id="link-title-text"
type="text"
class="form-control"
:value="menuNodeData.link_url"
:value="menuNodeData.link_title"
@blur="($event) => saveNodeDataInput('link_title', ($event.target as HTMLInputElement).value)"
>
</div>
<!-- LINK TARGET -->
<label
for="type"
for="link-target-checkbox"
class="col-sm-2 col-form-label p-2"
>
{{ trans.methods.__("Link target") }}
</label>
<div class="col-sm-4 p-2">
<input
type="text"
class="form-control"
:value="menuNodeData.link_target"
>
<div class="form-check form-switch">
<input
name="link_target"
class="form-check-input pt-3"
type="checkbox"
role="switch"
id="link-target-checkbox"
:checked="menuNodeData.link_target"
@change="($event) => saveNodeDataInput('link_target', ($event.target as HTMLInputElement).checked)"
>
</div>
</div>
<!-- LINK STATUS -->
<label
for="type"
class="col-sm-2 col-form-label p-2"
for="link-status-checkbox"
>
{{ trans.methods.__("Link status") }}
</label>
<div class="col-sm-4 p-2">
<input
type="text"
class="form-control"
:value="menuNodeData.link_status"
>
<div class="form-check form-switch">
<input
name="link_status"
class="form-check-input pt-3"
type="checkbox"
role="switch"
id="link-status-checkbox"
:checked="menuNodeData.link_status"
@change="($event) => saveNodeDataInput('link_status', ($event.target as HTMLInputElement).checked)"
>
</div>
</div>
</div>
@ -104,104 +125,134 @@
<!-- PICTURE -->
<div class="mb-3 row">
<!-- PICTURE -->
<label
for="picture-input"
class="col-sm-2 col-form-label p-2"
>
{{ trans.methods.__("Picture") }}
</label>
<ImageInputComponent
:required="false"
id="picture-input"
:required="true"
:show-labels="true"
name="picture"
helper="picture"
:width="100"
:height="100"
:value="menuNodeData.picture"
:preview="true"
:preview="false"
:browse-event="false"
@image-file=""
@image-file="(file) => file ? saveNodeDataInput('picture', file) : null"
/>
<!-- PICTURE ALT -->
<label
for="type"
for="picture-alt-text"
class="col-sm-2 col-form-label p-2"
>
{{ trans.methods.__("Picture alt") }}
</label>
<div class="col-sm-4 p-2">
<input
id="picture-alt-text"
type="text"
class="form-control"
:value="menuNodeData.picture_alt"
@blur="($event) => saveNodeDataInput('picture_alt', ($event.target as HTMLInputElement).value)"
>
</div>
<!-- PICTURE TITLE -->
<label
for="type"
for="picture-title-text"
class="col-sm-2 col-form-label p-2"
>
{{ trans.methods.__("Picture title") }}
</label>
<div class="col-sm-4 p-2">
<input
id="picture-title-text"
type="text"
class="form-control"
:value="menuNodeData.picture_title"
@blur="($event) => saveNodeDataInput('picture_title', ($event.target as HTMLInputElement).value)"
>
</div>
<!-- PICTURE STATUS -->
<label
for="type"
class="col-sm-2 col-form-label p-2"
for="picture-status-checkbox"
>
{{ trans.methods.__("Picture status") }}
</label>
<div class="col-sm-4 p-2">
<input
type="text"
class="form-control"
:value="menuNodeData.picture_status"
>
<div class="form-check form-switch">
<input
name="picture_status"
class="form-check-input pt-3"
type="checkbox"
role="switch"
id="picture-status-checkbox"
:checked="menuNodeData.picture_status"
@change="($event) => saveNodeDataInput('picture_status', ($event.target as HTMLInputElement).checked)"
>
</div>
</div>
</div>
<!-- --- -->
<hr>
<!-- BACKGROUND PICTURE -->
<!-- BACKGROUND PICTURE-->
<div class="mb-3 row">
<label
for="background-picture-input"
class="col-sm-2 col-form-label p-2"
>
{{ trans.methods.__("Background Picture") }}
</label>
<!-- BACKGROUND PICTURE -->
<ImageInputComponent
:required="false"
id="background-picture-input"
:required="true"
:show-labels="true"
helper="picture"
name="background_picture"
helper="background_picture"
:width="100"
:height="100"
:value="menuNodeData.background_picture"
:preview="true"
:preview="false"
:browse-event="false"
@image-file="(file) => file ? saveNodeDataInput('background_picture', file) : null"
/>
<!-- BACKGROUND PICTURE ALT -->
<label
for="type"
for="background-picture-alt-text"
class="col-sm-2 col-form-label p-2"
>
{{ trans.methods.__("Picture alt") }}
{{ trans.methods.__("Background Picture alt") }}
</label>
<div class="col-sm-4 p-2">
<input
id="background-picture-alt-text"
type="text"
class="form-control"
:value="menuNodeData.background_picture_alt"
@blur="($event) => saveNodeDataInput('background_picture_alt', ($event.target as HTMLInputElement).value)"
>
</div>
<!-- BACKGROUND PICTURE TITLE -->
<label
for="type"
for="background-picture-title-text"
class="col-sm-2 col-form-label p-2"
>
{{ trans.methods.__("Background picture title") }}
</label>
<div class="col-sm-4 p-2">
<input
id="background-picture-title-text"
type="text"
class="form-control"
:value="menuNodeData.background_picture_title"
@blur="($event) => saveNodeDataInput('background_picture_title', ($event.target as HTMLInputElement).value)"
>
</div>
</div>
@ -210,6 +261,7 @@
<script lang="ts" setup>
import { defineOptions, onMounted, ref, watch, type PropType } from "vue";
import type { IMenuNodeDataInputInterface } from "../../interfaces/IMenuNodeDataInputInterface";
import trans from "../../modules/trans";
import type { MenuNodeDataInterface } from "../../types/models/MenuNodeData";
import ImageInputComponent from "./ImageInputComponent.vue";
@ -220,7 +272,7 @@ defineOptions({
// * EMITS
const emits = defineEmits<{
updatedNodeData: [menuNode: MenuNodeDataInterface],
updatedNodeData: [prop: string, value: ValueOf<IMenuNodeDataInputInterface> ],
}>();
// * ATTRIBUTES
@ -231,6 +283,10 @@ const props = defineProps({
menuNodeData: {
type: Object as PropType<MenuNodeDataInterface>,
required: true
},
errors: {
type: Object as PropType<LaravelValidationErrors>,
required: true
}
});
@ -240,16 +296,14 @@ const menuNodeData = ref<MenuNodeDataInterface>(Object.assign({}, props.menuNode
// * LIFECYCLE
onMounted(() => {});
watch(menuNodeData, (newValue) => {
emits("updatedNodeData", newValue);
}, { deep: true });
watch(props, () => {
menuNodeData.value = Object.assign({}, props.menuNodeData);;
})
// * METHODS
// function isUrlValid(url: string):boolean {
// const regex = RegExp("(https?:\\/\\/)?((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|((\\d{1,3}\\.){3}\\d{1,3}))(\\:\\d+)?(\\/[-a-z\\d%_.~+@]*)*(\\?[;&a-z\\d%_.~+=-@]*)?(\\#[-a-z\\d_@]*)?$", "i");
// return url.match(regex)?.[0] !== undefined;
// }
function saveNodeDataInput(inputName: string, inputValue: ValueOf<IMenuNodeDataInputInterface>): void {
emits("updatedNodeData", inputName, inputValue);
}
</script>
<style lang="scss" scoped>

View file

@ -1,8 +1,8 @@
export { };
import type VueTagsInput from "@sipec/vue3-tags-input";
import { AxiosStatic } from "axios";
import type Swiper from "swiper";
import type { MenuNodeInterface } from "./types/models/MenuNode";
import type VueTagsInput from "@sipec/vue3-tags-input";
import { AxiosStatic } from "axios";
import type Swiper from "swiper";
import type { MenuNodeInterface } from "./types/models/MenuNode";
// * Enum
interface EnumInterface {
@ -78,6 +78,9 @@ declare global {
swiper: Swiper;
};
type ValueOf<T> = T[keyof T];
type LaravelValidationErrors = Record<string, string[]>;
interface Window {
axios: AxiosStatic;
Swal: Function;

View file

@ -0,0 +1,6 @@
import type { MenuNodeDataInterface } from "../types/models/MenuNodeData";
export interface IMenuNodeDataInputInterface extends Omit<MenuNodeDataInterface, "picture" | "background_picture"> {
picture: File;
background_picture: File;
}

View file

@ -10,4 +10,5 @@
window.__SYSTEM._routes['fo.contact.show'] = '{{ route('fo.contact.show') }}';
window.__SYSTEM._routes['fo.add-single-optin'] = "{{ route('fo.add-single-optin') }}";
window.__SYSTEM._routes['bo.menu_node_data.index'] = '{{ route('bo.menu_node_data.index') }}';
window.__SYSTEM._routes['bo.menu_node_data.update'] = '{{ route('bo.menu_node_data.update', ['menu_node_data' => 'MENU_NODE_DATA']) }}';
</script>