mirror of
https://github.com/hay-kot/homebox.git
synced 2024-12-18 13:06:32 +00:00
fix: conditionally filter parent locations (#133)
This commit is contained in:
parent
fbcbde836a
commit
8e1947d971
18 changed files with 135 additions and 67 deletions
36
backend/app/api/handlers/v1/query_params.go
Normal file
36
backend/app/api/handlers/v1/query_params.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func queryUUIDList(params url.Values, key string) []uuid.UUID {
|
||||
var ids []uuid.UUID
|
||||
for _, id := range params[key] {
|
||||
uid, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, uid)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func queryIntOrNegativeOne(s string) int {
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func queryBool(s string) bool {
|
||||
b, err := strconv.ParseBool(s)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return b
|
||||
}
|
|
@ -3,10 +3,7 @@ package v1
|
|||
import (
|
||||
"encoding/csv"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/homebox/backend/internal/core/services"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/repo"
|
||||
"github.com/hay-kot/homebox/backend/internal/sys/validate"
|
||||
|
@ -27,44 +24,17 @@ import (
|
|||
// @Router /v1/items [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemsGetAll() server.HandlerFunc {
|
||||
uuidList := func(params url.Values, key string) []uuid.UUID {
|
||||
var ids []uuid.UUID
|
||||
for _, id := range params[key] {
|
||||
uid, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, uid)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
intOrNegativeOne := func(s string) int {
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
getBool := func(s string) bool {
|
||||
b, err := strconv.ParseBool(s)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
extractQuery := func(r *http.Request) repo.ItemQuery {
|
||||
params := r.URL.Query()
|
||||
|
||||
return repo.ItemQuery{
|
||||
Page: intOrNegativeOne(params.Get("page")),
|
||||
PageSize: intOrNegativeOne(params.Get("perPage")),
|
||||
Page: queryIntOrNegativeOne(params.Get("page")),
|
||||
PageSize: queryIntOrNegativeOne(params.Get("perPage")),
|
||||
Search: params.Get("q"),
|
||||
LocationIDs: uuidList(params, "locations"),
|
||||
LabelIDs: uuidList(params, "labels"),
|
||||
IncludeArchived: getBool(params.Get("includeArchived")),
|
||||
LocationIDs: queryUUIDList(params, "locations"),
|
||||
LabelIDs: queryUUIDList(params, "labels"),
|
||||
IncludeArchived: queryBool(params.Get("includeArchived")),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,13 +15,21 @@ import (
|
|||
// @Summary Get All Locations
|
||||
// @Tags Locations
|
||||
// @Produce json
|
||||
// @Param filterChildren query bool false "Filter locations with parents"
|
||||
// @Success 200 {object} server.Results{items=[]repo.LocationOutCount}
|
||||
// @Router /v1/locations [GET]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLocationGetAll() server.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
user := services.UseUserCtx(r.Context())
|
||||
locations, err := ctrl.repo.Locations.GetAll(r.Context(), user.GroupID)
|
||||
|
||||
q := r.URL.Query()
|
||||
|
||||
filter := repo.LocationQuery{
|
||||
FilterChildren: queryBool(q.Get("filterChildren")),
|
||||
}
|
||||
|
||||
locations, err := ctrl.repo.Locations.GetAll(r.Context(), user.GroupID, filter)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to get locations")
|
||||
return validate.NewRequestError(err, http.StatusInternalServerError)
|
||||
|
|
|
@ -756,6 +756,14 @@ const docTemplate = `{
|
|||
"Locations"
|
||||
],
|
||||
"summary": "Get All Locations",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "Filter locations with parents",
|
||||
"name": "filterChildren",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
|
|
|
@ -748,6 +748,14 @@
|
|||
"Locations"
|
||||
],
|
||||
"summary": "Get All Locations",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "Filter locations with parents",
|
||||
"name": "filterChildren",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
|
|
|
@ -960,6 +960,11 @@ paths:
|
|||
- Labels
|
||||
/v1/locations:
|
||||
get:
|
||||
parameters:
|
||||
- description: Filter locations with parents
|
||||
in: query
|
||||
name: filterChildren
|
||||
type: boolean
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
|
|
@ -23,7 +23,7 @@ type ItemService struct {
|
|||
at attachmentTokens
|
||||
}
|
||||
|
||||
func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]string) (int, error) {
|
||||
func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data [][]string) (int, error) {
|
||||
loaded := []csvRow{}
|
||||
|
||||
// Skip first row
|
||||
|
@ -66,7 +66,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
|
|||
|
||||
// Bootstrap the locations and labels so we can reuse the created IDs for the items
|
||||
locations := map[string]uuid.UUID{}
|
||||
existingLocation, err := svc.repo.Locations.GetAll(ctx, gid)
|
||||
existingLocation, err := svc.repo.Locations.GetAll(ctx, GID, repo.LocationQuery{})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
@ -75,7 +75,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
|
|||
}
|
||||
|
||||
labels := map[string]uuid.UUID{}
|
||||
existingLabels, err := svc.repo.Labels.GetAll(ctx, gid)
|
||||
existingLabels, err := svc.repo.Labels.GetAll(ctx, GID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
|
|||
|
||||
// Locations
|
||||
if _, exists := locations[row.Location]; !exists {
|
||||
result, err := svc.repo.Locations.Create(ctx, gid, repo.LocationCreate{
|
||||
result, err := svc.repo.Locations.Create(ctx, GID, repo.LocationCreate{
|
||||
Name: row.Location,
|
||||
Description: "",
|
||||
})
|
||||
|
@ -103,7 +103,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
|
|||
if _, exists := labels[label]; exists {
|
||||
continue
|
||||
}
|
||||
result, err := svc.repo.Labels.Create(ctx, gid, repo.LabelCreate{
|
||||
result, err := svc.repo.Labels.Create(ctx, GID, repo.LabelCreate{
|
||||
Name: label,
|
||||
Description: "",
|
||||
})
|
||||
|
@ -119,7 +119,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
|
|||
for _, row := range loaded {
|
||||
// Check Import Ref
|
||||
if row.Item.ImportRef != "" {
|
||||
exists, err := svc.repo.Items.CheckRef(ctx, gid, row.Item.ImportRef)
|
||||
exists, err := svc.repo.Items.CheckRef(ctx, GID, row.Item.ImportRef)
|
||||
if exists {
|
||||
continue
|
||||
}
|
||||
|
@ -139,7 +139,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
|
|||
Str("location", row.Location).
|
||||
Msgf("Creating Item: %s", row.Item.Name)
|
||||
|
||||
result, err := svc.repo.Items.Create(ctx, gid, repo.ItemCreate{
|
||||
result, err := svc.repo.Items.Create(ctx, GID, repo.ItemCreate{
|
||||
ImportRef: row.Item.ImportRef,
|
||||
Name: row.Item.Name,
|
||||
Description: row.Item.Description,
|
||||
|
@ -152,7 +152,7 @@ func (svc *ItemService) CsvImport(ctx context.Context, gid uuid.UUID, data [][]s
|
|||
}
|
||||
|
||||
// Update the item with the rest of the data
|
||||
_, err = svc.repo.Items.UpdateByGroup(ctx, gid, repo.ItemUpdate{
|
||||
_, err = svc.repo.Items.UpdateByGroup(ctx, GID, repo.ItemUpdate{
|
||||
// Edges
|
||||
LocationID: locationID,
|
||||
LabelIDs: labelIDs,
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/repo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
@ -38,7 +39,7 @@ func TestItemService_CsvImport(t *testing.T) {
|
|||
dataCsv = append(dataCsv, newCsvRow(item))
|
||||
}
|
||||
|
||||
allLocation, err := tRepos.Locations.GetAll(context.Background(), tGroup.ID)
|
||||
allLocation, err := tRepos.Locations.GetAll(context.Background(), tGroup.ID, repo.LocationQuery{})
|
||||
assert.NoError(t, err)
|
||||
locNames := []string{}
|
||||
for _, loc := range allLocation {
|
||||
|
|
|
@ -2,6 +2,7 @@ package repo
|
|||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
@ -90,8 +91,12 @@ func mapLocationOut(location *ent.Location) LocationOut {
|
|||
}
|
||||
}
|
||||
|
||||
type LocationQuery struct {
|
||||
FilterChildren bool `json:"filterChildren"`
|
||||
}
|
||||
|
||||
// GetALlWithCount returns all locations with item count field populated
|
||||
func (r *LocationRepository) GetAll(ctx context.Context, groupId uuid.UUID) ([]LocationOutCount, error) {
|
||||
func (r *LocationRepository) GetAll(ctx context.Context, GID uuid.UUID, filter LocationQuery) ([]LocationOutCount, error) {
|
||||
query := `--sql
|
||||
SELECT
|
||||
id,
|
||||
|
@ -111,13 +116,18 @@ func (r *LocationRepository) GetAll(ctx context.Context, groupId uuid.UUID) ([]L
|
|||
FROM
|
||||
locations
|
||||
WHERE
|
||||
locations.group_locations = ?
|
||||
AND locations.location_children IS NULL
|
||||
locations.group_locations = ? {{ FILTER_CHILDREN }}
|
||||
ORDER BY
|
||||
locations.name ASC
|
||||
`
|
||||
|
||||
rows, err := r.db.Sql().QueryContext(ctx, query, groupId)
|
||||
if filter.FilterChildren {
|
||||
query = strings.Replace(query, "{{ FILTER_CHILDREN }}", "AND locations.location_children IS NULL", 1)
|
||||
} else {
|
||||
query = strings.Replace(query, "{{ FILTER_CHILDREN }}", "", 1)
|
||||
}
|
||||
|
||||
rows, err := r.db.Sql().QueryContext(ctx, query, GID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ func TestLocationRepositoryGetAllWithCount(t *testing.T) {
|
|||
|
||||
assert.NoError(t, err)
|
||||
|
||||
results, err := tRepos.Locations.GetAll(context.Background(), tGroup.ID)
|
||||
results, err := tRepos.Locations.GetAll(context.Background(), tGroup.ID, LocationQuery{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
for _, loc := range results {
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
const toast = useNotifier();
|
||||
|
||||
const locationsStore = useLocationStore();
|
||||
const locations = computed(() => locationsStore.locations);
|
||||
const locations = computed(() => locationsStore.allLocations);
|
||||
|
||||
const labelStore = useLabelStore();
|
||||
const labels = computed(() => labelStore.labels);
|
||||
|
|
|
@ -32,7 +32,8 @@
|
|||
const rmLocationStoreObserver = defineObserver("locationStore", {
|
||||
handler: r => {
|
||||
if (r.status === 201 || r.url.match(reLocation)) {
|
||||
locationStore.refresh();
|
||||
locationStore.refreshChildren();
|
||||
locationStore.refreshParents();
|
||||
}
|
||||
console.debug("locationStore handler called by observer");
|
||||
},
|
||||
|
@ -43,7 +44,8 @@
|
|||
EventTypes.ClearStores,
|
||||
() => {
|
||||
labelStore.refresh();
|
||||
locationStore.refresh();
|
||||
locationStore.refreshChildren();
|
||||
locationStore.refreshParents();
|
||||
},
|
||||
"stores"
|
||||
);
|
||||
|
|
|
@ -2,9 +2,13 @@ import { BaseAPI, route } from "../base";
|
|||
import { LocationOutCount, LocationCreate, LocationOut, LocationUpdate } from "../types/data-contracts";
|
||||
import { Results } from "../types/non-generated";
|
||||
|
||||
export type LocationsQuery = {
|
||||
filterChildren: boolean;
|
||||
};
|
||||
|
||||
export class LocationsApi extends BaseAPI {
|
||||
getAll() {
|
||||
return this.http.get<Results<LocationOutCount>>({ url: route("/locations") });
|
||||
getAll(q: LocationsQuery = { filterChildren: false }) {
|
||||
return this.http.get<Results<LocationOutCount>>({ url: route("/locations", q) });
|
||||
}
|
||||
|
||||
create(body: LocationCreate) {
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
const auth = useAuthStore();
|
||||
|
||||
const locationStore = useLocationStore();
|
||||
const locations = computed(() => locationStore.locations);
|
||||
const locations = computed(() => locationStore.parentLocations);
|
||||
|
||||
const labelsStore = useLabelStore();
|
||||
const labels = computed(() => labelsStore.labels);
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
const itemId = computed<string>(() => route.params.id as string);
|
||||
|
||||
const locationStore = useLocationStore();
|
||||
const locations = computed(() => locationStore.locations);
|
||||
const locations = computed(() => locationStore.allLocations);
|
||||
|
||||
const labelStore = useLabelStore();
|
||||
const labels = computed(() => labelStore.labels);
|
||||
|
|
|
@ -82,7 +82,7 @@
|
|||
});
|
||||
|
||||
const locationsStore = useLocationStore();
|
||||
const locations = computed(() => locationsStore.locations);
|
||||
const locations = computed(() => locationsStore.allLocations);
|
||||
|
||||
const labelStore = useLabelStore();
|
||||
const labels = computed(() => labelStore.labels);
|
||||
|
|
|
@ -117,7 +117,7 @@
|
|||
}
|
||||
|
||||
const locationStore = useLocationStore();
|
||||
const locations = computed(() => locationStore.locations);
|
||||
const locations = computed(() => locationStore.allLocations);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const parent = ref<LocationSummary | any>({});
|
||||
|
|
|
@ -3,7 +3,8 @@ import { LocationOutCount } from "~~/lib/api/types/data-contracts";
|
|||
|
||||
export const useLocationStore = defineStore("locations", {
|
||||
state: () => ({
|
||||
allLocations: null as LocationOutCount[] | null,
|
||||
parents: null as LocationOutCount[] | null,
|
||||
Locations: null as LocationOutCount[] | null,
|
||||
client: useUserApi(),
|
||||
}),
|
||||
getters: {
|
||||
|
@ -12,21 +13,36 @@ export const useLocationStore = defineStore("locations", {
|
|||
* synched with the server by intercepting the API calls and updating on the
|
||||
* response
|
||||
*/
|
||||
locations(state): LocationOutCount[] {
|
||||
if (state.allLocations === null) {
|
||||
Promise.resolve(this.refresh());
|
||||
parentLocations(state): LocationOutCount[] {
|
||||
if (state.parents === null) {
|
||||
Promise.resolve(this.refreshParents());
|
||||
}
|
||||
return state.allLocations;
|
||||
return state.parents;
|
||||
},
|
||||
allLocations(state): LocationOutCount[] {
|
||||
if (state.Locations === null) {
|
||||
Promise.resolve(this.refreshChildren());
|
||||
}
|
||||
return state.Locations;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async refresh(): Promise<LocationOutCount[]> {
|
||||
const result = await this.client.locations.getAll();
|
||||
async refreshParents(): Promise<LocationOutCount[]> {
|
||||
const result = await this.client.locations.getAll({ filterChildren: true });
|
||||
if (result.error) {
|
||||
return result;
|
||||
}
|
||||
|
||||
this.allLocations = result.data.items;
|
||||
this.parents = result.data.items;
|
||||
return result;
|
||||
},
|
||||
async refreshChildren(): Promise<LocationOutCount[]> {
|
||||
const result = await this.client.locations.getAll({ filterChildren: false });
|
||||
if (result.error) {
|
||||
return result;
|
||||
}
|
||||
|
||||
this.Locations = result.data.items;
|
||||
return result;
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue