feat: items-editor (#5)

* format readme

* update logo

* format html

* add logo to docs

* repository for document and document tokens

* add attachments type and repository

* autogenerate types via scripts

* use autogenerated types

* attachment type updates

* add insured and quantity fields for items

* implement HasID interface for entities

* implement label updates for items

* implement service update method

* WIP item update client side actions

* check err on attachment

* finish types for basic items editor

* remove unused var

* house keeping
This commit is contained in:
Hayden 2022-09-12 14:47:27 -08:00 committed by GitHub
parent fbc364dcd2
commit 95ab14b866
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
125 changed files with 15626 additions and 1791 deletions

View file

@ -1,5 +1,5 @@
import { describe, expect, test } from "vitest";
import { Label } from "../../classes/labels";
import { LabelOut } from "../../types/data-contracts";
import { UserApi } from "../../user";
import { sharedUserClient } from "../test-utils";
@ -10,7 +10,7 @@ describe("locations lifecycle (create, update, delete)", () => {
* useLabel sets up a label resource for testing, and returns a function
* that can be used to delete the label from the backend server.
*/
async function useLabel(api: UserApi): Promise<[Label, () => Promise<void>]> {
async function useLabel(api: UserApi): Promise<[LabelOut, () => Promise<void>]> {
const { response, data } = await api.labels.create({
name: `__test__.label.name_${increment}`,
description: `__test__.label.description_${increment}`,

View file

@ -1,5 +1,5 @@
import { describe, expect, test } from "vitest";
import { Location } from "../../classes/locations";
import { LocationOut } from "../../types/data-contracts";
import { UserApi } from "../../user";
import { sharedUserClient } from "../test-utils";
@ -10,7 +10,7 @@ describe("locations lifecycle (create, update, delete)", () => {
* useLocatio sets up a location resource for testing, and returns a function
* that can be used to delete the location from the backend server.
*/
async function useLocation(api: UserApi): Promise<[Location, () => Promise<void>]> {
async function useLocation(api: UserApi): Promise<[LocationOut, () => Promise<void>]> {
const { response, data } = await api.locations.create({
name: `__test__.location.name_${increment}`,
description: `__test__.location.description_${increment}`,

View file

@ -0,0 +1,30 @@
import { describe, expect, test } from "vitest";
import { hasKey, parseDate } from "./base-api";
describe("hasKey works as expected", () => {
test("hasKey returns true if the key exists", () => {
const obj = { createdAt: "2021-01-01" };
expect(hasKey(obj, "createdAt")).toBe(true);
});
test("hasKey returns false if the key does not exist", () => {
const obj = { createdAt: "2021-01-01" };
expect(hasKey(obj, "updatedAt")).toBe(false);
});
});
describe("parseDate should work as expected", () => {
test("parseDate should set defaults", () => {
const obj = { createdAt: "2021-01-01", updatedAt: "2021-01-01" };
const result = parseDate(obj);
expect(result.createdAt).toBeInstanceOf(Date);
expect(result.updatedAt).toBeInstanceOf(Date);
});
test("parseDate should set passed in types", () => {
const obj = { key1: "2021-01-01", key2: "2021-01-01" };
const result = parseDate(obj, ["key1", "key2"]);
expect(result.key1).toBeInstanceOf(Date);
expect(result.key2).toBeInstanceOf(Date);
});
});

View file

@ -8,10 +8,50 @@ import { Requests } from "../../requests";
// TDeleteResult = void
// >
type BaseApiType = {
createdAt: string;
updatedAt: string;
};
export function hasKey(obj: object, key: string): obj is Required<BaseApiType> {
return typeof obj[key] === "string";
}
export function parseDate<T>(obj: T, keys: Array<keyof T> = []): T {
const result = { ...obj };
[...keys, "createdAt", "updatedAt"].forEach(key => {
// @ts-ignore - we are checking for the key above
if (hasKey(result, key)) {
// @ts-ignore - we are guarding against this above
result[key] = new Date(result[key]);
}
});
return result;
}
export class BaseAPI {
http: Requests;
constructor(requests: Requests) {
this.http = requests;
}
/**
* dropFields will remove any fields that are specified in the fields array
* additionally, it will remove the `createdAt` and `updatedAt` fields if they
* are present. This is useful for when you want to send a subset of fields to
* the server like when performing an update.
*/
dropFields<T>(obj: T, keys: Array<keyof T> = []): T {
const result = { ...obj };
[...keys, "createdAt", "updatedAt"].forEach(key => {
// @ts-ignore - we are checking for the key above
if (hasKey(result, key)) {
// @ts-ignore - we are guarding against this above
delete result[key];
}
});
return result;
}
}

View file

@ -1,60 +1,26 @@
import { BaseAPI, route } from "../base";
import { Label } from "./labels";
import { Location } from "./locations";
import { parseDate } from "../base/base-api";
import { ItemCreate, ItemOut, ItemSummary, ItemUpdate } from "../types/data-contracts";
import { Results } from "./types";
export interface ItemCreate {
name: string;
description: string;
locationId: string;
labelIds: string[];
}
export interface Item {
createdAt: string;
description: string;
id: string;
labels: Label[];
location: Location;
manufacturer: string;
modelNumber: string;
name: string;
notes: string;
purchaseFrom: string;
purchasePrice: number;
purchaseTime: Date;
serialNumber: string;
soldNotes: string;
soldPrice: number;
soldTime: Date;
soldTo: string;
updatedAt: string;
lifetimeWarranty: boolean;
warrantyExpires: Date;
warrantyDetails: string;
}
export class ItemsApi extends BaseAPI {
getAll() {
return this.http.get<Results<Item>>({ url: route("/items") });
return this.http.get<Results<ItemOut>>({ url: route("/items") });
}
create(item: ItemCreate) {
return this.http.post<ItemCreate, Item>({ url: route("/items"), body: item });
return this.http.post<ItemCreate, ItemSummary>({ url: route("/items"), body: item });
}
async get(id: string) {
const payload = await this.http.get<Item>({ url: route(`/items/${id}`) });
const payload = await this.http.get<ItemOut>({ url: route(`/items/${id}`) });
if (!payload.data) {
return payload;
}
// Parse Date Types
payload.data.purchaseTime = new Date(payload.data.purchaseTime);
payload.data.soldTime = new Date(payload.data.soldTime);
payload.data.warrantyExpires = new Date(payload.data.warrantyExpires);
payload.data = parseDate(payload.data, ["purchaseTime", "soldTime", "warrantyExpires"]);
return payload;
}
@ -62,8 +28,17 @@ export class ItemsApi extends BaseAPI {
return this.http.delete<void>({ url: route(`/items/${id}`) });
}
update(id: string, item: ItemCreate) {
return this.http.put<ItemCreate, Item>({ url: route(`/items/${id}`), body: item });
async update(id: string, item: ItemUpdate) {
const payload = await this.http.put<ItemCreate, ItemOut>({
url: route(`/items/${id}`),
body: this.dropFields(item),
});
if (!payload.data) {
return payload;
}
payload.data = parseDate(payload.data, ["purchaseTime", "soldTime", "warrantyExpires"]);
return payload;
}
import(file: File) {

View file

@ -1,37 +1,25 @@
import { BaseAPI, route } from "../base";
import { Item } from "./items";
import { Details, OutType, Results } from "./types";
export type LabelCreate = Details & {
color: string;
};
export type LabelUpdate = LabelCreate;
export type Label = LabelCreate &
OutType & {
groupId: string;
items: Item[];
};
import { LabelCreate, LabelOut } from "../types/data-contracts";
import { Results } from "./types";
export class LabelsApi extends BaseAPI {
getAll() {
return this.http.get<Results<Label>>({ url: route("/labels") });
return this.http.get<Results<LabelOut>>({ url: route("/labels") });
}
create(body: LabelCreate) {
return this.http.post<LabelCreate, Label>({ url: route("/labels"), body });
return this.http.post<LabelCreate, LabelOut>({ url: route("/labels"), body });
}
get(id: string) {
return this.http.get<Label>({ url: route(`/labels/${id}`) });
return this.http.get<LabelOut>({ url: route(`/labels/${id}`) });
}
delete(id: string) {
return this.http.delete<void>({ url: route(`/labels/${id}`) });
}
update(id: string, body: LabelUpdate) {
return this.http.put<LabelUpdate, Label>({ url: route(`/labels/${id}`), body });
update(id: string, body: LabelCreate) {
return this.http.put<LabelCreate, LabelOut>({ url: route(`/labels/${id}`), body });
}
}

View file

@ -1,29 +1,20 @@
import { BaseAPI, route } from "../base";
import { Item } from "./items";
import { Details, OutType, Results } from "./types";
export type LocationCreate = Details;
export type Location = LocationCreate &
OutType & {
groupId: string;
items: Item[];
itemCount: number;
};
import { LocationCount, LocationCreate, LocationOut } from "../types/data-contracts";
import { Results } from "./types";
export type LocationUpdate = LocationCreate;
export class LocationsApi extends BaseAPI {
getAll() {
return this.http.get<Results<Location>>({ url: route("/locations") });
return this.http.get<Results<LocationCount>>({ url: route("/locations") });
}
create(body: LocationCreate) {
return this.http.post<LocationCreate, Location>({ url: route("/locations"), body });
return this.http.post<LocationCreate, LocationOut>({ url: route("/locations"), body });
}
get(id: string) {
return this.http.get<Location>({ url: route(`/locations/${id}`) });
return this.http.get<LocationOut>({ url: route(`/locations/${id}`) });
}
delete(id: string) {
@ -31,6 +22,6 @@ export class LocationsApi extends BaseAPI {
}
update(id: string, body: LocationUpdate) {
return this.http.put<LocationUpdate, Location>({ url: route(`/locations/${id}`), body });
return this.http.put<LocationUpdate, LocationOut>({ url: route(`/locations/${id}`), body });
}
}

View file

@ -1,19 +1,3 @@
/**
* OutType is the base type that is returned from the API.
* In contains the common fields that are included with every
* API response that isn't a bulk result
*/
export type OutType = {
id: string;
createdAt: string;
updatedAt: string;
};
export type Details = {
name: string;
description: string;
};
export type Results<T> = {
items: T[];
};

View file

@ -0,0 +1,262 @@
/* post-processed by ./scripts/process-types.py */
/* eslint-disable */
/* tslint:disable */
/*
* ---------------------------------------------------------------
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
* ## ##
* ## AUTHOR: acacode ##
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
* ---------------------------------------------------------------
*/
export interface ServerResult {
details: any;
error: boolean;
item: any;
message: string;
}
export interface ServerResults {
items: any;
}
export interface ApiSummary {
health: boolean;
message: string;
title: string;
versions: string[];
}
export interface DocumentOut {
id: string;
path: string;
title: string;
}
export interface ItemAttachment {
createdAt: Date;
document: DocumentOut;
id: string;
updatedAt: Date;
}
export interface ItemCreate {
description: string;
labelIds: string[];
/** Edges */
locationId: string;
name: string;
}
export interface ItemOut {
attachments: ItemAttachment[];
createdAt: Date;
description: string;
id: string;
insured: boolean;
labels: LabelSummary[];
/** Warranty */
lifetimeWarranty: boolean;
/** Edges */
location: LocationSummary;
manufacturer: string;
modelNumber: string;
name: string;
/** Extras */
notes: string;
purchaseFrom: string;
/** @example 0 */
purchasePrice: string;
/** Purchase */
purchaseTime: Date;
quantity: number;
/** Identifications */
serialNumber: string;
soldNotes: string;
/** @example 0 */
soldPrice: string;
/** Sold */
soldTime: Date;
soldTo: string;
updatedAt: Date;
warrantyDetails: string;
warrantyExpires: Date;
}
export interface ItemSummary {
createdAt: Date;
description: string;
id: string;
insured: boolean;
labels: LabelSummary[];
/** Warranty */
lifetimeWarranty: boolean;
/** Edges */
location: LocationSummary;
manufacturer: string;
modelNumber: string;
name: string;
/** Extras */
notes: string;
purchaseFrom: string;
/** @example 0 */
purchasePrice: string;
/** Purchase */
purchaseTime: Date;
quantity: number;
/** Identifications */
serialNumber: string;
soldNotes: string;
/** @example 0 */
soldPrice: string;
/** Sold */
soldTime: Date;
soldTo: string;
updatedAt: Date;
warrantyDetails: string;
warrantyExpires: Date;
}
export interface ItemUpdate {
description: string;
id: string;
insured: boolean;
labelIds: string[];
/** Warranty */
lifetimeWarranty: boolean;
/** Edges */
locationId: string;
manufacturer: string;
modelNumber: string;
name: string;
/** Extras */
notes: string;
purchaseFrom: string;
/** @example 0 */
purchasePrice: string;
/** Purchase */
purchaseTime: Date;
quantity: number;
/** Identifications */
serialNumber: string;
soldNotes: string;
/** @example 0 */
soldPrice: string;
/** Sold */
soldTime: Date;
soldTo: string;
warrantyDetails: string;
warrantyExpires: Date;
}
export interface LabelCreate {
color: string;
description: string;
name: string;
}
export interface LabelOut {
createdAt: Date;
description: string;
groupId: string;
id: string;
items: ItemSummary[];
name: string;
updatedAt: Date;
}
export interface LabelSummary {
createdAt: Date;
description: string;
groupId: string;
id: string;
name: string;
updatedAt: Date;
}
export interface LocationCount {
createdAt: Date;
description: string;
id: string;
itemCount: number;
name: string;
updatedAt: Date;
}
export interface LocationCreate {
description: string;
name: string;
}
export interface LocationOut {
createdAt: Date;
description: string;
id: string;
items: ItemSummary[];
name: string;
updatedAt: Date;
}
export interface LocationSummary {
createdAt: Date;
description: string;
id: string;
name: string;
updatedAt: Date;
}
export interface TokenResponse {
expiresAt: string;
token: string;
}
export interface UserIn {
email: string;
name: string;
password: string;
}
export interface UserOut {
email: string;
groupId: string;
groupName: string;
id: string;
isSuperuser: boolean;
name: string;
}
export interface UserRegistration {
groupName: string;
user: UserIn;
}
export interface UserUpdate {
email: string;
name: string;
}