- Added new variables for 'namaAsnaf', 'todoStatus', 'asnafScore', 'resultTimestamp', and 'resultSummary' to improve data handling in process definitions. - Updated process definition JSON to include new output mappings and enhanced script logic for better API response handling. - Implemented validation for external menu paths in the menu editor, ensuring proper URL formatting and handling for external links. - Enhanced the user interface in the menu editor to support external URL inputs, improving user experience when adding or editing menu items. - Updated API endpoints to skip file operations for external menus, streamlining the process of adding and editing external links.
894 lines
26 KiB
Vue
894 lines
26 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 = reactive([]);
|
|
|
|
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: "",
|
|
external: false,
|
|
});
|
|
// const showModalEditEl = ref(null);
|
|
|
|
const showModalAdd = ref(false);
|
|
const showModalAddForm = ref({
|
|
title: "",
|
|
name: "",
|
|
path: "",
|
|
external: false,
|
|
});
|
|
|
|
const systemPages = [
|
|
"/devtool",
|
|
"/dashboard",
|
|
"/login",
|
|
"/logout",
|
|
"/register",
|
|
"/reset-password",
|
|
"/forgot-password",
|
|
];
|
|
|
|
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 && !menu.path.startsWith("/devtool") && menu.path !== "/")
|
|
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;
|
|
}
|
|
|
|
showModalEditForm.value.external = menu.external === true;
|
|
showModalEditPath.value = menu.path;
|
|
|
|
showModalEdit.value = true;
|
|
};
|
|
|
|
const saveEditMenu = async () => {
|
|
// Check title regex to ensure no weird symbol only letters, numbers, spaces, underscores and dashes
|
|
if (!/^[a-zA-Z0-9\s_-]+$/.test(showModalEditForm.value.title)) {
|
|
nuxtApp.$swal.fire({
|
|
title: "Error",
|
|
text: "Title contains invalid characters. Only letters, numbers, spaces, underscores and dashes are allowed.",
|
|
icon: "error",
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Clean the name and title ensure not spacing at the beginning or end
|
|
showModalEditForm.value.title = showModalEditForm.value.title.trim();
|
|
showModalEditForm.value.name = showModalEditForm.value.name.trim();
|
|
|
|
// Path validation and formatting
|
|
let path = showModalEditForm.value.path.trim();
|
|
if (showModalEditForm.value.external) {
|
|
// Validate as URL
|
|
if (!/^https?:\/\//.test(path)) {
|
|
nuxtApp.$swal.fire({
|
|
title: "Error",
|
|
text: "External URL must start with http:// or https://",
|
|
icon: "error",
|
|
});
|
|
return;
|
|
}
|
|
} else {
|
|
// Internal path validation
|
|
if (!/^[a-z0-9/-]+$/.test(path)) {
|
|
nuxtApp.$swal.fire({
|
|
title: "Error",
|
|
text: "Path contains invalid characters or spacing before or after. Only letters, numbers, dashes, and underscores are allowed.",
|
|
icon: "error",
|
|
});
|
|
return;
|
|
}
|
|
if (!path.startsWith("/")) {
|
|
path = "/" + path;
|
|
}
|
|
}
|
|
|
|
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: path || "",
|
|
external: showModalEditForm.value.external === true,
|
|
},
|
|
}),
|
|
});
|
|
|
|
const data = res.data.value;
|
|
|
|
if (data.statusCode === 200) {
|
|
showModalEdit.value = false;
|
|
|
|
nuxtApp.$swal.fire({
|
|
title: "Success",
|
|
text: data.message,
|
|
icon: "success",
|
|
timer: 2000,
|
|
showConfirmButton: false,
|
|
});
|
|
|
|
window.location.reload();
|
|
}
|
|
};
|
|
|
|
const openModalAdd = () => {
|
|
showModalAddForm.value.title = "";
|
|
showModalAddForm.value.name = "";
|
|
showModalAddForm.value.path = "";
|
|
showModalAddForm.value.external = false;
|
|
|
|
showModalAdd.value = true;
|
|
};
|
|
|
|
const saveAddMenu = async () => {
|
|
// Check title regex to ensure no weird symbol only letters, numbers, spaces, underscores and dashes
|
|
if (!/^[a-zA-Z0-9\s_-]+$/.test(showModalAddForm.value.title)) {
|
|
nuxtApp.$swal.fire({
|
|
title: "Error",
|
|
text: "Title contains invalid characters. Only letters, numbers, spaces, underscores and dashes are allowed.",
|
|
icon: "error",
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Clean the name and title ensure not spacing at the beginning or end
|
|
showModalAddForm.value.title = showModalAddForm.value.title.trim();
|
|
showModalAddForm.value.name = showModalAddForm.value.name.trim();
|
|
|
|
// Path validation and formatting
|
|
let path = showModalAddForm.value.path.trim();
|
|
if (showModalAddForm.value.external) {
|
|
// Validate as URL
|
|
if (!/^https?:\/\//.test(path)) {
|
|
nuxtApp.$swal.fire({
|
|
title: "Error",
|
|
text: "External URL must start with http:// or https://",
|
|
icon: "error",
|
|
});
|
|
return;
|
|
}
|
|
} else {
|
|
// Internal path validation
|
|
if (!/^[a-z0-9/-]+$/.test(path)) {
|
|
nuxtApp.$swal.fire({
|
|
title: "Error",
|
|
text: "Path contains invalid characters or spacing before or after. Only letters, numbers, dashes, and underscores are allowed.",
|
|
icon: "error",
|
|
});
|
|
return;
|
|
}
|
|
if (!path.startsWith("/")) {
|
|
path = "/" + path;
|
|
}
|
|
}
|
|
|
|
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: path || "",
|
|
external: showModalAddForm.value.external === true,
|
|
},
|
|
}),
|
|
});
|
|
|
|
const data = res.data.value;
|
|
|
|
if (data.statusCode === 200) {
|
|
showModalAdd.value = false;
|
|
|
|
nuxtApp.$swal.fire({
|
|
title: "Success",
|
|
text: data.message,
|
|
icon: "success",
|
|
timer: 2000,
|
|
showConfirmButton: false,
|
|
});
|
|
|
|
window.location.reload();
|
|
} 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,
|
|
});
|
|
|
|
window.location.reload();
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
//----------------------------------------------------------------------------
|
|
//-------------------------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,
|
|
});
|
|
}
|
|
};
|
|
|
|
// 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)-------------------------
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// Add this watcher after the showModalEditForm ref declaration
|
|
watch(
|
|
() => showModalEditForm.value,
|
|
(newTitle) => {
|
|
showModalEditForm.value.name = newTitle.toLowerCase().replace(/\s+/g, "-");
|
|
}
|
|
);
|
|
|
|
watch(
|
|
() => showModalAddForm.value.title,
|
|
(newTitle) => {
|
|
showModalAddForm.value.name = newTitle.toLowerCase().replace(/\s+/g, "-");
|
|
}
|
|
);
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<LayoutsBreadcrumb />
|
|
|
|
<rs-card>
|
|
<template #header>
|
|
<div class="flex">
|
|
<Icon class="mr-2 flex justify-center" name="ic:outline-info"></Icon
|
|
>Info
|
|
</div>
|
|
</template>
|
|
<template #body>
|
|
<p class="mb-4">
|
|
This page is used to edit the menu of the website. You can add, edit,
|
|
and delete menu items. You can also change the order of the menu items
|
|
by dragging and dropping them.
|
|
</p>
|
|
</template>
|
|
</rs-card>
|
|
|
|
<rs-card>
|
|
<div class="pt-2">
|
|
<rs-tab fill>
|
|
<rs-tab-item title="All Menu">
|
|
<div class="flex justify-end items-center mb-4">
|
|
<rs-button @click="openModalAdd">
|
|
<Icon name="material-symbols:add" class="mr-1"></Icon>
|
|
Add 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-blue-700 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">
|
|
<template
|
|
v-if="
|
|
!systemPages.some((path) =>
|
|
data.value.path.startsWith(path)
|
|
) && 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>
|
|
</template>
|
|
<div v-else class="text-gray-400">-</div>
|
|
</div>
|
|
</template>
|
|
</rs-table>
|
|
</rs-tab-item>
|
|
<rs-tab-item title="Manage Side Menu">
|
|
<div class="flex justify-end items-center mb-4 gap-2">
|
|
<rs-button variant="outline" @click="addNewHeader">
|
|
<Icon name="material-symbols:docs-add-on" class="mr-2"></Icon>
|
|
Add Header Section
|
|
</rs-button>
|
|
<rs-button
|
|
variant="outline"
|
|
@click="showCode ? (showCode = false) : (showCode = true)"
|
|
>
|
|
<Icon name="ic:baseline-code" class="mr-2"></Icon>
|
|
{{ showCode ? "Hide" : "Show" }} JSON
|
|
</rs-button>
|
|
<rs-button @click="overwriteJsonFileLocal(sideMenuList)">
|
|
<Icon name="mdi:content-save-outline" class="mr-2"></Icon>
|
|
Save Changes
|
|
</rs-button>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 gap-5">
|
|
<!-- Available Menus Section -->
|
|
<div>
|
|
<div class="mb-4">
|
|
<h3 class="text-lg font-semibold mb-2">
|
|
Available Menu Items
|
|
</h3>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
Drag items from here to build your navigation menu
|
|
</p>
|
|
</div>
|
|
<FormKit
|
|
type="search"
|
|
placeholder="Search available menu items..."
|
|
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 hover:border-primary/50 transition-colors"
|
|
>
|
|
<div class="flex justify-between items-center">
|
|
<div>
|
|
<p class="font-medium">
|
|
{{ kebabtoTitle(element.name) }}
|
|
</p>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
{{ element.path }}
|
|
</p>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<Icon
|
|
v-if="!checkExistSideMenuList(element.path)"
|
|
name="material-symbols:add-circle-outline"
|
|
class="text-primary cursor-pointer transition-all duration-150 hover:scale-110"
|
|
@click="openModal(element)"
|
|
size="24"
|
|
>
|
|
<span class="sr-only">Add to menu</span>
|
|
</Icon>
|
|
<Icon
|
|
name="material-symbols:drag-indicator"
|
|
class="text-gray-400"
|
|
size="24"
|
|
>
|
|
<span class="sr-only">Drag to add</span>
|
|
</Icon>
|
|
</div>
|
|
</div>
|
|
</rs-card>
|
|
</template>
|
|
</draggable>
|
|
</NuxtScrollbar>
|
|
</div>
|
|
|
|
<!-- Menu Structure Section -->
|
|
<div>
|
|
<div class="mb-4">
|
|
<h3 class="text-lg font-semibold mb-2">Menu Structure</h3>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
Organize your navigation menu by dragging items and sections
|
|
</p>
|
|
</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"
|
|
>
|
|
<DraggableSideMenuNested
|
|
:menus="sideMenuList"
|
|
@changeSideMenu="changeSideMenuList"
|
|
/>
|
|
</rs-card>
|
|
</NuxtScrollbar>
|
|
<pre
|
|
v-else
|
|
class="p-4 bg-gray-100 dark:bg-gray-800 rounded-md overflow-auto"
|
|
v-html="JSON.stringify(sideMenuList, null, 2)"
|
|
></pre>
|
|
</div>
|
|
</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" :overlay-close="false">
|
|
<template #body>
|
|
<FormKit type="form" :actions="false" @submit="saveEditMenu">
|
|
<FormKit
|
|
type="text"
|
|
label="Title"
|
|
:validation="[['required']]"
|
|
:validation-messages="{
|
|
required: 'Title is required',
|
|
}"
|
|
v-model="showModalEditForm.title"
|
|
/>
|
|
<FormKit
|
|
v-if="!showModalEditForm.external"
|
|
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."
|
|
:validation="[['required'], ['matches', '/^[a-z0-9/-]+$/']]"
|
|
:validation-messages="{
|
|
required: 'Path is required',
|
|
matches:
|
|
'Path contains invalid characters or spacing before or after. Only letters, numbers, dashes, and underscores are allowed.',
|
|
}"
|
|
v-model="showModalEditForm.path"
|
|
>
|
|
<template #prefix>
|
|
<div
|
|
class="bg-slate-100 dark:bg-slate-700 h-full rounded-l-md p-3"
|
|
>
|
|
/
|
|
</div>
|
|
</template>
|
|
</FormKit>
|
|
<FormKit
|
|
v-else
|
|
type="text"
|
|
label="External URL"
|
|
help="Enter a full URL (e.g., https://example.com)"
|
|
:validation="[['required'], ['matches', '/^https?:\/\//']]"
|
|
:validation-messages="{
|
|
required: 'URL is required',
|
|
matches: 'URL must start with http:// or https://',
|
|
}"
|
|
v-model="showModalEditForm.path"
|
|
/>
|
|
<FormKit
|
|
type="checkbox"
|
|
label="Is External URL?"
|
|
v-model="showModalEditForm.external"
|
|
help="Check if this menu item is an external URL."
|
|
/>
|
|
<div class="flex justify-end gap-2">
|
|
<rs-button variant="outline" @click="showModalEdit = false">
|
|
Cancel
|
|
</rs-button>
|
|
<rs-button btnType="submit">
|
|
<Icon
|
|
name="material-symbols:save-outline"
|
|
class="mr-2 !w-4 !h-4"
|
|
/>
|
|
Save Changes
|
|
</rs-button>
|
|
</div>
|
|
</FormKit>
|
|
</template>
|
|
|
|
<template #footer> </template>
|
|
</rs-modal>
|
|
|
|
<rs-modal title="Add Menu" v-model="showModalAdd" :overlay-close="false">
|
|
<template #body>
|
|
<FormKit type="form" :actions="false" @submit="saveAddMenu">
|
|
<FormKit
|
|
type="text"
|
|
label="Title"
|
|
:validation="[['required']]"
|
|
:validation-messages="{
|
|
required: 'Title is required',
|
|
}"
|
|
v-model="showModalAddForm.title"
|
|
/>
|
|
<FormKit
|
|
v-if="!showModalAddForm.external"
|
|
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."
|
|
:validation="[['required'], ['matches', '/^[a-z0-9/-]+$/']]"
|
|
:validation-messages="{
|
|
required: 'Path is required',
|
|
matches:
|
|
'Path contains invalid characters or spacing before or after. Only letters, numbers, dashes, and underscores are allowed.',
|
|
}"
|
|
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>
|
|
<FormKit
|
|
v-else
|
|
type="text"
|
|
label="External URL"
|
|
help="Enter a full URL (e.g., https://example.com)"
|
|
:validation="[['required'], ['matches', '/^https?:\/\//']]"
|
|
:validation-messages="{
|
|
required: 'URL is required',
|
|
matches: 'URL must start with http:// or https://',
|
|
}"
|
|
v-model="showModalAddForm.path"
|
|
/>
|
|
<FormKit
|
|
type="checkbox"
|
|
label="Is External URL?"
|
|
v-model="showModalAddForm.external"
|
|
help="Check if this menu item is an external URL."
|
|
/>
|
|
<div class="flex justify-end gap-2">
|
|
<rs-button variant="outline" @click="showModalAdd = false">
|
|
Cancel
|
|
</rs-button>
|
|
<rs-button btnType="submit">
|
|
<Icon
|
|
name="material-symbols:add-circle-outline-rounded"
|
|
class="mr-2 !w-4 !h-4"
|
|
/>
|
|
Add Menu
|
|
</rs-button>
|
|
</div>
|
|
</FormKit>
|
|
</template>
|
|
|
|
<template #footer> </template>
|
|
</rs-modal>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.draggable-item {
|
|
cursor: move;
|
|
}
|
|
|
|
.ghost {
|
|
opacity: 0.5;
|
|
background: #c8ebfb;
|
|
}
|
|
|
|
.drag-handle {
|
|
cursor: move;
|
|
}
|
|
</style>
|