endpoint for fullpath of an item

This commit is contained in:
Hayden 2024-02-25 15:48:32 -06:00
parent 5fe5fac4d4
commit 102ee5afc3
No known key found for this signature in database
GPG key ID: 17CF79474E257545
11 changed files with 347 additions and 36 deletions

View file

@ -93,6 +93,48 @@ func (ctrl *V1Controller) HandleItemsGetAll() errchain.HandlerFunc {
} }
} }
// HandleItemFullPath godoc
//
// @Summary Get the full path of an item
// @Tags Items
// @Produce json
// @Param id path string true "Item ID"
// @Success 200 {object} []repo.ItemPath
// @Router /v1/items/{id}/path [GET]
// @Security Bearer
func (ctrl *V1Controller) HandleItemFullPath() errchain.HandlerFunc {
fn := func(r *http.Request, ID uuid.UUID) ([]repo.ItemPath, error) {
auth := services.NewContext(r.Context())
item, err := ctrl.repo.Items.GetOneByGroup(auth, auth.GID, ID)
if err != nil {
return nil, err
}
paths, err := ctrl.repo.Locations.PathForLoc(auth, auth.GID, item.Location.ID)
if err != nil {
return nil, err
}
if item.Parent != nil {
paths = append(paths, repo.ItemPath{
Type: repo.ItemTypeItem,
ID: item.Parent.ID,
Name: item.Parent.Name,
})
}
paths = append(paths, repo.ItemPath{
Type: repo.ItemTypeItem,
ID: item.ID,
Name: item.Name,
})
return paths, nil
}
return adapters.CommandID("id", fn, http.StatusOK)
}
// HandleItemsCreate godoc // HandleItemsCreate godoc
// //
// @Summary Create Item // @Summary Create Item

View file

@ -122,6 +122,7 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
r.Get(v1Base("/items/fields/values"), chain.ToHandlerFunc(v1Ctrl.HandleGetAllCustomFieldValues(), userMW...)) r.Get(v1Base("/items/fields/values"), chain.ToHandlerFunc(v1Ctrl.HandleGetAllCustomFieldValues(), userMW...))
r.Get(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemGet(), userMW...)) r.Get(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemGet(), userMW...))
r.Get(v1Base("/items/{id}/path"), chain.ToHandlerFunc(v1Ctrl.HandleItemFullPath(), userMW...))
r.Put(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemUpdate(), userMW...)) r.Put(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemUpdate(), userMW...))
r.Patch(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemPatch(), userMW...)) r.Patch(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemPatch(), userMW...))
r.Delete(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemDelete(), userMW...)) r.Delete(v1Base("/items/{id}"), chain.ToHandlerFunc(v1Ctrl.HandleItemDelete(), userMW...))

View file

@ -1017,6 +1017,42 @@ const docTemplate = `{
} }
} }
}, },
"/v1/items/{id}/path": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Items"
],
"summary": "Get the full path of an item",
"parameters": [
{
"type": "string",
"description": "Item ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.ItemPath"
}
}
}
}
}
},
"/v1/labels": { "/v1/labels": {
"get": { "get": {
"security": [ "security": [
@ -2168,6 +2204,20 @@ const docTemplate = `{
} }
} }
}, },
"repo.ItemPath": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"type": {
"$ref": "#/definitions/repo.ItemType"
}
}
},
"repo.ItemSummary": { "repo.ItemSummary": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -2220,6 +2270,17 @@ const docTemplate = `{
} }
} }
}, },
"repo.ItemType": {
"type": "string",
"enum": [
"location",
"item"
],
"x-enum-varnames": [
"ItemTypeLocation",
"ItemTypeItem"
]
},
"repo.ItemUpdate": { "repo.ItemUpdate": {
"type": "object", "type": "object",
"properties": { "properties": {

View file

@ -1010,6 +1010,42 @@
} }
} }
}, },
"/v1/items/{id}/path": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Items"
],
"summary": "Get the full path of an item",
"parameters": [
{
"type": "string",
"description": "Item ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.ItemPath"
}
}
}
}
}
},
"/v1/labels": { "/v1/labels": {
"get": { "get": {
"security": [ "security": [
@ -2161,6 +2197,20 @@
} }
} }
}, },
"repo.ItemPath": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"type": {
"$ref": "#/definitions/repo.ItemType"
}
}
},
"repo.ItemSummary": { "repo.ItemSummary": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -2213,6 +2263,17 @@
} }
} }
}, },
"repo.ItemType": {
"type": "string",
"enum": [
"location",
"item"
],
"x-enum-varnames": [
"ItemTypeLocation",
"ItemTypeItem"
]
},
"repo.ItemUpdate": { "repo.ItemUpdate": {
"type": "object", "type": "object",
"properties": { "properties": {

View file

@ -206,6 +206,15 @@ definitions:
x-nullable: true x-nullable: true
x-omitempty: true x-omitempty: true
type: object type: object
repo.ItemPath:
properties:
id:
type: string
name:
type: string
type:
$ref: '#/definitions/repo.ItemType'
type: object
repo.ItemSummary: repo.ItemSummary:
properties: properties:
archived: archived:
@ -240,6 +249,14 @@ definitions:
updatedAt: updatedAt:
type: string type: string
type: object type: object
repo.ItemType:
enum:
- location
- item
type: string
x-enum-varnames:
- ItemTypeLocation
- ItemTypeItem
repo.ItemUpdate: repo.ItemUpdate:
properties: properties:
archived: archived:
@ -1264,6 +1281,28 @@ paths:
summary: Update Maintenance Entry summary: Update Maintenance Entry
tags: tags:
- Maintenance - Maintenance
/v1/items/{id}/path:
get:
parameters:
- description: Item ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/repo.ItemPath'
type: array
security:
- Bearer: []
summary: Get the full path of an item
tags:
- Items
/v1/items/export: /v1/items/export:
get: get:
responses: responses:

View file

@ -84,7 +84,7 @@ func (csf LocationString) String() string {
return strings.Join(csf, " / ") return strings.Join(csf, " / ")
} }
func fromPathSlice(s []repo.LocationPath) LocationString { func fromPathSlice(s []repo.ItemPath) LocationString {
v := make(LocationString, len(s)) v := make(LocationString, len(s))
for i := range s { for i := range s {

View file

@ -260,12 +260,20 @@ type TreeQuery struct {
WithItems bool `json:"withItems" schema:"withItems"` WithItems bool `json:"withItems" schema:"withItems"`
} }
type LocationPath struct { type ItemType string
const (
ItemTypeLocation ItemType = "location"
ItemTypeItem ItemType = "item"
)
type ItemPath struct {
Type ItemType `json:"type"`
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
Name string `json:"name"` Name string `json:"name"`
} }
func (r *LocationRepository) PathForLoc(ctx context.Context, GID, locID uuid.UUID) ([]LocationPath, error) { func (r *LocationRepository) PathForLoc(ctx context.Context, GID, locID uuid.UUID) ([]ItemPath, error) {
query := `WITH RECURSIVE location_path AS ( query := `WITH RECURSIVE location_path AS (
SELECT id, name, location_children SELECT id, name, location_children
FROM locations FROM locations
@ -288,10 +296,11 @@ func (r *LocationRepository) PathForLoc(ctx context.Context, GID, locID uuid.UUI
} }
defer func() { _ = rows.Close() }() defer func() { _ = rows.Close() }()
var locations []LocationPath var locations []ItemPath
for rows.Next() { for rows.Next() {
var location LocationPath var location ItemPath
location.Type = ItemTypeLocation
if err := rows.Scan(&location.ID, &location.Name); err != nil { if err := rows.Scan(&location.ID, &location.Name); err != nil {
return nil, err return nil, err
} }

View file

@ -1010,6 +1010,42 @@
} }
} }
}, },
"/v1/items/{id}/path": {
"get": {
"security": [
{
"Bearer": []
}
],
"produces": [
"application/json"
],
"tags": [
"Items"
],
"summary": "Get the full path of an item",
"parameters": [
{
"type": "string",
"description": "Item ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.ItemPath"
}
}
}
}
}
},
"/v1/labels": { "/v1/labels": {
"get": { "get": {
"security": [ "security": [
@ -2161,6 +2197,20 @@
} }
} }
}, },
"repo.ItemPath": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"type": {
"$ref": "#/definitions/repo.ItemType"
}
}
},
"repo.ItemSummary": { "repo.ItemSummary": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -2213,6 +2263,17 @@
} }
} }
}, },
"repo.ItemType": {
"type": "string",
"enum": [
"location",
"item"
],
"x-enum-varnames": [
"ItemTypeLocation",
"ItemTypeItem"
]
},
"repo.ItemUpdate": { "repo.ItemUpdate": {
"type": "object", "type": "object",
"properties": { "properties": {

View file

@ -5,6 +5,7 @@ import {
ItemCreate, ItemCreate,
ItemOut, ItemOut,
ItemPatch, ItemPatch,
ItemPath,
ItemSummary, ItemSummary,
ItemUpdate, ItemUpdate,
MaintenanceEntry, MaintenanceEntry,
@ -105,6 +106,10 @@ export class ItemsApi extends BaseAPI {
this.maintenance = new MaintenanceAPI(http); this.maintenance = new MaintenanceAPI(http);
} }
fullpath(id: string) {
return this.http.get<ItemPath[]>({ url: route(`/items/${id}/path`) });
}
getAll(q: ItemsQuery = {}) { getAll(q: ItemsQuery = {}) {
return this.http.get<PaginationResult<ItemSummary>>({ url: route("/items", q) }); return this.http.get<PaginationResult<ItemSummary>>({ url: route("/items", q) });
} }

View file

@ -128,6 +128,12 @@ export interface ItemPatch {
quantity?: number | null; quantity?: number | null;
} }
export interface ItemPath {
id: string;
name: string;
type: ItemType;
}
export interface ItemSummary { export interface ItemSummary {
archived: boolean; archived: boolean;
createdAt: Date | string; createdAt: Date | string;
@ -145,6 +151,11 @@ export interface ItemSummary {
updatedAt: Date | string; updatedAt: Date | string;
} }
export enum ItemType {
ItemTypeLocation = "location",
ItemTypeItem = "item",
}
export interface ItemUpdate { export interface ItemUpdate {
archived: boolean; archived: boolean;
assetId: string; assetId: string;

View file

@ -146,11 +146,6 @@
} }
const ret: Details = [ const ret: Details = [
{
name: "Description",
type: "markdown",
text: item.value?.description,
},
{ {
name: "Quantity", name: "Quantity",
text: item.value?.quantity, text: item.value?.quantity,
@ -405,6 +400,20 @@
]; ];
}); });
const fullpath = computedAsync(async () => {
if (!item.value) {
return [];
}
const resp = await api.items.fullpath(item.value.id);
if (resp.error) {
toast.error("Failed to load item");
return [];
}
return resp.data;
});
const items = computedAsync(async () => { const items = computedAsync(async () => {
if (!item.value) { if (!item.value) {
return []; return [];
@ -442,33 +451,45 @@
</dialog> </dialog>
<section> <section>
<BaseSectionHeader> <div class="bg-base-100 rounded p-3">
<Icon name="mdi-package-variant" class="mr-2 -mt-1 text-base-content" /> <header class="mb-2">
<span class="text-base-content"> <div class="flex flex-wrap items-end gap-2">
{{ item ? item.name : "" }} <div class="avatar placeholder mb-auto">
</span> <div class="bg-neutral-focus text-neutral-content rounded-full w-12">
<Icon name="mdi-package-variant" class="h-7 w-7" />
<div v-if="item.parent" class="text-sm breadcrumbs pb-0"> </div>
<ul class="text-base-content/70"> </div>
<li> <div>
<NuxtLink :to="`/item/${item.parent.id}`"> {{ item.parent.name }}</NuxtLink> <div v-if="fullpath && fullpath.length > 0" class="text-sm breadcrumbs pt-0 pb-0">
</li> <ul class="text-base-content/70">
<li>{{ item.name }}</li> <li v-for="part in fullpath" :key="part.id">
</ul> <NuxtLink :to="`/${part.type}/${part.id}`"> {{ part.name }}</NuxtLink>
</div> </li>
<template #description> </ul>
<Markdown :source="item.description"> </Markdown> </div>
<div class="flex flex-wrap gap-2 mt-3"> <h1 class="text-2xl pb-1">
<NuxtLink v-if="item.location" ref="badge" class="badge p-3" :to="`/location/${item.location.id}`"> {{ item ? item.name : "" }}
<Icon name="heroicons-map-pin" class="mr-2 swap-on"></Icon> </h1>
{{ item.location.name }} <div class="flex gap-1 flex-wrap text-xs">
</NuxtLink> <div>
<template v-if="item.labels && item.labels.length > 0"> Created
<LabelChip v-for="label in item.labels" :key="label.id" class="badge-primary" :label="label" /> <DateTime :date="item?.createdAt" />
</template> </div>
-
<div>
Updated
<DateTime :date="item?.updatedAt" />
</div>
</div>
</div>
</div> </div>
</template> </header>
</BaseSectionHeader> <div class="divider my-0 mb-1"></div>
<div class="p-1 prose max-w-[100%]">
<Markdown v-if="item && item.description" class="text-base" :source="item.description"> </Markdown>
</div>
</div>
<div class="flex flex-wrap items-center justify-between mb-6 mt-3"> <div class="flex flex-wrap items-center justify-between mb-6 mt-3">
<div class="btn-group"> <div class="btn-group">
<NuxtLink <NuxtLink