316 lines
10 KiB
Vue
316 lines
10 KiB
Vue
<template>
|
|
<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>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ORDERABLE NODES -->
|
|
<div class="row justify-content-start">
|
|
<div class="col col-xl-4 my-3">
|
|
<fieldset
|
|
class="border p-4"
|
|
:disabled="nodeToEdit !== null"
|
|
>
|
|
<legend>
|
|
<span class="fs-5">{{ trans.methods.__('Ordonancement du menu') }}</span>
|
|
<button
|
|
class="btn btn-sm btn-primary float-end mt-1"
|
|
:title="trans.methods.__('Ajouter un lien au menu')"
|
|
@click.prevent="addLinkNode"
|
|
>
|
|
<FontAwesomeIcon icon="fa fa-plus" />
|
|
</button>
|
|
</legend>
|
|
<div class="list-group">
|
|
<Draggable
|
|
:list="nodeList"
|
|
item-key="type"
|
|
@change="orderChanged"
|
|
>
|
|
<!-- LIST ITEM -->
|
|
<template #item="{ element, index } : {element: MenuNodeInterface, index: number}">
|
|
<div class="list-group-item d-flex flux-column justify-content-between">
|
|
<span class="h6">
|
|
<FontAwesomeIcon
|
|
icon="fa-solid fa-align-justify"
|
|
class="handle text-secondary"
|
|
/>
|
|
<span> </span>
|
|
<strong>{{ element.title }}</strong>
|
|
</span>
|
|
|
|
<!-- ACTIONS -->
|
|
<div class="btn-group">
|
|
<button
|
|
v-if="element.type !== MenuNodeTypeEnum.siteLogo"
|
|
class="btn btn-sm btn-primary"
|
|
@click.prevent="editNode(index)"
|
|
>
|
|
<FontAwesomeIcon icon="fa fa-pencil" />
|
|
</button>
|
|
<button
|
|
v-if="element.type === MenuNodeTypeEnum.link"
|
|
class="btn btn-sm btn-danger"
|
|
@click.prevent="removeLinkNode(index)"
|
|
>
|
|
<FontAwesomeIcon icon="fa fa-trash" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Draggable>
|
|
</div>
|
|
</fieldset>
|
|
</div>
|
|
|
|
|
|
<div class="col-12 col-xl-8 main-menu-node-editor-component my-3 overflow-y-scroll">
|
|
<!-- NODE EDITOR -->
|
|
<MainMenuNodeEditor
|
|
v-if="nodeToEdit !== null"
|
|
:menu-node="nodeToEdit"
|
|
:enum="getEnum(nodeToEdit.type)"
|
|
@close-editor="closeEditor"
|
|
@updated-node="(menuNode) => nodeIndexToEdit !== null ? saveEditedNode(nodeIndexToEdit, menuNode) : null"
|
|
/>
|
|
|
|
<!-- NODE DATA EDITOR -->
|
|
<MainMenuNodeDataEditor
|
|
v-if="nodeDataToEdit !== null && nodeDataToEdit.type !== MenuNodeTypeEnum.link"
|
|
:menu-node-data="getNodeData(nodeDataToEdit.type)"
|
|
:errors="menuNodeDataErrors"
|
|
@updated-node-data="(prop, value) => nodeDataToEdit !== null ? saveMenuNodeData(nodeDataToEdit.type, prop, value) : null"
|
|
/>
|
|
</div>
|
|
|
|
<!-- INPUTS FOR SUBMISSION -->
|
|
<input
|
|
type="hidden"
|
|
name="jsonValue"
|
|
:value="JSON.stringify(nodeList)"
|
|
>
|
|
</div>
|
|
</fieldset>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
|
import type { AxiosError } from "axios";
|
|
import axios, { type AxiosResponse } from "axios";
|
|
import { defineOptions, onMounted, reactive, 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";
|
|
import { MenuNodeTypeEnum } from "../enums";
|
|
import type { MenuNodeInterface } from "./../../types/models/MenuNode";
|
|
import type { MenuNodeDataInterface } from "./../../types/models/MenuNodeData";
|
|
import MainMenuNodeDataEditor from "./MainMenuNodeDataEditor.vue";
|
|
import MainMenuNodeEditor from "./MainMenuNodeEditor.vue";
|
|
|
|
defineOptions({
|
|
name: "MainMenu",
|
|
inheritAttrs: true,
|
|
});
|
|
|
|
// * ATTRIBUTES
|
|
const attrs = useAttrs();
|
|
|
|
// * DATA
|
|
const loading = ref<boolean>(false);
|
|
const nodeList = ref<Array<MenuNodeInterface>>([]);
|
|
const menuNodeDataList = ref<Array<MenuNodeDataInterface>>([]);
|
|
let nodeTypeEnumList = reactive<Map<MenuNodeTypeEnum,Enum>>(new Map<MenuNodeTypeEnum,Enum>());
|
|
/** If set not null then opens the node editor */
|
|
let nodeIndexToEdit = ref<number|null>(null);
|
|
let nodeToEdit = ref<MenuNodeInterface|null>(null);
|
|
let nodeDataToEdit = ref<MenuNodeDataInterface|null>(null);
|
|
let menuNodeDataErrors = reactive<LaravelValidationErrors>({});
|
|
|
|
// * COMPUTED
|
|
|
|
// * LIFECYCLE
|
|
onMounted(() => {
|
|
const json = String(attrs.json ?? "{}"),
|
|
data = JSON.parse(json);
|
|
nodeList.value = data.nodeList ?? [];
|
|
nodeTypeEnumList = data.nodeTypeEnumList ?? [];
|
|
fetchMenuNodeDataList().then((dataList) => { menuNodeDataList.value = dataList; });
|
|
});
|
|
|
|
// * METHODS
|
|
function orderChanged(): void {
|
|
console.log("sdsd");
|
|
}
|
|
|
|
function addLinkNode(): void {
|
|
const newNode = {
|
|
type: MenuNodeTypeEnum.link,
|
|
title: trans.methods.__("Lien"),
|
|
link: {
|
|
routeName: null,
|
|
url: "https://example.net"
|
|
}
|
|
};
|
|
console.log("push nodeList", newNode);
|
|
nodeList.value.push(newNode);
|
|
}
|
|
|
|
function removeLinkNode(index: number): void {
|
|
nodeList.value.splice(index, 1);
|
|
}
|
|
|
|
function editNode(index: number):void {
|
|
nodeIndexToEdit.value = index;
|
|
nodeToEdit.value = nodeList.value[index];
|
|
nodeDataToEdit.value = menuNodeDataList.value.find((nodeData) => nodeData.type === nodeToEdit.value?.type) ?? null;
|
|
}
|
|
function saveEditedNode(index: number, menuNode: MenuNodeInterface):void {
|
|
nodeList.value[index] = menuNode;
|
|
}
|
|
function closeEditor(): void {
|
|
nodeIndexToEdit.value = null;
|
|
nodeToEdit.value = null;
|
|
nodeDataToEdit.value = null;
|
|
}
|
|
function refreshEditor(): void
|
|
{
|
|
if (nodeIndexToEdit.value !== null) {
|
|
editNode(nodeIndexToEdit.value);
|
|
}
|
|
}
|
|
|
|
function getNodeData(type: MenuNodeTypeEnum): MenuNodeDataInterface
|
|
{
|
|
const NodeData = menuNodeDataList.value.find((nodeData) => nodeData.type === type);
|
|
if(!NodeData) {
|
|
throw new Error(`menuNodeDataList does not contain value of type '${type}'`);
|
|
}
|
|
return NodeData;
|
|
}
|
|
|
|
async function fetchMenuNodeDataList(): Promise<Array<MenuNodeDataInterface>>
|
|
{
|
|
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.get<Array<MenuNodeDataInterface>, AxiosResponse<Array<MenuNodeDataInterface>, any>>(menuNodeDataListRoute)
|
|
.then((res) => resolve(res.data))
|
|
.catch(() => {
|
|
toast.methods.errorNotify(trans.methods.__("Impossible de récuperer les donnés des noeuds"));
|
|
reject(`Failed to fetch api data on route ${menuNodeDataListRoute}`);
|
|
}).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;
|
|
|
|
const succeedAction = () => {
|
|
loading.value = false;
|
|
menuNodeDataErrors = {};
|
|
fetchMenuNodeDataList().then((dataList) => { menuNodeDataList.value = dataList; refreshEditor(); });
|
|
resolve(true);
|
|
};
|
|
const catchAction = (error: AxiosError<{errors: LaravelValidationErrors|{}}>) => {
|
|
let message = "Une erreur est survenue merci de ressayer plus tard";
|
|
if (error!.response!.status === 422) {
|
|
// ts:ignore
|
|
menuNodeDataErrors = error.response?.data?.errors ?? {};
|
|
toast.methods.errorNotify(trans.methods.__("La valeur saisie est incorrecte"));
|
|
return;
|
|
}
|
|
if (error!.response!.status === 401) {
|
|
// ts:ignore
|
|
toast.methods.errorNotify(trans.methods.__("Vous devez vous reconnecter"));
|
|
console.warn(trans.methods.__("Vous devez vous reconnecter"));
|
|
return;
|
|
}
|
|
if (error!.response!.status === 419) {
|
|
// ts:ignore
|
|
toast.methods.errorNotify(trans.methods.__("Vous devez rafraichir la page"));
|
|
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);
|
|
};
|
|
|
|
if(menuNodeData instanceof File) {
|
|
const formData = new FormData();
|
|
formData.append("_method", "PUT");
|
|
formData.append(prop, menuNodeData);
|
|
// ! Apache or PHP ? cannot fetch bin file without using POST
|
|
axios.post<boolean>(menuNodeDataListRoute, formData, {
|
|
headers: {
|
|
"content-type": "multipart/form-data"
|
|
}
|
|
}).then(succeedAction).catch(catchAction)
|
|
.finally(() => { loading.value = false; });
|
|
return;
|
|
}
|
|
|
|
axios.patch<boolean>(menuNodeDataListRoute, {
|
|
[prop]: menuNodeData
|
|
}).then(succeedAction).catch(catchAction)
|
|
.finally(() => { loading.value = false; });
|
|
});
|
|
}
|
|
|
|
function getEnum(tsEnum: MenuNodeTypeEnum): Enum {
|
|
const enumValue: Enum|undefined = Object.values(nodeTypeEnumList).find((value) => value.value === tsEnum);
|
|
if (!enumValue) {
|
|
throw new Error(`Undefined enumValue for ${tsEnum}`);
|
|
}
|
|
return enumValue;
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.main-menu-component {
|
|
.handle {
|
|
cursor: grab;
|
|
}
|
|
}
|
|
</style>
|