Update various configuration files, components, and assets; enhance notification system and API endpoints; improve documentation and styles across the application.
This commit is contained in:
761
pages/devtool/menu-editor/index.vue
Normal file
761
pages/devtool/menu-editor/index.vue
Normal file
@@ -0,0 +1,761 @@
|
||||
<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: "",
|
||||
});
|
||||
// const showModalEditEl = ref(null);
|
||||
|
||||
const showModalAdd = ref(false);
|
||||
const showModalAddForm = ref({
|
||||
title: "",
|
||||
name: "",
|
||||
path: "",
|
||||
});
|
||||
|
||||
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)
|
||||
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 () => {
|
||||
// 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();
|
||||
|
||||
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) {
|
||||
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 = "";
|
||||
|
||||
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();
|
||||
|
||||
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) {
|
||||
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">
|
||||
<rs-button
|
||||
class="mr-2"
|
||||
@click="showCode ? (showCode = false) : (showCode = true)"
|
||||
>
|
||||
<Icon name="ic:baseline-code" class="mr-2"></Icon>
|
||||
{{ showCode ? "Hide" : "Show" }} JSON Code
|
||||
</rs-button>
|
||||
<rs-button @click="overwriteJsonFileLocal(sideMenuList)">
|
||||
<Icon name="mdi:content-save-outline" class="mr-2"></Icon>
|
||||
Save 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="Search 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" :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
|
||||
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>
|
||||
<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
|
||||
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>
|
||||
<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>
|
||||
Reference in New Issue
Block a user