mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-08-05 21:45:25 +02:00
feat: Migrate to Nuxt 3 framework (#5184)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com> Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
This commit is contained in:
parent
89ab7fac25
commit
c24d532608
403 changed files with 23959 additions and 19557 deletions
|
@ -4,9 +4,10 @@
|
|||
<!-- Delete Dialog -->
|
||||
<BaseDialog
|
||||
v-model="deleteDialog"
|
||||
:title="$tc('settings.backup.delete-backup')"
|
||||
:title="$t('settings.backup.delete-backup')"
|
||||
color="error"
|
||||
:icon="$globals.icons.alertCircle"
|
||||
can-confirm
|
||||
@confirm="deleteBackup()"
|
||||
>
|
||||
<v-card-text>
|
||||
|
@ -15,60 +16,83 @@
|
|||
</BaseDialog>
|
||||
|
||||
<!-- Import Dialog -->
|
||||
<BaseDialog v-model="importDialog" color="error" :title="$t('settings.backup.backup-restore')" :icon="$globals.icons.database">
|
||||
<v-divider></v-divider>
|
||||
<BaseDialog
|
||||
v-model="importDialog"
|
||||
color="error"
|
||||
:title="$t('settings.backup.backup-restore')"
|
||||
:icon="$globals.icons.database"
|
||||
>
|
||||
<v-divider />
|
||||
<v-card-text>
|
||||
<i18n path="settings.backup.back-restore-description">
|
||||
<i18n-t keypath="settings.backup.back-restore-description">
|
||||
<template #cannot-be-undone>
|
||||
<b> {{ $t('settings.backup.cannot-be-undone') }} </b>
|
||||
</template>
|
||||
</i18n>
|
||||
</i18n-t>
|
||||
|
||||
<p class="mt-3">
|
||||
<i18n path="settings.backup.postgresql-note">
|
||||
<i18n-t keypath="settings.backup.postgresql-note">
|
||||
<template #backup-restore-process>
|
||||
<a href="https://nightly.mealie.io/documentation/getting-started/usage/backups-and-restoring/" >{{ $t('settings.backup.backup-restore-process-in-the-documentation') }}</a >
|
||||
<a href="https://nightly.mealie.io/documentation/getting-started/usage/backups-and-restoring/">{{
|
||||
$t('settings.backup.backup-restore-process-in-the-documentation') }}</a>
|
||||
</template>
|
||||
</i18n>
|
||||
</i18n-t>
|
||||
{{ $t('') }}
|
||||
</p>
|
||||
|
||||
|
||||
<v-checkbox
|
||||
v-model="confirmImport"
|
||||
class="checkbox-top"
|
||||
color="error"
|
||||
hide-details
|
||||
:label="$t('settings.backup.irreversible-acknowledgment')"
|
||||
></v-checkbox>
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions class="justify-center pt-0">
|
||||
<BaseButton delete :disabled="!confirmImport || runningRestore" @click="restoreBackup(selected)">
|
||||
<template #icon> {{ $globals.icons.database }} </template>
|
||||
<BaseButton
|
||||
delete
|
||||
:disabled="!confirmImport || runningRestore"
|
||||
@click="restoreBackup(selected)"
|
||||
>
|
||||
<template #icon>
|
||||
{{ $globals.icons.database }}
|
||||
</template>
|
||||
{{ $t('settings.backup.restore-backup') }}
|
||||
</BaseButton>
|
||||
</v-card-actions>
|
||||
<p class="caption pb-0 mb-1 text-center">
|
||||
{{ selected }}
|
||||
</p>
|
||||
<v-progress-linear v-if="runningRestore" indeterminate></v-progress-linear>
|
||||
<v-progress-linear
|
||||
v-if="runningRestore"
|
||||
indeterminate
|
||||
/>
|
||||
</BaseDialog>
|
||||
|
||||
<section>
|
||||
<BaseCardSectionTitle :title="$tc('settings.backup-and-exports')">
|
||||
<BaseCardSectionTitle :title="$t('settings.backup-and-exports')">
|
||||
<v-card-text class="py-0 px-1">
|
||||
<i18n path="settings.backup.experimental-description" />
|
||||
<i18n-t keypath="settings.backup.experimental-description" />
|
||||
</v-card-text>
|
||||
</BaseCardSectionTitle>
|
||||
<v-toolbar color="transparent" flat class="justify-between">
|
||||
<BaseButton class="mr-2" @click="createBackup"> {{ $t("settings.backup.create-heading") }} </BaseButton>
|
||||
<AppButtonUpload
|
||||
:text-btn="false"
|
||||
url="/api/admin/backups/upload"
|
||||
accept=".zip"
|
||||
color="info"
|
||||
@uploaded="refreshBackups()"
|
||||
/>
|
||||
<v-toolbar
|
||||
color="transparent"
|
||||
flat
|
||||
class="justify-between"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-2"
|
||||
@click="createBackup"
|
||||
>
|
||||
{{ $t("settings.backup.create-heading") }}
|
||||
</BaseButton>
|
||||
<AppButtonUpload
|
||||
:text-btn="false"
|
||||
url="/api/admin/backups/upload"
|
||||
accept=".zip"
|
||||
color="info"
|
||||
@uploaded="refreshBackups()"
|
||||
/>
|
||||
</v-toolbar>
|
||||
|
||||
<v-data-table
|
||||
|
@ -80,14 +104,15 @@
|
|||
:search="search"
|
||||
@click:row="setSelected"
|
||||
>
|
||||
<template #item.date="{ item }">
|
||||
<template #[`item.date`]="{ item }">
|
||||
{{ $d(Date.parse(item.date), "medium") }}
|
||||
</template>
|
||||
<template #item.actions="{ item }">
|
||||
<template #[`item.actions`]="{ item }">
|
||||
<v-btn
|
||||
icon
|
||||
class="mx-1"
|
||||
color="error"
|
||||
variant="text"
|
||||
@click.stop="
|
||||
deleteDialog = true;
|
||||
deleteTarget = item.name;
|
||||
|
@ -95,18 +120,27 @@
|
|||
>
|
||||
<v-icon> {{ $globals.icons.delete }} </v-icon>
|
||||
</v-btn>
|
||||
<BaseButton small download :download-url="backupsFileNameDownload(item.name)" class="mx-1" @click.stop="() => {}"/>
|
||||
<BaseButton small @click.stop="setSelected(item); importDialog = true">
|
||||
<template #icon> {{ $globals.icons.backupRestore }}</template>
|
||||
<BaseButton
|
||||
small
|
||||
download
|
||||
:download-url="backupsFileNameDownload(item.name)"
|
||||
class="mx-1"
|
||||
@click.stop="() => { }"
|
||||
/>
|
||||
<BaseButton
|
||||
small
|
||||
@click.stop="setSelected(item); importDialog = true"
|
||||
>
|
||||
<template #icon>
|
||||
{{ $globals.icons.backupRestore }}
|
||||
</template>
|
||||
{{ $t("settings.backup.backup-restore") }}
|
||||
</BaseButton>
|
||||
</BaseButton>
|
||||
</template>
|
||||
</v-data-table>
|
||||
<v-divider></v-divider>
|
||||
<v-divider />
|
||||
<div class="d-flex justify-end mt-6">
|
||||
<div>
|
||||
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
@ -117,17 +151,20 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, reactive, ref, toRefs, useContext, onMounted, useRoute } from "@nuxtjs/composition-api";
|
||||
import { useAdminApi } from "~/composables/api";
|
||||
import { AllBackups } from "~/lib/api/types/admin";
|
||||
import type { AllBackups } from "~/lib/api/types/admin";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
|
||||
export default defineComponent({
|
||||
layout: "admin",
|
||||
export default defineNuxtComponent({
|
||||
setup() {
|
||||
const { i18n, $auth } = useContext();
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const i18n = useI18n();
|
||||
const $auth = useMealieAuth();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
|
||||
|
||||
const adminApi = useAdminApi();
|
||||
const selected = ref("");
|
||||
|
@ -149,9 +186,10 @@ export default defineComponent({
|
|||
|
||||
if (data?.error === false) {
|
||||
refreshBackups();
|
||||
alert.success(i18n.tc("settings.backup.backup-created"));
|
||||
} else {
|
||||
alert.error(i18n.tc("settings.backup.error-creating-backup-see-log-file"));
|
||||
alert.success(i18n.t("settings.backup.backup-created"));
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("settings.backup.error-creating-backup-see-log-file"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -163,10 +201,13 @@ export default defineComponent({
|
|||
console.log(error);
|
||||
state.importDialog = false;
|
||||
state.runningRestore = false;
|
||||
alert.error(i18n.tc("settings.backup.restore-fail"));
|
||||
} else {
|
||||
alert.success(i18n.tc("settings.backup.restore-success"));
|
||||
$auth.logout();
|
||||
alert.error(i18n.t("settings.backup.restore-fail"));
|
||||
}
|
||||
else {
|
||||
alert.success(i18n.t("settings.backup.restore-success"));
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -176,7 +217,7 @@ export default defineComponent({
|
|||
const { data } = await adminApi.backups.delete(deleteTarget.value);
|
||||
|
||||
if (!data?.error) {
|
||||
alert.success(i18n.tc("settings.backup.backup-deleted"));
|
||||
alert.success(i18n.t("settings.backup.backup-deleted"));
|
||||
refreshBackups();
|
||||
}
|
||||
}
|
||||
|
@ -189,10 +230,10 @@ export default defineComponent({
|
|||
runningRestore: false,
|
||||
search: "",
|
||||
headers: [
|
||||
{ text: i18n.t("general.name"), value: "name" },
|
||||
{ text: i18n.t("general.created"), value: "date" },
|
||||
{ text: i18n.t("export.size"), value: "size" },
|
||||
{ text: "", value: "actions", align: "right" },
|
||||
{ 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" },
|
||||
],
|
||||
});
|
||||
|
||||
|
@ -205,6 +246,10 @@ export default defineComponent({
|
|||
|
||||
const backupsFileNameDownload = (fileName: string) => `api/admin/backups/${fileName}`;
|
||||
|
||||
useSeoMeta({
|
||||
title: i18n.t("sidebar.backups"),
|
||||
});
|
||||
|
||||
onMounted(refreshBackups);
|
||||
|
||||
return {
|
||||
|
@ -223,7 +268,7 @@ export default defineComponent({
|
|||
},
|
||||
head() {
|
||||
return {
|
||||
title: this.$t("sidebar.backups") as string,
|
||||
title: useI18n().t("sidebar.backups"),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,25 +1,34 @@
|
|||
<template>
|
||||
<v-container class="pa-0">
|
||||
<v-container>
|
||||
<BaseCardSectionTitle :title="$tc('admin.debug-openai-services')">
|
||||
<BaseCardSectionTitle :title="$t('admin.debug-openai-services')">
|
||||
{{ $t('admin.debug-openai-services-description') }}
|
||||
<br />
|
||||
<DocLink class="mt-2" link="/documentation/getting-started/installation/open-ai" />
|
||||
<br>
|
||||
<DocLink
|
||||
class="mt-2"
|
||||
link="/documentation/getting-started/installation/open-ai"
|
||||
/>
|
||||
</BaseCardSectionTitle>
|
||||
</v-container>
|
||||
<v-form ref="uploadForm" @submit.prevent="testOpenAI">
|
||||
<v-form
|
||||
ref="uploadForm"
|
||||
@submit.prevent="testOpenAI"
|
||||
>
|
||||
<div>
|
||||
<v-card-text>
|
||||
<v-container class="pa-0">
|
||||
<v-row>
|
||||
<v-col cols="auto" align-self="center">
|
||||
<v-col
|
||||
cols="auto"
|
||||
align-self="center"
|
||||
>
|
||||
<AppButtonUpload
|
||||
v-if="!uploadedImage"
|
||||
class="ml-auto"
|
||||
url="none"
|
||||
file-name="image"
|
||||
accept="image/*"
|
||||
:text="$i18n.tc('recipe.upload-image')"
|
||||
:text="$t('recipe.upload-image')"
|
||||
:text-btn="false"
|
||||
:post="false"
|
||||
@uploaded="uploadImage"
|
||||
|
@ -29,13 +38,18 @@
|
|||
color="error"
|
||||
@click="clearImage"
|
||||
>
|
||||
<v-icon left>{{ $globals.icons.close }}</v-icon>
|
||||
{{ $i18n.tc("recipe.remove-image") }}
|
||||
<v-icon start>
|
||||
{{ $globals.icons.close }}
|
||||
</v-icon>
|
||||
{{ $t("recipe.remove-image") }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-spacer />
|
||||
</v-row>
|
||||
<v-row v-if="uploadedImage && uploadedImagePreviewUrl" style="max-width: 25%;">
|
||||
<v-row
|
||||
v-if="uploadedImage && uploadedImagePreviewUrl"
|
||||
style="max-width: 25%;"
|
||||
>
|
||||
<v-spacer />
|
||||
<v-col cols="12">
|
||||
<v-img :src="uploadedImagePreviewUrl" />
|
||||
|
@ -47,7 +61,7 @@
|
|||
<v-card-actions>
|
||||
<BaseButton
|
||||
type="submit"
|
||||
:text="$i18n.tc('admin.run-test')"
|
||||
:text="$t('admin.run-test')"
|
||||
:icon="$globals.icons.check"
|
||||
:loading="loading"
|
||||
class="ml-auto"
|
||||
|
@ -55,8 +69,14 @@
|
|||
</v-card-actions>
|
||||
</div>
|
||||
</v-form>
|
||||
<v-divider v-if="response" class="mt-4" />
|
||||
<v-container v-if="response" class="ma-0 pa-0">
|
||||
<v-divider
|
||||
v-if="response"
|
||||
class="mt-4"
|
||||
/>
|
||||
<v-container
|
||||
v-if="response"
|
||||
class="ma-0 pa-0"
|
||||
>
|
||||
<v-card-title> {{ $t('admin.test-results') }} </v-card-title>
|
||||
<v-card-text> {{ response }} </v-card-text>
|
||||
</v-container>
|
||||
|
@ -64,15 +84,23 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from "@nuxtjs/composition-api";
|
||||
import { useAdminApi } from "~/composables/api";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import { VForm } from "~/types/vuetify";
|
||||
|
||||
export default defineComponent({
|
||||
layout: "admin",
|
||||
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("");
|
||||
|
||||
|
@ -102,7 +130,8 @@ export default defineComponent({
|
|||
|
||||
if (!data) {
|
||||
alert.error("Unable to test OpenAI services");
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
response.value = data.response || (data.success ? "Test Successful" : "Test Failed");
|
||||
}
|
||||
}
|
||||
|
@ -118,10 +147,5 @@ export default defineComponent({
|
|||
testOpenAI,
|
||||
};
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: this.$t("admin.debug-openai-services"),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -10,43 +10,86 @@
|
|||
</BaseCardSectionTitle>
|
||||
|
||||
<div class="d-flex align-center justify-center justify-md-start flex-wrap">
|
||||
<v-btn-toggle v-model="parser" dense mandatory @change="processIngredient">
|
||||
<v-btn value="nlp"> {{ $t('admin.nlp') }} </v-btn>
|
||||
<v-btn value="brute"> {{ $t('admin.brute') }} </v-btn>
|
||||
<v-btn value="openai"> {{ $t('admin.openai') }} </v-btn>
|
||||
<v-btn-toggle
|
||||
v-model="parser"
|
||||
density="compact"
|
||||
mandatory="force"
|
||||
@change="processIngredient"
|
||||
>
|
||||
<v-btn value="nlp">
|
||||
{{ $t('admin.nlp') }}
|
||||
</v-btn>
|
||||
<v-btn value="brute">
|
||||
{{ $t('admin.brute') }}
|
||||
</v-btn>
|
||||
<v-btn value="openai">
|
||||
{{ $t('admin.openai') }}
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
|
||||
<v-checkbox v-model="showConfidence" class="ml-5" :label="$t('admin.show-individual-confidence')"></v-checkbox>
|
||||
<v-spacer />
|
||||
<v-checkbox
|
||||
v-model="showConfidence"
|
||||
class="ml-5"
|
||||
:label="$t('admin.show-individual-confidence')"
|
||||
hide-details
|
||||
/>
|
||||
</div>
|
||||
|
||||
<v-card flat>
|
||||
<v-card-text>
|
||||
<v-text-field v-model="ingredient" :label="$t('admin.ingredient-text')"> </v-text-field>
|
||||
<v-text-field
|
||||
v-model="ingredient"
|
||||
:label="$t('admin.ingredient-text')"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<BaseButton class="ml-auto" @click="processIngredient">
|
||||
<template #icon> {{ $globals.icons.check }}</template>
|
||||
<BaseButton
|
||||
class="ml-auto"
|
||||
@click="processIngredient"
|
||||
>
|
||||
<template #icon>
|
||||
{{ $globals.icons.check }}
|
||||
</template>
|
||||
{{ $t("general.submit") }}
|
||||
</BaseButton>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-container>
|
||||
<v-container v-if="results">
|
||||
<div v-if="parser !== 'brute' && getConfidence('average')" class="d-flex">
|
||||
<v-chip dark :color="getColor('average')" class="mx-auto mb-2">
|
||||
<div
|
||||
v-if="parser !== 'brute' && getConfidence('average')"
|
||||
class="d-flex"
|
||||
>
|
||||
<v-chip
|
||||
dark
|
||||
:color="getColor('average')"
|
||||
class="mx-auto mb-2"
|
||||
>
|
||||
{{ $t('admin.average-confident', [getConfidence("average")]) }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<div class="d-flex justify-center flex-wrap" style="gap: 1.5rem">
|
||||
<div
|
||||
class="d-flex justify-center flex-wrap"
|
||||
style="gap: 1.5rem"
|
||||
>
|
||||
<template v-for="(prop, index) in properties">
|
||||
<div v-if="prop.value" :key="index" class="flex-grow-1">
|
||||
<div
|
||||
v-if="prop.value"
|
||||
:key="index"
|
||||
class="flex-grow-1"
|
||||
>
|
||||
<v-card min-width="200px">
|
||||
<v-card-title> {{ prop.value }} </v-card-title>
|
||||
<v-card-text>
|
||||
{{ prop.subtitle }}
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-chip v-if="prop.confidence && showConfidence" dark :color="prop.color" class="mt-2">
|
||||
<v-chip
|
||||
v-if="prop.confidence && showConfidence"
|
||||
dark
|
||||
:color="prop.color!"
|
||||
class="mt-2"
|
||||
>
|
||||
{{ $t('admin.average-confident', [prop.confidence]) }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
@ -55,7 +98,13 @@
|
|||
</v-container>
|
||||
<v-container class="narrow-container">
|
||||
<v-card-title> {{ $t('admin.try-an-example') }} </v-card-title>
|
||||
<v-card v-for="(text, idx) in tryText" :key="idx" class="my-2" hover @click="processTryText(text)">
|
||||
<v-card
|
||||
v-for="(text, idx) in tryText"
|
||||
:key="idx"
|
||||
class="my-2"
|
||||
hover
|
||||
@click="processTryText(text)"
|
||||
>
|
||||
<v-card-text> {{ text }} </v-card-text>
|
||||
</v-card>
|
||||
</v-container>
|
||||
|
@ -63,17 +112,19 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, ref, toRefs, useContext } from "@nuxtjs/composition-api";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { IngredientConfidence } from "~/lib/api/types/recipe";
|
||||
import { Parser } from "~/lib/api/user/recipes/recipe";
|
||||
import type { IngredientConfidence } from "~/lib/api/types/recipe";
|
||||
import type { Parser } from "~/lib/api/user/recipes/recipe";
|
||||
|
||||
type ConfidenceAttribute = "average" | "comment" | "name" | "unit" | "quantity" | "food";
|
||||
|
||||
export default defineComponent({
|
||||
layout: "admin",
|
||||
export default defineNuxtComponent({
|
||||
setup() {
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const api = useUserApi();
|
||||
|
||||
const state = reactive({
|
||||
|
@ -83,7 +134,12 @@ export default defineComponent({
|
|||
parser: "nlp" as Parser,
|
||||
});
|
||||
|
||||
const { i18n } = useContext();
|
||||
const i18n = useI18n();
|
||||
|
||||
// Set page title
|
||||
useSeoMeta({
|
||||
title: i18n.t("admin.parser"),
|
||||
});
|
||||
|
||||
const confidence = ref<IngredientConfidence>({});
|
||||
|
||||
|
@ -96,9 +152,11 @@ export default defineComponent({
|
|||
// Set color based off range
|
||||
if (p_as_num > 75) {
|
||||
return "success";
|
||||
} else if (p_as_num > 60) {
|
||||
}
|
||||
else if (p_as_num > 60) {
|
||||
return "warning";
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
return "error";
|
||||
}
|
||||
}
|
||||
|
@ -109,8 +167,8 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
const property = confidence.value[attribute];
|
||||
if (property !== undefined) {
|
||||
return `${(property * 100).toFixed(0)}%`;
|
||||
if (property !== undefined && property !== null) {
|
||||
return `${(+property * 100).toFixed(0)}%`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
@ -154,15 +212,14 @@ export default defineComponent({
|
|||
const color = getColor(property);
|
||||
const confidence = getConfidence(property);
|
||||
if (color) {
|
||||
// @ts-ignore See above
|
||||
properties[property].color = color;
|
||||
}
|
||||
if (confidence) {
|
||||
// @ts-ignore See above
|
||||
properties[property].confidence = confidence;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("events.something-went-wrong") as string);
|
||||
state.results = false;
|
||||
}
|
||||
|
@ -210,11 +267,6 @@ export default defineComponent({
|
|||
processIngredient,
|
||||
};
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: this.$t("admin.parser"),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,70 +1,75 @@
|
|||
<template>
|
||||
<v-container fluid class="narrow-container">
|
||||
<BaseDialog
|
||||
v-model="state.storageDetails"
|
||||
:title="$t('admin.maintenance.storage-details')"
|
||||
<BaseDialog v-model="state.storageDetails" :title="$t('admin.maintenance.storage-details')"
|
||||
:icon="$globals.icons.folderOutline"
|
||||
>
|
||||
>
|
||||
<div class="py-2">
|
||||
<template v-for="(value, key, idx) in storageDetails">
|
||||
<v-list-item :key="`item-${key}`">
|
||||
<template v-for="(value, key, idx) in storageDetails" :key="`item-${key}`">
|
||||
<v-list-item>
|
||||
<v-list-item-title>
|
||||
<div>{{ storageDetailsText(key) }}</div>
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-end"> {{ value }} </v-list-item-subtitle>
|
||||
<v-list-item-subtitle class="text-end">
|
||||
{{ value }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-divider v-if="idx != 4" :key="`divider-${key}`" class="mx-2"></v-divider>
|
||||
<v-divider v-if="idx != 4" :key="`divider-${key}`" class="mx-2" />
|
||||
</template>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
||||
<BasePageTitle divider>
|
||||
<template #title> {{ $t("admin.maintenance.page-title") }} </template>
|
||||
<template #title>
|
||||
{{ $t("admin.maintenance.page-title") }}
|
||||
</template>
|
||||
</BasePageTitle>
|
||||
|
||||
<section>
|
||||
<BaseCardSectionTitle class="pb-0" :icon="$globals.icons.wrench" :title="$tc('admin.maintenance.summary-title')">
|
||||
</BaseCardSectionTitle>
|
||||
<BaseCardSectionTitle class="pb-0" :icon="$globals.icons.wrench" :title="$t('admin.maintenance.summary-title')" />
|
||||
<div class="mb-6 ml-2 d-flex" style="gap: 0.3rem">
|
||||
<BaseButton color="info" @click="getSummary">
|
||||
<template #icon> {{ $globals.icons.tools }} </template>
|
||||
<template #icon>
|
||||
{{ $globals.icons.tools }}
|
||||
</template>
|
||||
{{ $t("admin.maintenance.button-label-get-summary") }}
|
||||
</BaseButton>
|
||||
<BaseButton color="info" @click="openDetails">
|
||||
<template #icon> {{ $globals.icons.folderOutline }} </template>
|
||||
<template #icon>
|
||||
{{ $globals.icons.folderOutline }}
|
||||
</template>
|
||||
{{ $t("admin.maintenance.button-label-open-details") }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
<v-card class="ma-2" :loading="state.fetchingInfo">
|
||||
<template v-for="(value, idx) in info">
|
||||
<v-list-item :key="`item-${idx}`">
|
||||
<template v-for="(value, idx) in info" :key="`item-${idx}`">
|
||||
<v-list-item>
|
||||
<v-list-item-title class="py-2">
|
||||
<div>{{ value.name }}</div>
|
||||
<v-list-item-subtitle class="text-end"> {{ value.value }} </v-list-item-subtitle>
|
||||
<v-list-item-subtitle class="text-end">
|
||||
{{ value.value }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider :key="`divider-${idx}`" class="mx-2"></v-divider>
|
||||
<v-divider class="mx-2" />
|
||||
</template>
|
||||
</v-card>
|
||||
</section>
|
||||
<section>
|
||||
<BaseCardSectionTitle
|
||||
class="pb-0 mt-8"
|
||||
:icon="$globals.icons.wrench"
|
||||
:title="$tc('admin.mainentance.actions-title')"
|
||||
>
|
||||
<i18n path="admin.maintenance.actions-description">
|
||||
<BaseCardSectionTitle class="pb-0 mt-8" :icon="$globals.icons.wrench"
|
||||
:title="$t('admin.mainentance.actions-title')"
|
||||
>
|
||||
<i18n-t keypath="admin.maintenance.actions-description">
|
||||
<template #destructive_in_bold>
|
||||
<b>{{ $t("admin.maintenance.actions-description-destructive") }}</b>
|
||||
</template>
|
||||
<template #irreversible_in_bold>
|
||||
<b>{{ $t("admin.maintenance.actions-description-irreversible") }}</b>
|
||||
</template>
|
||||
</i18n>
|
||||
</i18n-t>
|
||||
</BaseCardSectionTitle>
|
||||
<v-card class="ma-2" :loading="state.actionLoading">
|
||||
<template v-for="(action, idx) in actions">
|
||||
<v-list-item :key="`item-${idx}`" class="py-1">
|
||||
<template v-for="(action, idx) in actions" :key="`item-${idx}`">
|
||||
<v-list-item class="py-1">
|
||||
<v-list-item-title>
|
||||
<div>{{ action.name }}</div>
|
||||
<v-list-item-subtitle class="wrap-word">
|
||||
|
@ -72,11 +77,13 @@
|
|||
</v-list-item-subtitle>
|
||||
</v-list-item-title>
|
||||
<BaseButton color="info" @click="action.handler">
|
||||
<template #icon> {{ $globals.icons.robot }}</template>
|
||||
<template #icon>
|
||||
{{ $globals.icons.robot }}
|
||||
</template>
|
||||
{{ $t("general.run") }}
|
||||
</BaseButton>
|
||||
</v-list-item>
|
||||
<v-divider :key="`divider-${idx}`" class="mx-2"></v-divider>
|
||||
<v-divider class="mx-2" />
|
||||
</template>
|
||||
</v-card>
|
||||
</section>
|
||||
|
@ -84,13 +91,15 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, ref, defineComponent, reactive, useContext } from "@nuxtjs/composition-api";
|
||||
import { useAdminApi } from "~/composables/api";
|
||||
import { MaintenanceStorageDetails, MaintenanceSummary } from "~/lib/api/types/admin";
|
||||
import type { MaintenanceStorageDetails, MaintenanceSummary } from "~/lib/api/types/admin";
|
||||
|
||||
export default defineComponent({
|
||||
layout: "admin",
|
||||
export default defineNuxtComponent({
|
||||
setup() {
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
storageDetails: false,
|
||||
storageDetailsLoading: false,
|
||||
|
@ -99,13 +108,18 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
const adminApi = useAdminApi();
|
||||
const { i18n } = useContext();
|
||||
const i18n = useI18n();
|
||||
|
||||
// Set page title
|
||||
useSeoMeta({
|
||||
title: i18n.t("admin.maintenance.page-title"),
|
||||
});
|
||||
|
||||
// ==========================================================================
|
||||
// General Info
|
||||
|
||||
const infoResults = ref<MaintenanceSummary>({
|
||||
dataDirSize: i18n.tc("about.unknown-version"),
|
||||
dataDirSize: i18n.t("about.unknown-version"),
|
||||
cleanableDirs: 0,
|
||||
cleanableImages: 0,
|
||||
});
|
||||
|
@ -115,7 +129,7 @@ export default defineComponent({
|
|||
const { data } = await adminApi.maintenance.getInfo();
|
||||
|
||||
infoResults.value = data ?? {
|
||||
dataDirSize: i18n.tc("about.unknown-version"),
|
||||
dataDirSize: i18n.t("about.unknown-version"),
|
||||
cleanableDirs: 0,
|
||||
cleanableImages: 0,
|
||||
};
|
||||
|
@ -152,7 +166,7 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
function storageDetailsText(key: string) {
|
||||
return storageTitles[key] ?? i18n.tc("about.unknown-version");
|
||||
return storageTitles[key] ?? i18n.t("about.unknown-version");
|
||||
}
|
||||
|
||||
const storageDetails = ref<MaintenanceStorageDetails | null>(null);
|
||||
|
@ -219,11 +233,6 @@ export default defineComponent({
|
|||
actions,
|
||||
};
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: this.$t("admin.maintenance.page-title") as string,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,47 +1,73 @@
|
|||
<template>
|
||||
<v-container v-if="group" class="narrow-container">
|
||||
<v-container
|
||||
v-if="group"
|
||||
class="narrow-container"
|
||||
>
|
||||
<BasePageTitle>
|
||||
<template #header>
|
||||
<v-img max-height="125" max-width="125" :src="require('~/static/svgs/manage-group-settings.svg')"></v-img>
|
||||
<v-img
|
||||
width="100%"
|
||||
max-height="125"
|
||||
max-width="125"
|
||||
:src="require('~/static/svgs/manage-group-settings.svg')"
|
||||
/>
|
||||
</template>
|
||||
<template #title>
|
||||
{{ $t('group.admin-group-management') }}
|
||||
</template>
|
||||
<template #title> {{ $t('group.admin-group-management') }} </template>
|
||||
{{ $t('group.admin-group-management-text') }}
|
||||
</BasePageTitle>
|
||||
<AppToolbar back> </AppToolbar>
|
||||
<AppToolbar back />
|
||||
<v-card-text> {{ $t('group.group-id-value', [group.id]) }} </v-card-text>
|
||||
<v-form v-if="!userError" ref="refGroupEditForm" @submit.prevent="handleSubmit">
|
||||
<v-card outlined>
|
||||
<v-form
|
||||
v-if="!userError"
|
||||
ref="refGroupEditForm"
|
||||
@submit.prevent="handleSubmit"
|
||||
>
|
||||
<v-card variant="outlined" style="border-color: lightgrey;">
|
||||
<v-card-text>
|
||||
<v-text-field v-model="group.name" :label="$t('group.group-name')"> </v-text-field>
|
||||
<GroupPreferencesEditor v-if="group.preferences" v-model="group.preferences" />
|
||||
<v-text-field
|
||||
v-model="group.name"
|
||||
:label="$t('group.group-name')"
|
||||
/>
|
||||
<GroupPreferencesEditor
|
||||
v-if="group.preferences"
|
||||
v-model="group.preferences"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<div class="d-flex pa-2">
|
||||
<BaseButton type="submit" edit class="ml-auto"> {{ $t("general.update") }}</BaseButton>
|
||||
<BaseButton
|
||||
type="submit"
|
||||
edit
|
||||
class="ml-auto"
|
||||
>
|
||||
{{ $t("general.update") }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</v-form>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, useRoute, onMounted, ref, useContext } from "@nuxtjs/composition-api";
|
||||
import GroupPreferencesEditor from "~/components/Domain/Group/GroupPreferencesEditor.vue";
|
||||
import { useAdminApi } from "~/composables/api";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import { GroupInDB } from "~/lib/api/types/user";
|
||||
import { VForm } from "~/types/vuetify";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
GroupPreferencesEditor,
|
||||
},
|
||||
layout: "admin",
|
||||
setup() {
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const { i18n } = useContext();
|
||||
const i18n = useI18n();
|
||||
|
||||
const groupId = route.value.params.id;
|
||||
const groupId = computed(() => route.params.id as string);
|
||||
|
||||
// ==============================================
|
||||
// New User Form
|
||||
|
@ -50,22 +76,20 @@ export default defineComponent({
|
|||
|
||||
const adminApi = useAdminApi();
|
||||
|
||||
const group = ref<GroupInDB | null>(null);
|
||||
|
||||
const userError = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
const { data, error } = await adminApi.groups.getOne(groupId);
|
||||
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.tc("user.user-not-found"));
|
||||
alert.error(i18n.t("user.user-not-found"));
|
||||
userError.value = true;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
group.value = data;
|
||||
}
|
||||
});
|
||||
return data;
|
||||
}, { watch: [groupId] });
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!refGroupEditForm.value?.validate() || group.value === null) {
|
||||
|
@ -79,8 +103,9 @@ export default defineComponent({
|
|||
window.location.reload();
|
||||
}
|
||||
group.value = data;
|
||||
} else {
|
||||
alert.error(i18n.tc("settings.settings-update-failed"));
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("settings.settings-update-failed"));
|
||||
}
|
||||
}
|
||||
|
|
@ -4,11 +4,16 @@
|
|||
v-model="createDialog"
|
||||
:title="$t('group.create-group')"
|
||||
:icon="$globals.icons.group"
|
||||
can-submit
|
||||
@submit="createGroup(createGroupForm.data)"
|
||||
>
|
||||
<template #activator> </template>
|
||||
<template #activator />
|
||||
<v-card-text>
|
||||
<AutoForm v-model="createGroupForm.data" :update-mode="updateMode" :items="createGroupForm.items" />
|
||||
<AutoForm
|
||||
v-model="createGroupForm.data"
|
||||
:update-mode="updateMode"
|
||||
:items="createGroupForm.items"
|
||||
/>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
|
||||
|
@ -16,18 +21,25 @@
|
|||
v-model="confirmDialog"
|
||||
:title="$t('general.confirm')"
|
||||
color="error"
|
||||
can-confirm
|
||||
@confirm="deleteGroup(deleteTarget)"
|
||||
>
|
||||
<template #activator> </template>
|
||||
<template #activator />
|
||||
<v-card-text>
|
||||
{{ $t("general.confirm-delete-generic") }}
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
|
||||
<BaseCardSectionTitle :title="$tc('group.group-management')"> </BaseCardSectionTitle>
|
||||
<BaseCardSectionTitle :title="$t('group.group-management')" />
|
||||
<section>
|
||||
<v-toolbar flat color="transparent" class="justify-between">
|
||||
<BaseButton @click="openDialog"> {{ $t("general.create") }} </BaseButton>
|
||||
<v-toolbar
|
||||
flat
|
||||
color="transparent"
|
||||
class="justify-between"
|
||||
>
|
||||
<BaseButton @click="openDialog">
|
||||
{{ $t("general.create") }}
|
||||
</BaseButton>
|
||||
</v-toolbar>
|
||||
|
||||
<v-data-table
|
||||
|
@ -38,26 +50,30 @@
|
|||
hide-default-footer
|
||||
disable-pagination
|
||||
:search="search"
|
||||
@click:row="handleRowClick"
|
||||
@click:row="($event, { item }) => handleRowClick(item)"
|
||||
>
|
||||
<template #item.households="{ item }">
|
||||
{{ item.households.length }}
|
||||
<template #[`item.households`]="{ item }">
|
||||
{{ item.households!.length }}
|
||||
</template>
|
||||
<template #item.users="{ item }">
|
||||
{{ item.users.length }}
|
||||
<template #[`item.users`]="{ item }">
|
||||
{{ item.users!.length }}
|
||||
</template>
|
||||
<template #item.actions="{ item }">
|
||||
<v-tooltip bottom :disabled="!(item && (item.households.length > 0 || item.users.length > 0))">
|
||||
<template #activator="{ on, attrs }">
|
||||
<div v-bind="attrs" v-on="on" >
|
||||
<template #[`item.actions`]="{ item }">
|
||||
<v-tooltip
|
||||
bottom
|
||||
:disabled="!(item && (item.households!.length > 0 || item.users!.length > 0))"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<div v-bind="props">
|
||||
<v-btn
|
||||
:disabled="item && (item.households.length > 0 || item.users.length > 0)"
|
||||
:disabled="item && (item.households!.length > 0 || item.users!.length > 0)"
|
||||
class="mr-1"
|
||||
icon
|
||||
color="error"
|
||||
variant="text"
|
||||
@click.stop="
|
||||
confirmDialog = true;
|
||||
deleteTarget = item.id;
|
||||
deleteTarget = +item.id;
|
||||
"
|
||||
>
|
||||
<v-icon>
|
||||
|
@ -66,25 +82,33 @@
|
|||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
<span>{{ $tc("admin.group-delete-note") }}</span>
|
||||
<span>{{ $t("admin.group-delete-note") }}</span>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
</v-data-table>
|
||||
<v-divider></v-divider>
|
||||
<v-divider />
|
||||
</section>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, toRefs, useContext, useRouter } from "@nuxtjs/composition-api";
|
||||
import { fieldTypes } from "~/composables/forms";
|
||||
import { useGroups } from "~/composables/use-groups";
|
||||
import { GroupInDB } from "~/lib/api/types/user";
|
||||
import type { GroupInDB } from "~/lib/api/types/user";
|
||||
|
||||
export default defineComponent({
|
||||
layout: "admin",
|
||||
export default defineNuxtComponent({
|
||||
setup() {
|
||||
const { i18n } = useContext();
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
// Set page title
|
||||
useSeoMeta({
|
||||
title: i18n.t("group.manage-groups"),
|
||||
});
|
||||
|
||||
const { groups, refreshAllGroups, deleteGroup, createGroup } = useGroups();
|
||||
|
||||
const state = reactive({
|
||||
|
@ -94,15 +118,15 @@ export default defineComponent({
|
|||
search: "",
|
||||
headers: [
|
||||
{
|
||||
text: i18n.t("group.group"),
|
||||
title: i18n.t("group.group"),
|
||||
align: "start",
|
||||
sortable: false,
|
||||
value: "id",
|
||||
},
|
||||
{ text: i18n.t("general.name"), value: "name" },
|
||||
{ text: i18n.t("group.total-households"), value: "households" },
|
||||
{ text: i18n.t("user.total-users"), value: "users" },
|
||||
{ text: i18n.t("general.delete"), value: "actions" },
|
||||
{ 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: {
|
||||
|
@ -125,17 +149,15 @@ export default defineComponent({
|
|||
state.createGroupForm.data.name = "";
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function handleRowClick(item: GroupInDB) {
|
||||
router.push(`/admin/manage/groups/${item.id}`);
|
||||
navigateTo(`/admin/manage/groups/${item.id}`);
|
||||
}
|
||||
|
||||
return { ...toRefs(state), groups, refreshAllGroups, deleteGroup, createGroup, openDialog, handleRowClick };
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: this.$t("group.manage-groups") as string,
|
||||
title: useI18n().t("group.manage-groups"),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,67 +1,94 @@
|
|||
<template>
|
||||
<v-container v-if="household" class="narrow-container">
|
||||
<v-container
|
||||
v-if="household"
|
||||
class="narrow-container"
|
||||
>
|
||||
<BasePageTitle>
|
||||
<template #header>
|
||||
<v-img max-height="125" max-width="125" :src="require('~/static/svgs/manage-group-settings.svg')"></v-img>
|
||||
<v-img
|
||||
width="100%"
|
||||
max-height="125"
|
||||
max-width="125"
|
||||
:src="require('~/static/svgs/manage-group-settings.svg')"
|
||||
/>
|
||||
</template>
|
||||
<template #title>
|
||||
{{ $t('household.admin-household-management') }}
|
||||
</template>
|
||||
<template #title> {{ $t('household.admin-household-management') }} </template>
|
||||
{{ $t('household.admin-household-management-text') }}
|
||||
</BasePageTitle>
|
||||
<AppToolbar back> </AppToolbar>
|
||||
<AppToolbar back />
|
||||
<v-card-text> {{ $t('household.household-id-value', [household.id]) }} </v-card-text>
|
||||
<v-form v-if="!userError" ref="refHouseholdEditForm" @submit.prevent="handleSubmit">
|
||||
<v-card outlined>
|
||||
<v-form
|
||||
v-if="!userError"
|
||||
ref="refHouseholdEditForm"
|
||||
@submit.prevent="handleSubmit"
|
||||
>
|
||||
<v-card variant="outlined" style="border-color: lightgrey;">
|
||||
<v-card-text>
|
||||
<v-select
|
||||
v-if="groups"
|
||||
v-model="household.groupId"
|
||||
disabled
|
||||
:items="groups"
|
||||
rounded
|
||||
variant="solo-filled"
|
||||
flat
|
||||
class="rounded-lg"
|
||||
item-text="name"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
:return-object="false"
|
||||
filled
|
||||
:label="$tc('group.user-group')"
|
||||
:label="$t('group.user-group')"
|
||||
:rules="[validators.required]"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="household.name"
|
||||
variant="solo-filled"
|
||||
flat
|
||||
:label="$t('household.household-name')"
|
||||
:rules="[validators.required]"
|
||||
/>
|
||||
<HouseholdPreferencesEditor v-if="household.preferences" v-model="household.preferences" />
|
||||
<HouseholdPreferencesEditor
|
||||
v-if="household.preferences"
|
||||
v-model="household.preferences"
|
||||
variant="solo-filled"
|
||||
flat
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<div class="d-flex pa-2">
|
||||
<BaseButton type="submit" edit class="ml-auto"> {{ $t("general.update") }}</BaseButton>
|
||||
<BaseButton
|
||||
type="submit"
|
||||
edit
|
||||
class="ml-auto"
|
||||
>
|
||||
{{ $t("general.update") }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</v-form>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, useRoute, onMounted, ref, useContext } from "@nuxtjs/composition-api";
|
||||
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 { HouseholdInDB } from "~/lib/api/types/household";
|
||||
import { VForm } from "~/types/vuetify";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
HouseholdPreferencesEditor,
|
||||
},
|
||||
layout: "admin",
|
||||
setup() {
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const { i18n } = useContext();
|
||||
const i18n = useI18n();
|
||||
|
||||
const { groups } = useGroups();
|
||||
const householdId = route.value.params.id;
|
||||
const householdId = computed(() => route.params.id as string);
|
||||
|
||||
// ==============================================
|
||||
// New User Form
|
||||
|
@ -70,22 +97,20 @@ export default defineComponent({
|
|||
|
||||
const adminApi = useAdminApi();
|
||||
|
||||
const household = ref<HouseholdInDB | null>(null);
|
||||
|
||||
const userError = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
const { data, error } = await adminApi.households.getOne(householdId);
|
||||
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.tc("user.user-not-found"));
|
||||
alert.error(i18n.t("user.user-not-found"));
|
||||
userError.value = true;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
household.value = data;
|
||||
}
|
||||
});
|
||||
return data;
|
||||
}, { watch: [householdId] });
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!refHouseholdEditForm.value?.validate() || household.value === null) {
|
||||
|
@ -95,9 +120,10 @@ export default defineComponent({
|
|||
const { response, data } = await adminApi.households.updateOne(household.value.id, household.value);
|
||||
if (response?.status === 200 && data) {
|
||||
household.value = data;
|
||||
alert.success(i18n.tc("settings.settings-updated"));
|
||||
} else {
|
||||
alert.error(i18n.tc("settings.settings-update-failed"));
|
||||
alert.success(i18n.t("settings.settings-updated"));
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("settings.settings-update-failed"));
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
:title="$t('household.create-household')"
|
||||
:icon="$globals.icons.household"
|
||||
>
|
||||
<template #activator> </template>
|
||||
<template #activator />
|
||||
<v-card-text>
|
||||
<v-form ref="refNewHouseholdForm">
|
||||
<v-select
|
||||
|
@ -14,18 +14,27 @@
|
|||
:items="groups"
|
||||
rounded
|
||||
class="rounded-lg"
|
||||
item-text="name"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
:return-object="false"
|
||||
filled
|
||||
:label="$tc('household.household-group')"
|
||||
variant="filled"
|
||||
:label="$t('household.household-group')"
|
||||
:rules="[validators.required]"
|
||||
/>
|
||||
<AutoForm v-model="createHouseholdForm.data" :update-mode="updateMode" :items="createHouseholdForm.items" />
|
||||
<AutoForm
|
||||
v-model="createHouseholdForm.data"
|
||||
:update-mode="updateMode"
|
||||
:items="createHouseholdForm.items"
|
||||
/>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<template #custom-card-action>
|
||||
<BaseButton type="submit" @click="handleCreateSubmit"> {{ $t("general.create") }} </BaseButton>
|
||||
<BaseButton
|
||||
type="submit"
|
||||
@click="handleCreateSubmit"
|
||||
>
|
||||
{{ $t("general.create") }}
|
||||
</BaseButton>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
|
||||
|
@ -33,51 +42,63 @@
|
|||
v-model="confirmDialog"
|
||||
:title="$t('general.confirm')"
|
||||
color="error"
|
||||
can-confirm
|
||||
@confirm="deleteHousehold(deleteTarget)"
|
||||
>
|
||||
<template #activator> </template>
|
||||
<template #activator />
|
||||
<v-card-text>
|
||||
{{ $t("general.confirm-delete-generic") }}
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
|
||||
<BaseCardSectionTitle :title="$tc('household.household-management')"> </BaseCardSectionTitle>
|
||||
<BaseCardSectionTitle :title="$t('household.household-management')" />
|
||||
<section>
|
||||
<v-toolbar flat color="transparent" class="justify-between">
|
||||
<BaseButton @click="openDialog"> {{ $t("general.create") }} </BaseButton>
|
||||
<v-toolbar
|
||||
flat
|
||||
color="transparent"
|
||||
class="justify-between"
|
||||
>
|
||||
<BaseButton @click="openDialog">
|
||||
{{ $t("general.create") }}
|
||||
</BaseButton>
|
||||
</v-toolbar>
|
||||
|
||||
<v-data-table
|
||||
v-if="headers && households"
|
||||
:headers="headers"
|
||||
:items="households || []"
|
||||
:items="households"
|
||||
item-key="id"
|
||||
class="elevation-0"
|
||||
hide-default-footer
|
||||
disable-pagination
|
||||
:search="search"
|
||||
@click:row="handleRowClick"
|
||||
@click:row="($event, { item }) => handleRowClick(item)"
|
||||
>
|
||||
<template #item.users="{ item }">
|
||||
{{ item.users.length }}
|
||||
<template #[`item.users`]="{ item }">
|
||||
{{ item.users?.length }}
|
||||
</template>
|
||||
<template #item.group="{ item }">
|
||||
<template #[`item.group`]="{ item }">
|
||||
{{ item.group }}
|
||||
</template>
|
||||
<template #item.webhookEnable="{ item }">
|
||||
{{ item.webhooks.length > 0 ? $t("general.yes") : $t("general.no") }}
|
||||
<template #[`item.webhookEnable`]="{ item }">
|
||||
{{ item.webhooks!.length > 0 ? $t("general.yes") : $t("general.no") }}
|
||||
</template>
|
||||
<template #item.actions="{ item }">
|
||||
<v-tooltip bottom :disabled="!(item && item.users.length > 0)">
|
||||
<template #activator="{ on, attrs }">
|
||||
<div v-bind="attrs" v-on="on" >
|
||||
<template #[`item.actions`]="{ item }">
|
||||
<v-tooltip
|
||||
bottom
|
||||
:disabled="!(item && item.users!.length > 0)"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<div v-bind="props">
|
||||
<v-btn
|
||||
:disabled="item && item.users.length > 0"
|
||||
:disabled="item && item.users!.length > 0"
|
||||
class="mr-1"
|
||||
icon
|
||||
color="error"
|
||||
variant="text"
|
||||
@click.stop="
|
||||
confirmDialog = true;
|
||||
deleteTarget = item.id;
|
||||
deleteTarget = +item.id;
|
||||
"
|
||||
>
|
||||
<v-icon>
|
||||
|
@ -86,28 +107,36 @@
|
|||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
<span>{{ $tc("admin.household-delete-note") }}</span>
|
||||
<span>{{ $t("admin.household-delete-note") }}</span>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
</v-data-table>
|
||||
<v-divider></v-divider>
|
||||
<v-divider />
|
||||
</section>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, ref, toRefs, useContext, useRouter } from "@nuxtjs/composition-api";
|
||||
import { fieldTypes } from "~/composables/forms";
|
||||
import { useGroups } from "~/composables/use-groups";
|
||||
import { useAdminHouseholds } from "~/composables/use-households";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import { HouseholdInDB } from "~/lib/api/types/household";
|
||||
import { VForm } from "~/types/vuetify";
|
||||
import type { HouseholdInDB } from "~/lib/api/types/household";
|
||||
import type { VForm } from "~/types/auto-forms";
|
||||
|
||||
export default defineComponent({
|
||||
layout: "admin",
|
||||
export default defineNuxtComponent({
|
||||
setup() {
|
||||
const { i18n } = useContext();
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
// Set page title
|
||||
useSeoMeta({
|
||||
title: i18n.t("household.manage-households"),
|
||||
});
|
||||
|
||||
const { groups } = useGroups();
|
||||
const { households, refreshAllHouseholds, deleteHousehold, createHousehold } = useAdminHouseholds();
|
||||
const refNewHouseholdForm = ref<VForm | null>(null);
|
||||
|
@ -120,16 +149,16 @@ export default defineComponent({
|
|||
search: "",
|
||||
headers: [
|
||||
{
|
||||
text: i18n.t("household.household"),
|
||||
title: i18n.t("household.household"),
|
||||
align: "start",
|
||||
sortable: false,
|
||||
value: "id",
|
||||
},
|
||||
{ text: i18n.t("general.name"), value: "name" },
|
||||
{ text: i18n.t("group.group"), value: "group" },
|
||||
{ text: i18n.t("user.total-users"), value: "users" },
|
||||
{ text: i18n.t("user.webhooks-enabled"), value: "webhookEnable" },
|
||||
{ text: i18n.t("general.delete"), value: "actions" },
|
||||
{ title: i18n.t("general.name"), value: "name" },
|
||||
{ title: i18n.t("group.group"), value: "group" },
|
||||
{ title: i18n.t("user.total-users"), value: "users" },
|
||||
{ title: i18n.t("user.webhooks-enabled"), value: "webhookEnable" },
|
||||
{ title: i18n.t("general.delete"), value: "actions" },
|
||||
],
|
||||
updateMode: false,
|
||||
createHouseholdForm: {
|
||||
|
@ -170,21 +199,16 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
refNewHouseholdForm,
|
||||
groups,
|
||||
households,
|
||||
validators,
|
||||
refreshAllHouseholds,
|
||||
deleteHousehold,
|
||||
handleCreateSubmit,
|
||||
openDialog,
|
||||
handleRowClick,
|
||||
};
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: this.$t("household.manage-households") as string,
|
||||
...toRefs(state),
|
||||
refNewHouseholdForm,
|
||||
groups,
|
||||
households,
|
||||
validators,
|
||||
refreshAllHouseholds,
|
||||
deleteHousehold,
|
||||
handleCreateSubmit,
|
||||
openDialog,
|
||||
handleRowClick,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,18 +1,35 @@
|
|||
<template>
|
||||
<v-container v-if="user" class="narrow-container">
|
||||
<v-container
|
||||
v-if="user"
|
||||
class="narrow-container"
|
||||
>
|
||||
<BasePageTitle>
|
||||
<template #header>
|
||||
<v-img max-height="125" max-width="125" :src="require('~/static/svgs/manage-profile.svg')"></v-img>
|
||||
<v-img
|
||||
width="100%"
|
||||
max-height="125"
|
||||
max-width="125"
|
||||
:src="require('~/static/svgs/manage-profile.svg')"
|
||||
/>
|
||||
</template>
|
||||
<template #title>
|
||||
{{ $t("user.admin-user-management") }}
|
||||
</template>
|
||||
<template #title> {{ $t("user.admin-user-management") }} </template>
|
||||
{{ $t("user.changes-reflected-immediately") }}
|
||||
</BasePageTitle>
|
||||
<AppToolbar back> </AppToolbar>
|
||||
<v-form v-if="!userError" ref="refNewUserForm" @submit.prevent="handleSubmit">
|
||||
<v-card outlined>
|
||||
<AppToolbar back />
|
||||
<v-form
|
||||
v-if="!userError"
|
||||
ref="refNewUserForm"
|
||||
@submit.prevent="handleSubmit"
|
||||
>
|
||||
<v-card
|
||||
variant="outlined"
|
||||
style="border-color: lightgrey;"
|
||||
>
|
||||
<v-card-text>
|
||||
<div class="d-flex">
|
||||
<p> {{ $t("user.user-id-with-value", {id: user.id} ) }}</p>
|
||||
<p> {{ $t("user.user-id-with-value", { id: user.id }) }}</p>
|
||||
</div>
|
||||
<!-- This is disabled since we can't properly handle changing the user's group in most scenarios -->
|
||||
<v-select
|
||||
|
@ -20,83 +37,118 @@
|
|||
v-model="user.group"
|
||||
disabled
|
||||
:items="groups"
|
||||
rounded
|
||||
class="rounded-lg"
|
||||
item-text="name"
|
||||
variant="solo-filled"
|
||||
flat
|
||||
item-title="name"
|
||||
item-value="name"
|
||||
:return-object="false"
|
||||
filled
|
||||
:label="$tc('group.user-group')"
|
||||
:label="$t('group.user-group')"
|
||||
:rules="[validators.required]"
|
||||
/>
|
||||
<v-select
|
||||
v-if="households"
|
||||
v-model="user.household"
|
||||
:items="households"
|
||||
rounded
|
||||
class="rounded-lg"
|
||||
item-text="name"
|
||||
variant="solo-filled"
|
||||
flat
|
||||
item-title="name"
|
||||
item-value="name"
|
||||
:return-object="false"
|
||||
filled
|
||||
:label="$tc('household.user-household')"
|
||||
:label="$t('household.user-household')"
|
||||
:rules="[validators.required]"
|
||||
/>
|
||||
<div class="d-flex py-2 pr-2">
|
||||
<BaseButton type="button" :loading="generatingToken" create @click.prevent="handlePasswordReset">
|
||||
<BaseButton
|
||||
type="button"
|
||||
:loading="generatingToken"
|
||||
create
|
||||
@click.prevent="handlePasswordReset"
|
||||
>
|
||||
{{ $t("user.generate-password-reset-link") }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
<div v-if="resetUrl" class="mb-2">
|
||||
<div
|
||||
v-if="resetUrl"
|
||||
class="mb-2"
|
||||
>
|
||||
<v-card-text>
|
||||
<p class="text-center pb-0">
|
||||
{{ resetUrl }}
|
||||
</p>
|
||||
</v-card-text>
|
||||
<v-card-actions class="align-center pt-0" style="gap: 4px">
|
||||
<BaseButton cancel @click="resetUrl = ''"> {{ $t("general.close") }} </BaseButton>
|
||||
<v-spacer></v-spacer>
|
||||
<BaseButton v-if="user.email" color="info" class="mr-1" @click="sendResetEmail">
|
||||
<v-card-actions
|
||||
class="align-center pt-0"
|
||||
style="gap: 4px"
|
||||
>
|
||||
<BaseButton
|
||||
cancel
|
||||
@click="resetUrl = ''"
|
||||
>
|
||||
{{ $t("general.close") }}
|
||||
</BaseButton>
|
||||
<v-spacer />
|
||||
<BaseButton
|
||||
v-if="user.email"
|
||||
color="info"
|
||||
class="mr-1"
|
||||
@click="sendResetEmail"
|
||||
>
|
||||
<template #icon>
|
||||
{{ $globals.icons.email }}
|
||||
</template>
|
||||
{{ $t("user.email") }}
|
||||
</BaseButton>
|
||||
<AppButtonCopy :icon="false" color="info" :copy-text="resetUrl" />
|
||||
<AppButtonCopy
|
||||
:icon="false"
|
||||
color="info"
|
||||
:copy-text="resetUrl"
|
||||
/>
|
||||
</v-card-actions>
|
||||
</div>
|
||||
|
||||
<AutoForm v-model="user" :items="userForm" update-mode :disabled-fields="disabledFields" />
|
||||
<AutoForm
|
||||
v-model="user"
|
||||
:items="userForm"
|
||||
update-mode
|
||||
:disabled-fields="disabledFields"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<div class="d-flex pa-2">
|
||||
<BaseButton type="submit" edit class="ml-auto"> {{ $t("general.update") }}</BaseButton>
|
||||
<BaseButton
|
||||
type="submit"
|
||||
edit
|
||||
class="ml-auto"
|
||||
>
|
||||
{{ $t("general.update") }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</v-form>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, useRoute, onMounted, ref, useContext } from "@nuxtjs/composition-api";
|
||||
import { useAdminApi, useUserApi } from "~/composables/api";
|
||||
import { useGroups } from "~/composables/use-groups";
|
||||
import { useAdminHouseholds } from "~/composables/use-households";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import { useUserForm } from "~/composables/use-users";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import { VForm } from "~/types/vuetify";
|
||||
import { UserOut } from "~/lib/api/types/user";
|
||||
import type { UserOut } from "~/lib/api/types/user";
|
||||
|
||||
export default defineComponent({
|
||||
layout: "admin",
|
||||
export default defineNuxtComponent({
|
||||
setup() {
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const { userForm } = useUserForm();
|
||||
const { groups } = useGroups();
|
||||
const { useHouseholdsInGroup } = useAdminHouseholds();
|
||||
const { i18n } = useContext();
|
||||
const i18n = useI18n();
|
||||
const route = useRoute();
|
||||
|
||||
const userId = route.value.params.id;
|
||||
const userId = route.params.id as string;
|
||||
|
||||
// ==============================================
|
||||
// New User Form
|
||||
|
@ -110,7 +162,7 @@ export default defineComponent({
|
|||
|
||||
const disabledFields = computed(() => {
|
||||
return user.value?.authMethod !== "Mealie" ? ["admin"] : [];
|
||||
})
|
||||
});
|
||||
|
||||
const userError = ref(false);
|
||||
|
||||
|
@ -121,7 +173,7 @@ export default defineComponent({
|
|||
const { data, error } = await adminApi.users.getOne(userId);
|
||||
|
||||
if (error?.response?.status === 404) {
|
||||
alert.error(i18n.tc("user.user-not-found"));
|
||||
alert.error(i18n.t("user.user-not-found"));
|
||||
userError.value = true;
|
||||
}
|
||||
|
||||
|
@ -159,9 +211,10 @@ export default defineComponent({
|
|||
if (!user.value?.email) return;
|
||||
const { response } = await userApi.email.sendForgotPassword({ email: user.value.email });
|
||||
if (response && response.status === 200) {
|
||||
alert.success(i18n.tc("profile.email-sent"));
|
||||
} else {
|
||||
alert.error(i18n.tc("profile.error-sending-email"));
|
||||
alert.success(i18n.t("profile.email-sent"));
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("profile.error-sending-email"));
|
||||
}
|
||||
}
|
||||
|
|
@ -2,23 +2,34 @@
|
|||
<v-container class="narrow-container">
|
||||
<BasePageTitle class="mb-2">
|
||||
<template #header>
|
||||
<v-img max-height="125" max-width="125" :src="require('~/static/svgs/manage-profile.svg')"></v-img>
|
||||
<v-img
|
||||
width="100%"
|
||||
max-height="125"
|
||||
max-width="125"
|
||||
:src="require('~/static/svgs/manage-profile.svg')"
|
||||
/>
|
||||
</template>
|
||||
<template #title>
|
||||
{{ $t('user.admin-user-creation') }}
|
||||
</template>
|
||||
<template #title> {{ $t('user.admin-user-creation') }} </template>
|
||||
</BasePageTitle>
|
||||
<AppToolbar back> </AppToolbar>
|
||||
<v-form ref="refNewUserForm" @submit.prevent="handleSubmit">
|
||||
<v-card outlined>
|
||||
<AppToolbar back />
|
||||
<v-form
|
||||
ref="refNewUserForm"
|
||||
@submit.prevent="handleSubmit"
|
||||
>
|
||||
<v-card variant="outlined">
|
||||
<v-card-text>
|
||||
<v-select
|
||||
v-if="groups"
|
||||
v-model="selectedGroupId"
|
||||
:items="groups"
|
||||
rounded
|
||||
class="rounded-lg"
|
||||
item-text="name"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
:return-object="false"
|
||||
filled
|
||||
variant="filled"
|
||||
:label="$t('group.user-group')"
|
||||
:rules="[validators.required]"
|
||||
/>
|
||||
|
@ -28,37 +39,46 @@
|
|||
:items="households"
|
||||
rounded
|
||||
class="rounded-lg"
|
||||
item-text="name"
|
||||
item-title="name"
|
||||
item-value="name"
|
||||
:return-object="false"
|
||||
filled
|
||||
variant="filled"
|
||||
:label="$t('household.user-household')"
|
||||
:hint="selectedGroupId ? '' : $tc('group.you-must-select-a-group-before-selecting-a-household')"
|
||||
:hint="selectedGroupId ? '' : $t('group.you-must-select-a-group-before-selecting-a-household')"
|
||||
persistent-hint
|
||||
:rules="[validators.required]"
|
||||
/>
|
||||
<AutoForm v-model="newUserData" :items="userForm" />
|
||||
<AutoForm
|
||||
v-model="newUserData"
|
||||
:items="userForm"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<div class="d-flex pa-2">
|
||||
<BaseButton type="submit" class="ml-auto"></BaseButton>
|
||||
<BaseButton
|
||||
type="submit"
|
||||
class="ml-auto"
|
||||
/>
|
||||
</div>
|
||||
</v-form>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, useRouter, reactive, ref, toRefs, watch } from "@nuxtjs/composition-api";
|
||||
import { useAdminApi } from "~/composables/api";
|
||||
import { useGroups } from "~/composables/use-groups";
|
||||
import { useAdminHouseholds } from "~/composables/use-households";
|
||||
import { useUserForm } from "~/composables/use-users";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import { VForm } from "~/types/vuetify";
|
||||
import type { UserIn } from "~/lib/api/types/user";
|
||||
import type { VForm } from "~/types/auto-forms";
|
||||
|
||||
export default defineComponent({
|
||||
layout: "admin",
|
||||
export default defineNuxtComponent({
|
||||
setup() {
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const { userForm } = useUserForm();
|
||||
const { groups } = useGroups();
|
||||
const { useHouseholdsInGroup } = useAdminHouseholds();
|
||||
|
@ -75,7 +95,7 @@ export default defineComponent({
|
|||
const households = useHouseholdsInGroup(selectedGroupId);
|
||||
|
||||
const selectedGroup = computed(() => {
|
||||
return groups.value?.find((group) => group.id === selectedGroupId.value);
|
||||
return groups.value?.find(group => group.id === selectedGroupId.value);
|
||||
});
|
||||
const state = reactive({
|
||||
newUserData: {
|
||||
|
@ -101,7 +121,7 @@ export default defineComponent({
|
|||
async function handleSubmit() {
|
||||
if (!refNewUserForm.value?.validate()) return;
|
||||
|
||||
const { response } = await adminApi.users.createOne(state.newUserData);
|
||||
const { response } = await adminApi.users.createOne(state.newUserData as UserIn);
|
||||
|
||||
if (response?.status === 201) {
|
||||
router.push("/admin/manage/users");
|
||||
|
|
|
@ -3,32 +3,52 @@
|
|||
<UserInviteDialog v-model="inviteDialog" />
|
||||
<BaseDialog
|
||||
v-model="deleteDialog"
|
||||
:title="$tc('general.confirm')"
|
||||
:title="$t('general.confirm')"
|
||||
color="error"
|
||||
can-confirm
|
||||
@confirm="deleteUser(deleteTargetId)"
|
||||
>
|
||||
<template #activator> </template>
|
||||
<template #activator />
|
||||
|
||||
<v-card-text>
|
||||
<v-alert v-if="isUserOwnAccount" type="warning" text outlined>
|
||||
{{ $t("general.confirm-delete-own-admin-account") }}
|
||||
</v-alert>
|
||||
<v-alert
|
||||
v-if="isUserOwnAccount"
|
||||
type="warning"
|
||||
:text="$t('general.confirm-delete-own-admin-account')"
|
||||
variant="outlined"
|
||||
/>
|
||||
{{ $t("general.confirm-delete-generic") }}
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
|
||||
<BaseCardSectionTitle :title="$tc('user.user-management')"> </BaseCardSectionTitle>
|
||||
<BaseCardSectionTitle :title="$t('user.user-management')" />
|
||||
<section>
|
||||
<v-toolbar color="transparent" flat class="justify-between">
|
||||
<BaseButton to="/admin/manage/users/create" class="mr-2">
|
||||
<v-toolbar
|
||||
color="transparent"
|
||||
flat
|
||||
class="justify-between"
|
||||
>
|
||||
<BaseButton
|
||||
to="/admin/manage/users/create"
|
||||
class="mr-2"
|
||||
>
|
||||
{{ $t("general.create") }}
|
||||
</BaseButton>
|
||||
<BaseButton class="mr-2" color="info" :icon="$globals.icons.link" @click="inviteDialog = true">
|
||||
<BaseButton
|
||||
class="mr-2"
|
||||
color="info"
|
||||
:icon="$globals.icons.link"
|
||||
@click="inviteDialog = true"
|
||||
>
|
||||
{{ $t("group.invite") }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseOverflowButton mode="event" :items="ACTIONS_OPTIONS" @unlock-all-users="unlockAllUsers">
|
||||
</BaseOverflowButton>
|
||||
<BaseOverflowButton
|
||||
mode="event"
|
||||
variant="elevated"
|
||||
:items="ACTIONS_OPTIONS"
|
||||
@unlock-all-users="unlockAllUsers"
|
||||
/>
|
||||
</v-toolbar>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
|
@ -39,18 +59,22 @@
|
|||
hide-default-footer
|
||||
disable-pagination
|
||||
:search="search"
|
||||
@click:row="handleRowClick"
|
||||
@click:row="($event, { item }) => handleRowClick(item)"
|
||||
>
|
||||
<template #item.admin="{ item }">
|
||||
<v-icon right :color="item.admin ? 'success' : null">
|
||||
<template #[`item.admin`]="{ item }">
|
||||
<v-icon
|
||||
end
|
||||
:color="item.admin ? 'success' : undefined"
|
||||
>
|
||||
{{ item.admin ? $globals.icons.checkboxMarkedCircle : $globals.icons.windowClose }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<template #item.actions="{ item }">
|
||||
<template #[`item.actions`]="{ item }">
|
||||
<v-btn
|
||||
icon
|
||||
:disabled="item.id == 1"
|
||||
:disabled="+item.id == 1"
|
||||
color="error"
|
||||
variant="text"
|
||||
@click.stop="
|
||||
deleteDialog = true;
|
||||
deleteTargetId = item.id;
|
||||
|
@ -62,33 +86,36 @@
|
|||
</v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
<v-divider></v-divider>
|
||||
<v-divider />
|
||||
</section>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, ref, toRefs, useContext, useRouter, computed } from "@nuxtjs/composition-api";
|
||||
import { useAdminApi } from "~/composables/api";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import { useUser, useAllUsers } from "~/composables/use-user";
|
||||
import { UserOut } from "~/lib/api/types/user";
|
||||
import type { UserOut } from "~/lib/api/types/user";
|
||||
import UserInviteDialog from "~/components/Domain/User/UserInviteDialog.vue";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
UserInviteDialog,
|
||||
},
|
||||
layout: "admin",
|
||||
setup() {
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const api = useAdminApi();
|
||||
const refUserDialog = ref();
|
||||
const inviteDialog = ref();
|
||||
const { $auth } = useContext();
|
||||
const $auth = useMealieAuth();
|
||||
|
||||
const user = computed(() => $auth.user);
|
||||
const user = computed(() => $auth.user.value);
|
||||
|
||||
const { $globals, i18n } = useContext();
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
@ -120,7 +147,7 @@ export default defineComponent({
|
|||
deleteUserMixin(id);
|
||||
|
||||
if (isUserOwnAccount.value) {
|
||||
$auth.logout();
|
||||
$auth.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,18 +160,18 @@ export default defineComponent({
|
|||
|
||||
const headers = [
|
||||
{
|
||||
text: i18n.t("user.user-id"),
|
||||
title: i18n.t("user.user-id"),
|
||||
align: "start",
|
||||
value: "id",
|
||||
},
|
||||
{ text: i18n.t("user.username"), value: "username" },
|
||||
{ text: i18n.t("user.full-name"), value: "fullName" },
|
||||
{ text: i18n.t("user.email"), value: "email" },
|
||||
{ text: i18n.t("group.group"), value: "group" },
|
||||
{ text: i18n.t("household.household"), value: "household" },
|
||||
{ text: i18n.t("user.auth-method"), value: "authMethod" },
|
||||
{ text: i18n.t("user.admin"), value: "admin" },
|
||||
{ text: i18n.t("general.delete"), value: "actions", sortable: false, align: "center" },
|
||||
{ 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> {
|
||||
|
@ -158,6 +185,10 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
|
||||
useSeoMeta({
|
||||
title: i18n.t("sidebar.manage-users"),
|
||||
});
|
||||
|
||||
return {
|
||||
isUserOwnAccount,
|
||||
unlockAllUsers,
|
||||
|
@ -175,7 +206,7 @@ export default defineComponent({
|
|||
},
|
||||
head() {
|
||||
return {
|
||||
title: this.$t("sidebar.manage-users") as string,
|
||||
title: useI18n().t("sidebar.manage-users"),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,41 +1,39 @@
|
|||
<template>
|
||||
<v-container
|
||||
fill-height
|
||||
fluid
|
||||
class="d-flex justify-center align-center"
|
||||
width="1200px"
|
||||
min-height="700px"
|
||||
:class="{
|
||||
'bg-off-white': !$vuetify.theme.dark,
|
||||
}"
|
||||
<v-container fill-height
|
||||
fluid
|
||||
class="d-flex justify-center align-center"
|
||||
width="1200px"
|
||||
min-height="700px"
|
||||
:class="{
|
||||
'bg-off-white': !$vuetify.theme.current.dark,
|
||||
}"
|
||||
>
|
||||
<BaseWizard
|
||||
v-model="currentPage"
|
||||
:max-page-number="totalPages"
|
||||
:title="$i18n.tc('admin.setup.first-time-setup')"
|
||||
:prev-button-show="activeConfig.showPrevButton"
|
||||
:next-button-show="activeConfig.showNextButton"
|
||||
:next-button-text="activeConfig.nextButtonText"
|
||||
:next-button-icon="activeConfig.nextButtonIcon"
|
||||
:next-button-color="activeConfig.nextButtonColor"
|
||||
:next-button-is-submit="activeConfig.isSubmit"
|
||||
:is-submitting="isSubmitting"
|
||||
@submit="handleSubmit"
|
||||
<BaseWizard v-model="currentPage"
|
||||
:max-page-number="totalPages"
|
||||
:title="$t('admin.setup.first-time-setup')"
|
||||
:prev-button-show="activeConfig.showPrevButton"
|
||||
:next-button-show="activeConfig.showNextButton"
|
||||
:next-button-text="activeConfig.nextButtonText"
|
||||
:next-button-icon="activeConfig.nextButtonIcon"
|
||||
:next-button-color="activeConfig.nextButtonColor"
|
||||
:next-button-is-submit="activeConfig.isSubmit"
|
||||
:is-submitting="isSubmitting"
|
||||
@submit="handleSubmit"
|
||||
>
|
||||
<v-container v-if="currentPage === Pages.LANDING" class="mb-12">
|
||||
<v-card-title class="text-h4 justify-center">
|
||||
{{ $i18n.tc('admin.setup.welcome-to-mealie-get-started') }}
|
||||
<v-container v-if="currentPage === Pages.LANDING"
|
||||
class="mb-12"
|
||||
>
|
||||
<v-card-title class="text-h4 justify-center text-center">
|
||||
{{ $t('admin.setup.welcome-to-mealie-get-started') }}
|
||||
</v-card-title>
|
||||
<v-btn
|
||||
:to="groupSlug ? `/g/${groupSlug}` : '/login'"
|
||||
rounded
|
||||
outlined
|
||||
text
|
||||
color="grey lighten-1"
|
||||
class="text-subtitle-2 d-flex mx-auto"
|
||||
style="width: fit-content;"
|
||||
<v-btn :to="groupSlug ? `/g/${groupSlug}` : '/login'"
|
||||
rounded
|
||||
variant="outlined"
|
||||
color="grey-lighten-1"
|
||||
class="text-subtitle-2 d-flex mx-auto"
|
||||
style="width: fit-content;"
|
||||
>
|
||||
{{ $i18n.tc('admin.setup.already-set-up-bring-to-homepage') }}
|
||||
{{ $t('admin.setup.already-set-up-bring-to-homepage') }}
|
||||
</v-btn>
|
||||
</v-container>
|
||||
<v-container v-if="currentPage === Pages.USER_INFO">
|
||||
|
@ -43,9 +41,11 @@
|
|||
</v-container>
|
||||
<v-container v-if="currentPage === Pages.PAGE_2">
|
||||
<v-card-title class="headline justify-center">
|
||||
{{ $i18n.tc('admin.setup.common-settings-for-new-sites') }}
|
||||
{{ $t('admin.setup.common-settings-for-new-sites') }}
|
||||
</v-card-title>
|
||||
<AutoForm v-model="commonSettings" :items="commonSettingsForm" />
|
||||
<AutoForm v-model="commonSettings"
|
||||
:items="commonSettingsForm"
|
||||
/>
|
||||
</v-container>
|
||||
<v-container v-if="currentPage === Pages.CONFIRM">
|
||||
<v-card-title class="headline justify-center">
|
||||
|
@ -53,33 +53,37 @@
|
|||
</v-card-title>
|
||||
<v-list>
|
||||
<template v-for="(item, idx) in confirmationData">
|
||||
<v-list-item v-if="item.display" :key="idx">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title> {{ item.text }} </v-list-item-title>
|
||||
<v-list-item-subtitle> {{ item.value }} </v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
<v-list-item v-if="item.display"
|
||||
:key="idx"
|
||||
>
|
||||
<v-list-item-title> {{ item.text }} </v-list-item-title>
|
||||
<v-list-item-subtitle> {{ item.value }} </v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-divider v-if="idx !== confirmationData.length - 1" :key="`divider-${idx}`" />
|
||||
<v-divider v-if="idx !== confirmationData.length - 1"
|
||||
:key="`divider-${idx}`"
|
||||
/>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-container>
|
||||
<v-container v-if="currentPage === Pages.END">
|
||||
<v-card-title class="text-h4 justify-center">
|
||||
{{ $i18n.tc('admin.setup.setup-complete') }}
|
||||
{{ $t('admin.setup.setup-complete') }}
|
||||
</v-card-title>
|
||||
<v-card-title class="text-h6 justify-center">
|
||||
{{ $i18n.tc('admin.setup.here-are-a-few-things-to-help-you-get-started') }}
|
||||
{{ $t('admin.setup.here-are-a-few-things-to-help-you-get-started') }}
|
||||
</v-card-title>
|
||||
<div v-for="link, idx in setupCompleteLinks" :key="idx" class="px-4 pt-4">
|
||||
<div v-for="link, idx in setupCompleteLinks"
|
||||
:key="idx"
|
||||
class="px-4 pt-4"
|
||||
>
|
||||
<div v-if="link.section">
|
||||
<v-divider v-if="idx" />
|
||||
<v-card-text class="headline pl-0">
|
||||
{{ link.section }}
|
||||
</v-card-text>
|
||||
</div>
|
||||
<v-btn
|
||||
:to="link.to"
|
||||
color="info"
|
||||
<v-btn :to="link.to"
|
||||
color="info"
|
||||
>
|
||||
{{ link.text }}
|
||||
</v-btn>
|
||||
|
@ -93,7 +97,6 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref, useContext, useRouter } from "@nuxtjs/composition-api";
|
||||
import { useAdminApi, useUserApi } from "~/composables/api";
|
||||
import { useLocales } from "~/composables/use-locales";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
|
@ -101,358 +104,363 @@ import { useUserRegistrationForm } from "~/composables/use-users/user-registrati
|
|||
import { useCommonSettingsForm } from "~/composables/use-setup/common-settings-form";
|
||||
import UserRegistrationForm from "~/components/Domain/User/UserRegistrationForm.vue";
|
||||
|
||||
export default defineComponent({
|
||||
export default defineNuxtComponent({
|
||||
components: { UserRegistrationForm },
|
||||
layout: "blank",
|
||||
setup() {
|
||||
definePageMeta({
|
||||
layout: "blank",
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// Setup
|
||||
const { $auth, $globals, i18n } = useContext();
|
||||
const i18n = useI18n();
|
||||
const $auth = useMealieAuth();
|
||||
const { $globals } = useNuxtApp();
|
||||
const userApi = useUserApi();
|
||||
const adminApi = useAdminApi();
|
||||
|
||||
const groupSlug = computed(() => $auth.user?.groupSlug);
|
||||
const groupSlug = computed(() => $auth.user.value?.groupSlug);
|
||||
const { locale } = useLocales();
|
||||
const router = useRouter();
|
||||
const isSubmitting = ref(false);
|
||||
useSeoMeta({
|
||||
title: i18n.t("admin.setup.first-time-setup"),
|
||||
});
|
||||
|
||||
if (!$auth.loggedIn) {
|
||||
if (!$auth.loggedIn.value) {
|
||||
router.push("/login");
|
||||
} else if (!$auth.user?.admin) {
|
||||
}
|
||||
else if (!$auth.user.value?.admin) {
|
||||
router.push(groupSlug.value ? `/g/${groupSlug.value}` : "/login");
|
||||
}
|
||||
|
||||
type Config = {
|
||||
nextButtonText: string | undefined;
|
||||
nextButtonIcon: string | undefined;
|
||||
nextButtonColor: string | undefined;
|
||||
showPrevButton: boolean;
|
||||
showNextButton: boolean;
|
||||
isSubmit: boolean;
|
||||
}
|
||||
type Config = {
|
||||
nextButtonText: string | undefined;
|
||||
nextButtonIcon: string | undefined;
|
||||
nextButtonColor: string | undefined;
|
||||
showPrevButton: boolean;
|
||||
showNextButton: boolean;
|
||||
isSubmit: boolean;
|
||||
};
|
||||
|
||||
const totalPages = 4;
|
||||
enum Pages {
|
||||
LANDING = 0,
|
||||
USER_INFO = 1,
|
||||
PAGE_2 = 2,
|
||||
CONFIRM = 3,
|
||||
END = 4,
|
||||
}
|
||||
const totalPages = 4;
|
||||
enum Pages {
|
||||
LANDING = 0,
|
||||
USER_INFO = 1,
|
||||
PAGE_2 = 2,
|
||||
CONFIRM = 3,
|
||||
END = 4,
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Forms
|
||||
const { accountDetails, credentials } = useUserRegistrationForm();
|
||||
const { commonSettingsForm } = useCommonSettingsForm();
|
||||
const commonSettings = ref({
|
||||
makeGroupRecipesPublic: false,
|
||||
useSeedData: true,
|
||||
})
|
||||
// ================================================================
|
||||
// Forms
|
||||
const { accountDetails, credentials } = useUserRegistrationForm();
|
||||
const { commonSettingsForm } = useCommonSettingsForm();
|
||||
const commonSettings = ref({
|
||||
makeGroupRecipesPublic: false,
|
||||
useSeedData: true,
|
||||
});
|
||||
|
||||
const confirmationData = computed(() => {
|
||||
return [
|
||||
{
|
||||
display: true,
|
||||
text: i18n.tc("user.email"),
|
||||
value: accountDetails.email.value,
|
||||
},
|
||||
{
|
||||
display: true,
|
||||
text: i18n.tc("user.username"),
|
||||
value: accountDetails.username.value,
|
||||
},
|
||||
{
|
||||
display: true,
|
||||
text: i18n.tc("user.full-name"),
|
||||
value: accountDetails.fullName.value,
|
||||
},
|
||||
{
|
||||
display: true,
|
||||
text: i18n.tc("user.enable-advanced-content"),
|
||||
value: accountDetails.advancedOptions.value ? i18n.tc("general.yes") : i18n.tc("general.no"),
|
||||
},
|
||||
{
|
||||
display: true,
|
||||
text: i18n.tc("group.enable-public-access"),
|
||||
value: commonSettings.value.makeGroupRecipesPublic ? i18n.tc("general.yes") : i18n.tc("general.no"),
|
||||
},
|
||||
{
|
||||
display: true,
|
||||
text: i18n.tc("user-registration.use-seed-data"),
|
||||
value: commonSettings.value.useSeedData ? i18n.tc("general.yes") : i18n.tc("general.no"),
|
||||
},
|
||||
];
|
||||
});
|
||||
const confirmationData = computed(() => {
|
||||
return [
|
||||
{
|
||||
display: true,
|
||||
text: i18n.t("user.email"),
|
||||
value: accountDetails.email.value,
|
||||
},
|
||||
{
|
||||
display: true,
|
||||
text: i18n.t("user.username"),
|
||||
value: accountDetails.username.value,
|
||||
},
|
||||
{
|
||||
display: true,
|
||||
text: i18n.t("user.full-name"),
|
||||
value: accountDetails.fullName.value,
|
||||
},
|
||||
{
|
||||
display: true,
|
||||
text: i18n.t("user.enable-advanced-content"),
|
||||
value: accountDetails.advancedOptions.value ? i18n.t("general.yes") : i18n.t("general.no"),
|
||||
},
|
||||
{
|
||||
display: true,
|
||||
text: i18n.t("group.enable-public-access"),
|
||||
value: commonSettings.value.makeGroupRecipesPublic ? i18n.t("general.yes") : i18n.t("general.no"),
|
||||
},
|
||||
{
|
||||
display: true,
|
||||
text: i18n.t("user-registration.use-seed-data"),
|
||||
value: commonSettings.value.useSeedData ? i18n.t("general.yes") : i18n.t("general.no"),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const setupCompleteLinks = ref([
|
||||
{
|
||||
section: i18n.tc("profile.data-migrations"),
|
||||
to: "/admin/backups",
|
||||
text: i18n.tc("settings.backup.backup-restore"),
|
||||
description: i18n.tc("admin.setup.restore-from-v1-backup"),
|
||||
},
|
||||
{
|
||||
to: "/group/migrations",
|
||||
text: i18n.tc("migration.recipe-migration"),
|
||||
description: i18n.tc("migration.coming-from-another-application-or-an-even-older-version-of-mealie"),
|
||||
},
|
||||
{
|
||||
section: i18n.tc("recipe.create-recipes"),
|
||||
to: computed(() => `/g/${groupSlug.value || ""}/r/create/new`),
|
||||
text: i18n.tc("recipe.create-recipe"),
|
||||
description: i18n.tc("recipe.create-recipe-description"),
|
||||
},
|
||||
{
|
||||
to: computed(() => `/g/${groupSlug.value || ""}/r/create/url`),
|
||||
text: i18n.tc("recipe.import-with-url"),
|
||||
description: i18n.tc("recipe.scrape-recipe-description"),
|
||||
},
|
||||
{
|
||||
section: i18n.tc("user.manage-users"),
|
||||
to: "/admin/manage/users",
|
||||
text: i18n.tc("user.manage-users"),
|
||||
description: i18n.tc("user.manage-users-description"),
|
||||
},
|
||||
{
|
||||
to: "/user/profile",
|
||||
text: i18n.tc("profile.manage-user-profile"),
|
||||
description: i18n.tc("admin.setup.manage-profile-or-get-invite-link"),
|
||||
},
|
||||
]);
|
||||
const setupCompleteLinks = ref([
|
||||
{
|
||||
section: i18n.t("profile.data-migrations"),
|
||||
to: "/admin/backups",
|
||||
text: i18n.t("settings.backup.backup-restore"),
|
||||
description: i18n.t("admin.setup.restore-from-v1-backup"),
|
||||
},
|
||||
{
|
||||
to: "/group/migrations",
|
||||
text: i18n.t("migration.recipe-migration"),
|
||||
description: i18n.t("migration.coming-from-another-application-or-an-even-older-version-of-mealie"),
|
||||
},
|
||||
{
|
||||
section: i18n.t("recipe.create-recipes"),
|
||||
to: computed(() => `/g/${groupSlug.value || ""}/r/create/new`),
|
||||
text: i18n.t("recipe.create-recipe"),
|
||||
description: i18n.t("recipe.create-recipe-description"),
|
||||
},
|
||||
{
|
||||
to: computed(() => `/g/${groupSlug.value || ""}/r/create/url`),
|
||||
text: i18n.t("recipe.import-with-url"),
|
||||
description: i18n.t("recipe.scrape-recipe-description"),
|
||||
},
|
||||
{
|
||||
section: i18n.t("user.manage-users"),
|
||||
to: "/admin/manage/users",
|
||||
text: i18n.t("user.manage-users"),
|
||||
description: i18n.t("user.manage-users-description"),
|
||||
},
|
||||
{
|
||||
to: "/user/profile",
|
||||
text: i18n.t("profile.manage-user-profile"),
|
||||
description: i18n.t("admin.setup.manage-profile-or-get-invite-link"),
|
||||
},
|
||||
]);
|
||||
|
||||
// ================================================================
|
||||
// Page Navigation
|
||||
const currentPage = ref(0);
|
||||
const activeConfig = computed<Config>(() => {
|
||||
const config: Config = {
|
||||
nextButtonText: undefined,
|
||||
nextButtonIcon: undefined,
|
||||
nextButtonColor: undefined,
|
||||
showPrevButton: true,
|
||||
showNextButton: true,
|
||||
isSubmit: false,
|
||||
}
|
||||
// ================================================================
|
||||
// Page Navigation
|
||||
const currentPage = ref(0);
|
||||
const activeConfig = computed<Config>(() => {
|
||||
const config: Config = {
|
||||
nextButtonText: undefined,
|
||||
nextButtonIcon: undefined,
|
||||
nextButtonColor: undefined,
|
||||
showPrevButton: true,
|
||||
showNextButton: true,
|
||||
isSubmit: false,
|
||||
};
|
||||
|
||||
switch (currentPage.value) {
|
||||
case Pages.LANDING:
|
||||
config.showPrevButton = false;
|
||||
config.nextButtonText = i18n.tc("general.start");
|
||||
config.nextButtonIcon = $globals.icons.forward;
|
||||
break;
|
||||
case Pages.USER_INFO:
|
||||
config.showPrevButton = false;
|
||||
config.nextButtonText = i18n.tc("general.next");
|
||||
config.nextButtonIcon = $globals.icons.forward;
|
||||
config.isSubmit = true;
|
||||
break;
|
||||
case Pages.CONFIRM:
|
||||
config.isSubmit = true;
|
||||
break;
|
||||
case Pages.END:
|
||||
config.nextButtonText = i18n.tc("general.home");
|
||||
config.nextButtonIcon = $globals.icons.home;
|
||||
config.nextButtonColor = "primary";
|
||||
config.showPrevButton = false;
|
||||
config.isSubmit = true;
|
||||
break;
|
||||
}
|
||||
switch (currentPage.value) {
|
||||
case Pages.LANDING:
|
||||
config.showPrevButton = false;
|
||||
config.nextButtonText = i18n.t("general.start");
|
||||
config.nextButtonIcon = $globals.icons.forward;
|
||||
break;
|
||||
case Pages.USER_INFO:
|
||||
config.showPrevButton = false;
|
||||
config.nextButtonText = i18n.t("general.next");
|
||||
config.nextButtonIcon = $globals.icons.forward;
|
||||
config.isSubmit = true;
|
||||
break;
|
||||
case Pages.CONFIRM:
|
||||
config.isSubmit = true;
|
||||
break;
|
||||
case Pages.END:
|
||||
config.nextButtonText = i18n.t("general.home");
|
||||
config.nextButtonIcon = $globals.icons.home;
|
||||
config.nextButtonColor = "primary";
|
||||
config.showPrevButton = false;
|
||||
config.isSubmit = true;
|
||||
break;
|
||||
}
|
||||
|
||||
return config;
|
||||
})
|
||||
return config;
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// Page Submission
|
||||
// ================================================================
|
||||
// Page Submission
|
||||
|
||||
async function updateUser() {
|
||||
// @ts-ignore-next-line user will never be null here
|
||||
const { response } = await userApi.users.updateOne($auth.user?.id, {
|
||||
...$auth.user,
|
||||
email: accountDetails.email.value,
|
||||
username: accountDetails.username.value,
|
||||
fullName: accountDetails.fullName.value,
|
||||
advancedOptions: accountDetails.advancedOptions.value,
|
||||
})
|
||||
async function updateUser() {
|
||||
// Note: $auth.user is now a ref
|
||||
const { response } = await userApi.users.updateOne($auth.user.value!.id, {
|
||||
...$auth.user.value,
|
||||
email: accountDetails.email.value,
|
||||
username: accountDetails.username.value,
|
||||
fullName: accountDetails.fullName.value,
|
||||
advancedOptions: accountDetails.advancedOptions.value,
|
||||
});
|
||||
|
||||
if (!response || response.status !== 200) {
|
||||
alert.error(i18n.tc("events.something-went-wrong"));
|
||||
} else {
|
||||
$auth.setUser({
|
||||
...$auth.user,
|
||||
email: accountDetails.email.value,
|
||||
username: accountDetails.username.value,
|
||||
fullName: accountDetails.fullName.value,
|
||||
})
|
||||
}
|
||||
}
|
||||
if (!response || response.status !== 200) {
|
||||
alert.error(i18n.t("events.something-went-wrong"));
|
||||
}
|
||||
else {
|
||||
$auth.refresh();
|
||||
/* $auth.setUser({
|
||||
...$auth.user.value,
|
||||
email: accountDetails.email.value,
|
||||
username: accountDetails.username.value,
|
||||
fullName: accountDetails.fullName.value,
|
||||
}) */
|
||||
}
|
||||
}
|
||||
|
||||
async function updatePassword() {
|
||||
const { response } = await userApi.users.changePassword({
|
||||
currentPassword: "MyPassword",
|
||||
newPassword: credentials.password1.value,
|
||||
});
|
||||
async function updatePassword() {
|
||||
const { response } = await userApi.users.changePassword({
|
||||
currentPassword: "MyPassword",
|
||||
newPassword: credentials.password1.value,
|
||||
});
|
||||
|
||||
if (!response || response.status !== 200) {
|
||||
alert.error(i18n.tc("events.something-went-wrong"));
|
||||
}
|
||||
}
|
||||
if (!response || response.status !== 200) {
|
||||
alert.error(i18n.t("events.something-went-wrong"));
|
||||
}
|
||||
}
|
||||
|
||||
async function submitRegistration() {
|
||||
// the backend will only update the password without the "currentPassword" field if the user is the default user,
|
||||
// so we update the password first, then update the user's details
|
||||
await updatePassword().then(updateUser);
|
||||
}
|
||||
async function submitRegistration() {
|
||||
// the backend will only update the password without the "currentPassword" field if the user is the default user,
|
||||
// so we update the password first, then update the user's details
|
||||
await updatePassword().then(updateUser);
|
||||
}
|
||||
|
||||
async function updateGroup() {
|
||||
// @ts-ignore-next-line user will never be null here
|
||||
const { data } = await userApi.groups.getOne($auth.user?.groupId);
|
||||
if (!data || !data.preferences) {
|
||||
alert.error(i18n.tc("events.something-went-wrong"));
|
||||
return;
|
||||
}
|
||||
async function updateGroup() {
|
||||
// Note: $auth.user is now a ref
|
||||
const { data } = await userApi.groups.getOne($auth.user.value!.groupId);
|
||||
if (!data || !data.preferences) {
|
||||
alert.error(i18n.t("events.something-went-wrong"));
|
||||
return;
|
||||
}
|
||||
|
||||
const preferences = {
|
||||
...data.preferences,
|
||||
privateGroup: !commonSettings.value.makeGroupRecipesPublic,
|
||||
}
|
||||
const preferences = {
|
||||
...data.preferences,
|
||||
privateGroup: !commonSettings.value.makeGroupRecipesPublic,
|
||||
};
|
||||
|
||||
const payload = {
|
||||
...data,
|
||||
preferences,
|
||||
}
|
||||
const payload = {
|
||||
...data,
|
||||
preferences,
|
||||
};
|
||||
|
||||
// @ts-ignore-next-line user will never be null here
|
||||
const { response } = await userApi.groups.updateOne($auth.user?.groupId, payload);
|
||||
if (!response || response.status !== 200) {
|
||||
alert.error(i18n.tc("events.something-went-wrong"));
|
||||
}
|
||||
}
|
||||
// Note: $auth.user is now a ref
|
||||
const { response } = await userApi.groups.updateOne($auth.user.value!.groupId, payload);
|
||||
if (!response || response.status !== 200) {
|
||||
alert.error(i18n.t("events.something-went-wrong"));
|
||||
}
|
||||
}
|
||||
|
||||
async function updateHousehold() {
|
||||
// @ts-ignore-next-line user will never be null here
|
||||
const { data } = await adminApi.households.getOne($auth.user?.householdId);
|
||||
if (!data || !data.preferences) {
|
||||
alert.error(i18n.tc("events.something-went-wrong"));
|
||||
return;
|
||||
}
|
||||
async function updateHousehold() {
|
||||
// Note: $auth.user is now a ref
|
||||
const { data } = await adminApi.households.getOne($auth.user.value!.householdId);
|
||||
if (!data || !data.preferences) {
|
||||
alert.error(i18n.t("events.something-went-wrong"));
|
||||
return;
|
||||
}
|
||||
|
||||
const preferences = {
|
||||
...data.preferences,
|
||||
privateHousehold: !commonSettings.value.makeGroupRecipesPublic,
|
||||
recipePublic: commonSettings.value.makeGroupRecipesPublic,
|
||||
}
|
||||
const preferences = {
|
||||
...data.preferences,
|
||||
privateHousehold: !commonSettings.value.makeGroupRecipesPublic,
|
||||
recipePublic: commonSettings.value.makeGroupRecipesPublic,
|
||||
};
|
||||
|
||||
const payload = {
|
||||
...data,
|
||||
preferences,
|
||||
}
|
||||
const payload = {
|
||||
...data,
|
||||
preferences,
|
||||
};
|
||||
|
||||
// @ts-ignore-next-line user will never be null here
|
||||
const { response } = await adminApi.households.updateOne($auth.user?.householdId, payload);
|
||||
if (!response || response.status !== 200) {
|
||||
alert.error(i18n.tc("events.something-went-wrong"));
|
||||
}
|
||||
}
|
||||
// Note: $auth.user is now a ref
|
||||
const { response } = await adminApi.households.updateOne($auth.user.value!.householdId, payload);
|
||||
if (!response || response.status !== 200) {
|
||||
alert.error(i18n.t("events.something-went-wrong"));
|
||||
}
|
||||
}
|
||||
|
||||
async function seedFoods() {
|
||||
const { response } = await userApi.seeders.foods({ locale: locale.value })
|
||||
if (!response || response.status !== 200) {
|
||||
alert.error(i18n.tc("events.something-went-wrong"));
|
||||
}
|
||||
}
|
||||
async function seedFoods() {
|
||||
const { response } = await userApi.seeders.foods({ locale: locale.value });
|
||||
if (!response || response.status !== 200) {
|
||||
alert.error(i18n.t("events.something-went-wrong"));
|
||||
}
|
||||
}
|
||||
|
||||
async function seedUnits() {
|
||||
const { response } = await userApi.seeders.units({ locale: locale.value })
|
||||
if (!response || response.status !== 200) {
|
||||
alert.error(i18n.tc("events.something-went-wrong"));
|
||||
}
|
||||
}
|
||||
async function seedUnits() {
|
||||
const { response } = await userApi.seeders.units({ locale: locale.value });
|
||||
if (!response || response.status !== 200) {
|
||||
alert.error(i18n.t("events.something-went-wrong"));
|
||||
}
|
||||
}
|
||||
|
||||
async function seedLabels() {
|
||||
const { response } = await userApi.seeders.labels({ locale: locale.value })
|
||||
if (!response || response.status !== 200) {
|
||||
alert.error(i18n.tc("events.something-went-wrong"));
|
||||
}
|
||||
}
|
||||
async function seedLabels() {
|
||||
const { response } = await userApi.seeders.labels({ locale: locale.value });
|
||||
if (!response || response.status !== 200) {
|
||||
alert.error(i18n.t("events.something-went-wrong"));
|
||||
}
|
||||
}
|
||||
|
||||
async function seedData() {
|
||||
if (!commonSettings.value.useSeedData) {
|
||||
return;
|
||||
}
|
||||
async function seedData() {
|
||||
if (!commonSettings.value.useSeedData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tasks = [
|
||||
seedFoods(),
|
||||
seedUnits(),
|
||||
seedLabels(),
|
||||
]
|
||||
const tasks = [
|
||||
seedFoods(),
|
||||
seedUnits(),
|
||||
seedLabels(),
|
||||
];
|
||||
|
||||
await Promise.all(tasks);
|
||||
}
|
||||
await Promise.all(tasks);
|
||||
}
|
||||
|
||||
async function submitCommonSettings() {
|
||||
const tasks = [
|
||||
updateGroup(),
|
||||
updateHousehold(),
|
||||
seedData(),
|
||||
]
|
||||
async function submitCommonSettings() {
|
||||
const tasks = [
|
||||
updateGroup(),
|
||||
updateHousehold(),
|
||||
seedData(),
|
||||
];
|
||||
|
||||
await Promise.all(tasks);
|
||||
}
|
||||
await Promise.all(tasks);
|
||||
}
|
||||
|
||||
async function submitAll() {
|
||||
const tasks = [
|
||||
submitRegistration(),
|
||||
submitCommonSettings(),
|
||||
]
|
||||
async function submitAll() {
|
||||
const tasks = [
|
||||
submitRegistration(),
|
||||
submitCommonSettings(),
|
||||
];
|
||||
|
||||
await Promise.all(tasks);
|
||||
}
|
||||
await Promise.all(tasks);
|
||||
}
|
||||
|
||||
async function handleSubmit(page: number) {
|
||||
if (isSubmitting.value) {
|
||||
return;
|
||||
}
|
||||
async function handleSubmit(page: number) {
|
||||
if (isSubmitting.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
isSubmitting.value = true;
|
||||
switch (page) {
|
||||
case Pages.USER_INFO:
|
||||
if (await accountDetails.validate()) {
|
||||
currentPage.value += 1;
|
||||
}
|
||||
break;
|
||||
case Pages.CONFIRM:
|
||||
await submitAll();
|
||||
currentPage.value += 1;
|
||||
break;
|
||||
case Pages.END:
|
||||
router.push(groupSlug.value ? `/g/${groupSlug.value}` : "/login");
|
||||
break;
|
||||
}
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
isSubmitting.value = true;
|
||||
switch (page) {
|
||||
case Pages.USER_INFO:
|
||||
if (await accountDetails.validate()) {
|
||||
currentPage.value += 1;
|
||||
}
|
||||
break;
|
||||
case Pages.CONFIRM:
|
||||
await submitAll();
|
||||
currentPage.value += 1;
|
||||
break;
|
||||
case Pages.END:
|
||||
router.push(groupSlug.value ? `/g/${groupSlug.value}` : "/login");
|
||||
break;
|
||||
}
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
// Setup
|
||||
groupSlug,
|
||||
// Forms
|
||||
commonSettingsForm,
|
||||
commonSettings,
|
||||
confirmationData,
|
||||
setupCompleteLinks,
|
||||
// Page Navigation
|
||||
Pages,
|
||||
currentPage,
|
||||
totalPages,
|
||||
activeConfig,
|
||||
// Page Submission
|
||||
isSubmitting,
|
||||
handleSubmit,
|
||||
}
|
||||
return {
|
||||
// Setup
|
||||
groupSlug,
|
||||
// Forms
|
||||
commonSettingsForm,
|
||||
commonSettings,
|
||||
confirmationData,
|
||||
setupCompleteLinks,
|
||||
// Page Navigation
|
||||
Pages,
|
||||
currentPage,
|
||||
totalPages,
|
||||
activeConfig,
|
||||
// Page Submission
|
||||
isSubmitting,
|
||||
handleSubmit,
|
||||
};
|
||||
},
|
||||
|
||||
head() {
|
||||
return {
|
||||
title: this.$i18n.tc("admin.setup.first-time-setup"),
|
||||
};
|
||||
},
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,26 +1,60 @@
|
|||
<template>
|
||||
<v-container fluid class="narrow-container">
|
||||
<v-container
|
||||
fluid
|
||||
class="narrow-container"
|
||||
>
|
||||
<!-- Image -->
|
||||
<BasePageTitle divider>
|
||||
<template #header>
|
||||
<v-img max-height="200" max-width="150" :src="require('~/static/svgs/admin-site-settings.svg')"></v-img>
|
||||
<v-img
|
||||
width="100%"
|
||||
max-height="200"
|
||||
max-width="150"
|
||||
:src="require('~/static/svgs/admin-site-settings.svg')"
|
||||
/>
|
||||
</template>
|
||||
<template #title>
|
||||
{{ $t("settings.site-settings") }}
|
||||
</template>
|
||||
<template #title> {{ $t("settings.site-settings") }} </template>
|
||||
</BasePageTitle>
|
||||
|
||||
<!-- Bug Report -->
|
||||
<BaseDialog v-model="bugReportDialog" :title="$t('settings.bug-report')" :width="800" :icon="$globals.icons.github">
|
||||
<BaseDialog
|
||||
v-model="bugReportDialog"
|
||||
:title="$t('settings.bug-report')"
|
||||
:width="800"
|
||||
:icon="$globals.icons.github"
|
||||
>
|
||||
<v-card-text>
|
||||
<div class="pb-4">
|
||||
{{ $t('settings.bug-report-information') }}
|
||||
</div>
|
||||
<v-textarea v-model="bugReportText" outlined rows="18" readonly> </v-textarea>
|
||||
<div class="d-flex justify-end" style="gap: 5px">
|
||||
<BaseButton color="gray" secondary target="_blank" href="https://github.com/hay-kot/mealie/issues/new/choose">
|
||||
<template #icon> {{ $globals.icons.github }}</template>
|
||||
<v-textarea
|
||||
v-model="bugReportText"
|
||||
variant="outlined"
|
||||
rows="18"
|
||||
readonly
|
||||
/>
|
||||
<div
|
||||
class="d-flex justify-end"
|
||||
style="gap: 5px"
|
||||
>
|
||||
<BaseButton
|
||||
color="gray"
|
||||
secondary
|
||||
target="_blank"
|
||||
href="https://github.com/hay-kot/mealie/issues/new/choose"
|
||||
>
|
||||
<template #icon>
|
||||
{{ $globals.icons.github }}
|
||||
</template>
|
||||
{{ $t('settings.tracker') }}
|
||||
</BaseButton>
|
||||
<AppButtonCopy :copy-text="bugReportText" color="info" :icon="false" />
|
||||
<AppButtonCopy
|
||||
:copy-text="bugReportText"
|
||||
color="info"
|
||||
:icon="false"
|
||||
/>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
|
@ -32,60 +66,88 @@
|
|||
bugReportDialog = true;
|
||||
"
|
||||
>
|
||||
<template #icon> {{ $globals.icons.github }}</template>
|
||||
<template #icon>
|
||||
{{ $globals.icons.github }}
|
||||
</template>
|
||||
{{ $t('settings.bug-report') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<!-- Configuration -->
|
||||
<section>
|
||||
<BaseCardSectionTitle class="pb-0" :icon="$globals.icons.cog" :title="$tc('settings.configuration')"> </BaseCardSectionTitle>
|
||||
<BaseCardSectionTitle
|
||||
class="pb-0"
|
||||
:icon="$globals.icons.cog"
|
||||
:title="$t('settings.configuration')"
|
||||
/>
|
||||
<v-card class="mb-4">
|
||||
<template v-for="(check, idx) in simpleChecks">
|
||||
<v-list-item :key="`list-item-${idx}`">
|
||||
<v-list-item-icon>
|
||||
<v-icon :color="check.color">
|
||||
<template
|
||||
v-for="(check, idx) in simpleChecks"
|
||||
:key="`list-item-${idx}`"
|
||||
>
|
||||
<v-list-item :title="check.text">
|
||||
<template #prepend>
|
||||
<v-icon :color="check.color" class="opacity-100">
|
||||
{{ check.icon }}
|
||||
</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ check.text }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="wrap-word">
|
||||
{{ check.status ? check.successText : check.errorText }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</template>
|
||||
<v-list-item-subtitle class="wrap-word">
|
||||
{{ check.status ? check.successText : check.errorText }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-divider :key="`divider-${idx}`"></v-divider>
|
||||
<v-divider />
|
||||
</template>
|
||||
</v-card>
|
||||
</section>
|
||||
|
||||
<!-- Email -->
|
||||
<section>
|
||||
<BaseCardSectionTitle class="pt-2" :icon="$globals.icons.email" :title="$tc('user.email')" />
|
||||
<v-alert border="left" colored-border :type="appConfig.emailReady ? 'success' : 'error'" elevation="2">
|
||||
<div class="font-weight-medium">{{ $t('settings.email-configuration-status') }}</div>
|
||||
<div>
|
||||
{{ appConfig.emailReady ? $t('settings.ready') : $t('settings.not-ready') }}
|
||||
<BaseCardSectionTitle
|
||||
class="pt-2"
|
||||
:icon="$globals.icons.email"
|
||||
:title="$t('user.email')"
|
||||
/>
|
||||
<v-alert
|
||||
border="start"
|
||||
:border-color="appConfig.emailReady ? 'success' : 'error'"
|
||||
variant="text"
|
||||
elevation="2"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon :color="appConfig.emailReady ? 'success' : 'warning'">
|
||||
{{ appConfig.emailReady ? $globals.icons.checkboxMarkedCircle : $globals.icons.alertCircle }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<div class="font-weight-medium">
|
||||
{{ $t('settings.email-configuration-status') }}
|
||||
</div>
|
||||
<div>
|
||||
<v-text-field v-model="address" class="mr-4" :label="$t('user.email')" :rules="[validators.email]">
|
||||
</v-text-field>
|
||||
{{ appConfig.emailReady ? $t('settings.ready') : $t('settings.not-ready') }}
|
||||
</div>
|
||||
<div>
|
||||
<v-text-field
|
||||
v-model="address"
|
||||
class="mr-4"
|
||||
:label="$t('user.email')"
|
||||
:rules="[validators.email]"
|
||||
/>
|
||||
<BaseButton
|
||||
color="info"
|
||||
variant="elevated"
|
||||
:disabled="!appConfig.emailReady || !validEmail"
|
||||
:loading="loading"
|
||||
class="opacity-100"
|
||||
@click="testEmail"
|
||||
>
|
||||
<template #icon> {{ $globals.icons.email }} </template>
|
||||
<template #icon>
|
||||
{{ $globals.icons.email }}
|
||||
</template>
|
||||
{{ $t("general.test") }}
|
||||
</BaseButton>
|
||||
<template v-if="tested">
|
||||
<v-divider class="my-x mt-6"></v-divider>
|
||||
<v-divider class="my-x mt-6" />
|
||||
<v-card-text class="px-0">
|
||||
<h4> {{ $tc("settings.email-test-results") }}</h4>
|
||||
<h4> {{ $t("settings.email-test-results") }}</h4>
|
||||
<span class="pl-4">
|
||||
{{ success ? $t('settings.succeeded') : $t('settings.failed') }}
|
||||
</span>
|
||||
|
@ -97,48 +159,56 @@
|
|||
|
||||
<!-- General App Info -->
|
||||
<section class="mt-4">
|
||||
<BaseCardSectionTitle class="pb-0" :icon="$globals.icons.cog" :title="$tc('settings.general-about')"> </BaseCardSectionTitle>
|
||||
<BaseCardSectionTitle
|
||||
class="pb-0"
|
||||
:icon="$globals.icons.cog"
|
||||
:title="$t('settings.general-about')"
|
||||
/>
|
||||
<v-card class="mb-4">
|
||||
<template v-if="appInfo && appInfo.length">
|
||||
<template v-for="(property, idx) in appInfo">
|
||||
<v-list-item :key="property.name">
|
||||
<v-list-item-icon>
|
||||
<v-icon> {{ property.icon || $globals.icons.user }} </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
<div>{{ property.name }}</div>
|
||||
</v-list-item-title>
|
||||
<template v-if="property.slot === 'recipe-scraper'">
|
||||
<v-list-item-subtitle>
|
||||
<a
|
||||
target="_blank"
|
||||
:href="`https://github.com/hhursev/recipe-scrapers/releases/tag/${property.value}`"
|
||||
>
|
||||
{{ property.value }}
|
||||
</a>
|
||||
</v-list-item-subtitle>
|
||||
</template>
|
||||
<template v-else-if="property.slot === 'build'">
|
||||
<v-list-item-subtitle>
|
||||
<a target="_blank" :href="`https://github.com/mealie-recipes/mealie/commit/${property.value}`">
|
||||
{{ property.value }}
|
||||
</a>
|
||||
</v-list-item-subtitle>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-list-item-subtitle>
|
||||
<template
|
||||
v-for="(property, idx) in appInfo"
|
||||
:key="property.name"
|
||||
>
|
||||
<v-list-item
|
||||
:title="property.name"
|
||||
:prepend-icon="property.icon || $globals.icons.user"
|
||||
>
|
||||
<template v-if="property.slot === 'recipe-scraper'">
|
||||
<v-list-item-subtitle>
|
||||
<a
|
||||
target="_blank"
|
||||
:href="`https://github.com/hhursev/recipe-scrapers/releases/tag/${property.value}`"
|
||||
>
|
||||
{{ property.value }}
|
||||
</v-list-item-subtitle>
|
||||
</template>
|
||||
</v-list-item-content>
|
||||
</a>
|
||||
</v-list-item-subtitle>
|
||||
</template>
|
||||
<template v-else-if="property.slot === 'build'">
|
||||
<v-list-item-subtitle>
|
||||
<a
|
||||
target="_blank"
|
||||
:href="`https://github.com/mealie-recipes/mealie/commit/${property.value}`"
|
||||
>
|
||||
{{ property.value }}
|
||||
</a>
|
||||
</v-list-item-subtitle>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-list-item-subtitle>
|
||||
{{ property.value }}
|
||||
</v-list-item-subtitle>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-divider v-if="appInfo && idx !== appInfo.length - 1" :key="`divider-${property.name}`"></v-divider>
|
||||
<v-divider
|
||||
v-if="appInfo && idx !== appInfo.length - 1"
|
||||
:key="`divider-${property.name}`"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="mb-3 text-center">
|
||||
<AppLoader :waiting-text="$tc('general.loading')" />
|
||||
<AppLoader :waiting-text="$t('general.loading')" />
|
||||
</div>
|
||||
</template>
|
||||
</v-card>
|
||||
|
@ -147,21 +217,11 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
computed,
|
||||
onMounted,
|
||||
reactive,
|
||||
toRefs,
|
||||
ref,
|
||||
defineComponent,
|
||||
useAsync,
|
||||
useContext,
|
||||
} from "@nuxtjs/composition-api";
|
||||
import { TranslateResult } from "vue-i18n";
|
||||
import type { TranslateResult } from "vue-i18n";
|
||||
import { useAdminApi, useUserApi } from "~/composables/api";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import { useAsyncKey } from "~/composables/use-utils";
|
||||
import { CheckAppConfig } from "~/lib/api/types/admin";
|
||||
import type { CheckAppConfig } from "~/lib/api/types/admin";
|
||||
import AppLoader from "~/components/global/AppLoader.vue";
|
||||
|
||||
enum DockerVolumeState {
|
||||
|
@ -184,255 +244,262 @@ interface CheckApp extends CheckAppConfig {
|
|||
isSiteSecure?: boolean;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: { AppLoader },
|
||||
layout: "admin",
|
||||
setup() {
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
address: "",
|
||||
success: false,
|
||||
error: "",
|
||||
tested: false,
|
||||
});
|
||||
const appConfig = ref<CheckApp>({
|
||||
emailReady: true,
|
||||
baseUrlSet: true,
|
||||
isSiteSecure: true,
|
||||
isUpToDate: false,
|
||||
ldapReady: false,
|
||||
oidcReady: 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;
|
||||
}
|
||||
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 { $globals, i18n } = useContext();
|
||||
const rawAppInfo = ref({
|
||||
version: "null",
|
||||
versionLatest: "null",
|
||||
});
|
||||
function getAppInfo() {
|
||||
const statistics = useAsync(async () => {
|
||||
const { data } = await adminApi.about.about();
|
||||
if (data) {
|
||||
rawAppInfo.value.version = data.version;
|
||||
rawAppInfo.value.versionLatest = data.versionLatest;
|
||||
const prettyInfo = [
|
||||
{
|
||||
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;
|
||||
}, useAsyncKey());
|
||||
return statistics;
|
||||
}
|
||||
const appInfo = getAppInfo();
|
||||
const bugReportDialog = ref(false);
|
||||
const bugReportText = computed(() => {
|
||||
const ignore = {
|
||||
[i18n.tc("about.database-url")]: true,
|
||||
[i18n.tc("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.tc("general.yes") : i18n.tc("general.no");
|
||||
text += `${item.text.toString()}: ${status}\n`;
|
||||
});
|
||||
text += `${i18n.tc("settings.email-configured")}: ${appConfig.value.emailReady ? i18n.tc("general.yes") : i18n.tc("general.no")}\n`;
|
||||
return text;
|
||||
});
|
||||
return {
|
||||
bugReportDialog,
|
||||
bugReportText,
|
||||
DockerVolumeState,
|
||||
simpleChecks,
|
||||
appConfig,
|
||||
validEmail,
|
||||
validators,
|
||||
...toRefs(state),
|
||||
testEmail,
|
||||
appInfo,
|
||||
};
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: this.$t("settings.site-settings") as string,
|
||||
};
|
||||
export default defineNuxtComponent({
|
||||
components: { AppLoader },
|
||||
setup() {
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const { $globals } = useNuxtApp();
|
||||
const i18n = useI18n();
|
||||
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
address: "",
|
||||
success: false,
|
||||
error: "",
|
||||
tested: false,
|
||||
});
|
||||
|
||||
// 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 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;
|
||||
}
|
||||
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 = [
|
||||
{
|
||||
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;
|
||||
}
|
||||
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,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue