ldc-legaulois/resources/ts/bo/components/MainMenu.vue
2024-04-19 12:23:01 +02:00

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>&nbsp;&nbsp;&nbsp;</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>