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>
|
||||
Reference in New Issue
Block a user