ldc-com-app-bo/resources/ts/bo/components/DirectoriesComponent.vue
2026-10-22 16:18:08 +02:00

442 lines
13 KiB
Vue

<template>
<div :class="`directories-${intId} position-relative`">
<Transition name="fade">
<p
v-if="intMessage"
class="d-flex align-items-center text-danger mb-2"
>
<span>{{ Object.values(intMessage)[0] }}&nbsp;</span>
<a
class="btn btn-sm btn-primary"
:href="getEditButtonRoute(Number(Object.keys(intMessage)[0]))"
:title="trans.methods.__('Modifier ce paramètre')"
data-bs="tooltip"
>
<FontAwesomeIcon icon="fa-solid fa-pencil" />
</a>
</p>
</Transition>
<vue-nestable
:value="intDirectories"
@change="updateOrderDirectories"
@input="intDirectories = $event"
:hooks="{ beforeMove: beforeMove }"
>
<template
#default="{
item,
}: {
item: {
id: number,
name: string[],
needAuthorization: boolean,
documents_count: number,
children: number[]
},
}"
>
<vue-nestable-handle
class="d-flex justify-content-between align-items-center border rounded p-1"
>
<div class="d-flex justify-content-start align-items-center">
<div
v-if="
typeof item.children != 'undefined' && item.children.length > 0
"
>
<button
v-if="intDisplay[item.id]"
@click="displayList($event, item.id)"
class="btn btn-sm btn-outline-dark h-auto"
:title="trans.methods.__('Dérouler la liste')"
data-bs="tooltip"
>
<FontAwesomeIcon icon="fa-solid fa-arrow-down" />
</button>
<button
v-else
@click="displayList($event, item.id)"
class="btn btn-sm btn-outline-dark h-auto"
:title="trans.methods.__('Rouler la liste')"
data-bs="tooltip"
>
<FontAwesomeIcon icon="fa-solid fa-arrow-up" />
</button>
</div>
<button class="btn btn-sm border-0 disabled opacity-100">
<FontAwesomeIcon icon="fa-solid fa-grip-vertical" />
</button>
<p class="m-0">
<span class="badge d-flex align-items-center p-1 pe-2 text-warning-emphasis bg-warning-subtle border border-warning-subtle rounded-pill">
<FontAwesomeIcon
icon="fa-solid fa-file"
class="rounded-circle me-1"
/>
{{ item.documents_count }}
</span>
</p>
&nbsp;
<p class="m-0">
{{ item.name[("fr" as keyof typeof item.name) as number] }}
</p>
</div>
<div
class="d-flex justify-content-end align-items-center"
>
<div class="input-group">
<button
v-if="item.needAuthorization"
@click="updateAuthorization(item)"
class="btn btn-sm btn-warning"
:title="trans.methods.__('L\'accès à ce dossier est restreint')"
data-bs="tooltip"
>
<FontAwesomeIcon icon="fa-solid fa-lock" />
</button>
<button
v-else
@click="updateAuthorization(item)"
class="btn btn-sm btn-success"
:title="trans.methods.__('L\'accès à ce dossier est tout public')"
data-bs="tooltip"
>
<FontAwesomeIcon icon="fa-solid fa-lock-open" />
</button>
<a
:href="getEditDirectoryRoute(item.id)"
class="btn btn-sm btn-primary"
:title="trans.methods.__('Modifier le Dossier')"
data-bs="tooltip"
>
<FontAwesomeIcon icon="fa-solid fa-pencil" />
</a>
<button
@click="deleteDirectory($event, item as never as DirectoryObject)"
class="btn btn-sm btn-danger confirmDelete"
:title="trans.methods.__('Supprimer le Dossier')"
data-bs="tooltip"
ref="confirmDelete"
>
<FontAwesomeIcon icon="fa-solid fa-trash" />
</button>
</div>
</div>
</vue-nestable-handle>
</template>
</vue-nestable>
<div
v-if="intLoading"
class="loading position-absolute top-0 start-0 d-flex justify-content-center align-items-center rounded-1 w-100 h-100"
>
<div
class="spinner-border text-secondary"
role="status"
>
<span class="visually-hidden">
{{ trans.methods.__("form.image_input_viewer_loading") }}
</span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { Tooltip } from "bootstrap";
import { onMounted, nextTick, ref, useAttrs } from "vue";
import { VueNestable, VueNestableHandle } from "vue3-nestable";
import parseLaravelErrorMessages from "../../modules/parseLaravelErrorMessages";
import route from "../../modules/route";
import sweetalertConfirm from "../../modules/sweetalertConfirm";
import toast from "../../modules/toast";
import trans from "../../modules/trans";
defineOptions({
name: "DirectoriesComponent"
});
// * ATTRIBUTES
const attrs = useAttrs();
// * REFS
const confirmDelete = ref<HTMLButtonElement|null>(null);
// * DATA
const intId = ref<string>("");
const intDirectories = ref<Array<DirectoryObject>>([]);
const intMessage = ref<object|object[]|null>(null);
const intDisplay = ref<boolean[]>([]);
const intLoading = ref<boolean>(false);
const intTooltipList = ref<HTMLButtonElement[]>([]);
onMounted(() => {
const json = String((attrs as any).json ?? "{}");
const data = JSON.parse(json);
intId.value = data.id;
if (data.directories) {
intDirectories.value = recursiveDir(data.directories);
}
nextTick(() => {
updateBootstrapTooltip();
});
});
/** Prepare the list of items. */
function recursiveDir(list: DirectoryObject[], parent: number | null = null) {
let root = list.filter((e: DirectoryObject) => e.directory_id == parent);
for (let i = 0; i < root.length; i++) {
let childs = recursiveDir(list, root[i].id);
root[i].children = [];
for (let j = 0; j < childs.length; j++) {
root[i].children?.push(childs[j]);
}
if (!root[i].children?.length) {
delete root[i].children;
}
}
return root;
}
/** Check depth before the move. */
function beforeMove({
pathTo,
}: {
pathTo: number[];
}) {
if (pathTo.length == 4) {
return false;
}
return true;
}
/** Update data's directories. */
function updateOrderDirectories() {
let newOrder = assignOrder(intDirectories.value, null);
const routePath = route.methods.route("bo.directories.save-order");
if (!routePath) {
throw new Error("Undefined route bo.directories.save-order");
}
window.axios.post(routePath, { directories: newOrder }).catch(ajaxErrorHandler);
nextTick(() => {
updateBootstrapTooltip();
});
}
/** Assign the new order and parent id to each directory. */
function assignOrder(children: DirectoryObject[], parentId: number | null) {
children.forEach((element, index) => {
element.order = index;
element.directory_id = parentId;
if (element.children && element.children.length) {
element.children = assignOrder(element.children, element.id);
} else {
element.children = [];
}
});
return children;
}
/** Return edit route for a directory. */
function getEditDirectoryRoute(id: number): string {
const routePath = route.methods.route("bo.directories.edit", {
ID: id,
});
if (!routePath) {
throw new Error("Undefined route bo.directories.edit");
}
return routePath;
}
/** Return edit route for a directory. */
function getEditButtonRoute(id: number): string {
const routePath = route.methods.route("bo.settings.edit", {
ID: id,
});
if (!routePath) {
throw new Error("Undefined route bo.settings.edit");
}
return routePath;
}
/** Show/Hide children. */
function displayList(e: Event, id: number) {
intDisplay.value[id] = intDisplay.value[id] ? false : true;
document
.querySelector(".nestable-item-" + id + " .nestable-list")
?.classList.toggle("visually-hidden");
nextTick(() => {
updateBootstrapTooltip();
});
}
/** Update authorization for a specific directory. */
function updateAuthorization(model: LaravelModel) {
intLoading.value = true;
const routePath = route.methods.route("bo.directories.need-authorization", {
ID: model.id as number,
});
if (!routePath) {
throw new Error("Undefined route bo.directories.need-authorization");
}
window.axios
.post(routePath, { id: model.id })
.then((reponse) => {
model.needAuthorization = reponse.data.needAuthorization;
intLoading.value = false;
nextTick(() => {
updateBootstrapTooltip();
});
})
.catch(ajaxErrorHandler);
}
/** Update authorization for a specific directory. */
function deleteDirectory(e: Event, model: DirectoryObject) {
let promise:Promise<boolean>|null = null;
(async () => {
if (model.children?.length) {
promise = new Promise((resolve: (value: boolean) => void) => {
sweetalertConfirm.methods.confirm(
"Les dossiers enfants seront déplacés plus haut",
"Êtes vous sure ?",
confirmDelete,
function (response) {
resolve(response.isConfirmed);
},
{ icon: "warning" }
);
return false;
});
}
if (promise !== null && await promise === false) {
return;
}
if (model.documents_count > 0) {
promise = new Promise((resolve: (value: boolean) => void) => {
sweetalertConfirm.methods.confirm(
"Les document enfants seront SUPPRIMÉS",
"Êtes vous sure ?",
confirmDelete,
function (response) {
resolve(response.isConfirmed);
},
{ icon: "warning" }
);
return false;
});
}
if (promise !== null && await promise === false) {
return;
}
promise = new Promise((resolve: (value: boolean) => void) => {
sweetalertConfirm.methods.confirm(
"Confirmez",
"Êtes vous sure ?",
confirmDelete,
function (response) {
resolve(response.isConfirmed);
},
{ icon: "warning" }
);
return false;
});
if (await promise) {
intLoading.value = true;
const routePath = route.methods.route("bo.directories.destroy", {
ID: model.id as number,
});
if (!routePath) {
throw new Error("Undefined route bo.directories.destroy");
}
window.axios
.post(routePath, { id: model.id, _method: "DELETE" })
.then((reponse) => {
if (reponse.data[0] !== undefined) {
intDirectories.value = recursiveDir(reponse.data);
} else {
intMessage.value = reponse.data;
setTimeout(() => {
intMessage.value = null;
nextTick(() => {
updateBootstrapTooltip();
});
}, 6000);
}
intLoading.value = false;
nextTick(() => {
updateBootstrapTooltip();
});
})
.catch(ajaxErrorHandler);
}
})();
}
/** Update Bootstrap tooltips. */
function updateBootstrapTooltip() {
let newTooltipList = [].slice.call(
document.querySelectorAll(
".directories-" + intId.value + " [data-bs=\"tooltip\"]"
)
) as HTMLButtonElement[];
let tmp = newTooltipList.filter((x) => !intTooltipList.value.includes(x));
tmp.map((tooltip) => {
return new Tooltip(tooltip);
});
closeBootstrapTooltip();
intTooltipList.value = newTooltipList;
}
/** Close all Bootstrap tooltips. */
function closeBootstrapTooltip() {
intTooltipList.value.forEach((tooltip) => {
tooltip.blur();
Tooltip.getInstance(tooltip)?.hide();
});
}
function ajaxErrorHandler(e: any) {
intLoading.value = false;
let message = trans.methods.__("Une erreur est survenue");
if (e.response.status === 422) {
message =
parseLaravelErrorMessages.methods.parseValidationErrors(
e.response?.data?.errors ?? {}
) || message;
if (window.vueDebug) {
console.warn(e.response.data.errors);
}
} else if (e.response.status === 419) {
// * CSRF TOKEN MISMATCH (On ne pourrais plus faire d'appels AJAX sans recharger la page)
toast.methods.errorNotify(trans.methods.__("Votre session a expiré, la page va être rechargée"));
setTimeout(() => window.location.reload(), 2000);
} else if (e.response.status === 403) {
// * Access denied
toast.methods.errorNotify(trans.methods.__("Vous n'êtes pas autorisé a effectuer cette action"));
setTimeout(() => window.location.reload(), 2000);
} else {
if (window.vueDebug) {
console.error(e);
}
}
toast.methods.errorNotify(message);
}
</script>
<style lang="scss">
.loading {
background-color: rgba(0, 0, 0, 0.15);
z-index: 5;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>