1
0
Fork 0
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:
Philipp Fischbeck 2022-01-09 07:15:23 +01:00 committed by GitHub
parent d5ab5ec66f
commit 86c99b10a2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
114 changed files with 2218 additions and 2033 deletions

View file

@ -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 {

View file

@ -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}`;
}

View file

@ -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;

View file

@ -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;

View file

@ -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>&frasl;<sub>${fraction[2]}</sub>`;
if (fraction[1] > 0) {
returnQty += ` <sup>${fraction[1]}</sup>&frasl;<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, " ");
}

View file

@ -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" },
],

View file

@ -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({

View file

@ -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;

View file

@ -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);

View file

@ -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 {

View file

@ -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);

View file

@ -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"),

View file

@ -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;
}

View file

@ -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`,
};