feat: locations tree viewer (#248)

* location tree API

* test fixes

* initial tree location elements

* locations tree page

* update meta-data

* code-gen

* store item display preferences

* introduce basic table/card view elements

* codegen

* set parent location during location creation

* add item support for tree query

* refactor tree view

* wip: location selector improvements

* type gen

* rename items -> search

* remove various log statements

* fix markdown rendering for description

* update location selectors

* fix tests

* fix currency tests

* formatting
This commit is contained in:
Hayden 2023-01-28 11:53:00 -09:00 committed by GitHub
parent 4d220cdd9c
commit 3d295b5132
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 1119 additions and 79 deletions

View file

@ -19,8 +19,9 @@ type LocationRepository struct {
type (
LocationCreate struct {
Name string `json:"name"`
Description string `json:"description"`
Name string `json:"name"`
ParentID uuid.UUID `json:"parentId" extensions:"x-nullable"`
Description string `json:"description"`
}
LocationUpdate struct {
@ -152,7 +153,9 @@ func (r *LocationRepository) getOne(ctx context.Context, where ...predicate.Loca
Where(where...).
WithGroup().
WithItems(func(iq *ent.ItemQuery) {
iq.Where(item.Archived(false)).WithLabel()
iq.Where(item.Archived(false)).
Order(ent.Asc(item.FieldName)).
WithLabel()
}).
WithParent().
WithChildren().
@ -168,11 +171,16 @@ func (r *LocationRepository) GetOneByGroup(ctx context.Context, GID, ID uuid.UUI
}
func (r *LocationRepository) Create(ctx context.Context, GID uuid.UUID, data LocationCreate) (LocationOut, error) {
location, err := r.db.Location.Create().
q := r.db.Location.Create().
SetName(data.Name).
SetDescription(data.Description).
SetGroupID(GID).
Save(ctx)
SetGroupID(GID)
if data.ParentID != uuid.Nil {
q.SetParentID(data.ParentID)
}
location, err := q.Save(ctx)
if err != nil {
return LocationOut{}, err
@ -218,3 +226,129 @@ func (r *LocationRepository) DeleteByGroup(ctx context.Context, GID, ID uuid.UUI
_, err := r.db.Location.Delete().Where(location.ID(ID), location.HasGroupWith(group.ID(GID))).Exec(ctx)
return err
}
type TreeItem struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Children []*TreeItem `json:"children"`
}
type FlatTreeItem struct {
ID uuid.UUID
Name string
Type string
ParentID uuid.UUID
Level int
}
type TreeQuery struct {
WithItems bool `json:"withItems"`
}
func (lr *LocationRepository) Tree(ctx context.Context, GID uuid.UUID, tq TreeQuery) ([]TreeItem, error) {
query := `
WITH recursive location_tree(id, NAME, location_children, level, node_type) AS
(
SELECT id,
NAME,
location_children,
0 AS level,
'location' AS node_type
FROM locations
WHERE location_children IS NULL
AND group_locations = ?
UNION ALL
SELECT c.id,
c.NAME,
c.location_children,
level + 1,
'location' AS node_type
FROM locations c
JOIN location_tree p
ON c.location_children = p.id
WHERE level < 10 -- prevent infinite loop & excessive recursion
{{ WITH_ITEMS }}
)
SELECT id,
NAME,
level,
location_children,
node_type
FROM location_tree
ORDER BY level,
node_type DESC, -- sort locations before items
NAME;`
if tq.WithItems {
itemQuery := `
UNION ALL
SELECT i.id,
i.name,
location_items as location_children,
level + 1,
'item' AS node_type
FROM items i
JOIN location_tree p
ON i.location_items = p.id
WHERE level < 10 -- prevent infinite loop & excessive recursion`
query = strings.ReplaceAll(query, "{{ WITH_ITEMS }}", itemQuery)
} else {
query = strings.ReplaceAll(query, "{{ WITH_ITEMS }}", "")
}
rows, err := lr.db.Sql().QueryContext(ctx, query, GID)
if err != nil {
return nil, err
}
defer rows.Close()
var locations []FlatTreeItem
for rows.Next() {
var location FlatTreeItem
if err := rows.Scan(&location.ID, &location.Name, &location.Level, &location.ParentID, &location.Type); err != nil {
return nil, err
}
locations = append(locations, location)
}
if err := rows.Err(); err != nil {
return nil, err
}
return ConvertLocationsToTree(locations), nil
}
func ConvertLocationsToTree(locations []FlatTreeItem) []TreeItem {
var locationMap = make(map[uuid.UUID]*TreeItem, len(locations))
var rootIds []uuid.UUID
for _, location := range locations {
loc := &TreeItem{
ID: location.ID,
Name: location.Name,
Type: location.Type,
Children: []*TreeItem{},
}
locationMap[location.ID] = loc
if location.ParentID != uuid.Nil {
parent, ok := locationMap[location.ParentID]
if ok {
parent.Children = append(parent.Children, loc)
}
} else {
rootIds = append(rootIds, location.ID)
}
}
roots := make([]TreeItem, 0, len(rootIds))
for _, id := range rootIds {
roots = append(roots, *locationMap[id])
}
return roots
}