2024-08-30 09:58:17 +08:00

660 lines
18 KiB
Vue

<script setup>
import Menu from "~/navigation/index.js";
definePageMeta({
title: "Menu Editor",
middleware: ["auth"],
requiresAuth: true,
});
const nuxtApp = useNuxtApp();
const sideMenuList = ref(Menu);
const router = useRouter();
const getRoutes = router.getRoutes();
const getNavigation = Menu ? ref(Menu) : ref([]);
const allMenus = [];
const showCode = ref(false);
let i = 1;
const searchInput = ref("");
const showModal = ref(false);
const showModalEl = ref(null);
const dropdownMenu = ref([]);
const dropdownMenuValue = ref(null);
const showModalEdit = ref(false);
const showModalEditPath = ref(null);
const showModalEditForm = ref({
title: "",
name: "",
path: "",
guardType: "",
});
// const showModalEditEl = ref(null);
const showModalAdd = ref(false);
const showModalAddForm = ref({
title: "",
name: "",
path: "",
});
const kebabtoTitle = (str) => {
if (!str) return str;
return str
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
};
// Sort the routes into menus
getRoutes.sort((a, b) => {
return a.path.localeCompare(b.path);
});
//----------------------------------------------------------------------------
//-------------------------FIRST CHILD TAB ITEM (END)-------------------------
//----------------------------------------------------------------------------
// Loop through the routes and add them to the menus
getRoutes.map((menu) => {
let visibleMenu = false;
// Check if the menu is visible
for (let i = 0; i < getNavigation.value.length; i++) {
if (getNavigation.value[i].child) {
for (let j = 0; j < getNavigation.value[i].child.length; j++) {
if (getNavigation.value[i].child[j].path === menu.path)
visibleMenu = true;
else if (getNavigation.value[i].child[j].child) {
for (
let k = 0;
k < getNavigation.value[i].child[j].child.length;
k++
) {
if (getNavigation.value[i].child[j].child[k].path === menu.path)
visibleMenu = true;
}
}
}
}
}
if (menu.name)
allMenus.push({
id: i++,
title:
menu.meta && menu.meta.title
? menu.meta.title
: kebabtoTitle(menu.name),
parentMenu: menu.path.split("/")[1],
name: menu.name,
path: menu.path,
visible: visibleMenu,
action: "",
});
});
const openModalEdit = (menu) => {
showModalEditForm.value.title = menu.title;
showModalEditForm.value.name = menu.name;
// If there is a slash in the beggining of the path, remove it
if (menu.path.charAt(0) === "/") {
showModalEditForm.value.path = menu.path.slice(1);
} else {
showModalEditForm.value.path = menu.path;
}
showModalEditPath.value = menu.path;
showModalEdit.value = true;
};
const saveEditMenu = async () => {
const res = await useFetch("/api/devtool/menu/edit", {
method: "POST",
initialCache: false,
body: JSON.stringify({
filePath: showModalEditPath.value,
formData: {
title: showModalEditForm.value.title || "",
name: showModalEditForm.value.name || "",
path: "/" + showModalEditForm.value.path || "",
},
// formData: showModalEditForm.value,
}),
});
const data = res.data.value;
if (data.statusCode === 200) {
nuxtApp.$swal.fire({
title: "Success",
text: data.message,
icon: "success",
timer: 2000,
showConfirmButton: false,
});
// refresh the page
nuxtApp.$router.go();
}
};
const openModalAdd = () => {
showModalAddForm.value.title = "";
showModalAddForm.value.name = "";
showModalAddForm.value.path = "";
showModalAdd.value = true;
};
const saveAddMenu = async () => {
const res = await useFetch("/api/devtool/menu/add", {
method: "POST",
initialCache: false,
body: JSON.stringify({
formData: {
title: showModalAddForm.value.title || "",
name: showModalAddForm.value.name || "",
path: "/" + showModalAddForm.value.path || "",
},
// formData: showModalAddForm.value
}),
});
const data = res.data.value;
if (data.statusCode === 200) {
nuxtApp.$swal.fire({
title: "Success",
text: data.message,
icon: "success",
timer: 2000,
showConfirmButton: false,
});
// refresh the page
nuxtApp.$router.go();
} else {
nuxtApp.$swal.fire({
title: "Error",
text: data.message,
icon: "error",
timer: 2000,
showConfirmButton: false,
});
}
};
const deleteMenu = async (menu) => {
nuxtApp.$swal
.fire({
title: "Are you sure?",
text: "You won't be able to revert this!",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "Yes, delete it!",
})
.then(async (result) => {
if (result.isConfirmed) {
const res = await useFetch("/api/devtool/menu/delete", {
method: "POST",
initialCache: false,
body: JSON.stringify({
filePath: menu.path,
}),
});
const data = res.data.value;
if (data.statusCode === 200) {
nuxtApp.$swal.fire({
title: "Deleted!",
text: data.message,
icon: "success",
timer: 2000,
showConfirmButton: false,
});
// refresh the page
nuxtApp.$router.go();
}
}
});
};
//----------------------------------------------------------------------------
//-------------------------FIRST CHILD TAB ITEM (END)-------------------------
//----------------------------------------------------------------------------
//-----------------------------------------------------------------------
//-------------------------SECOND CHILD TAB ITEM-------------------------
//-----------------------------------------------------------------------
const checkExistSideMenuList = (path) => {
let exist = false;
sideMenuList.value.map((menu) => {
// Search child path
if (menu.child) {
menu.child.map((child) => {
if (child.path == path) {
exist = true;
}
if (child.child) {
child.child.map((child2) => {
if (child2.path == path) {
exist = true;
}
});
}
});
}
});
return exist;
};
const menuList = computed(() => {
// If the search input is empty, return all menus
if (searchInput.value === "") {
return allMenus;
} else {
// If the search input is not empty, filter the menus
return allMenus.filter((menu) => {
return menu.name.toLowerCase().includes(searchInput.value.toLowerCase());
});
}
});
// Clone draggable item
const clone = (obj) => {
return JSON.parse(
JSON.stringify({
title: obj.title,
path: obj.path,
icon: obj.icon ? obj.icon : "",
child: [],
})
);
};
// Add Header
const addNewHeader = () => {
// Push index = 1
sideMenuList.value.splice(1, 0, {
header: "New Header",
description: "New Description",
child: [],
});
};
// changeSideMenuList
const changeSideMenuList = (menus) => {
sideMenuList.value = menus;
};
// Save the menu
const overwriteJsonFileLocal = async (menus) => {
const res = await useFetch("/api/devtool/menu/overwrite-navigation", {
method: "POST",
initialCache: false,
body: JSON.stringify({
menuData: menus,
}),
});
const data = res.data.value;
if (data.statusCode === 200) {
nuxtApp.$swal.fire({
title: "Success",
text: data.message,
icon: "success",
timer: 2000,
showConfirmButton: false,
});
// refresh the page
nuxtApp.$router.go();
}
};
// open modal
const openModal = (menu) => {
showModalEl.value = menu;
// Get All Menu includes child and assign to dropdownMenu in one array
let i = 0;
dropdownMenu.value = [
{
label: "Choose Menu",
value: null,
attrs: {
disabled: true,
},
},
];
sideMenuList.value.map((menu) => {
if (menu.header || menu.description) {
dropdownMenu.value.push({
label: `${menu.header} (Header)`,
value: `header|${i}`,
});
} else if (menu.hasOwnProperty("header")) {
dropdownMenu.value.push({
label: `<No Header Name> (Header)`,
value: `header|${i}`,
});
}
if (menu.child) {
menu.child.map((child) => {
dropdownMenu.value.push({
label: `${child.title} (Menu)`,
value: `menu|${child.path}`,
});
});
}
i++;
});
showModal.value = true;
};
// Add new menu from list
const addMenuFromList = () => {
if (dropdownMenuValue.value) {
const menuType = dropdownMenuValue.value.split("|")[0];
const menuValue = dropdownMenuValue.value.split("|")[1];
if (menuType === "header") {
// Add Header
sideMenuList.value[menuValue].child.push(clone(showModalEl.value));
} else if (menuType === "menu") {
// Add Menu
sideMenuList.value.map((menu) => {
if (menu.child) {
menu.child.map((child) => {
if (child.path == menuValue) {
child.child.push(clone(showModalEl.value));
}
});
}
});
}
showModal.value = false;
}
};
//-----------------------------------------------------------------------------
//-------------------------SECOND CHILD TAB ITEM (END)-------------------------
//-----------------------------------------------------------------------------
</script>
<template>
<div>
<LayoutsBreadcrumb />
<rs-card>
<template #header>
<div class="flex">
<Icon class="mr-2 flex justify-center" name="ic:outline-info"></Icon
>Maklumat
</div>
</template>
<template #body>
<p class="mb-4">
Halaman ini digunakan untuk mengedit menu laman web. Anda boleh
menambah, mengedit, dan memadam item menu. Anda juga boleh mengubah
susunan item menu dengan menyeret dan melepaskannya.
</p>
</template>
</rs-card>
<rs-card>
<div class="pt-2">
<rs-tab fill>
<rs-tab-item title="Semua Menu">
<div class="flex justify-end items-center mb-4">
<rs-button @click="openModalAdd">
<Icon name="material-symbols:add" class="mr-1"></Icon>
Tambah Menu
</rs-button>
</div>
<!-- Table All Menu -->
<rs-table
:data="allMenus"
:options="{
variant: 'default',
striped: true,
borderless: true,
}"
:options-advanced="{
sortable: true,
filterable: false,
responsive: false,
}"
advanced
>
<template v-slot:name="data">
<NuxtLink
class="text-primary hover:underline"
:to="data.value.path"
target="_blank"
>{{ data.text }}</NuxtLink
>
</template>
<template v-slot:visible="data">
<div class="flex items-center">
<Icon
name="mdi:eye-outline"
class="text-primary"
size="22"
v-if="data.value.visible"
/>
<Icon
name="mdi:eye-off-outline"
class="text-primary/20"
size="22"
v-else
/>
</div>
</template>
<template v-slot:action="data">
<div
class="flex items-center"
v-if="data.value.parentMenu != 'admin'"
>
<Icon
name="material-symbols:edit-outline-rounded"
class="text-primary hover:text-primary/90 cursor-pointer mr-1"
size="22"
@click="openModalEdit(data.value)"
></Icon>
<Icon
name="material-symbols:close-rounded"
class="text-primary hover:text-primary/90 cursor-pointer"
size="22"
@click="deleteMenu(data.value)"
></Icon>
</div>
<div class="flex items-center" v-else>-</div>
</template>
</rs-table>
</rs-tab-item>
<rs-tab-item title="Urus Menu Sisi">
<div class="flex justify-end items-center mb-4">
<rs-button
class="mr-2"
@click="showCode ? (showCode = false) : (showCode = true)"
>
<Icon name="ic:baseline-code" class="mr-2"></Icon>
{{ showCode ? "Sembunyikan" : "Tunjukkan" }} Kod JSON
</rs-button>
<rs-button @click="overwriteJsonFileLocal(sideMenuList)">
<Icon name="mdi:content-save-outline" class="mr-2"></Icon>
Simpan Menu
</rs-button>
</div>
<div class="grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 gap-5">
<div>
<FormKit
type="search"
placeholder="Cari Menu..."
outer-class="mb-5"
v-model="searchInput"
/>
<NuxtScrollbar
style="height: 735px"
class="px-5 pt-5 border border-[rgb(var(--border-color))] bg-[rgb(var(--bg-1))] rounded-md"
>
<draggable
item-key="id"
v-model="menuList"
:group="{ name: 'menu', pull: 'clone', put: false }"
:clone="clone"
:sort="false"
>
<template #item="{ element }">
<rs-card
class="p-4 mb-4 border-2 border-[rgb(var(--border-color))] !shadow-none"
>
<div class="flex justify-between items-center">
<p>
{{ kebabtoTitle(element.name) }} (
<NuxtLink
class="text-primary hover:underline"
:to="element.path"
target="_blank"
>
{{ element.path }}
</NuxtLink>
)
</p>
<Icon
v-if="checkExistSideMenuList(element.path) == false"
name="ic:baseline-arrow-circle-right"
class="text-primary cursor-pointer transition-all duration-150 hover:scale-110"
@click="openModal(element)"
></Icon>
</div>
</rs-card>
</template>
</draggable>
</NuxtScrollbar>
</div>
<NuxtScrollbar v-if="!showCode" style="height: 825px">
<rs-card
class="p-4 border border-[rgb(var(--border-color))] bg-[rgb(var(--bg-1))] rounded-md"
>
<div class="flex justify-end items-center">
<rs-button
class="!p-2 mt-3 justify-center items-center"
@click="addNewHeader"
>
<Icon
class="mr-1"
name="material-symbols:docs-add-on"
size="18"
></Icon>
Add Header
</rs-button>
</div>
<DraggableSideMenuNested
:menus="sideMenuList"
@changeSideMenu="changeSideMenuList"
/>
</rs-card>
</NuxtScrollbar>
<pre v-else v-html="JSON.stringify(sideMenuList, null, 2)"></pre>
</div>
</rs-tab-item>
</rs-tab>
</div>
</rs-card>
<rs-modal
title="Select Menu"
v-model="showModal"
ok-title="Confirm"
:ok-callback="addMenuFromList"
>
<FormKit
label="Please Select Menu or Header"
help="Select menu or header to add as their child menu"
type="select"
v-model="dropdownMenuValue"
:options="dropdownMenu"
></FormKit>
</rs-modal>
<rs-modal
title="Edit Menu"
v-model="showModalEdit"
ok-title="Save"
:ok-callback="saveEditMenu"
>
<FormKit
type="text"
label="Title"
v-model="showModalEditForm.title"
></FormKit>
<FormKit
type="text"
label="Name"
v-model="showModalEditForm.name"
></FormKit>
<FormKit
type="text"
label="Path"
help="If the last path name is '/', the name of the file will be from its name property. While if the last path name is not '/', the name of the file will be from its path property."
v-model="showModalEditForm.path"
>
<template #prefix>
<div class="bg-slate-100 h-full rounded-l-md p-3">/</div>
</template>
</FormKit>
</rs-modal>
<rs-modal
title="Add Menu"
v-model="showModalAdd"
ok-title="Save"
:ok-callback="saveAddMenu"
>
<FormKit
type="text"
label="Title"
v-model="showModalAddForm.title"
></FormKit>
<FormKit
type="text"
label="Name"
v-model="showModalAddForm.name"
></FormKit>
<FormKit
type="text"
label="Path"
help="If the last path name is '/', the name of the file will be from its name property. While if the last path name is not '/', the name of the file will be from its path property."
v-model="showModalAddForm.path"
>
<template #prefix>
<div class="bg-slate-100 dark:bg-slate-700 h-full rounded-l-md p-3">
/
</div>
</template>
</FormKit>
</rs-modal>
</div>
</template>