From b8cee37fdcb553d8ab702a19c1f1a1622dd08c9a Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sun, 11 Sep 2022 21:07:51 -0800 Subject: [PATCH] WIP item update client side actions --- Taskfile.yml | 23 ++- backend/app/api/docs/docs.go | 86 +++++++++++ backend/app/api/docs/swagger.json | 86 +++++++++++ backend/app/api/docs/swagger.yaml | 59 ++++++++ backend/app/api/v1/v1_ctrl_items.go | 5 +- backend/pkgs/server/request.go | 2 +- frontend/components/Base/Details.vue | 6 +- frontend/components/Form/DatePicker.vue | 5 + frontend/components/Form/Multiselect.vue | 33 ++--- frontend/components/Form/Select.vue | 14 +- frontend/components/Item/Card.vue | 4 +- frontend/components/Item/CreateModal.vue | 7 +- frontend/components/global/DateTime.vue | 52 +++++++ frontend/composables/use-preferences.ts | 2 + frontend/composables/utils.ts | 22 +++ frontend/lib/api/base/base-api.ts | 22 +++ frontend/lib/api/classes/items.ts | 17 ++- frontend/lib/api/types/data-contracts.ts | 36 +++++ frontend/pages/item/[id]/edit.vue | 180 ++++++++++++++++++----- frontend/pages/item/[id]/index.vue | 27 +++- 20 files changed, 595 insertions(+), 93 deletions(-) create mode 100644 frontend/components/global/DateTime.vue create mode 100644 frontend/composables/utils.ts diff --git a/Taskfile.yml b/Taskfile.yml index ad23351..722d647 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -5,7 +5,6 @@ tasks: cmds: - | cd backend && ent generate ./ent/schema \ - --template=ent/schema/templates/stringer.tmpl \ --template=ent/schema/templates/has_id.tmpl - cd backend/app/api/ && swag fmt - cd backend/app/api/ && swag init --dir=./,../../internal,../../pkgs @@ -17,18 +16,12 @@ tasks: --path ./backend/app/api/docs/swagger.json \ --output ./frontend/lib/api/types - # Output cleanup for the generated types - # 1. Remove the prefix `Types` from each type generated - # 2. Remove the ?: from the type definition since types are not properly annotated in the swagger.json - python3 ./scripts/process-types.py ./frontend/lib/api/types/data-contracts.ts api: cmds: - task: generate - cd backend && go run ./app/api/ {{.CLI_ARGS}} silent: false - sources: - - ./backend/**/*.go api:build: cmds: @@ -40,6 +33,10 @@ tasks: - cd backend && go test ./app/api/ silent: true + api:watch: + cmds: + - cd backend && gotestsum --watch ./... + api:coverage: cmds: - cd backend && go test -race -coverprofile=coverage.out -covermode=atomic ./app/... ./internal/... ./pkgs/... -v -cover @@ -53,12 +50,12 @@ tasks: - cd frontend && pnpm run test:ci silent: true - docker:build: + frontend:watch: + desc: Starts the vitest test runner in watch mode cmds: - - cd backend && docker-compose up --build - silent: true + - cd frontend && pnpm vitest --watch - generate:types: + frontend: + desc: Run frontend development server cmds: - - cd backend && go run ./app/generator - silent: true + - cd frontend && pnpm dev diff --git a/backend/app/api/docs/docs.go b/backend/app/api/docs/docs.go index 9442e2d..5222143 100644 --- a/backend/app/api/docs/docs.go +++ b/backend/app/api/docs/docs.go @@ -175,6 +175,15 @@ const docTemplate = `{ "name": "id", "in": "path", "required": true + }, + { + "description": "Item Data", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/types.ItemUpdate" + } } ], "responses": { @@ -1053,6 +1062,83 @@ const docTemplate = `{ } } }, + "types.ItemUpdate": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "insured": { + "type": "boolean" + }, + "labelIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "lifetimeWarranty": { + "description": "Warranty", + "type": "boolean" + }, + "locationId": { + "description": "Edges", + "type": "string" + }, + "manufacturer": { + "type": "string" + }, + "modelNumber": { + "type": "string" + }, + "name": { + "type": "string" + }, + "notes": { + "description": "Extras", + "type": "string" + }, + "purchaseFrom": { + "type": "string" + }, + "purchasePrice": { + "type": "number" + }, + "purchaseTime": { + "description": "Purchase", + "type": "string" + }, + "quantity": { + "type": "integer" + }, + "serialNumber": { + "description": "Identifications", + "type": "string" + }, + "soldNotes": { + "type": "string" + }, + "soldPrice": { + "type": "number" + }, + "soldTime": { + "description": "Sold", + "type": "string" + }, + "soldTo": { + "type": "string" + }, + "warrantyDetails": { + "type": "string" + }, + "warrantyExpires": { + "type": "string" + } + } + }, "types.LabelCreate": { "type": "object", "properties": { diff --git a/backend/app/api/docs/swagger.json b/backend/app/api/docs/swagger.json index 7e5b242..b75149f 100644 --- a/backend/app/api/docs/swagger.json +++ b/backend/app/api/docs/swagger.json @@ -167,6 +167,15 @@ "name": "id", "in": "path", "required": true + }, + { + "description": "Item Data", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/types.ItemUpdate" + } } ], "responses": { @@ -1045,6 +1054,83 @@ } } }, + "types.ItemUpdate": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "insured": { + "type": "boolean" + }, + "labelIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "lifetimeWarranty": { + "description": "Warranty", + "type": "boolean" + }, + "locationId": { + "description": "Edges", + "type": "string" + }, + "manufacturer": { + "type": "string" + }, + "modelNumber": { + "type": "string" + }, + "name": { + "type": "string" + }, + "notes": { + "description": "Extras", + "type": "string" + }, + "purchaseFrom": { + "type": "string" + }, + "purchasePrice": { + "type": "number" + }, + "purchaseTime": { + "description": "Purchase", + "type": "string" + }, + "quantity": { + "type": "integer" + }, + "serialNumber": { + "description": "Identifications", + "type": "string" + }, + "soldNotes": { + "type": "string" + }, + "soldPrice": { + "type": "number" + }, + "soldTime": { + "description": "Sold", + "type": "string" + }, + "soldTo": { + "type": "string" + }, + "warrantyDetails": { + "type": "string" + }, + "warrantyExpires": { + "type": "string" + } + } + }, "types.LabelCreate": { "type": "object", "properties": { diff --git a/backend/app/api/docs/swagger.yaml b/backend/app/api/docs/swagger.yaml index ad01ccb..bac550f 100644 --- a/backend/app/api/docs/swagger.yaml +++ b/backend/app/api/docs/swagger.yaml @@ -179,6 +179,59 @@ definitions: warrantyExpires: type: string type: object + types.ItemUpdate: + properties: + description: + type: string + id: + type: string + insured: + type: boolean + labelIds: + items: + type: string + type: array + lifetimeWarranty: + description: Warranty + type: boolean + locationId: + description: Edges + type: string + manufacturer: + type: string + modelNumber: + type: string + name: + type: string + notes: + description: Extras + type: string + purchaseFrom: + type: string + purchasePrice: + type: number + purchaseTime: + description: Purchase + type: string + quantity: + type: integer + serialNumber: + description: Identifications + type: string + soldNotes: + type: string + soldPrice: + type: number + soldTime: + description: Sold + type: string + soldTo: + type: string + warrantyDetails: + type: string + warrantyExpires: + type: string + type: object types.LabelCreate: properties: color: @@ -415,6 +468,12 @@ paths: name: id required: true type: string + - description: Item Data + in: body + name: payload + required: true + schema: + $ref: '#/definitions/types.ItemUpdate' produces: - application/json responses: diff --git a/backend/app/api/v1/v1_ctrl_items.go b/backend/app/api/v1/v1_ctrl_items.go index 3c3132f..23a0d92 100644 --- a/backend/app/api/v1/v1_ctrl_items.go +++ b/backend/app/api/v1/v1_ctrl_items.go @@ -64,7 +64,7 @@ func (ctrl *V1Controller) HandleItemsCreate() http.HandlerFunc { // @Summary deletes a item // @Tags Items // @Produce json -// @Param id path string true "Item ID" +// @Param id path string true "Item ID" // @Success 204 // @Router /v1/items/{id} [DELETE] // @Security Bearer @@ -90,7 +90,7 @@ func (ctrl *V1Controller) HandleItemDelete() http.HandlerFunc { // @Tags Items // @Produce json // @Param id path string true "Item ID" -// @Success 200 {object} types.ItemOut +// @Success 200 {object} types.ItemOut // @Router /v1/items/{id} [GET] // @Security Bearer func (ctrl *V1Controller) HandleItemGet() http.HandlerFunc { @@ -115,6 +115,7 @@ func (ctrl *V1Controller) HandleItemGet() http.HandlerFunc { // @Tags Items // @Produce json // @Param id path string true "Item ID" +// @Param payload body types.ItemUpdate true "Item Data" // @Success 200 {object} types.ItemOut // @Router /v1/items/{id} [PUT] // @Security Bearer diff --git a/backend/pkgs/server/request.go b/backend/pkgs/server/request.go index c4b30a4..ffb76d1 100644 --- a/backend/pkgs/server/request.go +++ b/backend/pkgs/server/request.go @@ -9,7 +9,7 @@ import ( // body is decoded into the provided value. func Decode(r *http.Request, val interface{}) error { decoder := json.NewDecoder(r.Body) - decoder.DisallowUnknownFields() + // decoder.DisallowUnknownFields() if err := decoder.Decode(val); err != nil { return err } diff --git a/frontend/components/Base/Details.vue b/frontend/components/Base/Details.vue index ca1225b..27d41f8 100644 --- a/frontend/components/Base/Details.vue +++ b/frontend/components/Base/Details.vue @@ -15,7 +15,7 @@ {{ dKey }}
- + {{ dValue }}
@@ -28,6 +28,10 @@ diff --git a/frontend/components/Form/Select.vue b/frontend/components/Form/Select.vue index 8f69c6a..bfdc9ff 100644 --- a/frontend/components/Form/Select.vue +++ b/frontend/components/Form/Select.vue @@ -3,9 +3,9 @@ - - @@ -47,10 +47,16 @@ () => props.items, () => { if (props.selectFirst && props.items.length > 0) { - value.value = props.items[0]; + selectedIdx.value = 0; } } ); - const value = useVModel(props, "modelValue", emit); + const selectedIdx = ref(0); + watch( + () => selectedIdx.value, + () => { + emit("update:modelValue", props.items[selectedIdx.value]); + } + ); diff --git a/frontend/components/Item/Card.vue b/frontend/components/Item/Card.vue index c76b576..d5a8fed 100644 --- a/frontend/components/Item/Card.vue +++ b/frontend/components/Item/Card.vue @@ -22,11 +22,11 @@ diff --git a/frontend/composables/use-preferences.ts b/frontend/composables/use-preferences.ts index db3eab0..c1e8f66 100644 --- a/frontend/composables/use-preferences.ts +++ b/frontend/composables/use-preferences.ts @@ -3,6 +3,7 @@ import { Ref } from "vue"; export type LocationViewPreferences = { showDetails: boolean; showEmpty: boolean; + editorSimpleView: boolean; }; /** @@ -15,6 +16,7 @@ export function useViewPreferences(): Ref { { showDetails: true, showEmpty: true, + editorSimpleView: true, }, { mergeDefaults: true } ); diff --git a/frontend/composables/utils.ts b/frontend/composables/utils.ts new file mode 100644 index 0000000..05dbb87 --- /dev/null +++ b/frontend/composables/utils.ts @@ -0,0 +1,22 @@ +export function validDate(dt: Date | string | null | undefined): boolean { + if (!dt) { + return false; + } + + // If it's a string, try to parse it + if (typeof dt === "string") { + const parsed = new Date(dt); + if (isNaN(parsed.getTime())) { + return false; + } + } + + // If it's a date, check if it's valid + if (dt instanceof Date) { + if (dt.getFullYear() < 1000) { + return false; + } + } + + return true; +} diff --git a/frontend/lib/api/base/base-api.ts b/frontend/lib/api/base/base-api.ts index 55c5b5a..cf3a9f5 100644 --- a/frontend/lib/api/base/base-api.ts +++ b/frontend/lib/api/base/base-api.ts @@ -36,4 +36,26 @@ export class BaseAPI { 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(obj: T, keys: Array = []): T { + const result = { ...obj }; + console.log("dropFields", result); + [...keys, "createdAt", "updatedAt"].forEach(key => { + console.log(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]; + console.log("dropping", key); + } + }); + console.log("dropFields", result); + return result; + } } diff --git a/frontend/lib/api/classes/items.ts b/frontend/lib/api/classes/items.ts index 46b90c4..bfcfde3 100644 --- a/frontend/lib/api/classes/items.ts +++ b/frontend/lib/api/classes/items.ts @@ -1,6 +1,6 @@ import { BaseAPI, route } from "../base"; import { parseDate } from "../base/base-api"; -import { ItemCreate, ItemOut } from "../types/data-contracts"; +import { ItemCreate, ItemOut, ItemSummary, ItemUpdate } from "../types/data-contracts"; import { Results } from "./types"; export class ItemsApi extends BaseAPI { @@ -9,7 +9,7 @@ export class ItemsApi extends BaseAPI { } create(item: ItemCreate) { - return this.http.post({ url: route("/items"), body: item }); + return this.http.post({ url: route("/items"), body: item }); } async get(id: string) { @@ -28,8 +28,17 @@ export class ItemsApi extends BaseAPI { return this.http.delete({ url: route(`/items/${id}`) }); } - update(id: string, item: ItemCreate) { - return this.http.put({ url: route(`/items/${id}`), body: item }); + async update(id: string, item: ItemUpdate) { + const payload = await this.http.put({ + 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) { diff --git a/frontend/lib/api/types/data-contracts.ts b/frontend/lib/api/types/data-contracts.ts index 4e40c38..49c573a 100644 --- a/frontend/lib/api/types/data-contracts.ts +++ b/frontend/lib/api/types/data-contracts.ts @@ -127,6 +127,42 @@ export interface ItemSummary { 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; + purchasePrice: number; + + /** Purchase */ + purchaseTime: Date; + quantity: number; + + /** Identifications */ + serialNumber: string; + soldNotes: string; + soldPrice: number; + + /** Sold */ + soldTime: Date; + soldTo: string; + warrantyDetails: string; + warrantyExpires: Date; +} + export interface LabelCreate { color: string; description: string; diff --git a/frontend/pages/item/[id]/edit.vue b/frontend/pages/item/[id]/edit.vue index 62f4ef9..ff29bfa 100644 --- a/frontend/pages/item/[id]/edit.vue +++ b/frontend/pages/item/[id]/edit.vue @@ -1,4 +1,6 @@