chore: script setup components (#7299)

This commit is contained in:
Kuchenpirat
2026-03-23 21:18:25 +01:00
committed by GitHub
parent 3ad2d9155d
commit 5ab6e98f9e
47 changed files with 1721 additions and 2453 deletions

View File

@@ -37,73 +37,68 @@
</v-container>
</template>
<script lang="ts">
export default defineNuxtComponent({
setup() {
const i18n = useI18n();
const auth = useMealieAuth();
const groupSlug = computed(() => auth.user.value?.groupSlug);
const { $globals } = useNuxtApp();
<script setup lang="ts">
const i18n = useI18n();
const auth = useMealieAuth();
const groupSlug = computed(() => auth.user.value?.groupSlug);
const { $globals } = useNuxtApp();
const sections = ref([
const sections = ref([
{
title: i18n.t("profile.data-migrations"),
color: "info",
links: [
{
title: i18n.t("profile.data-migrations"),
color: "info",
links: [
{
icon: $globals.icons.backupRestore,
to: "/admin/backups",
text: i18n.t("settings.backup.backup-restore"),
description: i18n.t("admin.setup.restore-from-v1-backup"),
},
{
icon: $globals.icons.import,
to: "/group/migrations",
text: i18n.t("migration.recipe-migration"),
description: i18n.t("migration.coming-from-another-application-or-an-even-older-version-of-mealie"),
},
],
icon: $globals.icons.backupRestore,
to: "/admin/backups",
text: i18n.t("settings.backup.backup-restore"),
description: i18n.t("admin.setup.restore-from-v1-backup"),
},
{
title: i18n.t("recipe.create-recipes"),
color: "success",
links: [
{
icon: $globals.icons.createAlt,
to: computed(() => `/g/${groupSlug.value || ""}/r/create/new`),
text: i18n.t("recipe.create-recipe"),
description: i18n.t("recipe.create-recipe-description"),
},
{
icon: $globals.icons.link,
to: computed(() => `/g/${groupSlug.value || ""}/r/create/url`),
text: i18n.t("recipe.import-with-url"),
description: i18n.t("recipe.scrape-recipe-description"),
},
],
icon: $globals.icons.import,
to: "/group/migrations",
text: i18n.t("migration.recipe-migration"),
description: i18n.t("migration.coming-from-another-application-or-an-even-older-version-of-mealie"),
},
{
title: i18n.t("user.manage-users"),
color: "primary",
links: [
{
icon: $globals.icons.group,
to: "/admin/manage/users",
text: i18n.t("user.manage-users"),
description: i18n.t("user.manage-users-description"),
},
{
icon: $globals.icons.user,
to: "/user/profile",
text: i18n.t("profile.manage-user-profile"),
description: i18n.t("admin.setup.manage-profile-or-get-invite-link"),
},
],
},
]);
return { sections };
],
},
});
{
title: i18n.t("recipe.create-recipes"),
color: "success",
links: [
{
icon: $globals.icons.createAlt,
to: computed(() => `/g/${groupSlug.value || ""}/r/create/new`),
text: i18n.t("recipe.create-recipe"),
description: i18n.t("recipe.create-recipe-description"),
},
{
icon: $globals.icons.link,
to: computed(() => `/g/${groupSlug.value || ""}/r/create/url`),
text: i18n.t("recipe.import-with-url"),
description: i18n.t("recipe.scrape-recipe-description"),
},
],
},
{
title: i18n.t("user.manage-users"),
color: "primary",
links: [
{
icon: $globals.icons.group,
to: "/admin/manage/users",
text: i18n.t("user.manage-users"),
description: i18n.t("user.manage-users-description"),
},
{
icon: $globals.icons.user,
to: "/user/profile",
text: i18n.t("profile.manage-user-profile"),
description: i18n.t("admin.setup.manage-profile-or-get-invite-link"),
},
],
},
]);
</script>
<style>

View File

@@ -25,48 +25,32 @@
</v-container>
</template>
<script lang="ts">
<script setup lang="ts">
import RecipeExplorerPageSearch from "./RecipeExplorerPageParts/RecipeExplorerPageSearch.vue";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
import { useLazyRecipes } from "~/composables/recipes";
export default defineNuxtComponent({
components: { RecipeCardSection, RecipeExplorerPageSearch },
setup() {
const auth = useMealieAuth();
const route = useRoute();
const auth = useMealieAuth();
const route = useRoute();
const { isOwnGroup } = useLoggedInState();
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
const { isOwnGroup } = useLoggedInState();
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
const { recipes, appendRecipes, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
const { recipes, appendRecipes, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
const ready = ref(false);
const searchComponent = ref<InstanceType<typeof RecipeExplorerPageSearch>>();
const ready = ref(false);
const searchComponent = ref<InstanceType<typeof RecipeExplorerPageSearch>>();
const searchQuery = computed(() => {
return searchComponent.value?.passedQueryWithSeed || {};
});
function onSearchReady() {
ready.value = true;
}
function onItemSelected(item: any, urlPrefix: string) {
searchComponent.value?.filterItems(item, urlPrefix);
}
return {
ready,
searchComponent,
searchQuery,
recipes,
appendRecipes,
replaceRecipes,
onSearchReady,
onItemSelected,
};
},
const searchQuery = computed(() => {
return searchComponent.value?.passedQueryWithSeed || {};
});
function onSearchReady() {
ready.value = true;
}
function onItemSelected(item: any, urlPrefix: string) {
searchComponent.value?.filterItems(item, urlPrefix);
}
</script>

View File

@@ -7,7 +7,7 @@
nudge-bottom="3"
:close-on-content-click="false"
>
<template #activator="{ props }">
<template #activator="{ props: menuProps }">
<v-badge
v-memo="[selectedCount]"
:model-value="selectedCount > 0"
@@ -19,7 +19,7 @@
size="small"
color="accent"
dark
v-bind="props"
v-bind="menuProps"
>
<slot />
</v-btn>
@@ -145,89 +145,72 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import type { ISearchableItem } from "~/composables/use-search";
import { useSearch } from "~/composables/use-search";
export default defineNuxtComponent({
props: {
items: {
type: Array as () => ISearchableItem[],
required: true,
},
modelValue: {
type: Array as () => any[],
required: true,
},
requireAll: {
type: Boolean,
default: undefined,
},
radio: {
type: Boolean,
default: false,
},
const props = defineProps({
items: {
type: Array as () => ISearchableItem[],
required: true,
},
emits: ["update:requireAll", "update:modelValue"],
setup(props, context) {
const state = reactive({
menu: false,
});
// Use the search composable
const { search: searchInput, filtered } = useSearch(computed(() => props.items));
const combinator = computed({
get: () => (props.requireAll ? "hasAll" : "hasAny"),
set: (value) => {
context.emit("update:requireAll", value === "hasAll");
},
});
// Use shallowRef to prevent deep reactivity on large arrays
const selected = computed({
get: () => props.modelValue as ISearchableItem[],
set: (value) => {
context.emit("update:modelValue", value);
},
});
const selectedRadio = computed({
get: () => (selected.value.length > 0 ? selected.value[0] : null),
set: (value) => {
context.emit("update:modelValue", value ? [value] : []);
},
});
const selectedCount = computed(() => selected.value.length);
const selectedIds = computed(() => {
return new Set(selected.value.map(item => item.id));
});
const handleRadioClick = (item: ISearchableItem) => {
if (selectedRadio.value === item) {
selectedRadio.value = null;
}
};
function clearSelection() {
selected.value = [];
selectedRadio.value = null;
searchInput.value = "";
}
return {
combinator,
state,
searchInput,
selected,
selectedRadio,
selectedCount,
selectedIds,
filtered,
handleRadioClick,
clearSelection,
};
requireAll: {
type: Boolean,
default: undefined,
},
radio: {
type: Boolean,
default: false,
},
});
const modelValue = defineModel<ISearchableItem[]>();
const emit = defineEmits<{
(e: "update:requireAll", value: boolean | undefined): void;
}>();
const state = reactive({
menu: false,
});
// Use the search composable
const { search: searchInput, filtered } = useSearch(computed(() => props.items));
const combinator = computed({
get: () => (props.requireAll ? "hasAll" : "hasAny"),
set: (value: string) => {
emit("update:requireAll", value === "hasAll");
},
});
const selected = computed<ISearchableItem[]>({
get: () => modelValue.value ?? [],
set: (value: ISearchableItem[]) => {
modelValue.value = value;
},
});
const selectedRadio = computed<null | ISearchableItem>({
get: () => (selected.value.length > 0 ? selected.value[0] : null),
set: (value: ISearchableItem | null) => {
const next = value ? [value] : [];
selected.value = next;
},
});
const selectedCount = computed(() => selected.value.length);
const selectedIds = computed(() => new Set(selected.value.map(item => item.id)));
const handleRadioClick = (item: ISearchableItem) => {
if (selectedRadio.value === item) {
selectedRadio.value = null;
}
};
function clearSelection() {
selected.value = [];
selectedRadio.value = null;
searchInput.value = "";
}
</script>

View File

@@ -12,23 +12,13 @@
</v-chip>
</template>
<script lang="ts">
<script setup lang="ts">
import { getTextColor } from "~/composables/use-text-color";
import type { MultiPurposeLabelSummary } from "~/lib/api/types/recipe";
export default defineNuxtComponent({
props: {
label: {
type: Object as () => MultiPurposeLabelSummary,
required: true,
},
},
setup(props) {
const textColor = computed(() => getTextColor(props.label.color));
const props = defineProps<{
label: MultiPurposeLabelSummary;
}>();
return {
textColor,
};
},
});
const textColor = computed(() => getTextColor(props.label.color));
</script>

View File

@@ -17,13 +17,13 @@
start
min-width="125px"
>
<template #activator="{ props }">
<template #activator="{ props: hoverProps }">
<v-btn
size="small"
variant="text"
class="ml-2 handle"
icon
v-bind="props"
v-bind="hoverProps"
>
<v-icon>
{{ $globals.icons.arrowUpDown }}
@@ -35,31 +35,13 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import type { ShoppingListMultiPurposeLabelOut } from "~/lib/api/types/household";
export default defineNuxtComponent({
props: {
modelValue: {
type: Object as () => ShoppingListMultiPurposeLabelOut,
required: true,
},
useColor: {
type: Boolean,
default: false,
},
},
setup(props, context) {
const labelColor = ref<string | undefined>(props.useColor ? props.modelValue.label.color : undefined);
const props = defineProps<{
useColor?: boolean;
}>();
const modelValue = defineModel<ShoppingListMultiPurposeLabelOut>({ required: true });
function contextHandler(event: string) {
context.emit(event);
}
return {
contextHandler,
labelColor,
};
},
});
const labelColor = ref<string | undefined>(props.useColor ? modelValue.value.label.color : undefined);
</script>

View File

@@ -10,15 +10,12 @@
<v-col :cols="itemLabelCols">
<div class="d-flex align-center flex-nowrap">
<v-checkbox
v-model="listItem.checked"
:model-value="listItem.checked"
hide-details
density="compact"
class="mt-0 flex-shrink-0"
color="null"
@click="() => {
listItem.checked = !listItem.checked
$emit('checked', listItem)
}"
@click="toggleChecked"
/>
<div
class="ml-2 text-truncate"
@@ -43,7 +40,7 @@
start
min-width="125px"
>
<template #activator="{ props }">
<template #activator="{ props: hoverProps }">
<v-tooltip
v-if="recipeList && recipeList.length"
open-delay="200"
@@ -84,7 +81,7 @@
variant="text"
class="handle"
icon
v-bind="props"
v-bind="hoverProps"
>
<v-icon>
{{ $globals.icons.arrowUpDown }}
@@ -155,158 +152,99 @@
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
import { useOnline } from "@vueuse/core";
import RecipeIngredientListItem from "../Recipe/RecipeIngredientListItem.vue";
import ShoppingListItemEditor from "./ShoppingListItemEditor.vue";
import type { ShoppingListItemOut } from "~/lib/api/types/household";
import type { MultiPurposeLabelOut, MultiPurposeLabelSummary } from "~/lib/api/types/labels";
import type { MultiPurposeLabelOut } from "~/lib/api/types/labels";
import type { IngredientFood, IngredientUnit, RecipeSummary } from "~/lib/api/types/recipe";
import RecipeList from "~/components/Domain/Recipe/RecipeList.vue";
interface actions {
text: string;
event: string;
const model = defineModel<ShoppingListItemOut>({ type: Object as () => ShoppingListItemOut, required: true });
const props = defineProps({
labels: {
type: Array as () => MultiPurposeLabelOut[],
required: true,
},
units: {
type: Array as () => IngredientUnit[],
required: true,
},
foods: {
type: Array as () => IngredientFood[],
required: true,
},
recipes: {
type: Map as unknown as () => Map<string, RecipeSummary>,
default: undefined,
},
});
const emit = defineEmits<{
(e: "checked" | "save", item: ShoppingListItemOut): void;
(e: "delete"): void;
}>();
const i18n = useI18n();
const displayRecipeRefs = ref(false);
const itemLabelCols = computed<string>(() => (model.value?.checked ? "auto" : "6"));
const online = useOnline();
const isOffline = computed(() => online.value === false);
type actions = { text: string; event: string };
const contextMenu = ref<actions[]>([
{ text: i18n.t("general.edit") as string, event: "edit" },
{ text: i18n.t("general.delete") as string, event: "delete" },
]);
// copy prop value so a refresh doesn't interrupt the user
const localListItem = ref(Object.assign({}, model.value));
const listItem = computed<ShoppingListItemOut>({
get: () => model.value,
set: (val: ShoppingListItemOut) => {
localListItem.value = val;
model.value = val;
},
});
const edit = ref(false);
function toggleEdit(val = !edit.value) {
if (edit.value === val) return;
if (val) localListItem.value = model.value;
edit.value = val;
}
export default defineNuxtComponent({
components: { ShoppingListItemEditor, RecipeList, RecipeIngredientListItem },
props: {
modelValue: {
type: Object as () => ShoppingListItemOut,
required: true,
},
labels: {
type: Array as () => MultiPurposeLabelOut[],
required: true,
},
units: {
type: Array as () => IngredientUnit[],
required: true,
},
foods: {
type: Array as () => IngredientFood[],
required: true,
},
recipes: {
type: Map<string, RecipeSummary>,
default: undefined,
},
},
emits: ["checked", "update:modelValue", "save", "delete"],
setup(props, context) {
const i18n = useI18n();
const displayRecipeRefs = ref(false);
const itemLabelCols = ref<string>(props.modelValue.checked ? "auto" : "6");
const isOffline = computed(() => useOnline().value === false);
function toggleChecked() {
const updated = { ...model.value, checked: !model.value.checked } as ShoppingListItemOut;
model.value = updated;
emit("checked", updated);
}
const contextMenu: actions[] = [
{
text: i18n.t("general.edit") as string,
event: "edit",
},
{
text: i18n.t("general.delete") as string,
event: "delete",
},
];
function contextHandler(event: string) {
if (event === "edit") {
toggleEdit(true);
}
else {
emit(event as any);
}
}
// copy prop value so a refresh doesn't interrupt the user
const localListItem = ref(Object.assign({}, props.modelValue));
const listItem = computed({
get: () => {
return props.modelValue;
},
set: (val) => {
// keep local copy in sync
localListItem.value = val;
context.emit("update:modelValue", val);
},
});
const edit = ref(false);
function toggleEdit(val = !edit.value) {
if (edit.value === val) {
return;
}
function save() {
emit("save", localListItem.value);
edit.value = false;
}
if (val) {
// update local copy of item with the current value
localListItem.value = props.modelValue;
}
edit.value = val;
}
function contextHandler(event: string) {
if (event === "edit") {
toggleEdit(true);
}
else {
context.emit(event);
}
}
function save() {
context.emit("save", localListItem.value);
edit.value = false;
}
const updatedLabels = computed(() => {
return props.labels.map((label) => {
return {
id: label.id,
text: label.name,
};
});
});
/**
* Gets the label for the shopping list item. Either the label assign to the item
* or the label of the food applied.
*/
const label = computed<MultiPurposeLabelSummary | undefined>(() => {
if (listItem.value.label) {
return listItem.value.label as MultiPurposeLabelSummary;
}
if (listItem.value.food?.label) {
return listItem.value.food.label;
}
return undefined;
});
const recipeList = computed<RecipeSummary[]>(() => {
const recipeList: RecipeSummary[] = [];
if (!listItem.value.recipeReferences) {
return recipeList;
}
listItem.value.recipeReferences.forEach((ref) => {
const recipe = props.recipes?.get(ref.recipeId);
if (recipe) {
recipeList.push(recipe);
}
});
return recipeList;
});
return {
updatedLabels,
save,
contextHandler,
displayRecipeRefs,
edit,
contextMenu,
itemLabelCols,
listItem,
localListItem,
label,
recipeList,
toggleEdit,
isOffline,
};
},
const recipeList = computed<RecipeSummary[]>(() => {
const ret: RecipeSummary[] = [];
if (!listItem.value.recipeReferences) return ret;
listItem.value.recipeReferences.forEach((ref) => {
const recipe = props.recipes?.get(ref.recipeId);
if (recipe) ret.push(recipe);
});
return ret;
});
</script>

View File

@@ -102,125 +102,108 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import type { ShoppingListItemCreate, ShoppingListItemOut } from "~/lib/api/types/household";
import type { MultiPurposeLabelOut } from "~/lib/api/types/labels";
import type { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
import { useFoodStore, useFoodData, useUnitStore, useUnitData } from "~/composables/store";
export default defineNuxtComponent({
props: {
modelValue: {
type: Object as () => ShoppingListItemCreate | ShoppingListItemOut,
required: true,
},
labels: {
type: Array as () => MultiPurposeLabelOut[],
required: true,
},
units: {
type: Array as () => IngredientUnit[],
required: true,
},
foods: {
type: Array as () => IngredientFood[],
required: true,
},
allowDelete: {
type: Boolean,
required: false,
default: true,
},
// modelValue as reactive v-model
const listItem = defineModel<ShoppingListItemCreate | ShoppingListItemOut>({ required: true });
defineProps({
labels: {
type: Array as () => MultiPurposeLabelOut[],
required: true,
},
emits: ["update:modelValue", "save", "cancel", "delete"],
setup(props, context) {
const foodStore = useFoodStore();
const foodData = useFoodData();
const unitStore = useUnitStore();
const unitData = useUnitData();
const listItem = computed({
get: () => {
return props.modelValue;
},
set: (val) => {
context.emit("update:modelValue", val);
},
});
watch(
() => props.modelValue.quantity,
() => {
if (!props.modelValue.quantity) {
listItem.value.quantity = 0;
}
},
);
watch(
() => props.modelValue.food,
(newFood) => {
listItem.value.label = newFood?.label || null;
listItem.value.labelId = listItem.value.label?.id || null;
},
);
const autoFocus = !listItem.value.food && listItem.value.note ? "note" : "food";
async function createAssignFood(val: string) {
// keep UI reactive
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
listItem.value.food ? (listItem.value.food.name = val) : (listItem.value.food = { name: val });
foodData.data.name = val;
const newFood = await foodStore.actions.createOne(foodData.data);
if (newFood) {
listItem.value.food = newFood;
listItem.value.foodId = newFood.id;
}
foodData.reset();
}
async function createAssignUnit(val: string) {
// keep UI reactive
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
listItem.value.unit ? (listItem.value.unit.name = val) : (listItem.value.unit = { name: val });
unitData.data.name = val;
const newUnit = await unitStore.actions.createOne(unitData.data);
if (newUnit) {
listItem.value.unit = newUnit;
listItem.value.unitId = newUnit.id;
}
unitData.reset();
}
async function assignLabelToFood() {
if (!(listItem.value.food && listItem.value.foodId && listItem.value.labelId)) {
return;
}
listItem.value.food.labelId = listItem.value.labelId;
await foodStore.actions.updateOne(listItem.value.food);
}
return {
listItem,
autoFocus,
createAssignFood,
createAssignUnit,
assignLabelToFood,
};
units: {
type: Array as () => IngredientUnit[],
required: true,
},
methods: {
handleNoteKeyPress(event) {
// Save on Enter
if (!event.shiftKey && event.key === "Enter") {
event.preventDefault();
this.$emit("save");
}
},
foods: {
type: Array as () => IngredientFood[],
required: true,
},
allowDelete: {
type: Boolean,
required: false,
default: true,
},
});
// const emit = defineEmits<["save", "cancel", "delete"]>();
const emit = defineEmits<{
(e: "save", item: ShoppingListItemOut): void;
(e: "cancel" | "delete"): void;
}>();
const foodStore = useFoodStore();
const foodData = useFoodData();
const unitStore = useUnitStore();
const unitData = useUnitData();
watch(
() => listItem.value.quantity,
(newQty) => {
if (!newQty) {
listItem.value.quantity = 0;
}
},
);
watch(
() => listItem.value.food,
(newFood) => {
listItem.value.label = newFood?.label || null;
listItem.value.labelId = listItem.value.label?.id || null;
},
);
const autoFocus = computed(() => (!listItem.value.food && listItem.value.note ? "note" : "food"));
async function createAssignFood(val: string) {
// keep UI reactive
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
listItem.value.food ? (listItem.value.food.name = val) : (listItem.value.food = { name: val } as any);
foodData.data.name = val;
const newFood = await foodStore.actions.createOne(foodData.data);
if (newFood) {
listItem.value.food = newFood;
listItem.value.foodId = newFood.id;
}
foodData.reset();
}
async function createAssignUnit(val: string) {
// keep UI reactive
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
listItem.value.unit ? (listItem.value.unit.name = val) : (listItem.value.unit = { name: val } as any);
unitData.data.name = val;
const newUnit = await unitStore.actions.createOne(unitData.data);
if (newUnit) {
listItem.value.unit = newUnit;
listItem.value.unitId = newUnit.id;
}
unitData.reset();
}
async function assignLabelToFood() {
if (!(listItem.value.food && listItem.value.foodId && listItem.value.labelId)) {
return;
}
listItem.value.food.labelId = listItem.value.labelId;
await foodStore.actions.updateOne(listItem.value.food);
}
function handleNoteKeyPress(event: KeyboardEvent) {
const e = event as KeyboardEvent & { key: string; shiftKey: boolean };
if (!e.shiftKey && e.key === "Enter") {
e.preventDefault();
emit("save");
}
}
</script>

View File

@@ -4,10 +4,10 @@
:disabled="!user || !tooltip"
location="end"
>
<template #activator="{ props }">
<template #activator="{ props: tooltipProps }">
<v-avatar
v-if="list"
v-bind="props"
v-bind="tooltipProps"
>
<v-img
:src="imageURL"
@@ -19,7 +19,7 @@
<v-avatar
v-else
:size="size"
v-bind="props"
v-bind="tooltipProps"
>
<v-img
:src="imageURL"
@@ -35,51 +35,40 @@
</v-tooltip>
</template>
<script lang="ts">
<script setup lang="ts">
import { useUserStore } from "~/composables/store/use-user-store";
export default defineNuxtComponent({
props: {
userId: {
type: String,
required: true,
},
list: {
type: Boolean,
default: false,
},
size: {
type: String,
default: "42",
},
tooltip: {
type: Boolean,
default: true,
},
const props = defineProps({
userId: {
type: String,
required: true,
},
setup(props) {
const state = reactive({
error: false,
});
const auth = useMealieAuth();
const { store: users } = useUserStore();
const user = computed(() => {
return users.value.find(user => user.id === props.userId);
});
const imageURL = computed(() => {
// Note: auth.user is a ref now
const authUser = auth.user.value;
const key = authUser?.cacheKey ?? "";
return `/api/media/users/${props.userId}/profile.webp?cacheKey=${key}`;
});
return {
user,
imageURL,
...toRefs(state),
};
list: {
type: Boolean,
default: false,
},
size: {
type: String,
default: "42",
},
tooltip: {
type: Boolean,
default: true,
},
});
const error = ref(false);
const auth = useMealieAuth();
const { store: users } = useUserStore();
const user = computed(() => {
return users.value.find(user => user.id === props.userId);
});
const imageURL = computed(() => {
// Note: auth.user is a ref now
const authUser = auth.user.value;
const key = authUser?.cacheKey ?? "";
return `/api/media/users/${props.userId}/profile.webp?cacheKey=${key}`;
});
</script>

View File

@@ -73,8 +73,7 @@
</BaseDialog>
</template>
<script lang="ts">
import { watchEffect } from "vue";
<script setup lang="ts">
import { useUserApi } from "@/composables/api";
import BaseDialog from "~/components/global/BaseDialog.vue";
import AppButtonCopy from "~/components/global/AppButtonCopy.vue";
@@ -86,147 +85,95 @@ import type { HouseholdInDB } from "~/lib/api/types/household";
import { useGroups } from "~/composables/use-groups";
import { useAdminHouseholds } from "~/composables/use-households";
export default defineNuxtComponent({
name: "UserInviteDialog",
components: {
BaseDialog,
AppButtonCopy,
BaseButton,
},
props: {
modelValue: {
type: Boolean,
default: false,
},
},
emits: ["update:modelValue"],
setup(props, context) {
const i18n = useI18n();
const auth = useMealieAuth();
const inviteDialog = defineModel<boolean>("modelValue", { type: Boolean, default: false });
const isAdmin = computed(() => auth.user.value?.admin);
const token = ref("");
const selectedGroup = ref<string | null>(null);
const selectedHousehold = ref<string | null>(null);
const groups = ref<GroupInDB[]>([]);
const households = ref<HouseholdInDB[]>([]);
const api = useUserApi();
const i18n = useI18n();
const auth = useMealieAuth();
const fetchGroupsAndHouseholds = () => {
if (isAdmin.value) {
const groupsResponse = useGroups();
const householdsResponse = useAdminHouseholds();
watchEffect(() => {
groups.value = groupsResponse.groups.value || [];
households.value = householdsResponse.households.value || [];
});
}
};
const isAdmin = computed(() => auth.user.value?.admin);
const token = ref("");
const selectedGroup = ref<string | null>(null);
const selectedHousehold = ref<string | null>(null);
const groups = ref<GroupInDB[]>([]);
const households = ref<HouseholdInDB[]>([]);
const api = useUserApi();
const inviteDialog = computed<boolean>({
get() {
return props.modelValue;
},
set(val) {
context.emit("update:modelValue", val);
},
const fetchGroupsAndHouseholds = () => {
if (isAdmin.value) {
const groupsResponse = useGroups();
const householdsResponse = useAdminHouseholds();
watchEffect(() => {
groups.value = groupsResponse.groups.value || [];
households.value = householdsResponse.households.value || [];
});
}
};
async function getSignupLink(group: string | null = null, household: string | null = null) {
const payload = (group && household) ? { uses: 1, group_id: group, household_id: household } : { uses: 1 };
const { data } = await api.households.createInvitation(payload);
if (data) {
token.value = data.token;
}
}
async function getSignupLink(group: string | null = null, household: string | null = null) {
const payload = (group && household) ? { uses: 1, group_id: group, household_id: household } : { uses: 1 };
const { data } = await api.households.createInvitation(payload);
if (data) {
token.value = data.token;
}
}
const filteredHouseholds = computed(() => {
if (!selectedGroup.value) return [];
return households.value?.filter(household => household.groupId === selectedGroup.value);
});
function constructLink(token: string) {
return token ? `${window.location.origin}/register?token=${token}` : "";
}
const generatedSignupLink = computed(() => {
return constructLink(token.value);
});
// =================================================
// Email Invitation
const state = reactive({
loading: false,
sendTo: "",
});
async function sendInvite() {
state.loading = true;
if (!token.value) {
getSignupLink(selectedGroup.value, selectedHousehold.value);
}
const { data } = await api.email.sendInvitation({
email: state.sendTo,
token: token.value,
});
if (data && data.success) {
alert.success(i18n.t("profile.email-sent"));
}
else {
alert.error(i18n.t("profile.error-sending-email"));
}
state.loading = false;
inviteDialog.value = false;
}
const validEmail = computed(() => {
if (state.sendTo === "") {
return false;
}
const valid = validators.email(state.sendTo);
// Explicit bool check because validators.email sometimes returns a string
if (valid === true) {
return true;
}
return false;
});
return {
sendInvite,
validators,
validEmail,
inviteDialog,
getSignupLink,
generatedSignupLink,
selectedGroup,
selectedHousehold,
filteredHouseholds,
groups,
households,
fetchGroupsAndHouseholds,
...toRefs(state),
isAdmin,
};
},
watch: {
modelValue: {
immediate: false,
handler(val) {
if (val && !this.isAdmin) {
this.getSignupLink();
}
},
},
selectedHousehold(newVal) {
if (newVal && this.selectedGroup) {
this.getSignupLink(this.selectedGroup, this.selectedHousehold);
}
},
},
created() {
this.fetchGroupsAndHouseholds();
},
const filteredHouseholds = computed(() => {
if (!selectedGroup.value) return [];
return households.value?.filter(household => household.groupId === selectedGroup.value);
});
function constructLink(tokenVal: string) {
return tokenVal ? `${window.location.origin}/register?token=${tokenVal}` : "";
}
const generatedSignupLink = computed(() => constructLink(token.value));
// Email Invitation
const state = reactive({
loading: false,
sendTo: "",
});
const { loading, sendTo } = toRefs(state);
async function sendInvite() {
state.loading = true;
if (!token.value) {
getSignupLink(selectedGroup.value, selectedHousehold.value);
}
const { data } = await api.email.sendInvitation({
email: state.sendTo,
token: token.value,
});
if (data && data.success) {
alert.success(i18n.t("profile.email-sent"));
}
else {
alert.error(i18n.t("profile.error-sending-email"));
}
state.loading = false;
inviteDialog.value = false;
}
const validEmail = computed(() => {
if (sendTo.value === "") return false;
const valid = validators.email(sendTo.value);
return valid === true;
});
// Watchers (replacing options API watchers)
watch(inviteDialog, (val) => {
if (val && !isAdmin.value) {
getSignupLink();
}
});
watch(selectedHousehold, (newVal) => {
if (newVal && selectedGroup.value) {
getSignupLink(selectedGroup.value, selectedHousehold.value);
}
});
// initial fetch
fetchGroupsAndHouseholds();
</script>

View File

@@ -52,14 +52,14 @@
</v-card>
</template>
<script lang="ts" setup>
<script setup lang="ts">
interface LinkProp {
text: string;
url?: string;
to: string;
}
const props = defineProps({
defineProps({
link: {
type: Object as () => LinkProp,
required: true,
@@ -70,6 +70,4 @@ const props = defineProps({
default: "",
},
});
console.log("Props", props);
</script>

View File

@@ -78,57 +78,29 @@
</div>
</template>
<script lang="ts">
import { useDark } from "@vueuse/core";
<script setup lang="ts">
import { validators } from "~/composables/use-validators";
import { useUserRegistrationForm } from "~/composables/use-users/user-registration-form";
import { usePasswordField } from "~/composables/use-passwords";
import UserPasswordStrength from "~/components/Domain/User/UserPasswordStrength.vue";
definePageMeta({ layout: "blank" });
const inputAttrs = {
validateOnBlur: true,
class: "pb-1",
variant: "solo-filled" as any,
};
export default defineNuxtComponent({
components: { UserPasswordStrength },
setup() {
definePageMeta({
layout: "blank",
});
const isDark = useDark();
const langDialog = ref(false);
const pwFields = usePasswordField();
const {
accountDetails,
credentials,
emailErrorMessages,
usernameErrorMessages,
validateUsername,
validateEmail,
domAccountForm,
} = useUserRegistrationForm();
return {
accountDetails,
credentials,
emailErrorMessages,
inputAttrs,
isDark,
langDialog,
pwFields,
usernameErrorMessages,
validators,
// Validators
validateUsername,
validateEmail,
// Dom Refs
domAccountForm,
};
},
});
const pwFields = usePasswordField();
const {
accountDetails,
credentials,
emailErrorMessages,
usernameErrorMessages,
validateUsername,
validateEmail,
} = useUserRegistrationForm();
</script>
<style lang="css" scoped>