chore: developer cleanup (#300)

* new PR tasks

* add homebox to know words

* formatting

* bump deps

* generate db models

* ts errors

* drop id

* fix accessor

* drop unused time field

* change CI

* add expected error

* add type check

* resolve serveral type errors

* hoise in CI
This commit is contained in:
Hayden 2023-02-17 21:41:01 -09:00 committed by GitHub
parent 88f9ff90d4
commit bd321af29f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
142 changed files with 817 additions and 1200 deletions

View file

@ -28,7 +28,7 @@
<li v-for="(obj, idx) in filtered" :key="idx">
<div type="button" @click="select(obj)">
<slot name="display" v-bind="{ item: obj }">
{{ usingObjects ? obj[itemText] : obj }}
{{ extractor(obj, itemText) }}
</slot>
</div>
</li>
@ -94,6 +94,14 @@
}
);
function extractor(obj: string | ItemsObject, key: string | number): string {
if (typeof obj === "string") {
return obj;
}
return obj[key] as string;
}
const value = useVModel(props, "modelValue", emit);
const usingObjects = computed(() => {
@ -135,6 +143,7 @@
value.value = "";
return;
}
// @ts-ignore
value.value = obj;
} else {
if (obj === value.value) {
@ -142,6 +151,7 @@
return;
}
// @ts-ignore
value.value = obj;
}
}

View file

@ -38,12 +38,10 @@
default: "",
},
modelValue: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: Array as () => any[],
default: null,
},
items: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: Array as () => any[],
required: true,
},

View file

@ -24,12 +24,10 @@
default: "",
},
modelValue: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: [Object, String] as any,
default: null,
},
items: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: Array as () => any[],
required: true,
},
@ -86,7 +84,6 @@
{ immediate: true }
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function compare(a: any, b: any): boolean {
if (a === b) {
return true;

View file

@ -36,7 +36,7 @@
</template>
<script setup lang="ts">
import { ItemCreate, LocationOut } from "~~/lib/api/types/data-contracts";
import { ItemCreate, LabelOut, LocationOut } from "~~/lib/api/types/data-contracts";
import { useLabelStore } from "~~/stores/labels";
import { useLocationStore } from "~~/stores/locations";
@ -82,7 +82,7 @@
name: "",
description: "",
color: "", // Future!
labels: [],
labels: [] as LabelOut[],
});
whenever(
@ -91,7 +91,10 @@
focused.value = true;
if (locationId.value) {
form.location = locations.value.find(l => l.id === locationId.value);
const found = locations.value.find(l => l.id === locationId.value);
if (found) {
form.location = found;
}
}
if (labelId.value) {
@ -106,7 +109,7 @@
}
const out: ItemCreate = {
parentId: undefined,
parentId: null,
name: form.name,
description: form.description,
locationId: form.location.id as string,

View file

@ -18,7 +18,7 @@
const emit = defineEmits(["update:modelValue", "drop"]);
const el = ref<HTMLDivElement>(null);
const el = ref<HTMLDivElement>();
const { isOverDropZone } = useDropZone(el, files => {
emit("drop", files);
});

View file

@ -1,7 +1,6 @@
import { UseConfirmDialogRevealResult, UseConfirmDialogReturn } from "@vueuse/core";
import { Ref } from "vue";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Store = UseConfirmDialogReturn<any, boolean, boolean> & {
text: Ref<string>;
setup: boolean;
@ -23,7 +22,7 @@ const store: Partial<Store> = {
export function useConfirm(): Store {
if (!store.setup) {
store.setup = true;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { isRevealed, reveal, confirm, cancel } = useConfirmDialog<any, boolean, boolean>();
store.isRevealed = isRevealed;
store.reveal = reveal;

View file

@ -7,7 +7,7 @@ export function useRouteQuery(q: string, def: string[]): WritableComputedRef<str
export function useRouteQuery(q: string, def: string): WritableComputedRef<string>;
export function useRouteQuery(q: string, def: boolean): WritableComputedRef<boolean>;
export function useRouteQuery(q: string, def: number): WritableComputedRef<number>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useRouteQuery(q: string, def: any): WritableComputedRef<any> {
const route = useRoute();
const router = useRouter();

View file

@ -16,13 +16,13 @@ export function useTheme(): UseTheme {
preferences.value.theme = newTheme;
if (htmlEl) {
htmlEl.value.setAttribute("data-theme", newTheme);
htmlEl.value?.setAttribute("data-theme", newTheme);
}
themeRef.value = newTheme;
};
const htmlEl = ref<HTMLElement>(null);
const htmlEl = ref<HTMLElement | null>();
onMounted(() => {
if (htmlEl.value) {

View file

@ -9,13 +9,14 @@ import { Requests } from "../../../requests";
function itemField(id = null): ItemField {
return {
// @ts-expect-error
id,
name: faker.lorem.word(),
type: "text",
textValue: faker.lorem.sentence(),
booleanValue: false,
numberValue: faker.datatype.number(),
timeValue: null,
timeValue: "",
};
}
@ -32,8 +33,9 @@ function user(): UserRegistration {
};
}
function location(): LocationCreate {
function location(parentId: string | null = null): LocationCreate {
return {
parentId,
name: faker.address.city(),
description: faker.lorem.sentence(),
};
@ -56,7 +58,7 @@ function publicClient(): PublicApi {
function userClient(token: string): UserClient {
overrideParts(config.BASE_URL, "/api/v1");
const requests = new Requests("", token);
return new UserClient(requests);
return new UserClient(requests, "");
}
type TestUser = {
@ -75,7 +77,7 @@ async function userSingleUse(): Promise<TestUser> {
expect(result.status).toBe(200);
return {
client: new UserClient(new Requests("", result.data.token)),
client: new UserClient(new Requests("", result.data.token), result.data.attachmentToken),
user: usr,
};
}

View file

@ -1,6 +1,6 @@
import { faker } from "@faker-js/faker";
import { describe, test, expect } from "vitest";
import { ItemField, LocationOut } from "../../types/data-contracts";
import { ItemField, ItemUpdate, LocationOut } from "../../types/data-contracts";
import { AttachmentTypes } from "../../types/non-generated";
import { UserClient } from "../../user";
import { factories } from "../factories";
@ -14,6 +14,7 @@ describe("user should be able to create an item and add an attachment", () => {
*/
async function useLocation(api: UserClient): Promise<[LocationOut, () => Promise<void>]> {
const { response, data } = await api.locations.create({
parentId: null,
name: `__test__.location.name_${increment}`,
description: `__test__.location.description_${increment}`,
});
@ -86,12 +87,12 @@ describe("user should be able to create an item and add an attachment", () => {
const itemUpdate = {
parentId: null,
...item,
locationId: item.location.id,
locationId: item.location?.id || null,
labelIds: item.labels.map(l => l.id),
fields,
};
const { response: updateResponse, data: item2 } = await api.items.update(item.id, itemUpdate);
const { response: updateResponse, data: item2 } = await api.items.update(item.id, itemUpdate as ItemUpdate);
expect(updateResponse.status).toBe(200);
expect(item2.fields).toHaveLength(fields.length);
@ -104,7 +105,7 @@ describe("user should be able to create an item and add an attachment", () => {
itemUpdate.fields = [fields[0], fields[1]];
const { response: updateResponse2, data: item3 } = await api.items.update(item.id, itemUpdate);
const { response: updateResponse2, data: item3 } = await api.items.update(item.id, itemUpdate as ItemUpdate);
expect(updateResponse2.status).toBe(200);
expect(item3.fields).toHaveLength(2);

View file

@ -49,6 +49,8 @@ describe("locations lifecycle (create, update, delete)", () => {
const [location, cleanup] = await useLocation(api);
const updateData = {
id: location.id,
parentId: location.parent?.id,
name: "test-location-updated",
description: "test-description-updated",
};

View file

@ -77,13 +77,20 @@ function importFileGenerator(entries: number): ImportObj[] {
describe("group related statistics tests", () => {
const TOTAL_ITEMS = 30;
let api: UserClient | undefined;
let tAPI: UserClient | undefined;
const imports = importFileGenerator(TOTAL_ITEMS);
const api = (): UserClient => {
if (!tAPI) {
throw new Error("API not initialized");
}
return tAPI;
};
beforeAll(async () => {
// -- Setup --
const { client } = await factories.client.singleUse();
api = client;
tAPI = client;
const csv = toCsv(imports);
@ -95,7 +102,7 @@ describe("group related statistics tests", () => {
// Write to file system for debugging
// fs.writeFileSync("test.csv", csv);
test("Validate Group Statistics", async () => {
const { status, data } = await api.stats.group();
const { status, data } = await api().stats.group();
expect(status).toBe(200);
expect(data.totalItems).toEqual(TOTAL_ITEMS);
@ -117,7 +124,7 @@ describe("group related statistics tests", () => {
}
test("Validate Labels Statistics", async () => {
const { status, data } = await api.stats.labels();
const { status, data } = await api().stats.labels();
expect(status).toBe(200);
for (const label of data) {
@ -126,7 +133,7 @@ describe("group related statistics tests", () => {
});
test("Validate Locations Statistics", async () => {
const { status, data } = await api.stats.locations();
const { status, data } = await api().stats.locations();
expect(status).toBe(200);
for (const location of data) {
@ -135,7 +142,7 @@ describe("group related statistics tests", () => {
});
test("Validate Purchase Over Time", async () => {
const { status, data } = await api.stats.totalPriceOverTime();
const { status, data } = await api().stats.totalPriceOverTime();
expect(status).toBe(200);
expect(data.entries.length).toEqual(TOTAL_ITEMS);
});

View file

@ -9,8 +9,8 @@ type BaseApiType = {
[key: string]: any;
};
export function hasKey(obj: object, key: string): obj is Required<BaseApiType> {
return typeof obj[key] === "string";
export function hasKey(obj: Record<string, any>, key: string): obj is Required<BaseApiType> {
return key in obj ? typeof obj[key] === "string" : false;
}
export function parseDate<T>(obj: T, keys: Array<keyof T> = []): T {

View file

@ -141,4 +141,8 @@ export class ItemsApi extends BaseAPI {
data: formData,
});
}
exportURL() {
return route("/items/export");
}
}

View file

@ -1,27 +1,7 @@
import { BaseAPI, route } from "../base";
export class ReportsAPI extends BaseAPI {
async billOfMaterials(): Promise<void> {
const { data: stream, error } = await this.http.get<ReadableStream>({ url: route("/reporting/bill-of-materials") });
if (error) {
return;
}
const reader = stream.getReader();
let data = "";
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
data += new TextDecoder("utf-8").decode(value);
}
const blob = new Blob([data], { type: "text/tsv" });
const link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.download = "bill-of-materials.tsv";
link.click();
billOfMaterialsURL(): string {
return route("/reporting/bill-of-materials");
}
}

View file

@ -66,7 +66,6 @@ export interface ItemField {
name: string;
numberValue: number;
textValue: string;
timeValue: string;
type: string;
}

View file

@ -76,6 +76,7 @@ export class Requests {
const token = this.token();
if (token !== "" && payload.headers !== undefined) {
// @ts-expect-error - we know that the header is there
payload.headers["Authorization"] = token; // eslint-disable-line dot-notation
}
@ -83,6 +84,7 @@ export class Requests {
if (rargs.data) {
payload.body = rargs.data;
} else {
// @ts-expect-error - we know that the header is there
payload.headers["Content-Type"] = "application/json";
payload.body = JSON.stringify(rargs.body);
}

View file

@ -7,6 +7,8 @@
"postinstall": "nuxt prepare",
"lint": "eslint --ext \".ts,.js,.vue\" --ignore-path ../.gitignore .",
"lint:fix": "eslint --ext \".ts,.js,.vue\" --ignore-path ../.gitignore . --fix",
"lint:ci": "eslint --ext \".ts,.js,.vue\" --ignore-path ../.gitignore . --max-warnings 1",
"typecheck": "nuxi typecheck",
"test:ci": "TEST_SHUTDOWN_API_SERVER=true vitest --run --config ./test/vitest.config.ts",
"test:local": "TEST_SHUTDOWN_API_SERVER=false && vitest --run --config ./test/vitest.config.ts",
"test:watch": " TEST_SHUTDOWN_API_SERVER=false vitest --config ./test/vitest.config.ts"
@ -14,6 +16,8 @@
"devDependencies": {
"@faker-js/faker": "^7.5.0",
"@nuxtjs/eslint-config-typescript": "^12.0.0",
"@types/dompurify": "^2.4.0",
"@types/markdown-it": "^12.2.3",
"@typescript-eslint/eslint-plugin": "^5.36.2",
"@typescript-eslint/parser": "^5.36.2",
"eslint": "^8.23.0",
@ -45,7 +49,7 @@
"pinia": "^2.0.21",
"postcss": "^8.4.16",
"tailwindcss": "^3.1.8",
"vue": "^3.2.38",
"vue": "^3.2.45",
"vue-chartjs": "^4.1.2",
"vue-router": "4"
}

View file

@ -101,6 +101,7 @@
toast.success("Logged in successfully");
// @ts-ignore
authStore.$patch({
token: data.token,
expires: data.expiresAt,

View file

@ -408,7 +408,7 @@
<template #description>
<Markdown :source="item.description"> </Markdown>
<div class="flex flex-wrap gap-2 mt-3">
<NuxtLink ref="badge" class="badge p-3" :to="`/location/${item.location.id}`">
<NuxtLink v-if="item.location" ref="badge" class="badge p-3" :to="`/location/${item.location.id}`">
<Icon name="heroicons-map-pin" class="mr-2 swap-on"></Icon>
{{ item.location.name }}
</NuxtLink>

View file

@ -392,7 +392,7 @@
:items="allFields ?? []"
@change="fetchValues(f[0])"
>
<option v-for="fv in allFields" :key="fv" :value="fv">{{ fv }}</option>
<option v-for="(fv, _, i) in allFields" :key="i" :value="fv">{{ fv }}</option>
</select>
</div>
<div class="form-control w-full max-w-xs">

View file

@ -28,7 +28,7 @@
{
name: "Name",
text: label.value?.name,
},
} as AnyDetail,
{
name: "Description",
type: "markdown",
@ -43,16 +43,16 @@
name: "Created",
text: label.value?.createdAt,
type: "date",
},
} as AnyDetail,
{
name: "Updated",
text: label.value?.updatedAt,
type: "date",
},
} as AnyDetail,
{
name: "Database ID",
text: label.value?.id,
},
} as AnyDetail,
];
}

View file

@ -50,16 +50,16 @@
name: "Created",
text: location.value?.createdAt,
type: "date",
},
} as AnyDetail,
{
name: "Updated",
text: location.value?.updatedAt,
type: "date",
},
} as AnyDetail,
{
name: "Database ID",
text: location.value?.id,
},
} as AnyDetail,
];
}
@ -120,7 +120,6 @@
const locationStore = useLocationStore();
const locations = computed(() => locationStore.allLocations);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const parent = ref<LocationSummary | any>({});
</script>

View file

@ -102,8 +102,8 @@
const confirm = useConfirm();
const notify = useNotifier();
async function getBillOfMaterials() {
await api.reports.billOfMaterials();
function getBillOfMaterials() {
api.reports.billOfMaterialsURL();
}
async function ensureAssetIDs() {

646
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -14,13 +14,17 @@ export const useLabelStore = defineStore("labels", {
*/
labels(state): LabelOut[] {
if (state.allLabels === null) {
Promise.resolve(this.refresh());
this.client.labels.getAll().then(result => {
if (result.error) {
console.error(result.error);
}
});
}
return state.allLabels;
return state.allLabels ?? [];
},
},
actions: {
async refresh(): Promise<LabelOut[]> {
async refresh() {
const result = await this.client.labels.getAll();
if (result.error) {
return result;

View file

@ -1,4 +1,5 @@
import { defineStore } from "pinia";
import { LocationsApi } from "~~/lib/api/classes/locations";
import { LocationOutCount } from "~~/lib/api/types/data-contracts";
export const useLocationStore = defineStore("locations", {
@ -15,19 +16,33 @@ export const useLocationStore = defineStore("locations", {
*/
parentLocations(state): LocationOutCount[] {
if (state.parents === null) {
Promise.resolve(this.refreshParents());
this.client.locations.getAll({ filterChildren: true }).then(result => {
if (result.error) {
console.error(result.error);
return;
}
this.parents = result.data.items;
});
}
return state.parents;
return state.parents ?? [];
},
allLocations(state): LocationOutCount[] {
if (state.Locations === null) {
Promise.resolve(this.refreshChildren());
this.client.locations.getAll({ filterChildren: false }).then(result => {
if (result.error) {
console.error(result.error);
return;
}
this.Locations = result.data.items;
});
}
return state.Locations;
return state.Locations ?? [];
},
},
actions: {
async refreshParents(): Promise<LocationOutCount[]> {
async refreshParents(): ReturnType<LocationsApi["getAll"]> {
const result = await this.client.locations.getAll({ filterChildren: true });
if (result.error) {
return result;
@ -36,7 +51,7 @@ export const useLocationStore = defineStore("locations", {
this.parents = result.data.items;
return result;
},
async refreshChildren(): Promise<LocationOutCount[]> {
async refreshChildren(): ReturnType<LocationsApi["getAll"]> {
const result = await this.client.locations.getAll({ filterChildren: false });
if (result.error) {
return result;

View file

@ -13,7 +13,7 @@ export const setup = () => {
export const teardown = () => {
if (process.env.TEST_SHUTDOWN_API_SERVER) {
const pc = exec("pkill -SIGTERM api"); // Kill background API process
pc.stdout.on("data", data => {
pc.stdout?.on("data", data => {
console.log(`stdout: ${data}`);
});
}