Add mantis summaries and start of email summaries... Need to figure out a way to get the emails since MS block IMAP :(
This commit is contained in:
parent
2d11d0bd79
commit
2ad9a63582
18 changed files with 1993 additions and 577 deletions
|
@ -48,6 +48,53 @@
|
|||
<q-item-label caption>Create a new form</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item
|
||||
clickable
|
||||
v-ripple
|
||||
:to="{ name: 'mantisSummaries' }"
|
||||
exact
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="summarize" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>Mantis Summaries</q-item-label>
|
||||
<q-item-label caption>View daily summaries</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item
|
||||
clickable
|
||||
v-ripple
|
||||
:to="{ name: 'emailSummaries' }"
|
||||
exact
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="email" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>Email Summaries</q-item-label>
|
||||
<q-item-label caption>View email summaries</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item
|
||||
clickable
|
||||
to="/settings" exact
|
||||
>
|
||||
<q-item-section
|
||||
avatar
|
||||
>
|
||||
<q-icon name="settings" />
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
<q-item-label>Settings</q-item-label>
|
||||
<q-item-label caption>Manage application settings</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
</q-list>
|
||||
</q-drawer>
|
||||
|
||||
|
|
201
src/pages/EmailSummariesPage.vue
Normal file
201
src/pages/EmailSummariesPage.vue
Normal file
|
@ -0,0 +1,201 @@
|
|||
<template>
|
||||
<q-page padding>
|
||||
<q-card flat bordered>
|
||||
<q-card-section class="row items-center justify-between">
|
||||
<div class="text-h6">Email Summaries</div>
|
||||
<q-btn
|
||||
label="Generate Email Summary"
|
||||
color="primary"
|
||||
@click="generateSummary"
|
||||
:loading="generating"
|
||||
:disable="generating"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section v-if="generationError">
|
||||
<q-banner inline-actions class="text-white bg-red">
|
||||
<template v-slot:avatar>
|
||||
<q-icon name="error" />
|
||||
</template>
|
||||
{{ generationError }}
|
||||
</q-banner>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-if="loading">
|
||||
<q-spinner-dots size="40px" color="primary" />
|
||||
<span class="q-ml-md">Loading summaries...</span>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-if="error && !generationError">
|
||||
<q-banner inline-actions class="text-white bg-red">
|
||||
<template v-slot:avatar>
|
||||
<q-icon name="error" />
|
||||
</template>
|
||||
{{ error }}
|
||||
</q-banner>
|
||||
</q-card-section>
|
||||
|
||||
<q-list separator v-if="!loading && !error && summaries.length > 0">
|
||||
<q-item v-for="summary in summaries" :key="summary.id">
|
||||
<q-item-section>
|
||||
<q-item-label class="text-weight-bold">{{ formatDate(summary.summaryDate) }}</q-item-label>
|
||||
<q-item-label caption>Generated: {{ formatDateTime(summary.generatedAt) }}</q-item-label>
|
||||
<q-item-label class="q-mt-sm markdown-content" v-html="parseMarkdown(summary.summaryText)"></q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
|
||||
<q-card-section v-if="totalPages > 1" class="flex flex-center q-mt-md">
|
||||
<q-pagination
|
||||
v-model="currentPage"
|
||||
:max="totalPages"
|
||||
@update:model-value="fetchSummaries"
|
||||
direction-links
|
||||
flat
|
||||
color="primary"
|
||||
active-color="primary"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-if="!loading && !error && summaries.length === 0">
|
||||
<div class="text-center text-grey">No summaries found.</div>
|
||||
</q-card-section>
|
||||
|
||||
</q-card>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { date, useQuasar } from 'quasar'; // Import useQuasar
|
||||
import axios from 'axios';
|
||||
import { marked } from 'marked';
|
||||
|
||||
const $q = useQuasar(); // Initialize Quasar plugin usage
|
||||
const summaries = ref([]);
|
||||
const loading = ref(true);
|
||||
const error = ref(null);
|
||||
const generating = ref(false); // State for generation button
|
||||
const generationError = ref(null); // State for generation error
|
||||
const currentPage = ref(1);
|
||||
const itemsPerPage = ref(10); // Or your desired page size
|
||||
const totalItems = ref(0);
|
||||
|
||||
// Create a custom renderer
|
||||
const renderer = new marked.Renderer();
|
||||
const linkRenderer = renderer.link;
|
||||
renderer.link = (href, title, text) => {
|
||||
const html = linkRenderer.call(renderer, href, title, text);
|
||||
// Add target="_blank" to the link
|
||||
return html.replace(/^<a /, '<a target="_blank" rel="noopener noreferrer" ');
|
||||
};
|
||||
|
||||
const fetchSummaries = async (page = 1) => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
// *** CHANGED API ENDPOINT ***
|
||||
const response = await axios.get(`/api/email-summaries`, {
|
||||
params: {
|
||||
page: page,
|
||||
limit: itemsPerPage.value
|
||||
}
|
||||
});
|
||||
summaries.value = response.data.summaries;
|
||||
totalItems.value = response.data.total;
|
||||
currentPage.value = page;
|
||||
} catch (err) {
|
||||
console.error('Error fetching Email summaries:', err);
|
||||
error.value = err.response?.data?.error || 'Failed to load summaries. Please try again later.';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const generateSummary = async () => {
|
||||
generating.value = true;
|
||||
generationError.value = null;
|
||||
error.value = null; // Clear previous loading errors
|
||||
try {
|
||||
// *** CHANGED API ENDPOINT ***
|
||||
await axios.post('/api/email-summaries/generate');
|
||||
$q.notify({
|
||||
color: 'positive',
|
||||
icon: 'check_circle',
|
||||
// *** CHANGED MESSAGE ***
|
||||
message: 'Email summary generation started successfully. It may take a few moments to appear.',
|
||||
});
|
||||
// Optionally, refresh the list after a short delay or immediately
|
||||
// Consider that generation might be async on the backend
|
||||
setTimeout(() => fetchSummaries(1), 3000); // Refresh after 3 seconds
|
||||
} catch (err) {
|
||||
console.error('Error generating Email summary:', err);
|
||||
// *** CHANGED MESSAGE ***
|
||||
generationError.value = err.response?.data?.error || 'Failed to start email summary generation.';
|
||||
$q.notify({
|
||||
color: 'negative',
|
||||
icon: 'error',
|
||||
message: generationError.value,
|
||||
});
|
||||
} finally {
|
||||
generating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
// Assuming dateString is YYYY-MM-DD
|
||||
return date.formatDate(dateString + 'T00:00:00', 'DD MMMM YYYY');
|
||||
};
|
||||
|
||||
const formatDateTime = (dateTimeString) => {
|
||||
return date.formatDate(dateTimeString, 'DD MMMM YYYY HH:mm');
|
||||
};
|
||||
|
||||
const parseMarkdown = (markdownText) => {
|
||||
if (!markdownText) return '';
|
||||
// Use the custom renderer with marked
|
||||
return marked(markdownText, { renderer });
|
||||
};
|
||||
|
||||
const totalPages = computed(() => {
|
||||
return Math.ceil(totalItems.value / itemsPerPage.value);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
fetchSummaries(currentPage.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.markdown-content :deep(table) {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.markdown-content :deep(th),
|
||||
.markdown-content :deep(td) {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown-content :deep(th) {
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.markdown-content :deep(a) {
|
||||
color: var(--q-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-content :deep(a:hover) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Add any specific styles if needed */
|
||||
</style>
|
197
src/pages/MantisSummariesPage.vue
Normal file
197
src/pages/MantisSummariesPage.vue
Normal file
|
@ -0,0 +1,197 @@
|
|||
<template>
|
||||
<q-page padding>
|
||||
<q-card flat bordered>
|
||||
<q-card-section class="row items-center justify-between">
|
||||
<div class="text-h6">Mantis Summaries</div>
|
||||
<q-btn
|
||||
label="Generate Today's Summary"
|
||||
color="primary"
|
||||
@click="generateSummary"
|
||||
:loading="generating"
|
||||
:disable="generating"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section v-if="generationError">
|
||||
<q-banner inline-actions class="text-white bg-red">
|
||||
<template v-slot:avatar>
|
||||
<q-icon name="error" />
|
||||
</template>
|
||||
{{ generationError }}
|
||||
</q-banner>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-if="loading">
|
||||
<q-spinner-dots size="40px" color="primary" />
|
||||
<span class="q-ml-md">Loading summaries...</span>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-if="error && !generationError">
|
||||
<q-banner inline-actions class="text-white bg-red">
|
||||
<template v-slot:avatar>
|
||||
<q-icon name="error" />
|
||||
</template>
|
||||
{{ error }}
|
||||
</q-banner>
|
||||
</q-card-section>
|
||||
|
||||
<q-list separator v-if="!loading && !error && summaries.length > 0">
|
||||
<q-item v-for="summary in summaries" :key="summary.id">
|
||||
<q-item-section>
|
||||
<q-item-label class="text-weight-bold">{{ formatDate(summary.summaryDate) }}</q-item-label>
|
||||
<q-item-label caption>Generated: {{ formatDateTime(summary.generatedAt) }}</q-item-label>
|
||||
<q-item-label class="q-mt-sm markdown-content" v-html="parseMarkdown(summary.summaryText)"></q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
|
||||
<q-card-section v-if="totalPages > 1" class="flex flex-center q-mt-md">
|
||||
<q-pagination
|
||||
v-model="currentPage"
|
||||
:max="totalPages"
|
||||
@update:model-value="fetchSummaries"
|
||||
direction-links
|
||||
flat
|
||||
color="primary"
|
||||
active-color="primary"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-if="!loading && !error && summaries.length === 0">
|
||||
<div class="text-center text-grey">No summaries found.</div>
|
||||
</q-card-section>
|
||||
|
||||
</q-card>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { date, useQuasar } from 'quasar'; // Import useQuasar
|
||||
import axios from 'axios';
|
||||
import { marked } from 'marked';
|
||||
|
||||
const $q = useQuasar(); // Initialize Quasar plugin usage
|
||||
const summaries = ref([]);
|
||||
const loading = ref(true);
|
||||
const error = ref(null);
|
||||
const generating = ref(false); // State for generation button
|
||||
const generationError = ref(null); // State for generation error
|
||||
const currentPage = ref(1);
|
||||
const itemsPerPage = ref(10); // Or your desired page size
|
||||
const totalItems = ref(0);
|
||||
|
||||
// Create a custom renderer
|
||||
const renderer = new marked.Renderer();
|
||||
const linkRenderer = renderer.link;
|
||||
renderer.link = (href, title, text) => {
|
||||
const html = linkRenderer.call(renderer, href, title, text);
|
||||
// Add target="_blank" to the link
|
||||
return html.replace(/^<a /, '<a target="_blank" rel="noopener noreferrer" ');
|
||||
};
|
||||
|
||||
const fetchSummaries = async (page = 1) => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await axios.get(`/api/mantis-summaries`, {
|
||||
params: {
|
||||
page: page,
|
||||
limit: itemsPerPage.value
|
||||
}
|
||||
});
|
||||
summaries.value = response.data.summaries;
|
||||
totalItems.value = response.data.total;
|
||||
currentPage.value = page;
|
||||
} catch (err) {
|
||||
console.error('Error fetching Mantis summaries:', err);
|
||||
error.value = err.response?.data?.error || 'Failed to load summaries. Please try again later.';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const generateSummary = async () => {
|
||||
generating.value = true;
|
||||
generationError.value = null;
|
||||
error.value = null; // Clear previous loading errors
|
||||
try {
|
||||
await axios.post('/api/mantis-summaries/generate');
|
||||
$q.notify({
|
||||
color: 'positive',
|
||||
icon: 'check_circle',
|
||||
message: 'Summary generation started successfully. It may take a few moments to appear.',
|
||||
});
|
||||
// Optionally, refresh the list after a short delay or immediately
|
||||
// Consider that generation might be async on the backend
|
||||
setTimeout(() => fetchSummaries(1), 3000); // Refresh after 3 seconds
|
||||
} catch (err) {
|
||||
console.error('Error generating Mantis summary:', err);
|
||||
generationError.value = err.response?.data?.error || 'Failed to start summary generation.';
|
||||
$q.notify({
|
||||
color: 'negative',
|
||||
icon: 'error',
|
||||
message: generationError.value,
|
||||
});
|
||||
} finally {
|
||||
generating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
// Assuming dateString is YYYY-MM-DD
|
||||
return date.formatDate(dateString + 'T00:00:00', 'DD MMMM YYYY');
|
||||
};
|
||||
|
||||
const formatDateTime = (dateTimeString) => {
|
||||
return date.formatDate(dateTimeString, 'DD MMMM YYYY HH:mm');
|
||||
};
|
||||
|
||||
const parseMarkdown = (markdownText) => {
|
||||
if (!markdownText) return '';
|
||||
// Use the custom renderer with marked
|
||||
return marked(markdownText, { renderer });
|
||||
};
|
||||
|
||||
const totalPages = computed(() => {
|
||||
return Math.ceil(totalItems.value / itemsPerPage.value);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
fetchSummaries(currentPage.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.markdown-content :deep(table) {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.markdown-content :deep(th),
|
||||
.markdown-content :deep(td) {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown-content :deep(th) {
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.markdown-content :deep(a) {
|
||||
color: var(--q-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-content :deep(a:hover) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Add any specific styles if needed */
|
||||
</style>
|
163
src/pages/SettingsPage.vue
Normal file
163
src/pages/SettingsPage.vue
Normal file
|
@ -0,0 +1,163 @@
|
|||
<template>
|
||||
<q-page padding>
|
||||
<div class="q-gutter-md" style="max-width: 800px; margin: auto;">
|
||||
<h5 class="q-mt-none q-mb-md">Settings</h5>
|
||||
|
||||
<q-card flat bordered>
|
||||
<q-card-section>
|
||||
<div class="text-h6">Mantis Summary Prompt</div>
|
||||
<div class="text-caption text-grey q-mb-sm">
|
||||
Edit the prompt used to generate Mantis summaries. Use $DATE and $MANTIS_TICKETS as placeholders.
|
||||
</div>
|
||||
<q-input
|
||||
v-model="mantisPrompt"
|
||||
type="textarea"
|
||||
filled
|
||||
autogrow
|
||||
label="Mantis Prompt"
|
||||
:loading="loadingPrompt"
|
||||
:disable="savingPrompt"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn
|
||||
label="Save Prompt"
|
||||
color="primary"
|
||||
@click="saveMantisPrompt"
|
||||
:loading="savingPrompt"
|
||||
:disable="!mantisPrompt || loadingPrompt"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
|
||||
<q-card flat bordered>
|
||||
<q-card-section>
|
||||
<div class="text-h6">Email Summary Prompt</div>
|
||||
<div class="text-caption text-grey q-mb-sm">
|
||||
Edit the prompt used to generate Email summaries. Use $EMAIL_DATA as a placeholder for the JSON email array.
|
||||
</div>
|
||||
<q-input
|
||||
v-model="emailPrompt"
|
||||
type="textarea"
|
||||
filled
|
||||
autogrow
|
||||
label="Email Prompt"
|
||||
:loading="loadingEmailPrompt"
|
||||
:disable="savingEmailPrompt"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<q-btn
|
||||
label="Save Prompt"
|
||||
color="primary"
|
||||
@click="saveEmailPrompt"
|
||||
:loading="savingEmailPrompt"
|
||||
:disable="!emailPrompt || loadingEmailPrompt"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import axios from 'axios';
|
||||
|
||||
const $q = useQuasar();
|
||||
|
||||
const mantisPrompt = ref('');
|
||||
const loadingPrompt = ref(false);
|
||||
const savingPrompt = ref(false);
|
||||
|
||||
const fetchMantisPrompt = async () => {
|
||||
loadingPrompt.value = true;
|
||||
try {
|
||||
const response = await axios.get('/api/settings/mantisPrompt');
|
||||
mantisPrompt.value = response.data.value || ''; // Handle case where setting might not exist yet
|
||||
} catch (error) {
|
||||
console.error('Error fetching Mantis prompt:', error);
|
||||
$q.notify({
|
||||
color: 'negative',
|
||||
message: 'Failed to load Mantis prompt setting.',
|
||||
icon: 'report_problem'
|
||||
});
|
||||
} finally {
|
||||
loadingPrompt.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const saveMantisPrompt = async () => {
|
||||
savingPrompt.value = true;
|
||||
try {
|
||||
await axios.put('/api/settings/mantisPrompt', { value: mantisPrompt.value });
|
||||
$q.notify({
|
||||
color: 'positive',
|
||||
message: 'Mantis prompt updated successfully.',
|
||||
icon: 'check_circle'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error saving Mantis prompt:', error);
|
||||
$q.notify({
|
||||
color: 'negative',
|
||||
message: 'Failed to save Mantis prompt setting.',
|
||||
icon: 'report_problem'
|
||||
});
|
||||
} finally {
|
||||
savingPrompt.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const emailPrompt = ref('');
|
||||
const loadingEmailPrompt = ref(false);
|
||||
const savingEmailPrompt = ref(false);
|
||||
|
||||
const fetchEmailPrompt = async () => {
|
||||
loadingEmailPrompt.value = true;
|
||||
try {
|
||||
const response = await axios.get('/api/settings/emailPrompt');
|
||||
emailPrompt.value = response.data.value || ''; // Handle case where setting might not exist yet
|
||||
} catch (error) {
|
||||
console.error('Error fetching Email prompt:', error);
|
||||
$q.notify({
|
||||
color: 'negative',
|
||||
message: 'Failed to load Email prompt setting.',
|
||||
icon: 'report_problem'
|
||||
});
|
||||
} finally {
|
||||
loadingEmailPrompt.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const saveEmailPrompt = async () => {
|
||||
savingEmailPrompt.value = true;
|
||||
try {
|
||||
await axios.put('/api/settings/emailPrompt', { value: emailPrompt.value });
|
||||
$q.notify({
|
||||
color: 'positive',
|
||||
message: 'Email prompt updated successfully.',
|
||||
icon: 'check_circle'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error saving Email prompt:', error);
|
||||
$q.notify({
|
||||
color: 'negative',
|
||||
message: 'Failed to save Email prompt setting.',
|
||||
icon: 'report_problem'
|
||||
});
|
||||
} finally {
|
||||
savingEmailPrompt.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchMantisPrompt();
|
||||
fetchEmailPrompt();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Add any specific styles if needed */
|
||||
</style>
|
|
@ -8,7 +8,10 @@ const routes = [
|
|||
{ path: 'forms/new', name: 'formCreate', component: () => import('pages/FormCreatePage.vue') },
|
||||
{ path: 'forms/:id/edit', name: 'formEdit', component: () => import('pages/FormEditPage.vue'), props: true },
|
||||
{ path: 'forms/:id/fill', name: 'formFill', component: () => import('pages/FormFillPage.vue'), props: true },
|
||||
{ path: 'forms/:id/responses', name: 'formResponses', component: () => import('pages/FormResponsesPage.vue'), props: true }
|
||||
{ path: 'forms/:id/responses', name: 'formResponses', component: () => import('pages/FormResponsesPage.vue'), props: true },
|
||||
{ path: 'mantis-summaries', name: 'mantisSummaries', component: () => import('pages/MantisSummariesPage.vue') },
|
||||
{ path: 'email-summaries', name: 'emailSummaries', component: () => import('pages/EmailSummariesPage.vue') },
|
||||
{ path: 'settings', name: 'settings', component: () => import('pages/SettingsPage.vue') }
|
||||
]
|
||||
},
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue