1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-08-02 20:15:24 +02:00

feat: Migrate to Nuxt 3 framework (#5184)

Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
This commit is contained in:
Hoa (Kyle) Trinh 2025-06-20 00:09:12 +07:00 committed by GitHub
parent 89ab7fac25
commit c24d532608
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
403 changed files with 23959 additions and 19557 deletions

View file

@ -1,7 +1,5 @@
import { AxiosResponse } from "axios";
import { useContext } from "@nuxtjs/composition-api";
import type { NuxtAxiosInstance } from "@nuxtjs/axios";
import { ApiRequestInstance, RequestResponse } from "~/lib/api/types/non-generated";
import type { AxiosInstance, AxiosResponse } from "axios";
import type { ApiRequestInstance, RequestResponse } from "~/lib/api/types/non-generated";
import { AdminAPI, PublicApi, UserApi } from "~/lib/api";
import { PublicExploreApi } from "~/lib/api/client-public";
@ -9,7 +7,7 @@ const request = {
async safe<T, U>(
funcCall: (url: string, data: U) => Promise<AxiosResponse<T>>,
url: string,
data: U
data: U,
): Promise<RequestResponse<T>> {
let error = null;
const response = await funcCall(url, data).catch(function (e) {
@ -22,7 +20,7 @@ const request = {
},
};
function getRequests(axiosInstance: NuxtAxiosInstance): ApiRequestInstance {
function getRequests(axiosInstance: AxiosInstance): ApiRequestInstance {
return {
async get<T>(url: string, params = {}): Promise<RequestResponse<T>> {
let error = null;
@ -36,31 +34,28 @@ function getRequests(axiosInstance: NuxtAxiosInstance): ApiRequestInstance {
},
async post<T, U>(url: string, data: U) {
// eslint-disable-next-line @typescript-eslint/unbound-method
return await request.safe<T, U>(axiosInstance.post, url, data);
},
async put<T, U = T>(url: string, data: U) {
// eslint-disable-next-line @typescript-eslint/unbound-method
return await request.safe<T, U>(axiosInstance.put, url, data);
},
async patch<T, U = Partial<T>>(url: string, data: U) {
// eslint-disable-next-line @typescript-eslint/unbound-method
return await request.safe<T, U>(axiosInstance.patch, url, data);
},
async delete<T>(url: string) {
// eslint-disable-next-line @typescript-eslint/unbound-method
return await request.safe<T, undefined>(axiosInstance.delete, url, undefined);
},
};
}
export const useRequests = function (): ApiRequestInstance {
const { $axios, i18n } = useContext();
const i18n = useI18n();
const { $axios } = useNuxtApp();
$axios.setHeader("Accept-Language", i18n.locale);
$axios.defaults.headers.common["Accept-Language"] = i18n.locale.value;
return getRequests($axios);
};
@ -83,4 +78,4 @@ export const usePublicApi = function (): PublicApi {
export const usePublicExploreApi = function (groupSlug: string): PublicExploreApi {
const requests = useRequests();
return new PublicExploreApi(requests, groupSlug);
}
};

View file

@ -1,15 +1,12 @@
import { useContext } from "@nuxtjs/composition-api";
import { detectServerBaseUrl } from "../use-utils";
function UnknownToString(ukn: string | unknown) {
return typeof ukn === "string" ? ukn : "";
}
export const useStaticRoutes = () => {
const { $config, req } = useContext();
const serverBase = detectServerBaseUrl(req);
const { $config } = useNuxtApp();
const serverBase = useRequestURL().origin;
const prefix = `${$config.SUB_PATH as string}/api`.replace("//", "/");
const prefix = `${$config.public.SUB_PATH}/api`.replace("//", "/");
const fullBase = serverBase + prefix;
@ -20,13 +17,13 @@ export const useStaticRoutes = () => {
function recipeSmallImage(recipeId: string, version: string | unknown = "", key: string | number = 1) {
return `${fullBase}/media/recipes/${recipeId}/images/min-original.webp?rnd=${key}&version=${UnknownToString(
version
version,
)}`;
}
function recipeTinyImage(recipeId: string, version: string | unknown = "", key: string | number = 1) {
return `${fullBase}/media/recipes/${recipeId}/images/tiny-original.webp?rnd=${key}&version=${UnknownToString(
version
version,
)}`;
}

View file

@ -1,17 +1,17 @@
import { ref, Ref, useAsync, useContext } from "@nuxtjs/composition-api";
import { useAsyncKey } from "../use-utils";
import { AppInfo } from "~/lib/api/types/admin";
import type { AppInfo } from "~/lib/api/types/admin";
export function useAppInfo(): Ref<AppInfo | null> {
const appInfo = ref<null | AppInfo>(null);
const { $axios, i18n } = useContext();
$axios.setHeader("Accept-Language", i18n.locale);
const i18n = useI18n();
const { $axios } = useNuxtApp();
$axios.defaults.headers.common["Accept-Language"] = i18n.locale.value;
useAsync(async () => {
useAsyncData(useAsyncKey(), async () => {
const data = await $axios.get<AppInfo>("/api/app/about");
appInfo.value = data.data;
}, useAsyncKey());
});
return appInfo;
}

View file

@ -1,22 +0,0 @@
import { useContext } from "@nuxtjs/composition-api";
export function useAxiosDownloader() {
const { $axios } = useContext();
function download(url: string, filename: string) {
$axios({
url,
method: "GET",
responseType: "blob",
}).then((response) => {
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", filename);
document.body.appendChild(link);
link.click();
});
}
return download;
}

View file

@ -0,0 +1,18 @@
export function useDownloader() {
function download(url: string, filename: string) {
useFetch(url, {
method: "GET",
responseType: "blob",
onResponse({ response }) {
const url = window.URL.createObjectURL(new Blob([response._data]));
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", filename);
document.body.appendChild(link);
link.click();
},
});
}
return download;
}

View file

@ -1,8 +1,7 @@
import { Ref, useAsync } from "@nuxtjs/composition-api";
import { useAsyncKey } from "../use-utils";
import { BoundT } from "./types";
import { BaseCRUDAPI, BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients";
import { QueryValue } from "~/lib/api/base/route";
import type { BoundT } from "./types";
import type { BaseCRUDAPI, BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients";
import type { QueryValue } from "~/lib/api/base/route";
interface ReadOnlyStoreActions<T extends BoundT> {
getAll(page?: number, perPage?: number, params?: any): Ref<T[] | null>;
@ -15,7 +14,6 @@ interface StoreActions<T extends BoundT> extends ReadOnlyStoreActions<T> {
deleteOne(id: string | number): Promise<T | null>;
}
/**
* useReadOnlyActions is a factory function that returns a set of methods
* that can be reused to manage the state of a data store without using
@ -25,14 +23,14 @@ interface StoreActions<T extends BoundT> extends ReadOnlyStoreActions<T> {
export function useReadOnlyActions<T extends BoundT>(
api: BaseCRUDAPIReadOnly<T>,
allRef: Ref<T[] | null> | null,
loading: Ref<boolean>
loading: Ref<boolean>,
): ReadOnlyStoreActions<T> {
function getAll(page = 1, perPage = -1, params = {} as Record<string, QueryValue>) {
params.orderBy ??= "name";
params.orderDirection ??= "asc";
loading.value = true;
const allItems = useAsync(async () => {
const allItems = useAsyncData(useAsyncKey(), async () => {
const { data } = await api.getAll(page, perPage, params);
loading.value = false;
@ -42,10 +40,11 @@ export function useReadOnlyActions<T extends BoundT>(
if (data) {
return data.items ?? [];
} else {
}
else {
return [];
}
}, useAsyncKey());
});
return allItems;
}
@ -79,14 +78,14 @@ export function useReadOnlyActions<T extends BoundT>(
export function useStoreActions<T extends BoundT>(
api: BaseCRUDAPI<unknown, T, unknown>,
allRef: Ref<T[] | null> | null,
loading: Ref<boolean>
loading: Ref<boolean>,
): StoreActions<T> {
function getAll(page = 1, perPage = -1, params = {} as Record<string, QueryValue>) {
params.orderBy ??= "name";
params.orderDirection ??= "asc";
loading.value = true;
const allItems = useAsync(async () => {
const allItems = useAsyncData(useAsyncKey(), async () => {
const { data } = await api.getAll(page, perPage, params);
loading.value = false;
@ -96,10 +95,11 @@ export function useStoreActions<T extends BoundT>(
if (data) {
return data.items ?? [];
} else {
}
else {
return [];
}
}, useAsyncKey());
});
return allItems;
}
@ -123,7 +123,8 @@ export function useStoreActions<T extends BoundT>(
const { data } = await api.createOne(createData);
if (data && allRef?.value) {
allRef.value.push(data);
} else {
}
else {
await refresh();
}
loading.value = false;

View file

@ -1,19 +1,18 @@
import { ref, reactive, Ref } from "@nuxtjs/composition-api";
import { useReadOnlyActions, useStoreActions } from "./use-actions-factory";
import { BoundT } from "./types";
import { BaseCRUDAPI, BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients";
import { QueryValue } from "~/lib/api/base/route";
import type { BoundT } from "./types";
import type { BaseCRUDAPI, BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients";
import type { QueryValue } from "~/lib/api/base/route";
export const useData = function<T extends BoundT>(defaultObject: T) {
export const useData = function <T extends BoundT>(defaultObject: T) {
const data = reactive({ ...defaultObject });
function reset() {
Object.assign(data, defaultObject);
};
return { data, reset };
}
};
export const useReadOnlyStore = function<T extends BoundT>(
export const useReadOnlyStore = function <T extends BoundT>(
store: Ref<T[]>,
loading: Ref<boolean>,
api: BaseCRUDAPIReadOnly<T>,
@ -36,9 +35,9 @@ export const useReadOnlyStore = function<T extends BoundT>(
}
return { store, actions };
}
};
export const useStore = function<T extends BoundT>(
export const useStore = function <T extends BoundT>(
store: Ref<T[]>,
loading: Ref<boolean>,
api: BaseCRUDAPI<unknown, T, unknown>,
@ -61,4 +60,4 @@ export const useStore = function<T extends BoundT>(
}
return { store, actions };
}
};

View file

@ -1,5 +1,4 @@
import { computed, ComputedRef, ref, Ref, useContext } from "@nuxtjs/composition-api";
import { UserOut } from "~/lib/api/types/user";
import type { UserOut } from "~/lib/api/types/user";
import { useNavigationWarning } from "~/composables/use-navigation-warning";
export enum PageMode {
@ -30,20 +29,20 @@ interface PageState {
editMode: ComputedRef<EditorMode>;
/**
* true is the page is in edit mode and the edit mode is in form mode.
*/
* true is the page is in edit mode and the edit mode is in form mode.
*/
isEditForm: ComputedRef<boolean>;
/**
* true is the page is in edit mode and the edit mode is in json mode.
*/
* true is the page is in edit mode and the edit mode is in json mode.
*/
isEditJSON: ComputedRef<boolean>;
/**
* true is the page is in view mode.
*/
* true is the page is in view mode.
*/
isEditMode: ComputedRef<boolean>;
/**
* true is the page is in cook mode.
*/
* true is the page is in cook mode.
*/
isCookMode: ComputedRef<boolean>;
setMode: (v: PageMode) => void;
@ -96,7 +95,8 @@ function pageState({ slugRef, pageModeRef, editModeRef, imageKey }: PageRefs): P
setEditMode(EditorMode.FORM);
}
deactivateNavigationWarning();
} else if (toMode === PageMode.EDIT) {
}
else if (toMode === PageMode.EDIT) {
activateNavigationWarning();
}
@ -142,6 +142,7 @@ export function usePageState(slug: string): PageState {
}
export function clearPageState(slug: string) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete memo[slug];
}
@ -151,9 +152,9 @@ export function clearPageState(slug: string) {
* object with all properties set to their zero value is returned.
*/
export function usePageUser(): { user: UserOut } {
const { $auth } = useContext();
const $auth = useMealieAuth();
if (!$auth.user) {
if (!$auth.user.value) {
return {
user: {
id: "",
@ -169,5 +170,5 @@ export function usePageUser(): { user: UserOut } {
};
}
return { user: $auth.user };
return { user: $auth.user.value };
}

View file

@ -3,66 +3,59 @@ import { useExtractIngredientReferences } from "./use-extract-ingredient-referen
const punctuationMarks = ["*", "?", "/", "!", "**", "&", "."];
describe("test use extract ingredient references", () => {
test("when text empty return empty", () => {
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "", true)
expect(result).toStrictEqual(new Set());
});
test("when text empty return empty", () => {
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "", true);
expect(result).toStrictEqual(new Set());
});
test("when and ingredient matches exactly and has a reference id, return the referenceId", () => {
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing Onion", true);
test("when and ingredient matches exactly and has a reference id, return the referenceId", () => {
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing Onion", true);
expect(result).toEqual(new Set(["123"]));
});
expect(result).toEqual(new Set(["123"]));
});
test.each(punctuationMarks)("when ingredient is suffixed by punctuation, return the referenceId", (suffix) => {
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing Onion" + suffix, true);
test.each(punctuationMarks)("when ingredient is suffixed by punctuation, return the referenceId", (suffix) => {
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing Onion" + suffix, true);
expect(result).toEqual(new Set(["123"]));
});
expect(result).toEqual(new Set(["123"]));
});
test.each(punctuationMarks)("when ingredient is prefixed by punctuation, return the referenceId", (prefix) => {
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing " + prefix + "Onion", true);
expect(result).toEqual(new Set(["123"]));
});
test.each(punctuationMarks)("when ingredient is prefixed by punctuation, return the referenceId", (prefix) => {
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing " + prefix + "Onion", true);
expect(result).toEqual(new Set(["123"]));
});
test("when ingredient is first on a multiline, return the referenceId", () => {
const multilineSting = "lksjdlk\nOnion";
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], multilineSting, true);
expect(result).toEqual(new Set(["123"]));
});
test("when ingredient is first on a multiline, return the referenceId", () => {
const multilineSting = "lksjdlk\nOnion"
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], multilineSting, true);
expect(result).toEqual(new Set(["123"]));
});
test("when the ingredient matches partially exactly and has a reference id, return the referenceId", () => {
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing Onions", true);
expect(result).toEqual(new Set(["123"]));
});
test("when the ingredient matches partially exactly and has a reference id, return the referenceId", () => {
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing Onions", true);
expect(result).toEqual(new Set(["123"]));
});
test("when the ingredient matches with different casing and has a reference id, return the referenceId", () => {
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing oNions", true);
expect(result).toEqual(new Set(["123"]));
});
test("when no ingredients, return empty", () => {
const result = useExtractIngredientReferences([], [], "A sentence containing oNions", true);
expect(result).toEqual(new Set());
});
test("when the ingredient matches with different casing and has a reference id, return the referenceId", () => {
const result = useExtractIngredientReferences([{ note: "Onions", referenceId: "123" }], [], "A sentence containing oNions", true);
expect(result).toEqual(new Set(["123"]));
});
test("when no ingredients, return empty", () => {
const result = useExtractIngredientReferences([], [], "A sentence containing oNions", true);
expect(result).toEqual(new Set());
});
test("when and ingredient matches but in the existing referenceIds, do not return the referenceId", () => {
const result = useExtractIngredientReferences([{ note: "Onion", referenceId: "123" }], ["123"], "A sentence containing Onion", true);
expect(result).toEqual(new Set());
});
test("when an word is 2 letter of shorter, it is ignored", () => {
const result = useExtractIngredientReferences([{ note: "Onion", referenceId: "123" }], [], "A sentence containing On", true);
expect(result).toEqual(new Set());
})
test("when and ingredient matches but in the existing referenceIds, do not return the referenceId", () => {
const result = useExtractIngredientReferences([{ note: "Onion", referenceId: "123" }], ["123"], "A sentence containing Onion", true);
expect(result).toEqual(new Set());
});
test("when an word is 2 letter of shorter, it is ignored", () => {
const result = useExtractIngredientReferences([{ note: "Onion", referenceId: "123" }], [], "A sentence containing On", true);
expect(result).toEqual(new Set());
});
});

View file

@ -1,60 +1,58 @@
import { RecipeIngredient } from "~/lib/api/types/recipe";
import type { RecipeIngredient } from "~/lib/api/types/recipe";
import { parseIngredientText } from "~/composables/recipes";
function normalize(word: string): string {
let normalizing = word;
normalizing = removeTrailingPunctuation(normalizing);
normalizing = removeStartingPunctuation(normalizing);
return normalizing;
let normalizing = word;
normalizing = removeTrailingPunctuation(normalizing);
normalizing = removeStartingPunctuation(normalizing);
return normalizing;
}
function removeTrailingPunctuation(word: string): string {
const punctuationAtEnding = /\p{P}+$/u;
return word.replace(punctuationAtEnding, "");
const punctuationAtEnding = /\p{P}+$/u;
return word.replace(punctuationAtEnding, "");
}
function removeStartingPunctuation(word: string): string {
const punctuationAtBeginning = /^\p{P}+/u;
return word.replace(punctuationAtBeginning, "");
const punctuationAtBeginning = /^\p{P}+/u;
return word.replace(punctuationAtBeginning, "");
}
function ingredientMatchesWord(ingredient: RecipeIngredient, word: string, recipeIngredientAmountsDisabled: boolean) {
const searchText = parseIngredientText(ingredient, recipeIngredientAmountsDisabled);
return searchText.toLowerCase().includes(word.toLowerCase());
const searchText = parseIngredientText(ingredient, recipeIngredientAmountsDisabled);
return searchText.toLowerCase().includes(word.toLowerCase());
}
function isBlackListedWord(word: string) {
// Ignore matching blacklisted words when auto-linking - This is kind of a cludgey implementation. We're blacklisting common words but
// other common phrases trigger false positives and I'm not sure how else to approach this. In the future I maybe look at looking directly
// at the food variable and seeing if the food is in the instructions, but I still need to support those who don't want to provide the value
// and only use the "notes" feature.
const blackListedText: string[] = [
"and",
"the",
"for",
"with",
"without"
];
const blackListedRegexMatch = /\d/gm; // Match Any Number
return blackListedText.includes(word) || word.match(blackListedRegexMatch);
// Ignore matching blacklisted words when auto-linking - This is kind of a cludgey implementation. We're blacklisting common words but
// other common phrases trigger false positives and I'm not sure how else to approach this. In the future I maybe look at looking directly
// at the food variable and seeing if the food is in the instructions, but I still need to support those who don't want to provide the value
// and only use the "notes" feature.
const blackListedText: string[] = [
"and",
"the",
"for",
"with",
"without",
];
const blackListedRegexMatch = /\d/gm; // Match Any Number
return blackListedText.includes(word) || word.match(blackListedRegexMatch);
}
export function useExtractIngredientReferences(recipeIngredients: RecipeIngredient[], activeRefs: string[], text: string, recipeIngredientAmountsDisabled: boolean): Set<string> {
const availableIngredients = recipeIngredients
.filter((ingredient) => ingredient.referenceId !== undefined)
.filter((ingredient) => !activeRefs.includes(ingredient.referenceId as string));
const availableIngredients = recipeIngredients
.filter(ingredient => ingredient.referenceId !== undefined)
.filter(ingredient => !activeRefs.includes(ingredient.referenceId as string));
const allMatchedIngredientIds: string[] = text
.toLowerCase()
.split(/\s/)
.map(normalize)
.filter((word) => word.length > 2)
.filter((word) => !isBlackListedWord(word))
.flatMap((word) => availableIngredients.filter((ingredient) => ingredientMatchesWord(ingredient, word, recipeIngredientAmountsDisabled)))
.map((ingredient) => ingredient.referenceId as string);
// deduplicate
return new Set<string>(allMatchedIngredientIds)
const allMatchedIngredientIds: string[] = text
.toLowerCase()
.split(/\s/)
.map(normalize)
.filter(word => word.length > 2)
.filter(word => !isBlackListedWord(word))
.flatMap(word => availableIngredients.filter(ingredient => ingredientMatchesWord(ingredient, word, recipeIngredientAmountsDisabled)))
.map(ingredient => ingredient.referenceId as string);
// deduplicate
return new Set<string>(allMatchedIngredientIds);
}

View file

@ -14,13 +14,16 @@ function frac(x: number, D: number, mixed: boolean) {
d1 += d2;
n1 += n2;
d2 = D + 1;
} else if (d1 > d2) d2 = D + 1;
}
else if (d1 > d2) d2 = D + 1;
else d1 = D + 1;
break;
} else if (x < m) {
}
else if (x < m) {
n2 = n1 + n2;
d2 = d1 + d2;
} else {
}
else {
n1 = n1 + n2;
d1 = d1 + d2;
}
@ -58,7 +61,8 @@ function cont(x: number, D: number, mixed: boolean) {
if (Q_1 > D) {
Q = Q_2;
P = P_2;
} else {
}
else {
Q = Q_1;
P = P_1;
}

View file

@ -1,6 +1,6 @@
import { describe, test, expect } from "vitest";
import { parseIngredientText } from "./use-recipe-ingredients";
import { RecipeIngredient } from "~/lib/api/types/recipe";
import type { RecipeIngredient } from "~/lib/api/types/recipe";
describe(parseIngredientText.name, () => {
const createRecipeIngredient = (overrides: Partial<RecipeIngredient>): RecipeIngredient => ({
@ -59,7 +59,7 @@ describe(parseIngredientText.name, () => {
const ingredient = createRecipeIngredient({
quantity: 2,
unit: { id: "1", name: "tablespoon", pluralName: "tablespoons", abbreviation: "tbsp", pluralAbbreviation: "tbsps", useAbbreviation: true },
food: { id: "1", name: "diced onion", pluralName: "diced onions" }
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
});
expect(parseIngredientText(ingredient, false)).toEqual("2 tbsps diced onions");
@ -69,7 +69,7 @@ describe(parseIngredientText.name, () => {
const ingredient = createRecipeIngredient({
quantity: 2,
unit: { id: "1", name: "tablespoon", pluralName: "tablespoons", abbreviation: "tbsp", pluralAbbreviation: "tbsps", useAbbreviation: false },
food: { id: "1", name: "diced onion", pluralName: "diced onions" }
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
});
expect(parseIngredientText(ingredient, false)).toEqual("2 tablespoons diced onions");
@ -79,7 +79,7 @@ describe(parseIngredientText.name, () => {
const ingredient = createRecipeIngredient({
quantity: 1,
unit: { id: "1", name: "tablespoon", pluralName: "tablespoons", abbreviation: "tbsp", pluralAbbreviation: "tbsps", useAbbreviation: true },
food: { id: "1", name: "diced onion", pluralName: "diced onions" }
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
});
expect(parseIngredientText(ingredient, false)).toEqual("1 tbsp diced onion");
@ -89,7 +89,7 @@ describe(parseIngredientText.name, () => {
const ingredient = createRecipeIngredient({
quantity: 1,
unit: { id: "1", name: "tablespoon", pluralName: "tablespoons", abbreviation: "tbsp", pluralAbbreviation: "tbsps", useAbbreviation: false },
food: { id: "1", name: "diced onion", pluralName: "diced onions" }
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
});
expect(parseIngredientText(ingredient, false)).toEqual("1 tablespoon diced onion");
@ -99,7 +99,7 @@ describe(parseIngredientText.name, () => {
const ingredient = createRecipeIngredient({
quantity: 0.5,
unit: { id: "1", name: "tablespoon", pluralName: "tablespoons", abbreviation: "tbsp", pluralAbbreviation: "tbsps", useAbbreviation: true },
food: { id: "1", name: "diced onion", pluralName: "diced onions" }
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
});
expect(parseIngredientText(ingredient, false)).toEqual("0.5 tbsp diced onion");
@ -109,7 +109,7 @@ describe(parseIngredientText.name, () => {
const ingredient = createRecipeIngredient({
quantity: 0.5,
unit: { id: "1", name: "tablespoon", pluralName: "tablespoons", abbreviation: "tbsp", pluralAbbreviation: "tbsps", useAbbreviation: false },
food: { id: "1", name: "diced onion", pluralName: "diced onions" }
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
});
expect(parseIngredientText(ingredient, false)).toEqual("0.5 tablespoon diced onion");
@ -119,7 +119,7 @@ describe(parseIngredientText.name, () => {
const ingredient = createRecipeIngredient({
quantity: 0,
unit: { id: "1", name: "tablespoon", pluralName: "tablespoons", abbreviation: "tbsp", pluralAbbreviation: "tbsps", useAbbreviation: false },
food: { id: "1", name: "diced onion", pluralName: "diced onions" }
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
});
expect(parseIngredientText(ingredient, false)).toEqual("diced onions");
@ -129,7 +129,7 @@ describe(parseIngredientText.name, () => {
const ingredient = createRecipeIngredient({
quantity: 1,
unit: { id: "1", name: "tablespoon", pluralName: "tablespoons", abbreviation: "tbsp", pluralAbbreviation: "tbsps", useAbbreviation: false },
food: { id: "1", name: "diced onion", pluralName: "diced onions" }
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
});
expect(parseIngredientText(ingredient, false, 2)).toEqual("2 tablespoons diced onions");

View file

@ -1,6 +1,7 @@
import DOMPurify from "isomorphic-dompurify";
import { useFraction } from "./use-fraction";
import { CreateIngredientFood, CreateIngredientUnit, IngredientFood, IngredientUnit, RecipeIngredient } from "~/lib/api/types/recipe";
import type { CreateIngredientFood, CreateIngredientUnit, IngredientFood, IngredientUnit, RecipeIngredient } from "~/lib/api/types/recipe";
const { frac } = useFraction();
export function sanitizeIngredientHTML(rawHtml: string) {
@ -47,7 +48,7 @@ export function useParsedIngredientText(ingredient: RecipeIngredient, disableAmo
const { quantity, food, unit, note } = ingredient;
const usePluralUnit = quantity !== undefined && ((quantity || 0) * scale > 1 || (quantity || 0) * scale === 0);
const usePluralFood = (!quantity) || quantity * scale > 1
const usePluralFood = (!quantity) || quantity * scale > 1;
let returnQty = "";
@ -55,16 +56,17 @@ export function useParsedIngredientText(ingredient: RecipeIngredient, disableAmo
if (quantity && Number(quantity) !== 0) {
if (unit && !unit.fraction) {
returnQty = Number((quantity * scale).toPrecision(3)).toString();
} else {
}
else {
const fraction = frac(quantity * scale, 10, true);
if (fraction[0] !== undefined && fraction[0] > 0) {
returnQty += fraction[0];
}
if (fraction[1] > 0) {
returnQty += includeFormating ?
`<sup>${fraction[1]}</sup><span>&frasl;</span><sub>${fraction[2]}</sub>` :
` ${fraction[1]}/${fraction[2]}`;
returnQty += includeFormating
? `<sup>${fraction[1]}</sup><span>&frasl;</span><sub>${fraction[2]}</sub>`
: ` ${fraction[1]}/${fraction[2]}`;
}
}
}

View file

@ -1,6 +1,3 @@
import { useContext } from "@nuxtjs/composition-api";
export interface NutritionLabelType {
[key: string]: {
label: string;
@ -9,55 +6,54 @@ export interface NutritionLabelType {
};
};
export function useNutritionLabels() {
const { i18n } = useContext();
const i18n = useI18n();
const labels = <NutritionLabelType>{
calories: {
label: i18n.tc("recipe.calories"),
suffix: i18n.tc("recipe.calories-suffix"),
label: i18n.t("recipe.calories"),
suffix: i18n.t("recipe.calories-suffix"),
},
carbohydrateContent: {
label: i18n.tc("recipe.carbohydrate-content"),
suffix: i18n.tc("recipe.grams"),
label: i18n.t("recipe.carbohydrate-content"),
suffix: i18n.t("recipe.grams"),
},
cholesterolContent: {
label: i18n.tc("recipe.cholesterol-content"),
suffix: i18n.tc("recipe.milligrams"),
label: i18n.t("recipe.cholesterol-content"),
suffix: i18n.t("recipe.milligrams"),
},
fatContent: {
label: i18n.tc("recipe.fat-content"),
suffix: i18n.tc("recipe.grams"),
label: i18n.t("recipe.fat-content"),
suffix: i18n.t("recipe.grams"),
},
fiberContent: {
label: i18n.tc("recipe.fiber-content"),
suffix: i18n.tc("recipe.grams"),
label: i18n.t("recipe.fiber-content"),
suffix: i18n.t("recipe.grams"),
},
proteinContent: {
label: i18n.tc("recipe.protein-content"),
suffix: i18n.tc("recipe.grams"),
label: i18n.t("recipe.protein-content"),
suffix: i18n.t("recipe.grams"),
},
saturatedFatContent: {
label: i18n.tc("recipe.saturated-fat-content"),
suffix: i18n.tc("recipe.grams"),
label: i18n.t("recipe.saturated-fat-content"),
suffix: i18n.t("recipe.grams"),
},
sodiumContent: {
label: i18n.tc("recipe.sodium-content"),
suffix: i18n.tc("recipe.milligrams"),
label: i18n.t("recipe.sodium-content"),
suffix: i18n.t("recipe.milligrams"),
},
sugarContent: {
label: i18n.tc("recipe.sugar-content"),
suffix: i18n.tc("recipe.grams"),
label: i18n.t("recipe.sugar-content"),
suffix: i18n.t("recipe.grams"),
},
transFatContent: {
label: i18n.tc("recipe.trans-fat-content"),
suffix: i18n.tc("recipe.grams"),
label: i18n.t("recipe.trans-fat-content"),
suffix: i18n.t("recipe.grams"),
},
unsaturatedFatContent: {
label: i18n.tc("recipe.unsaturated-fat-content"),
suffix: i18n.tc("recipe.grams"),
label: i18n.t("recipe.unsaturated-fat-content"),
suffix: i18n.t("recipe.grams"),
},
};
return { labels }
return { labels };
}

View file

@ -1,9 +1,9 @@
import { describe, test, expect } from "vitest";
import { ref, Ref } from "@nuxtjs/composition-api";
import { ref } from "vue";
import { useRecipePermissions } from "./use-recipe-permissions";
import { HouseholdSummary } from "~/lib/api/types/household";
import { Recipe } from "~/lib/api/types/recipe";
import { UserOut } from "~/lib/api/types/user";
import type { HouseholdSummary } from "~/lib/api/types/household";
import type { Recipe } from "~/lib/api/types/recipe";
import type { UserOut } from "~/lib/api/types/user";
describe("test use recipe permissions", () => {
const commonUserId = "my-user-id";
@ -67,7 +67,7 @@ describe("test use recipe permissions", () => {
createUser({ id: "other-user-id" }),
);
expect(result.canEditRecipe.value).toBe(true);
}
},
);
test(
@ -79,14 +79,14 @@ describe("test use recipe permissions", () => {
createUser({ id: "other-user-id" }),
);
expect(result.canEditRecipe.value).toBe(true);
}
},
);
test("when user is not recipe owner, and user is other group, cannot edit", () => {
const result = useRecipePermissions(
createRecipe({}),
createRecipeHousehold({}),
createUser({ id: "other-user-id", groupId: "other-group-id"}),
createUser({ id: "other-user-id", groupId: "other-group-id" }),
);
expect(result.canEditRecipe.value).toBe(false);
});
@ -113,7 +113,7 @@ describe("test use recipe permissions", () => {
const result = useRecipePermissions(
createRecipe({}, true),
createRecipeHousehold({}),
createUser({ id: "other-user-id"}),
createUser({ id: "other-user-id" }),
);
expect(result.canEditRecipe.value).toBe(false);
});

View file

@ -1,7 +1,7 @@
import { computed, Ref } from "@nuxtjs/composition-api";
import { Recipe } from "~/lib/api/types/recipe";
import { HouseholdSummary } from "~/lib/api/types/household";
import { UserOut } from "~/lib/api/types/user";
import { computed } from "vue";
import type { Recipe } from "~/lib/api/types/recipe";
import type { HouseholdSummary } from "~/lib/api/types/household";
import type { UserOut } from "~/lib/api/types/user";
export function useRecipePermissions(
recipe: Recipe,
@ -40,5 +40,5 @@ export function useRecipePermissions(
return {
canEditRecipe,
}
};
}

View file

@ -1,8 +1,7 @@
import { Ref, ref } from "@nuxtjs/composition-api";
import { watchDebounced } from "@vueuse/core";
import { UserApi } from "~/lib/api";
import { ExploreApi } from "~/lib/api/public/explore";
import { Recipe } from "~/lib/api/types/recipe";
import type { UserApi } from "~/lib/api";
import type { ExploreApi } from "~/lib/api/public/explore";
import type { Recipe } from "~/lib/api/types/recipe";
export interface UseRecipeSearchReturn {
query: Ref<string>;
@ -54,7 +53,7 @@ export function useRecipeSearch(api: UserApi | ExploreApi): UseRecipeSearchRetur
async (term: string) => {
await searchRecipes(term);
},
{ debounce: 500 }
{ debounce: 500 },
);
async function trigger() {

View file

@ -1,5 +1,4 @@
import { computed, useContext } from "@nuxtjs/composition-api";
import { TimelineEventType } from "~/lib/api/types/recipe";
import type { TimelineEventType } from "~/lib/api/types/recipe";
export interface TimelineEventTypeData {
value: TimelineEventType;
@ -8,22 +7,23 @@ export interface TimelineEventTypeData {
}
export const useTimelineEventTypes = () => {
const { $globals, i18n } = useContext();
const i18n = useI18n();
const { $globals } = useNuxtApp();
const eventTypeOptions = computed<TimelineEventTypeData[]>(() => {
return [
{
value: "comment",
label: i18n.tc("recipe.comment"),
label: i18n.t("recipe.comment"),
icon: $globals.icons.commentTextMultiple,
},
{
value: "info",
label: i18n.tc("settings.theme.info"),
label: i18n.t("settings.theme.info"),
icon: $globals.icons.informationVariant,
},
{
value: "system",
label: i18n.tc("general.system"),
label: i18n.t("general.system"),
icon: $globals.icons.cog,
},
];
@ -31,5 +31,5 @@ export const useTimelineEventTypes = () => {
return {
eventTypeOptions,
}
}
};
};

View file

@ -1,8 +1,7 @@
import { reactive, ref, useAsync } from "@nuxtjs/composition-api";
import { useAsyncKey } from "../use-utils";
import { useUserApi } from "~/composables/api";
import { VForm } from "~/types/vuetify";
import { RecipeTool } from "~/lib/api/types/recipe";
import type { VForm } from "~/types/vuetify";
import type { RecipeTool } from "~/lib/api/types/recipe";
export const useTools = function (eager = true) {
const workingToolData = reactive<RecipeTool>({
@ -18,15 +17,16 @@ export const useTools = function (eager = true) {
const actions = {
getAll() {
loading.value = true;
const units = useAsync(async () => {
const units = useAsyncData(useAsyncKey(), async () => {
const { data } = await api.tools.getAll();
if (data) {
return data.items;
} else {
}
else {
return null;
}
}, useAsyncKey());
});
loading.value = false;
return units;
@ -86,7 +86,8 @@ export const useTools = function (eager = true) {
const tools = (() => {
if (eager) {
return actions.getAll();
} else {
}
else {
return ref([]);
}
})();

View file

@ -1,6 +1,5 @@
import { ref, onMounted } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api";
import { Recipe } from "~/lib/api/types/recipe";
import type { Recipe } from "~/lib/api/types/recipe";
export const useRecipe = function (slug: string, eager = true) {
const api = useUserApi();

View file

@ -1,9 +1,9 @@
import { useAsync, useRouter, ref } from "@nuxtjs/composition-api";
import { ref } from "vue";
import { useAsyncKey } from "../use-utils";
import { usePublicExploreApi } from "~/composables/api/api-client";
import { useUserApi } from "~/composables/api";
import { OrderByNullPosition, Recipe } from "~/lib/api/types/recipe";
import { RecipeSearchQuery } from "~/lib/api/user/recipes/recipe";
import type { OrderByNullPosition, Recipe } from "~/lib/api/types/recipe";
import type { RecipeSearchQuery } from "~/lib/api/user/recipes/recipe";
export const allRecipes = ref<Recipe[]>([]);
export const recentRecipes = ref<Recipe[]>([]);
@ -13,7 +13,7 @@ function getParams(
orderDirection = "desc",
orderByNullPosition: OrderByNullPosition | null = null,
query: RecipeSearchQuery | null = null,
queryFilter: string | null = null
queryFilter: string | null = null,
) {
return {
orderBy,
@ -53,7 +53,6 @@ export const useLazyRecipes = function (publicGroupSlug: string | null = null) {
query: RecipeSearchQuery | null = null,
queryFilter: string | null = null,
) {
const { data, error } = await api.recipes.getAll(
page,
perPage,
@ -113,7 +112,7 @@ export const useRecipes = (
fetchRecipes = true,
loadFood = false,
queryFilter: string | null = null,
publicGroupSlug: string | null = null
publicGroupSlug: string | null = null,
) => {
const api = publicGroupSlug ? usePublicExploreApi(publicGroupSlug).explore : useUserApi();
@ -125,7 +124,8 @@ export const useRecipes = (
page: 1,
perPage: -1,
};
} else {
}
else {
return {
recipes: recentRecipes,
page: 1,
@ -142,9 +142,9 @@ export const useRecipes = (
}
function getAllRecipes() {
useAsync(async () => {
useAsyncData(useAsyncKey(), async () => {
await refreshRecipes();
}, useAsyncKey());
});
}
function assignSorted(val: Array<Recipe>) {

View file

@ -11,11 +11,11 @@ function formatQuantity(val: number): string {
const fraction = frac(val, 10, true);
if (fraction[0] !== undefined && fraction[0] > 0) {
valString += fraction[0];
valString += fraction[0];
}
if (fraction[1] > 0) {
valString += `<sup>${fraction[1]}</sup><span>&frasl;</span><sub>${fraction[2]}</sub>`;
valString += `<sup>${fraction[1]}</sup><span>&frasl;</span><sub>${fraction[2]}</sub>`;
}
return valString.trim();

View file

@ -1,6 +1,5 @@
import { ref, Ref } from "@nuxtjs/composition-api";
import { useData, useReadOnlyStore, useStore } from "../partials/use-store-factory";
import { RecipeCategory } from "~/lib/api/types/recipe";
import type { RecipeCategory } from "~/lib/api/types/recipe";
import { usePublicExploreApi, useUserApi } from "~/composables/api";
const store: Ref<RecipeCategory[]> = ref([]);
@ -13,14 +12,14 @@ export const useCategoryData = function () {
name: "",
slug: "",
});
}
};
export const useCategoryStore = function () {
const api = useUserApi();
return useStore<RecipeCategory>(store, loading, api.categories);
}
};
export const usePublicCategoryStore = function (groupSlug: string) {
const api = usePublicExploreApi(groupSlug).explore;
return useReadOnlyStore<RecipeCategory>(store, publicLoading, api.categories);
}
};

View file

@ -1,6 +1,5 @@
import { ref, Ref } from "@nuxtjs/composition-api";
import { useData, useReadOnlyStore, useStore } from "../partials/use-store-factory";
import { IngredientFood } from "~/lib/api/types/recipe";
import type { IngredientFood } from "~/lib/api/types/recipe";
import { usePublicExploreApi, useUserApi } from "~/composables/api";
const store: Ref<IngredientFood[]> = ref([]);
@ -14,14 +13,14 @@ export const useFoodData = function () {
description: "",
labelId: undefined,
});
}
};
export const useFoodStore = function () {
const api = useUserApi();
return useStore<IngredientFood>(store, loading, api.foods);
}
};
export const usePublicFoodStore = function (groupSlug: string) {
const api = usePublicExploreApi(groupSlug).explore;
return useReadOnlyStore<IngredientFood>(store, publicLoading, api.foods);
}
};

View file

@ -1,6 +1,5 @@
import { ref, Ref } from "@nuxtjs/composition-api";
import { useReadOnlyStore } from "../partials/use-store-factory";
import { HouseholdSummary } from "~/lib/api/types/household";
import type { HouseholdSummary } from "~/lib/api/types/household";
import { usePublicExploreApi, useUserApi } from "~/composables/api";
const store: Ref<HouseholdSummary[]> = ref([]);
@ -10,9 +9,9 @@ const publicLoading = ref(false);
export const useHouseholdStore = function () {
const api = useUserApi();
return useReadOnlyStore<HouseholdSummary>(store, loading, api.households);
}
};
export const usePublicHouseholdStore = function (groupSlug: string) {
const api = usePublicExploreApi(groupSlug).explore;
return useReadOnlyStore<HouseholdSummary>(store, publicLoading, api.households);
}
};

View file

@ -1,6 +1,5 @@
import { ref, Ref } from "@nuxtjs/composition-api";
import { useData, useStore } from "../partials/use-store-factory";
import { MultiPurposeLabelOut } from "~/lib/api/types/labels";
import type { MultiPurposeLabelOut } from "~/lib/api/types/labels";
import { useUserApi } from "~/composables/api";
const store: Ref<MultiPurposeLabelOut[]> = ref([]);
@ -13,9 +12,9 @@ export const useLabelData = function () {
name: "",
color: "",
});
}
};
export const useLabelStore = function () {
const api = useUserApi();
return useStore<MultiPurposeLabelOut>(store, loading, api.multiPurposeLabels);
}
};

View file

@ -1,6 +1,5 @@
import { ref, Ref } from "@nuxtjs/composition-api";
import { useData, useReadOnlyStore, useStore } from "../partials/use-store-factory";
import { RecipeTag } from "~/lib/api/types/recipe";
import type { RecipeTag } from "~/lib/api/types/recipe";
import { usePublicExploreApi, useUserApi } from "~/composables/api";
const store: Ref<RecipeTag[]> = ref([]);
@ -13,14 +12,14 @@ export const useTagData = function () {
name: "",
slug: "",
});
}
};
export const useTagStore = function () {
const api = useUserApi();
return useStore<RecipeTag>(store, loading, api.tags);
}
};
export const usePublicTagStore = function (groupSlug: string) {
const api = usePublicExploreApi(groupSlug).explore;
return useReadOnlyStore<RecipeTag>(store, publicLoading, api.tags);
}
};

View file

@ -1,6 +1,5 @@
import { ref, Ref } from "@nuxtjs/composition-api";
import { useData, useReadOnlyStore, useStore } from "../partials/use-store-factory";
import { RecipeTool } from "~/lib/api/types/recipe";
import type { RecipeTool } from "~/lib/api/types/recipe";
import { usePublicExploreApi, useUserApi } from "~/composables/api";
interface RecipeToolWithOnHand extends RecipeTool {
@ -19,14 +18,14 @@ export const useToolData = function () {
onHand: false,
householdsWithTool: [],
});
}
};
export const useToolStore = function () {
const api = useUserApi();
return useStore<RecipeTool>(store, loading, api.tools);
}
};
export const usePublicToolStore = function (groupSlug: string) {
const api = usePublicExploreApi(groupSlug).explore;
return useReadOnlyStore<RecipeTool>(store, publicLoading, api.tools);
}
};

View file

@ -1,6 +1,5 @@
import { ref, Ref } from "@nuxtjs/composition-api";
import { useData, useStore } from "../partials/use-store-factory";
import { IngredientUnit } from "~/lib/api/types/recipe";
import type { IngredientUnit } from "~/lib/api/types/recipe";
import { useUserApi } from "~/composables/api";
const store: Ref<IngredientUnit[]> = ref([]);
@ -14,9 +13,9 @@ export const useUnitData = function () {
abbreviation: "",
description: "",
});
}
};
export const useUnitStore = function () {
const api = useUserApi();
return useStore<IngredientUnit>(store, loading, api.units);
}
};

View file

@ -1,7 +1,6 @@
import { ref, Ref } from "@nuxtjs/composition-api";
import { useReadOnlyStore } from "../partials/use-store-factory";
import { useRequests } from "../api/api-client";
import { UserSummary } from "~/lib/api/types/user";
import type { UserSummary } from "~/lib/api/types/user";
import { BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients";
const store: Ref<UserSummary[]> = ref([]);
@ -16,5 +15,5 @@ export const useUserStore = function () {
const requests = useRequests();
const api = new GroupUserAPIReadOnly(requests);
return useReadOnlyStore<UserSummary>(store, loading, api, {orderBy: "full_name"});
}
return useReadOnlyStore<UserSummary>(store, loading, api, { orderBy: "full_name" });
};

View file

@ -1,6 +1,5 @@
import { useAsync, ref, reactive } from "@nuxtjs/composition-api";
import { toastLoading, loader } from "./use-toast";
import { AllBackups, BackupOptions } from "~/lib/api/types/admin";
import type { AllBackups, BackupOptions } from "~/lib/api/types/admin";
import { useUserApi } from "~/composables/api";
interface ImportBackup {
@ -54,7 +53,7 @@ export const useBackups = function (fetch = true) {
});
function getBackups() {
const backups = useAsync(async () => {
const backups = useAsyncData(async () => {
const { data } = await api.backups.getAll();
return data;
});

View file

@ -1,5 +1,3 @@
import { useContext } from "@nuxtjs/composition-api";
export interface ContextMenuItem {
title: string;
icon: string;
@ -14,21 +12,22 @@ export interface ContextMenuPresets {
}
export function useContextPresets(): ContextMenuPresets {
const { $globals, i18n } = useContext();
const i18n = useI18n();
const { $globals } = useNuxtApp();
return {
delete: {
title: i18n.tc("general.delete"),
title: i18n.t("general.delete"),
icon: $globals.icons.delete,
event: "delete",
},
edit: {
title: i18n.tc("general.edit"),
title: i18n.t("general.edit"),
icon: $globals.icons.edit,
event: "edit",
},
save: {
title: i18n.tc("general.save"),
title: i18n.t("general.save"),
icon: $globals.icons.save,
event: "save",
},

View file

@ -1,23 +1,22 @@
import { useContext } from "@nuxtjs/composition-api";
import { useClipboard } from "@vueuse/core";
import { alert } from "./use-toast";
export function useCopy() {
const { copy, copied, isSupported } = useClipboard();
const { i18n } = useContext();
const i18n = useI18n();
function copyText(text: string) {
if (!isSupported.value) {
alert.error(i18n.tc("general.clipboard-not-supported"));
alert.error(i18n.t("general.clipboard-not-supported"));
return;
}
copy(text).then(() => {
// Verify copy success as no error is thrown on failure.
if (copied.value) {
alert.success(i18n.tc("general.copied-to-clipboard"));
alert.success(i18n.t("general.copied-to-clipboard"));
}
else {
alert.error(i18n.tc("general.clipboard-copy-failure"));
alert.error(i18n.t("general.clipboard-copy-failure"));
}
});
}
@ -27,11 +26,11 @@ export function useCopy() {
export function useCopyList() {
const { copy, isSupported, copied } = useClipboard();
const { i18n } = useContext();
const i18n = useI18n();
function checkClipboard() {
if (!isSupported.value) {
alert.error(i18n.tc("general.your-browser-does-not-support-clipboard"));
alert.error(i18n.t("general.your-browser-does-not-support-clipboard"));
return false;
}
@ -48,14 +47,14 @@ export function useCopyList() {
function copyMarkdown(list: string[]) {
if (!checkClipboard()) return;
const text = list.map((item) => `- ${item}`).join("\n");
const text = list.map(item => `- ${item}`).join("\n");
copyText(text, list.length);
}
function copyMarkdownCheckList(list: string[]) {
if (!checkClipboard()) return;
const text = list.map((item) => `- [ ] ${item}`).join("\n");
const text = list.map(item => `- [ ] ${item}`).join("\n");
copyText(text, list.length);
}
@ -63,10 +62,10 @@ export function useCopyList() {
copy(text).then(() => {
// Verify copy success as no error is thrown on failure.
if (copied.value) {
alert.success(i18n.tc("general.copied-items-to-clipboard", len));
alert.success(i18n.t("general.copied-items-to-clipboard", len));
}
else {
alert.error(i18n.tc("general.clipboard-copy-failure"));
alert.error(i18n.t("general.clipboard-copy-failure"));
}
});
}

View file

@ -1,9 +1,8 @@
import { useAsync, ref, Ref, useContext } from "@nuxtjs/composition-api";
import { useAsyncKey } from "./use-utils";
import { usePublicExploreApi } from "./api/api-client";
import { useHouseholdSelf } from "./use-households";
import { useUserApi } from "~/composables/api";
import { ReadCookBook, UpdateCookBook } from "~/lib/api/types/cookbook";
import type { ReadCookBook, UpdateCookBook } from "~/lib/api/types/cookbook";
let cookbookStore: Ref<ReadCookBook[] | null> | null = null;
@ -12,11 +11,11 @@ export const useCookbook = function (publicGroupSlug: string | null = null) {
// passing the group slug switches to using the public API
const api = publicGroupSlug ? usePublicExploreApi(publicGroupSlug).explore : useUserApi();
const units = useAsync(async () => {
const { data: units } = useAsyncData(useAsyncKey(), async () => {
const { data } = await api.cookbooks.getOne(id);
return data;
}, useAsyncKey());
});
return units;
}
@ -31,15 +30,16 @@ export const usePublicCookbooks = function (groupSlug: string) {
const actions = {
getAll() {
loading.value = true;
const units = useAsync(async () => {
const { data: units } = useAsyncData(useAsyncKey(), async () => {
const { data } = await api.cookbooks.getAll(1, -1, { orderBy: "position", orderDirection: "asc" });
if (data) {
return data.items;
} else {
}
else {
return null;
}
}, useAsyncKey());
});
loading.value = false;
return units;
@ -64,27 +64,28 @@ export const usePublicCookbooks = function (groupSlug: string) {
}
return { cookbooks: cookbookStore, actions };
}
};
export const useCookbooks = function () {
const api = useUserApi();
const { household } = useHouseholdSelf();
const loading = ref(false);
const { i18n } = useContext();
const i18n = useI18n();
const actions = {
getAll() {
loading.value = true;
const units = useAsync(async () => {
const { data: units } = useAsyncData(useAsyncKey(), async () => {
const { data } = await api.cookbooks.getAll(1, -1, { orderBy: "position", orderDirection: "asc" });
if (data) {
return data.items;
} else {
}
else {
return null;
}
}, useAsyncKey());
});
loading.value = false;
return units;
@ -108,7 +109,8 @@ export const useCookbooks = function () {
});
if (data && cookbookStore?.value) {
cookbookStore.value.push(data);
} else {
}
else {
this.refreshAll();
}

View file

@ -1,27 +1,26 @@
import { useAsync, ref, Ref, watch, useContext } from "@nuxtjs/composition-api";
import { format } from "date-fns";
import { useAsyncKey } from "./use-utils";
import { useUserApi } from "~/composables/api";
import { CreatePlanEntry, PlanEntryType, UpdatePlanEntry } from "~/lib/api/types/meal-plan";
import type { CreatePlanEntry, PlanEntryType, UpdatePlanEntry } from "~/lib/api/types/meal-plan";
type PlanOption = {
text: string;
value: PlanEntryType;
};
export function usePlanTypeOptions() {
const { i18n } = useContext();
const i18n = useI18n();
return [
{ text: i18n.tc("meal-plan.breakfast"), value: "breakfast" },
{ text: i18n.tc("meal-plan.lunch"), value: "lunch" },
{ text: i18n.tc("meal-plan.dinner"), value: "dinner" },
{ text: i18n.tc("meal-plan.side"), value: "side" },
{ text: i18n.t("meal-plan.breakfast"), value: "breakfast" },
{ text: i18n.t("meal-plan.lunch"), value: "lunch" },
{ text: i18n.t("meal-plan.dinner"), value: "dinner" },
{ text: i18n.t("meal-plan.side"), value: "side" },
] as PlanOption[];
}
export function getEntryTypeText(value: PlanEntryType) {
const { i18n } = useContext();
return i18n.tc("meal-plan." + value);
const i18n = useI18n();
return i18n.t("meal-plan." + value);
}
export interface DateRange {
start: Date;
@ -36,7 +35,7 @@ export const useMealplans = function (range: Ref<DateRange>) {
const actions = {
getAll() {
loading.value = true;
const units = useAsync(async () => {
const { data: units } = useAsyncData(useAsyncKey(), async () => {
const query = {
start_date: format(range.value.start, "yyyy-MM-dd"),
end_date: format(range.value.end, "yyyy-MM-dd"),
@ -45,15 +44,16 @@ export const useMealplans = function (range: Ref<DateRange>) {
if (data) {
return data.items;
} else {
}
else {
return null;
}
}, useAsyncKey());
});
loading.value = false;
return units;
},
async refreshAll(this: void) {
async refreshAll() {
loading.value = true;
const query = {
start_date: format(range.value.start, "yyyy-MM-dd"),

View file

@ -1,10 +1,9 @@
import { computed, reactive, ref } from "@nuxtjs/composition-api";
import { useStoreActions } from "./partials/use-actions-factory";
import { useUserApi } from "~/composables/api";
import { GroupRecipeActionOut, GroupRecipeActionType } from "~/lib/api/types/household";
import { RequestResponse } from "~/lib/api/types/non-generated";
import { Recipe } from "~/lib/api/types/recipe";
import { useScaledAmount } from "~/composables/recipes/use-scaled-amount";
import type { GroupRecipeActionOut, GroupRecipeActionType } from "~/lib/api/types/household";
import type { RequestResponse } from "~/lib/api/types/non-generated";
import type { Recipe } from "~/lib/api/types/recipe";
const groupRecipeActions = ref<GroupRecipeActionOut[] | null>(null);
const loading = ref(false);
@ -31,8 +30,8 @@ export function useGroupRecipeActionData() {
}
export const useGroupRecipeActions = function (
orderBy: string | null = "title",
orderDirection: string | null = "asc",
orderBy: string | null = "title",
orderDirection: string | null = "asc",
) {
const api = useUserApi();
@ -51,17 +50,16 @@ export const useGroupRecipeActions = function (
const recipeServings = (recipe.recipeServings || 1) * recipeScale;
const recipeYieldQuantity = (recipe.recipeYieldQuantity || 1) * recipeScale;
/* eslint-disable no-template-curly-in-string */
return url
.replace("${url}", window.location.href)
.replace("${id}", recipe.id || "")
.replace("${slug}", recipe.slug || "")
.replace("${servings}", recipeServings.toString())
.replace("${yieldQuantity}", recipeYieldQuantity.toString())
.replace("${yieldText}", recipe.recipeYield || "")
/* eslint-enable no-template-curly-in-string */
.replace("${yieldText}", recipe.recipeYield || "");
};
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
async function execute(action: GroupRecipeActionOut, recipe: Recipe, recipeScale: number): Promise<void | RequestResponse<unknown>> {
const url = parseRecipeActionUrl(action.url, recipe, recipeScale);
@ -84,8 +82,8 @@ export const useGroupRecipeActions = function (
...useStoreActions<GroupRecipeActionOut>(api.groupRecipeActions, groupRecipeActions, loading),
flushStore() {
groupRecipeActions.value = [];
}
}
},
};
return {
actions,

View file

@ -1,7 +1,6 @@
import { useAsync, ref } from "@nuxtjs/composition-api";
import { useAsyncKey } from "./use-utils";
import { useUserApi } from "~/composables/api";
import { ReadWebhook } from "~/lib/api/types/household";
import type { ReadWebhook } from "~/lib/api/types/household";
export const useGroupWebhooks = function () {
const api = useUserApi();
@ -11,15 +10,16 @@ export const useGroupWebhooks = function () {
const actions = {
getAll() {
loading.value = true;
const units = useAsync(async () => {
const { data: units } = useAsyncData(useAsyncKey(), async () => {
const { data } = await api.groupWebhooks.getAll();
if (data) {
return data.items;
} else {
}
else {
return null;
}
}, useAsyncKey());
});
loading.value = false;
return units;
@ -91,7 +91,7 @@ export const useGroupWebhooks = function () {
loading.value = true;
await api.groupWebhooks.testOne(id);
loading.value = false;
}
},
};
const webhooks = actions.getAll();

View file

@ -1,6 +1,5 @@
import { useAsync, ref } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api";
import { GroupBase, GroupSummary } from "~/lib/api/types/user";
import type { GroupBase, GroupSummary } from "~/lib/api/types/user";
const groupSelfRef = ref<GroupSummary | null>(null);
const loading = ref(false);
@ -50,15 +49,16 @@ export const useGroups = function () {
function getAllGroups() {
loading.value = true;
const asyncKey = String(Date.now());
const groups = useAsync(async () => {
const { data } = await api.groups.getAll(1, -1, {orderBy: "name", orderDirection: "asc"});;
const { data: groups } = useAsyncData(asyncKey, async () => {
const { data } = await api.groups.getAll(1, -1, { orderBy: "name", orderDirection: "asc" }); ;
if (data) {
return data.items;
} else {
}
else {
return null;
}
}, asyncKey);
});
loading.value = false;
return groups;
@ -66,11 +66,12 @@ export const useGroups = function () {
async function refreshAllGroups() {
loading.value = true;
const { data } = await api.groups.getAll(1, -1, {orderBy: "name", orderDirection: "asc"});;
const { data } = await api.groups.getAll(1, -1, { orderBy: "name", orderDirection: "asc" }); ;
if (data) {
groups.value = data.items;
} else {
}
else {
groups.value = null;
}

View file

@ -1,6 +1,5 @@
import { computed, ref, Ref, useAsync } from "@nuxtjs/composition-api";
import { useAdminApi, useUserApi } from "~/composables/api";
import { HouseholdCreate, HouseholdInDB } from "~/lib/api/types/household";
import type { HouseholdCreate, HouseholdInDB } from "~/lib/api/types/household";
const householdSelfRef = ref<HouseholdInDB | null>(null);
const loading = ref(false);
@ -53,15 +52,16 @@ export const useAdminHouseholds = function () {
function getAllHouseholds() {
loading.value = true;
const asyncKey = String(Date.now());
const households = useAsync(async () => {
const { data } = await api.households.getAll(1, -1, {orderBy: "name, group.name", orderDirection: "asc"});
const { data: households } = useAsyncData(asyncKey, async () => {
const { data } = await api.households.getAll(1, -1, { orderBy: "name, group.name", orderDirection: "asc" });
if (data) {
return data.items;
} else {
}
else {
return null;
}
}, asyncKey);
});
loading.value = false;
return households;
@ -69,12 +69,13 @@ export const useAdminHouseholds = function () {
async function refreshAllHouseholds() {
loading.value = true;
const { data } = await api.households.getAll(1, -1, {orderBy: "name, group.name", orderDirection: "asc"});;
const { data } = await api.households.getAll(1, -1, { orderBy: "name, group.name", orderDirection: "asc" }); ;
if (data) {
households.value = data.items;
} else {
households.value = null;
}
else {
households.value = null;
}
loading.value = false;
@ -93,7 +94,7 @@ export const useAdminHouseholds = function () {
const { data } = await api.households.createOne(payload);
if (data && households.value) {
households.value.push(data);
households.value.push(data);
}
}
@ -102,8 +103,8 @@ export const useAdminHouseholds = function () {
return computed(
() => {
return (households.value && groupIdRef.value)
? households.value.filter((h) => h.groupId === groupIdRef.value)
: [];
? households.value.filter(h => h.groupId === groupIdRef.value)
: [];
},
);
}

View file

@ -246,4 +246,4 @@ export const LOCALES = [
progress: 90,
dir: "ltr",
},
]
];

View file

@ -1,39 +1,29 @@
import { computed, useContext } from "@nuxtjs/composition-api";
import type { LocaleObject } from "@nuxtjs/i18n";
import { LOCALES } from "./available-locales";
export const useLocales = () => {
const { i18n, $vuetify } = useContext();
const i18n = useI18n();
function getLocale(value: string) {
const currentLocale = LOCALES.filter((locale) => locale.value === value);
return currentLocale.length ? currentLocale[0] : null;
}
const { isRtl } = useRtl();
const { current: vuetifyLocale } = useLocale();
const locale = computed<string>({
get() {
// dirty hack
$vuetify.lang.current = i18n.locale;
const currentLocale = getLocale(i18n.locale);
if (currentLocale) {
$vuetify.rtl = currentLocale.dir === "rtl";
}
return i18n.locale;
},
const locale = computed<LocaleObject["code"]>({
get: () => i18n.locale.value,
set(value) {
i18n.setLocale(value);
// this does not persist after window reload :-(
$vuetify.lang.current = value;
const currentLocale = getLocale(value);
if (currentLocale) {
$vuetify.rtl = currentLocale.dir === "rtl";
}
// Reload the page to update the language - not all strings are reactive
window.location.reload();
},
});
// auto update vuetify locale
watch(locale, (lc) => {
vuetifyLocale.value = lc;
});
// auto update rtl
watch(vuetifyLocale, (vl) => {
const currentLocale = LOCALES.find(lc => lc.value === vl);
if (currentLocale) {
isRtl.value = currentLocale.dir === "rtl";
}
});
return {
locale,

View file

@ -1,17 +1,16 @@
import { computed, useContext, useRoute } from "@nuxtjs/composition-api";
export const useLoggedInState = function () {
const { $auth } = useContext();
const $auth = useMealieAuth();
const route = useRoute();
const loggedIn = computed(() => $auth.loggedIn);
const loggedIn = computed(() => $auth.loggedIn.value);
const isOwnGroup = computed(() => {
if (!route.value.params.groupSlug) {
if (!route.params.groupSlug) {
return loggedIn.value;
} else {
return loggedIn.value && $auth.user?.groupSlug === route.value.params.groupSlug;
}
else {
return loggedIn.value && $auth.user.value?.groupSlug === route.params.groupSlug;
}
});
return { loggedIn, isOwnGroup };
}
};

View file

@ -1,5 +1,5 @@
export function useNavigationWarning() {
return { activateNavigationWarning, deactivateNavigationWarning };
return { activateNavigationWarning, deactivateNavigationWarning };
}
/**
@ -9,12 +9,12 @@ export function useNavigationWarning() {
* or closing the tab.
*/
const activateNavigationWarning = () => {
window.onbeforeunload = () => true;
}
window.onbeforeunload = () => true;
};
/**
* Disables the warning when navigating to a page
*/
const deactivateNavigationWarning = () => {
window.onbeforeunload = null;
}
window.onbeforeunload = null;
};

View file

@ -1,15 +1,14 @@
import { ref } from "@nuxtjs/composition-api";
import { ref } from "vue";
import { describe, expect, test } from "vitest";
import { usePasswordStrength } from "./use-passwords";
import { stubI18n } from "~/tests/utils";
describe("test usePasswordStrength", () => {
test("weak password", () => {
const pw = ref("123456");
const result = usePasswordStrength(pw, stubI18n());
const { score, strength, color } = result
const { score, strength, color } = result;
expect(score.value).toBeGreaterThan(0);
expect(score.value).toBeLessThan(40);

View file

@ -1,10 +1,10 @@
import { computed, Ref, ref, useContext } from "@nuxtjs/composition-api";
import VueI18n from "vue-i18n";
import { computed } from "vue";
import type { VueI18n } from "vue-i18n";
import { scorePassword } from "~/lib/validators";
export function usePasswordField() {
const show = ref(false);
const { $globals } = useContext();
const { $globals } = useNuxtApp();
const passwordIcon = computed(() => {
return show.value ? $globals.icons.eyeOff : $globals.icons.eye;
@ -26,24 +26,30 @@ export const usePasswordStrength = (password: Ref<string>, i18n: VueI18n) => {
const score = computed(() => scorePassword(password.value));
const strength = computed(() => {
if (score.value < 50) {
return i18n.tc("user.password-strength-values.weak");
} else if (score.value < 80) {
return i18n.tc("user.password-strength-values.good");
} else if (score.value < 100) {
return i18n.tc("user.password-strength-values.strong");
} else {
return i18n.tc("user.password-strength-values.very-strong");
return i18n.t("user.password-strength-values.weak");
}
else if (score.value < 80) {
return i18n.t("user.password-strength-values.good");
}
else if (score.value < 100) {
return i18n.t("user.password-strength-values.strong");
}
else {
return i18n.t("user.password-strength-values.very-strong");
}
});
const color = computed(() => {
if (score.value < 50) {
return "error";
} else if (score.value < 80) {
}
else if (score.value < 80) {
return "warning";
} else if (score.value < 100) {
}
else if (score.value < 100) {
return "info";
} else {
}
else {
return "success";
}
});

View file

@ -1,6 +1,5 @@
import { computed, useContext } from "@nuxtjs/composition-api";
import { Organizer, RecipeOrganizer } from "~/lib/api/types/non-generated";
import { LogicalOperator, RelationalKeyword, RelationalOperator } from "~/lib/api/types/response";
import { Organizer, type RecipeOrganizer } from "~/lib/api/types/non-generated";
import type { LogicalOperator, RelationalKeyword, RelationalOperator } from "~/lib/api/types/response";
export interface FieldLogicalOperator {
label: string;
@ -60,16 +59,16 @@ export interface Field extends FieldDefinition {
}
export function useQueryFilterBuilder() {
const { i18n } = useContext();
const i18n = useI18n();
const logOps = computed<Record<LogicalOperator, FieldLogicalOperator>>(() => {
const AND = {
label: i18n.tc("query-filter.logical-operators.and"),
label: i18n.t("query-filter.logical-operators.and"),
value: "AND",
} as FieldLogicalOperator;
const OR = {
label: i18n.tc("query-filter.logical-operators.or"),
label: i18n.t("query-filter.logical-operators.or"),
value: "OR",
} as FieldLogicalOperator;
@ -81,71 +80,70 @@ export function useQueryFilterBuilder() {
const relOps = computed<Record<RelationalKeyword | RelationalOperator, FieldRelationalOperator>>(() => {
const EQ = {
label: i18n.tc("query-filter.relational-operators.equals"),
label: i18n.t("query-filter.relational-operators.equals"),
value: "=",
} as FieldRelationalOperator;
const NOT_EQ = {
label: i18n.tc("query-filter.relational-operators.does-not-equal"),
label: i18n.t("query-filter.relational-operators.does-not-equal"),
value: "<>",
} as FieldRelationalOperator;
const GT = {
label: i18n.tc("query-filter.relational-operators.is-greater-than"),
label: i18n.t("query-filter.relational-operators.is-greater-than"),
value: ">",
} as FieldRelationalOperator;
const GTE = {
label: i18n.tc("query-filter.relational-operators.is-greater-than-or-equal-to"),
label: i18n.t("query-filter.relational-operators.is-greater-than-or-equal-to"),
value: ">=",
} as FieldRelationalOperator;
const LT = {
label: i18n.tc("query-filter.relational-operators.is-less-than"),
label: i18n.t("query-filter.relational-operators.is-less-than"),
value: "<",
} as FieldRelationalOperator;
const LTE = {
label: i18n.tc("query-filter.relational-operators.is-less-than-or-equal-to"),
label: i18n.t("query-filter.relational-operators.is-less-than-or-equal-to"),
value: "<=",
} as FieldRelationalOperator;
const IS = {
label: i18n.tc("query-filter.relational-keywords.is"),
label: i18n.t("query-filter.relational-keywords.is"),
value: "IS",
} as FieldRelationalOperator;
const IS_NOT = {
label: i18n.tc("query-filter.relational-keywords.is-not"),
label: i18n.t("query-filter.relational-keywords.is-not"),
value: "IS NOT",
} as FieldRelationalOperator;
const IN = {
label: i18n.tc("query-filter.relational-keywords.is-one-of"),
label: i18n.t("query-filter.relational-keywords.is-one-of"),
value: "IN",
} as FieldRelationalOperator;
const NOT_IN = {
label: i18n.tc("query-filter.relational-keywords.is-not-one-of"),
label: i18n.t("query-filter.relational-keywords.is-not-one-of"),
value: "NOT IN",
} as FieldRelationalOperator;
const CONTAINS_ALL = {
label: i18n.tc("query-filter.relational-keywords.contains-all-of"),
label: i18n.t("query-filter.relational-keywords.contains-all-of"),
value: "CONTAINS ALL",
} as FieldRelationalOperator;
const LIKE = {
label: i18n.tc("query-filter.relational-keywords.is-like"),
label: i18n.t("query-filter.relational-keywords.is-like"),
value: "LIKE",
} as FieldRelationalOperator;
const NOT_LIKE = {
label: i18n.tc("query-filter.relational-keywords.is-not-like"),
label: i18n.t("query-filter.relational-keywords.is-not-like"),
value: "NOT LIKE",
} as FieldRelationalOperator;
/* eslint-disable object-shorthand */
return {
"=": EQ,
"<>": NOT_EQ,
@ -161,22 +159,20 @@ export function useQueryFilterBuilder() {
"LIKE": LIKE,
"NOT LIKE": NOT_LIKE,
};
/* eslint-enable object-shorthand */
});
function isOrganizerType(type: FieldType): type is Organizer {
return (
type === Organizer.Category ||
type === Organizer.Tag ||
type === Organizer.Tool ||
type === Organizer.Food ||
type === Organizer.Household
type === Organizer.Category
|| type === Organizer.Tag
|| type === Organizer.Tool
|| type === Organizer.Food
|| type === Organizer.Household
);
};
function getFieldFromFieldDef(field: Field | FieldDefinition, resetValue = false): Field {
/* eslint-disable dot-notation */
const updatedField = {logicalOperator: logOps.value.AND, ...field} as Field;
const updatedField = { logicalOperator: logOps.value.AND, ...field } as Field;
let operatorOptions: FieldRelationalOperator[];
if (updatedField.fieldOptions?.length || isOrganizerType(updatedField.type)) {
operatorOptions = [
@ -184,7 +180,8 @@ export function useQueryFilterBuilder() {
relOps.value["NOT IN"],
relOps.value["CONTAINS ALL"],
];
} else {
}
else {
switch (updatedField.type) {
case "string":
operatorOptions = [
@ -209,7 +206,7 @@ export function useQueryFilterBuilder() {
break;
case "date":
operatorOptions = [
relOps.value["="],
relOps.value["="],
relOps.value["<>"],
relOps.value[">"],
relOps.value[">="],
@ -230,14 +227,14 @@ export function useQueryFilterBuilder() {
updatedField.value = "";
updatedField.values = [];
updatedField.organizers = [];
} else {
}
else {
updatedField.value = updatedField.value || "";
updatedField.values = updatedField.values || [];
updatedField.organizers = updatedField.organizers || [];
}
return updatedField;
/* eslint-enable dot-notation */
};
function buildQueryFilterString(fields: Field[], useParenthesis: boolean): string {
@ -261,13 +258,15 @@ export function useQueryFilterBuilder() {
if (field.label) {
parts.push(field.name);
} else {
}
else {
isValid = false;
}
if (field.relationalOperatorValue) {
parts.push(field.relationalOperatorValue.value);
} else if (field.type !== "boolean") {
}
else if (field.type !== "boolean") {
isValid = false;
}
@ -275,23 +274,29 @@ export function useQueryFilterBuilder() {
if (field.values?.length) {
let val: string;
if (field.type === "string" || field.type === "date" || isOrganizerType(field.type)) {
val = field.values.map((value) => `"${value.toString()}"`).join(",");
} else {
val = field.values.map(value => `"${value.toString()}"`).join(",");
}
else {
val = field.values.join(",");
}
parts.push(`[${val}]`);
} else {
}
else {
isValid = false;
}
} else if (field.value) {
}
else if (field.value) {
if (field.type === "string" || field.type === "date") {
parts.push(`"${field.value.toString()}"`);
} else {
}
else {
parts.push(field.value.toString());
}
} else if (field.type === "boolean") {
}
else if (field.type === "boolean") {
parts.push("false");
} else {
}
else {
isValid = false;
}

View file

@ -1,5 +1,3 @@
import { useRoute, WritableComputedRef, computed, nextTick, useRouter } from "@nuxtjs/composition-api";
export function useRouterQuery(query: string) {
const router = useRoute();
// TODO FUTURE: Remove when migrating to Vue 3
@ -7,11 +5,10 @@ export function useRouterQuery(query: string) {
const param: WritableComputedRef<string> = computed({
get(): string {
console.log("Get Query Change");
// @ts-ignore For some reason, this could also return an array
return router.value?.query[query] || "";
return router?.query[query] as string || "";
},
set(v: string): void {
router.value.query[query] = v;
router.query[query] = v;
},
});
@ -24,13 +21,13 @@ export function useRouteQuery<T extends string | string[]>(name: string, default
return computed<any>({
get() {
const data = route.value.query[name];
const data = route.query[name];
if (data == null) return defaultValue ?? null;
return data;
},
set(v) {
nextTick(() => {
router.replace({ query: { ...route.value.query, [name]: v } });
router.replace({ query: { ...route.query, [name]: v } });
});
},
});

View file

@ -1,30 +1,29 @@
import { useContext } from "@nuxtjs/composition-api";
import { fieldTypes } from "../forms";
import { AutoFormItems } from "~/types/auto-forms";
import type { AutoFormItems } from "~/types/auto-forms";
export const useCommonSettingsForm = () => {
const { i18n } = useContext();
const i18n = useI18n();
const commonSettingsForm: AutoFormItems = [
{
section: i18n.tc("profile.group-settings"),
label: i18n.tc("group.enable-public-access"),
hint: i18n.tc("group.enable-public-access-description"),
varName: "makeGroupRecipesPublic",
type: fieldTypes.BOOLEAN,
rules: ["required"],
},
{
section: i18n.tc("data-pages.data-management"),
label: i18n.tc("user-registration.use-seed-data"),
hint: i18n.tc("user-registration.use-seed-data-description"),
varName: "useSeedData",
type: fieldTypes.BOOLEAN,
rules: ["required"],
},
];
const commonSettingsForm: AutoFormItems = [
{
section: i18n.t("profile.group-settings"),
label: i18n.t("group.enable-public-access"),
hint: i18n.t("group.enable-public-access-description"),
varName: "makeGroupRecipesPublic",
type: fieldTypes.BOOLEAN,
rules: ["required"],
},
{
section: i18n.t("data-pages.data-management"),
label: i18n.t("user-registration.use-seed-data"),
hint: i18n.t("user-registration.use-seed-data-description"),
varName: "useSeedData",
type: fieldTypes.BOOLEAN,
rules: ["required"],
},
];
return {
commonSettingsForm,
}
}
return {
commonSettingsForm,
};
};

View file

@ -1,11 +1,10 @@
import { computed, reactive, watch } from "@nuxtjs/composition-api";
import { useLocalStorage } from "@vueuse/core";
import { useLocalStorage, useOnline } from "@vueuse/core";
import { useUserApi } from "~/composables/api";
import { ShoppingListItemOut, ShoppingListOut } from "~/lib/api/types/household";
import { RequestResponse } from "~/lib/api/types/non-generated";
import type { ShoppingListItemOut, ShoppingListOut } from "~/lib/api/types/household";
import type { RequestResponse } from "~/lib/api/types/non-generated";
const localStorageKey = "shopping-list-queue";
const queueTimeout = 5 * 60 * 1000; // 5 minutes
const queueTimeout = 5 * 60 * 1000; // 5 minutes
type ItemQueueType = "create" | "update" | "delete";
@ -22,6 +21,7 @@ interface Storage {
}
export function useShoppingListItemActions(shoppingListId: string) {
const isOnline = useOnline();
const api = useUserApi();
const storage = useLocalStorage(localStorageKey, {} as Storage, { deep: true });
const queue = reactive(getQueue());
@ -30,17 +30,17 @@ export function useShoppingListItemActions(shoppingListId: string) {
queue.lastUpdate = Date.now();
}
storage.value[shoppingListId] = { ...queue }
storage.value[shoppingListId] = { ...queue };
watch(
() => queue,
(value) => {
storage.value[shoppingListId] = { ...value }
storage.value[shoppingListId] = { ...value };
},
{
deep: true,
immediate: true,
},
)
);
function isValidQueueObject(obj: any): obj is ShoppingListQueue {
if (typeof obj !== "object" || obj === null) {
@ -53,7 +53,7 @@ export function useShoppingListItemActions(shoppingListId: string) {
}
const arraysValid = Array.isArray(obj.create) && Array.isArray(obj.update) && Array.isArray(obj.delete);
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const lastUpdateValid = typeof obj.lastUpdate === "number" && !isNaN(new Date(obj.lastUpdate).getTime());
return arraysValid && lastUpdateValid;
@ -70,10 +70,12 @@ export function useShoppingListItemActions(shoppingListId: string) {
if (!isValidQueueObject(fetchedQueue)) {
console.log("Invalid queue object in local storage; resetting queue.");
return createEmptyQueue();
} else {
}
else {
return fetchedQueue;
}
} catch (error) {
}
catch (error) {
console.log("Error validating queue object in local storage; resetting queue.", error);
return createEmptyQueue();
}
@ -91,29 +93,30 @@ export function useShoppingListItemActions(shoppingListId: string) {
function mergeListItemsByLatest(
list1: ShoppingListItemOut[],
list2: ShoppingListItemOut[]
list2: ShoppingListItemOut[],
) {
const mergedList = [...list1];
list2.forEach((list2Item) => {
const conflictingItem = mergedList.find((item) => item.id === list2Item.id)
if (conflictingItem &&
list2Item.updatedAt && conflictingItem.updatedAt &&
list2Item.updatedAt > conflictingItem.updatedAt) {
mergedList.splice(mergedList.indexOf(conflictingItem), 1, list2Item)
} else if (!conflictingItem) {
mergedList.push(list2Item)
const conflictingItem = mergedList.find(item => item.id === list2Item.id);
if (conflictingItem
&& list2Item.updatedAt && conflictingItem.updatedAt
&& list2Item.updatedAt > conflictingItem.updatedAt) {
mergedList.splice(mergedList.indexOf(conflictingItem), 1, list2Item);
}
})
return mergedList
else if (!conflictingItem) {
mergedList.push(list2Item);
}
});
return mergedList;
}
async function getList() {
const response = await api.shopping.lists.getOne(shoppingListId);
if (window.$nuxt.isOffline && response.data) {
if (!isOnline.value && response.data) {
const createAndUpdateQueues = mergeListItemsByLatest(queue.update, queue.create);
response.data.listItems = mergeListItemsByLatest(response.data.listItems ?? [], createAndUpdateQueues);
}
return response.data
return response.data;
}
function createItem(item: ShoppingListItemOut) {
@ -174,8 +177,8 @@ export function useShoppingListItemActions(shoppingListId: string) {
}
/**
* Processes the queue items and returns whether the processing was successful.
*/
* Processes the queue items and returns whether the processing was successful.
*/
async function processQueueItems(
action: (items: ShoppingListItemOut[]) => Promise<RequestResponse<any>>,
itemQueueType: ItemQueueType,
@ -186,7 +189,8 @@ export function useShoppingListItemActions(shoppingListId: string) {
if (!queueItems.length) {
return true;
}
} catch (error) {
}
catch (error) {
console.log(`Error fetching queue items of type ${itemQueueType}:`, error);
clearQueueItems(itemQueueType);
return false;
@ -196,11 +200,12 @@ export function useShoppingListItemActions(shoppingListId: string) {
const itemsToProcess = [...queueItems];
await action(itemsToProcess)
.then(() => {
if (window.$nuxt.isOnline) {
if (isOnline.value) {
clearQueueItems(itemQueueType, itemsToProcess.map(item => item.id));
}
});
} catch (error) {
}
catch (error) {
console.log(`Error processing queue items of type ${itemQueueType}:`, error);
clearQueueItems(itemQueueType);
return false;
@ -224,13 +229,13 @@ export function useShoppingListItemActions(shoppingListId: string) {
// We send each bulk request one at a time, since the backend may merge items
// "failures" here refers to an actual error, rather than failing to reach the backend
let failures = 0;
if (!(await processQueueItems((items) => api.shopping.items.deleteMany(items), "delete"))) failures++;
if (!(await processQueueItems((items) => api.shopping.items.updateMany(items), "update"))) failures++;
if (!(await processQueueItems((items) => api.shopping.items.createMany(items), "create"))) failures++;
if (!(await processQueueItems(items => api.shopping.items.deleteMany(items), "delete"))) failures++;
if (!(await processQueueItems(items => api.shopping.items.updateMany(items), "update"))) failures++;
if (!(await processQueueItems(items => api.shopping.items.createMany(items), "create"))) failures++;
// If we're online, or the queue is empty, the queue is fully processed, so we're up to date
// Otherwise, if all three queue processes failed, we've already reset the queue, so we need to reset the date
if (window.$nuxt.isOnline || queueEmpty.value || failures === 3) {
if (isOnline.value || queueEmpty.value || failures === 3) {
queue.lastUpdate = Date.now();
}
}

View file

@ -1,4 +1,3 @@
// @ts-ignore missing color types
import Color from "@sphinxxxx/color-conversion";
const LIGHT_COLOR = "white";
@ -32,7 +31,8 @@ export function getTextColor(bgColor: string | undefined): string {
});
const L = 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
return L > ACCESSIBILITY_THRESHOLD ? DARK_COLOR : LIGHT_COLOR;
} catch (error) {
}
catch (error) {
console.warn(error);
return DARK_COLOR;
}

View file

@ -1,5 +1,3 @@
import { reactive } from "@nuxtjs/composition-api";
interface Toast {
open: boolean;
text: string;

View file

@ -1,6 +1,5 @@
import { useAsync, ref } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api";
import { UserIn, UserOut } from "~/lib/api/types/user";
import type { UserIn, UserOut } from "~/lib/api/types/user";
/*
TODO: Potentially combine useAllUsers and useUser by delaying the get all users functionality
@ -10,38 +9,16 @@ to control whether the object is substantiated... but some of the others rely on
export const useAllUsers = function () {
const api = useUserApi();
const loading = ref(false);
function getAllUsers() {
loading.value = true;
const asyncKey = String(Date.now());
const allUsers = useAsync(async () => {
const { data } = await api.users.getAll();
if (data) {
return data.items;
} else {
return null;
}
}, asyncKey);
loading.value = false;
return allUsers;
}
async function refreshAllUsers() {
loading.value = true;
const asyncKey = String(Date.now());
const { data: users, refresh: refreshAllUsers } = useLazyAsyncData(asyncKey, async () => {
const { data } = await api.users.getAll();
if (data) {
users.value = data.items;
} else {
users.value = null;
return data.items;
}
loading.value = false;
}
const users = getAllUsers();
else {
return null;
}
});
return { users, refreshAllUsers };
};
@ -52,10 +29,10 @@ export const useUser = function (refreshFunc: CallableFunction | null = null) {
function getUser(id: string) {
loading.value = true;
const user = useAsync(async () => {
const user = useAsyncData(id, async () => {
const { data } = await api.users.getOne(id);
return data;
}, id);
});
loading.value = false;
return user;

View file

@ -1,7 +1,6 @@
import { Ref, useContext } from "@nuxtjs/composition-api";
import { useLocalStorage, useSessionStorage } from "@vueuse/core";
import { RegisteredParser, TimelineEventType } from "~/lib/api/types/recipe";
import { QueryFilterJSON } from "~/lib/api/types/response";
import type { RegisteredParser, TimelineEventType } from "~/lib/api/types/recipe";
import type { QueryFilterJSON } from "~/lib/api/types/response";
export interface UserPrintPreferences {
imagePosition: string;
@ -67,7 +66,7 @@ export function useUserMealPlanPreferences(): Ref<UserMealPlanPreferences> {
{
numberOfDays: 7,
},
{ mergeDefaults: true }
{ mergeDefaults: true },
// we cast to a Ref because by default it will return an optional type ref
// but since we pass defaults we know all properties are set.
) as unknown as Ref<UserMealPlanPreferences>;
@ -83,7 +82,7 @@ export function useUserPrintPreferences(): Ref<UserPrintPreferences> {
showDescription: true,
showNotes: true,
},
{ mergeDefaults: true }
{ mergeDefaults: true },
// we cast to a Ref because by default it will return an optional type ref
// but since we pass defaults we know all properties are set.
) as unknown as Ref<UserPrintPreferences>;
@ -92,7 +91,7 @@ export function useUserPrintPreferences(): Ref<UserPrintPreferences> {
}
export function useUserSortPreferences(): Ref<UserRecipePreferences> {
const { $globals } = useContext();
const { $globals } = useNuxtApp();
const fromStorage = useLocalStorage(
"recipe-section-preferences",
@ -103,7 +102,7 @@ export function useUserSortPreferences(): Ref<UserRecipePreferences> {
sortIcon: $globals.icons.sortAlphabeticalAscending,
useMobileCards: false,
},
{ mergeDefaults: true }
{ mergeDefaults: true },
// we cast to a Ref because by default it will return an optional type ref
// but since we pass defaults we know all properties are set.
) as unknown as Ref<UserRecipePreferences>;
@ -117,7 +116,7 @@ export function useUserSearchQuerySession(): Ref<UserSearchQuery> {
{
recipe: "",
},
{ mergeDefaults: true }
{ mergeDefaults: true },
// we cast to a Ref because by default it will return an optional type ref
// but since we pass defaults we know all properties are set.
) as unknown as Ref<UserSearchQuery>;
@ -125,7 +124,6 @@ export function useUserSearchQuerySession(): Ref<UserSearchQuery> {
return fromStorage;
}
export function useShoppingListPreferences(): Ref<UserShoppingListPreferences> {
const fromStorage = useLocalStorage(
"shopping-list-preferences",
@ -133,7 +131,7 @@ export function useShoppingListPreferences(): Ref<UserShoppingListPreferences> {
viewAllLists: false,
viewByLabel: true,
},
{ mergeDefaults: true }
{ mergeDefaults: true },
// we cast to a Ref because by default it will return an optional type ref
// but since we pass defaults we know all properties are set.
) as unknown as Ref<UserShoppingListPreferences>;
@ -148,7 +146,7 @@ export function useTimelinePreferences(): Ref<UserTimelinePreferences> {
orderDirection: "asc",
types: ["info", "system", "comment"] as TimelineEventType[],
},
{ mergeDefaults: true }
{ mergeDefaults: true },
// we cast to a Ref because by default it will return an optional type ref
// but since we pass defaults we know all properties are set.
) as unknown as Ref<UserTimelinePreferences>;
@ -162,7 +160,7 @@ export function useParsingPreferences(): Ref<UserParsingPreferences> {
{
parser: "nlp",
},
{ mergeDefaults: true }
{ mergeDefaults: true },
// we cast to a Ref because by default it will return an optional type ref
// but since we pass defaults we know all properties are set.
) as unknown as Ref<UserParsingPreferences>;
@ -176,7 +174,7 @@ export function useCookbookPreferences(): Ref<UserCookbooksPreferences> {
{
hideOtherHouseholds: false,
},
{ mergeDefaults: true }
{ mergeDefaults: true },
// we cast to a Ref because by default it will return an optional type ref
// but since we pass defaults we know all properties are set.
) as unknown as Ref<UserCookbooksPreferences>;
@ -197,7 +195,7 @@ export function useRecipeFinderPreferences(): Ref<UserRecipeFinderPreferences> {
includeFoodsOnHand: true,
includeToolsOnHand: true,
},
{ mergeDefaults: true }
{ mergeDefaults: true },
// we cast to a Ref because by default it will return an optional type ref
// but since we pass defaults we know all properties are set.
) as unknown as Ref<UserRecipeFinderPreferences>;

View file

@ -1,78 +1,77 @@
import { useContext } from "@nuxtjs/composition-api";
import { fieldTypes } from "../forms";
import { AutoFormItems } from "~/types/auto-forms";
import type { AutoFormItems } from "~/types/auto-forms";
export const useUserForm = () => {
const { i18n } = useContext();
const i18n = useI18n();
const userForm: AutoFormItems = [
{
section: i18n.tc("user.user-details"),
label: i18n.tc("user.user-name"),
section: i18n.t("user.user-details"),
label: i18n.t("user.user-name"),
varName: "username",
type: fieldTypes.TEXT,
rules: ["required"],
},
{
label: i18n.tc("user.full-name"),
label: i18n.t("user.full-name"),
varName: "fullName",
type: fieldTypes.TEXT,
rules: ["required"],
},
{
label: i18n.tc("user.email"),
label: i18n.t("user.email"),
varName: "email",
type: fieldTypes.TEXT,
rules: ["required"],
},
{
label: i18n.tc("user.password"),
label: i18n.t("user.password"),
varName: "password",
disableUpdate: true,
type: fieldTypes.PASSWORD,
rules: ["required", "minLength:8"],
},
{
label: i18n.tc("user.authentication-method"),
label: i18n.t("user.authentication-method"),
varName: "authMethod",
type: fieldTypes.SELECT,
hint: i18n.tc("user.authentication-method-hint"),
hint: i18n.t("user.authentication-method-hint"),
disableCreate: true,
options: [{ text: "Mealie" }, { text: "LDAP" }, { text: "OIDC" }],
},
{
section: i18n.tc("user.permissions"),
label: i18n.tc("user.administrator"),
section: i18n.t("user.permissions"),
label: i18n.t("user.administrator"),
varName: "admin",
type: fieldTypes.BOOLEAN,
rules: ["required"],
},
{
label: i18n.tc("user.user-can-invite-other-to-group"),
label: i18n.t("user.user-can-invite-other-to-group"),
varName: "canInvite",
type: fieldTypes.BOOLEAN,
rules: ["required"],
},
{
label: i18n.tc("user.user-can-manage-group"),
label: i18n.t("user.user-can-manage-group"),
varName: "canManage",
type: fieldTypes.BOOLEAN,
rules: ["required"],
},
{
label: i18n.tc("user.user-can-organize-group-data"),
label: i18n.t("user.user-can-organize-group-data"),
varName: "canOrganize",
type: fieldTypes.BOOLEAN,
rules: ["required"],
},
{
label: i18n.tc("user.user-can-manage-household"),
label: i18n.t("user.user-can-manage-household"),
varName: "canManageHousehold",
type: fieldTypes.BOOLEAN,
rules: ["required"],
},
{
label: i18n.tc("user.enable-advanced-features"),
label: i18n.t("user.enable-advanced-features"),
varName: "advanced",
type: fieldTypes.BOOLEAN,
rules: ["required"],

View file

@ -1,17 +1,16 @@
import { ref, useContext } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api";
import { UserRatingSummary } from "~/lib/api/types/user";
import type { UserRatingSummary } from "~/lib/api/types/user";
const userRatings = ref<UserRatingSummary[]>([]);
const loading = ref(false);
const ready = ref(false);
export const useUserSelfRatings = function () {
const { $auth } = useContext();
const $auth = useMealieAuth();
const api = useUserApi();
async function refreshUserRatings() {
if (!$auth.user || loading.value) {
if (!$auth.user.value || loading.value) {
return;
}
@ -24,7 +23,7 @@ export const useUserSelfRatings = function () {
async function setRating(slug: string, rating: number | null, isFavorite: boolean | null) {
loading.value = true;
const userId = $auth.user?.id || "";
const userId = $auth.user.value?.id || "";
await api.users.setRating(userId, slug, rating, isFavorite);
loading.value = false;
await refreshUserRatings();
@ -39,5 +38,5 @@ export const useUserSelfRatings = function () {
refreshUserRatings,
setRating,
ready,
}
}
};
};

View file

@ -1,6 +1,5 @@
import { ref, Ref, useContext } from "@nuxtjs/composition-api";
import { useAsyncValidator } from "~/composables/use-validators";
import { VForm } from "~/types/vuetify";
import type { VForm } from "~/types/vuetify";
import { usePublicApi } from "~/composables/api/api-client";
const domAccountForm = ref<VForm | null>(null);
@ -12,7 +11,8 @@ const password2 = ref("");
const advancedOptions = ref(false);
export const useUserRegistrationForm = () => {
const { i18n } = useContext();
const i18n = useI18n();
function safeValidate(form: Ref<VForm | null>) {
if (form.value && form.value.validate) {
return form.value.validate();
@ -29,15 +29,15 @@ export const useUserRegistrationForm = () => {
const { validate: validateUsername, valid: validUsername } = useAsyncValidator(
username,
(v: string) => publicApi.validators.username(v),
i18n.tc("validation.username-is-taken"),
usernameErrorMessages
i18n.t("validation.username-is-taken"),
usernameErrorMessages,
);
const emailErrorMessages = ref<string[]>([]);
const { validate: validateEmail, valid: validEmail } = useAsyncValidator(
email,
(v: string) => publicApi.validators.email(v),
i18n.tc("validation.email-is-taken"),
emailErrorMessages
i18n.t("validation.email-is-taken"),
emailErrorMessages,
);
const accountDetails = {
username,
@ -60,7 +60,7 @@ export const useUserRegistrationForm = () => {
};
// ================================================================
// Provide Credentials
const passwordMatch = () => password1.value === password2.value || i18n.tc("user.password-must-match");
const passwordMatch = () => password1.value === password2.value || i18n.t("user.password-must-match");
const credentials = {
password1,
password2,
@ -68,7 +68,7 @@ export const useUserRegistrationForm = () => {
reset: () => {
credentials.password1.value = "";
credentials.password2.value = "";
}
},
};
return {

View file

@ -1,17 +1,10 @@
import { IncomingMessage } from "connect";
import { useDark } from "@vueuse/core";
import { useContext } from "@nuxtjs/composition-api";
import { useDark, useToggle } from "@vueuse/core";
export const useToggleDarkMode = () => {
const isDark = useDark();
const { $vuetify } = useContext();
const toggleDark = useToggle(isDark);
function toggleDark() {
isDark.value = !$vuetify.theme.dark;
$vuetify.theme.dark = !$vuetify.theme.dark;
}
return toggleDark;
return () => toggleDark();
};
export const useAsyncKey = function () {
@ -21,34 +14,13 @@ export const useAsyncKey = function () {
export const titleCase = function (str: string) {
return str
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
};
export function detectServerBaseUrl(req?: IncomingMessage | null) {
if (!req || req === undefined) {
return "";
}
if (req.headers.referer) {
const url = new URL(req.headers.referer);
return `${url.protocol}//${url.host}`;
} else if (req.headers.host) {
// TODO Socket.encrypted doesn't exist. What is needed here?
// @ts-ignore See above
const protocol = req.socket.encrypted ? "https:" : "http:";
return `${protocol}//${req.headers.host}`;
} else if (req.socket.remoteAddress) {
// @ts-ignore See above
const protocol = req.socket.encrypted ? "https:" : "http:";
return `${protocol}//${req.socket.localAddress || ""}:${req.socket.localPort || ""}`;
}
return "";
}
export function uuid4() {
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) =>
(parseInt(c) ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (parseInt(c) / 4)))).toString(16)
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c =>
(parseInt(c) ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (parseInt(c) / 4)))).toString(16),
);
}
@ -61,7 +33,8 @@ export function deepCopy<T>(obj: T): T {
if (obj === null) {
// null => null
rv = null;
} else {
}
else {
switch (Object.prototype.toString.call(obj)) {
case "[object Array]":
// It's an array, create a new array with
@ -81,7 +54,6 @@ export function deepCopy<T>(obj: T): T {
// Some other kind of object, deep-copy its
// properties into a new object
rv = Object.keys(obj).reduce(function (prev, key) {
// @ts-ignore This is hard to make type-safe
prev[key] = deepCopy(obj[key]);
return prev;
}, {});

View file

@ -1,6 +1,5 @@
import { ref, Ref } from "@nuxtjs/composition-api";
import { RequestResponse } from "~/lib/api/types/non-generated";
import { ValidationResponse } from "~/lib/api/types/response";
import type { RequestResponse } from "~/lib/api/types/non-generated";
import type { ValidationResponse } from "~/lib/api/types/response";
import { required, email, whitespace, url, minLength, maxLength } from "~/lib/validators";
export const validators = {
@ -21,7 +20,7 @@ export const useAsyncValidator = (
value: Ref<string>,
validatorFunc: (v: string) => Promise<RequestResponse<ValidationResponse>>,
validatorMessage: string,
errorMessages: Ref<string[]>
errorMessages: Ref<string[]>,
) => {
const valid = ref(false);

View file

@ -0,0 +1,61 @@
import { ref, watch, computed } from "vue";
import type { UserOut } from "~/lib/api/types/user";
export const useMealieAuth = function () {
const auth = useAuth();
const { setToken } = useAuthState();
const { $axios } = useNuxtApp();
// User Management
const lastUser = ref<UserOut | null>(null);
const user = computed(() => lastUser.value);
watch(
() => auth.data.value,
(val) => {
if (val) {
lastUser.value = val as UserOut;
}
else {
lastUser.value = null;
}
},
{ immediate: true },
);
// Auth Status Management
const lastAuthStatus = ref<string>(auth.status.value);
const loggedIn = computed(() => lastAuthStatus.value === "authenticated");
watch(
() => auth.status.value,
(val) => {
if (val !== "loading") {
lastAuthStatus.value = val;
}
},
{ immediate: true },
);
async function signIn(...params: Parameters<typeof auth.signIn>) {
await auth.signIn(...params);
refreshCookie(useRuntimeConfig().public.AUTH_TOKEN);
}
async function oauthSignIn() {
const params = new URLSearchParams(window.location.search);
const { data: token } = await $axios.get<{ access_token: string; token_type: "bearer" }>("/api/auth/oauth/callback", { params });
setToken(token.access_token);
await auth.getSession();
}
return {
user,
loggedIn,
signIn,
signOut: auth.signOut,
signUp: auth.signUp,
refresh: auth.refresh,
oauthSignIn,
};
};