mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-04-09 06:25:35 -04:00
chore: migrate remaining pages to script setup (#7310)
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<section>
|
||||
<!-- Delete Dialog -->
|
||||
<BaseDialog
|
||||
v-model="deleteDialog"
|
||||
v-model="state.deleteDialog"
|
||||
:title="$t('settings.backup.delete-backup')"
|
||||
color="error"
|
||||
:icon="$globals.icons.alertCircle"
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<!-- Import Dialog -->
|
||||
<BaseDialog
|
||||
v-model="importDialog"
|
||||
v-model="state.importDialog"
|
||||
color="error"
|
||||
:title="$t('settings.backup.backup-restore')"
|
||||
:icon="$globals.icons.database"
|
||||
@@ -40,7 +40,7 @@
|
||||
</p>
|
||||
|
||||
<v-checkbox
|
||||
v-model="confirmImport"
|
||||
v-model="state.confirmImport"
|
||||
class="checkbox-top"
|
||||
color="error"
|
||||
hide-details
|
||||
@@ -50,7 +50,7 @@
|
||||
<v-card-actions class="justify-center pt-0">
|
||||
<BaseButton
|
||||
delete
|
||||
:disabled="!confirmImport || runningRestore"
|
||||
:disabled="!state.confirmImport || state.runningRestore"
|
||||
@click="restoreBackup(selected)"
|
||||
>
|
||||
<template #icon>
|
||||
@@ -63,7 +63,7 @@
|
||||
{{ selected }}
|
||||
</p>
|
||||
<v-progress-linear
|
||||
v-if="runningRestore"
|
||||
v-if="state.runningRestore"
|
||||
indeterminate
|
||||
/>
|
||||
</BaseDialog>
|
||||
@@ -81,7 +81,7 @@
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-2"
|
||||
:loading="runningBackup"
|
||||
:loading="state.runningBackup"
|
||||
@click="createBackup"
|
||||
>
|
||||
{{ $t("settings.backup.create-heading") }}
|
||||
@@ -96,13 +96,13 @@
|
||||
</v-toolbar>
|
||||
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:headers="state.headers"
|
||||
:items="backups.imports || []"
|
||||
class="elevation-0"
|
||||
:items-per-page="-1"
|
||||
hide-default-footer
|
||||
disable-pagination
|
||||
:search="search"
|
||||
:search="state.search"
|
||||
@click:row="setSelected"
|
||||
>
|
||||
<template #[`item.date`]="{ item }">
|
||||
@@ -115,7 +115,7 @@
|
||||
color="error"
|
||||
variant="text"
|
||||
@click.stop="
|
||||
deleteDialog = true;
|
||||
state.deleteDialog = true;
|
||||
deleteTarget = item.name;
|
||||
"
|
||||
>
|
||||
@@ -130,7 +130,7 @@
|
||||
/>
|
||||
<BaseButton
|
||||
small
|
||||
@click.stop="setSelected(item); importDialog = true"
|
||||
@click.stop="setSelected(item); state.importDialog = true"
|
||||
>
|
||||
<template #icon>
|
||||
{{ $globals.icons.backupRestore }}
|
||||
@@ -151,130 +151,108 @@
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useAdminApi } from "~/composables/api";
|
||||
import type { AllBackups } from "~/lib/api/types/admin";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
setup() {
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const i18n = useI18n();
|
||||
const auth = useMealieAuth();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug || auth.user.value?.groupSlug || "");
|
||||
const i18n = useI18n();
|
||||
|
||||
const adminApi = useAdminApi();
|
||||
const selected = ref("");
|
||||
const adminApi = useAdminApi();
|
||||
const selected = ref("");
|
||||
|
||||
const backups = ref<AllBackups>({
|
||||
imports: [],
|
||||
templates: [],
|
||||
});
|
||||
const backups = ref<AllBackups>({
|
||||
imports: [],
|
||||
templates: [],
|
||||
});
|
||||
|
||||
async function refreshBackups() {
|
||||
const { data } = await adminApi.backups.getAll();
|
||||
if (data) {
|
||||
backups.value = data;
|
||||
}
|
||||
}
|
||||
async function refreshBackups() {
|
||||
const { data } = await adminApi.backups.getAll();
|
||||
if (data) {
|
||||
backups.value = data;
|
||||
}
|
||||
}
|
||||
|
||||
async function createBackup() {
|
||||
state.runningBackup = true;
|
||||
const { data } = await adminApi.backups.create();
|
||||
async function createBackup() {
|
||||
state.runningBackup = true;
|
||||
const { data } = await adminApi.backups.create();
|
||||
|
||||
if (data?.error === false) {
|
||||
refreshBackups();
|
||||
alert.success(i18n.t("settings.backup.backup-created"));
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("settings.backup.error-creating-backup-see-log-file"));
|
||||
}
|
||||
state.runningBackup = false;
|
||||
}
|
||||
if (data?.error === false) {
|
||||
refreshBackups();
|
||||
alert.success(i18n.t("settings.backup.backup-created"));
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("settings.backup.error-creating-backup-see-log-file"));
|
||||
}
|
||||
state.runningBackup = false;
|
||||
}
|
||||
|
||||
async function restoreBackup(fileName: string) {
|
||||
state.runningRestore = true;
|
||||
const { error } = await adminApi.backups.restore(fileName);
|
||||
async function restoreBackup(fileName: string) {
|
||||
state.runningRestore = true;
|
||||
const { error } = await adminApi.backups.restore(fileName);
|
||||
|
||||
if (error) {
|
||||
console.log(error);
|
||||
state.importDialog = false;
|
||||
state.runningRestore = false;
|
||||
alert.error(i18n.t("settings.backup.restore-fail"));
|
||||
}
|
||||
else {
|
||||
alert.success(i18n.t("settings.backup.restore-success"));
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
if (error) {
|
||||
console.log(error);
|
||||
state.importDialog = false;
|
||||
state.runningRestore = false;
|
||||
alert.error(i18n.t("settings.backup.restore-fail"));
|
||||
}
|
||||
else {
|
||||
alert.success(i18n.t("settings.backup.restore-success"));
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
const deleteTarget = ref("");
|
||||
const deleteTarget = ref("");
|
||||
|
||||
async function deleteBackup() {
|
||||
const { data } = await adminApi.backups.delete(deleteTarget.value);
|
||||
async function deleteBackup() {
|
||||
const { data } = await adminApi.backups.delete(deleteTarget.value);
|
||||
|
||||
if (!data?.error) {
|
||||
alert.success(i18n.t("settings.backup.backup-deleted"));
|
||||
refreshBackups();
|
||||
}
|
||||
}
|
||||
if (!data?.error) {
|
||||
alert.success(i18n.t("settings.backup.backup-deleted"));
|
||||
refreshBackups();
|
||||
}
|
||||
}
|
||||
|
||||
const state = reactive({
|
||||
confirmImport: false,
|
||||
deleteDialog: false,
|
||||
createDialog: false,
|
||||
importDialog: false,
|
||||
runningBackup: false,
|
||||
runningRestore: false,
|
||||
search: "",
|
||||
headers: [
|
||||
{ title: i18n.t("general.name"), value: "name" },
|
||||
{ title: i18n.t("general.created"), value: "date" },
|
||||
{ title: i18n.t("export.size"), value: "size" },
|
||||
{ title: "", value: "actions", align: "right" },
|
||||
],
|
||||
});
|
||||
const state = reactive({
|
||||
confirmImport: false,
|
||||
deleteDialog: false,
|
||||
createDialog: false,
|
||||
importDialog: false,
|
||||
runningBackup: false,
|
||||
runningRestore: false,
|
||||
search: "",
|
||||
headers: [
|
||||
{ title: i18n.t("general.name"), value: "name" },
|
||||
{ title: i18n.t("general.created"), value: "date" },
|
||||
{ title: i18n.t("export.size"), value: "size" },
|
||||
{ title: "", value: "actions", align: "right" },
|
||||
],
|
||||
});
|
||||
|
||||
function setSelected(data: { name: string; date: string }) {
|
||||
if (!data.name) {
|
||||
return;
|
||||
}
|
||||
selected.value = data.name;
|
||||
}
|
||||
function setSelected(data: { name: string; date: string }) {
|
||||
if (!data.name) {
|
||||
return;
|
||||
}
|
||||
selected.value = data.name;
|
||||
}
|
||||
|
||||
const backupsFileNameDownload = (fileName: string) => `api/admin/backups/${fileName}`;
|
||||
const backupsFileNameDownload = (fileName: string) => `api/admin/backups/${fileName}`;
|
||||
|
||||
useSeoMeta({
|
||||
title: i18n.t("sidebar.backups"),
|
||||
});
|
||||
useSeoMeta({
|
||||
title: i18n.t("sidebar.backups"),
|
||||
});
|
||||
|
||||
onMounted(refreshBackups);
|
||||
onMounted(refreshBackups);
|
||||
|
||||
return {
|
||||
groupSlug,
|
||||
restoreBackup,
|
||||
selected,
|
||||
...toRefs(state),
|
||||
backups,
|
||||
createBackup,
|
||||
deleteBackup,
|
||||
deleteTarget,
|
||||
setSelected,
|
||||
refreshBackups,
|
||||
backupsFileNameDownload,
|
||||
};
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: useI18n().t("sidebar.backups"),
|
||||
};
|
||||
},
|
||||
useHead({
|
||||
title: i18n.t("sidebar.backups"),
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -83,69 +83,53 @@
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useAdminApi } from "~/composables/api";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
setup() {
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const api = useAdminApi();
|
||||
const i18n = useI18n();
|
||||
|
||||
// Set page title
|
||||
useSeoMeta({
|
||||
title: i18n.t("admin.debug-openai-services"),
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const response = ref("");
|
||||
|
||||
const uploadForm = ref<VForm | null>(null);
|
||||
const uploadedImage = ref<Blob | File>();
|
||||
const uploadedImageName = ref<string>("");
|
||||
const uploadedImagePreviewUrl = ref<string>();
|
||||
|
||||
function uploadImage(fileObject: File) {
|
||||
uploadedImage.value = fileObject;
|
||||
uploadedImageName.value = fileObject.name;
|
||||
uploadedImagePreviewUrl.value = URL.createObjectURL(fileObject);
|
||||
}
|
||||
|
||||
function clearImage() {
|
||||
uploadedImage.value = undefined;
|
||||
uploadedImageName.value = "";
|
||||
uploadedImagePreviewUrl.value = undefined;
|
||||
}
|
||||
|
||||
async function testOpenAI() {
|
||||
response.value = "";
|
||||
|
||||
loading.value = true;
|
||||
const { data } = await api.debug.debugOpenAI(uploadedImage.value);
|
||||
loading.value = false;
|
||||
|
||||
if (!data) {
|
||||
alert.error("Unable to test OpenAI services");
|
||||
}
|
||||
else {
|
||||
response.value = data.response || (data.success ? "Test Successful" : "Test Failed");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
response,
|
||||
uploadForm,
|
||||
uploadedImage,
|
||||
uploadedImagePreviewUrl,
|
||||
uploadImage,
|
||||
clearImage,
|
||||
testOpenAI,
|
||||
};
|
||||
},
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const api = useAdminApi();
|
||||
const i18n = useI18n();
|
||||
|
||||
// Set page title
|
||||
useSeoMeta({
|
||||
title: i18n.t("admin.debug-openai-services"),
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const response = ref("");
|
||||
|
||||
const uploadedImage = ref<Blob | File>();
|
||||
const uploadedImageName = ref<string>("");
|
||||
const uploadedImagePreviewUrl = ref<string>();
|
||||
|
||||
function uploadImage(fileObject: File) {
|
||||
uploadedImage.value = fileObject;
|
||||
uploadedImageName.value = fileObject.name;
|
||||
uploadedImagePreviewUrl.value = URL.createObjectURL(fileObject);
|
||||
}
|
||||
|
||||
function clearImage() {
|
||||
uploadedImage.value = undefined;
|
||||
uploadedImageName.value = "";
|
||||
uploadedImagePreviewUrl.value = undefined;
|
||||
}
|
||||
|
||||
async function testOpenAI() {
|
||||
response.value = "";
|
||||
|
||||
loading.value = true;
|
||||
const { data } = await api.debug.debugOpenAI(uploadedImage.value);
|
||||
loading.value = false;
|
||||
|
||||
if (!data) {
|
||||
alert.error("Unable to test OpenAI services");
|
||||
}
|
||||
else {
|
||||
response.value = data.response || (data.success ? "Test Successful" : "Test Failed");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<div class="d-flex align-center justify-center justify-md-start flex-wrap">
|
||||
<v-btn-toggle
|
||||
v-model="parser"
|
||||
v-model="state.parser"
|
||||
density="compact"
|
||||
mandatory="force"
|
||||
@change="processIngredient"
|
||||
@@ -38,7 +38,7 @@
|
||||
<v-card flat>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="ingredient"
|
||||
v-model="state.ingredient"
|
||||
:label="$t('admin.ingredient-text')"
|
||||
/>
|
||||
</v-card-text>
|
||||
@@ -55,9 +55,9 @@
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-container>
|
||||
<v-container v-if="results">
|
||||
<v-container v-if="state.results">
|
||||
<div
|
||||
v-if="parser !== 'brute' && getConfidence('average')"
|
||||
v-if="state.parser !== 'brute' && getConfidence('average')"
|
||||
class="d-flex"
|
||||
>
|
||||
<v-chip
|
||||
@@ -111,7 +111,7 @@
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import type { IngredientConfidence } from "~/lib/api/types/recipe";
|
||||
@@ -119,155 +119,139 @@ import type { Parser } from "~/lib/api/user/recipes/recipe";
|
||||
|
||||
type ConfidenceAttribute = "average" | "comment" | "name" | "unit" | "quantity" | "food";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
setup() {
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const api = useUserApi();
|
||||
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
ingredient: "",
|
||||
results: false,
|
||||
parser: "nlp" as Parser,
|
||||
});
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
// Set page title
|
||||
useSeoMeta({
|
||||
title: i18n.t("admin.parser"),
|
||||
});
|
||||
|
||||
const confidence = ref<IngredientConfidence>({});
|
||||
|
||||
function getColor(attribute: ConfidenceAttribute) {
|
||||
const percentage = getConfidence(attribute);
|
||||
if (percentage === undefined) return;
|
||||
|
||||
const p_as_num = parseFloat(percentage.replace("%", ""));
|
||||
|
||||
// Set color based off range
|
||||
if (p_as_num > 75) {
|
||||
return "success";
|
||||
}
|
||||
else if (p_as_num > 60) {
|
||||
return "warning";
|
||||
}
|
||||
else {
|
||||
return "error";
|
||||
}
|
||||
}
|
||||
|
||||
function getConfidence(attribute: ConfidenceAttribute) {
|
||||
if (!confidence.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const property = confidence.value[attribute];
|
||||
if (property !== undefined && property !== null) {
|
||||
return `${(+property * 100).toFixed(0)}%`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const tryText = [
|
||||
"2 tbsp minced cilantro, leaves and stems",
|
||||
"1 large yellow onion, coarsely chopped",
|
||||
"1 1/2 tsp garam masala",
|
||||
"1 inch piece fresh ginger, (peeled and minced)",
|
||||
"2 cups mango chunks, (2 large mangoes) (fresh or frozen)",
|
||||
];
|
||||
|
||||
function processTryText(str: string) {
|
||||
state.ingredient = str;
|
||||
processIngredient();
|
||||
}
|
||||
|
||||
async function processIngredient() {
|
||||
if (state.ingredient === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
state.loading = true;
|
||||
|
||||
const { data } = await api.recipes.parseIngredient(state.parser, state.ingredient);
|
||||
|
||||
if (data) {
|
||||
state.results = true;
|
||||
|
||||
if (data.confidence) confidence.value = data.confidence;
|
||||
|
||||
// TODO: Remove ts-ignore
|
||||
// ts-ignore because data will likely change significantly once I figure out how to return results
|
||||
// for the parser. For now we'll leave it like this
|
||||
properties.comment.value = data.ingredient.note || "";
|
||||
properties.quantity.value = data.ingredient.quantity || "";
|
||||
properties.unit.value = data.ingredient?.unit?.name || "";
|
||||
properties.food.value = data.ingredient?.food?.name || "";
|
||||
|
||||
(["comment", "quantity", "unit", "food"] as ConfidenceAttribute[]).forEach((property) => {
|
||||
const color = getColor(property);
|
||||
const confidence = getConfidence(property);
|
||||
if (color) {
|
||||
properties[property].color = color;
|
||||
}
|
||||
if (confidence) {
|
||||
properties[property].confidence = confidence;
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("events.something-went-wrong") as string);
|
||||
state.results = false;
|
||||
}
|
||||
state.loading = false;
|
||||
}
|
||||
|
||||
const api = useUserApi();
|
||||
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
ingredient: "",
|
||||
results: false,
|
||||
parser: "nlp" as Parser,
|
||||
});
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
// Set page title
|
||||
useSeoMeta({
|
||||
title: i18n.t("admin.parser"),
|
||||
});
|
||||
|
||||
const confidence = ref<IngredientConfidence>({});
|
||||
|
||||
function getColor(attribute: ConfidenceAttribute) {
|
||||
const percentage = getConfidence(attribute);
|
||||
if (percentage === undefined) return;
|
||||
|
||||
const p_as_num = parseFloat(percentage.replace("%", ""));
|
||||
|
||||
// Set color based off range
|
||||
if (p_as_num > 75) {
|
||||
return "success";
|
||||
}
|
||||
else if (p_as_num > 60) {
|
||||
return "warning";
|
||||
}
|
||||
else {
|
||||
return "error";
|
||||
}
|
||||
}
|
||||
|
||||
function getConfidence(attribute: ConfidenceAttribute) {
|
||||
if (!confidence.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const property = confidence.value[attribute];
|
||||
if (property !== undefined && property !== null) {
|
||||
return `${(+property * 100).toFixed(0)}%`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const tryText = [
|
||||
"2 tbsp minced cilantro, leaves and stems",
|
||||
"1 large yellow onion, coarsely chopped",
|
||||
"1 1/2 tsp garam masala",
|
||||
"1 inch piece fresh ginger, (peeled and minced)",
|
||||
"2 cups mango chunks, (2 large mangoes) (fresh or frozen)",
|
||||
];
|
||||
|
||||
function processTryText(str: string) {
|
||||
state.ingredient = str;
|
||||
processIngredient();
|
||||
}
|
||||
|
||||
async function processIngredient() {
|
||||
if (state.ingredient === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
state.loading = true;
|
||||
|
||||
const { data } = await api.recipes.parseIngredient(state.parser, state.ingredient);
|
||||
|
||||
if (data) {
|
||||
state.results = true;
|
||||
|
||||
if (data.confidence) confidence.value = data.confidence;
|
||||
|
||||
// TODO: Remove ts-ignore
|
||||
// ts-ignore because data will likely change significantly once I figure out how to return results
|
||||
// for the parser. For now we'll leave it like this
|
||||
properties.comment.value = data.ingredient.note || "";
|
||||
properties.quantity.value = data.ingredient.quantity || "";
|
||||
properties.unit.value = data.ingredient?.unit?.name || "";
|
||||
properties.food.value = data.ingredient?.food?.name || "";
|
||||
|
||||
(["comment", "quantity", "unit", "food"] as ConfidenceAttribute[]).forEach((property) => {
|
||||
const color = getColor(property);
|
||||
const confidence = getConfidence(property);
|
||||
if (color) {
|
||||
properties[property].color = color;
|
||||
}
|
||||
if (confidence) {
|
||||
properties[property].confidence = confidence;
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("events.something-went-wrong") as string);
|
||||
state.results = false;
|
||||
}
|
||||
state.loading = false;
|
||||
}
|
||||
|
||||
const properties = reactive({
|
||||
quantity: {
|
||||
subtitle: i18n.t("recipe.quantity"),
|
||||
value: "" as string | number,
|
||||
color: null,
|
||||
confidence: null,
|
||||
},
|
||||
unit: {
|
||||
subtitle: i18n.t("recipe.unit"),
|
||||
value: "",
|
||||
color: null,
|
||||
confidence: null,
|
||||
},
|
||||
food: {
|
||||
subtitle: i18n.t("shopping-list.food"),
|
||||
value: "",
|
||||
color: null,
|
||||
confidence: null,
|
||||
},
|
||||
comment: {
|
||||
subtitle: i18n.t("recipe.comment"),
|
||||
value: "",
|
||||
color: null,
|
||||
confidence: null,
|
||||
},
|
||||
});
|
||||
|
||||
const showConfidence = ref(false);
|
||||
|
||||
return {
|
||||
showConfidence,
|
||||
getColor,
|
||||
confidence,
|
||||
getConfidence,
|
||||
...toRefs(state),
|
||||
tryText,
|
||||
properties,
|
||||
processTryText,
|
||||
processIngredient,
|
||||
};
|
||||
const properties = reactive({
|
||||
quantity: {
|
||||
subtitle: i18n.t("recipe.quantity"),
|
||||
value: "" as string | number,
|
||||
color: null,
|
||||
confidence: null,
|
||||
},
|
||||
unit: {
|
||||
subtitle: i18n.t("recipe.unit"),
|
||||
value: "",
|
||||
color: null,
|
||||
confidence: null,
|
||||
},
|
||||
food: {
|
||||
subtitle: i18n.t("shopping-list.food"),
|
||||
value: "",
|
||||
color: null,
|
||||
confidence: null,
|
||||
},
|
||||
comment: {
|
||||
subtitle: i18n.t("recipe.comment"),
|
||||
value: "",
|
||||
color: null,
|
||||
confidence: null,
|
||||
},
|
||||
});
|
||||
|
||||
const showConfidence = ref(false);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -96,150 +96,136 @@
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useAdminApi } from "~/composables/api";
|
||||
import type { MaintenanceStorageDetails, MaintenanceSummary } from "~/lib/api/types/admin";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
setup() {
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
storageDetails: false,
|
||||
storageDetailsLoading: false,
|
||||
fetchingInfo: false,
|
||||
actionLoading: false,
|
||||
});
|
||||
|
||||
const adminApi = useAdminApi();
|
||||
const i18n = useI18n();
|
||||
|
||||
// Set page title
|
||||
useSeoMeta({
|
||||
title: i18n.t("admin.maintenance.page-title"),
|
||||
});
|
||||
|
||||
// ==========================================================================
|
||||
// General Info
|
||||
|
||||
const infoResults = ref<MaintenanceSummary>({
|
||||
dataDirSize: i18n.t("about.unknown-version"),
|
||||
cleanableDirs: 0,
|
||||
cleanableImages: 0,
|
||||
});
|
||||
|
||||
async function getSummary() {
|
||||
state.fetchingInfo = true;
|
||||
const { data } = await adminApi.maintenance.getInfo();
|
||||
|
||||
infoResults.value = data ?? {
|
||||
dataDirSize: i18n.t("about.unknown-version"),
|
||||
cleanableDirs: 0,
|
||||
cleanableImages: 0,
|
||||
};
|
||||
|
||||
state.fetchingInfo = false;
|
||||
}
|
||||
|
||||
const info = computed(() => {
|
||||
return [
|
||||
{
|
||||
name: i18n.t("admin.maintenance.info-description-data-dir-size"),
|
||||
value: infoResults.value.dataDirSize,
|
||||
},
|
||||
{
|
||||
name: i18n.t("admin.maintenance.info-description-cleanable-directories"),
|
||||
value: infoResults.value.cleanableDirs,
|
||||
},
|
||||
{
|
||||
name: i18n.t("admin.maintenance.info-description-cleanable-images"),
|
||||
value: infoResults.value.cleanableImages,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
// ==========================================================================
|
||||
// Storage Details
|
||||
|
||||
const storageTitles: { [key: string]: string } = {
|
||||
tempDirSize: i18n.t("admin.maintenance.storage.title-temporary-directory") as string,
|
||||
backupsDirSize: i18n.t("admin.maintenance.storage.title-backups-directory") as string,
|
||||
groupsDirSize: i18n.t("admin.maintenance.storage.title-groups-directory") as string,
|
||||
recipesDirSize: i18n.t("admin.maintenance.storage.title-recipes-directory") as string,
|
||||
userDirSize: i18n.t("admin.maintenance.storage.title-user-directory") as string,
|
||||
};
|
||||
|
||||
function storageDetailsText(key: string) {
|
||||
return storageTitles[key] ?? i18n.t("about.unknown-version");
|
||||
}
|
||||
|
||||
const storageDetails = ref<MaintenanceStorageDetails | null>(null);
|
||||
|
||||
async function openDetails() {
|
||||
state.storageDetailsLoading = true;
|
||||
state.storageDetails = true;
|
||||
|
||||
const { data } = await adminApi.maintenance.getStorageDetails();
|
||||
|
||||
if (data) {
|
||||
storageDetails.value = data;
|
||||
}
|
||||
|
||||
state.storageDetailsLoading = true;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Actions
|
||||
|
||||
async function handleCleanDirectories() {
|
||||
state.actionLoading = true;
|
||||
await adminApi.maintenance.cleanRecipeFolders();
|
||||
state.actionLoading = false;
|
||||
}
|
||||
|
||||
async function handleCleanImages() {
|
||||
state.actionLoading = true;
|
||||
await adminApi.maintenance.cleanImages();
|
||||
state.actionLoading = false;
|
||||
}
|
||||
|
||||
async function handleCleanTemp() {
|
||||
state.actionLoading = true;
|
||||
await adminApi.maintenance.cleanTemp();
|
||||
state.actionLoading = false;
|
||||
}
|
||||
|
||||
const actions = [
|
||||
{
|
||||
name: i18n.t("admin.maintenance.action-clean-directories-name"),
|
||||
handler: handleCleanDirectories,
|
||||
subtitle: i18n.t("admin.maintenance.action-clean-directories-description"),
|
||||
},
|
||||
{
|
||||
name: i18n.t("admin.maintenance.action-clean-temporary-files-name"),
|
||||
handler: handleCleanTemp,
|
||||
subtitle: i18n.t("admin.maintenance.action-clean-temporary-files-description"),
|
||||
},
|
||||
{
|
||||
name: i18n.t("admin.maintenance.action-clean-images-name"),
|
||||
handler: handleCleanImages,
|
||||
subtitle: i18n.t("admin.maintenance.action-clean-images-description"),
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
storageDetailsText,
|
||||
openDetails,
|
||||
storageDetails,
|
||||
state,
|
||||
info,
|
||||
getSummary,
|
||||
actions,
|
||||
};
|
||||
},
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
storageDetails: false,
|
||||
storageDetailsLoading: false,
|
||||
fetchingInfo: false,
|
||||
actionLoading: false,
|
||||
});
|
||||
|
||||
const adminApi = useAdminApi();
|
||||
const i18n = useI18n();
|
||||
|
||||
// Set page title
|
||||
useSeoMeta({
|
||||
title: i18n.t("admin.maintenance.page-title"),
|
||||
});
|
||||
|
||||
// ==========================================================================
|
||||
// General Info
|
||||
|
||||
const infoResults = ref<MaintenanceSummary>({
|
||||
dataDirSize: i18n.t("about.unknown-version"),
|
||||
cleanableDirs: 0,
|
||||
cleanableImages: 0,
|
||||
});
|
||||
|
||||
async function getSummary() {
|
||||
state.fetchingInfo = true;
|
||||
const { data } = await adminApi.maintenance.getInfo();
|
||||
|
||||
infoResults.value = data ?? {
|
||||
dataDirSize: i18n.t("about.unknown-version"),
|
||||
cleanableDirs: 0,
|
||||
cleanableImages: 0,
|
||||
};
|
||||
|
||||
state.fetchingInfo = false;
|
||||
}
|
||||
|
||||
const info = computed(() => {
|
||||
return [
|
||||
{
|
||||
name: i18n.t("admin.maintenance.info-description-data-dir-size"),
|
||||
value: infoResults.value.dataDirSize,
|
||||
},
|
||||
{
|
||||
name: i18n.t("admin.maintenance.info-description-cleanable-directories"),
|
||||
value: infoResults.value.cleanableDirs,
|
||||
},
|
||||
{
|
||||
name: i18n.t("admin.maintenance.info-description-cleanable-images"),
|
||||
value: infoResults.value.cleanableImages,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
// ==========================================================================
|
||||
// Storage Details
|
||||
|
||||
const storageTitles: { [key: string]: string } = {
|
||||
tempDirSize: i18n.t("admin.maintenance.storage.title-temporary-directory") as string,
|
||||
backupsDirSize: i18n.t("admin.maintenance.storage.title-backups-directory") as string,
|
||||
groupsDirSize: i18n.t("admin.maintenance.storage.title-groups-directory") as string,
|
||||
recipesDirSize: i18n.t("admin.maintenance.storage.title-recipes-directory") as string,
|
||||
userDirSize: i18n.t("admin.maintenance.storage.title-user-directory") as string,
|
||||
};
|
||||
|
||||
function storageDetailsText(key: string) {
|
||||
return storageTitles[key] ?? i18n.t("about.unknown-version");
|
||||
}
|
||||
|
||||
const storageDetails = ref<MaintenanceStorageDetails | null>(null);
|
||||
|
||||
async function openDetails() {
|
||||
state.storageDetailsLoading = true;
|
||||
state.storageDetails = true;
|
||||
|
||||
const { data } = await adminApi.maintenance.getStorageDetails();
|
||||
|
||||
if (data) {
|
||||
storageDetails.value = data;
|
||||
}
|
||||
|
||||
state.storageDetailsLoading = true;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Actions
|
||||
|
||||
async function handleCleanDirectories() {
|
||||
state.actionLoading = true;
|
||||
await adminApi.maintenance.cleanRecipeFolders();
|
||||
state.actionLoading = false;
|
||||
}
|
||||
|
||||
async function handleCleanImages() {
|
||||
state.actionLoading = true;
|
||||
await adminApi.maintenance.cleanImages();
|
||||
state.actionLoading = false;
|
||||
}
|
||||
|
||||
async function handleCleanTemp() {
|
||||
state.actionLoading = true;
|
||||
await adminApi.maintenance.cleanTemp();
|
||||
state.actionLoading = false;
|
||||
}
|
||||
|
||||
const actions = [
|
||||
{
|
||||
name: i18n.t("admin.maintenance.action-clean-directories-name"),
|
||||
handler: handleCleanDirectories,
|
||||
subtitle: i18n.t("admin.maintenance.action-clean-directories-description"),
|
||||
},
|
||||
{
|
||||
name: i18n.t("admin.maintenance.action-clean-temporary-files-name"),
|
||||
handler: handleCleanTemp,
|
||||
subtitle: i18n.t("admin.maintenance.action-clean-temporary-files-description"),
|
||||
},
|
||||
{
|
||||
name: i18n.t("admin.maintenance.action-clean-images-name"),
|
||||
handler: handleCleanImages,
|
||||
subtitle: i18n.t("admin.maintenance.action-clean-images-description"),
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -49,72 +49,58 @@
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import GroupPreferencesEditor from "~/components/Domain/Group/GroupPreferencesEditor.vue";
|
||||
import { useAdminApi } from "~/composables/api";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import type { VForm } from "vuetify/components";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
GroupPreferencesEditor,
|
||||
},
|
||||
setup() {
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const groupId = computed(() => route.params.id as string);
|
||||
|
||||
// ==============================================
|
||||
// New User Form
|
||||
|
||||
const refGroupEditForm = ref<VForm | null>(null);
|
||||
|
||||
const adminApi = useAdminApi();
|
||||
|
||||
const userError = ref(false);
|
||||
|
||||
const { data: group } = useLazyAsyncData(`get-household-${groupId.value}`, async () => {
|
||||
if (!groupId.value) {
|
||||
return null;
|
||||
}
|
||||
const { data, error } = await adminApi.groups.getOne(groupId.value);
|
||||
|
||||
if (error?.response?.status === 404) {
|
||||
alert.error(i18n.t("user.user-not-found"));
|
||||
userError.value = true;
|
||||
}
|
||||
return data;
|
||||
}, { watch: [groupId] });
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!refGroupEditForm.value?.validate() || group.value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { response, data } = await adminApi.groups.updateOne(group.value.id, group.value);
|
||||
if (response?.status === 200 && data) {
|
||||
if (group.value.slug !== data.slug) {
|
||||
// the slug updated, which invalidates the nav URLs
|
||||
window.location.reload();
|
||||
}
|
||||
group.value = data;
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("settings.settings-update-failed"));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
group,
|
||||
userError,
|
||||
refGroupEditForm,
|
||||
handleSubmit,
|
||||
};
|
||||
},
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
const route = useRoute();
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const groupId = computed(() => route.params.id as string);
|
||||
|
||||
// ==============================================
|
||||
// New User Form
|
||||
|
||||
const refGroupEditForm = ref<VForm | null>(null);
|
||||
|
||||
const adminApi = useAdminApi();
|
||||
|
||||
const userError = ref(false);
|
||||
|
||||
const { data: group } = useLazyAsyncData(`get-household-${groupId.value}`, async () => {
|
||||
if (!groupId.value) {
|
||||
return null;
|
||||
}
|
||||
const { data, error } = await adminApi.groups.getOne(groupId.value);
|
||||
|
||||
if (error?.response?.status === 404) {
|
||||
alert.error(i18n.t("user.user-not-found"));
|
||||
userError.value = true;
|
||||
}
|
||||
return data;
|
||||
}, { watch: [groupId] });
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!refGroupEditForm.value?.validate() || group.value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { response, data } = await adminApi.groups.updateOne(group.value.id, group.value);
|
||||
if (response?.status === 200 && data) {
|
||||
if (group.value.slug !== data.slug) {
|
||||
// the slug updated, which invalidates the nav URLs
|
||||
window.location.reload();
|
||||
}
|
||||
group.value = data;
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("settings.settings-update-failed"));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<BaseDialog
|
||||
v-model="createDialog"
|
||||
v-model="state.createDialog"
|
||||
:title="$t('group.create-group')"
|
||||
:icon="$globals.icons.group"
|
||||
can-submit
|
||||
@submit="createGroup(createGroupForm.data)"
|
||||
@submit="createGroup(state.createGroupForm.data)"
|
||||
>
|
||||
<template #activator />
|
||||
<v-card-text>
|
||||
<AutoForm
|
||||
v-model="createGroupForm.data"
|
||||
:update-mode="updateMode"
|
||||
:items="createGroupForm.items"
|
||||
v-model="state.createGroupForm.data"
|
||||
:update-mode="state.updateMode"
|
||||
:items="state.createGroupForm.items"
|
||||
/>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
|
||||
<BaseDialog
|
||||
v-model="confirmDialog"
|
||||
v-model="state.confirmDialog"
|
||||
:title="$t('general.confirm')"
|
||||
color="error"
|
||||
can-confirm
|
||||
@confirm="deleteGroup(deleteTarget)"
|
||||
@confirm="deleteGroup(state.deleteTarget)"
|
||||
>
|
||||
<template #activator />
|
||||
<v-card-text>
|
||||
@@ -43,14 +43,14 @@
|
||||
</v-toolbar>
|
||||
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:headers="state.headers"
|
||||
:items="groups || []"
|
||||
item-key="id"
|
||||
class="elevation-0"
|
||||
:items-per-page="-1"
|
||||
hide-default-footer
|
||||
disable-pagination
|
||||
:search="search"
|
||||
:search="state.search"
|
||||
@click:row="($event, { item }) => handleRowClick(item)"
|
||||
>
|
||||
<template #[`item.households`]="{ item }">
|
||||
@@ -73,8 +73,8 @@
|
||||
color="error"
|
||||
variant="text"
|
||||
@click.stop="
|
||||
confirmDialog = true;
|
||||
deleteTarget = item.id;
|
||||
state.confirmDialog = true;
|
||||
state.deleteTarget = item.id;
|
||||
"
|
||||
>
|
||||
<v-icon>
|
||||
@@ -92,75 +92,68 @@
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { fieldTypes } from "~/composables/forms";
|
||||
import { useGroups } from "~/composables/use-groups";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import type { GroupInDB } from "~/lib/api/types/user";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
setup() {
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const i18n = useI18n();
|
||||
const i18n = useI18n();
|
||||
|
||||
// Set page title
|
||||
useSeoMeta({
|
||||
title: i18n.t("group.manage-groups"),
|
||||
});
|
||||
useHead({
|
||||
title: i18n.t("group.manage-groups"),
|
||||
});
|
||||
|
||||
const { groups, refreshAllGroups, deleteGroup, createGroup } = useGroups();
|
||||
// Set page title
|
||||
useSeoMeta({
|
||||
title: i18n.t("group.manage-groups"),
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
createDialog: false,
|
||||
confirmDialog: false,
|
||||
deleteTarget: "",
|
||||
search: "",
|
||||
headers: [
|
||||
{
|
||||
title: i18n.t("group.group"),
|
||||
align: "start",
|
||||
sortable: false,
|
||||
value: "id",
|
||||
},
|
||||
{ title: i18n.t("general.name"), value: "name" },
|
||||
{ title: i18n.t("group.total-households"), value: "households" },
|
||||
{ title: i18n.t("user.total-users"), value: "users" },
|
||||
{ title: i18n.t("general.delete"), value: "actions" },
|
||||
],
|
||||
updateMode: false,
|
||||
createGroupForm: {
|
||||
items: [
|
||||
{
|
||||
label: i18n.t("group.group-name"),
|
||||
varName: "name",
|
||||
type: fieldTypes.TEXT,
|
||||
rules: [validators.required],
|
||||
},
|
||||
],
|
||||
data: {
|
||||
name: "",
|
||||
},
|
||||
const { groups, deleteGroup, createGroup } = useGroups();
|
||||
|
||||
const state = reactive({
|
||||
createDialog: false,
|
||||
confirmDialog: false,
|
||||
deleteTarget: "",
|
||||
search: "",
|
||||
headers: [
|
||||
{
|
||||
title: i18n.t("group.group"),
|
||||
align: "start",
|
||||
sortable: false,
|
||||
value: "id",
|
||||
},
|
||||
{ title: i18n.t("general.name"), value: "name" },
|
||||
{ title: i18n.t("group.total-households"), value: "households" },
|
||||
{ title: i18n.t("user.total-users"), value: "users" },
|
||||
{ title: i18n.t("general.delete"), value: "actions" },
|
||||
],
|
||||
updateMode: false,
|
||||
createGroupForm: {
|
||||
items: [
|
||||
{
|
||||
label: i18n.t("group.group-name"),
|
||||
varName: "name",
|
||||
type: fieldTypes.TEXT,
|
||||
rules: [validators.required],
|
||||
},
|
||||
});
|
||||
|
||||
function openDialog() {
|
||||
state.createDialog = true;
|
||||
state.createGroupForm.data.name = "";
|
||||
}
|
||||
|
||||
function handleRowClick(item: GroupInDB) {
|
||||
navigateTo(`/admin/manage/groups/${item.id}`);
|
||||
}
|
||||
|
||||
return { ...toRefs(state), groups, refreshAllGroups, deleteGroup, createGroup, openDialog, handleRowClick };
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: useI18n().t("group.manage-groups"),
|
||||
};
|
||||
],
|
||||
data: {
|
||||
name: "",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function openDialog() {
|
||||
state.createDialog = true;
|
||||
state.createGroupForm.data.name = "";
|
||||
}
|
||||
|
||||
function handleRowClick(item: GroupInDB) {
|
||||
navigateTo(`/admin/manage/groups/${item.id}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -67,73 +67,58 @@
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import HouseholdPreferencesEditor from "~/components/Domain/Household/HouseholdPreferencesEditor.vue";
|
||||
import { useGroups } from "~/composables/use-groups";
|
||||
import { useAdminApi } from "~/composables/api";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import type { VForm } from "vuetify/components";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
HouseholdPreferencesEditor,
|
||||
},
|
||||
setup() {
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const i18n = useI18n();
|
||||
|
||||
const { groups } = useGroups();
|
||||
const householdId = computed(() => route.params.id as string);
|
||||
|
||||
// ==============================================
|
||||
// New User Form
|
||||
|
||||
const refHouseholdEditForm = ref<VForm | null>(null);
|
||||
|
||||
const adminApi = useAdminApi();
|
||||
|
||||
const userError = ref(false);
|
||||
|
||||
const { data: household } = useAsyncData(`get-household-${householdId.value}`, async () => {
|
||||
if (!householdId.value) {
|
||||
return null;
|
||||
}
|
||||
const { data, error } = await adminApi.households.getOne(householdId.value);
|
||||
|
||||
if (error?.response?.status === 404) {
|
||||
alert.error(i18n.t("user.user-not-found"));
|
||||
userError.value = true;
|
||||
}
|
||||
return data;
|
||||
}, { watch: [householdId] });
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!refHouseholdEditForm.value?.validate() || household.value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { response, data } = await adminApi.households.updateOne(household.value.id, household.value);
|
||||
if (response?.status === 200 && data) {
|
||||
household.value = data;
|
||||
alert.success(i18n.t("settings.settings-updated"));
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("settings.settings-update-failed"));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
groups,
|
||||
household,
|
||||
validators,
|
||||
userError,
|
||||
refHouseholdEditForm,
|
||||
handleSubmit,
|
||||
};
|
||||
},
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const i18n = useI18n();
|
||||
|
||||
const { groups } = useGroups();
|
||||
const householdId = computed(() => route.params.id as string);
|
||||
|
||||
// ==============================================
|
||||
// New User Form
|
||||
|
||||
const refHouseholdEditForm = ref<VForm | null>(null);
|
||||
|
||||
const adminApi = useAdminApi();
|
||||
|
||||
const userError = ref(false);
|
||||
|
||||
const { data: household } = useAsyncData(`get-household-${householdId.value}`, async () => {
|
||||
if (!householdId.value) {
|
||||
return null;
|
||||
}
|
||||
const { data, error } = await adminApi.households.getOne(householdId.value);
|
||||
|
||||
if (error?.response?.status === 404) {
|
||||
alert.error(i18n.t("user.user-not-found"));
|
||||
userError.value = true;
|
||||
}
|
||||
return data;
|
||||
}, { watch: [householdId] });
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!refHouseholdEditForm.value?.validate() || household.value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { response, data } = await adminApi.households.updateOne(household.value.id, household.value);
|
||||
if (response?.status === 200 && data) {
|
||||
household.value = data;
|
||||
alert.success(i18n.t("settings.settings-updated"));
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("settings.settings-update-failed"));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useAdminApi, useUserApi } from "~/composables/api";
|
||||
import { useGroups } from "~/composables/use-groups";
|
||||
import { useAdminHouseholds } from "~/composables/use-households";
|
||||
@@ -146,103 +146,83 @@ import { useUserForm } from "~/composables/use-users";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import type { UserOut } from "~/lib/api/types/user";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
setup() {
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const { userForm } = useUserForm();
|
||||
const { groups } = useGroups();
|
||||
const { useHouseholdsInGroup } = useAdminHouseholds();
|
||||
const i18n = useI18n();
|
||||
const route = useRoute();
|
||||
|
||||
const userId = route.params.id as string;
|
||||
|
||||
// ==============================================
|
||||
// New User Form
|
||||
|
||||
const refNewUserForm = ref<VForm | null>(null);
|
||||
|
||||
const adminApi = useAdminApi();
|
||||
|
||||
const user = ref<UserOut | null>(null);
|
||||
const households = useHouseholdsInGroup(computed(() => user.value?.groupId || ""));
|
||||
|
||||
const disabledFields = computed(() => {
|
||||
return user.value?.authMethod !== "Mealie" ? ["admin"] : [];
|
||||
});
|
||||
|
||||
const userError = ref(false);
|
||||
|
||||
const resetUrl = ref<string | null>(null);
|
||||
const generatingToken = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
const { data, error } = await adminApi.users.getOne(userId);
|
||||
|
||||
if (error?.response?.status === 404) {
|
||||
alert.error(i18n.t("user.user-not-found"));
|
||||
userError.value = true;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
user.value = data;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!refNewUserForm.value?.validate() || user.value === null) return;
|
||||
|
||||
const { response, data } = await adminApi.users.updateOne(user.value.id, user.value);
|
||||
|
||||
if (response?.status === 200 && data) {
|
||||
user.value = data;
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePasswordReset() {
|
||||
if (user.value === null) return;
|
||||
generatingToken.value = true;
|
||||
|
||||
const { response, data } = await adminApi.users.generatePasswordResetToken({ email: user.value.email });
|
||||
|
||||
if (response?.status === 201 && data) {
|
||||
const token: string = data.token;
|
||||
resetUrl.value = `${window.location.origin}/reset-password/?token=${token}`;
|
||||
}
|
||||
|
||||
generatingToken.value = false;
|
||||
}
|
||||
|
||||
const userApi = useUserApi();
|
||||
async function sendResetEmail() {
|
||||
if (!user.value?.email) return;
|
||||
const { response } = await userApi.email.sendForgotPassword({ email: user.value.email });
|
||||
if (response && response.status === 200) {
|
||||
alert.success(i18n.t("profile.email-sent"));
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("profile.error-sending-email"));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
disabledFields,
|
||||
userError,
|
||||
userForm,
|
||||
refNewUserForm,
|
||||
handleSubmit,
|
||||
groups,
|
||||
households,
|
||||
validators,
|
||||
handlePasswordReset,
|
||||
resetUrl,
|
||||
generatingToken,
|
||||
sendResetEmail,
|
||||
};
|
||||
},
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const { userForm } = useUserForm();
|
||||
const { groups } = useGroups();
|
||||
const { useHouseholdsInGroup } = useAdminHouseholds();
|
||||
const i18n = useI18n();
|
||||
const route = useRoute();
|
||||
|
||||
const userId = route.params.id as string;
|
||||
|
||||
// ==============================================
|
||||
// New User Form
|
||||
|
||||
const refNewUserForm = ref<VForm | null>(null);
|
||||
|
||||
const adminApi = useAdminApi();
|
||||
|
||||
const user = ref<UserOut | null>(null);
|
||||
const households = useHouseholdsInGroup(computed(() => user.value?.groupId || ""));
|
||||
|
||||
const disabledFields = computed(() => {
|
||||
return user.value?.authMethod !== "Mealie" ? ["admin"] : [];
|
||||
});
|
||||
|
||||
const userError = ref(false);
|
||||
|
||||
const resetUrl = ref<string | null>(null);
|
||||
const generatingToken = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
const { data, error } = await adminApi.users.getOne(userId);
|
||||
|
||||
if (error?.response?.status === 404) {
|
||||
alert.error(i18n.t("user.user-not-found"));
|
||||
userError.value = true;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
user.value = data;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!refNewUserForm.value?.validate() || user.value === null) return;
|
||||
|
||||
const { response, data } = await adminApi.users.updateOne(user.value.id, user.value);
|
||||
|
||||
if (response?.status === 200 && data) {
|
||||
user.value = data;
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePasswordReset() {
|
||||
if (user.value === null) return;
|
||||
generatingToken.value = true;
|
||||
|
||||
const { response, data } = await adminApi.users.generatePasswordResetToken({ email: user.value.email });
|
||||
|
||||
if (response?.status === 201 && data) {
|
||||
const token: string = data.token;
|
||||
resetUrl.value = `${window.location.origin}/reset-password/?token=${token}`;
|
||||
}
|
||||
|
||||
generatingToken.value = false;
|
||||
}
|
||||
|
||||
const userApi = useUserApi();
|
||||
async function sendResetEmail() {
|
||||
if (!user.value?.email) return;
|
||||
const { response } = await userApi.email.sendForgotPassword({ email: user.value.email });
|
||||
if (response && response.status === 200) {
|
||||
alert.success(i18n.t("profile.email-sent"));
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("profile.error-sending-email"));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
<v-container fluid>
|
||||
<UserInviteDialog v-model="inviteDialog" />
|
||||
<BaseDialog
|
||||
v-model="deleteDialog"
|
||||
v-model="state.deleteDialog"
|
||||
:title="$t('general.confirm')"
|
||||
color="error"
|
||||
can-confirm
|
||||
@confirm="deleteUser(deleteTargetId)"
|
||||
@confirm="deleteUser(state.deleteTargetId)"
|
||||
>
|
||||
<template #activator />
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
:items-per-page="-1"
|
||||
hide-default-footer
|
||||
disable-pagination
|
||||
:search="search"
|
||||
:search="state.search"
|
||||
@click:row="($event, { item }) => handleRowClick(item)"
|
||||
>
|
||||
<template #[`item.admin`]="{ item }">
|
||||
@@ -78,8 +78,8 @@
|
||||
color="error"
|
||||
variant="text"
|
||||
@click.stop="
|
||||
deleteDialog = true;
|
||||
deleteTargetId = item.id;
|
||||
state.deleteDialog = true;
|
||||
state.deleteTargetId = item.id;
|
||||
"
|
||||
>
|
||||
<v-icon>
|
||||
@@ -93,123 +93,99 @@
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useAdminApi } from "~/composables/api";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import { useUser, useAllUsers } from "~/composables/use-user";
|
||||
import type { UserOut } from "~/lib/api/types/user";
|
||||
import UserInviteDialog from "~/components/Domain/User/UserInviteDialog.vue";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
UserInviteDialog,
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
const i18n = useI18n();
|
||||
|
||||
useHead({
|
||||
title: i18n.t("sidebar.manage-users"),
|
||||
});
|
||||
|
||||
const api = useAdminApi();
|
||||
const inviteDialog = ref();
|
||||
const auth = useMealieAuth();
|
||||
|
||||
const user = computed(() => auth.user.value);
|
||||
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const isUserOwnAccount = computed(() => {
|
||||
return state.deleteTargetId === user.value?.id;
|
||||
});
|
||||
|
||||
const ACTIONS_OPTIONS = [
|
||||
{
|
||||
text: i18n.t("user.reset-locked-users"),
|
||||
icon: $globals.icons.lock,
|
||||
event: "unlock-all-users",
|
||||
},
|
||||
setup() {
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
];
|
||||
|
||||
const api = useAdminApi();
|
||||
const refUserDialog = ref();
|
||||
const inviteDialog = ref();
|
||||
const auth = useMealieAuth();
|
||||
const state = reactive({
|
||||
deleteDialog: false,
|
||||
deleteTargetId: "",
|
||||
search: "",
|
||||
groups: [],
|
||||
households: [],
|
||||
sendTo: "",
|
||||
});
|
||||
|
||||
const user = computed(() => auth.user.value);
|
||||
const { users, refreshAllUsers } = useAllUsers();
|
||||
const { deleteUser: deleteUserMixin } = useUser(refreshAllUsers);
|
||||
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
function deleteUser(id: string) {
|
||||
deleteUserMixin(id);
|
||||
|
||||
const router = useRouter();
|
||||
if (isUserOwnAccount.value) {
|
||||
auth.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
const isUserOwnAccount = computed(() => {
|
||||
return state.deleteTargetId === user.value?.id;
|
||||
});
|
||||
function handleRowClick(item: UserOut) {
|
||||
router.push(`/admin/manage/users/${item.id}`);
|
||||
}
|
||||
|
||||
const ACTIONS_OPTIONS = [
|
||||
{
|
||||
text: i18n.t("user.reset-locked-users"),
|
||||
icon: $globals.icons.lock,
|
||||
event: "unlock-all-users",
|
||||
},
|
||||
];
|
||||
// ==========================================================
|
||||
// Constants / Non-reactive
|
||||
|
||||
const state = reactive({
|
||||
deleteDialog: false,
|
||||
deleteTargetId: "",
|
||||
search: "",
|
||||
groups: [],
|
||||
households: [],
|
||||
sendTo: "",
|
||||
});
|
||||
|
||||
const { users, refreshAllUsers } = useAllUsers();
|
||||
const { loading, deleteUser: deleteUserMixin } = useUser(refreshAllUsers);
|
||||
|
||||
function deleteUser(id: string) {
|
||||
deleteUserMixin(id);
|
||||
|
||||
if (isUserOwnAccount.value) {
|
||||
auth.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
function handleRowClick(item: UserOut) {
|
||||
router.push(`/admin/manage/users/${item.id}`);
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// Constants / Non-reactive
|
||||
|
||||
const headers = [
|
||||
{
|
||||
title: i18n.t("user.user-id"),
|
||||
align: "start",
|
||||
value: "id",
|
||||
},
|
||||
{ title: i18n.t("user.username"), value: "username" },
|
||||
{ title: i18n.t("user.full-name"), value: "fullName" },
|
||||
{ title: i18n.t("user.email"), value: "email" },
|
||||
{ title: i18n.t("group.group"), value: "group" },
|
||||
{ title: i18n.t("household.household"), value: "household" },
|
||||
{ title: i18n.t("user.auth-method"), value: "authMethod" },
|
||||
{ title: i18n.t("user.admin"), value: "admin" },
|
||||
{ title: i18n.t("general.delete"), value: "actions", sortable: false, align: "center" },
|
||||
];
|
||||
|
||||
async function unlockAllUsers(): Promise<void> {
|
||||
const { data } = await api.users.unlockAllUsers(true);
|
||||
|
||||
if (data) {
|
||||
const unlocked = data.unlocked ?? 0;
|
||||
|
||||
alert.success(`${unlocked} user(s) unlocked`);
|
||||
refreshAllUsers();
|
||||
}
|
||||
}
|
||||
|
||||
useSeoMeta({
|
||||
title: i18n.t("sidebar.manage-users"),
|
||||
});
|
||||
|
||||
return {
|
||||
isUserOwnAccount,
|
||||
unlockAllUsers,
|
||||
...toRefs(state),
|
||||
headers,
|
||||
deleteUser,
|
||||
loading,
|
||||
refUserDialog,
|
||||
inviteDialog,
|
||||
users,
|
||||
user,
|
||||
handleRowClick,
|
||||
ACTIONS_OPTIONS,
|
||||
};
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: useI18n().t("sidebar.manage-users"),
|
||||
};
|
||||
const headers = [
|
||||
{
|
||||
title: i18n.t("user.user-id"),
|
||||
align: "start",
|
||||
value: "id",
|
||||
},
|
||||
{ title: i18n.t("user.username"), value: "username" },
|
||||
{ title: i18n.t("user.full-name"), value: "fullName" },
|
||||
{ title: i18n.t("user.email"), value: "email" },
|
||||
{ title: i18n.t("group.group"), value: "group" },
|
||||
{ title: i18n.t("household.household"), value: "household" },
|
||||
{ title: i18n.t("user.auth-method"), value: "authMethod" },
|
||||
{ title: i18n.t("user.admin"), value: "admin" },
|
||||
{ title: i18n.t("general.delete"), value: "actions", sortable: false, align: "center" },
|
||||
];
|
||||
|
||||
async function unlockAllUsers(): Promise<void> {
|
||||
const { data } = await api.users.unlockAllUsers(true);
|
||||
|
||||
if (data) {
|
||||
const unlocked = data.unlocked ?? 0;
|
||||
|
||||
alert.success(`${unlocked} user(s) unlocked`);
|
||||
refreshAllUsers();
|
||||
}
|
||||
}
|
||||
|
||||
useSeoMeta({
|
||||
title: i18n.t("sidebar.manage-users"),
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<v-text-field
|
||||
v-model="address"
|
||||
v-model="state.address"
|
||||
class="mr-4"
|
||||
:label="$t('user.email')"
|
||||
:rules="[validators.email]"
|
||||
@@ -135,7 +135,7 @@
|
||||
color="info"
|
||||
variant="elevated"
|
||||
:disabled="!appConfig.emailReady || !validEmail"
|
||||
:loading="loading"
|
||||
:loading="state.loading"
|
||||
class="opacity-100"
|
||||
@click="testEmail"
|
||||
>
|
||||
@@ -144,12 +144,12 @@
|
||||
</template>
|
||||
{{ $t("general.test") }}
|
||||
</BaseButton>
|
||||
<template v-if="tested">
|
||||
<template v-if="state.tested">
|
||||
<v-divider class="my-x mt-6" />
|
||||
<v-card-text class="px-0">
|
||||
<h4> {{ $t("settings.email-test-results") }}</h4>
|
||||
<span class="pl-4">
|
||||
{{ success ? $t('settings.succeeded') : $t('settings.failed') }}
|
||||
{{ state.success ? $t('settings.succeeded') : $t('settings.failed') }}
|
||||
</span>
|
||||
</v-card-text>
|
||||
</template>
|
||||
@@ -226,7 +226,7 @@
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import type { TranslateResult } from "vue-i18n";
|
||||
import { useAdminApi, useUserApi } from "~/composables/api";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
@@ -234,12 +234,6 @@ import { useAsyncKey } from "~/composables/use-utils";
|
||||
import type { CheckAppConfig } from "~/lib/api/types/admin";
|
||||
import AppLoader from "~/components/global/AppLoader.vue";
|
||||
|
||||
enum DockerVolumeState {
|
||||
Unknown = "unknown",
|
||||
Success = "success",
|
||||
Error = "error",
|
||||
}
|
||||
|
||||
interface SimpleCheck {
|
||||
id: string;
|
||||
text: TranslateResult;
|
||||
@@ -254,269 +248,252 @@ interface CheckApp extends CheckAppConfig {
|
||||
isSiteSecure?: boolean;
|
||||
}
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { AppLoader },
|
||||
setup() {
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
// For some reason the layout is not set automatically, so we set it here,
|
||||
// even though it's defined above in the page meta.
|
||||
onMounted(() => {
|
||||
setPageLayout("admin");
|
||||
});
|
||||
// For some reason the layout is not set automatically, so we set it here,
|
||||
// even though it's defined above in the page meta.
|
||||
onMounted(() => {
|
||||
setPageLayout("admin");
|
||||
});
|
||||
|
||||
const { $globals } = useNuxtApp();
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
const i18n = useI18n();
|
||||
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
address: "",
|
||||
success: false,
|
||||
error: "",
|
||||
tested: false,
|
||||
});
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
address: "",
|
||||
success: false,
|
||||
error: "",
|
||||
tested: false,
|
||||
});
|
||||
|
||||
// Set page title
|
||||
useSeoMeta({
|
||||
title: i18n.t("settings.site-settings"),
|
||||
});
|
||||
// Set page title
|
||||
useSeoMeta({
|
||||
title: i18n.t("settings.site-settings"),
|
||||
});
|
||||
|
||||
const appConfig = ref<CheckApp>({
|
||||
emailReady: true,
|
||||
baseUrlSet: true,
|
||||
isSiteSecure: true,
|
||||
isUpToDate: false,
|
||||
ldapReady: false,
|
||||
oidcReady: false,
|
||||
enableOpenai: false,
|
||||
});
|
||||
function isLocalHostOrHttps() {
|
||||
return window.location.hostname === "localhost" || window.location.protocol === "https:";
|
||||
const appConfig = ref<CheckApp>({
|
||||
emailReady: true,
|
||||
baseUrlSet: true,
|
||||
isSiteSecure: true,
|
||||
isUpToDate: false,
|
||||
ldapReady: false,
|
||||
oidcReady: false,
|
||||
enableOpenai: false,
|
||||
});
|
||||
function isLocalHostOrHttps() {
|
||||
return window.location.hostname === "localhost" || window.location.protocol === "https:";
|
||||
}
|
||||
const api = useUserApi();
|
||||
const adminApi = useAdminApi();
|
||||
onMounted(async () => {
|
||||
const { data } = await adminApi.about.checkApp();
|
||||
if (data) {
|
||||
appConfig.value = { ...data, isSiteSecure: false };
|
||||
}
|
||||
appConfig.value.isSiteSecure = isLocalHostOrHttps();
|
||||
});
|
||||
const simpleChecks = computed<SimpleCheck[]>(() => {
|
||||
const goodIcon = $globals.icons.checkboxMarkedCircle;
|
||||
const badIcon = $globals.icons.alert;
|
||||
const warningIcon = $globals.icons.alertCircle;
|
||||
const goodColor = "success";
|
||||
const badColor = "error";
|
||||
const warningColor = "warning";
|
||||
const data: SimpleCheck[] = [
|
||||
{
|
||||
id: "application-version",
|
||||
text: i18n.t("settings.application-version"),
|
||||
status: appConfig.value.isUpToDate,
|
||||
errorText: i18n.t("settings.application-version-error-text", [rawAppInfo.value.version, rawAppInfo.value.versionLatest]),
|
||||
successText: i18n.t("settings.mealie-is-up-to-date"),
|
||||
color: appConfig.value.isUpToDate ? goodColor : warningColor,
|
||||
icon: appConfig.value.isUpToDate ? goodIcon : warningIcon,
|
||||
},
|
||||
{
|
||||
id: "secure-site",
|
||||
text: i18n.t("settings.secure-site"),
|
||||
status: appConfig.value.isSiteSecure,
|
||||
errorText: i18n.t("settings.secure-site-error-text"),
|
||||
successText: i18n.t("settings.secure-site-success-text"),
|
||||
color: appConfig.value.isSiteSecure ? goodColor : badColor,
|
||||
icon: appConfig.value.isSiteSecure ? goodIcon : badIcon,
|
||||
},
|
||||
{
|
||||
id: "server-side-base-url",
|
||||
text: i18n.t("settings.server-side-base-url"),
|
||||
status: appConfig.value.baseUrlSet,
|
||||
errorText: i18n.t("settings.server-side-base-url-error-text"),
|
||||
successText: i18n.t("settings.server-side-base-url-success-text"),
|
||||
color: appConfig.value.baseUrlSet ? goodColor : badColor,
|
||||
icon: appConfig.value.baseUrlSet ? goodIcon : badIcon,
|
||||
},
|
||||
{
|
||||
id: "ldap-ready",
|
||||
text: i18n.t("settings.ldap-ready"),
|
||||
status: appConfig.value.ldapReady,
|
||||
errorText: i18n.t("settings.ldap-ready-error-text"),
|
||||
successText: i18n.t("settings.ldap-ready-success-text"),
|
||||
color: appConfig.value.ldapReady ? goodColor : warningColor,
|
||||
icon: appConfig.value.ldapReady ? goodIcon : warningIcon,
|
||||
},
|
||||
{
|
||||
id: "oidc-ready",
|
||||
text: i18n.t("settings.oidc-ready"),
|
||||
status: appConfig.value.oidcReady,
|
||||
errorText: i18n.t("settings.oidc-ready-error-text"),
|
||||
successText: i18n.t("settings.oidc-ready-success-text"),
|
||||
color: appConfig.value.oidcReady ? goodColor : warningColor,
|
||||
icon: appConfig.value.oidcReady ? goodIcon : warningIcon,
|
||||
},
|
||||
{
|
||||
id: "openai-ready",
|
||||
text: i18n.t("settings.openai-ready"),
|
||||
status: appConfig.value.enableOpenai,
|
||||
errorText: i18n.t("settings.openai-ready-error-text"),
|
||||
successText: i18n.t("settings.openai-ready-success-text"),
|
||||
color: appConfig.value.enableOpenai ? goodColor : warningColor,
|
||||
icon: appConfig.value.enableOpenai ? goodIcon : warningIcon,
|
||||
},
|
||||
];
|
||||
return data;
|
||||
});
|
||||
async function testEmail() {
|
||||
state.loading = true;
|
||||
state.tested = false;
|
||||
const { data } = await api.email.test({ email: state.address });
|
||||
if (data) {
|
||||
if (data.success) {
|
||||
state.success = true;
|
||||
}
|
||||
const api = useUserApi();
|
||||
const adminApi = useAdminApi();
|
||||
onMounted(async () => {
|
||||
const { data } = await adminApi.about.checkApp();
|
||||
if (data) {
|
||||
appConfig.value = { ...data, isSiteSecure: false };
|
||||
}
|
||||
appConfig.value.isSiteSecure = isLocalHostOrHttps();
|
||||
});
|
||||
const simpleChecks = computed<SimpleCheck[]>(() => {
|
||||
const goodIcon = $globals.icons.checkboxMarkedCircle;
|
||||
const badIcon = $globals.icons.alert;
|
||||
const warningIcon = $globals.icons.alertCircle;
|
||||
const goodColor = "success";
|
||||
const badColor = "error";
|
||||
const warningColor = "warning";
|
||||
const data: SimpleCheck[] = [
|
||||
else {
|
||||
state.error = data.error ?? "";
|
||||
state.success = false;
|
||||
}
|
||||
}
|
||||
state.loading = false;
|
||||
state.tested = true;
|
||||
}
|
||||
const validEmail = computed(() => {
|
||||
if (state.address === "") {
|
||||
return false;
|
||||
}
|
||||
const valid = validators.email(state.address);
|
||||
// Explicit bool check because validators.email sometimes returns a string
|
||||
if (valid === true) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
// ============================================================
|
||||
// General About Info
|
||||
const rawAppInfo = ref({
|
||||
version: "null",
|
||||
versionLatest: "null",
|
||||
});
|
||||
function getAppInfo() {
|
||||
const { data: statistics } = useAsyncData(useAsyncKey(), async () => {
|
||||
const { data } = await adminApi.about.about();
|
||||
if (data) {
|
||||
rawAppInfo.value.version = data.version;
|
||||
rawAppInfo.value.versionLatest = data.versionLatest;
|
||||
const prettyInfo = [
|
||||
{
|
||||
id: "application-version",
|
||||
text: i18n.t("settings.application-version"),
|
||||
status: appConfig.value.isUpToDate,
|
||||
errorText: i18n.t("settings.application-version-error-text", [rawAppInfo.value.version, rawAppInfo.value.versionLatest]),
|
||||
successText: i18n.t("settings.mealie-is-up-to-date"),
|
||||
color: appConfig.value.isUpToDate ? goodColor : warningColor,
|
||||
icon: appConfig.value.isUpToDate ? goodIcon : warningIcon,
|
||||
slot: "version",
|
||||
name: i18n.t("about.version"),
|
||||
icon: $globals.icons.information,
|
||||
value: data.version,
|
||||
},
|
||||
{
|
||||
id: "secure-site",
|
||||
text: i18n.t("settings.secure-site"),
|
||||
status: appConfig.value.isSiteSecure,
|
||||
errorText: i18n.t("settings.secure-site-error-text"),
|
||||
successText: i18n.t("settings.secure-site-success-text"),
|
||||
color: appConfig.value.isSiteSecure ? goodColor : badColor,
|
||||
icon: appConfig.value.isSiteSecure ? goodIcon : badIcon,
|
||||
slot: "build",
|
||||
name: i18n.t("settings.build"),
|
||||
icon: $globals.icons.information,
|
||||
value: data.buildId,
|
||||
},
|
||||
{
|
||||
id: "server-side-base-url",
|
||||
text: i18n.t("settings.server-side-base-url"),
|
||||
status: appConfig.value.baseUrlSet,
|
||||
errorText: i18n.t("settings.server-side-base-url-error-text"),
|
||||
successText: i18n.t("settings.server-side-base-url-success-text"),
|
||||
color: appConfig.value.baseUrlSet ? goodColor : badColor,
|
||||
icon: appConfig.value.baseUrlSet ? goodIcon : badIcon,
|
||||
name: i18n.t("about.application-mode"),
|
||||
icon: $globals.icons.devTo,
|
||||
value: data.production ? i18n.t("about.production") : i18n.t("about.development"),
|
||||
},
|
||||
{
|
||||
id: "ldap-ready",
|
||||
text: i18n.t("settings.ldap-ready"),
|
||||
status: appConfig.value.ldapReady,
|
||||
errorText: i18n.t("settings.ldap-ready-error-text"),
|
||||
successText: i18n.t("settings.ldap-ready-success-text"),
|
||||
color: appConfig.value.ldapReady ? goodColor : warningColor,
|
||||
icon: appConfig.value.ldapReady ? goodIcon : warningIcon,
|
||||
name: i18n.t("about.demo-status"),
|
||||
icon: $globals.icons.testTube,
|
||||
value: data.demoStatus ? i18n.t("about.demo") : i18n.t("about.not-demo"),
|
||||
},
|
||||
{
|
||||
id: "oidc-ready",
|
||||
text: i18n.t("settings.oidc-ready"),
|
||||
status: appConfig.value.oidcReady,
|
||||
errorText: i18n.t("settings.oidc-ready-error-text"),
|
||||
successText: i18n.t("settings.oidc-ready-success-text"),
|
||||
color: appConfig.value.oidcReady ? goodColor : warningColor,
|
||||
icon: appConfig.value.oidcReady ? goodIcon : warningIcon,
|
||||
name: i18n.t("about.api-port"),
|
||||
icon: $globals.icons.api,
|
||||
value: data.apiPort,
|
||||
},
|
||||
{
|
||||
id: "openai-ready",
|
||||
text: i18n.t("settings.openai-ready"),
|
||||
status: appConfig.value.enableOpenai,
|
||||
errorText: i18n.t("settings.openai-ready-error-text"),
|
||||
successText: i18n.t("settings.openai-ready-success-text"),
|
||||
color: appConfig.value.enableOpenai ? goodColor : warningColor,
|
||||
icon: appConfig.value.enableOpenai ? goodIcon : warningIcon,
|
||||
name: i18n.t("about.api-docs"),
|
||||
icon: $globals.icons.file,
|
||||
value: data.apiDocs ? i18n.t("general.enabled") : i18n.t("general.disabled"),
|
||||
},
|
||||
{
|
||||
name: i18n.t("about.database-type"),
|
||||
icon: $globals.icons.database,
|
||||
value: data.dbType,
|
||||
},
|
||||
{
|
||||
name: i18n.t("about.database-url"),
|
||||
icon: $globals.icons.database,
|
||||
value: data.dbUrl,
|
||||
},
|
||||
{
|
||||
name: i18n.t("about.default-group"),
|
||||
icon: $globals.icons.group,
|
||||
value: data.defaultGroup,
|
||||
},
|
||||
{
|
||||
name: i18n.t("about.default-household"),
|
||||
icon: $globals.icons.household,
|
||||
value: data.defaultHousehold,
|
||||
},
|
||||
{
|
||||
slot: "recipe-scraper",
|
||||
name: i18n.t("settings.recipe-scraper-version"),
|
||||
icon: $globals.icons.primary,
|
||||
value: data.recipeScraperVersion,
|
||||
},
|
||||
];
|
||||
return data;
|
||||
});
|
||||
async function testEmail() {
|
||||
state.loading = true;
|
||||
state.tested = false;
|
||||
const { data } = await api.email.test({ email: state.address });
|
||||
if (data) {
|
||||
if (data.success) {
|
||||
state.success = true;
|
||||
}
|
||||
else {
|
||||
state.error = data.error ?? "";
|
||||
state.success = false;
|
||||
}
|
||||
}
|
||||
state.loading = false;
|
||||
state.tested = true;
|
||||
return prettyInfo;
|
||||
}
|
||||
const validEmail = computed(() => {
|
||||
if (state.address === "") {
|
||||
return false;
|
||||
}
|
||||
const valid = validators.email(state.address);
|
||||
// Explicit bool check because validators.email sometimes returns a string
|
||||
if (valid === true) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
// ============================================================
|
||||
// General About Info
|
||||
const rawAppInfo = ref({
|
||||
version: "null",
|
||||
versionLatest: "null",
|
||||
});
|
||||
function getAppInfo() {
|
||||
const { data: statistics } = useAsyncData(useAsyncKey(), async () => {
|
||||
const { data } = await adminApi.about.about();
|
||||
if (data) {
|
||||
rawAppInfo.value.version = data.version;
|
||||
rawAppInfo.value.versionLatest = data.versionLatest;
|
||||
const prettyInfo = [
|
||||
{
|
||||
slot: "version",
|
||||
name: i18n.t("about.version"),
|
||||
icon: $globals.icons.information,
|
||||
value: data.version,
|
||||
},
|
||||
{
|
||||
slot: "build",
|
||||
name: i18n.t("settings.build"),
|
||||
icon: $globals.icons.information,
|
||||
value: data.buildId,
|
||||
},
|
||||
{
|
||||
name: i18n.t("about.application-mode"),
|
||||
icon: $globals.icons.devTo,
|
||||
value: data.production ? i18n.t("about.production") : i18n.t("about.development"),
|
||||
},
|
||||
{
|
||||
name: i18n.t("about.demo-status"),
|
||||
icon: $globals.icons.testTube,
|
||||
value: data.demoStatus ? i18n.t("about.demo") : i18n.t("about.not-demo"),
|
||||
},
|
||||
{
|
||||
name: i18n.t("about.api-port"),
|
||||
icon: $globals.icons.api,
|
||||
value: data.apiPort,
|
||||
},
|
||||
{
|
||||
name: i18n.t("about.api-docs"),
|
||||
icon: $globals.icons.file,
|
||||
value: data.apiDocs ? i18n.t("general.enabled") : i18n.t("general.disabled"),
|
||||
},
|
||||
{
|
||||
name: i18n.t("about.database-type"),
|
||||
icon: $globals.icons.database,
|
||||
value: data.dbType,
|
||||
},
|
||||
{
|
||||
name: i18n.t("about.database-url"),
|
||||
icon: $globals.icons.database,
|
||||
value: data.dbUrl,
|
||||
},
|
||||
{
|
||||
name: i18n.t("about.default-group"),
|
||||
icon: $globals.icons.group,
|
||||
value: data.defaultGroup,
|
||||
},
|
||||
{
|
||||
name: i18n.t("about.default-household"),
|
||||
icon: $globals.icons.household,
|
||||
value: data.defaultHousehold,
|
||||
},
|
||||
{
|
||||
slot: "recipe-scraper",
|
||||
name: i18n.t("settings.recipe-scraper-version"),
|
||||
icon: $globals.icons.primary,
|
||||
value: data.recipeScraperVersion,
|
||||
},
|
||||
];
|
||||
return prettyInfo;
|
||||
}
|
||||
return data;
|
||||
});
|
||||
return statistics;
|
||||
return data;
|
||||
});
|
||||
return statistics;
|
||||
}
|
||||
const appInfo = getAppInfo();
|
||||
const bugReportDialog = ref(false);
|
||||
const bugReportText = computed(() => {
|
||||
const ignore = {
|
||||
[i18n.t("about.database-url")]: true,
|
||||
[i18n.t("about.default-group")]: true,
|
||||
};
|
||||
let text = "**Details**\n";
|
||||
appInfo.value?.forEach((item) => {
|
||||
if (ignore[item.name as string]) {
|
||||
return;
|
||||
}
|
||||
const appInfo = getAppInfo();
|
||||
const bugReportDialog = ref(false);
|
||||
const bugReportText = computed(() => {
|
||||
const ignore = {
|
||||
[i18n.t("about.database-url")]: true,
|
||||
[i18n.t("about.default-group")]: true,
|
||||
};
|
||||
let text = "**Details**\n";
|
||||
appInfo.value?.forEach((item) => {
|
||||
if (ignore[item.name as string]) {
|
||||
return;
|
||||
}
|
||||
text += `${item.name as string}: ${item.value as string}\n`;
|
||||
});
|
||||
const ignoreChecks: {
|
||||
[key: string]: boolean;
|
||||
} = {
|
||||
"application-version": true,
|
||||
};
|
||||
text += "\n**Checks**\n";
|
||||
simpleChecks.value.forEach((item) => {
|
||||
if (ignoreChecks[item.id]) {
|
||||
return;
|
||||
}
|
||||
const status = item.status ? i18n.t("general.yes") : i18n.t("general.no");
|
||||
text += `${item.text.toString()}: ${status}\n`;
|
||||
});
|
||||
text += `${i18n.t("settings.email-configured")}: ${appConfig.value.emailReady ? i18n.t("general.yes") : i18n.t("general.no")}\n`;
|
||||
return text;
|
||||
});
|
||||
return {
|
||||
bugReportDialog,
|
||||
bugReportText,
|
||||
DockerVolumeState,
|
||||
simpleChecks,
|
||||
appConfig,
|
||||
validEmail,
|
||||
validators,
|
||||
...toRefs(state),
|
||||
testEmail,
|
||||
appInfo,
|
||||
};
|
||||
},
|
||||
text += `${item.name as string}: ${item.value as string}\n`;
|
||||
});
|
||||
const ignoreChecks: {
|
||||
[key: string]: boolean;
|
||||
} = {
|
||||
"application-version": true,
|
||||
};
|
||||
text += "\n**Checks**\n";
|
||||
simpleChecks.value.forEach((item) => {
|
||||
if (ignoreChecks[item.id]) {
|
||||
return;
|
||||
}
|
||||
const status = item.status ? i18n.t("general.yes") : i18n.t("general.no");
|
||||
text += `${item.text.toString()}: ${status}\n`;
|
||||
});
|
||||
text += `${i18n.t("settings.email-configured")}: ${appConfig.value.emailReady ? i18n.t("general.yes") : i18n.t("general.no")}\n`;
|
||||
return text;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user