Skip to content

Commit cc66330

Browse files
feat: Add item templates feature (#435) (#1099)
* feat: Add item templates feature (#435) Add ability to create and manage item templates for quick item creation. Templates store default values and custom fields that can be applied when creating new items. Backend changes: - New ItemTemplate and TemplateField Ent schemas - Template CRUD API endpoints - Create item from template endpoint Frontend changes: - Templates management page with create/edit/delete - Template selector in item creation modal - 'Use as Template' action on item detail page - Templates link in navigation menu * refactor: Improve template item creation with a single query - Add `CreateFromTemplate` method to ItemsRepository that creates items with all template data (including custom fields) in a single atomic transaction, replacing the previous two-phase create-then-update pattern - Fix `GetOne` to require group ID parameter so templates can only be accessed by users in the owning group (security fix) - Simplify `HandleItemTemplatesCreateItem` handler using the new transactional method * Refactor item template types and formatting Updated type annotations in CreateModal.vue to use specific ItemTemplate types instead of 'any'. Improved code formatting for template fields and manufacturer display. Also refactored warranty field logic in item details page for better readability. This resolves the linter issues as well that the bot in github keeps nagging at. * Add 'id' property to template fields Introduces an 'id' property to each field object in CreateModal.vue and item details page to support unique identification of fields. This change prepares the codebase for future enhancements that may require field-level identification. * Removed redundant SQL migrations. Removed redundant SQL migrations per @tankerkiller125's findings. * Updates to PR #1099. Regarding pull #1099. Fixed an issue causing some conflict with GUIDs and old rows in the migration files. * Add new fields and location edge to ItemTemplate Addresses recommendations from @tonyaellie. * Relocated add template button * Added more default fields to the template * Added translation of all strings (think so?) * Make oval buttons round * Added duplicate button to the template (this required a rewrite of the migration files, I made sure only 1 exists per DB type) * Added a Save as template button to a item detail view (this creates a template with all the current data of that item) * Changed all occurrences of space to gap and flex where applicable. * Made template selection persistent after item created. * Collapsible template info on creation view. * Updates to translation and fix for labels/locations I also added a test in here because I keep missing small function tests. That should prevent that from happening again. * Linted * Bring up to date with main, fix some lint/type check issues * In theory fix playwright tests * Fix defaults being unable to be nullable/empty (and thus limiting flexibility) * Last few fixes I think * Forgot to fix the golang tests --------- Co-authored-by: Matthew Kilgore <[email protected]>
1 parent 3671ba2 commit cc66330

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+29982
-1529
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package v1
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/google/uuid"
7+
"github.com/hay-kot/httpkit/errchain"
8+
"github.com/sysadminsmedia/homebox/backend/internal/core/services"
9+
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
10+
"github.com/sysadminsmedia/homebox/backend/internal/web/adapters"
11+
)
12+
13+
// HandleItemTemplatesGetAll godoc
14+
//
15+
// @Summary Get All Item Templates
16+
// @Tags Item Templates
17+
// @Produce json
18+
// @Success 200 {object} []repo.ItemTemplateSummary
19+
// @Router /v1/templates [GET]
20+
// @Security Bearer
21+
func (ctrl *V1Controller) HandleItemTemplatesGetAll() errchain.HandlerFunc {
22+
fn := func(r *http.Request) ([]repo.ItemTemplateSummary, error) {
23+
auth := services.NewContext(r.Context())
24+
return ctrl.repo.ItemTemplates.GetAll(r.Context(), auth.GID)
25+
}
26+
27+
return adapters.Command(fn, http.StatusOK)
28+
}
29+
30+
// HandleItemTemplatesGet godoc
31+
//
32+
// @Summary Get Item Template
33+
// @Tags Item Templates
34+
// @Produce json
35+
// @Param id path string true "Template ID"
36+
// @Success 200 {object} repo.ItemTemplateOut
37+
// @Router /v1/templates/{id} [GET]
38+
// @Security Bearer
39+
func (ctrl *V1Controller) HandleItemTemplatesGet() errchain.HandlerFunc {
40+
fn := func(r *http.Request, ID uuid.UUID) (repo.ItemTemplateOut, error) {
41+
auth := services.NewContext(r.Context())
42+
return ctrl.repo.ItemTemplates.GetOne(r.Context(), auth.GID, ID)
43+
}
44+
45+
return adapters.CommandID("id", fn, http.StatusOK)
46+
}
47+
48+
// HandleItemTemplatesCreate godoc
49+
//
50+
// @Summary Create Item Template
51+
// @Tags Item Templates
52+
// @Produce json
53+
// @Param payload body repo.ItemTemplateCreate true "Template Data"
54+
// @Success 201 {object} repo.ItemTemplateOut
55+
// @Router /v1/templates [POST]
56+
// @Security Bearer
57+
func (ctrl *V1Controller) HandleItemTemplatesCreate() errchain.HandlerFunc {
58+
fn := func(r *http.Request, body repo.ItemTemplateCreate) (repo.ItemTemplateOut, error) {
59+
auth := services.NewContext(r.Context())
60+
return ctrl.repo.ItemTemplates.Create(r.Context(), auth.GID, body)
61+
}
62+
63+
return adapters.Action(fn, http.StatusCreated)
64+
}
65+
66+
// HandleItemTemplatesUpdate godoc
67+
//
68+
// @Summary Update Item Template
69+
// @Tags Item Templates
70+
// @Produce json
71+
// @Param id path string true "Template ID"
72+
// @Param payload body repo.ItemTemplateUpdate true "Template Data"
73+
// @Success 200 {object} repo.ItemTemplateOut
74+
// @Router /v1/templates/{id} [PUT]
75+
// @Security Bearer
76+
func (ctrl *V1Controller) HandleItemTemplatesUpdate() errchain.HandlerFunc {
77+
fn := func(r *http.Request, ID uuid.UUID, body repo.ItemTemplateUpdate) (repo.ItemTemplateOut, error) {
78+
auth := services.NewContext(r.Context())
79+
body.ID = ID
80+
return ctrl.repo.ItemTemplates.Update(r.Context(), auth.GID, body)
81+
}
82+
83+
return adapters.ActionID("id", fn, http.StatusOK)
84+
}
85+
86+
// HandleItemTemplatesDelete godoc
87+
//
88+
// @Summary Delete Item Template
89+
// @Tags Item Templates
90+
// @Produce json
91+
// @Param id path string true "Template ID"
92+
// @Success 204
93+
// @Router /v1/templates/{id} [DELETE]
94+
// @Security Bearer
95+
func (ctrl *V1Controller) HandleItemTemplatesDelete() errchain.HandlerFunc {
96+
fn := func(r *http.Request, ID uuid.UUID) (any, error) {
97+
auth := services.NewContext(r.Context())
98+
err := ctrl.repo.ItemTemplates.Delete(r.Context(), auth.GID, ID)
99+
return nil, err
100+
}
101+
102+
return adapters.CommandID("id", fn, http.StatusNoContent)
103+
}
104+
105+
type ItemTemplateCreateItemRequest struct {
106+
Name string `json:"name" validate:"required,min=1,max=255"`
107+
Description string `json:"description" validate:"max=1000"`
108+
LocationID uuid.UUID `json:"locationId" validate:"required"`
109+
LabelIDs []uuid.UUID `json:"labelIds"`
110+
Quantity *int `json:"quantity"`
111+
}
112+
113+
// HandleItemTemplatesCreateItem godoc
114+
//
115+
// @Summary Create Item from Template
116+
// @Tags Item Templates
117+
// @Produce json
118+
// @Param id path string true "Template ID"
119+
// @Param payload body ItemTemplateCreateItemRequest true "Item Data"
120+
// @Success 201 {object} repo.ItemOut
121+
// @Router /v1/templates/{id}/create-item [POST]
122+
// @Security Bearer
123+
func (ctrl *V1Controller) HandleItemTemplatesCreateItem() errchain.HandlerFunc {
124+
fn := func(r *http.Request, templateID uuid.UUID, body ItemTemplateCreateItemRequest) (repo.ItemOut, error) {
125+
auth := services.NewContext(r.Context())
126+
127+
template, err := ctrl.repo.ItemTemplates.GetOne(r.Context(), auth.GID, templateID)
128+
if err != nil {
129+
return repo.ItemOut{}, err
130+
}
131+
132+
quantity := template.DefaultQuantity
133+
if body.Quantity != nil {
134+
quantity = *body.Quantity
135+
}
136+
137+
// Build custom fields from template
138+
fields := make([]repo.ItemField, len(template.Fields))
139+
for i, f := range template.Fields {
140+
fields[i] = repo.ItemField{
141+
Type: f.Type,
142+
Name: f.Name,
143+
TextValue: f.TextValue,
144+
}
145+
}
146+
147+
// Create item with all template data in a single transaction
148+
return ctrl.repo.Items.CreateFromTemplate(r.Context(), auth.GID, repo.ItemCreateFromTemplate{
149+
Name: body.Name,
150+
Description: body.Description,
151+
Quantity: quantity,
152+
LocationID: body.LocationID,
153+
LabelIDs: body.LabelIDs,
154+
Insured: template.DefaultInsured,
155+
Manufacturer: template.DefaultManufacturer,
156+
ModelNumber: template.DefaultModelNumber,
157+
LifetimeWarranty: template.DefaultLifetimeWarranty,
158+
WarrantyDetails: template.DefaultWarrantyDetails,
159+
Fields: fields,
160+
})
161+
}
162+
163+
return adapters.ActionID("id", fn, http.StatusCreated)
164+
}

backend/app/api/routes.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,14 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
145145

146146
r.Get("/assets/{id}", chain.ToHandlerFunc(v1Ctrl.HandleAssetGet(), userMW...))
147147

148+
// Item Templates
149+
r.Get("/templates", chain.ToHandlerFunc(v1Ctrl.HandleItemTemplatesGetAll(), userMW...))
150+
r.Post("/templates", chain.ToHandlerFunc(v1Ctrl.HandleItemTemplatesCreate(), userMW...))
151+
r.Get("/templates/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemTemplatesGet(), userMW...))
152+
r.Put("/templates/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemTemplatesUpdate(), userMW...))
153+
r.Delete("/templates/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemTemplatesDelete(), userMW...))
154+
r.Post("/templates/{id}/create-item", chain.ToHandlerFunc(v1Ctrl.HandleItemTemplatesCreateItem(), userMW...))
155+
148156
// Maintenance
149157
r.Get("/maintenance", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceGetAll(), userMW...))
150158
r.Put("/maintenance/{id}", chain.ToHandlerFunc(v1Ctrl.HandleMaintenanceEntryUpdate(), userMW...))

0 commit comments

Comments
 (0)