660 lines
18 KiB
Vue
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>
|