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:
parent
89ab7fac25
commit
c24d532608
403 changed files with 23959 additions and 19557 deletions
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
)}`;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
18
frontend/composables/api/use-downloader.ts
Normal file
18
frontend/composables/api/use-downloader.ts
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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>⁄</span><sub>${fraction[2]}</sub>` :
|
||||
` ${fraction[1]}/${fraction[2]}`;
|
||||
returnQty += includeFormating
|
||||
? `<sup>${fraction[1]}</sup><span>⁄</span><sub>${fraction[2]}</sub>`
|
||||
: ` ${fraction[1]}/${fraction[2]}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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([]);
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>) {
|
||||
|
|
|
@ -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>⁄</span><sub>${fraction[2]}</sub>`;
|
||||
valString += `<sup>${fraction[1]}</sup><span>⁄</span><sub>${fraction[2]}</sub>`;
|
||||
}
|
||||
|
||||
return valString.trim();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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" });
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
: [];
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -246,4 +246,4 @@ export const LOCALES = [
|
|||
progress: 90,
|
||||
dir: "ltr",
|
||||
},
|
||||
]
|
||||
];
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 } });
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { reactive } from "@nuxtjs/composition-api";
|
||||
|
||||
interface Toast {
|
||||
open: boolean;
|
||||
text: string;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}, {});
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
61
frontend/composables/useMealieAuth.ts
Normal file
61
frontend/composables/useMealieAuth.ts
Normal 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,
|
||||
};
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue