chore: migrate remaining pages to script setup (#7310)

This commit is contained in:
Kuchenpirat
2026-03-24 16:07:08 +01:00
committed by GitHub
parent 27cb585c80
commit 18b3c4beab
57 changed files with 4160 additions and 4971 deletions

View File

@@ -100,6 +100,7 @@ const {
usernameErrorMessages,
validateUsername,
validateEmail,
domAccountForm,
} = useUserRegistrationForm();
</script>

View File

@@ -21,5 +21,6 @@ export default withNuxt({
],
"vue/no-mutating-props": "error",
"vue/no-v-html": "error",
"vue/component-api-style": ["error", ["script-setup"]],
},
});

View File

@@ -48,30 +48,29 @@
</v-app>
</template>
<script lang="ts">
<script setup lang="ts">
import { useGlobalI18n } from "~/composables/use-global-i18n";
export default defineNuxtComponent({
props: {
const props = defineProps({
error: {
type: Object,
default: null,
},
},
setup(props) {
definePageMeta({
});
definePageMeta({
layout: "basic",
});
});
const i18n = useGlobalI18n();
const auth = useMealieAuth();
const { $globals } = useNuxtApp();
const ready = ref(false);
const i18n = useGlobalI18n();
const auth = useMealieAuth();
const { $globals } = useNuxtApp();
const ready = ref(false);
const route = useRoute();
const router = useRouter();
const route = useRoute();
const router = useRouter();
async function insertGroupSlugIntoRoute() {
async function insertGroupSlugIntoRoute() {
const groupSlug = ref(auth.user.value?.groupSlug);
if (!groupSlug.value) {
return;
@@ -99,9 +98,9 @@ export default defineNuxtComponent({
if (replaceRoute) {
await router.replace(routeVal);
}
}
}
async function handle404() {
async function handle404() {
const normalizedRoute = route.fullPath.replace(/\/$/, "");
const newRoute = normalizedRoute.replace(/^\/group\/(mealplan|members|notifiers|webhooks)(\/.*)?$/, "/household/$1$2");
@@ -113,32 +112,25 @@ export default defineNuxtComponent({
}
ready.value = true;
}
}
if (props.error.statusCode === 404) {
if (props.error.statusCode === 404) {
handle404();
}
else {
}
else {
ready.value = true;
}
}
useSeoMeta({
useSeoMeta({
title:
props.error.statusCode === 404
? (i18n.t("page.404-not-found") as string)
: (i18n.t("page.an-error-occurred") as string),
});
const buttons = [
{ icon: $globals.icons.home, to: "/", text: i18n.t("general.home") },
];
return {
buttons,
ready,
};
},
});
const buttons = [
{ icon: $globals.icons.home, to: "/", text: i18n.t("general.home") },
];
</script>
<style scoped>

View File

@@ -1,9 +1,9 @@
<template>
<NuxtPage />
</template>
<script setup lang="ts">
definePageMeta({
middleware: ["admin-only"],
});
</script>
<template>
<NuxtPage />
</template>

View File

@@ -3,7 +3,7 @@
<section>
<!-- Delete Dialog -->
<BaseDialog
v-model="deleteDialog"
v-model="state.deleteDialog"
:title="$t('settings.backup.delete-backup')"
color="error"
:icon="$globals.icons.alertCircle"
@@ -17,7 +17,7 @@
<!-- Import Dialog -->
<BaseDialog
v-model="importDialog"
v-model="state.importDialog"
color="error"
:title="$t('settings.backup.backup-restore')"
:icon="$globals.icons.database"
@@ -40,7 +40,7 @@
</p>
<v-checkbox
v-model="confirmImport"
v-model="state.confirmImport"
class="checkbox-top"
color="error"
hide-details
@@ -50,7 +50,7 @@
<v-card-actions class="justify-center pt-0">
<BaseButton
delete
:disabled="!confirmImport || runningRestore"
:disabled="!state.confirmImport || state.runningRestore"
@click="restoreBackup(selected)"
>
<template #icon>
@@ -63,7 +63,7 @@
{{ selected }}
</p>
<v-progress-linear
v-if="runningRestore"
v-if="state.runningRestore"
indeterminate
/>
</BaseDialog>
@@ -81,7 +81,7 @@
>
<BaseButton
class="mr-2"
:loading="runningBackup"
:loading="state.runningBackup"
@click="createBackup"
>
{{ $t("settings.backup.create-heading") }}
@@ -96,13 +96,13 @@
</v-toolbar>
<v-data-table
:headers="headers"
:headers="state.headers"
:items="backups.imports || []"
class="elevation-0"
:items-per-page="-1"
hide-default-footer
disable-pagination
:search="search"
:search="state.search"
@click:row="setSelected"
>
<template #[`item.date`]="{ item }">
@@ -115,7 +115,7 @@
color="error"
variant="text"
@click.stop="
deleteDialog = true;
state.deleteDialog = true;
deleteTarget = item.name;
"
>
@@ -130,7 +130,7 @@
/>
<BaseButton
small
@click.stop="setSelected(item); importDialog = true"
@click.stop="setSelected(item); state.importDialog = true"
>
<template #icon>
{{ $globals.icons.backupRestore }}
@@ -151,38 +151,33 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import { useAdminApi } from "~/composables/api";
import type { AllBackups } from "~/lib/api/types/admin";
import { alert } from "~/composables/use-toast";
export default defineNuxtComponent({
setup() {
definePageMeta({
definePageMeta({
layout: "admin",
});
});
const i18n = useI18n();
const auth = useMealieAuth();
const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug || auth.user.value?.groupSlug || "");
const i18n = useI18n();
const adminApi = useAdminApi();
const selected = ref("");
const adminApi = useAdminApi();
const selected = ref("");
const backups = ref<AllBackups>({
const backups = ref<AllBackups>({
imports: [],
templates: [],
});
});
async function refreshBackups() {
async function refreshBackups() {
const { data } = await adminApi.backups.getAll();
if (data) {
backups.value = data;
}
}
}
async function createBackup() {
async function createBackup() {
state.runningBackup = true;
const { data } = await adminApi.backups.create();
@@ -194,9 +189,9 @@ export default defineNuxtComponent({
alert.error(i18n.t("settings.backup.error-creating-backup-see-log-file"));
}
state.runningBackup = false;
}
}
async function restoreBackup(fileName: string) {
async function restoreBackup(fileName: string) {
state.runningRestore = true;
const { error } = await adminApi.backups.restore(fileName);
@@ -212,20 +207,20 @@ export default defineNuxtComponent({
window.location.reload();
}, 500);
}
}
}
const deleteTarget = ref("");
const deleteTarget = ref("");
async function deleteBackup() {
async function deleteBackup() {
const { data } = await adminApi.backups.delete(deleteTarget.value);
if (!data?.error) {
alert.success(i18n.t("settings.backup.backup-deleted"));
refreshBackups();
}
}
}
const state = reactive({
const state = reactive({
confirmImport: false,
deleteDialog: false,
createDialog: false,
@@ -239,42 +234,25 @@ export default defineNuxtComponent({
{ title: i18n.t("export.size"), value: "size" },
{ title: "", value: "actions", align: "right" },
],
});
});
function setSelected(data: { name: string; date: string }) {
function setSelected(data: { name: string; date: string }) {
if (!data.name) {
return;
}
selected.value = data.name;
}
}
const backupsFileNameDownload = (fileName: string) => `api/admin/backups/${fileName}`;
const backupsFileNameDownload = (fileName: string) => `api/admin/backups/${fileName}`;
useSeoMeta({
useSeoMeta({
title: i18n.t("sidebar.backups"),
});
});
onMounted(refreshBackups);
onMounted(refreshBackups);
return {
groupSlug,
restoreBackup,
selected,
...toRefs(state),
backups,
createBackup,
deleteBackup,
deleteTarget,
setSelected,
refreshBackups,
backupsFileNameDownload,
};
},
head() {
return {
title: useI18n().t("sidebar.backups"),
};
},
useHead({
title: i18n.t("sidebar.backups"),
});
</script>

View File

@@ -83,45 +83,42 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import { useAdminApi } from "~/composables/api";
import { alert } from "~/composables/use-toast";
export default defineNuxtComponent({
setup() {
definePageMeta({
definePageMeta({
layout: "admin",
});
});
const api = useAdminApi();
const i18n = useI18n();
const api = useAdminApi();
const i18n = useI18n();
// Set page title
useSeoMeta({
// Set page title
useSeoMeta({
title: i18n.t("admin.debug-openai-services"),
});
});
const loading = ref(false);
const response = ref("");
const loading = ref(false);
const response = ref("");
const uploadForm = ref<VForm | null>(null);
const uploadedImage = ref<Blob | File>();
const uploadedImageName = ref<string>("");
const uploadedImagePreviewUrl = ref<string>();
const uploadedImage = ref<Blob | File>();
const uploadedImageName = ref<string>("");
const uploadedImagePreviewUrl = ref<string>();
function uploadImage(fileObject: File) {
function uploadImage(fileObject: File) {
uploadedImage.value = fileObject;
uploadedImageName.value = fileObject.name;
uploadedImagePreviewUrl.value = URL.createObjectURL(fileObject);
}
}
function clearImage() {
function clearImage() {
uploadedImage.value = undefined;
uploadedImageName.value = "";
uploadedImagePreviewUrl.value = undefined;
}
}
async function testOpenAI() {
async function testOpenAI() {
response.value = "";
loading.value = true;
@@ -134,18 +131,5 @@ export default defineNuxtComponent({
else {
response.value = data.response || (data.success ? "Test Successful" : "Test Failed");
}
}
return {
loading,
response,
uploadForm,
uploadedImage,
uploadedImagePreviewUrl,
uploadImage,
clearImage,
testOpenAI,
};
},
});
}
</script>

View File

@@ -11,7 +11,7 @@
<div class="d-flex align-center justify-center justify-md-start flex-wrap">
<v-btn-toggle
v-model="parser"
v-model="state.parser"
density="compact"
mandatory="force"
@change="processIngredient"
@@ -38,7 +38,7 @@
<v-card flat>
<v-card-text>
<v-text-field
v-model="ingredient"
v-model="state.ingredient"
:label="$t('admin.ingredient-text')"
/>
</v-card-text>
@@ -55,9 +55,9 @@
</v-card-actions>
</v-card>
</v-container>
<v-container v-if="results">
<v-container v-if="state.results">
<div
v-if="parser !== 'brute' && getConfidence('average')"
v-if="state.parser !== 'brute' && getConfidence('average')"
class="d-flex"
>
<v-chip
@@ -111,7 +111,7 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import { alert } from "~/composables/use-toast";
import { useUserApi } from "~/composables/api";
import type { IngredientConfidence } from "~/lib/api/types/recipe";
@@ -119,31 +119,29 @@ import type { Parser } from "~/lib/api/user/recipes/recipe";
type ConfidenceAttribute = "average" | "comment" | "name" | "unit" | "quantity" | "food";
export default defineNuxtComponent({
setup() {
definePageMeta({
definePageMeta({
layout: "admin",
});
});
const api = useUserApi();
const api = useUserApi();
const state = reactive({
const state = reactive({
loading: false,
ingredient: "",
results: false,
parser: "nlp" as Parser,
});
});
const i18n = useI18n();
const i18n = useI18n();
// Set page title
useSeoMeta({
// Set page title
useSeoMeta({
title: i18n.t("admin.parser"),
});
});
const confidence = ref<IngredientConfidence>({});
const confidence = ref<IngredientConfidence>({});
function getColor(attribute: ConfidenceAttribute) {
function getColor(attribute: ConfidenceAttribute) {
const percentage = getConfidence(attribute);
if (percentage === undefined) return;
@@ -159,9 +157,9 @@ export default defineNuxtComponent({
else {
return "error";
}
}
}
function getConfidence(attribute: ConfidenceAttribute) {
function getConfidence(attribute: ConfidenceAttribute) {
if (!confidence.value) {
return;
}
@@ -171,22 +169,22 @@ export default defineNuxtComponent({
return `${(+property * 100).toFixed(0)}%`;
}
return undefined;
}
}
const tryText = [
const tryText = [
"2 tbsp minced cilantro, leaves and stems",
"1 large yellow onion, coarsely chopped",
"1 1/2 tsp garam masala",
"1 inch piece fresh ginger, (peeled and minced)",
"2 cups mango chunks, (2 large mangoes) (fresh or frozen)",
];
];
function processTryText(str: string) {
function processTryText(str: string) {
state.ingredient = str;
processIngredient();
}
}
async function processIngredient() {
async function processIngredient() {
if (state.ingredient === "") {
return;
}
@@ -224,9 +222,9 @@ export default defineNuxtComponent({
state.results = false;
}
state.loading = false;
}
}
const properties = reactive({
const properties = reactive({
quantity: {
subtitle: i18n.t("recipe.quantity"),
value: "" as string | number,
@@ -251,23 +249,9 @@ export default defineNuxtComponent({
color: null,
confidence: null,
},
});
const showConfidence = ref(false);
return {
showConfidence,
getColor,
confidence,
getConfidence,
...toRefs(state),
tryText,
properties,
processTryText,
processIngredient,
};
},
});
const showConfidence = ref(false);
</script>
<style scoped></style>

View File

@@ -96,41 +96,39 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import { useAdminApi } from "~/composables/api";
import type { MaintenanceStorageDetails, MaintenanceSummary } from "~/lib/api/types/admin";
export default defineNuxtComponent({
setup() {
definePageMeta({
definePageMeta({
layout: "admin",
});
});
const state = reactive({
const state = reactive({
storageDetails: false,
storageDetailsLoading: false,
fetchingInfo: false,
actionLoading: false,
});
});
const adminApi = useAdminApi();
const i18n = useI18n();
const adminApi = useAdminApi();
const i18n = useI18n();
// Set page title
useSeoMeta({
// Set page title
useSeoMeta({
title: i18n.t("admin.maintenance.page-title"),
});
});
// ==========================================================================
// General Info
// ==========================================================================
// General Info
const infoResults = ref<MaintenanceSummary>({
const infoResults = ref<MaintenanceSummary>({
dataDirSize: i18n.t("about.unknown-version"),
cleanableDirs: 0,
cleanableImages: 0,
});
});
async function getSummary() {
async function getSummary() {
state.fetchingInfo = true;
const { data } = await adminApi.maintenance.getInfo();
@@ -141,9 +139,9 @@ export default defineNuxtComponent({
};
state.fetchingInfo = false;
}
}
const info = computed(() => {
const info = computed(() => {
return [
{
name: i18n.t("admin.maintenance.info-description-data-dir-size"),
@@ -158,26 +156,26 @@ export default defineNuxtComponent({
value: infoResults.value.cleanableImages,
},
];
});
});
// ==========================================================================
// Storage Details
// ==========================================================================
// Storage Details
const storageTitles: { [key: string]: string } = {
const storageTitles: { [key: string]: string } = {
tempDirSize: i18n.t("admin.maintenance.storage.title-temporary-directory") as string,
backupsDirSize: i18n.t("admin.maintenance.storage.title-backups-directory") as string,
groupsDirSize: i18n.t("admin.maintenance.storage.title-groups-directory") as string,
recipesDirSize: i18n.t("admin.maintenance.storage.title-recipes-directory") as string,
userDirSize: i18n.t("admin.maintenance.storage.title-user-directory") as string,
};
};
function storageDetailsText(key: string) {
function storageDetailsText(key: string) {
return storageTitles[key] ?? i18n.t("about.unknown-version");
}
}
const storageDetails = ref<MaintenanceStorageDetails | null>(null);
const storageDetails = ref<MaintenanceStorageDetails | null>(null);
async function openDetails() {
async function openDetails() {
state.storageDetailsLoading = true;
state.storageDetails = true;
@@ -188,30 +186,30 @@ export default defineNuxtComponent({
}
state.storageDetailsLoading = true;
}
}
// ==========================================================================
// Actions
// ==========================================================================
// Actions
async function handleCleanDirectories() {
async function handleCleanDirectories() {
state.actionLoading = true;
await adminApi.maintenance.cleanRecipeFolders();
state.actionLoading = false;
}
}
async function handleCleanImages() {
async function handleCleanImages() {
state.actionLoading = true;
await adminApi.maintenance.cleanImages();
state.actionLoading = false;
}
}
async function handleCleanTemp() {
async function handleCleanTemp() {
state.actionLoading = true;
await adminApi.maintenance.cleanTemp();
state.actionLoading = false;
}
}
const actions = [
const actions = [
{
name: i18n.t("admin.maintenance.action-clean-directories-name"),
handler: handleCleanDirectories,
@@ -227,19 +225,7 @@ export default defineNuxtComponent({
handler: handleCleanImages,
subtitle: i18n.t("admin.maintenance.action-clean-images-description"),
},
];
return {
storageDetailsText,
openDetails,
storageDetails,
state,
info,
getSummary,
actions,
};
},
});
];
</script>
<style scoped>

View File

@@ -49,36 +49,31 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import GroupPreferencesEditor from "~/components/Domain/Group/GroupPreferencesEditor.vue";
import { useAdminApi } from "~/composables/api";
import { alert } from "~/composables/use-toast";
import type { VForm } from "vuetify/components";
export default defineNuxtComponent({
components: {
GroupPreferencesEditor,
},
setup() {
definePageMeta({
definePageMeta({
layout: "admin",
});
});
const route = useRoute();
const route = useRoute();
const i18n = useI18n();
const i18n = useI18n();
const groupId = computed(() => route.params.id as string);
const groupId = computed(() => route.params.id as string);
// ==============================================
// New User Form
// ==============================================
// New User Form
const refGroupEditForm = ref<VForm | null>(null);
const refGroupEditForm = ref<VForm | null>(null);
const adminApi = useAdminApi();
const adminApi = useAdminApi();
const userError = ref(false);
const userError = ref(false);
const { data: group } = useLazyAsyncData(`get-household-${groupId.value}`, async () => {
const { data: group } = useLazyAsyncData(`get-household-${groupId.value}`, async () => {
if (!groupId.value) {
return null;
}
@@ -89,9 +84,9 @@ export default defineNuxtComponent({
userError.value = true;
}
return data;
}, { watch: [groupId] });
}, { watch: [groupId] });
async function handleSubmit() {
async function handleSubmit() {
if (!refGroupEditForm.value?.validate() || group.value === null) {
return;
}
@@ -107,14 +102,5 @@ export default defineNuxtComponent({
else {
alert.error(i18n.t("settings.settings-update-failed"));
}
}
return {
group,
userError,
refGroupEditForm,
handleSubmit,
};
},
});
}
</script>

View File

@@ -1,28 +1,28 @@
<template>
<v-container fluid>
<BaseDialog
v-model="createDialog"
v-model="state.createDialog"
:title="$t('group.create-group')"
:icon="$globals.icons.group"
can-submit
@submit="createGroup(createGroupForm.data)"
@submit="createGroup(state.createGroupForm.data)"
>
<template #activator />
<v-card-text>
<AutoForm
v-model="createGroupForm.data"
:update-mode="updateMode"
:items="createGroupForm.items"
v-model="state.createGroupForm.data"
:update-mode="state.updateMode"
:items="state.createGroupForm.items"
/>
</v-card-text>
</BaseDialog>
<BaseDialog
v-model="confirmDialog"
v-model="state.confirmDialog"
:title="$t('general.confirm')"
color="error"
can-confirm
@confirm="deleteGroup(deleteTarget)"
@confirm="deleteGroup(state.deleteTarget)"
>
<template #activator />
<v-card-text>
@@ -43,14 +43,14 @@
</v-toolbar>
<v-data-table
:headers="headers"
:headers="state.headers"
:items="groups || []"
item-key="id"
class="elevation-0"
:items-per-page="-1"
hide-default-footer
disable-pagination
:search="search"
:search="state.search"
@click:row="($event, { item }) => handleRowClick(item)"
>
<template #[`item.households`]="{ item }">
@@ -73,8 +73,8 @@
color="error"
variant="text"
@click.stop="
confirmDialog = true;
deleteTarget = item.id;
state.confirmDialog = true;
state.deleteTarget = item.id;
"
>
<v-icon>
@@ -92,28 +92,30 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import { fieldTypes } from "~/composables/forms";
import { useGroups } from "~/composables/use-groups";
import { validators } from "~/composables/use-validators";
import type { GroupInDB } from "~/lib/api/types/user";
export default defineNuxtComponent({
setup() {
definePageMeta({
definePageMeta({
layout: "admin",
});
});
const i18n = useI18n();
const i18n = useI18n();
// Set page title
useSeoMeta({
useHead({
title: i18n.t("group.manage-groups"),
});
});
const { groups, refreshAllGroups, deleteGroup, createGroup } = useGroups();
// Set page title
useSeoMeta({
title: i18n.t("group.manage-groups"),
});
const state = reactive({
const { groups, deleteGroup, createGroup } = useGroups();
const state = reactive({
createDialog: false,
confirmDialog: false,
deleteTarget: "",
@@ -144,23 +146,14 @@ export default defineNuxtComponent({
name: "",
},
},
});
});
function openDialog() {
function openDialog() {
state.createDialog = true;
state.createGroupForm.data.name = "";
}
}
function handleRowClick(item: GroupInDB) {
function handleRowClick(item: GroupInDB) {
navigateTo(`/admin/manage/groups/${item.id}`);
}
return { ...toRefs(state), groups, refreshAllGroups, deleteGroup, createGroup, openDialog, handleRowClick };
},
head() {
return {
title: useI18n().t("group.manage-groups"),
};
},
});
}
</script>

View File

@@ -67,38 +67,34 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import HouseholdPreferencesEditor from "~/components/Domain/Household/HouseholdPreferencesEditor.vue";
import { useGroups } from "~/composables/use-groups";
import { useAdminApi } from "~/composables/api";
import { alert } from "~/composables/use-toast";
import { validators } from "~/composables/use-validators";
import type { VForm } from "vuetify/components";
export default defineNuxtComponent({
components: {
HouseholdPreferencesEditor,
},
setup() {
definePageMeta({
definePageMeta({
layout: "admin",
});
});
const route = useRoute();
const i18n = useI18n();
const route = useRoute();
const i18n = useI18n();
const { groups } = useGroups();
const householdId = computed(() => route.params.id as string);
const { groups } = useGroups();
const householdId = computed(() => route.params.id as string);
// ==============================================
// New User Form
// ==============================================
// New User Form
const refHouseholdEditForm = ref<VForm | null>(null);
const refHouseholdEditForm = ref<VForm | null>(null);
const adminApi = useAdminApi();
const adminApi = useAdminApi();
const userError = ref(false);
const userError = ref(false);
const { data: household } = useAsyncData(`get-household-${householdId.value}`, async () => {
const { data: household } = useAsyncData(`get-household-${householdId.value}`, async () => {
if (!householdId.value) {
return null;
}
@@ -109,9 +105,9 @@ export default defineNuxtComponent({
userError.value = true;
}
return data;
}, { watch: [householdId] });
}, { watch: [householdId] });
async function handleSubmit() {
async function handleSubmit() {
if (!refHouseholdEditForm.value?.validate() || household.value === null) {
return;
}
@@ -124,16 +120,5 @@ export default defineNuxtComponent({
else {
alert.error(i18n.t("settings.settings-update-failed"));
}
}
return {
groups,
household,
validators,
userError,
refHouseholdEditForm,
handleSubmit,
};
},
});
}
</script>

View File

@@ -137,7 +137,7 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import { useAdminApi, useUserApi } from "~/composables/api";
import { useGroups } from "~/composables/use-groups";
import { useAdminHouseholds } from "~/composables/use-households";
@@ -146,40 +146,38 @@ import { useUserForm } from "~/composables/use-users";
import { validators } from "~/composables/use-validators";
import type { UserOut } from "~/lib/api/types/user";
export default defineNuxtComponent({
setup() {
definePageMeta({
definePageMeta({
layout: "admin",
});
});
const { userForm } = useUserForm();
const { groups } = useGroups();
const { useHouseholdsInGroup } = useAdminHouseholds();
const i18n = useI18n();
const route = useRoute();
const { userForm } = useUserForm();
const { groups } = useGroups();
const { useHouseholdsInGroup } = useAdminHouseholds();
const i18n = useI18n();
const route = useRoute();
const userId = route.params.id as string;
const userId = route.params.id as string;
// ==============================================
// New User Form
// ==============================================
// New User Form
const refNewUserForm = ref<VForm | null>(null);
const refNewUserForm = ref<VForm | null>(null);
const adminApi = useAdminApi();
const adminApi = useAdminApi();
const user = ref<UserOut | null>(null);
const households = useHouseholdsInGroup(computed(() => user.value?.groupId || ""));
const user = ref<UserOut | null>(null);
const households = useHouseholdsInGroup(computed(() => user.value?.groupId || ""));
const disabledFields = computed(() => {
const disabledFields = computed(() => {
return user.value?.authMethod !== "Mealie" ? ["admin"] : [];
});
});
const userError = ref(false);
const userError = ref(false);
const resetUrl = ref<string | null>(null);
const generatingToken = ref(false);
const resetUrl = ref<string | null>(null);
const generatingToken = ref(false);
onMounted(async () => {
onMounted(async () => {
const { data, error } = await adminApi.users.getOne(userId);
if (error?.response?.status === 404) {
@@ -190,9 +188,9 @@ export default defineNuxtComponent({
if (data) {
user.value = data;
}
});
});
async function handleSubmit() {
async function handleSubmit() {
if (!refNewUserForm.value?.validate() || user.value === null) return;
const { response, data } = await adminApi.users.updateOne(user.value.id, user.value);
@@ -200,9 +198,9 @@ export default defineNuxtComponent({
if (response?.status === 200 && data) {
user.value = data;
}
}
}
async function handlePasswordReset() {
async function handlePasswordReset() {
if (user.value === null) return;
generatingToken.value = true;
@@ -214,10 +212,10 @@ export default defineNuxtComponent({
}
generatingToken.value = false;
}
}
const userApi = useUserApi();
async function sendResetEmail() {
const userApi = useUserApi();
async function sendResetEmail() {
if (!user.value?.email) return;
const { response } = await userApi.email.sendForgotPassword({ email: user.value.email });
if (response && response.status === 200) {
@@ -226,23 +224,5 @@ export default defineNuxtComponent({
else {
alert.error(i18n.t("profile.error-sending-email"));
}
}
return {
user,
disabledFields,
userError,
userForm,
refNewUserForm,
handleSubmit,
groups,
households,
validators,
handlePasswordReset,
resetUrl,
generatingToken,
sendResetEmail,
};
},
});
}
</script>

View File

@@ -2,11 +2,11 @@
<v-container fluid>
<UserInviteDialog v-model="inviteDialog" />
<BaseDialog
v-model="deleteDialog"
v-model="state.deleteDialog"
:title="$t('general.confirm')"
color="error"
can-confirm
@confirm="deleteUser(deleteTargetId)"
@confirm="deleteUser(state.deleteTargetId)"
>
<template #activator />
@@ -60,7 +60,7 @@
:items-per-page="-1"
hide-default-footer
disable-pagination
:search="search"
:search="state.search"
@click:row="($event, { item }) => handleRowClick(item)"
>
<template #[`item.admin`]="{ item }">
@@ -78,8 +78,8 @@
color="error"
variant="text"
@click.stop="
deleteDialog = true;
deleteTargetId = item.id;
state.deleteDialog = true;
state.deleteTargetId = item.id;
"
>
<v-icon>
@@ -93,74 +93,72 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import { useAdminApi } from "~/composables/api";
import { alert } from "~/composables/use-toast";
import { useUser, useAllUsers } from "~/composables/use-user";
import type { UserOut } from "~/lib/api/types/user";
import UserInviteDialog from "~/components/Domain/User/UserInviteDialog.vue";
export default defineNuxtComponent({
components: {
UserInviteDialog,
},
setup() {
definePageMeta({
definePageMeta({
layout: "admin",
});
});
const i18n = useI18n();
const api = useAdminApi();
const refUserDialog = ref();
const inviteDialog = ref();
const auth = useMealieAuth();
useHead({
title: i18n.t("sidebar.manage-users"),
});
const user = computed(() => auth.user.value);
const api = useAdminApi();
const inviteDialog = ref();
const auth = useMealieAuth();
const i18n = useI18n();
const { $globals } = useNuxtApp();
const user = computed(() => auth.user.value);
const router = useRouter();
const { $globals } = useNuxtApp();
const isUserOwnAccount = computed(() => {
const router = useRouter();
const isUserOwnAccount = computed(() => {
return state.deleteTargetId === user.value?.id;
});
});
const ACTIONS_OPTIONS = [
const ACTIONS_OPTIONS = [
{
text: i18n.t("user.reset-locked-users"),
icon: $globals.icons.lock,
event: "unlock-all-users",
},
];
];
const state = reactive({
const state = reactive({
deleteDialog: false,
deleteTargetId: "",
search: "",
groups: [],
households: [],
sendTo: "",
});
});
const { users, refreshAllUsers } = useAllUsers();
const { loading, deleteUser: deleteUserMixin } = useUser(refreshAllUsers);
const { users, refreshAllUsers } = useAllUsers();
const { deleteUser: deleteUserMixin } = useUser(refreshAllUsers);
function deleteUser(id: string) {
function deleteUser(id: string) {
deleteUserMixin(id);
if (isUserOwnAccount.value) {
auth.refresh();
}
}
}
function handleRowClick(item: UserOut) {
function handleRowClick(item: UserOut) {
router.push(`/admin/manage/users/${item.id}`);
}
}
// ==========================================================
// Constants / Non-reactive
// ==========================================================
// Constants / Non-reactive
const headers = [
const headers = [
{
title: i18n.t("user.user-id"),
align: "start",
@@ -174,9 +172,9 @@ export default defineNuxtComponent({
{ 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> {
async function unlockAllUsers(): Promise<void> {
const { data } = await api.users.unlockAllUsers(true);
if (data) {
@@ -185,31 +183,9 @@ export default defineNuxtComponent({
alert.success(`${unlocked} user(s) unlocked`);
refreshAllUsers();
}
}
}
useSeoMeta({
useSeoMeta({
title: i18n.t("sidebar.manage-users"),
});
return {
isUserOwnAccount,
unlockAllUsers,
...toRefs(state),
headers,
deleteUser,
loading,
refUserDialog,
inviteDialog,
users,
user,
handleRowClick,
ACTIONS_OPTIONS,
};
},
head() {
return {
title: useI18n().t("sidebar.manage-users"),
};
},
});
</script>

View File

@@ -126,7 +126,7 @@
</div>
<div>
<v-text-field
v-model="address"
v-model="state.address"
class="mr-4"
:label="$t('user.email')"
:rules="[validators.email]"
@@ -135,7 +135,7 @@
color="info"
variant="elevated"
:disabled="!appConfig.emailReady || !validEmail"
:loading="loading"
:loading="state.loading"
class="opacity-100"
@click="testEmail"
>
@@ -144,12 +144,12 @@
</template>
{{ $t("general.test") }}
</BaseButton>
<template v-if="tested">
<template v-if="state.tested">
<v-divider class="my-x mt-6" />
<v-card-text class="px-0">
<h4> {{ $t("settings.email-test-results") }}</h4>
<span class="pl-4">
{{ success ? $t('settings.succeeded') : $t('settings.failed') }}
{{ state.success ? $t('settings.succeeded') : $t('settings.failed') }}
</span>
</v-card-text>
</template>
@@ -226,7 +226,7 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import type { TranslateResult } from "vue-i18n";
import { useAdminApi, useUserApi } from "~/composables/api";
import { validators } from "~/composables/use-validators";
@@ -234,12 +234,6 @@ import { useAsyncKey } from "~/composables/use-utils";
import type { CheckAppConfig } from "~/lib/api/types/admin";
import AppLoader from "~/components/global/AppLoader.vue";
enum DockerVolumeState {
Unknown = "unknown",
Success = "success",
Error = "error",
}
interface SimpleCheck {
id: string;
text: TranslateResult;
@@ -254,36 +248,33 @@ interface CheckApp extends CheckAppConfig {
isSiteSecure?: boolean;
}
export default defineNuxtComponent({
components: { AppLoader },
setup() {
definePageMeta({
definePageMeta({
layout: "admin",
});
});
// For some reason the layout is not set automatically, so we set it here,
// even though it's defined above in the page meta.
onMounted(() => {
// For some reason the layout is not set automatically, so we set it here,
// even though it's defined above in the page meta.
onMounted(() => {
setPageLayout("admin");
});
});
const { $globals } = useNuxtApp();
const i18n = useI18n();
const { $globals } = useNuxtApp();
const i18n = useI18n();
const state = reactive({
const state = reactive({
loading: false,
address: "",
success: false,
error: "",
tested: false,
});
});
// Set page title
useSeoMeta({
// Set page title
useSeoMeta({
title: i18n.t("settings.site-settings"),
});
});
const appConfig = ref<CheckApp>({
const appConfig = ref<CheckApp>({
emailReady: true,
baseUrlSet: true,
isSiteSecure: true,
@@ -291,20 +282,20 @@ export default defineNuxtComponent({
ldapReady: false,
oidcReady: false,
enableOpenai: false,
});
function isLocalHostOrHttps() {
});
function isLocalHostOrHttps() {
return window.location.hostname === "localhost" || window.location.protocol === "https:";
}
const api = useUserApi();
const adminApi = useAdminApi();
onMounted(async () => {
}
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 simpleChecks = computed<SimpleCheck[]>(() => {
const goodIcon = $globals.icons.checkboxMarkedCircle;
const badIcon = $globals.icons.alert;
const warningIcon = $globals.icons.alertCircle;
@@ -368,8 +359,8 @@ export default defineNuxtComponent({
},
];
return data;
});
async function testEmail() {
});
async function testEmail() {
state.loading = true;
state.tested = false;
const { data } = await api.email.test({ email: state.address });
@@ -384,8 +375,8 @@ export default defineNuxtComponent({
}
state.loading = false;
state.tested = true;
}
const validEmail = computed(() => {
}
const validEmail = computed(() => {
if (state.address === "") {
return false;
}
@@ -395,14 +386,14 @@ export default defineNuxtComponent({
return true;
}
return false;
});
// ============================================================
// General About Info
const rawAppInfo = ref({
});
// ============================================================
// General About Info
const rawAppInfo = ref({
version: "null",
versionLatest: "null",
});
function getAppInfo() {
});
function getAppInfo() {
const { data: statistics } = useAsyncData(useAsyncKey(), async () => {
const { data } = await adminApi.about.about();
if (data) {
@@ -473,10 +464,10 @@ export default defineNuxtComponent({
return data;
});
return statistics;
}
const appInfo = getAppInfo();
const bugReportDialog = ref(false);
const bugReportText = computed(() => {
}
const appInfo = getAppInfo();
const bugReportDialog = ref(false);
const bugReportText = computed(() => {
const ignore = {
[i18n.t("about.database-url")]: true,
[i18n.t("about.default-group")]: true,
@@ -503,20 +494,6 @@ export default defineNuxtComponent({
});
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>

View File

@@ -16,7 +16,7 @@
<v-card-text>
<v-form @submit.prevent="requestLink()">
<v-text-field
v-model="email"
v-model="state.email"
:prepend-inner-icon="$globals.icons.email"
variant="solo-filled"
flat
@@ -31,7 +31,7 @@
<v-card-actions class="justify-center">
<div class="max-button">
<v-btn
:loading="loading"
:loading="state.loading"
color="primary"
type="submit"
size="large"
@@ -60,32 +60,30 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import { useUserApi } from "~/composables/api";
import { alert } from "~/composables/use-toast";
export default defineNuxtComponent({
setup() {
definePageMeta({
definePageMeta({
layout: "basic",
});
});
const state = reactive({
const state = reactive({
email: "",
loading: false,
error: false,
});
});
const i18n = useI18n();
const i18n = useI18n();
// Set page title
useSeoMeta({
// Set page title
useSeoMeta({
title: i18n.t("user.login"),
});
});
const api = useUserApi();
const api = useUserApi();
async function requestLink() {
async function requestLink() {
state.loading = true;
// TODO: Fix Response to send meaningful error
const { response } = await api.email.sendForgotPassword({ email: state.email });
@@ -100,14 +98,7 @@ export default defineNuxtComponent({
state.error = true;
alert.error(i18n.t("profile.error-sending-email"));
}
}
return {
requestLink,
...toRefs(state),
};
},
});
}
</script>
<style lang="css">

View File

@@ -2,10 +2,6 @@
<CookbookPage />
</template>
<script lang="ts">
<script setup lang="ts">
import CookbookPage from "@/components/Domain/Cookbook/CookbookPage.vue";
export default defineNuxtComponent({
components: { CookbookPage },
});
</script>

View File

@@ -135,7 +135,7 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { VueDraggable } from "vue-draggable-plus";
import { useCookbookStore } from "~/composables/store/use-cookbook-store";
import { useHouseholdSelf } from "@/composables/use-households";
@@ -143,28 +143,28 @@ import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
import type { CreateCookBook, ReadCookBook } from "~/lib/api/types/cookbook";
import { useCookbookPreferences } from "~/composables/use-users/preferences";
export default defineNuxtComponent({
components: { CookbookEditor, VueDraggable },
definePageMeta({
middleware: ["group-only"],
setup() {
const dialogStates = reactive({
});
const dialogStates = reactive({
create: false,
delete: false,
});
});
const i18n = useI18n();
const i18n = useI18n();
// Set page title
useSeoMeta({
// Set page title
useSeoMeta({
title: i18n.t("cookbook.cookbooks"),
});
});
const auth = useMealieAuth();
const { store: allCookbooks, actions, updateAll } = useCookbookStore();
const auth = useMealieAuth();
const { store: allCookbooks, actions, updateAll } = useCookbookStore();
// Make a local reactive copy of myCookbooks
const myCookbooks = ref<ReadCookBook[]>([]);
watch(
// Make a local reactive copy of myCookbooks
const myCookbooks = ref<ReadCookBook[]>([]);
watch(
allCookbooks,
(cookbooks) => {
myCookbooks.value
@@ -173,15 +173,15 @@ export default defineNuxtComponent({
).sort((a, b) => a.position > b.position) ?? [];
},
{ immediate: true },
);
);
const { household } = useHouseholdSelf();
const cookbookPreferences = useCookbookPreferences();
const { household } = useHouseholdSelf();
const cookbookPreferences = useCookbookPreferences();
// create
const createTargetKey = ref(0);
const createTarget = ref<ReadCookBook | null>(null);
async function createCookbook() {
// create
const createTargetKey = ref(0);
const createTarget = ref<ReadCookBook | null>(null);
async function createCookbook() {
const name = i18n.t("cookbook.household-cookbook-name", [
household.value?.name || "",
String((myCookbooks.value?.length ?? 0) + 1),
@@ -198,15 +198,15 @@ export default defineNuxtComponent({
createTargetKey.value++;
});
dialogStates.create = true;
}
}
// delete
const deleteTarget = ref<ReadCookBook | null>(null);
function deleteEventHandler(item: ReadCookBook) {
// delete
const deleteTarget = ref<ReadCookBook | null>(null);
function deleteEventHandler(item: ReadCookBook) {
deleteTarget.value = item;
dialogStates.delete = true;
}
async function deleteCookbook() {
}
async function deleteCookbook() {
if (!deleteTarget.value) {
return;
}
@@ -214,9 +214,9 @@ export default defineNuxtComponent({
myCookbooks.value = myCookbooks.value.filter(c => c.id !== deleteTarget.value?.id);
dialogStates.delete = false;
deleteTarget.value = null;
}
}
async function deleteCreateTarget() {
async function deleteCreateTarget() {
if (!createTarget.value?.id) {
return;
}
@@ -224,40 +224,18 @@ export default defineNuxtComponent({
myCookbooks.value = myCookbooks.value.filter(c => c.id !== createTarget.value?.id);
dialogStates.create = false;
createTarget.value = null;
}
function handleUnmount() {
}
function handleUnmount() {
if (!createTarget.value?.id || createTarget.value.queryFilterString) {
return;
}
deleteCreateTarget();
}
onMounted(() => {
}
onMounted(() => {
window.addEventListener("beforeunload", handleUnmount);
});
onBeforeUnmount(() => {
});
onBeforeUnmount(() => {
handleUnmount();
window.removeEventListener("beforeunload", handleUnmount);
});
return {
myCookbooks,
cookbookPreferences,
actions,
dialogStates,
// create
createTargetKey,
createTarget,
createCookbook,
// update
updateAll,
// delete
deleteTarget,
deleteEventHandler,
deleteCookbook,
deleteCreateTarget,
};
},
});
</script>

View File

@@ -4,10 +4,6 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import RecipeExplorerPage from "~/components/Domain/Recipe/RecipeExplorerPage/RecipeExplorerPage.vue";
export default defineNuxtComponent({
components: { RecipeExplorerPage },
});
</script>

View File

@@ -42,23 +42,23 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import type { MenuItem } from "~/components/global/BaseOverflowButton.vue";
import AdvancedOnly from "~/components/global/AdvancedOnly.vue";
export default defineNuxtComponent({
components: { AdvancedOnly },
definePageMeta({
middleware: ["group-only"],
setup() {
const i18n = useI18n();
const auth = useMealieAuth();
const { $appInfo, $globals } = useNuxtApp();
});
useSeoMeta({
const i18n = useI18n();
const auth = useMealieAuth();
const { $appInfo, $globals } = useNuxtApp();
useSeoMeta({
title: i18n.t("general.create"),
});
});
const subpages = computed<MenuItem[]>(() => [
const subpages = computed<MenuItem[]>(() => [
{
icon: $globals.icons.link,
text: i18n.t("recipe.import-with-url"),
@@ -95,26 +95,18 @@ export default defineNuxtComponent({
text: i18n.t("recipe.debug-scraper"),
value: "debug",
},
]);
]);
const route = useRoute();
const router = useRouter();
const groupSlug = computed(() => route.params.groupSlug || auth.user.value?.groupSlug || "");
const route = useRoute();
const router = useRouter();
const groupSlug = computed(() => route.params.groupSlug || auth.user.value?.groupSlug || "");
const subpage = computed({
const subpage = computed({
set(subpage: string) {
router.push({ path: `/g/${groupSlug.value}/r/create/${subpage}`, query: route.query });
},
get() {
return route.path.split("/").pop() ?? "url";
},
});
return {
groupSlug,
subpages,
subpage,
};
},
});
</script>

View File

@@ -49,7 +49,7 @@
</template>
</v-text-field>
</v-col>
<template v-if="showCatTags">
<template v-if="state.showCatTags">
<v-col
cols="12"
xs="12"
@@ -115,14 +115,14 @@
{{ $t('general.new') }}
</BaseButton>
<RecipeDialogBulkAdd
v-model="bulkDialog"
v-model="state.bulkDialog"
class="mr-1 mr-sm-0 mb-1"
@bulk-data="assignUrls"
/>
</v-card-actions>
<div class="px-0">
<v-checkbox
v-model="showCatTags"
v-model="state.showCatTags"
hide-details
:label="$t('recipe.set-categories-and-tags')"
/>
@@ -154,7 +154,7 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { whenever } from "@vueuse/shared";
import { useUserApi } from "~/composables/api";
import { alert } from "~/composables/use-toast";
@@ -162,30 +162,25 @@ import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerS
import type { ReportSummary } from "~/lib/api/types/reports";
import RecipeDialogBulkAdd from "~/components/Domain/Recipe/RecipeDialogBulkAdd.vue";
export default defineNuxtComponent({
components: { RecipeOrganizerSelector, RecipeDialogBulkAdd },
setup() {
const state = reactive({
error: false,
loading: false,
const state = reactive({
showCatTags: false,
bulkDialog: false,
});
});
whenever(
whenever(
() => !state.showCatTags,
() => {
console.log("showCatTags changed");
},
);
);
const api = useUserApi();
const i18n = useI18n();
const api = useUserApi();
const i18n = useI18n();
const bulkUrls = ref([{ url: "", categories: [], tags: [] }]);
const lockBulkImport = ref(false);
const bulkUrls = ref([{ url: "", categories: [], tags: [] }]);
const lockBulkImport = ref(false);
async function bulkCreate() {
async function bulkCreate() {
if (bulkUrls.value.length === 0) {
return;
}
@@ -201,19 +196,19 @@ export default defineNuxtComponent({
}
fetchReports();
}
}
// =========================================================
// Reports
// =========================================================
// Reports
const reports = ref<ReportSummary[]>([]);
const reports = ref<ReportSummary[]>([]);
async function fetchReports() {
async function fetchReports() {
const { data } = await api.groupReports.getAll("bulk_import");
reports.value = data ?? [];
}
}
async function deleteReport(id: string) {
async function deleteReport(id: string) {
console.log(id);
const { response } = await api.groupReports.deleteOne(id);
@@ -223,23 +218,11 @@ export default defineNuxtComponent({
else {
alert.error(i18n.t("recipe.report-deletion-failed"));
}
}
}
fetchReports();
fetchReports();
function assignUrls(urls: string[]) {
function assignUrls(urls: string[]) {
bulkUrls.value = urls.map(url => ({ url, categories: [], tags: [] }));
}
return {
assignUrls,
reports,
deleteReport,
bulkCreate,
bulkUrls,
lockBulkImport,
...toRefs(state),
};
},
});
}
</script>

View File

@@ -28,7 +28,7 @@
<v-card-text v-if="$appInfo.enableOpenai">
{{ $t('recipe.recipe-debugger-use-openai-description') }}
<v-checkbox
v-model="useOpenAI"
v-model="state.useOpenAI"
:label="$t('recipe.use-openai')"
/>
</v-card-text>
@@ -40,7 +40,7 @@
block
type="submit"
color="info"
:loading="loading"
:loading="state.loading"
>
<template #icon>
{{ $globals.icons.robot }}
@@ -67,24 +67,21 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { useUserApi } from "~/composables/api";
import { validators } from "~/composables/use-validators";
import type { Recipe } from "~/lib/api/types/recipe";
export default defineNuxtComponent({
setup() {
const state = reactive({
error: false,
const state = reactive({
loading: false,
useOpenAI: false,
});
});
const api = useUserApi();
const route = useRoute();
const router = useRouter();
const api = useUserApi();
const route = useRoute();
const router = useRouter();
const recipeUrl = computed({
const recipeUrl = computed({
set(recipe_import_url: string | null) {
if (recipe_import_url !== null) {
recipe_import_url = recipe_import_url.trim();
@@ -94,13 +91,13 @@ export default defineNuxtComponent({
get() {
return route.query.recipe_import_url as string | null;
},
});
});
const debugTreeView = ref(false);
const debugTreeView = ref(false);
const debugData = ref<Recipe | null>(null);
const debugData = ref<Recipe | null>(null);
async function debugUrl(url: string | null) {
async function debugUrl(url: string | null) {
if (url === null) {
return;
}
@@ -111,16 +108,5 @@ export default defineNuxtComponent({
state.loading = false;
debugData.value = data;
}
return {
recipeUrl,
debugTreeView,
debugUrl,
debugData,
...toRefs(state),
validators,
};
},
});
}
</script>

View File

@@ -90,7 +90,7 @@
rounded
block
type="submit"
:loading="loading"
:loading="state.loading"
/>
</div>
<v-card-text class="py-2">
@@ -103,7 +103,7 @@
</v-form>
</template>
<script lang="ts">
<script setup lang="ts">
import type { AxiosResponse } from "axios";
import { useTagStore } from "~/composables/store/use-tag-store";
import { useUserApi } from "~/composables/api";
@@ -111,30 +111,28 @@ import { useNewRecipeOptions } from "~/composables/use-new-recipe-options";
import { validators } from "~/composables/use-validators";
import type { VForm } from "~/types/auto-forms";
export default defineNuxtComponent({
setup() {
const state = reactive({
const state = reactive({
error: false,
loading: false,
isEditJSON: false,
});
const auth = useMealieAuth();
const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
const domUrlForm = ref<VForm | null>(null);
});
const auth = useMealieAuth();
const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
const domUrlForm = ref<VForm | null>(null);
const api = useUserApi();
const tags = useTagStore();
const api = useUserApi();
const tags = useTagStore();
const {
const {
importKeywordsAsTags,
importCategories,
stayInEditMode,
parseRecipe,
navigateToRecipe,
} = useNewRecipeOptions();
} = useNewRecipeOptions();
function handleResponse(response: AxiosResponse<string> | null, refreshTags = false) {
function handleResponse(response: AxiosResponse<string> | null, refreshTags = false) {
if (response?.status !== 201) {
state.error = true;
state.loading = false;
@@ -145,12 +143,12 @@ export default defineNuxtComponent({
}
navigateToRecipe(response.data, groupSlug.value, `/g/${groupSlug.value}/r/create/html`);
}
}
const newRecipeData = ref<string | object | null>(null);
const newRecipeUrl = ref<string | null>(null);
const newRecipeData = ref<string | object | null>(null);
const newRecipeUrl = ref<string | null>(null);
function handleIsEditJson() {
function handleIsEditJson() {
if (state.isEditJSON) {
if (newRecipeData.value) {
try {
@@ -170,11 +168,11 @@ export default defineNuxtComponent({
else {
newRecipeData.value = null;
}
}
handleIsEditJson();
}
handleIsEditJson();
const createStatus = ref<string | null>(null);
async function createFromHtmlOrJson(htmlOrJsonData: string | object | null, importKeywordsAsTags: boolean, importCategories: boolean, url: string | null = null) {
const createStatus = ref<string | null>(null);
async function createFromHtmlOrJson(htmlOrJsonData: string | object | null, importKeywordsAsTags: boolean, importCategories: boolean, url: string | null = null) {
if (!htmlOrJsonData) {
return;
}
@@ -202,22 +200,5 @@ export default defineNuxtComponent({
);
createStatus.value = null;
handleResponse(response, importKeywordsAsTags);
}
return {
domUrlForm,
importKeywordsAsTags,
stayInEditMode,
parseRecipe,
importCategories,
newRecipeData,
newRecipeUrl,
handleIsEditJson,
createStatus,
createFromHtmlOrJson,
...toRefs(state),
validators,
};
},
});
}
</script>

View File

@@ -37,7 +37,7 @@
:img="imageUrl"
cropper-height="100%"
cropper-width="100%"
:submitted="loading"
:submitted="state.loading"
class="mt-4 mb-2"
@save="(croppedImage) => updateUploadedImage(index, croppedImage)"
@delete="clearImage(index)"
@@ -45,7 +45,7 @@
<v-btn
v-if="uploadedImages.length > 1"
:disabled="loading || index === 0"
:disabled="state.loading || index === 0"
color="primary"
@click="() => setCoverImage(index)"
>
@@ -66,7 +66,7 @@
color="primary"
hide-details
:label="$t('recipe.should-translate-description')"
:disabled="loading"
:disabled="state.loading"
/>
<v-checkbox
v-if="uploadedImages.length"
@@ -74,15 +74,15 @@
color="primary"
hide-details
:label="$t('recipe.parse-recipe-ingredients-after-import')"
:disabled="loading"
:disabled="state.loading"
/>
</v-card-text>
<v-card-actions v-if="uploadedImages.length">
<div class="w-100 d-flex flex-column align-center">
<p style="width: 250px">
<BaseButton rounded block type="submit" :loading="loading" />
<BaseButton rounded block type="submit" :loading="state.loading" />
</p>
<p v-if="loading" class="mb-0">
<p v-if="state.loading" class="mb-0">
{{
uploadedImages.length > 1
? $t("recipe.please-wait-images-processing")
@@ -96,50 +96,48 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { useUserApi } from "~/composables/api";
import { alert } from "~/composables/use-toast";
import { useNewRecipeOptions } from "~/composables/use-new-recipe-options";
import type { VForm } from "~/types/auto-forms";
export default defineNuxtComponent({
setup() {
const state = reactive({
const state = reactive({
loading: false,
});
});
const i18n = useI18n();
const api = useUserApi();
const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug || "");
const i18n = useI18n();
const api = useUserApi();
const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug || "");
const domUrlForm = ref<VForm | null>(null);
const uploadedImages = ref<(Blob | File)[]>([]);
const uploadedImageNames = ref<string[]>([]);
const uploadedImagesPreviewUrls = ref<string[]>([]);
const shouldTranslate = ref(true);
const domUrlForm = ref<VForm | null>(null);
const uploadedImages = ref<(Blob | File)[]>([]);
const uploadedImageNames = ref<string[]>([]);
const uploadedImagesPreviewUrls = ref<string[]>([]);
const shouldTranslate = ref(true);
const { parseRecipe, navigateToRecipe } = useNewRecipeOptions();
const { parseRecipe, navigateToRecipe } = useNewRecipeOptions();
function uploadImages(files: File[]) {
function uploadImages(files: File[]) {
uploadedImages.value = [...uploadedImages.value, ...files];
uploadedImageNames.value = [...uploadedImageNames.value, ...files.map(file => file.name)];
uploadedImagesPreviewUrls.value = [
...uploadedImagesPreviewUrls.value,
...files.map(file => URL.createObjectURL(file)),
];
}
}
function clearImage(index: number) {
function clearImage(index: number) {
// Revoke _before_ splicing
URL.revokeObjectURL(uploadedImagesPreviewUrls.value[index]);
uploadedImages.value.splice(index, 1);
uploadedImageNames.value.splice(index, 1);
uploadedImagesPreviewUrls.value.splice(index, 1);
}
}
async function createRecipe() {
async function createRecipe() {
if (uploadedImages.value.length === 0) {
return;
}
@@ -155,14 +153,14 @@ export default defineNuxtComponent({
else {
navigateToRecipe(data, groupSlug.value, `/g/${groupSlug.value}/r/create/image`);
}
}
}
function updateUploadedImage(index: number, croppedImage: Blob) {
function updateUploadedImage(index: number, croppedImage: Blob) {
uploadedImages.value[index] = croppedImage;
uploadedImagesPreviewUrls.value[index] = URL.createObjectURL(croppedImage);
}
}
function swapItem(array: any[], i: number, j: number) {
function swapItem(array: any[], i: number, j: number) {
if (i < 0 || j < 0 || i >= array.length || j >= array.length) {
return;
}
@@ -170,37 +168,21 @@ export default defineNuxtComponent({
const temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
function swapImages(i: number, j: number) {
function swapImages(i: number, j: number) {
swapItem(uploadedImages.value, i, j);
swapItem(uploadedImageNames.value, i, j);
swapItem(uploadedImagesPreviewUrls.value, i, j);
}
}
// Put the intended cover image at the start of the array
// The backend currently sets the first image as the cover image
function setCoverImage(index: number) {
// Put the intended cover image at the start of the array
// The backend currently sets the first image as the cover image
function setCoverImage(index: number) {
if (index < 0 || index >= uploadedImages.value.length || index === 0) {
return;
}
swapImages(0, index);
}
return {
...toRefs(state),
domUrlForm,
uploadedImages,
uploadedImagesPreviewUrls,
shouldTranslate,
parseRecipe,
uploadImages,
clearImage,
createRecipe,
updateUploadedImage,
setCoverImage,
};
},
});
}
</script>

View File

@@ -2,15 +2,10 @@
<div />
</template>
<script lang="ts">
export default defineNuxtComponent({
setup() {
const router = useRouter();
onMounted(() => {
<script setup lang="ts">
const router = useRouter();
onMounted(() => {
// Force redirect to first valid page
router.replace("/r/create/url");
});
return {};
},
});
</script>

View File

@@ -33,7 +33,7 @@
:disabled="newRecipeName.trim() === ''"
rounded
block
:loading="loading"
:loading="state.loading"
@click="createByName(newRecipeName)"
/>
</div>
@@ -41,51 +41,40 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import type { AxiosResponse } from "axios";
import { useUserApi } from "~/composables/api";
import { validators } from "~/composables/use-validators";
import type { VForm } from "~/types/auto-forms";
export default defineNuxtComponent({
setup() {
const state = reactive({
const state = reactive({
error: false,
loading: false,
});
const auth = useMealieAuth();
const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
});
const auth = useMealieAuth();
const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
const api = useUserApi();
const router = useRouter();
const api = useUserApi();
const router = useRouter();
function handleResponse(response: AxiosResponse<string> | null, edit = false) {
function handleResponse(response: AxiosResponse<string> | null, edit = false) {
if (response?.status !== 201) {
state.error = true;
state.loading = false;
return;
}
router.push(`/g/${groupSlug.value}/r/${response.data}?edit=${edit.toString()}`);
}
}
const newRecipeName = ref("");
const domCreateByName = ref<VForm | null>(null);
const newRecipeName = ref("");
const domCreateByName = ref<VForm | null>(null);
async function createByName(name: string) {
async function createByName(name: string) {
if (!domCreateByName.value?.validate() || name === "") {
return;
}
const { response } = await api.recipes.createOne({ name });
handleResponse(response as any, true);
}
return {
domCreateByName,
newRecipeName,
createByName,
...toRefs(state),
validators,
};
},
});
}
</script>

View File

@@ -72,7 +72,7 @@
rounded
block
type="submit"
:loading="loading"
:loading="state.loading"
/>
</div>
<v-card-text class="py-2">
@@ -85,7 +85,7 @@
</v-form>
<v-expand-transition>
<v-alert
v-if="error"
v-if="state.error"
color="error"
class="mt-6 white--text"
>
@@ -140,7 +140,7 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import type { AxiosResponse } from "axios";
import { useUserApi } from "~/composables/api";
import { useTagStore } from "~/composables/store/use-tag-store";
@@ -148,36 +148,34 @@ import { useNewRecipeOptions } from "~/composables/use-new-recipe-options";
import { validators } from "~/composables/use-validators";
import type { VForm } from "~/types/auto-forms";
export default defineNuxtComponent({
setup() {
definePageMeta({
definePageMeta({
key: route => route.path,
});
const state = reactive({
});
const state = reactive({
error: false,
loading: false,
});
});
const auth = useMealieAuth();
const api = useUserApi();
const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
const auth = useMealieAuth();
const api = useUserApi();
const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
const router = useRouter();
const tags = useTagStore();
const router = useRouter();
const tags = useTagStore();
const {
const {
importKeywordsAsTags,
importCategories,
stayInEditMode,
parseRecipe,
navigateToRecipe,
} = useNewRecipeOptions();
} = useNewRecipeOptions();
const bulkImporterTarget = computed(() => `/g/${groupSlug.value}/r/create/bulk`);
const htmlOrJsonImporterTarget = computed(() => `/g/${groupSlug.value}/r/create/html`);
const bulkImporterTarget = computed(() => `/g/${groupSlug.value}/r/create/bulk`);
const htmlOrJsonImporterTarget = computed(() => `/g/${groupSlug.value}/r/create/html`);
function handleResponse(response: AxiosResponse<string> | null, refreshTags = false) {
function handleResponse(response: AxiosResponse<string> | null, refreshTags = false) {
if (response?.status !== 201) {
state.error = true;
state.loading = false;
@@ -188,9 +186,9 @@ export default defineNuxtComponent({
}
navigateToRecipe(response.data, groupSlug.value, `/g/${groupSlug.value}/r/create/url`);
}
}
const recipeUrl = computed({
const recipeUrl = computed({
set(recipe_import_url: string | null) {
if (recipe_import_url !== null) {
recipe_import_url = recipe_import_url.trim();
@@ -200,9 +198,9 @@ export default defineNuxtComponent({
get() {
return route.query.recipe_import_url as string | null;
},
});
});
onMounted(() => {
onMounted(() => {
if (recipeUrl.value && recipeUrl.value.includes("https")) {
// Check if we have a query params for using keywords as tags or staying in edit mode.
// We don't use these in the app anymore, but older automations such as Bookmarklet might still use them,
@@ -226,22 +224,22 @@ export default defineNuxtComponent({
createByUrl(recipeUrl.value, importKeywordsAsTags.value, false);
return;
}
});
});
const domUrlForm = ref<VForm | null>(null);
const domUrlForm = ref<VForm | null>(null);
// Remove import URL from query params when leaving the page
const isLeaving = ref(false);
onBeforeRouteLeave((to) => {
// Remove import URL from query params when leaving the page
const isLeaving = ref(false);
onBeforeRouteLeave((to) => {
if (isLeaving.value) {
return;
}
isLeaving.value = true;
router.replace({ query: undefined }).then(() => router.push(to));
});
});
const createStatus = ref<string | null>(null);
async function createByUrl(url: string | null, importKeywordsAsTags: boolean, importCategories: boolean) {
const createStatus = ref<string | null>(null);
async function createByUrl(url: string | null, importKeywordsAsTags: boolean, importCategories: boolean) {
if (url === null) {
return;
}
@@ -259,24 +257,7 @@ export default defineNuxtComponent({
);
createStatus.value = null;
handleResponse(response, importKeywordsAsTags);
}
return {
bulkImporterTarget,
htmlOrJsonImporterTarget,
recipeUrl,
importKeywordsAsTags,
importCategories: importCategories,
stayInEditMode,
parseRecipe,
domUrlForm,
createStatus,
createByUrl,
...toRefs(state),
validators,
};
},
});
}
</script>
<style scoped>

View File

@@ -27,7 +27,7 @@
:disabled="newRecipeZip === null"
rounded
block
:loading="loading"
:loading="state.loading"
@click="createByZip"
/>
</div>
@@ -36,28 +36,25 @@
</v-form>
</template>
<script lang="ts">
<script setup lang="ts">
import { useUserApi } from "~/composables/api";
import { useGlobalI18n } from "~/composables/use-global-i18n";
import { alert } from "~/composables/use-toast";
import { validators } from "~/composables/use-validators";
export default defineNuxtComponent({
setup() {
const state = reactive({
const state = reactive({
loading: false,
});
const auth = useMealieAuth();
const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
});
const auth = useMealieAuth();
const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
const api = useUserApi();
const router = useRouter();
const api = useUserApi();
const router = useRouter();
const newRecipeZip = ref<File | null>(null);
const newRecipeZipFileName = "archive";
const newRecipeZip = ref<File | null>(null);
const newRecipeZipFileName = "archive";
async function createByZip() {
async function createByZip() {
if (!newRecipeZip.value) {
return;
}
@@ -79,14 +76,5 @@ export default defineNuxtComponent({
finally {
state.loading = false;
}
}
return {
newRecipeZip,
createByZip,
...toRefs(state),
validators,
};
},
});
}
</script>

View File

@@ -15,27 +15,18 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import RecipeOrganizerPage from "~/components/Domain/Recipe/RecipeOrganizerPage.vue";
import { useCategoryStore } from "~/composables/store";
export default defineNuxtComponent({
components: {
RecipeOrganizerPage,
},
definePageMeta({
middleware: ["group-only"],
setup() {
const { store, actions } = useCategoryStore();
const i18n = useI18n();
});
useSeoMeta({
const { store, actions } = useCategoryStore();
const i18n = useI18n();
useSeoMeta({
title: i18n.t("category.categories"),
});
return {
store,
actions,
};
},
});
</script>

View File

@@ -17,7 +17,7 @@
{{ $t('recipe-finder.recipe-finder-description') }}
</template>
</BasePageTitle>
<v-container v-if="ready">
<v-container v-if="state.ready">
<v-row>
<v-col :cols="useMobile ? 12 : 3">
<v-container class="ma-0 pa-0">
@@ -51,38 +51,38 @@
</SearchFilter>
<div :class="attrs.searchFilter.filterClass">
<v-badge
:model-value="!!queryFilterJSON.parts && queryFilterJSON.parts.length > 0"
:model-value="!!state.queryFilterJSON.parts && state.queryFilterJSON.parts.length > 0"
size="small"
color="primary"
:content="(queryFilterJSON.parts || []).length"
:content="(state.queryFilterJSON.parts || []).length"
>
<v-btn
size="small"
color="accent"
dark
@click="queryFilterMenu = !queryFilterMenu"
@click="state.queryFilterMenu = !state.queryFilterMenu"
>
<v-icon start>
{{ $globals.icons.filter }}
</v-icon>
{{ $t("recipe-finder.other-filters") }}
<BaseDialog
v-model="queryFilterMenu"
v-model="state.queryFilterMenu"
:title="$t('recipe-finder.other-filters')"
:icon="$globals.icons.filter"
width="100%"
max-width="1100px"
:submit-disabled="!queryFilterEditorValue"
:submit-disabled="!state.queryFilterEditorValue"
can-confirm
@confirm="saveQueryFilter"
>
<v-card-text>
<QueryFilterBuilder
:key="queryFilterMenuKey"
:initial-query-filter="queryFilterJSON"
:key="state.queryFilterMenuKey"
:initial-query-filter="state.queryFilterJSON"
:field-defs="queryFilterBuilderFields"
@input="(value) => queryFilterEditorValue = value"
@input-j-s-o-n="(value) => queryFilterEditorValueJSON = value"
@input="(value) => state.queryFilterEditorValue = value"
@input-j-s-o-n="(value) => state.queryFilterEditorValueJSON = value"
/>
</v-card-text>
<template #custom-card-action>
@@ -113,7 +113,7 @@
:class="attrs.settings.colClass"
>
<v-menu
v-model="settingsMenu"
v-model="state.settingsMenu"
offset-y
nudge-bottom="3"
:close-on-content-click="false"
@@ -135,7 +135,7 @@
<v-card-text>
<div>
<v-number-input
v-model="settings.maxMissingFoods"
v-model="state.settings.maxMissingFoods"
:precision="null"
:min="0"
control-variant="stacked"
@@ -144,7 +144,7 @@
:label="$t('recipe-finder.max-missing-ingredients')"
/>
<v-number-input
v-model="settings.maxMissingTools"
v-model="state.settings.maxMissingTools"
:precision="null"
:min="0"
control-variant="stacked"
@@ -157,7 +157,7 @@
<div class="mt-1">
<v-checkbox
v-if="isOwnGroup"
v-model="settings.includeFoodsOnHand"
v-model="state.settings.includeFoodsOnHand"
density="compact"
size="small"
hide-details
@@ -166,7 +166,7 @@
/>
<v-checkbox
v-if="isOwnGroup"
v-model="settings.includeToolsOnHand"
v-model="state.settings.includeToolsOnHand"
density="compact"
size="small"
hide-details
@@ -328,7 +328,7 @@
:recipe="item.recipe"
:missing-foods="item.missingFoods"
:missing-tools="item.missingTools"
:disable-checkbox="loading"
:disable-checkbox="state.loading"
@add-food="addFood"
@remove-food="removeFood"
@add-tool="addTool"
@@ -356,7 +356,7 @@
:recipe="item.recipe"
:missing-foods="item.missingFoods"
:missing-tools="item.missingTools"
:disable-checkbox="loading"
:disable-checkbox="state.loading"
@add-food="addFood"
@remove-food="removeFood"
@add-tool="addTool"
@@ -366,7 +366,7 @@
</v-col>
</v-row>
</v-container>
<v-container v-else-if="!recipesReady">
<v-container v-else-if="!state.recipesReady">
<v-row>
<v-col
cols="12"
@@ -411,7 +411,7 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import { watchDebounced } from "@vueuse/core";
import { useUserApi } from "~/composables/api";
import { usePublicExploreApi } from "~/composables/api/api-client";
@@ -431,26 +431,23 @@ interface RecipeSuggestions {
missingItems: RecipeSuggestionResponseItem[];
}
export default defineNuxtComponent({
components: { QueryFilterBuilder, RecipeSuggestion, SearchFilter },
setup() {
const display = useDisplay();
const i18n = useI18n();
const auth = useMealieAuth();
const route = useRoute();
const display = useDisplay();
const i18n = useI18n();
const auth = useMealieAuth();
const route = useRoute();
useSeoMeta({
useSeoMeta({
title: i18n.t("recipe-finder.recipe-finder"),
});
});
const useMobile = computed(() => display.smAndDown.value);
const useMobile = computed(() => display.smAndDown.value);
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
const { isOwnGroup } = useLoggedInState();
const api = isOwnGroup.value ? useUserApi() : usePublicExploreApi(groupSlug.value).explore;
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
const { isOwnGroup } = useLoggedInState();
const api = isOwnGroup.value ? useUserApi() : usePublicExploreApi(groupSlug.value).explore;
const preferences = useRecipeFinderPreferences();
const state = reactive({
const preferences = useRecipeFinderPreferences();
const state = reactive({
ready: false,
loading: false,
recipesReady: false,
@@ -468,16 +465,16 @@ export default defineNuxtComponent({
queryFilter: preferences.value.queryFilter,
limit: 20,
},
});
});
onMounted(() => {
onMounted(() => {
if (!isOwnGroup.value) {
state.settings.includeFoodsOnHand = false;
state.settings.includeToolsOnHand = false;
}
});
});
watch(
watch(
() => state,
(newState) => {
preferences.value.queryFilter = newState.settings.queryFilter;
@@ -490,9 +487,9 @@ export default defineNuxtComponent({
{
deep: true,
},
);
);
const attrs = computed(() => {
const attrs = computed(() => {
return {
title: {
class: {
@@ -508,51 +505,53 @@ export default defineNuxtComponent({
colClass: useMobile.value ? "d-flex flex-wrap justify-end" : "d-flex flex-wrap justify-start",
},
};
});
});
const foodStore = isOwnGroup.value ? useFoodStore() : usePublicFoodStore(groupSlug.value);
const selectedFoods = ref<IngredientFood[]>([]);
function addFood(food: IngredientFood) {
const foodStore = isOwnGroup.value ? useFoodStore() : usePublicFoodStore(groupSlug.value);
const foods = foodStore.store.value;
const selectedFoods = ref<IngredientFood[]>([]);
function addFood(food: IngredientFood) {
selectedFoods.value = [...selectedFoods.value, food];
handleFoodUpdates();
}
function removeFood(food: IngredientFood) {
}
function removeFood(food: IngredientFood) {
selectedFoods.value = selectedFoods.value.filter(f => f.id !== food.id);
handleFoodUpdates();
}
function handleFoodUpdates() {
}
function handleFoodUpdates() {
selectedFoods.value.sort((a, b) => (a.pluralName || a.name).localeCompare(b.pluralName || b.name));
preferences.value.foodIds = selectedFoods.value.map(food => food.id);
}
watch(
}
watch(
() => selectedFoods.value,
() => {
handleFoodUpdates();
},
);
);
const toolStore = isOwnGroup.value ? useToolStore() : usePublicToolStore(groupSlug.value);
const selectedTools = ref<RecipeTool[]>([]);
function addTool(tool: RecipeTool) {
const toolStore = isOwnGroup.value ? useToolStore() : usePublicToolStore(groupSlug.value);
const tools = toolStore.store.value;
const selectedTools = ref<RecipeTool[]>([]);
function addTool(tool: RecipeTool) {
selectedTools.value = [...selectedTools.value, tool];
handleToolUpdates();
}
function removeTool(tool: RecipeTool) {
}
function removeTool(tool: RecipeTool) {
selectedTools.value = selectedTools.value.filter(t => t.id !== tool.id);
handleToolUpdates();
}
function handleToolUpdates() {
}
function handleToolUpdates() {
selectedTools.value.sort((a, b) => a.name.localeCompare(b.name));
preferences.value.toolIds = selectedTools.value.map(tool => tool.id);
}
watch(
}
watch(
() => selectedTools.value,
() => {
handleToolUpdates();
},
);
);
async function hydrateFoods() {
async function hydrateFoods() {
if (!preferences.value.foodIds.length) {
return;
}
@@ -565,9 +564,9 @@ export default defineNuxtComponent({
.filter(food => !!food);
selectedFoods.value = foods;
}
}
async function hydrateTools() {
async function hydrateTools() {
if (!preferences.value.toolIds.length) {
return;
}
@@ -580,18 +579,18 @@ export default defineNuxtComponent({
.filter(tool => !!tool);
selectedTools.value = tools;
}
}
onMounted(async () => {
onMounted(async () => {
await Promise.all([hydrateFoods(), hydrateTools()]);
state.ready = true;
if (!selectedFoods.value.length) {
state.recipesReady = true;
};
});
});
const recipeResponseItems = ref<RecipeSuggestionResponseItem[]>([]);
const recipeSuggestions = computed<RecipeSuggestions>(() => {
const recipeResponseItems = ref<RecipeSuggestionResponseItem[]>([]);
const recipeSuggestions = computed<RecipeSuggestions>(() => {
const readyToMake: RecipeSuggestionResponseItem[] = [];
const missingItems: RecipeSuggestionResponseItem[] = [];
recipeResponseItems.value.forEach((responseItem) => {
@@ -607,9 +606,9 @@ export default defineNuxtComponent({
readyToMake,
missingItems,
};
});
});
watchDebounced(
watchDebounced(
[selectedFoods, selectedTools, state.settings], async () => {
// don't search for suggestions if no foods are selected
if (!selectedFoods.value.length) {
@@ -641,9 +640,9 @@ export default defineNuxtComponent({
{
debounce: 500,
},
);
);
const queryFilterBuilderFields: FieldDefinition[] = [
const queryFilterBuilderFields: FieldDefinition[] = [
{
name: "recipe_category.id",
label: i18n.t("category.categories"),
@@ -669,41 +668,20 @@ export default defineNuxtComponent({
label: i18n.t("general.last-made"),
type: "relativeDate",
},
];
];
function clearQueryFilter() {
function clearQueryFilter() {
state.queryFilterEditorValue = "";
state.queryFilterEditorValueJSON = { parts: [] } as QueryFilterJSON;
state.settings.queryFilter = "";
state.queryFilterJSON = { parts: [] } as QueryFilterJSON;
state.queryFilterMenu = false;
state.queryFilterMenuKey += 1;
}
}
function saveQueryFilter() {
function saveQueryFilter() {
state.settings.queryFilter = state.queryFilterEditorValue || "";
state.queryFilterJSON = state.queryFilterEditorValueJSON || { parts: [] } as QueryFilterJSON;
state.queryFilterMenu = false;
}
return {
...toRefs(state),
useMobile,
attrs,
isOwnGroup,
foods: foodStore.store,
selectedFoods,
addFood,
removeFood,
tools: toolStore.store,
selectedTools,
addTool,
removeTool,
recipeSuggestions,
queryFilterBuilderFields,
clearQueryFilter,
saveQueryFilter,
};
},
});
}
</script>

View File

@@ -15,27 +15,18 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import RecipeOrganizerPage from "~/components/Domain/Recipe/RecipeOrganizerPage.vue";
import { useTagStore } from "~/composables/store";
export default defineNuxtComponent({
components: {
RecipeOrganizerPage,
},
definePageMeta({
middleware: ["group-only"],
setup() {
const { store, actions } = useTagStore();
const i18n = useI18n();
});
useSeoMeta({
const { store, actions } = useTagStore();
const i18n = useI18n();
useSeoMeta({
title: i18n.t("tag.tags"),
});
return {
store,
actions,
};
},
});
</script>

View File

@@ -30,25 +30,25 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { useUserApi } from "~/composables/api";
import RecipeTimeline from "~/components/Domain/Recipe/RecipeTimeline.vue";
export default defineNuxtComponent({
components: { RecipeTimeline },
definePageMeta({
middleware: ["group-only"],
setup() {
const i18n = useI18n();
const api = useUserApi();
const ready = ref<boolean>(false);
});
useSeoMeta({
const i18n = useI18n();
const api = useUserApi();
const ready = ref<boolean>(false);
useSeoMeta({
title: i18n.t("recipe.timeline"),
});
});
const groupName = ref<string>("");
const queryFilter = ref<string>("");
async function fetchHousehold() {
const groupName = ref<string>("");
const queryFilter = ref<string>("");
async function fetchHousehold() {
const { data } = await api.households.getCurrentUserHousehold();
if (data) {
queryFilter.value = `recipe.group_id="${data.groupId}"`;
@@ -56,15 +56,7 @@ export default defineNuxtComponent({
}
ready.value = true;
}
}
useAsyncData("house-hold", fetchHousehold);
return {
groupName,
queryFilter,
ready,
};
},
});
useAsyncData("house-hold", fetchHousehold);
</script>

View File

@@ -15,7 +15,7 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import RecipeOrganizerPage from "~/components/Domain/Recipe/RecipeOrganizerPage.vue";
import { useToolStore } from "~/composables/store";
import type { RecipeTool } from "~/lib/api/types/recipe";
@@ -24,34 +24,31 @@ interface RecipeToolWithOnHand extends RecipeTool {
onHand: boolean;
}
export default defineNuxtComponent({
components: {
RecipeOrganizerPage,
},
definePageMeta({
middleware: ["group-only"],
setup() {
const auth = useMealieAuth();
const toolStore = useToolStore();
const dialog = ref(false);
const i18n = useI18n();
});
useSeoMeta({
const auth = useMealieAuth();
const toolStore = useToolStore();
const i18n = useI18n();
useSeoMeta({
title: i18n.t("tool.tools"),
});
});
const userHousehold = computed(() => auth.user.value?.householdSlug || "");
const tools = computed(() => toolStore.store.value.map(tool => (
const userHousehold = computed(() => auth.user.value?.householdSlug || "");
const tools = computed(() => toolStore.store.value.map(tool => (
{
...tool,
onHand: tool.householdsWithTool?.includes(userHousehold.value) || false,
} as RecipeToolWithOnHand
)));
)));
async function deleteOne(id: string | number) {
async function deleteOne(id: string | number) {
await toolStore.actions.deleteOne(id);
}
}
async function updateOne(tool: RecipeToolWithOnHand) {
async function updateOne(tool: RecipeToolWithOnHand) {
if (userHousehold.value) {
if (tool.onHand && !tool.householdsWithTool?.includes(userHousehold.value)) {
if (!tool.householdsWithTool) {
@@ -66,14 +63,5 @@ export default defineNuxtComponent({
}
}
await toolStore.actions.updateOne(tool);
}
return {
dialog,
tools,
deleteOne,
updateOne,
};
},
});
}
</script>

View File

@@ -34,12 +34,13 @@
</v-container>
</template>
<script lang="ts">
export default defineNuxtComponent({
<script setup lang="ts">
definePageMeta({
middleware: ["can-organize-only"],
setup() {
const i18n = useI18n();
const buttonLookup: { [key: string]: string } = {
});
const i18n = useI18n();
const buttonLookup: { [key: string]: string } = {
recipes: i18n.t("general.recipes"),
recipeActions: i18n.t("recipe.recipe-actions"),
foods: i18n.t("general.foods"),
@@ -48,11 +49,11 @@ export default defineNuxtComponent({
categories: i18n.t("category.categories"),
tags: i18n.t("tag.tags"),
tools: i18n.t("tool.tools"),
};
};
const route = useRoute();
const route = useRoute();
const DATA_TYPE_OPTIONS = computed(() => [
const DATA_TYPE_OPTIONS = computed(() => [
{
text: i18n.t("general.recipes"),
value: "new",
@@ -95,9 +96,9 @@ export default defineNuxtComponent({
value: "new",
to: "/group/data/tools",
},
]);
]);
const buttonText = computed(() => {
const buttonText = computed(() => {
const last = route.path
.split("/")
.pop()
@@ -111,16 +112,9 @@ export default defineNuxtComponent({
}
return i18n.t("data-pages.select-data");
});
});
useSeoMeta({
useSeoMeta({
title: i18n.t("data-pages.data-management"),
});
return {
buttonText,
DATA_TYPE_OPTIONS,
};
},
});
</script>

View File

@@ -2,15 +2,10 @@
<div />
</template>
<script lang="ts">
export default defineNuxtComponent({
setup() {
const router = useRouter();
onMounted(() => {
<script setup lang="ts">
const router = useRouter();
onMounted(() => {
// Force redirect to first valid page
router.push("/group/data/foods");
});
return {};
},
});
</script>

View File

@@ -226,7 +226,7 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import RecipeDataTable from "~/components/Domain/Recipe/RecipeDataTable.vue";
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
import { useUserApi } from "~/composables/api";
@@ -249,29 +249,29 @@ enum MODES {
changeOwner = "changeOwner",
}
export default defineNuxtComponent({
components: { RecipeDataTable, RecipeOrganizerSelector, GroupExportData, RecipeSettingsSwitches, UserAvatar },
definePageMeta({
scrollToTop: true,
setup() {
const i18n = useI18n();
const auth = useMealieAuth();
const { $globals } = useNuxtApp();
});
useSeoMeta({
const i18n = useI18n();
const auth = useMealieAuth();
const { $globals } = useNuxtApp();
useSeoMeta({
title: i18n.t("data-pages.recipes.recipe-data"),
});
});
const { getAllRecipes, refreshRecipes } = useRecipes(true, true, false, `householdId=${auth.user.value?.householdId || ""}`);
const selected = ref<Recipe[]>([]);
const { refreshRecipes } = useRecipes(true, true, false, `householdId=${auth.user.value?.householdId || ""}`);
const selected = ref<Recipe[]>([]);
function resetAll() {
function resetAll() {
selected.value = [];
toSetTags.value = [];
toSetCategories.value = [];
loading.value = false;
}
}
const headers = reactive({
const headers = reactive({
id: false,
owner: false,
tags: true,
@@ -281,9 +281,9 @@ export default defineNuxtComponent({
recipeYieldQuantity: false,
recipeYield: false,
dateAdded: false,
});
});
const headerLabels = {
const headerLabels = {
id: i18n.t("general.id"),
owner: i18n.t("general.owner"),
tags: i18n.t("tag.tags"),
@@ -293,9 +293,9 @@ export default defineNuxtComponent({
recipeYieldQuantity: i18n.t("recipe.recipe-yield"),
recipeYield: i18n.t("recipe.recipe-yield-text"),
dateAdded: i18n.t("general.date-added"),
};
};
const actions: MenuItem[] = [
const actions: MenuItem[] = [
{
icon: $globals.icons.database,
text: i18n.t("export.export"),
@@ -326,42 +326,42 @@ export default defineNuxtComponent({
text: i18n.t("general.delete"),
event: "delete-selected",
},
];
];
const api = useUserApi();
const loading = ref(false);
const api = useUserApi();
const loading = ref(false);
// ===============================================================
// Group Exports
// ===============================================================
// Group Exports
const purgeExportsDialog = ref(false);
const purgeExportsDialog = ref(false);
async function purgeExports() {
async function purgeExports() {
await api.bulk.purgeExports();
refreshExports();
}
}
const groupExports = ref<GroupDataExport[]>([]);
const groupExports = ref<GroupDataExport[]>([]);
async function refreshExports() {
async function refreshExports() {
const { data } = await api.bulk.fetchExports();
if (data) {
groupExports.value = data;
}
}
}
onMounted(async () => {
onMounted(async () => {
await refreshExports();
});
// ===============================================================
// All Recipes
});
// ===============================================================
// All Recipes
function selectAll() {
function selectAll() {
selected.value = allRecipes.value;
}
}
async function exportSelected() {
async function exportSelected() {
loading.value = true;
const { data } = await api.bulk.bulkExport({
recipes: selected.value.map((x: Recipe) => x.slug ?? ""),
@@ -374,31 +374,31 @@ export default defineNuxtComponent({
resetAll();
refreshExports();
}
}
const toSetTags = ref([]);
const toSetTags = ref([]);
async function tagSelected() {
async function tagSelected() {
loading.value = true;
const recipes = selected.value.map((x: Recipe) => x.slug ?? "");
await api.bulk.bulkTag({ recipes, tags: toSetTags.value });
await refreshRecipes();
resetAll();
}
}
const toSetCategories = ref([]);
const toSetCategories = ref([]);
async function categorizeSelected() {
async function categorizeSelected() {
loading.value = true;
const recipes = selected.value.map((x: Recipe) => x.slug ?? "");
await api.bulk.bulkCategorize({ recipes, categories: toSetCategories.value });
await refreshRecipes();
resetAll();
}
}
async function deleteSelected() {
async function deleteSelected() {
loading.value = true;
const recipes = selected.value.map((x: Recipe) => x.slug ?? "");
@@ -407,18 +407,18 @@ export default defineNuxtComponent({
await refreshRecipes();
resetAll();
}
}
const recipeSettings = reactive<RecipeSettings>({
const recipeSettings = reactive<RecipeSettings>({
public: false,
showNutrition: false,
showAssets: false,
landscapeView: false,
disableComments: false,
locked: false,
});
});
async function updateSettings() {
async function updateSettings() {
loading.value = true;
const recipes = selected.value.map((x: Recipe) => x.slug ?? "");
@@ -427,9 +427,9 @@ export default defineNuxtComponent({
await refreshRecipes();
resetAll();
}
}
async function changeOwner() {
async function changeOwner() {
if (!selected.value.length || !selectedOwner.value) {
return;
}
@@ -443,12 +443,12 @@ export default defineNuxtComponent({
await refreshRecipes();
resetAll();
}
}
// ============================================================
// Dialog Management
// ============================================================
// Dialog Management
const dialog = reactive({
const dialog = reactive({
state: false,
title: i18n.t("data-pages.recipes.tag-recipes"),
mode: MODES.tag,
@@ -458,9 +458,9 @@ export default defineNuxtComponent({
return Promise.resolve();
},
icon: $globals.icons.tags,
});
});
function openDialog(mode: MODES) {
function openDialog(mode: MODES) {
const titles: Record<MODES, string> = {
[MODES.tag]: i18n.t("data-pages.recipes.tag-recipes"),
[MODES.category]: i18n.t("data-pages.recipes.categorize-recipes"),
@@ -493,12 +493,12 @@ export default defineNuxtComponent({
dialog.callback = callbacks[mode];
dialog.icon = icons[mode];
dialog.state = true;
}
}
const { store: allUsers } = useUserStore();
const { store: households } = useHouseholdStore();
const selectedOwner = ref("");
const selectedOwnerHousehold = computed(() => {
const { store: allUsers } = useUserStore();
const { store: households } = useHouseholdStore();
const selectedOwner = ref("");
const selectedOwnerHousehold = computed(() => {
if (!selectedOwner.value) {
return null;
}
@@ -509,35 +509,6 @@ export default defineNuxtComponent({
};
return households.value.find(h => h.id === owner.householdId);
});
return {
recipeSettings,
selectAll,
loading,
actions,
allRecipes,
categorizeSelected,
deleteSelected,
dialog,
exportSelected,
getAllRecipes,
headerLabels,
headers,
MODES,
openDialog,
selected,
tagSelected,
toSetCategories,
toSetTags,
groupExports,
purgeExportsDialog,
purgeExports,
allUsers,
selectedOwner,
selectedOwnerHousehold,
};
},
});
</script>

View File

@@ -43,24 +43,18 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import { useGroupSelf } from "~/composables/use-groups";
export default defineNuxtComponent({
definePageMeta({
middleware: ["can-manage-only"],
setup() {
const { group, actions: groupActions } = useGroupSelf();
const i18n = useI18n();
});
useSeoMeta({
const { group, actions: groupActions } = useGroupSelf();
const i18n = useI18n();
useSeoMeta({
title: i18n.t("group.group"),
});
return {
group,
groupActions,
};
},
});
</script>

View File

@@ -19,7 +19,7 @@
<BaseCardSectionTitle :title="$t('migration.new-migration')" />
<v-card
variant="outlined"
:loading="loading"
:loading="state.loading"
style="border-color: lightgrey;"
>
<v-card-title> {{ $t('migration.choose-migration-type') }} </v-card-title>
@@ -29,7 +29,7 @@
>
<div class="mb-2">
<BaseOverflowButton
v-model="migrationType"
v-model="state.migrationType"
mode="model"
:items="items"
/>
@@ -37,7 +37,7 @@
{{ content.text }}
<v-treeview
v-if="content.tree && Array.isArray(content.tree)"
:key="migrationType"
:key="state.migrationType"
density="compact"
:items="content.tree"
>
@@ -59,15 +59,15 @@
:text-btn="false"
@uploaded="setFileObject"
/>
{{ fileObject.name || $t('migration.no-file-selected') }}
{{ state.fileObject.name || $t('migration.no-file-selected') }}
</v-card-text>
<v-card-text>
<v-checkbox v-model="addMigrationTag">
<v-checkbox v-model="state.addMigrationTag">
<template #label>
<i18n-t keypath="migration.tag-all-recipes">
<template #tag-name>
<b class="mx-1"> {{ migrationType }} </b>
<b class="mx-1"> {{ state.migrationType }} </b>
</template>
</i18n-t>
</template>
@@ -76,7 +76,7 @@
<v-card-actions class="justify-end">
<BaseButton
:disabled="!fileObject.name"
:disabled="!state.fileObject.name"
submit
@click="startMigration"
>
@@ -88,14 +88,14 @@
<v-container class="$vuetify.display.smAndDown ? 'px-0': ''">
<BaseCardSectionTitle :title="$t('migration.previous-migrations')" />
<ReportTable
:items="reports"
:items="state.reports"
@delete="deleteReport"
/>
</v-container>
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import type { ReportSummary } from "~/lib/api/types/reports";
import type { MenuItem } from "~/components/global/BaseOverflowButton.vue";
import { useUserApi } from "~/composables/api";
@@ -127,28 +127,29 @@ const MIGRATIONS = {
cookn: "cookn",
};
export default defineNuxtComponent({
definePageMeta({
middleware: ["advanced-only"],
setup() {
const i18n = useI18n();
const { $globals } = useNuxtApp();
});
useSeoMeta({
const i18n = useI18n();
const { $globals } = useNuxtApp();
useSeoMeta({
title: i18n.t("settings.migrations"),
});
});
const api = useUserApi();
const api = useUserApi();
const state = reactive({
const state = reactive({
addMigrationTag: false,
loading: false,
treeState: true,
migrationType: MIGRATIONS.mealie as SupportedMigrations,
fileObject: {} as File,
reports: [] as ReportSummary[],
});
});
const items: MenuItem[] = [
const items: MenuItem[] = [
{
text: i18n.t("migration.mealie-pre-v1.title"),
value: MIGRATIONS.mealie,
@@ -190,8 +191,8 @@ export default defineNuxtComponent({
text: i18n.t("migration.cookn.title"),
value: MIGRATIONS.cookn,
},
];
const _content: Record<string, MigrationContent> = {
];
const _content: Record<string, MigrationContent> = {
[MIGRATIONS.mealie]: {
text: i18n.t("migration.mealie-pre-v1.description-long"),
acceptedFileType: ".zip",
@@ -431,9 +432,9 @@ export default defineNuxtComponent({
},
],
},
};
};
function addIdToNode(counter: number, node: TreeNode): number {
function addIdToNode(counter: number, node: TreeNode): number {
node.id = counter;
counter += 1;
if (node.children) {
@@ -442,9 +443,9 @@ export default defineNuxtComponent({
});
}
return counter;
}
}
for (const key in _content) {
for (const key in _content) {
const migration = _content[key];
if (migration.tree && Array.isArray(migration.tree)) {
let counter = 1;
@@ -452,15 +453,15 @@ export default defineNuxtComponent({
counter = addIdToNode(counter, node);
});
}
}
}
console.log(_content);
console.log(_content);
function setFileObject(fileObject: File) {
function setFileObject(fileObject: File) {
state.fileObject = fileObject;
}
}
async function startMigration() {
async function startMigration() {
state.loading = true;
const payload = {
addMigrationTag: state.addMigrationTag,
@@ -475,26 +476,26 @@ export default defineNuxtComponent({
if (data) {
state.reports.unshift(data);
}
}
}
async function getMigrationReports() {
async function getMigrationReports() {
const { data } = await api.groupReports.getAll("migration");
if (data) {
state.reports = data;
}
}
}
async function deleteReport(id: string) {
async function deleteReport(id: string) {
await api.groupReports.deleteOne(id);
getMigrationReports();
}
}
onMounted(() => {
onMounted(() => {
getMigrationReports();
});
});
const content = computed(() => {
const content = computed(() => {
const data = _content[state.migrationType];
if (data) {
@@ -507,18 +508,6 @@ export default defineNuxtComponent({
tree: false,
};
}
});
return {
...toRefs(state),
items,
content,
setFileObject,
deleteReport,
startMigration,
getMigrationReports,
};
},
});
</script>

View File

@@ -47,41 +47,31 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import { useUserApi } from "~/composables/api";
import type { ReportOut } from "~/lib/api/types/reports";
export default defineNuxtComponent({
setup() {
const route = useRoute();
const id = route.params.id as string;
const route = useRoute();
const id = route.params.id as string;
const api = useUserApi();
const api = useUserApi();
const report = ref<ReportOut | null>(null);
const report = ref<ReportOut | null>(null);
async function getReport() {
async function getReport() {
const { data } = await api.groupReports.getOne(id);
report.value = data ?? null;
}
}
onMounted(async () => {
onMounted(async () => {
await getReport();
});
});
const itemHeaders = [
const itemHeaders = [
{ title: "Success", value: "success" },
{ title: "Message", value: "message" },
{ title: "Timestamp", value: "timestamp" },
];
return {
report,
id,
itemHeaders,
};
},
});
];
</script>
<style lang="scss" scoped></style>

View File

@@ -32,105 +32,26 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import HouseholdPreferencesEditor from "~/components/Domain/Household/HouseholdPreferencesEditor.vue";
import { useHouseholdSelf } from "~/composables/use-households";
import type { ReadHouseholdPreferences } from "~/lib/api/types/household";
import { alert } from "~/composables/use-toast";
import type { VForm } from "~/types/auto-forms";
export default defineNuxtComponent({
components: {
HouseholdPreferencesEditor,
},
definePageMeta({
middleware: ["can-manage-household-only"],
setup() {
const { household, actions: householdActions } = useHouseholdSelf();
const i18n = useI18n();
});
useSeoMeta({
const { household, actions: householdActions } = useHouseholdSelf();
const i18n = useI18n();
useSeoMeta({
title: i18n.t("household.household"),
});
});
const refHouseholdEditForm = ref<VForm | null>(null);
const refHouseholdEditForm = ref<VForm | null>(null);
type Preference = {
key: keyof ReadHouseholdPreferences;
value: boolean;
label: string;
description: string;
};
const preferencesEditor = computed<Preference[]>(() => {
if (!household.value || !household.value.preferences) {
return [];
}
return [
{
key: "recipePublic",
value: household.value.preferences.recipePublic || false,
label: i18n.t("household.allow-users-outside-of-your-household-to-see-your-recipes"),
description: i18n.t("household.allow-users-outside-of-your-household-to-see-your-recipes-description"),
} as Preference,
{
key: "recipeShowNutrition",
value: household.value.preferences.recipeShowNutrition || false,
label: i18n.t("group.show-nutrition-information"),
description: i18n.t("group.show-nutrition-information-description"),
} as Preference,
{
key: "recipeShowAssets",
value: household.value.preferences.recipeShowAssets || false,
label: i18n.t("group.show-recipe-assets"),
description: i18n.t("group.show-recipe-assets-description"),
} as Preference,
{
key: "recipeLandscapeView",
value: household.value.preferences.recipeLandscapeView || false,
label: i18n.t("group.default-to-landscape-view"),
description: i18n.t("group.default-to-landscape-view-description"),
} as Preference,
{
key: "recipeDisableComments",
value: household.value.preferences.recipeDisableComments || false,
label: i18n.t("group.disable-users-from-commenting-on-recipes"),
description: i18n.t("group.disable-users-from-commenting-on-recipes-description"),
} as Preference,
];
});
const allDays = [
{
name: i18n.t("general.sunday"),
value: 0,
},
{
name: i18n.t("general.monday"),
value: 1,
},
{
name: i18n.t("general.tuesday"),
value: 2,
},
{
name: i18n.t("general.wednesday"),
value: 3,
},
{
name: i18n.t("general.thursday"),
value: 4,
},
{
name: i18n.t("general.friday"),
value: 5,
},
{
name: i18n.t("general.saturday"),
value: 6,
},
];
async function handleSubmit() {
async function handleSubmit() {
if (!refHouseholdEditForm.value?.validate() || !household.value?.preferences) {
console.log(refHouseholdEditForm.value?.validate());
return;
@@ -143,18 +64,7 @@ export default defineNuxtComponent({
else {
alert.error(i18n.t("settings.settings-update-failed"));
}
}
return {
household,
householdActions,
allDays,
preferencesEditor,
refHouseholdEditForm,
handleSubmit,
};
},
});
}
</script>
<style lang="css">

View File

@@ -86,7 +86,7 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import { isSameDay, addDays, parseISO, format, isValid } from "date-fns";
import RecipeDialogAddToShoppingList from "~/components/Domain/Recipe/RecipeDialogAddToShoppingList.vue";
import { useHouseholdSelf } from "~/composables/use-households";
@@ -95,41 +95,36 @@ import { useUserMealPlanPreferences } from "~/composables/use-users/preferences"
import type { ShoppingListSummary } from "~/lib/api/types/household";
import { useUserApi } from "~/composables/api";
export default defineNuxtComponent({
components: {
RecipeDialogAddToShoppingList,
},
setup() {
const TABS = {
const TABS = {
view: "household-mealplan-planner-view",
edit: "household-mealplan-planner-edit",
};
};
const route = useRoute();
const router = useRouter();
const i18n = useI18n();
const api = useUserApi();
const { household } = useHouseholdSelf();
const route = useRoute();
const router = useRouter();
const i18n = useI18n();
const api = useUserApi();
const { household } = useHouseholdSelf();
useSeoMeta({
useSeoMeta({
title: i18n.t("meal-plan.dinner-this-week"),
});
});
const mealPlanPreferences = useUserMealPlanPreferences();
const numberOfDays = ref<number>(mealPlanPreferences.value.numberOfDays || 7);
watch(numberOfDays, (val) => {
const mealPlanPreferences = useUserMealPlanPreferences();
const numberOfDays = ref<number>(mealPlanPreferences.value.numberOfDays || 7);
watch(numberOfDays, (val) => {
mealPlanPreferences.value.numberOfDays = Number(val);
});
});
// Force to /view if current route is /planner
if (route.path === "/household/mealplan/planner") {
// Force to /view if current route is /planner
if (route.path === "/household/mealplan/planner") {
router.push({
name: TABS.view,
query: route.query,
});
}
}
function safeParseISO(date: string, fallback: Date | undefined = undefined) {
function safeParseISO(date: string, fallback: Date | undefined = undefined) {
try {
const parsed = parseISO(date);
return isValid(parsed) ? parsed : fallback;
@@ -137,28 +132,28 @@ export default defineNuxtComponent({
catch {
return fallback;
}
}
}
// Initialize dates from query parameters or defaults
const initialStartDate = safeParseISO(route.query.start as string, new Date());
const initialEndDate = safeParseISO(route.query.end as string, addDays(new Date(), adjustForToday(numberOfDays.value)));
// Initialize dates from query parameters or defaults
const initialStartDate = safeParseISO(route.query.start as string, new Date());
const initialEndDate = safeParseISO(route.query.end as string, addDays(new Date(), adjustForToday(numberOfDays.value)));
const state = ref({
const state = ref({
range: [initialStartDate, initialEndDate] as [Date, Date],
start: initialStartDate,
picker: false,
end: initialEndDate,
shoppingListDialog: false,
addAllLoading: false,
});
});
const shoppingLists = ref<ShoppingListSummary[]>();
const shoppingLists = ref<ShoppingListSummary[]>();
const firstDayOfWeek = computed(() => {
const firstDayOfWeek = computed(() => {
return household.value?.preferences?.firstDayOfWeek || 0;
});
});
const weekRange = computed(() => {
const weekRange = computed(() => {
const sorted = [...state.value.range].sort((a, b) => a.getTime() - b.getTime());
const start = sorted[0];
@@ -171,10 +166,10 @@ export default defineNuxtComponent({
start: new Date(),
end: addDays(new Date(), adjustForToday(numberOfDays.value)),
};
});
});
// Update query parameters when date range changes
watch(weekRange, (newRange) => {
// Update query parameters when date range changes
watch(weekRange, (newRange) => {
// Keep current route name and params, just update the query
router.replace({
name: route.name || TABS.view,
@@ -185,25 +180,25 @@ export default defineNuxtComponent({
end: format(newRange.end, "yyyy-MM-dd"),
},
});
}, { immediate: true });
}, { immediate: true });
const { mealplans, actions } = useMealplans(weekRange);
const { mealplans, actions } = useMealplans(weekRange);
function filterMealByDate(date: Date) {
function filterMealByDate(date: Date) {
if (!mealplans.value) return [];
return mealplans.value.filter((meal) => {
const mealDate = parseISO(meal.date);
return isSameDay(mealDate, date);
});
}
}
function adjustForToday(days: number) {
function adjustForToday(days: number) {
// The use case for this function is "how many days are we adding to 'today'?"
// e.g. If the user wants 7 days, we substract one to do "today + 6"
return days > 0 ? days - 1 : days + 1;
}
}
const days = computed(() => {
const days = computed(() => {
const numDays
= Math.floor((weekRange.value.end.getTime() - weekRange.value.start.getTime()) / (1000 * 60 * 60 * 24)) + 1;
@@ -217,19 +212,19 @@ export default defineNuxtComponent({
return date;
},
);
});
});
const mealsByDate = computed(() => {
const mealsByDate = computed(() => {
return days.value.map((day) => {
return { date: day, meals: filterMealByDate(day) };
});
});
});
const hasRecipes = computed(() => {
const hasRecipes = computed(() => {
return mealsByDate.value.some(day => day.meals.some(meal => meal.recipe));
});
});
const weekRecipesWithScales = computed(() => {
const weekRecipesWithScales = computed(() => {
const allRecipes: any[] = [];
for (const day of mealsByDate.value) {
for (const meal of day.meals) {
@@ -242,38 +237,21 @@ export default defineNuxtComponent({
scale: 1,
...recipe,
}));
});
});
async function getShoppingLists() {
async function getShoppingLists() {
const { data } = await api.shopping.lists.getAll(1, -1, { orderBy: "name", orderDirection: "asc" });
if (data) {
shoppingLists.value = data.items as ShoppingListSummary[] ?? [];
}
}
}
async function addAllToList() {
async function addAllToList() {
state.value.addAllLoading = true;
await getShoppingLists();
state.value.shoppingListDialog = true;
state.value.addAllLoading = false;
}
return {
TABS,
route,
state,
actions,
mealsByDate,
weekRange,
firstDayOfWeek,
numberOfDays,
hasRecipes,
shoppingLists,
weekRecipesWithScales,
addAllToList,
};
},
});
}
</script>
<style lang="css">

View File

@@ -129,9 +129,9 @@
</v-icon>
</v-btn>
<v-menu offset-y>
<template #activator="{ props }">
<template #activator="{ props: menuProps }">
<v-chip
v-bind="props"
v-bind="menuProps"
label
variant="elevated"
size="small"
@@ -232,7 +232,7 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { format } from "date-fns";
import type { SortableEvent } from "sortablejs";
import { VueDraggable } from "vue-draggable-plus";
@@ -246,38 +246,27 @@ import { useHouseholdSelf } from "~/composables/use-households";
import { normalizeFilter } from "~/composables/use-utils";
import { useRecipeSearch } from "~/composables/recipes/use-recipe-search";
export default defineNuxtComponent({
components: {
VueDraggable,
RecipeCardImage,
},
props: {
mealplans: {
type: Array as () => MealsByDate[],
required: true,
},
actions: {
type: Object as () => ReturnType<typeof useMealplans>["actions"],
required: true,
},
},
setup(props) {
const api = useUserApi();
const auth = useMealieAuth();
const { household } = useHouseholdSelf();
const requiredRule = (value: any) => !!value || "Required.";
const props = defineProps<{
mealplans: MealsByDate[];
actions: ReturnType<typeof useMealplans>["actions"];
}>();
const state = ref({
const api = useUserApi();
const auth = useMealieAuth();
const { household } = useHouseholdSelf();
const requiredRule = (value: any) => !!value || "Required.";
const state = ref({
dialog: false,
});
});
const firstDayOfWeek = computed(() => {
const firstDayOfWeek = computed(() => {
return household.value?.preferences?.firstDayOfWeek || 0;
});
});
// Local mutable meals object
const mealplansByDate = reactive<{ [date: string]: UpdatePlanEntry[] }>({});
watch(
// Local mutable meals object
const mealplansByDate = reactive<{ [date: string]: UpdatePlanEntry[] }>({});
watch(
() => props.mealplans,
(plans) => {
for (const plan of plans) {
@@ -291,9 +280,9 @@ export default defineNuxtComponent({
});
},
{ immediate: true, deep: true },
);
);
function onMoveCallback(evt: SortableEvent) {
function onMoveCallback(evt: SortableEvent) {
const supportedEvents = ["drop", "touchend"];
// Adapted From https://github.com/SortableJS/Vue.Draggable/issues/1029
@@ -317,24 +306,24 @@ export default defineNuxtComponent({
props.actions.updateOne(mealData);
}
}
}
}
// =====================================================
// New Meal Dialog
// =====================================================
// New Meal Dialog
const dialog = reactive({
const dialog = reactive({
loading: false,
error: false,
note: false,
});
});
watch(dialog, () => {
watch(dialog, () => {
if (dialog.note) {
newMeal.recipeId = undefined;
}
});
});
const newMeal = reactive({
const newMeal = reactive({
date: new Date(Date.now() - new Date().getTimezoneOffset() * 60000),
title: "",
text: "",
@@ -344,25 +333,25 @@ export default defineNuxtComponent({
id: 0,
groupId: "",
userId: auth.user.value?.id || "",
});
});
const newMealDateString = computed(() => {
const newMealDateString = computed(() => {
return format(newMeal.date, "yyyy-MM-dd");
});
});
const isCreateDisabled = computed(() => {
const isCreateDisabled = computed(() => {
if (dialog.note) {
return !newMeal.title.trim();
}
return !newMeal.recipeId;
});
});
function openDialog(date: Date) {
function openDialog(date: Date) {
newMeal.date = date;
state.value.dialog = true;
}
}
function editMeal(mealplan: UpdatePlanEntry) {
function editMeal(mealplan: UpdatePlanEntry) {
const { date, title, text, entryType, recipeId, id, groupId, userId } = mealplan;
if (!entryType) return;
@@ -379,18 +368,18 @@ export default defineNuxtComponent({
state.value.dialog = true;
dialog.note = !recipeId;
}
}
function resetDialog() {
function resetDialog() {
newMeal.date = new Date(Date.now() - new Date().getTimezoneOffset() * 60000);
newMeal.title = "";
newMeal.text = "";
newMeal.entryType = "dinner";
newMeal.recipeId = undefined;
newMeal.existing = false;
}
}
async function randomMeal(date: Date, type: PlanEntryType) {
async function randomMeal(date: Date, type: PlanEntryType) {
const { data } = await api.mealplans.setRandom({
date: format(date, "yyyy-MM-dd"),
entryType: type,
@@ -399,41 +388,15 @@ export default defineNuxtComponent({
if (data) {
props.actions.refreshAll();
}
}
}
// =====================================================
// Search
// =====================================================
// Search
const search = useRecipeSearch(api);
const planTypeOptions = usePlanTypeOptions();
const search = useRecipeSearch(api);
const planTypeOptions = usePlanTypeOptions();
onMounted(async () => {
onMounted(async () => {
await search.trigger();
});
return {
state,
onMoveCallback,
planTypeOptions,
getEntryTypeText,
requiredRule,
isCreateDisabled,
normalizeFilter,
// Dialog
dialog,
newMeal,
newMealDateString,
openDialog,
editMeal,
resetDialog,
randomMeal,
// Search
search,
firstDayOfWeek,
mealplansByDate,
};
},
});
</script>

View File

@@ -50,7 +50,7 @@
</v-container>
</template>
<script lang="ts" setup>
<script setup lang="ts">
import type { MealsByDate } from "./types";
import type { ReadPlanEntry } from "~/lib/api/types/meal-plan";
import GroupMealPlanDayContextMenu from "~/components/Domain/Household/GroupMealPlanDayContextMenu.vue";

View File

@@ -171,65 +171,53 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import { useUserApi } from "~/composables/api";
import type { PlanRulesCreate, PlanRulesOut } from "~/lib/api/types/meal-plan";
import GroupMealPlanRuleForm from "~/components/Domain/Household/GroupMealPlanRuleForm.vue";
import { useAsyncKey } from "~/composables/use-utils";
import RecipeChips from "~/components/Domain/Recipe/RecipeChips.vue";
export default defineNuxtComponent({
components: {
GroupMealPlanRuleForm,
RecipeChips,
},
props: {
modelValue: {
type: Boolean,
default: false,
},
},
setup() {
const api = useUserApi();
const i18n = useI18n();
const api = useUserApi();
const i18n = useI18n();
useSeoMeta({
useSeoMeta({
title: i18n.t("meal-plan.meal-plan-settings"),
});
});
// ======================================================
// Manage All
const editState = ref<{ [key: string]: boolean }>({});
const allRules = ref<PlanRulesOut[]>([]);
// ======================================================
// Manage All
const editState = ref<{ [key: string]: boolean }>({});
const allRules = ref<PlanRulesOut[]>([]);
function toggleEditState(id: string) {
function toggleEditState(id: string) {
editState.value[id] = !editState.value[id];
editState.value = { ...editState.value };
}
}
async function refreshAll() {
async function refreshAll() {
const { data } = await api.mealplanRules.getAll();
if (data) {
allRules.value = data.items ?? [];
}
}
}
useAsyncData(useAsyncKey(), async () => {
useAsyncData(useAsyncKey(), async () => {
await refreshAll();
});
});
// ======================================================
// Creating Rules
// ======================================================
// Creating Rules
const createDataFormKey = ref(0);
const createData = ref<PlanRulesCreate>({
const createDataFormKey = ref(0);
const createData = ref<PlanRulesCreate>({
entryType: "unset",
day: "unset",
queryFilterString: "",
});
});
async function createRule() {
async function createRule() {
const { data } = await api.mealplanRules.createOne(createData.value);
if (data) {
refreshAll();
@@ -240,33 +228,20 @@ export default defineNuxtComponent({
};
createDataFormKey.value++;
}
}
}
async function deleteRule(ruleId: string) {
async function deleteRule(ruleId: string) {
const { data } = await api.mealplanRules.deleteOne(ruleId);
if (data) {
refreshAll();
}
}
}
async function updateRule(rule: PlanRulesOut) {
async function updateRule(rule: PlanRulesOut) {
const { data } = await api.mealplanRules.updateOne(rule.id, rule);
if (data) {
refreshAll();
toggleEditState(rule.id);
}
}
return {
allRules,
createDataFormKey,
createData,
createRule,
deleteRule,
editState,
updateRule,
toggleEditState,
};
},
});
}
</script>

View File

@@ -115,27 +115,21 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import { useUserApi } from "~/composables/api";
import type { UserOut } from "~/lib/api/types/user";
import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
export default defineNuxtComponent({
components: {
UserAvatar,
},
setup() {
const auth = useMealieAuth();
const api = useUserApi();
const i18n = useI18n();
const api = useUserApi();
const i18n = useI18n();
useSeoMeta({
useSeoMeta({
title: i18n.t("profile.members"),
});
});
const members = ref<UserOut[] | null[]>([]);
const members = ref<UserOut[] | null[]>([]);
const headers = [
const headers = [
{ title: "", value: "avatar", sortable: false, align: "center" },
{ title: i18n.t("user.username"), value: "username" },
{ title: i18n.t("user.full-name"), value: "fullName" },
@@ -144,16 +138,16 @@ export default defineNuxtComponent({
{ title: i18n.t("settings.organize"), value: "organize", sortable: false, align: "center" },
{ title: i18n.t("group.invite"), value: "invite", sortable: false, align: "center" },
{ title: i18n.t("group.manage-household"), value: "manageHousehold", sortable: false, align: "center" },
];
];
async function refreshMembers() {
async function refreshMembers() {
const { data } = await api.households.fetchMembers();
if (data) {
members.value = data.items;
}
}
}
async function setPermissions(user: UserOut) {
async function setPermissions(user: UserOut) {
const payload = {
userId: user.id,
canInvite: user.canInvite,
@@ -163,13 +157,9 @@ export default defineNuxtComponent({
};
await api.households.setMemberPermissions(payload);
}
}
onMounted(async () => {
onMounted(async () => {
await refreshMembers();
});
return { members, headers, setPermissions, sessionUser: auth.user };
},
});
</script>

View File

@@ -1,19 +1,19 @@
<template>
<v-container class="narrow-container">
<BaseDialog
v-model="deleteDialog"
v-model="state.deleteDialog"
color="error"
:title="$t('general.confirm')"
:icon="$globals.icons.alertCircle"
can-confirm
@confirm="deleteNotifier(deleteTargetId)"
@confirm="deleteNotifier(state.deleteTargetId)"
>
<v-card-text>
{{ $t("general.confirm-delete-generic") }}
</v-card-text>
</BaseDialog>
<BaseDialog
v-model="createDialog"
v-model="state.createDialog"
:title="$t('events.new-notification')"
:icon="$globals.icons.bellPlus"
can-submit
@@ -83,7 +83,7 @@
<BaseButton
create
@click="createDialog = true"
@click="state.createDialog = true"
/>
<v-expansion-panels
v-if="notifiers"
@@ -182,7 +182,7 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import { useUserApi } from "~/composables/api";
import { useAsyncKey } from "~/composables/use-utils";
import type { GroupEventNotifierCreate, GroupEventNotifierOut } from "~/lib/api/types/household";
@@ -198,67 +198,68 @@ interface OptionSection {
options: OptionKey[];
}
export default defineNuxtComponent({
definePageMeta({
middleware: ["advanced-only"],
setup() {
const api = useUserApi();
const i18n = useI18n();
});
useSeoMeta({
const api = useUserApi();
const i18n = useI18n();
useSeoMeta({
title: i18n.t("profile.notifiers"),
});
});
const state = reactive({
const state = reactive({
deleteDialog: false,
createDialog: false,
deleteTargetId: "",
});
});
const { data: notifiers } = useAsyncData(useAsyncKey(), async () => {
const { data: notifiers } = useAsyncData(useAsyncKey(), async () => {
const { data } = await api.groupEventNotifier.getAll();
return data?.items;
});
});
async function refreshNotifiers() {
async function refreshNotifiers() {
const { data } = await api.groupEventNotifier.getAll();
notifiers.value = data?.items;
}
}
const createNotifierData: GroupEventNotifierCreate = reactive({
const createNotifierData: GroupEventNotifierCreate = reactive({
name: "",
enabled: true,
appriseUrl: "",
});
});
async function createNewNotifier() {
async function createNewNotifier() {
await api.groupEventNotifier.createOne(createNotifierData);
refreshNotifiers();
}
}
function openDelete(notifier: GroupEventNotifierOut) {
function openDelete(notifier: GroupEventNotifierOut) {
state.deleteDialog = true;
state.deleteTargetId = notifier.id;
}
}
async function deleteNotifier(targetId: string) {
async function deleteNotifier(targetId: string) {
await api.groupEventNotifier.deleteOne(targetId);
refreshNotifiers();
state.deleteTargetId = "";
}
}
async function saveNotifier(notifier: GroupEventNotifierOut) {
async function saveNotifier(notifier: GroupEventNotifierOut) {
await api.groupEventNotifier.updateOne(notifier.id, notifier);
refreshNotifiers();
}
}
async function testNotifier(notifier: GroupEventNotifierOut) {
async function testNotifier(notifier: GroupEventNotifierOut) {
await api.groupEventNotifier.test(notifier.id);
}
}
// ===============================================================
// Options Definitions
// ===============================================================
// Options Definitions
const optionsSections: OptionSection[] = [
const optionsSections: OptionSection[] = [
{
id: 1,
text: i18n.t("events.recipe-events"),
@@ -387,21 +388,7 @@ export default defineNuxtComponent({
},
],
},
];
return {
...toRefs(state),
openDelete,
notifiers,
createNotifierData,
optionsSections,
deleteNotifier,
testNotifier,
saveNotifier,
createNewNotifier,
};
},
});
];
</script>
<style>

View File

@@ -68,28 +68,19 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import { useGroupWebhooks, timeUTC } from "~/composables/use-group-webhooks";
import GroupWebhookEditor from "~/components/Domain/Household/GroupWebhookEditor.vue";
import { alert } from "~/composables/use-toast";
export default defineNuxtComponent({
components: { GroupWebhookEditor },
definePageMeta({
middleware: ["advanced-only"],
setup() {
const i18n = useI18n();
const { actions, webhooks } = useGroupWebhooks();
});
useSeoMeta({
const i18n = useI18n();
const { actions, webhooks } = useGroupWebhooks();
useSeoMeta({
title: i18n.t("settings.webhooks.webhooks"),
});
return {
alert,
webhooks,
actions,
timeUTC,
};
},
});
</script>

View File

@@ -2,26 +2,24 @@
<div />
</template>
<script lang="ts">
<script setup lang="ts">
import useDefaultActivity from "~/composables/use-default-activity";
import { useUserActivityPreferences } from "~/composables/use-users/preferences";
import { useAsyncKey } from "~/composables/use-utils";
import type { AppInfo, AppStartupInfo } from "~/lib/api/types/admin";
export default defineNuxtComponent({
setup() {
definePageMeta({
definePageMeta({
layout: "blank",
});
});
const auth = useMealieAuth();
const { $axios } = useNuxtApp();
const router = useRouter();
const activityPreferences = useUserActivityPreferences();
const { getDefaultActivityRoute } = useDefaultActivity();
const groupSlug = computed(() => auth.user.value?.groupSlug);
const auth = useMealieAuth();
const { $axios } = useNuxtApp();
const router = useRouter();
const activityPreferences = useUserActivityPreferences();
const { getDefaultActivityRoute } = useDefaultActivity();
const groupSlug = computed(() => auth.user.value?.groupSlug);
async function redirectPublicUserToDefaultGroup() {
async function redirectPublicUserToDefaultGroup() {
const { data } = await $axios.get<AppInfo>("/api/app/about");
if (data?.defaultGroupSlug) {
router.push(`/g/${data.defaultGroupSlug}`);
@@ -29,9 +27,9 @@ export default defineNuxtComponent({
else {
router.push("/login");
}
}
}
useAsyncData(useAsyncKey(), async () => {
useAsyncData(useAsyncKey(), async () => {
if (groupSlug.value) {
const data = await $axios.get<AppStartupInfo>("/api/app/about/startup-info");
const isDemo = data.data.isDemo;
@@ -53,7 +51,5 @@ export default defineNuxtComponent({
else {
redirectPublicUserToDefaultGroup();
}
});
},
});
</script>

View File

@@ -210,7 +210,7 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import { useDark, whenever } from "@vueuse/core";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import { usePasswordField } from "~/composables/use-passwords";
@@ -219,35 +219,33 @@ import { useAsyncKey } from "~/composables/use-utils";
import type { AppStartupInfo } from "~/lib/api/types/admin";
import { useUserActivityPreferences } from "~/composables/use-users/preferences";
export default defineNuxtComponent({
setup() {
definePageMeta({
definePageMeta({
layout: "blank",
});
const isDark = useDark();
});
const isDark = useDark();
const router = useRouter();
const i18n = useI18n();
const auth = useMealieAuth();
const { $appInfo, $axios } = useNuxtApp();
const { loggedIn } = useLoggedInState();
const groupSlug = computed(() => auth.user.value?.groupSlug);
const isDemo = ref(false);
const isFirstLogin = ref(false);
const activityPreferences = useUserActivityPreferences();
const { getDefaultActivityRoute } = useDefaultActivity();
const router = useRouter();
const i18n = useI18n();
const auth = useMealieAuth();
const { $appInfo, $axios } = useNuxtApp();
const { loggedIn } = useLoggedInState();
const groupSlug = computed(() => auth.user.value?.groupSlug);
const isDemo = ref(false);
const isFirstLogin = ref(false);
const activityPreferences = useUserActivityPreferences();
const { getDefaultActivityRoute } = useDefaultActivity();
useSeoMeta({
useSeoMeta({
title: i18n.t("user.login"),
});
});
const form = reactive({
const form = reactive({
email: "",
password: "",
remember: false,
});
});
useAsyncData(useAsyncKey(), async () => {
useAsyncData(useAsyncKey(), async () => {
const data = await $axios.get<AppStartupInfo>("/api/app/about/startup-info");
isDemo.value = data.data.isDemo;
isFirstLogin.value = data.data.isFirstLogin;
@@ -256,9 +254,9 @@ export default defineNuxtComponent({
form.email = "changeme@example.com";
form.password = "MyPassword";
}
});
});
whenever(
whenever(
() => loggedIn.value && groupSlug.value,
() => {
const defaultActivityRoute = getDefaultActivityRoute(
@@ -276,36 +274,36 @@ export default defineNuxtComponent({
}
},
{ immediate: true },
);
);
const loggingIn = ref(false);
const oidcLoggingIn = ref(false);
const loggingIn = ref(false);
const oidcLoggingIn = ref(false);
const { passwordIcon, inputType, togglePasswordShow } = usePasswordField();
const { passwordIcon, inputType, togglePasswordShow } = usePasswordField();
whenever(
whenever(
() => $appInfo.enableOidc && $appInfo.oidcRedirect && !isCallback() && !isDirectLogin() /* && !auth.check().valid */,
() => oidcAuthenticate(),
{ immediate: true },
);
);
onBeforeMount(async () => {
onBeforeMount(async () => {
if (isCallback()) {
await oidcAuthenticate(true);
}
});
});
function isCallback() {
function isCallback() {
const params = new URLSearchParams(window.location.search);
return params.has("code") || params.has("error");
}
}
function isDirectLogin() {
function isDirectLogin() {
const params = new URLSearchParams(window.location.search);
return params.has("direct") && params.get("direct") === "1";
}
}
async function oidcAuthenticate(callback = false) {
async function oidcAuthenticate(callback = false) {
if (callback) {
oidcLoggingIn.value = true;
try {
@@ -320,9 +318,9 @@ export default defineNuxtComponent({
else {
navigateTo("/api/auth/oauth", { external: true }); // start the redirect process
}
}
}
async function authenticate() {
async function authenticate() {
if (form.email.length === 0 || form.password.length === 0) {
alert.error(i18n.t("user.please-enter-your-email-and-password"));
return;
@@ -342,9 +340,9 @@ export default defineNuxtComponent({
alertOnError(error);
}
loggingIn.value = false;
}
}
function alertOnError(error: any) {
function alertOnError(error: any) {
// TODO Check if error is an AxiosError, but isAxiosError is not working right now
// See https://github.com/nuxt-community/axios-module/issues/550
// Import $axios from useContext()
@@ -358,22 +356,7 @@ export default defineNuxtComponent({
else {
alert.error(i18n.t("events.something-went-wrong"));
}
}
return {
isDark,
form,
loggingIn,
authenticate,
oidcAuthenticate,
oidcLoggingIn,
passwordIcon,
inputType,
togglePasswordShow,
isFirstLogin,
};
},
});
}
</script>
<style lang="css" scoped>

View File

@@ -298,7 +298,7 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import { useDark } from "@vueuse/core";
import { States, RegistrationType, useRegistration } from "./states";
import { useUserRegistrationForm } from "~/composables/use-users/user-registration-form";
@@ -307,50 +307,39 @@ import { validators, useAsyncValidator } from "~/composables/use-validators";
import { useUserApi } from "~/composables/api";
import { alert } from "~/composables/use-toast";
import type { CreateUserRegistration } from "~/lib/api/types/user";
import { usePasswordField } from "~/composables/use-passwords";
import { usePublicApi } from "~/composables/api/api-client";
import { useLocales } from "~/composables/use-locales";
import UserRegistrationForm from "~/components/Domain/User/UserRegistrationForm.vue";
import type { VForm } from "~/types/auto-forms";
definePageMeta({
layout: "blank",
});
const inputAttrs = {
variant: "filled",
validateOnBlur: true,
};
export default defineNuxtComponent({
components: { UserRegistrationForm },
setup() {
definePageMeta({
layout: "blank",
});
const i18n = useI18n();
const isDark = useDark();
const i18n = useI18n();
const isDark = useDark();
function safeValidate(form: Ref<VForm | null>) {
function safeValidate(form: Ref<VForm | null>) {
if (form.value && form.value.validate) {
return form.value.validate();
}
return false;
}
// ================================================================
// Registration Context
//
// State is used to manage the registration process states and provide
// a state machine esq interface to interact with the registration workflow.
const state = useRegistration();
// ================================================================
// Handle Token URL / Initialization
//
const token = useRouteQuery("token");
// TODO: We need to have some way to check to see if the site is in a state
// Where it needs to be initialized with a user, in that case we'll handle that
// somewhere...
function initialUser() {
}
// Registration Context
const state = useRegistration();
// Handle Token URL / Initialization
const token = useRouteQuery("token");
function initialUser() {
return false;
}
onMounted(() => {
}
onMounted(() => {
if (token.value) {
state.setState(States.ProvideAccountDetails);
state.setType(RegistrationType.JoinGroup);
@@ -359,10 +348,10 @@ export default defineNuxtComponent({
state.setState(States.ProvideGroupDetails);
state.setType(RegistrationType.InitialGroup);
}
});
// ================================================================
// Initial
const initial = {
});
// Initial
const initial = {
createGroup: () => {
state.setState(States.ProvideGroupDetails);
state.setType(RegistrationType.CreateGroup);
@@ -374,14 +363,14 @@ export default defineNuxtComponent({
state.setState(States.ProvideToken);
state.setType(RegistrationType.JoinGroup);
},
};
// ================================================================
// Provide Token
const domTokenForm = ref<VForm | null>(null);
function validateToken() {
};
// Provide Token
const domTokenForm = ref<VForm | null>(null);
function validateToken() {
return true;
}
const provideToken = {
}
const provideToken = {
next: () => {
if (!safeValidate(domTokenForm as Ref<VForm>)) {
return;
@@ -390,22 +379,22 @@ export default defineNuxtComponent({
state.setState(States.ProvideAccountDetails);
}
},
};
// ================================================================
// Provide Group Details
const publicApi = usePublicApi();
const domGroupForm = ref<VForm | null>(null);
const groupName = ref("");
const groupSeed = ref(false);
const groupPrivate = ref(false);
const groupErrorMessages = ref<string[]>([]);
const { validate: validGroupName, valid: groupNameValid } = useAsyncValidator(
};
// Provide Group Details
const publicApi = usePublicApi();
const domGroupForm = ref<VForm | null>(null);
const groupName = ref("");
const groupSeed = ref(false);
const groupPrivate = ref(false);
const groupErrorMessages = ref<string[]>([]);
const { validate: validGroupName, valid: groupNameValid } = useAsyncValidator(
groupName,
(v: string) => publicApi.validators.group(v),
i18n.t("validation.group-name-is-taken"),
groupErrorMessages,
);
const groupDetails = {
);
const groupDetails = {
groupName,
groupSeed,
groupPrivate,
@@ -415,30 +404,26 @@ export default defineNuxtComponent({
}
state.setState(States.ProvideAccountDetails);
},
};
const pwFields = usePasswordField();
const {
};
const {
accountDetails,
credentials,
domAccountForm,
emailErrorMessages,
usernameErrorMessages,
validateUsername,
validateEmail,
} = useUserRegistrationForm();
async function accountDetailsNext() {
} = useUserRegistrationForm();
async function accountDetailsNext() {
if (!await accountDetails.validate()) {
return;
}
state.setState(States.Confirmation);
}
// ================================================================
// Locale
const { locale } = useLocales();
const langDialog = ref(false);
// ================================================================
// Confirmation
const confirmationData = computed(() => {
}
// Locale
const { locale } = useLocales();
const langDialog = ref(false);
// Confirmation
const confirmationData = computed(() => {
return [
{
display: state.ctx.type === RegistrationType.CreateGroup,
@@ -476,10 +461,11 @@ export default defineNuxtComponent({
value: accountDetails.advancedOptions.value ? i18n.t("general.yes") : i18n.t("general.no"),
},
];
});
const api = useUserApi();
const router = useRouter();
async function submitRegistration() {
});
const api = useUserApi();
const router = useRouter();
async function submitRegistration() {
const payload: CreateUserRegistration = {
email: accountDetails.email.value,
username: accountDetails.username.value,
@@ -507,39 +493,7 @@ export default defineNuxtComponent({
else {
alert.error(i18n.t("events.something-went-wrong"));
}
}
return {
accountDetails,
accountDetailsNext,
confirmationData,
credentials,
emailErrorMessages,
groupDetails,
groupErrorMessages,
initial,
inputAttrs,
isDark,
langDialog,
provideToken,
pwFields,
RegistrationType,
state,
States,
token,
usernameErrorMessages,
validators,
submitRegistration,
// Validators
validGroupName,
validateUsername,
validateEmail,
// Dom Refs
domAccountForm,
domGroupForm,
domTokenForm,
};
},
});
}
</script>
<style lang="css" scoped>

View File

@@ -16,7 +16,7 @@
<v-card-text>
<v-form @submit.prevent="requestLink()">
<v-text-field
v-model="email"
v-model="state.email"
:prepend-inner-icon="$globals.icons.email"
variant="solo-filled"
flat
@@ -26,7 +26,7 @@
type="text"
/>
<v-text-field
v-model="password"
v-model="state.password"
variant="solo-filled"
flat
:prepend-inner-icon="$globals.icons.lock"
@@ -36,7 +36,7 @@
:rules="[validators.required]"
/>
<v-text-field
v-model="passwordConfirm"
v-model="state.passwordConfirm"
variant="solo-filled"
flat
validate-on="blur"
@@ -52,7 +52,7 @@
<v-card-actions class="justify-center">
<div class="max-button">
<v-btn
:loading="loading"
:loading="state.loading"
color="primary"
:disabled="token === ''"
type="submit"
@@ -81,42 +81,40 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import { useUserApi } from "~/composables/api";
import { alert } from "~/composables/use-toast";
import { validators } from "@/composables/use-validators";
import { useRouteQuery } from "~/composables/use-router";
export default defineNuxtComponent({
setup() {
definePageMeta({
definePageMeta({
layout: "basic",
});
});
const state = reactive({
const state = reactive({
email: "",
password: "",
passwordConfirm: "",
loading: false,
error: false,
});
});
const i18n = useI18n();
const passwordMatch = () => state.password === state.passwordConfirm || i18n.t("user.password-must-match");
const i18n = useI18n();
const passwordMatch = () => state.password === state.passwordConfirm || i18n.t("user.password-must-match");
// Set page title
useSeoMeta({
// Set page title
useSeoMeta({
title: i18n.t("user.login"),
});
});
// ===================
// Token Getter
const token = useRouteQuery("token", "");
// ===================
// Token Getter
const token = useRouteQuery("token", "");
// ===================
// API
const api = useUserApi();
async function requestLink() {
// ===================
// API
const api = useUserApi();
async function requestLink() {
state.loading = true;
// TODO: Fix Response to send meaningful error
const { response } = await api.users.resetPassword({
@@ -138,17 +136,7 @@ export default defineNuxtComponent({
state.error = true;
alert.error(i18n.t("events.something-went-wrong"));
}
}
return {
passwordMatch,
token,
requestLink,
validators,
...toRefs(state),
};
},
});
}
</script>
<style lang="css">

View File

@@ -331,7 +331,7 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import { VueDraggable } from "vue-draggable-plus";
import RecipeList from "~/components/Domain/Recipe/RecipeList.vue";
import MultiPurposeLabelSection from "~/components/Domain/ShoppingList/MultiPurposeLabelSection.vue";
@@ -339,48 +339,56 @@ import ShoppingListItem from "~/components/Domain/ShoppingList/ShoppingListItem.
import ShoppingListItemEditor from "~/components/Domain/ShoppingList/ShoppingListItemEditor.vue";
import { useShoppingListPage } from "~/composables/shopping-list-page/use-shopping-list-page";
import { useFoodStore, useLabelStore, useUnitStore } from "~/composables/store";
import { getTextColor } from "~/composables/use-text-color";
import { useShoppingListPreferences } from "~/composables/use-users/preferences";
export default defineNuxtComponent({
components: {
VueDraggable,
MultiPurposeLabelSection,
ShoppingListItem,
RecipeList,
ShoppingListItemEditor,
},
setup() {
const { mdAndUp } = useDisplay();
const i18n = useI18n();
const auth = useMealieAuth();
const preferences = useShoppingListPreferences();
const { mdAndUp } = useDisplay();
const i18n = useI18n();
useSeoMeta({
useSeoMeta({
title: i18n.t("shopping-list.shopping-list"),
});
const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
const id = route.params.id as string;
const shoppingListPage = useShoppingListPage(id);
const { store: allLabels } = useLabelStore();
const { store: allUnits } = useUnitStore();
const { store: allFoods } = useFoodStore();
return {
groupSlug,
preferences,
allLabels,
allUnits,
allFoods,
getTextColor,
mdAndUp,
...shoppingListPage,
};
},
});
const route = useRoute();
const id = route.params.id as string;
const shoppingListPage = useShoppingListPage(id);
const { store: allLabels } = useLabelStore();
const { store: allUnits } = useUnitStore();
const { store: allFoods } = useFoodStore();
const {
shoppingList,
state,
checkAll,
uncheckAll,
deleteChecked,
reorderLabelsDialog,
localLabels,
saveLabelOrder,
cancelLabelOrder,
updateLabelOrder,
edit,
threeDot,
openCheckAll,
copyListItems,
toggleReorderLabelsDialog,
isOffline,
createEditorOpen,
createListItemData,
createListItem,
itemsByLabel,
getLabelColor,
loadingCounter,
updateIndexUncheckedByLabel,
recipeMap,
saveListItem,
deleteListItem,
listItems,
openUncheckAll,
openDeleteChecked,
recipeList,
removeRecipeReferenceToList,
addRecipeReferenceToList,
} = shoppingListPage;
</script>
<style>

View File

@@ -4,14 +4,14 @@
class="narrow-container"
>
<BaseDialog
v-model="createDialog"
v-model="state.createDialog"
:title="$t('shopping-list.create-shopping-list')"
can-submit
@submit="createOne"
>
<v-card-text>
<v-text-field
v-model="createName"
v-model="state.createName"
autofocus
:label="$t('shopping-list.new-list')"
/>
@@ -20,7 +20,7 @@
<!-- Settings -->
<BaseDialog
v-model="ownerDialog"
v-model="state.ownerDialog"
:icon="$globals.icons.admin"
:title="$t('user.edit-user')"
can-confirm
@@ -41,7 +41,7 @@
</BaseDialog>
<BaseDialog
v-model="deleteDialog"
v-model="state.deleteDialog"
:title="$t('general.confirm')"
color="error"
can-confirm
@@ -73,7 +73,7 @@
<BaseButton
create
class="my-0"
@click="createDialog = true"
@click="state.createDialog = true"
/>
</v-container>
@@ -123,60 +123,57 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import type { ShoppingListOut } from "~/lib/api/types/household";
import { useUserApi } from "~/composables/api";
import { useAsyncKey } from "~/composables/use-utils";
import { useShoppingListPreferences } from "~/composables/use-users/preferences";
import type { UserOut } from "~/lib/api/types/user";
export default defineNuxtComponent({
setup() {
const auth = useMealieAuth();
const i18n = useI18n();
const ready = ref(false);
const userApi = useUserApi();
const route = useRoute();
const auth = useMealieAuth();
const i18n = useI18n();
const ready = ref(false);
const userApi = useUserApi();
const route = useRoute();
useSeoMeta({
useSeoMeta({
title: i18n.t("shopping-list.shopping-list"),
});
});
const groupSlug = computed(() => route.params.groupSlug || auth.user.value?.groupSlug || "");
const overrideDisableRedirect = ref(false);
const disableRedirect = computed(() => route.query.disableRedirect === "true" || overrideDisableRedirect.value);
const preferences = useShoppingListPreferences();
const overrideDisableRedirect = ref(false);
const disableRedirect = computed(() => route.query.disableRedirect === "true" || overrideDisableRedirect.value);
const preferences = useShoppingListPreferences();
const state = reactive({
const state = reactive({
createName: "",
createDialog: false,
deleteDialog: false,
deleteTarget: "",
ownerDialog: false,
ownerTarget: ref<ShoppingListOut | null>(null),
});
});
const { data: shoppingLists } = useAsyncData(useAsyncKey(), async () => {
const { data: shoppingLists } = useAsyncData(useAsyncKey(), async () => {
return await fetchShoppingLists();
});
});
const shoppingListChoices = computed(() => {
const shoppingListChoices = computed(() => {
if (!shoppingLists.value) {
return [];
}
return shoppingLists.value.filter(list => preferences.value.viewAllLists || list.userId === auth.user.value?.id);
});
});
// This has to appear before the shoppingListChoices watcher, otherwise that runs first and the redirect is not disabled
watch(
// This has to appear before the shoppingListChoices watcher, otherwise that runs first and the redirect is not disabled
watch(
() => preferences.value.viewAllLists,
() => {
overrideDisableRedirect.value = true;
},
);
);
watch(
watch(
() => shoppingListChoices,
() => {
if (!disableRedirect.value && shoppingListChoices.value.length === 1) {
@@ -189,9 +186,9 @@ export default defineNuxtComponent({
{
deep: true,
},
);
);
async function fetchShoppingLists() {
async function fetchShoppingLists() {
const { data } = await userApi.shopping.lists.getAll(1, -1, { orderBy: "name", orderDirection: "asc" });
if (!data) {
@@ -199,35 +196,35 @@ export default defineNuxtComponent({
}
return data.items;
}
}
async function refresh() {
async function refresh() {
shoppingLists.value = await fetchShoppingLists();
}
}
async function createOne() {
async function createOne() {
const { data } = await userApi.shopping.lists.createOne({ name: state.createName });
if (data) {
refresh();
state.createName = "";
}
}
}
async function toggleOwnerDialog(list: ShoppingListOut) {
async function toggleOwnerDialog(list: ShoppingListOut) {
if (!state.ownerDialog) {
state.ownerTarget = list;
await fetchAllUsers();
}
state.ownerDialog = !state.ownerDialog;
}
}
// ===============================================================
// Shopping List Edit User/Owner
// ===============================================================
// Shopping List Edit User/Owner
const allUsers = ref<UserOut[]>([]);
const updateUserId = ref<string | undefined>();
async function fetchAllUsers() {
const allUsers = ref<UserOut[]>([]);
const updateUserId = ref<string | undefined>();
async function fetchAllUsers() {
const { data } = await userApi.households.fetchMembers();
if (!data) {
return;
@@ -236,9 +233,9 @@ export default defineNuxtComponent({
// update current user
allUsers.value = data.items.sort((a, b) => ((a.fullName || "") < (b.fullName || "") ? -1 : 1));
updateUserId.value = state.ownerTarget?.userId;
}
}
async function updateOwner() {
async function updateOwner() {
if (!state.ownerTarget || !updateUserId.value) {
return;
}
@@ -259,34 +256,17 @@ export default defineNuxtComponent({
if (data) {
refresh();
}
}
}
function openDelete(id: string) {
function openDelete(id: string) {
state.deleteDialog = true;
state.deleteTarget = id;
}
}
async function deleteOne() {
async function deleteOne() {
const { data } = await userApi.shopping.lists.deleteOne(state.deleteTarget);
if (data) {
refresh();
}
}
return {
...toRefs(state),
ready,
groupSlug,
preferences,
shoppingListChoices,
createOne,
toggleOwnerDialog,
allUsers,
updateUserId,
updateOwner,
deleteOne,
openDelete,
};
},
});
}
</script>

View File

@@ -14,37 +14,22 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
import { useLazyRecipes } from "~/composables/recipes";
import { useLoggedInState } from "~/composables/use-logged-in-state";
export default defineNuxtComponent({
components: { RecipeCardSection },
setup() {
const route = useRoute();
const i18n = useI18n();
const { isOwnGroup } = useLoggedInState();
const route = useRoute();
const i18n = useI18n();
const { isOwnGroup } = useLoggedInState();
useSeoMeta({
useSeoMeta({
title: i18n.t("general.favorites"),
});
const userId = route.params.id;
const query = { queryFilter: `favoritedBy.id = "${userId}"` };
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes();
return {
query,
recipes,
isOwnGroup,
appendRecipes,
assignSorted,
removeRecipe,
replaceRecipes,
};
},
});
const userId = route.params.id;
const query = { queryFilter: `favoritedBy.id = "${userId}"` };
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes();
</script>
<style scoped></style>

View File

@@ -106,40 +106,41 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import { useUserApi } from "~/composables/api";
import type { VForm } from "~/types/auto-forms";
export default defineNuxtComponent({
definePageMeta({
middleware: ["advanced-only"],
setup() {
const i18n = useI18n();
const auth = useMealieAuth();
});
useSeoMeta({
const i18n = useI18n();
const auth = useMealieAuth();
useSeoMeta({
title: i18n.t("settings.token.api-tokens"),
});
});
const user = computed(() => {
const user = computed(() => {
return auth.user.value;
});
});
const api = useUserApi();
const api = useUserApi();
const domNewTokenForm = ref<VForm | null>(null);
const domNewTokenForm = ref<VForm | null>(null);
const createdToken = ref("");
const name = ref("");
const loading = ref(false);
const createdToken = ref("");
const name = ref("");
const loading = ref(false);
function resetCreate() {
function resetCreate() {
createdToken.value = "";
loading.value = false;
name.value = "";
auth.refresh();
}
}
async function createToken(name: string) {
async function createToken(name: string) {
if (loading.value) {
resetCreate();
return;
@@ -157,15 +158,11 @@ export default defineNuxtComponent({
if (data) {
createdToken.value = data.token;
}
}
}
async function deleteToken(id: number) {
async function deleteToken(id: number) {
const { data } = await api.users.deleteAPIToken(id);
auth.refresh();
return data;
}
return { createToken, deleteToken, createdToken, loading, name, user, resetCreate };
},
});
}
</script>

View File

@@ -24,9 +24,9 @@
<section class="mt-5">
<ToggleState tag="article">
<template #activator="{ toggle, state }">
<template #activator="{ toggle, modelValue: toggleState }">
<v-btn
v-if="!state && $appInfo.allowPasswordLogin"
v-if="!toggleState && $appInfo.allowPasswordLogin"
color="info"
class="mt-2 mb-n3"
@click="toggle"
@@ -48,13 +48,13 @@
{{ $t("settings.profile") }}
</v-btn>
</template>
<template #default="{ state }">
<template #default="{ modelValue: toggleState }">
<v-slide-x-transition
leave-absolute
hide-on-leave
>
<div
v-if="!state"
v-if="!toggleState"
key="personal-info"
>
<BaseCardSectionTitle
@@ -214,64 +214,74 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import { useUserApi } from "~/composables/api";
import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
import UserPasswordStrength from "~/components/Domain/User/UserPasswordStrength.vue";
import { validators } from "~/composables/use-validators";
import type { VForm } from "~/types/auto-forms";
import { useUserActivityPreferences } from "~/composables/use-users/preferences";
import useDefaultActivity from "~/composables/use-default-activity";
import { ActivityKey } from "~/lib/api/types/activity";
import type { UserBase } from "~/lib/api/types/user";
export default defineNuxtComponent({
components: {
UserAvatar,
UserPasswordStrength,
},
setup() {
const i18n = useI18n();
const auth = useMealieAuth();
const { getDefaultActivityLabels, getActivityLabel, getActivityKey } = useDefaultActivity();
const user = computed(() => auth.user.value);
const i18n = useI18n();
const auth = useMealieAuth();
const { getDefaultActivityLabels, getActivityLabel, getActivityKey } = useDefaultActivity();
const user = computed(() => auth.user.value);
useSeoMeta({
useSeoMeta({
title: i18n.t("settings.profile"),
});
});
const activityPreferences = useUserActivityPreferences();
const activityOptions = getDefaultActivityLabels(i18n);
const selectedDefaultActivity = ref(getActivityLabel(i18n, activityPreferences.value.defaultActivity));
watch(selectedDefaultActivity, () => {
const activityPreferences = useUserActivityPreferences();
const activityOptions = getDefaultActivityLabels(i18n);
const selectedDefaultActivity = ref(getActivityLabel(i18n, activityPreferences.value.defaultActivity));
watch(selectedDefaultActivity, () => {
activityPreferences.value.defaultActivity = getActivityKey(i18n, selectedDefaultActivity.value) ?? ActivityKey.RECIPES;
});
});
watch(user, () => {
const userCopy = ref({ ...user.value });
watch(user, () => {
userCopy.value = { ...user.value };
});
});
const userCopy = ref({ ...user.value });
const api = useUserApi();
const domUpdatePassword = ref<VForm | null>(null);
const password = reactive({
const api = useUserApi();
const showPassword = ref(false);
const password = reactive({
current: "",
newOne: "",
newTwo: "",
});
});
const passwordsMatch = computed(() => password.newOne === password.newTwo && password.newOne.length > 0);
const passwordsMatch = computed(() => password.newOne === password.newTwo && password.newOne.length > 0);
async function updateUser() {
if (!userCopy.value?.id) return;
const { response } = await api.users.updateOne(userCopy.value.id, userCopy.value);
async function updateUser() {
const userData = userCopy.value;
if (!userData?.id || !userData.email) return;
const updatePayload: UserBase = {
id: userData.id,
username: userData.username,
fullName: userData.fullName,
email: userData.email,
authMethod: userData.authMethod,
admin: userData.admin,
group: userData.group,
household: userData.household,
advanced: userData.advanced,
canInvite: userData.canInvite,
canManage: userData.canManage,
canManageHousehold: userData.canManageHousehold,
canOrganize: userData.canOrganize,
};
const { response } = await api.users.updateOne(userData.id, updatePayload);
if (response?.status === 200) {
auth.refresh();
}
}
}
async function updatePassword() {
async function updatePassword() {
if (!userCopy.value?.id) {
return;
}
@@ -283,28 +293,5 @@ export default defineNuxtComponent({
if (response?.status === 200) {
console.log("Password Changed");
}
}
const state = reactive({
hideImage: false,
passwordLoading: false,
showPassword: false,
loading: false,
});
return {
...toRefs(state),
updateUser,
updatePassword,
userCopy,
selectedDefaultActivity,
activityOptions,
password,
domUpdatePassword,
passwordsMatch,
validators,
auth,
};
},
});
}
</script>

View File

@@ -274,7 +274,7 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import UserProfileLinkCard from "@/components/Domain/User/UserProfileLinkCard.vue";
import { useUserApi } from "~/composables/api";
import UserAvatar from "@/components/Domain/User/UserAvatar.vue";
@@ -283,27 +283,22 @@ import StatsCards from "~/components/global/StatsCards.vue";
import type { UserOut } from "~/lib/api/types/user";
import UserInviteDialog from "~/components/Domain/User/UserInviteDialog.vue";
export default defineNuxtComponent({
definePageMeta({
name: "UserProfile",
components: {
UserInviteDialog,
UserProfileLinkCard,
UserAvatar,
StatsCards,
},
scrollToTop: true,
async setup() {
const i18n = useI18n();
const auth = useMealieAuth();
const { $appInfo } = useNuxtApp();
const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug || auth.user.value?.groupSlug || "");
});
useSeoMeta({
const i18n = useI18n();
const auth = useMealieAuth();
const { $appInfo } = useNuxtApp();
const route = useRoute();
const groupSlug = computed(() => route.params.groupSlug || auth.user.value?.groupSlug || "");
useSeoMeta({
title: i18n.t("settings.profile"),
});
});
const user = computed<UserOut | null>(() => {
const user = computed<UserOut | null>(() => {
const authUser = auth.user.value;
if (!authUser) return null;
@@ -314,45 +309,45 @@ export default defineNuxtComponent({
...authUser,
canInvite,
};
});
});
const inviteDialog = ref(false);
const api = useUserApi();
const inviteDialog = ref(false);
const api = useUserApi();
const { data: stats } = useAsyncData(useAsyncKey(), async () => {
const { data: stats } = useAsyncData(useAsyncKey(), async () => {
const { data } = await api.households.statistics();
if (data) {
return data;
}
});
});
const statsText: { [key: string]: string } = {
const statsText: { [key: string]: string } = {
totalRecipes: i18n.t("general.recipes"),
totalUsers: i18n.t("user.users"),
totalCategories: i18n.t("sidebar.categories"),
totalTags: i18n.t("sidebar.tags"),
totalTools: i18n.t("tool.tools"),
};
};
function getStatsTitle(key: string) {
function getStatsTitle(key: string) {
return statsText[key] ?? "unknown";
}
}
const { $globals } = useNuxtApp();
const { $globals } = useNuxtApp();
const iconText: { [key: string]: string } = {
const iconText: { [key: string]: string } = {
totalUsers: $globals.icons.user,
totalCategories: $globals.icons.categories,
totalTags: $globals.icons.tags,
totalTools: $globals.icons.potSteam,
};
};
function getStatsIcon(key: string) {
function getStatsIcon(key: string) {
return iconText[key] ?? $globals.icons.primary;
}
}
const statsTo = computed<{ [key: string]: string }>(() => {
const statsTo = computed<{ [key: string]: string }>(() => {
return {
totalRecipes: `/g/${groupSlug.value}/`,
totalUsers: "/household/members",
@@ -360,21 +355,9 @@ export default defineNuxtComponent({
totalTags: `/g/${groupSlug.value}/recipes/tags`,
totalTools: `/g/${groupSlug.value}/recipes/tools`,
};
});
function getStatsTo(key: string) {
return statsTo.value[key] ?? "unknown";
}
return {
groupSlug,
getStatsTitle,
getStatsIcon,
getStatsTo,
inviteDialog,
stats,
user,
};
},
});
function getStatsTo(key: string) {
return statsTo.value[key] ?? "unknown";
}
</script>