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:
244
pages/devtool/api-editor/code/index.vue
Normal file
244
pages/devtool/api-editor/code/index.vue
Normal 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>
|
||||
327
pages/devtool/api-editor/index.vue
Normal file
327
pages/devtool/api-editor/index.vue
Normal 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>
|
||||
35
pages/devtool/code-playground/index.js
Normal file
35
pages/devtool/code-playground/index.js
Normal 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,
|
||||
};
|
||||
422
pages/devtool/code-playground/index.vue
Normal file
422
pages/devtool/code-playground/index.vue
Normal 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>
|
||||
7
pages/devtool/config/application-log/index.vue
Normal file
7
pages/devtool/config/application-log/index.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
27
pages/devtool/config/environment/index.vue
Normal file
27
pages/devtool/config/environment/index.vue
Normal 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>
|
||||
7
pages/devtool/config/index.vue
Normal file
7
pages/devtool/config/index.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
1108
pages/devtool/config/site-settings/index.vue
Normal file
1108
pages/devtool/config/site-settings/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
226
pages/devtool/content-editor/code/index.vue
Normal file
226
pages/devtool/content-editor/code/index.vue
Normal 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>
|
||||
247
pages/devtool/content-editor/index.vue
Normal file
247
pages/devtool/content-editor/index.vue
Normal 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>
|
||||
127
pages/devtool/content-editor/template/index.vue
Normal file
127
pages/devtool/content-editor/template/index.vue
Normal 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>
|
||||
33
pages/devtool/content-editor/template/view/[id]/index.vue
Normal file
33
pages/devtool/content-editor/template/view/[id]/index.vue
Normal 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>
|
||||
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>
|
||||
169
pages/devtool/orm/index.vue
Normal file
169
pages/devtool/orm/index.vue
Normal 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>
|
||||
315
pages/devtool/orm/table/create/index.vue
Normal file
315
pages/devtool/orm/table/create/index.vue
Normal 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>
|
||||
360
pages/devtool/orm/table/modify/[table].vue
Normal file
360
pages/devtool/orm/table/modify/[table].vue
Normal 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>
|
||||
79
pages/devtool/orm/view/[table]/index.vue
Normal file
79
pages/devtool/orm/view/[table]/index.vue
Normal 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>
|
||||
499
pages/devtool/user-management/role/index.vue
Normal file
499
pages/devtool/user-management/role/index.vue
Normal 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>
|
||||
497
pages/devtool/user-management/user/index.vue
Normal file
497
pages/devtool/user-management/user/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user