mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-08-02 20:15:24 +02:00
Use composition API for more components, enable more type checking (#914)
* Activate more linting rules from eslint and typescript * Properly add VForm as type information * Fix usage of native types * Fix more linting issues * Rename vuetify types file, add VTooltip * Fix some more typing problems * Use composition API for more components * Convert RecipeRating * Convert RecipeNutrition * Convert more components to composition API * Fix globals plugin for type checking * Add missing icon types * Fix vuetify types in Nuxt context * Use composition API for RecipeActionMenu * Convert error.vue to composition API * Convert RecipeContextMenu to composition API * Use more composition API and type checking in recipe/create * Convert AppButtonUpload to composition API * Fix some type checking in RecipeContextMenu * Remove unused components BaseAutoForm and BaseColorPicker * Convert RecipeCategoryTagDialog to composition API * Convert RecipeCardSection to composition API * Convert RecipeCategoryTagSelector to composition API * Properly import vuetify type definitions * Convert BaseButton to composition API * Convert AutoForm to composition API * Remove unused requests API file * Remove static routes from recipe API * Fix more type errors * Convert AppHeader to composition API, fixing some search bar focus problems * Convert RecipeDialogSearch to composition API * Update API types from pydantic models, handle undefined values * Improve more typing problems * Add types to other plugins * Properly type the CRUD API access * Fix typing of static image routes * Fix more typing stuff * Fix some more typing problems * Turn off more rules
This commit is contained in:
parent
d5ab5ec66f
commit
86c99b10a2
114 changed files with 2218 additions and 2033 deletions
|
@ -2,30 +2,26 @@ import { AxiosResponse } from "axios";
|
|||
import { useContext } from "@nuxtjs/composition-api";
|
||||
import { NuxtAxiosInstance } from "@nuxtjs/axios";
|
||||
import { AdminAPI, Api } from "~/api";
|
||||
import { ApiRequestInstance } from "~/types/api";
|
||||
|
||||
interface RequestResponse<T> {
|
||||
response: AxiosResponse<T> | null;
|
||||
data: T | null;
|
||||
error: any;
|
||||
}
|
||||
import { ApiRequestInstance, RequestResponse } from "~/types/api";
|
||||
|
||||
const request = {
|
||||
async safe<T>(funcCall: any, url: string, data: object = {}): Promise<RequestResponse<T>> {
|
||||
const response = await funcCall(url, data).catch(function (error: object) {
|
||||
console.log(error);
|
||||
async safe<T, U>(funcCall: (url: string, data: U) => Promise<AxiosResponse<T>>, url: string, data: U): Promise<RequestResponse<T>> {
|
||||
let error = null;
|
||||
const response = await funcCall(url, data).catch(function (e) {
|
||||
console.log(e);
|
||||
// Insert Generic Error Handling Here
|
||||
return { response: null, error, data: null };
|
||||
error = e;
|
||||
return null;
|
||||
});
|
||||
return { response, error: null, data: response.data };
|
||||
return { response, error, data: response?.data ?? null };
|
||||
},
|
||||
};
|
||||
|
||||
function getRequests(axoisInstance: NuxtAxiosInstance): ApiRequestInstance {
|
||||
const requests = {
|
||||
function getRequests(axiosInstance: NuxtAxiosInstance): ApiRequestInstance {
|
||||
return {
|
||||
async get<T>(url: string, params = {}): Promise<RequestResponse<T>> {
|
||||
let error = null;
|
||||
const response = await axoisInstance.get<T>(url, params).catch((e) => {
|
||||
const response = await axiosInstance.get<T>(url, params).catch((e) => {
|
||||
error = e;
|
||||
});
|
||||
if (response != null) {
|
||||
|
@ -34,23 +30,26 @@ function getRequests(axoisInstance: NuxtAxiosInstance): ApiRequestInstance {
|
|||
return { response: null, error, data: null };
|
||||
},
|
||||
|
||||
async post<T>(url: string, data: object) {
|
||||
return await request.safe<T>(axoisInstance.post, url, data);
|
||||
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>(url: string, data: object) {
|
||||
return await request.safe<T>(axoisInstance.put, 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>(url: string, data: object) {
|
||||
return await request.safe<T>(axoisInstance.patch, 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) {
|
||||
return await request.safe<T>(axoisInstance.delete, url);
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
return await request.safe<T, undefined>(axiosInstance.delete, url, undefined);
|
||||
},
|
||||
};
|
||||
return requests;
|
||||
}
|
||||
|
||||
export const useAdminApi = function (): AdminAPI {
|
||||
|
|
|
@ -5,20 +5,20 @@ export const useStaticRoutes = () => {
|
|||
const { $config, req } = useContext();
|
||||
const serverBase = detectServerBaseUrl(req);
|
||||
|
||||
const prefix = `${$config.SUB_PATH}/api`.replace("//", "/");
|
||||
const prefix = `${$config.SUB_PATH as string}/api`.replace("//", "/");
|
||||
|
||||
const fullBase = serverBase + prefix;
|
||||
|
||||
// Methods to Generate reference urls for assets/images *
|
||||
function recipeImage(recipeSlug: string, version = null, key = null) {
|
||||
function recipeImage(recipeSlug: string, version = "", key = 1) {
|
||||
return `${fullBase}/media/recipes/${recipeSlug}/images/original.webp?&rnd=${key}&version=${version}`;
|
||||
}
|
||||
|
||||
function recipeSmallImage(recipeSlug: string, version = null, key = null) {
|
||||
function recipeSmallImage(recipeSlug: string, version = "", key = 1) {
|
||||
return `${fullBase}/media/recipes/${recipeSlug}/images/min-original.webp?&rnd=${key}&version=${version}`;
|
||||
}
|
||||
|
||||
function recipeTinyImage(recipeSlug: string, version = null, key = null) {
|
||||
function recipeTinyImage(recipeSlug: string, version = "", key = 1) {
|
||||
return `${fullBase}/media/recipes/${recipeSlug}/images/tiny-original.webp?&rnd=${key}&version=${version}`;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* frac.js (C) 2012-present SheetJS -- http://sheetjs.com */
|
||||
/* https://developer.aliyun.com/mirror/npm/package/frac/v/0.3.0 Apache license */
|
||||
|
||||
function frac(x: number, D: number, mixed: Boolean) {
|
||||
function frac(x: number, D: number, mixed: boolean) {
|
||||
let n1 = Math.floor(x);
|
||||
let d1 = 1;
|
||||
let n2 = n1 + 1;
|
||||
|
@ -33,7 +33,7 @@ function frac(x: number, D: number, mixed: Boolean) {
|
|||
const q = Math.floor(n1 / d1);
|
||||
return [q, n1 - q * d1, d1];
|
||||
}
|
||||
function cont(x: number, D: number, mixed: Boolean) {
|
||||
function cont(x: number, D: number, mixed: boolean) {
|
||||
const sgn = x < 0 ? -1 : 1;
|
||||
let B = x * sgn;
|
||||
let P_2 = 0;
|
||||
|
|
|
@ -2,6 +2,7 @@ import { useAsync, ref, reactive, Ref } from "@nuxtjs/composition-api";
|
|||
import { useAsyncKey } from "../use-utils";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { Food } from "~/api/class-interfaces/recipe-foods";
|
||||
import { VForm} from "~/types/vuetify";
|
||||
|
||||
let foodStore: Ref<Food[] | null> | null = null;
|
||||
|
||||
|
|
|
@ -3,30 +3,28 @@ import { RecipeIngredient } from "~/types/api-types/recipe";
|
|||
|
||||
const { frac } = useFraction();
|
||||
|
||||
export function parseIngredientText(ingredient: RecipeIngredient, disableAmount: boolean, scale: number = 1): string {
|
||||
export function parseIngredientText(ingredient: RecipeIngredient, disableAmount: boolean, scale = 1): string {
|
||||
if (disableAmount) {
|
||||
return ingredient.note;
|
||||
return ingredient.note || "";
|
||||
}
|
||||
|
||||
const { quantity, food, unit, note } = ingredient;
|
||||
|
||||
const validQuantity = quantity !== null && quantity !== undefined && quantity !== 0;
|
||||
|
||||
let returnQty = "";
|
||||
if (unit?.fraction) {
|
||||
const fraction = frac(quantity * scale, 10, true);
|
||||
if (fraction[0] !== undefined && fraction[0] > 0) {
|
||||
returnQty += fraction[0];
|
||||
}
|
||||
if (quantity !== undefined && quantity !== 0) {
|
||||
if (unit?.fraction) {
|
||||
const fraction = frac(quantity * scale, 10, true);
|
||||
if (fraction[0] !== undefined && fraction[0] > 0) {
|
||||
returnQty += fraction[0];
|
||||
}
|
||||
|
||||
if (fraction[1] > 0) {
|
||||
returnQty += ` <sup>${fraction[1]}</sup>⁄<sub>${fraction[2]}</sub>`;
|
||||
if (fraction[1] > 0) {
|
||||
returnQty += ` <sup>${fraction[1]}</sup>⁄<sub>${fraction[2]}</sub>`;
|
||||
}
|
||||
} else {
|
||||
returnQty = (quantity * scale).toString();
|
||||
}
|
||||
} else if (validQuantity) {
|
||||
returnQty = (quantity * scale).toString();
|
||||
} else {
|
||||
returnQty = "";
|
||||
}
|
||||
|
||||
return `${returnQty} ${unit?.name || " "} ${food?.name || " "} ${note}`.replace(/ {2,}/g, " ");
|
||||
return `${returnQty} ${unit?.name || " "} ${food?.name || " "} ${note || " "}`.replace(/ {2,}/g, " ");
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ export const useRecipeMeta = (recipe: Ref<Recipe>) => {
|
|||
{
|
||||
hid: "og:desc",
|
||||
property: "og:description",
|
||||
content: recipe?.value?.description,
|
||||
content: recipe?.value?.description ?? "",
|
||||
},
|
||||
{
|
||||
hid: "og-image",
|
||||
|
@ -25,12 +25,12 @@ export const useRecipeMeta = (recipe: Ref<Recipe>) => {
|
|||
{
|
||||
hid: "twitter:title",
|
||||
property: "twitter:title",
|
||||
content: recipe?.value?.name,
|
||||
content: recipe?.value?.name ?? "",
|
||||
},
|
||||
{
|
||||
hid: "twitter:desc",
|
||||
property: "twitter:description",
|
||||
content: recipe?.value?.description,
|
||||
content: recipe?.value?.description ?? "",
|
||||
},
|
||||
{ hid: "t-type", name: "twitter:card", content: "summary_large_image" },
|
||||
],
|
||||
|
|
|
@ -1,6 +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";
|
||||
|
||||
export const useTools = function (eager = true) {
|
||||
const workingToolData = reactive({
|
||||
|
|
|
@ -2,6 +2,7 @@ import { useAsync, ref, reactive, Ref } from "@nuxtjs/composition-api";
|
|||
import { useAsyncKey } from "../use-utils";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { Unit } from "~/api/class-interfaces/recipe-units";
|
||||
import { VForm } from "~/types/vuetify";
|
||||
|
||||
let unitStore: Ref<Unit[] | null> | null = null;
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { ref, onMounted } from "@nuxtjs/composition-api";
|
|||
import { useUserApi } from "~/composables/api";
|
||||
import { Recipe } from "~/types/api-types/recipe";
|
||||
|
||||
export const useRecipe = function (slug: string, eager: boolean = true) {
|
||||
export const useRecipe = function (slug: string, eager = true) {
|
||||
const api = useUserApi();
|
||||
const loading = ref(false);
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ export const recentRecipes = ref<Recipe[] | null>([]);
|
|||
|
||||
const rand = (n: number) => Math.floor(Math.random() * n);
|
||||
|
||||
function swap(t: Array<any>, i: number, j: number) {
|
||||
function swap(t: Array<unknown>, i: number, j: number) {
|
||||
const q = t[i];
|
||||
t[i] = t[j];
|
||||
t[j] = q;
|
||||
|
@ -19,19 +19,19 @@ function swap(t: Array<any>, i: number, j: number) {
|
|||
export const useSorter = () => {
|
||||
function sortAToZ(list: Array<Recipe>) {
|
||||
list.sort((a, b) => {
|
||||
const textA = a.name.toUpperCase();
|
||||
const textB = b.name.toUpperCase();
|
||||
const textA = a.name?.toUpperCase() ?? "";
|
||||
const textB = b.name?.toUpperCase() ?? "";
|
||||
return textA < textB ? -1 : textA > textB ? 1 : 0;
|
||||
});
|
||||
}
|
||||
function sortByCreated(list: Array<Recipe>) {
|
||||
list.sort((a, b) => (a.dateAdded > b.dateAdded ? -1 : 1));
|
||||
list.sort((a, b) => ((a.dateAdded ?? "") > (b.dateAdded ?? "") ? -1 : 1));
|
||||
}
|
||||
function sortByUpdated(list: Array<Recipe>) {
|
||||
list.sort((a, b) => (a.dateUpdated > b.dateUpdated ? -1 : 1));
|
||||
list.sort((a, b) => ((a.dateUpdated ?? "") > (b.dateUpdated ?? "") ? -1 : 1));
|
||||
}
|
||||
function sortByRating(list: Array<Recipe>) {
|
||||
list.sort((a, b) => (a.rating > b.rating ? -1 : 1));
|
||||
list.sort((a, b) => ((a.rating ?? 0) > (b.rating ?? 0) ? -1 : 1));
|
||||
}
|
||||
|
||||
function randomRecipe(list: Array<Recipe>): Recipe {
|
||||
|
|
|
@ -60,8 +60,7 @@ export const useCookbooks = function () {
|
|||
async createOne() {
|
||||
loading.value = true;
|
||||
const { data } = await api.cookbooks.createOne({
|
||||
// @ts-ignore. I"m thinking this will always be defined.
|
||||
name: "Cookbook " + String(cookbookStore?.value?.length + 1 || 1),
|
||||
name: "Cookbook " + String((cookbookStore?.value?.length ?? 0) + 1),
|
||||
});
|
||||
if (data && cookbookStore?.value) {
|
||||
cookbookStore.value.push(data);
|
||||
|
|
|
@ -40,7 +40,7 @@ export const useMealplans = function (range: Ref<DateRange>) {
|
|||
loading.value = false;
|
||||
return units;
|
||||
},
|
||||
async refreshAll() {
|
||||
async refreshAll(this: void) {
|
||||
loading.value = true;
|
||||
const query = {
|
||||
start: format(range.value.start, "yyyy-MM-dd"),
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { IncomingMessage } from "connect";
|
||||
|
||||
export const useAsyncKey = function () {
|
||||
return String(Date.now());
|
||||
};
|
||||
|
||||
export function detectServerBaseUrl(req: any) {
|
||||
export function detectServerBaseUrl(req?: IncomingMessage | null) {
|
||||
if (!req || req === undefined) {
|
||||
return "";
|
||||
}
|
||||
|
@ -10,26 +12,27 @@ export function detectServerBaseUrl(req: any) {
|
|||
const url = new URL(req.headers.referer);
|
||||
return `${url.protocol}//${url.host}`;
|
||||
} else if (req.headers.host) {
|
||||
const protocol = req.connection.encrypted ? "https" : "http:";
|
||||
// TODO Socket.encrypted doesn't exist. What is needed here?
|
||||
// @ts-ignore
|
||||
const protocol = req.socket.encrypted ? "https:" : "http:";
|
||||
return `${protocol}//${req.headers.host}`;
|
||||
} else if (req.connection.remoteAddress) {
|
||||
const protocol = req.connection.encrypted ? "https" : "http:";
|
||||
return `${protocol}//${req.connection.localAddress}:${req.connection.localPort}`;
|
||||
} else if (req.socket.remoteAddress) {
|
||||
// @ts-ignore
|
||||
const protocol = req.socket.encrypted ? "https:" : "http:";
|
||||
return `${protocol}//${req.socket.localAddress}:${req.socket.localPort}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
export function uuid4() {
|
||||
// @ts-ignore
|
||||
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
|
||||
(c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (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)
|
||||
);
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/28876300/deep-copying-array-of-nested-objects-in-javascript
|
||||
const toString = Object.prototype.toString;
|
||||
export function deepCopy(obj: any) {
|
||||
export function deepCopy<T>(obj: T): T {
|
||||
let rv;
|
||||
|
||||
switch (typeof obj) {
|
||||
|
@ -38,19 +41,19 @@ export function deepCopy(obj: any) {
|
|||
// null => null
|
||||
rv = null;
|
||||
} else {
|
||||
switch (toString.call(obj)) {
|
||||
switch (Object.prototype.toString.call(obj)) {
|
||||
case "[object Array]":
|
||||
// It's an array, create a new array with
|
||||
// deep copies of the entries
|
||||
rv = obj.map(deepCopy);
|
||||
rv = (obj as unknown as Array<unknown>).map(deepCopy);
|
||||
break;
|
||||
case "[object Date]":
|
||||
// Clone the date
|
||||
rv = new Date(obj);
|
||||
rv = new Date(obj as unknown as Date);
|
||||
break;
|
||||
case "[object RegExp]":
|
||||
// Clone the RegExp
|
||||
rv = new RegExp(obj);
|
||||
rv = new RegExp(obj as unknown as RegExp);
|
||||
break;
|
||||
// ...probably a few others
|
||||
default:
|
||||
|
@ -70,5 +73,5 @@ export function deepCopy(obj: any) {
|
|||
rv = obj;
|
||||
break;
|
||||
}
|
||||
return rv;
|
||||
return rv as T;
|
||||
}
|
||||
|
|
|
@ -3,11 +3,12 @@ const EMAIL_REGEX =
|
|||
|
||||
const URL_REGEX = /[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
|
||||
|
||||
export const validators = {
|
||||
export const validators: {[key: string]: (v: string) => boolean | string} = {
|
||||
required: (v: string) => !!v || "This Field is Required",
|
||||
email: (v: string) => !v || EMAIL_REGEX.test(v) || "Email Must Be Valid",
|
||||
whitespace: (v: string) => !v || v.split(" ").length <= 1 || "No Whitespace Allowed",
|
||||
url: (v: string) => !v || URL_REGEX.test(v) || "Must Be A Valid URL",
|
||||
minLength: (min: number) => (v: string) => !v || v.length >= min || `Must Be At Least ${min} Characters`,
|
||||
maxLength: (max: number) => (v: string) => !v || v.length <= max || `Must Be At Most ${max} Characters`,
|
||||
// TODO These appear to be unused?
|
||||
// minLength: (min: number) => (v: string) => !v || v.length >= min || `Must Be At Least ${min} Characters`,
|
||||
// maxLength: (max: number) => (v: string) => !v || v.length <= max || `Must Be At Most ${max} Characters`,
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue