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:
Haqeem Solehan
2025-10-16 16:05:39 +08:00
commit b124ff8092
336 changed files with 94392 additions and 0 deletions

View File

@@ -0,0 +1,244 @@
<script setup>
// import pinia store
import { useThemeStore } from "~/stores/theme";
definePageMeta({
title: "API Code Editor",
middleware: ["auth"],
requiresAuth: true,
});
const { $swal, $router } = useNuxtApp();
const route = useRoute();
const router = useRouter();
const fileCode = ref("");
const fileCodeConstant = ref("");
const componentKey = ref(0);
const hasError = ref(false);
const error = ref("");
const themeStore = useThemeStore();
const editorTheme = ref({
label: themeStore.codeTheme,
value: themeStore.codeTheme,
});
const dropdownThemes = ref([]);
const linterError = ref(false);
const linterErrorText = ref("");
const linterErrorColumn = ref(0);
const linterErrorLine = ref(0);
// Add new ref for loading state
const isLinterChecking = ref(false);
// Get all themes
const themes = codemirrorThemes();
// map the themes to the dropdown
dropdownThemes.value = themes.map((theme) => {
return {
label: theme.name,
value: theme.name,
};
});
// watch for changes in the theme
watch(editorTheme, (theme) => {
themeStore.setCodeTheme(theme.value);
forceRerender();
});
// Call API to get the code
const { data } = await useFetch("/api/devtool/api/file-code", {
initialCache: false,
method: "GET",
query: {
path: route.query?.path,
},
});
if (data.value.statusCode === 200) {
fileCode.value = data.value.data;
fileCodeConstant.value = data.value.data;
} else {
$swal
.fire({
title: "Error",
text: "The API you are trying to edit is not found. Please choose a API to edit.",
icon: "error",
confirmButtonText: "Ok",
})
.then(async (result) => {
if (result.isConfirmed) {
await $router.push("/devtool/api-editor");
}
});
}
async function formatCode() {
// Call API to get the code
const { data } = await useFetch("/api/devtool/api/prettier-format", {
initialCache: false,
method: "POST",
body: JSON.stringify({
code: fileCode.value,
}),
});
forceRerender();
if (data.value.statusCode === 200) {
fileCode.value = data.value.data;
}
}
async function checkLinterVue() {
isLinterChecking.value = true;
try {
// Call API to get the code
const { data } = await useFetch("/api/devtool/api/linter", {
initialCache: false,
method: "POST",
body: JSON.stringify({
code: fileCode.value,
}),
});
if (data.value.statusCode === 200) {
linterError.value = false;
linterErrorText.value = "";
linterErrorColumn.value = 0;
linterErrorLine.value = 0;
} else if (data.value.statusCode === 400) {
linterError.value = true;
linterErrorText.value = data.value.data.message;
linterErrorColumn.value = data.value.data.column;
linterErrorLine.value = data.value.data.line;
}
} finally {
isLinterChecking.value = false;
}
}
const forceRerender = () => {
componentKey.value += 1;
};
const keyPress = (key) => {
console.log(key);
const event = new KeyboardEvent("keydown", {
key: key,
ctrlKey: true,
});
console.log(event);
document.dispatchEvent(event);
};
const saveCode = async () => {
// Check Linter Vue
await checkLinterVue();
if (linterError.value) {
$swal.fire({
title: "Error",
text: "There is an error in your code. Please fix it before saving.",
icon: "error",
confirmButtonText: "Ok",
});
return;
}
const { data } = await useFetch("/api/devtool/api/save", {
initialCache: false,
method: "POST",
body: {
path: route.query?.path,
code: fileCode.value,
type: "update",
},
});
if (data.value.statusCode === 200) {
$swal.fire({
title: "Success",
text: "The code has been saved successfully.",
icon: "success",
confirmButtonText: "Ok",
timer: 1000,
});
setTimeout(() => {
$router.go();
}, 1000);
}
};
</script>
<template>
<div>
<LayoutsBreadcrumb />
<rs-alert v-if="hasError" class="mb-4" variant="danger">{{
error
}}</rs-alert>
<rs-card>
<rs-tab fill>
<rs-tab-item title="Editor">
<div class="flex justify-end gap-2 mb-4">
<rs-button
class="!p-2"
@click="saveCode"
:disabled="isLinterChecking"
>
<div class="flex items-center">
<Icon
v-if="!isLinterChecking"
name="material-symbols:save-outline-rounded"
size="20px"
class="mr-1"
/>
<Icon
v-else
name="eos-icons:loading"
size="20px"
class="mr-1 animate-spin"
/>
{{ isLinterChecking ? "Checking..." : "Save API" }}
</div>
</rs-button>
</div>
<Transition>
<rs-alert v-if="linterError" variant="danger" class="mb-4">
<div class="flex gap-2">
<Icon
name="material-symbols:error-outline-rounded"
size="20px"
/>
<div>
<div class="font-bold">ESLint Error</div>
<div class="text-sm">
{{ linterErrorText }}
</div>
<div class="text-xs mt-2">
Line: {{ linterErrorLine }} Column: {{ linterErrorColumn }}
</div>
</div>
</div>
</rs-alert>
</Transition>
<rs-code-mirror
:key="componentKey"
v-model="fileCode"
mode="javascript"
/>
</rs-tab-item>
<rs-tab-item title="API Tester">
<rs-api-tester :url="route.query?.path" />
</rs-tab-item>
</rs-tab>
</rs-card>
</div>
</template>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,327 @@
<script setup>
definePageMeta({
title: "API Editor",
middleware: ["auth"],
requiresAuth: true,
});
const nuxtApp = useNuxtApp();
const searchText = ref("");
const showModalAdd = ref(false);
const showModalAddForm = ref({
apiURL: "",
});
const showModalEdit = ref(false);
const showModalEditForm = ref({
apiURL: "",
oldApiURL: "",
});
const openModalAdd = () => {
showModalAddForm.value = {
apiURL: "",
method: "all",
};
showModalAdd.value = true;
};
const openModalEdit = (url, method = "all") => {
const apiURL = url.replace("/api/", "");
showModalEditForm.value = {
apiURL: apiURL,
oldApiURL: apiURL,
method: method,
};
showModalEdit.value = true;
};
const { data: apiList, refresh } = await useFetch("/api/devtool/api/list");
const searchApi = () => {
if (!apiList.value || !apiList.value.data) return [];
return apiList.value.data.filter((api) => {
return (
api.name.toLowerCase().includes(searchText.value.toLowerCase()) ||
api.url.toLowerCase().includes(searchText.value.toLowerCase())
);
});
};
const kebabCasetoTitleCase = (str) => {
return str
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
};
const redirectToApiCode = (api) => {
window.location.href = `/devtool/api-editor/code?path=${api}`;
};
const saveAddAPI = async () => {
const { data } = await useFetch("/api/devtool/api/save", {
initialCache: false,
method: "POST",
body: {
path: "/api/" + showModalAddForm.value.apiURL,
type: "add",
},
});
if (data.value.statusCode === 200) {
nuxtApp.$swal.fire({
title: "Success",
text: "The code has been saved successfully.",
icon: "success",
confirmButtonText: "Ok",
timer: 1000,
});
// Close modal and refresh list
showModalAdd.value = false;
refresh();
}
};
const saveEditAPI = async () => {
const { data } = await useFetch("/api/devtool/api/save", {
initialCache: false,
method: "POST",
body: {
path: "/api/" + showModalEditForm.value.apiURL,
oldPath: "/api/" + showModalEditForm.value.oldApiURL,
type: "edit",
},
});
if (data.value.statusCode === 200) {
nuxtApp.$swal.fire({
title: "Success",
text: "The code has been saved successfully.",
icon: "success",
confirmButtonText: "Ok",
timer: 1000,
});
// Close modal and refresh list
showModalEdit.value = false;
refresh();
}
};
const deleteAPI = async (apiURL) => {
nuxtApp.$swal
.fire({
title: "Are you sure to delete this API?",
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 { data } = await useFetch("/api/devtool/api/save", {
initialCache: false,
method: "POST",
body: {
path: apiURL,
type: "delete",
},
});
if (data.value.statusCode === 200) {
nuxtApp.$swal.fire({
title: "Success",
text: "The code has been saved successfully.",
icon: "success",
confirmButtonText: "Ok",
timer: 1000,
});
// Refresh list after deletion
refresh();
}
}
});
};
</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 api for the server side. You can edit
the api by choosing the api to edit from the card list below.
</p>
</template>
</rs-card>
<rs-card>
<div class="p-4">
<div class="flex justify-end items-center mb-4">
<rs-button @click="openModalAdd">
<Icon name="material-symbols:add" class="mr-1"></Icon>
Add API
</rs-button>
</div>
<!-- Search Button -->
<FormKit
v-model="searchText"
placeholder="Search Title..."
type="search"
class="mb-4"
/>
<div v-auto-animate>
<div
class="shadow-md shadow-black/5 ring-1 ring-slate-700/10 rounded-lg mb-4"
v-for="api in searchApi()"
>
<div class="relative p-4 border-l-8 border-primary rounded-lg">
<div class="flex justify-between items-center">
<div class="block">
<span class="font-semibold text-lg">{{
kebabCasetoTitleCase(api.name)
}}</span>
<br />
<span class=""> {{ api.url }}</span>
</div>
<div class="flex gap-4">
<rs-button
variant="primary-outline"
@click="redirectToApiCode(api.url)"
>
<Icon
name="material-symbols:code-blocks-outline-rounded"
class="mr-2"
/>
Code Editor
</rs-button>
<div class="flex gap-2">
<rs-button @click="openModalEdit(api.url)">
<Icon name="material-symbols:edit-outline-rounded" />
</rs-button>
<rs-button @click="deleteAPI(api.url)">
<Icon name="carbon:trash-can" />
</rs-button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</rs-card>
<rs-modal title="Add API" v-model="showModalAdd" :overlay-close="false">
<template #body>
<FormKit type="form" :actions="false" @submit="saveAddAPI">
<FormKit
type="text"
label="URL"
:validation="[['required'], ['matches', '/^[a-z0-9/-]+$/']]"
:validation-messages="{
required: 'URL is required',
matches:
'URL contains invalid characters. Only letters, numbers, dashes, and forward slashes are allowed.',
}"
v-model="showModalAddForm.apiURL"
>
<template #prefix>
<div
class="bg-slate-100 dark:bg-slate-700 h-full rounded-l-md p-3"
>
/api/
</div>
</template>
</FormKit>
<!-- <FormKit
type="select"
label="Request Method"
:options="requestMethods"
validation="required"
placeholder="Select a method"
v-model="showModalAddForm.method"
/> -->
<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:save-outline"
class="mr-2 !w-4 !h-4"
/>
Save
</rs-button>
</div>
</FormKit>
</template>
<template #footer></template>
</rs-modal>
<rs-modal title="Edit API" v-model="showModalEdit" :overlay-close="false">
<template #body>
<FormKit type="form" :actions="false" @submit="saveEditAPI">
<FormKit
type="text"
label="URL"
:validation="[['required'], ['matches', '/^[a-z0-9/-]+$/']]"
:validation-messages="{
required: 'URL is required',
matches:
'URL contains invalid characters. Only letters, numbers, dashes, and forward slashes are allowed.',
}"
v-model="showModalEditForm.apiURL"
>
<template #prefix>
<div
class="bg-slate-100 dark:bg-slate-700 h-full rounded-l-md p-3"
>
/api/
</div>
</template>
</FormKit>
<!-- <FormKit
type="select"
label="Request Method"
:options="requestMethods"
validation="required"
placeholder="Select a method"
v-model="showModalEditForm.method"
/> -->
<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
</rs-button>
</div>
</FormKit>
</template>
<template #footer></template>
</rs-modal>
</div>
</template>

View File

@@ -0,0 +1,35 @@
import RsAlert from "../../../components/RsAlert.vue";
import RsBadge from "../../../components/RsBadge.vue";
import RsButton from "../../../components/RsButton.vue";
import RsCard from "../../../components/RsCard.vue";
import RsCodeMirror from "../../../components/RsCodeMirror.vue";
import RsCollapse from "../../../components/RsCollapse.vue";
import RsCollapseItem from "../../../components/RsCollapseItem.vue";
import RsDropdown from "../../../components/RsDropdown.vue";
import RsDropdownItem from "../../../components/RsDropdownItem.vue";
import RsFieldset from "../../../components/RsFieldset.vue";
import RsModal from "../../../components/RsModal.vue";
import RsProgressBar from "../../../components/RsProgressBar.vue";
import RsTab from "../../../components/RsTab.vue";
import RsTabItem from "../../../components/RsTabItem.vue";
import RsTable from "../../../components/RsTable.vue";
import RsWizard from "../../../components/RsWizard.vue";
export {
RsAlert,
RsBadge,
RsButton,
RsCard,
RsCodeMirror,
RsCollapse,
RsCollapseItem,
RsDropdown,
RsDropdownItem,
RsFieldset,
RsModal,
RsProgressBar,
RsTab,
RsTabItem,
RsTable,
RsWizard,
};

View File

@@ -0,0 +1,422 @@
<script setup>
import { parse } from "@vue/compiler-sfc";
import { watchDebounced } from "@vueuse/core";
import {
RsAlert,
RsBadge,
RsButton,
RsCard,
RsCodeMirror,
RsCollapse,
RsCollapseItem,
RsDropdown,
RsDropdownItem,
RsFieldset,
RsModal,
RsProgressBar,
RsTab,
RsTabItem,
RsTable,
RsWizard,
} from "./index.js";
// Import pinia store
import { useThemeStore } from "~/stores/theme";
definePageMeta({
title: "AI SFC Playground",
description: "AI SFC Playground page",
layout: "empty",
middleware: ["auth"],
requiresAuth: true,
});
const CODE_STORAGE_KEY = "playground-code";
const code = ref(
localStorage.getItem(CODE_STORAGE_KEY) ||
`<template>
<rs-card>
<template #header>SFC Playground Demo</template>
<template #body>
<div class="space-y-4">
<rs-alert variant="info">{{ msg }}</rs-alert>
<rs-button @click="count++">Clicked {{ count }} times</rs-button>
<rs-badge>{{ count > 5 ? 'High' : 'Low' }}</rs-badge>
</div>
</template>
</rs-card>
</template>
<script setup>
const msg = 'Hello from SFC Playground';
const count = ref(0);
<\/script>`
);
const compiledCode = ref(null);
const componentKey = ref(0);
const compilationError = ref(null);
const previewSizes = [
{ name: "Mobile", width: "320px", icon: "ph:device-mobile-camera" },
{ name: "Tablet", width: "768px", icon: "ph:device-tablet-camera" },
{ name: "Desktop", width: "1024px", icon: "ph:desktop" },
{ name: "Full", width: "100%", icon: "material-symbols:fullscreen" },
];
const currentPreviewSize = ref(previewSizes[3]); // Default to Full
// Theme-related code
const themeStore = useThemeStore();
const editorTheme = ref({
label: themeStore.codeTheme,
value: themeStore.codeTheme,
});
const dropdownThemes = ref([]);
// Get all themes
const themes = codemirrorThemes();
// map the themes to the dropdown
dropdownThemes.value = themes.map((theme) => {
return {
label: theme.name,
value: theme.name,
};
});
// watch for changes in the theme
watch(editorTheme, (theme) => {
themeStore.setCodeTheme(theme.value);
});
const compileCode = async (newCode) => {
try {
const { descriptor, errors } = parse(newCode);
if (errors && errors.length > 0) {
compilationError.value = {
message: errors[0].message,
location: errors[0].loc,
};
return;
}
if (descriptor.template && descriptor.scriptSetup) {
const template = descriptor.template.content;
const scriptSetup = descriptor.scriptSetup.content;
// Dynamically import FormKit components
const {
FormKit,
FormKitSchema,
FormKitSchemaNode,
FormKitSchemaCondition,
FormKitSchemaValidation,
} = await import("@formkit/vue");
const component = defineComponent({
components: {
RsAlert,
RsBadge,
RsButton,
RsCard,
RsCodeMirror,
RsCollapse,
RsCollapseItem,
RsDropdown,
RsDropdownItem,
RsFieldset,
RsModal,
RsProgressBar,
RsTab,
RsTabItem,
RsTable,
RsWizard,
FormKit,
FormKitSchema,
FormKitSchemaNode,
FormKitSchemaCondition,
FormKitSchemaValidation,
},
template,
setup() {
const setupContext = reactive({});
try {
// Extract top-level declarations
const declarations =
scriptSetup.match(/const\s+(\w+)\s*=\s*([^;]+)/g) || [];
declarations.forEach((decl) => {
const [, varName, varValue] = decl.match(
/const\s+(\w+)\s*=\s*(.+)/
);
if (
varValue.trim().startsWith("'") ||
varValue.trim().startsWith('"')
) {
// It's a string literal, use it directly
setupContext[varName] = varValue.trim().slice(1, -1);
} else if (varValue.trim().startsWith("ref(")) {
// It's already a ref, use ref
setupContext[varName] = ref(null);
} else {
// For other cases, wrap in ref
setupContext[varName] = ref(null);
}
});
const setupFunction = new Function(
"ctx",
"ref",
"reactive",
"computed",
"watch",
"onMounted",
"onUnmounted",
"useFetch",
"fetch",
"useAsyncData",
"useNuxtApp",
"useRuntimeConfig",
"useRoute",
"useRouter",
"useState",
"FormKit",
"FormKitSchema",
"FormKitSchemaNode",
"FormKitSchemaCondition",
"FormKitSchemaValidation",
`
with (ctx) {
${scriptSetup}
}
return ctx;
`
);
const result = setupFunction(
setupContext,
ref,
reactive,
computed,
watch,
onMounted,
onUnmounted,
useFetch,
fetch,
useAsyncData,
useNuxtApp,
useRuntimeConfig,
useRoute,
useRouter,
useState,
FormKit,
FormKitSchema,
FormKitSchemaNode,
FormKitSchemaCondition,
FormKitSchemaValidation
);
// Merge the result back into setupContext
Object.assign(setupContext, result);
return setupContext;
} catch (error) {
console.error("Error in setup function:", error);
compilationError.value = {
message: `Error in setup function: ${error.message}`,
location: { start: 0, end: 0 },
};
// Return an empty object to prevent breaking the component
return {};
}
},
});
compiledCode.value = markRaw(component);
componentKey.value++;
compilationError.value = null;
} else {
compiledCode.value = null;
compilationError.value = {
message: "Invalid SFC format.",
location: { start: 0, end: 0 },
};
}
} catch (error) {
console.error("Compilation error:", error);
compiledCode.value = null;
compilationError.value = {
message: `Compilation error: ${error.message}`,
location: { start: 0, end: 0 },
};
}
};
watchDebounced(
code,
async (newCode) => {
await compileCode(newCode);
},
{ debounce: 300, immediate: true }
);
const handleFormatCode = () => {
// Recompile the code after formatting
setTimeout(() => compileCode(code.value), 100);
};
onMounted(async () => {
await compileCode(code.value);
});
const defaultCode = `<template>
<rs-card>
<template #header>SFC Playground Demo</template>
<template #body>
<div class="space-y-4">
<rs-alert variant="info">{{ msg }}</rs-alert>
<rs-button @click="count++">Clicked {{ count }} times</rs-button>
<rs-badge>{{ count > 5 ? 'High' : 'Low' }}</rs-badge>
</div>
</template>
</rs-card>
</template>
<script setup>
const msg = 'Hello from SFC Playground';
const count = ref(0);
<\/script>`;
const resetCode = () => {
code.value = defaultCode;
localStorage.setItem(CODE_STORAGE_KEY, defaultCode);
compileCode(code.value);
};
// Add a watch effect to save code changes to localStorage
watch(
code,
(newCode) => {
localStorage.setItem(CODE_STORAGE_KEY, newCode);
},
{ deep: true }
);
</script>
<template>
<div class="flex flex-col h-screen bg-gray-900">
<!-- Header -->
<header
class="bg-gray-800 p-2 flex flex-wrap items-center justify-between text-white"
>
<div class="flex items-center mb-2 sm:mb-0 gap-4">
<Icon
@click="navigateTo('/')"
name="ph:arrow-circle-left-duotone"
class="cursor-pointer"
/>
<img
src="@/assets/img/logo/logo-word-white.svg"
alt="Vue Logo"
class="h-8 block mr-2"
/>
</div>
<div class="flex flex-wrap items-center space-x-2">
<rs-button @click="resetCode" class="mr-2">
<Icon name="material-symbols:refresh" class="mr-2" />
Reset Code
</rs-button>
<h1 class="text-lg font-semibold">Code Playground</h1>
</div>
</header>
<!-- Main content -->
<div class="flex flex-col sm:flex-row flex-1 overflow-hidden">
<!-- Editor section -->
<div
class="w-full sm:w-1/2 flex flex-col border-b sm:border-b-0 sm:border-r border-gray-900"
>
<div class="flex-grow overflow-hidden">
<rs-code-mirror
v-model="code"
mode="javascript"
class="h-full"
@format-code="handleFormatCode"
/>
</div>
</div>
<!-- Preview section -->
<div class="w-full sm:w-1/2 bg-white overflow-auto flex flex-col">
<div
class="bg-gray-800 p-2 flex justify-between items-center text-white"
>
<h2 class="text-sm font-semibold">Preview</h2>
<div class="flex space-x-2">
<rs-button
v-for="size in previewSizes"
:key="size.name"
@click="currentPreviewSize = size"
:class="{
'bg-blue-600': currentPreviewSize === size,
'bg-gray-600': currentPreviewSize !== size,
}"
class="px-2 py-1 text-xs rounded"
>
<Icon v-if="size.icon" :name="size.icon" class="!w-5 !h-5 mr-2" />
{{ size.name }}
</rs-button>
</div>
</div>
<div class="flex-grow overflow-auto p-4 flex justify-center">
<div
:style="{
width: currentPreviewSize.width,
height: '100%',
overflow: 'auto',
}"
class="border border-gray-300 transition-all duration-300 ease-in-out"
>
<component
:key="componentKey"
v-if="compiledCode && !compilationError"
:is="compiledCode"
/>
<div v-else-if="compilationError?.message">
<div class="flex justify-center items-center p-5">
<div class="text-center">
<Icon name="ph:warning" class="text-6xl" />
<p class="text-lg font-semibold mt-4">
Something went wrong. Please refer the error in the editor.
</p>
</div>
</div>
</div>
<div v-else class="text-gray-500">Waiting for code changes...</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.device-frame {
background-color: #f0f0f0;
border-radius: 16px;
padding: 16px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
@media (max-width: 640px) {
.device-frame {
padding: 8px;
border-radius: 8px;
}
}
:deep(.cm-editor) {
height: 100%;
}
</style>

View File

@@ -0,0 +1,7 @@
<template>
<div></div>
</template>
<script setup></script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,27 @@
<script setup>
definePageMeta({
title: "Environment",
middleware: ["auth"],
requiresAuth: true,
});
const { $swal, $router } = useNuxtApp();
const env = ref({});
const { data: envData } = await useFetch("/api/devtool/config/env", {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
env.value = envData.value.data;
</script>
<template>
<div>
<LayoutsBreadcrumb />
<rs-code-mirror mode="javascript" v-model="env" disabled />
</div>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<div></div>
</template>
<script setup></script>
<style lang="scss" scoped></style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,226 @@
<script setup>
definePageMeta({
title: "Code Editor",
middleware: ["auth"],
requiresAuth: true,
});
const { $swal, $router } = useNuxtApp();
const route = useRoute();
const router = useRouter();
const fileCode = ref("");
const fileCodeConstant = ref("");
const componentKey = ref(0);
const hasError = ref(false);
const error = ref("");
const linterError = ref(false);
const linterErrorText = ref("");
const linterErrorColumn = ref(0);
const linterErrorLine = ref(0);
const isLinterChecking = ref(false);
const page = router.getRoutes().find((page) => {
return page.name === route.query?.page;
});
if (!route.query.page || !page) {
$swal
.fire({
title: "Error",
text: "The page you are trying to edit is not found. Please choose a page to edit.",
icon: "error",
confirmButtonText: "Ok",
})
.then(async (result) => {
if (result.isConfirmed) {
await $router.push("/devtool/content-editor");
}
});
}
if (page?.path)
page.path = page.path.replace(/:(\w+)/g, "[$1]").replace(/\(\)/g, "");
// Call API to get the code
const { data } = await useFetch("/api/devtool/content/code/file-code", {
initialCache: false,
method: "GET",
query: {
path: page.path,
},
});
// console.log(data.value);
if (data.value.statusCode === 200) {
fileCode.value = data.value.data;
fileCodeConstant.value = data.value.data;
// If its index append the path
if (data.value?.mode == "index") page.path = page.path + "/index";
} else {
$swal.fire({
title: "Error",
text: "The page you are trying to edit is not found. Please choose a page to edit. You will be redirected to the content editor page.",
icon: "error",
confirmButtonText: "Ok",
timer: 3000,
});
setTimeout(() => {
$router.push("/devtool/content-editor");
}, 3000);
}
async function formatCode() {
// Call API to get the code
const { data } = await useFetch("/api/devtool/content/code/prettier-format", {
initialCache: false,
method: "POST",
body: JSON.stringify({
code: fileCode.value,
}),
});
forceRerender();
if (data.value.statusCode === 200) {
fileCode.value = data.value.data;
}
}
async function checkLinterVue() {
isLinterChecking.value = true;
try {
// Call API to get the code
const { data } = await useFetch("/api/devtool/content/code/linter", {
initialCache: false,
method: "POST",
body: JSON.stringify({
code: fileCode.value,
}),
});
if (data.value.statusCode === 200) {
linterError.value = false;
linterErrorText.value = "";
linterErrorColumn.value = 0;
linterErrorLine.value = 0;
} else if (data.value.statusCode === 400) {
linterError.value = true;
linterErrorText.value = data.value.data.message;
linterErrorColumn.value = data.value.data.column;
linterErrorLine.value = data.value.data.line;
}
} finally {
isLinterChecking.value = false;
}
}
const forceRerender = () => {
componentKey.value += 1;
};
const keyPress = (key) => {
// console.log(key);
const event = new KeyboardEvent("keydown", {
key: key,
ctrlKey: true,
});
// console.log(event);
document.dispatchEvent(event);
};
const saveCode = async () => {
// Check Linter Vue
await checkLinterVue();
if (linterError.value) {
$swal.fire({
title: "Error",
text: "There is an error in your code. Please fix it before saving.",
icon: "error",
confirmButtonText: "Ok",
});
return;
}
const { data } = await useFetch("/api/devtool/content/code/save", {
initialCache: false,
method: "POST",
body: {
path: page.path,
code: fileCode.value,
},
});
if (data.value.statusCode === 200) {
$swal.fire({
title: "Success",
text: "The code has been saved successfully.",
icon: "success",
confirmButtonText: "Ok",
timer: 1000,
});
setTimeout(() => {
$router.go();
}, 1000);
}
};
</script>
<template>
<div>
<LayoutsBreadcrumb />
<rs-alert v-if="hasError" variant="danger" class="mb-4">{{
error
}}</rs-alert>
<rs-card class="mb-0">
<div class="p-4">
<div class="flex justify-end gap-2 mb-4">
<rs-button
class="!p-2"
@click="saveCode"
:disabled="isLinterChecking"
>
<div class="flex items-center">
<Icon
v-if="!isLinterChecking"
name="material-symbols:save-outline-rounded"
size="20px"
class="mr-1"
/>
<Icon
v-else
name="eos-icons:loading"
size="20px"
class="mr-1 animate-spin"
/>
{{ isLinterChecking ? "Checking..." : "Save Code" }}
</div>
</rs-button>
</div>
<Transition>
<rs-alert v-if="linterError" variant="danger" class="mb-4">
<div class="flex gap-2">
<Icon name="material-symbols:error-outline-rounded" size="20px" />
<div>
<div class="font-bold">ESLint Error</div>
<div class="text-sm">
{{ linterErrorText }}
</div>
<div class="text-xs mt-2">
Line: {{ linterErrorLine }} Column: {{ linterErrorColumn }}
</div>
</div>
</div>
</rs-alert>
</Transition>
</div>
<rs-code-mirror :key="componentKey" v-model="fileCode" />
</rs-card>
</div>
</template>

View File

@@ -0,0 +1,247 @@
<script setup>
definePageMeta({
title: "Content Editor",
middleware: ["auth"],
requiresAuth: true,
});
const { $swal, $router } = useNuxtApp();
const router = useRouter();
const getPages = router.getRoutes();
const pages = getPages.filter((page) => {
// Filter out pages in the devtool path
if (page.path.includes("/devtool")) {
return false;
}
// Use page.name if page.meta.title doesn't exist
const pageTitle = page.meta?.title || page.name;
return pageTitle && pageTitle !== "Home" && page.name;
});
const searchText = ref("");
const showModal = ref(false);
const modalData = ref({
name: "",
path: "",
});
const searchPages = () => {
return pages.filter((page) => {
const pageTitle = page.meta?.title || page.name;
return pageTitle.toLowerCase().includes(searchText.value.toLowerCase());
});
};
const capitalizeSentence = (sentence) => {
return sentence
.split(" ")
.map((word) => {
return word[0].toUpperCase() + word.slice(1);
})
.join(" ");
};
const templateOptions = ref([{ label: "Select Template", value: "" }]);
const selectTemplate = ref("");
const { data: templates } = await useFetch(
"/api/devtool/content/template/list",
{
method: "GET",
}
);
templateOptions.value.push(
...templates?.value.data.map((template) => {
return {
label: `${template.title} - ${template.id}`,
value: template.id,
};
})
);
const importTemplate = (pageName) => {
showModal.value = true;
modalData.value.name = pageName;
modalData.value.path = router.getRoutes().find((page) => {
return page.name === pageName;
}).path;
};
const confirmModal = async () => {
$swal
.fire({
title: "Are you sure you want to import this template?",
text: "This action cannot be undone.",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "Yes",
})
.then(async (result) => {
if (result.isConfirmed) {
const { data: res } = await useFetch(
"/api/devtool/content/template/import",
{
initialCache: false,
method: "GET",
query: {
path: modalData.value.path + "/index",
templateId: selectTemplate.value,
},
}
);
if (res.value.statusCode == 200) {
$swal.fire({
title: "Success",
text: res.value.message,
icon: "success",
confirmButtonText: "Ok",
timer: 1000,
});
setTimeout(() => {
$router.go();
}, 1000);
}
}
})
.finally(() => {
showModal.value = false;
});
};
</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 content of a page. You can edit the
content of the page by choosing the page to edit from the card list
below.
</p>
</template>
</rs-card>
<rs-card>
<div class="p-4">
<!-- Search Button -->
<FormKit
v-model="searchText"
placeholder="Search Title..."
type="search"
/>
<div
class="page-wrapper grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5"
v-auto-animate
>
<!-- <div
class="page border-2 border-gray-400 border-dashed rounded-lg"
style="min-height: 250px"
>
Add New Page
</div> -->
<div
v-for="page in searchPages()"
:key="page.path"
class="page shadow-md shadow-black/5 p-5 ring-1 ring-slate-700/10 rounded-lg"
>
<div class="pb-4">
<h4 class="font-semibold">
{{ capitalizeSentence(page.meta?.title || page.name) }}
</h4>
<nuxt-link :to="page.path">
<div
class="flex items-center text-primary hover:text-primary/70"
>
<Icon
class="mr-2"
name="ic:outline-link"
style="font-size: 1.2rem"
></Icon>
<p class="text-sm">{{ page.path }}</p>
</div>
</nuxt-link>
</div>
<div
class="button-list flex justify-between border-t pt-4 border-gray-300"
>
<div class="flex gap-x-2">
<!-- <nuxt-link
:to="`/devtool/content-editor/canvas?page=${page.name}`"
>
<rs-button variant="primary" class="!py-2 !px-3">
<Icon name="ph:paint-brush-broad"></Icon>
</rs-button>
</nuxt-link> -->
<nuxt-link
:to="`/devtool/content-editor/code?page=${page.name}`"
>
<rs-button variant="primary" class="!py-2 !px-3">
<Icon
name="material-symbols:code-blocks-outline-rounded"
></Icon>
</rs-button>
</nuxt-link>
</div>
<rs-button
variant="primary-text"
class="!py-2 !px-3"
@click="importTemplate(page.name)"
>
<Icon name="mdi:import"></Icon>
</rs-button>
</div>
</div>
</div>
</div>
</rs-card>
<rs-modal :title="`Import (${modalData.name})`" v-model="showModal">
<FormKit
v-model="selectTemplate"
type="select"
label="Content Template"
:options="templateOptions"
validation="required"
validation-visibility="dirty"
help="Please choose carefully the template that you want to import. This action cannot be undone."
/>
<template #footer>
<rs-button @click="showModal = false" variant="primary-text">
Cancel
</rs-button>
<rs-button @click="confirmModal" :disabled="!selectTemplate"
>Confirm</rs-button
>
</template>
</rs-modal>
</div>
</template>
<style lang="scss" scoped>
.thumbnail::before {
content: "";
display: block;
height: 100%;
position: absolute;
top: 0;
left: 0;
width: 100%;
background-color: rgba(255, 255, 255, 0.8);
}
</style>

View File

@@ -0,0 +1,127 @@
<script setup>
definePageMeta({
title: "Template Editor",
middleware: ["auth"],
requiresAuth: true,
});
const searchText = ref("");
const { data: templateList } = await useFetch(
"/api/devtool/content/template/list",
{
method: "GET",
}
);
// Search function that can search the template by title and tags if tags is available after data is fetched
const searchTemplate = () => {
return templateList?.value.data.filter((template) => {
return template.title
.toLowerCase()
.includes(searchText.value.toLowerCase());
});
};
</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 webpage serves as a platform for template management, enabling
users to select and utilize templates for rendering pages according to
their chosen design.
</p>
</template>
</rs-card>
<rs-card>
<div class="p-4">
<!-- Search Button -->
<FormKit
v-model="searchText"
placeholder="Search Title..."
type="search"
/>
<div
class="page-wrapper grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5"
v-auto-animate
>
<!-- <div
class="page border-2 border-gray-400 border-dashed rounded-lg"
style="min-height: 250px"
>
Add New Page
</div> -->
<div
v-for="val in searchTemplate()"
class="page shadow-md shadow-black/5 ring-1 ring-slate-700/10 rounded-lg"
style="min-height: 250px"
>
<div class="thumbnail-wrapper relative">
<div class="button-list absolute bottom-3 right-3 flex z-10">
<nuxt-link :to="val.img" target="_blank">
<rs-button class="!py-2 !px-3 rounded-r-none">
<Icon name="material-symbols:fullscreen-rounded"></Icon>
</rs-button>
</nuxt-link>
<nuxt-link
:to="`/devtool/content-editor/template/view/${val.id}`"
>
<rs-button class="!py-2 !px-3 rounded-l-none">
<Icon name="material-symbols:preview"></Icon>
</rs-button>
</nuxt-link>
</div>
<img
class="thumbnail rounded-tl-lg rounded-tr-lg bg-[#F3F4F6]"
style="height: 250px; width: 100%; object-fit: contain"
:src="val.img"
alt=""
/>
<div
class="overlay-img opacity-10 bg-black text-black before:content-['Hello_World'] absolute top-0 left-0 w-full h-full rounded-tl-lg rounded-tr-lg"
></div>
</div>
<div class="p-4">
<h4 class="font-semibold">{{ val.title }} ({{ val.id }})</h4>
<div class="flex items-center mb-4">
<p class="text-sm">{{ val.description }}</p>
</div>
<div
class="tag h-10 flex justify-start items-center overflow-x-auto gap-x-2"
>
<rs-badge v-for="val2 in val.tag">
{{ val2 }}
</rs-badge>
</div>
</div>
</div>
</div>
</div>
</rs-card>
</div>
</template>
<style lang="scss" scoped>
.thumbnail::before {
content: "";
display: block;
height: 100%;
position: absolute;
top: 0;
left: 0;
width: 100%;
background-color: rgba(255, 255, 255, 0.8);
}
</style>

View File

@@ -0,0 +1,33 @@
<script setup>
definePageMeta({
title: "Template Viewer",
middleware: ["auth"],
requiresAuth: true,
});
const id = useRoute().params.id;
const { data: template } = await useFetch(
`/api/devtool/content/template/get-list`,
{
method: "GET",
params: {
id: id,
},
}
);
console.log(template.value.data);
const templateComponent = defineAsyncComponent(
() => import(`../../../../../templates/${template.value.data.filename}.vue`)
);
</script>
<template>
<div>
<LayoutsBreadcrumb />
<component :is="templateComponent" />
</div>
</template>

View 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>

169
pages/devtool/orm/index.vue Normal file
View File

@@ -0,0 +1,169 @@
<script setup>
definePageMeta({
title: "Database (ORM)",
middleware: ["auth"],
requiresAuth: true,
});
const { $swal } = useNuxtApp();
const searchText = ref("");
const tableList = ref([]);
const { data } = await useFetch("/api/devtool/orm/schema", {
method: "GET",
query: {
type: "table",
},
});
if (data.value.statusCode === 200) {
tableList.value = data.value.data;
}
const deleteTable = async (tableName) => {
try {
const result = await $swal.fire({
title: "Are you sure?",
text: `You are about to delete the table '${tableName}'. This action cannot be undone!`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "Yes, delete it!",
});
if (result.isConfirmed) {
const { data } = await useFetch(
`/api/devtool/orm/table/delete/${tableName}`,
{
method: "DELETE",
}
);
if (data.value.statusCode === 200) {
await $swal.fire("Deleted!", data.value.message, "success");
// Remove the deleted table from the list
tableList.value = tableList.value.filter(
(table) => table.name !== tableName
);
} else {
throw new Error(data.value.message);
}
}
} catch (error) {
console.error("Error deleting table:", error);
await $swal.fire(
"Error!",
`Failed to delete table: ${error.message}`,
"error"
);
}
};
</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 ORM schema. You can add, edit, and
delete the model and its fields. The changes will be saved to the
database.
</p>
</template>
</rs-card>
<rs-card>
<div class="p-4">
<div class="flex justify-end items-center mb-4">
<nuxt-link to="/devtool/orm/table/create">
<rs-button>
<Icon name="material-symbols:add" class="mr-1"></Icon>
Add Table
</rs-button>
</nuxt-link>
</div>
<!-- Search Button -->
<FormKit
v-model="searchText"
placeholder="Search Title..."
type="search"
/>
<div v-if="tableList" class="grid grid-cols-1 gap-5">
<div v-for="tbl in tableList" class="p-5 border rounded-md">
<div class="flex-1 flex flex-col gap-2">
<div class="flex items-center text-primary gap-2">
<Icon name="ph:table-fill" />
<h4>
{{ tbl.name }}
</h4>
</div>
<div class="flex flex-wrap gap-3">
<span
v-for="field in tbl.fields"
class="text-xs py-1 px-3 inline-block bg-slate-100 rounded-lg ring-1 ring-slate-200"
>
{{ field }}
</span>
</div>
</div>
<div class="flex justify-between mt-5">
<NuxtLink
:to="`/devtool/orm/view/${tbl.name}`"
class="flex justify-center items-center"
>
<rs-button
variant="primary"
class="flex justify-center items-center"
>
View Data
</rs-button>
</NuxtLink>
<div v-if="!tbl.disabled" class="flex justify-between gap-3">
<NuxtLink
:to="`/devtool/orm/table/modify/${tbl.name}`"
class="flex justify-center items-center"
>
<rs-button
variant="secondary"
class="flex justify-center items-center"
>
<Icon name="ph:note-pencil-bold" class="w-4 h-4 mr-1" />
Modify
</rs-button>
</NuxtLink>
<rs-button
variant="danger-outline"
class="flex justify-center items-center"
@click="deleteTable(tbl.name)"
>
<Icon name="ph:trash-simple-bold" class="w-4 h-4 mr-1" />
Delete
</rs-button>
</div>
<div
class="flex items-end text-xs py-1 px-3 text-slate-400 cursor-not-allowed"
v-else
>
Cannot Modify System Table
</div>
</div>
</div>
</div>
</div>
</rs-card>
</div>
</template>

View File

@@ -0,0 +1,315 @@
<script setup>
definePageMeta({
title: "Database Create Table",
middleware: ["auth"],
requiresAuth: true,
});
const tableName = ref("");
const tableKey = ref(0);
const tableData = ref([]);
const columnTypes = ref([]);
const { $swal } = useNuxtApp();
const { data: dbConfiguration } = await useFetch(
"/api/devtool/orm/table/config"
);
if (dbConfiguration.value && dbConfiguration.value.statusCode === 200) {
let tempObject = {};
dbConfiguration.value.data.tableField.forEach((field) => {
tempObject[field] = "";
});
tableData.value.push(tempObject);
// Update columnTypes to use the new structure
columnTypes.value = dbConfiguration.value.data.columnTypes.flatMap((group) =>
group.options.map((option) =>
typeof option === "string" ? { label: option, value: option } : option
)
);
}
const addNewField = () => {
let tempObject = {};
// Add new field after the last field
dbConfiguration.value.data.tableField.forEach((field) => {
tempObject[field] = "";
});
tableData.value.push(tempObject);
tableKey.value++;
};
const removeField = (index) => {
tableData.value.splice(index, 1);
};
const sortField = (index, direction) => {
if (direction === "up") {
if (index === 0) return;
let temp = tableData.value[index];
tableData.value[index] = tableData.value[index - 1];
tableData.value[index - 1] = temp;
tableKey.value++;
} else {
if (index === tableData.value.length - 1) return;
let temp = tableData.value[index];
tableData.value[index] = tableData.value[index + 1];
tableData.value[index + 1] = temp;
tableKey.value++;
}
};
const autoIcrementColumn = ref("");
const computedAutoIncrementColumn = computed(() => {
return tableData.value.map((data) => {
return {
label: data.name,
value: data.name,
};
});
});
const checkRadioButton = async (index, event) => {
try {
// change tableData[index].primaryKey value to true
tableData.value[index].primaryKey = event.target.checked;
// change all other tableData[index].primaryKey value to false
tableData.value.forEach((data, i) => {
if (i !== index) {
tableData.value[i].primaryKey = "";
}
});
} catch (error) {
console.log(error);
}
};
const resetData = () => {
$swal
.fire({
title: "Are you sure?",
text: "You will lose all the data you have entered.",
icon: "warning",
showCancelButton: true,
confirmButtonText: "Yes, reset it!",
cancelButtonText: "No, cancel!",
})
.then((result) => {
if (result.isConfirmed) {
tableName.value = "";
tableData.value = [];
tableKey.value = 0;
let tempObject = {};
dbConfiguration.value.data.tableField.forEach((field) => {
tempObject[field] = "";
});
tableData.value.push(tempObject);
}
});
};
const submitCreateTable = async () => {
try {
const { data } = await useFetch("/api/devtool/orm/table/create", {
method: "POST",
body: {
tableName: tableName.value,
tableSchema: tableData.value,
autoIncrementColumn: autoIcrementColumn.value,
},
});
if (data.value.statusCode == 200) {
$swal.fire({
title: "Success",
text: data.value.message,
icon: "success",
});
navigateTo("/devtool/orm");
} else {
$swal.fire({
title: "Error",
text: data.value.message,
icon: "error",
});
}
} catch (error) {
console.log(error);
}
};
</script>
<template>
<div>
<rs-card class="py-5">
<FormKit
type="form"
:classes="{
messages: 'px-5',
}"
:actions="false"
@submit="submitCreateTable"
>
<div
class="flex flex-col md:flex-row justify-between items-start md:items-center gap-y-2 mb-5 px-5"
>
<div>
<h5 class="font-semibold">Create Table</h5>
<span class="text-sm text-gray-500">
Create a new table in the database.
</span>
</div>
<div class="flex gap-3">
<rs-button @click="resetData" variant="primary-outline">
Reset Table
</rs-button>
<rs-button btnType="submit" class="mb-4 w-[100px]">
Save
</rs-button>
</div>
</div>
<section class="px-5">
<FormKit
v-model="tableName"
type="text"
label="Table name"
placeholder="Enter table name"
validation="required|length:3,64"
:classes="{ outer: 'mb-8' }"
:validation-messages="{
required: 'Table name is required',
length:
'Table name must be at least 3 characters and at most 64 characters',
}"
/>
</section>
<rs-table
v-if="tableData && tableData.length > 0"
:data="tableData"
:key="tableKey"
:disableSort="true"
class="mb-8"
>
<template v-slot:name="data">
<FormKit
v-model="data.value.name"
:classes="{
outer: 'mb-0 w-[200px]',
}"
placeholder="Enter column name"
type="text"
validation="required|length:3,64"
:validation-messages="{
required: 'Column name is required',
length:
'Column name must be at least 3 characters and at most 64 characters',
}"
/>
</template>
<template v-slot:type="data">
<FormKit
v-if="columnTypes && columnTypes.length > 0"
v-model="data.value.type"
:classes="{ outer: 'mb-0 w-[100px]' }"
:options="columnTypes"
type="select"
placeholder="Select type"
validation="required"
:validation-messages="{
required: 'Column type is required',
}"
/>
</template>
<template v-slot:length="data">
<FormKit
v-model="data.value.length"
:classes="{ outer: 'mb-0 w-[150px]' }"
placeholder="Enter length"
type="number"
/>
</template>
<template v-slot:defaultValue="data">
<FormKit
v-model="data.value.defaultValue"
:classes="{ outer: 'mb-0 w-[150px]' }"
placeholder="Optional"
type="text"
/>
</template>
<template v-slot:nullable="data">
<FormKit
v-model="data.value.nullable"
:classes="{ wrapper: 'mb-0', outer: 'mb-0' }"
type="checkbox"
/>
</template>
<template v-slot:primaryKey="data">
<FormKit
v-model="data.value.primaryKey"
:classes="{
wrapper: 'mb-0',
outer: 'mb-0',
input: 'icon-check rounded-full',
}"
type="checkbox"
@change="checkRadioButton(data.index, $event)"
/>
</template>
<template v-slot:actions="data">
<div class="flex justify-center items-center gap-2">
<rs-button @click="addNewField" type="button" class="p-1 w-6 h-6">
<Icon name="ph:plus" />
</rs-button>
<rs-button
@click="sortField(data.index, 'up')"
type="button"
class="p-1 w-6 h-6"
:disabled="data.index === 0"
>
<Icon name="ph:arrow-up" />
</rs-button>
<rs-button
@click="sortField(data.index, 'down')"
type="button"
class="p-1 w-6 h-6"
:disabled="data.index === tableData.length - 1"
>
<Icon name="ph:arrow-down" />
</rs-button>
<rs-button
@click="removeField(data.index)"
type="button"
class="p-1 w-6 h-6"
variant="danger"
:disabled="data.index === 0 && tableData.length === 1"
>
<Icon name="ph:x" />
</rs-button>
</div>
</template>
</rs-table>
</FormKit>
</rs-card>
</div>
</template>

View File

@@ -0,0 +1,360 @@
<script setup>
import { v4 as uuidv4 } from "uuid";
definePageMeta({
title: "Database Modify Table",
middleware: ["auth"],
requiresAuth: true,
});
const tableName = ref("");
const tableKey = ref(0);
const tableData = ref([]);
const columnTypes = ref([]);
const { table } = useRoute().params;
const { $swal } = useNuxtApp();
const { data: dbConfiguration } = await useFetch(
"/api/devtool/orm/table/config"
);
if (dbConfiguration.value && dbConfiguration.value.statusCode === 200) {
let tempObject = {};
dbConfiguration.value.data.tableField.forEach((field) => {
tempObject[field] = "";
});
tableData.value.push(tempObject);
// Update columnTypes to use the new structure
columnTypes.value = dbConfiguration.value.data.columnTypes.flatMap((group) =>
group.options.map((option) =>
typeof option === "string" ? { label: option, value: option } : option
)
);
}
const { data: tableDetail } = await useFetch(
`/api/devtool/orm/table/modify/get`,
{
method: "GET",
params: {
tableName: table,
},
}
);
// console.log(tableDetail.value);
if (tableDetail.value.statusCode === 200) {
tableData.value = tableDetail.value.data.map((item) => ({
...item,
actions: {
...item.actions,
id: uuidv4(), // Add a unique id to each item's actions
},
}));
tableName.value = table;
}
const addNewField = (index) => {
let tempObject = {
actions: {
id: uuidv4(), // Add a unique id to the new field's actions
},
};
dbConfiguration.value.data.tableField.forEach((field) => {
tempObject[field] = "";
});
tableData.value.splice(index + 1, 0, tempObject);
tableKey.value++;
};
const sortField = (id, direction) => {
const index = tableData.value.findIndex((item) => item.actions.id === id);
if (direction === "up" && index > 0) {
// Move the current field up
const temp = tableData.value[index];
tableData.value[index] = tableData.value[index - 1];
tableData.value[index - 1] = temp;
} else if (direction === "down" && index < tableData.value.length - 1) {
// Move the current field down
const temp = tableData.value[index];
tableData.value[index] = tableData.value[index + 1];
tableData.value[index + 1] = temp;
}
tableKey.value++;
};
const removeField = (id) => {
if (tableData.value.length > 1) {
const index = tableData.value.findIndex((item) => item.actions.id === id);
tableData.value.splice(index, 1);
tableKey.value++;
} else {
$swal.fire({
title: "Error",
text: "Cannot remove the last field.",
icon: "error",
});
}
};
const autoIcrementColumn = ref("");
const computedAutoIncrementColumn = computed(() => {
return tableData.value.map((data) => {
return {
label: data.name,
value: data.name,
};
});
});
const checkRadioButton = async (index, event) => {
try {
// change tableData[index].primaryKey value to true
tableData.value[index].primaryKey = event.target.checked;
// change all other tableData[index].primaryKey value to false
tableData.value.forEach((data, i) => {
if (i !== index) {
tableData.value[i].primaryKey = "";
}
});
} catch (error) {
console.log(error);
}
};
const resetData = () => {
$swal
.fire({
title: "Are you sure?",
text: "You will lose all the data you have entered.",
icon: "warning",
showCancelButton: true,
confirmButtonText: "Yes, reset it!",
cancelButtonText: "No, cancel!",
})
.then((result) => {
if (result.isConfirmed) {
tableName.value = "";
tableData.value = [];
tableKey.value = 0;
let tempObject = {};
dbConfiguration.value.data.tableField.forEach((field) => {
tempObject[field] = "";
});
tableData.value.push(tempObject);
}
});
};
const submitModifyTable = async () => {
try {
// console.log({
// tableName: tableName.value,
// tableSchema: tableData.value,
// autoIncrementColumn: autoIcrementColumn.value,
// });
// return;
const { data } = await useFetch("/api/devtool/orm/table/modify", {
method: "POST",
body: {
tableName: tableName.value,
tableSchema: tableData.value,
autoIncrementColumn: autoIcrementColumn.value,
},
});
if (data.value.statusCode == 200) {
$swal.fire({
title: "Success",
text: data.value.message,
icon: "success",
});
} else {
$swal.fire({
title: "Error",
text: data.value.message,
icon: "error",
});
}
} catch (error) {
console.log(error);
}
};
</script>
<template>
<div>
<rs-card class="py-5">
<FormKit
type="form"
:classes="{
messages: 'px-5',
}"
:actions="false"
@submit="submitModifyTable"
>
<div
class="flex flex-col md:flex-row justify-between items-start md:items-center gap-y-2 mb-5 px-5"
>
<div>
<h5 class="font-semibold">Modify Table</h5>
<span class="text-sm text-gray-500">
Modify a new table in the database.
</span>
</div>
<div class="flex gap-3">
<rs-button @click="resetData" variant="primary-outline">
Reset Table
</rs-button>
<rs-button btnType="submit" class="mb-4 w-[100px]">
Save
</rs-button>
</div>
</div>
<section class="px-5">
<FormKit
v-model="tableName"
type="text"
label="Table name"
placeholder="Enter table name"
validation="required|length:3,64"
:classes="{ outer: 'mb-8' }"
:validation-messages="{
required: 'Table name is required',
length:
'Table name must be at least 3 characters and at most 64 characters',
}"
/>
</section>
<rs-table
v-if="tableData && tableData.length > 0"
:data="tableData"
:key="tableKey"
:disableSort="true"
:pageSize="100"
class="mb-8"
>
<template v-slot:name="data">
<FormKit
v-model="data.value.name"
:classes="{
outer: 'mb-0 w-[200px]',
}"
placeholder="Enter column name"
type="text"
validation="required|length:3,64"
:validation-messages="{
required: 'Column name is required',
length:
'Column name must be at least 3 characters and at most 64 characters',
}"
/>
</template>
<template v-slot:type="data">
<FormKit
v-if="columnTypes && columnTypes.length > 0"
v-model="data.value.type"
:classes="{ outer: 'mb-0 w-[100px]' }"
:options="columnTypes"
type="select"
placeholder="Select type"
validation="required"
:validation-messages="{
required: 'Column type is required',
}"
/>
</template>
<template v-slot:length="data">
<FormKit
v-model="data.value.length"
:classes="{ outer: 'mb-0 w-[150px]' }"
placeholder="Enter length"
type="number"
/>
</template>
<template v-slot:defaultValue="data">
<FormKit
v-model="data.value.defaultValue"
:classes="{ outer: 'mb-0 w-[150px]' }"
placeholder="Optional"
type="text"
/>
</template>
<template v-slot:nullable="data">
<FormKit
v-model="data.value.nullable"
:classes="{ wrapper: 'mb-0', outer: 'mb-0' }"
type="checkbox"
/>
</template>
<template v-slot:primaryKey="data">
<FormKit
v-model="data.value.primaryKey"
:classes="{
wrapper: 'mb-0',
outer: 'mb-0',
input: 'icon-check rounded-full',
}"
type="checkbox"
@change="checkRadioButton(data.index, $event)"
/>
</template>
<template v-slot:actions="data">
<div class="flex justify-center items-center gap-2">
<rs-button
@click="addNewField(tableData.indexOf(data.value))"
type="button"
class="p-1 w-6 h-6"
>
<Icon name="ph:plus" />
</rs-button>
<rs-button
@click="sortField(data.value.actions.id, 'up')"
type="button"
class="p-1 w-6 h-6"
:disabled="tableData.indexOf(data.value) === 0"
>
<Icon name="ph:arrow-up" />
</rs-button>
<rs-button
@click="sortField(data.value.actions.id, 'down')"
type="button"
class="p-1 w-6 h-6"
:disabled="
tableData.indexOf(data.value) === tableData.length - 1
"
>
<Icon name="ph:arrow-down" />
</rs-button>
<rs-button
@click="removeField(data.value.actions.id)"
type="button"
class="p-1 w-6 h-6"
variant="danger"
:disabled="tableData.length === 1"
>
<Icon name="ph:x" />
</rs-button>
</div>
</template>
</rs-table>
</FormKit>
</rs-card>
</div>
</template>

View File

@@ -0,0 +1,79 @@
<script setup>
definePageMeta({
title: "ORM Table Editor",
middleware: ["auth"],
requiresAuth: true,
});
const { $swal } = useNuxtApp();
const { table } = useRoute().params;
const { data: tableData } = await useFetch("/api/devtool/orm/data/get", {
method: "GET",
query: {
tableName: table,
},
});
const openPrismaStudio = async () => {
const { data } = await useFetch("/api/devtool/orm/studio", {
method: "GET",
query: {
tableName: table,
},
});
if (data.value.statusCode === 200) {
$swal.fire({
title: "Prisma Studio",
text: data.value.message,
icon: "success",
});
} else {
$swal.fire({
title: "Prisma Studio",
text: data.value.message,
icon: "error",
});
}
};
</script>
<template>
<div>
<LayoutsBreadcrumb />
<rs-card class="py-5">
<div
class="flex flex-col md:flex-row justify-between items-start md:items-center gap-y-2 px-5"
>
<div>
<h5 class="font-semibold">Table - {{ table }}</h5>
<span class="text-sm text-gray-500">
Below is the data of the table.
</span>
</div>
<rs-button @click="openPrismaStudio" class="mb-4">
Open Prisma Studio
</rs-button>
</div>
<rs-table
v-if="tableData.data && tableData.data.length > 0"
:key="tableData.data"
:data="tableData.data"
advanced
/>
<div v-else class="flex justify-center my-3">
<div class="text-center">
<h6 class="font-semibold">Data Not Found</h6>
<span class="text-sm text-gray-500">
There is no data available for this table.
</span>
</div>
</div>
</rs-card>
</div>
</template>

View File

@@ -0,0 +1,499 @@
<script setup>
definePageMeta({
title: "Role List",
middleware: ["auth"],
requiresAuth: true,
});
const { $swal, $router } = useNuxtApp();
const roleList = ref([]);
const roleUserList = ref([]);
const showModal = ref(false);
const showModalForm = ref({
id: "",
name: "",
description: "",
users: [],
status: "",
});
const modalType = ref("edit");
const showModalUser = ref(false);
const showModalUserForm = ref({
username: "",
fullname: "",
email: "",
phone: "",
password: "",
role: "",
status: "",
});
const showModalDelete = ref(false);
const showModalDeleteForm = ref({
id: "",
name: "",
});
const statusDropdown = ref([
{ label: "Active", value: "ACTIVE" },
{ label: "Inactive", value: "INACTIVE" },
]);
const roleListbyUser = ref([]);
const checkAllUser = ref(false);
// Call API
// onMounted(async () => {
// await getUserList();
// await getRoleList();
// });
getRoleList();
getUserList();
async function getRoleList() {
const { data } = await useFetch("/api/devtool/role/list", {
initialCache: false,
});
// Rename the key
if (data.value?.statusCode === 200) {
roleList.value = data.value.data.map((role) => ({
id: role.roleID,
name: role.roleName,
description: role.roleDescription,
users: role.users.map((u) => {
return {
label: u.user.userUsername,
value: u.user.userUsername,
};
}),
status: role.roleStatus,
createdDate: role.roleCreatedDate,
action: null,
}));
groupRoleByUser();
}
}
async function getUserList() {
const { data } = await useFetch("/api/devtool/user/list", {
initialCache: false,
});
if (data.value.statusCode === 200) {
roleUserList.value = data.value.data.map((user) => ({
label: user.userUsername,
value: user.userUsername,
}));
}
}
function usersWithCommans(users) {
// Limit the number of users to 4 and add "..." if there are more than 4 users
const userList = users.map((u) => u.label);
return userList.length > 4
? userList.slice(0, 4).join(", ") + "..."
: userList.join(", ");
}
// Watch checkAllUser value
watch(
checkAllUser,
async (value) => {
if (value) {
showModalForm.value.users = roleUserList.value;
} else {
if (showModalForm.value.users.length === roleUserList.value.length) {
showModalForm.value.users = [];
}
}
},
{ immediate: true }
);
// Watch showModalForm.users value
watch(
showModalForm,
async (value) => {
if (value.users.length === roleUserList.value.length) {
checkAllUser.value = true;
} else {
checkAllUser.value = false;
}
},
{ deep: true }
);
// Open Modal Add or Edit User
const openModal = async (value, type) => {
modalType.value = type;
if (type == "edit") {
showModalForm.value = {
id: value.id,
name: value.name,
description: value.description,
users: value.users,
status: value.status,
};
} else {
showModalForm.value = {
id: "",
name: "",
description: "",
users: "",
status: "",
};
}
showModalUser.value = false;
showModal.value = true;
};
// Open Modal Add Role
const openModalUser = async () => {
showModalUserForm.value = {
username: "",
fullname: "",
email: "",
phone: "",
password: "",
role: "",
status: "",
};
showModal.value = false;
showModalUser.value = true;
};
// Close Modal Role
const closeModalUser = () => {
showModalUser.value = false;
showModal.value = true;
};
// Open Modal Delete User
const openModalDelete = async (value) => {
showModalDeleteForm.value.id = value.id;
showModalDeleteForm.value.name = value.name;
showModalDelete.value = true;
};
const saveUser = async () => {
const { data } = await useFetch("/api/devtool/user/add", {
initialCache: false,
method: "POST",
body: JSON.stringify({
...showModalUserForm.value,
module: "role",
}),
});
if (data.value.statusCode === 200) {
$swal.fire({
icon: "success",
title: "Success",
text: "User has been added successfully",
});
await getUserList();
showModalUser.value = false;
showModal.value = true;
} else {
$swal.fire({
icon: "error",
title: "Error",
text: data.value.message,
});
}
};
const saveRole = async () => {
if (modalType.value == "edit") {
const { data } = await useFetch("/api/devtool/role/edit", {
initialCache: false,
method: "POST",
body: JSON.stringify(showModalForm.value),
});
if (data.value.statusCode === 200) {
$swal.fire({
position: "center",
icon: "success",
title: "Success",
text: "Role has been updated successfully",
timer: 1000,
showConfirmButton: false,
});
await getRoleList();
showModal.value = false;
} else {
$swal.fire({
icon: "error",
title: "Error",
text: data.value.message,
});
}
} else {
const { data } = await useFetch("/api/devtool/role/add", {
initialCache: false,
method: "POST",
body: JSON.stringify({ ...showModalForm.value, module: "role" }),
});
if (data.value.statusCode === 200) {
$swal.fire({
position: "center",
icon: "success",
title: "Success",
text: "Role has been added",
timer: 1000,
showConfirmButton: false,
});
await getRoleList();
showModal.value = false;
} else {
$swal.fire({
icon: "error",
title: "Error",
text: data.value.message,
});
}
}
};
const deleteRole = async () => {
const { data } = await useFetch("/api/devtool/role/delete", {
initialCache: false,
method: "POST",
body: JSON.stringify({ id: showModalDeleteForm.value.id }),
});
if (data.value.statusCode === 200) {
$swal.fire({
position: "center",
icon: "success",
title: "Success",
text: "Role has been deleted",
timer: 1000,
showConfirmButton: false,
});
await getRoleList();
showModalDelete.value = false;
} else {
$swal.fire({
position: "center",
icon: "error",
title: "Error",
text: data.value.message,
});
}
};
function groupRoleByUser() {
roleListbyUser.value = roleList.value.reduce((acc, role) => {
const users = role.users.map((user) => user.userUsername);
if (acc[users]) {
acc[users].push(role);
} else {
acc[users] = [role];
}
return acc;
}, {});
}
</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 only accessible by admin users. You can manage users
here. You can also add new users. You can also change user roles.
</p>
</template>
</rs-card>
<rs-card>
<div class="pt-2">
<rs-tab fill>
<rs-tab-item title="All Role">
<div class="flex justify-end items-center mb-4">
<rs-button @click="openModal(null, 'add')">
<Icon name="material-symbols:add" class="mr-1"></Icon>
Add Role
</rs-button>
</div>
<rs-table
v-if="roleList && roleList.length > 0"
:data="roleList"
:options="{
variant: 'default',
striped: true,
borderless: true,
}"
advanced
>
<template v-slot:users="data">
{{ usersWithCommans(data.text) }}
</template>
<template v-slot:action="data">
<div
class="flex justify-center items-center"
v-if="data.value.role?.value != '1'"
>
<Icon
name="material-symbols:edit-outline-rounded"
class="text-primary hover:text-primary/90 cursor-pointer mr-1"
size="22"
@click="openModal(data.value, 'edit')"
></Icon>
<Icon
name="material-symbols:close-rounded"
class="text-primary hover:text-primary/90 cursor-pointer"
size="22"
@click="openModalDelete(data.value)"
></Icon>
</div>
<div class="flex justify-center items-center" v-else>-</div>
</template>
</rs-table>
</rs-tab-item>
</rs-tab>
<!-- <rs-tab-item title="Role Tree">
<div v-for="(value, index) in roleListbyUser">
{{ value }}
</div>
</rs-tab-item> -->
</div>
</rs-card>
<rs-modal
:title="modalType == 'edit' ? 'Edit Role' : 'Add Role'"
ok-title="Save"
:ok-callback="saveRole"
v-model="showModal"
:overlay-close="false"
>
<FormKit
type="text"
v-model="showModalForm.name"
label="Name"
validation="required"
validation-visibility="live"
/>
<FormKit
type="textarea"
v-model="showModalForm.description"
label="Description"
/>
<div class="flex justify-between items-center mb-2">
<label
class="formkit-label font-semibold text-gray-700 dark:text-gray-200 blockfont-semibold text-sm formkit-invalid:text-red-500 dark:formkit-invalid:text-danger"
for="input_4"
>
Users
</label>
<rs-button size="sm" @click="openModalUser"> Add User </rs-button>
</div>
<v-select
class="formkit-vselect"
:options="roleUserList"
v-model="showModalForm.users"
multiple
></v-select>
<FormKit
type="checkbox"
v-model="checkAllUser"
label="All Users"
input-class="icon-check"
/>
<FormKit
type="select"
:options="statusDropdown"
v-model="showModalForm.status"
name="status"
label="Status"
/>
</rs-modal>
<!-- Modal Role -->
<rs-modal
title="Add User"
ok-title="Save"
cancel-title="Back"
:cancel-callback="closeModalUser"
:ok-callback="saveUser"
v-model="showModalUser"
:overlay-close="false"
>
<FormKit
type="text"
v-model="showModalUserForm.username"
name="username"
label="Username"
/>
<FormKit
type="text"
v-model="showModalUserForm.fullname"
name="fullname"
label="Fullname"
/>
<FormKit
type="text"
v-model="showModalUserForm.email"
name="email"
label="Email"
validation="email"
validation-visibility="dirty"
/>
<FormKit
type="mask"
v-model="showModalUserForm.phone"
name="phone"
label="Phone"
mask="###########"
/>
<FormKit
type="select"
:options="statusDropdown"
v-model="showModalUserForm.status"
name="status"
label="Status"
/>
</rs-modal>
<!-- Modal Delete Confirmation -->
<rs-modal
title="Delete Confirmation"
ok-title="Yes"
cancel-title="No"
:ok-callback="deleteRole"
v-model="showModalDelete"
:overlay-close="false"
>
<p>
Are you sure want to delete this role ({{ showModalDeleteForm.name }})?
</p>
</rs-modal>
</div>
</template>

View File

@@ -0,0 +1,497 @@
<script setup>
definePageMeta({
title: "User List",
middleware: ["auth"],
requiresAuth: true,
});
const { $swal } = useNuxtApp();
const userList = ref([]);
const userRoleList = ref([]);
const showModal = ref(false);
const showModalForm = ref({
username: "",
fullname: "",
email: "",
phone: "",
password: "",
role: "",
status: "",
});
const modalType = ref("");
const showModalRole = ref(false);
const showModalRoleForm = ref({
role: "",
description: "",
});
const showModalDelete = ref(false);
const showModalDeleteForm = ref({
username: "",
});
const statusDropdown = ref([
{ label: "Active", value: "ACTIVE" },
{ label: "Inactive", value: "INACTIVE" },
]);
const checkAllRole = ref(false);
const userListbyRole = ref([]);
// Call API
// onMounted(async () => {
// await getUserList();
// await getRoleList();
// });
await getUserList();
await getRoleList();
async function getUserList() {
const { data } = await useFetch("/api/devtool/user/list", {
initialCache: false,
});
// Rename the key
if (data.value?.statusCode === 200) {
userList.value = data.value.data.map((user) => ({
username: user.userUsername,
fullname: user.userFullName,
email: user.userEmail,
phone: user.userPhone,
role: user.roles.map((r) => {
return {
label: r.role.roleName,
value: r.role.roleID,
};
}),
status: user.userStatus,
action: null,
}));
groupUserByRole();
}
}
async function getRoleList() {
const { data } = await useFetch("/api/devtool/role/list", {
initialCache: false,
});
if (data.value.statusCode === 200) {
userRoleList.value = data.value.data.map((role) => ({
label: role.roleName,
value: role.roleID,
}));
}
}
function roleWithComma(role) {
// Limit the number of role to 4 and add "..." if there are more than 4 role
const roleList = role.map((r) => r.label);
return roleList.length > 4
? roleList.slice(0, 4).join(", ") + "..."
: roleList.join(", ");
}
// Watch checkAllRole value
watch(
checkAllRole,
async (value) => {
if (value) {
showModalForm.value.role = userRoleList.value;
} else {
if (showModalForm.value.role.length === userRoleList.value.length) {
showModalForm.value.role = [];
}
}
},
{ immediate: true }
);
// Watch showModalForm.role value
watch(
showModalForm,
async (value) => {
if (value.role.length === userRoleList.value.length) {
checkAllRole.value = true;
} else {
checkAllRole.value = false;
}
},
{ deep: true }
);
// Open Modal Add or Edit User
const openModal = async (value, type) => {
modalType.value = type;
if (type == "edit") {
showModalForm.value.username = value.username;
showModalForm.value.fullname = value.fullname;
showModalForm.value.phone = value.phone;
showModalForm.value.email = value.email;
showModalForm.value.role = value.role;
showModalForm.value.status = value.status;
} else {
showModalForm.value.username = "";
showModalForm.value.fullname = "";
showModalForm.value.phone = "";
showModalForm.value.email = "";
showModalForm.value.role = "";
showModalForm.value.status = "";
}
showModalRole.value = false;
showModal.value = true;
};
// Open Modal Add Role
const openModalRole = async () => {
showModalRoleForm.value.role = "";
showModalRoleForm.value.description = "";
showModal.value = false;
showModalRole.value = true;
};
// Close Modal Role
const closeModalRole = () => {
showModalRole.value = false;
showModal.value = true;
};
// Open Modal Delete User
const openModalDelete = async (value) => {
showModalDeleteForm.value.username = value.username;
showModalDelete.value = true;
};
const checkDeveloperRole = (role) => {
return role.some((r) => r.label === "Developer");
};
const saveUser = async () => {
if (modalType.value == "add") {
const { data } = await useFetch("/api/devtool/user/add", {
initialCache: false,
method: "POST",
body: JSON.stringify({
...showModalForm.value,
module: "user",
}),
});
if (data.value.statusCode === 200) {
$swal.fire({
position: "center",
icon: "success",
title: "Success",
text: "User has been added",
timer: 1000,
showConfirmButton: false,
});
await getUserList();
showModal.value = false;
} else {
$swal.fire({
position: "center",
icon: "error",
title: "Error",
text: data.value.message,
});
}
} else {
const { data } = await useFetch("/api/devtool/user/edit", {
initialCache: false,
method: "POST",
body: JSON.stringify(showModalForm.value),
});
if (data.value.statusCode === 200) {
$swal.fire({
position: "center",
icon: "success",
title: "Success",
text: "User has been updated",
timer: 1000,
showConfirmButton: false,
});
await getUserList();
showModal.value = false;
} else {
$swal.fire({
position: "center",
icon: "error",
title: "Error",
text: data.value.message,
});
}
}
};
const deleteUser = async () => {
const { data } = await useFetch("/api/devtool/user/delete", {
initialCache: false,
method: "POST",
body: JSON.stringify({ username: showModalDeleteForm.value.username }),
});
if (data.value.statusCode === 200) {
$swal.fire({
position: "center",
icon: "success",
title: "Success",
text: "User has been deleted",
timer: 1000,
showConfirmButton: false,
});
await getUserList();
showModalDelete.value = false;
} else {
$swal.fire({
position: "center",
icon: "error",
title: "Error",
text: data.value.message,
});
}
};
const saveRole = async () => {
if (
showModalRoleForm.value.role == "" ||
showModalRoleForm.value.role == " "
) {
return false;
}
const { data } = await useFetch("/api/devtool/role/add", {
method: "POST",
body: JSON.stringify({
name: showModalRoleForm.value.role,
description: showModalRoleForm.value.description,
module: "user",
}),
});
if (data.value.statusCode === 200) {
$swal.fire({
position: "center",
title: "Success",
text: data.value.message,
icon: "success",
timer: 1000,
showConfirmButton: false,
});
await getRoleList();
closeModalRole();
} else {
$swal.fire({
position: "center",
title: "Error",
text: data.value.message,
icon: "error",
});
}
};
function groupUserByRole() {
userListbyRole.value = userList.value.reduce((acc, cur) => {
const { role } = cur;
if (!acc[role.value]) {
acc[role.value] = {
role: role,
users: [],
};
}
acc[role.value].users.push(cur);
return acc;
}, {});
}
</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 only accessible by admin users. You can manage users
here. You can also add new users. You can also change user roles.
</p>
</template>
</rs-card>
<rs-card>
<div class="pt-2">
<rs-tab fill>
<rs-tab-item title="All User">
<div class="flex justify-end items-center mb-4">
<rs-button @click="openModal(null, 'add')">
<Icon name="material-symbols:add" class="mr-1"></Icon>
Add User
</rs-button>
</div>
<rs-table
v-if="userList && userList.length > 0"
:data="userList"
:options="{
variant: 'default',
striped: true,
borderless: true,
}"
advanced
>
<template v-slot:role="data">
<!-- {{ data.text?.label }} -->
{{ roleWithComma(data.text) }}
</template>
<template v-slot:action="data">
<div
class="flex justify-center items-center"
v-if="!checkDeveloperRole(data.value.role)"
>
<Icon
name="material-symbols:edit-outline-rounded"
class="text-primary hover:text-primary/90 cursor-pointer mr-1"
size="22"
@click="openModal(data.value, 'edit')"
></Icon>
<Icon
name="material-symbols:close-rounded"
class="text-primary hover:text-primary/90 cursor-pointer"
size="22"
@click="openModalDelete(data.value)"
></Icon>
</div>
<div class="flex justify-center items-center" v-else>-</div>
</template>
</rs-table>
</rs-tab-item>
</rs-tab>
<!-- <rs-tab-item title="User Tree">
<div v-for="(value, index) in userListbyRole">
{{ value }}
</div>
</rs-tab-item> -->
</div>
</rs-card>
<rs-modal
:title="modalType == 'edit' ? 'Edit User' : 'Add User'"
ok-title="Save"
:ok-callback="saveUser"
v-model="showModal"
:overlay-close="false"
>
<FormKit
type="text"
v-model="showModalForm.username"
name="username"
label="Username"
:disabled="modalType == 'edit' ? true : false"
/>
<FormKit
type="text"
v-model="showModalForm.fullname"
name="fullname"
label="Fullname"
/>
<FormKit
type="text"
v-model="showModalForm.email"
name="email"
label="Email"
validation="email"
validation-visibility="dirty"
/>
<FormKit
type="mask"
v-model="showModalForm.phone"
name="phone"
label="Phone"
mask="###########"
/>
<div class="flex justify-between items-center mb-2">
<label
class="formkit-label flex items-center gap-x-4 font-semibold text-gray-700 dark:text-gray-200 blockfont-semibold text-sm formkit-invalid:text-red-500 dark:formkit-invalid:text-danger"
for="input_4"
>
Role
</label>
<rs-button size="sm" @click="openModalRole"> Add Role </rs-button>
</div>
<v-select
class="formkit-vselect"
:options="userRoleList"
v-model="showModalForm.role"
multiple
></v-select>
<FormKit
type="checkbox"
v-model="checkAllRole"
label="All Role"
input-class="icon-check"
/>
<FormKit
type="select"
:options="statusDropdown"
v-model="showModalForm.status"
name="status"
label="Status"
/>
</rs-modal>
<!-- Modal Role -->
<rs-modal
title="Add Role"
ok-title="Save"
cancel-title="Back"
:cancel-callback="closeModalRole"
:ok-callback="saveRole"
v-model="showModalRole"
:overlay-close="false"
>
<FormKit
type="text"
v-model="showModalRoleForm.role"
label="Name"
validation="required"
validation-visibility="live"
/>
<FormKit
type="textarea"
v-model="showModalRoleForm.description"
label="Description"
/>
</rs-modal>
<!-- Modal Delete Confirmation -->
<rs-modal
title="Delete Confirmation"
ok-title="Yes"
cancel-title="No"
:ok-callback="deleteUser"
v-model="showModalDelete"
:overlay-close="false"
>
<p>
Are you sure want to delete this user ({{
showModalDeleteForm.username
}})?
</p>
</rs-modal>
</div>
</template>