diff --git a/backend/app/api/docs/docs.go b/backend/app/api/docs/docs.go index 5ea7d64..c2a9f17 100644 --- a/backend/app/api/docs/docs.go +++ b/backend/app/api/docs/docs.go @@ -1273,13 +1273,70 @@ const docTemplate = `{ } }, "types.LabelCreate": { - "type": "object" + "type": "object", + "properties": { + "color": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } }, "types.LabelOut": { - "type": "object" + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/types.ItemSummary" + } + }, + "name": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } }, "types.LabelSummary": { - "type": "object" + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } }, "types.LocationCreate": { "type": "object", diff --git a/backend/app/api/docs/swagger.json b/backend/app/api/docs/swagger.json index 5f4ab5a..dbfe062 100644 --- a/backend/app/api/docs/swagger.json +++ b/backend/app/api/docs/swagger.json @@ -1265,13 +1265,70 @@ } }, "types.LabelCreate": { - "type": "object" + "type": "object", + "properties": { + "color": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } }, "types.LabelOut": { - "type": "object" + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/types.ItemSummary" + } + }, + "name": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } }, "types.LabelSummary": { - "type": "object" + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "groupId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } }, "types.LocationCreate": { "type": "object", diff --git a/backend/app/api/docs/swagger.yaml b/backend/app/api/docs/swagger.yaml index c13ac90..71f2970 100644 --- a/backend/app/api/docs/swagger.yaml +++ b/backend/app/api/docs/swagger.yaml @@ -354,10 +354,47 @@ definitions: type: string type: object types.LabelCreate: + properties: + color: + type: string + description: + type: string + name: + type: string type: object types.LabelOut: + properties: + createdAt: + type: string + description: + type: string + groupId: + type: string + id: + type: string + items: + items: + $ref: '#/definitions/types.ItemSummary' + type: array + name: + type: string + updatedAt: + type: string type: object types.LabelSummary: + properties: + createdAt: + type: string + description: + type: string + groupId: + type: string + id: + type: string + name: + type: string + updatedAt: + type: string type: object types.LocationCreate: properties: diff --git a/backend/app/api/v1/v1_ctrl_labels.go b/backend/app/api/v1/v1_ctrl_labels.go index f31976b..19dfebc 100644 --- a/backend/app/api/v1/v1_ctrl_labels.go +++ b/backend/app/api/v1/v1_ctrl_labels.go @@ -2,6 +2,10 @@ package v1 import ( "net/http" + + "github.com/hay-kot/content/backend/internal/services" + "github.com/hay-kot/content/backend/internal/types" + "github.com/hay-kot/content/backend/pkgs/server" ) // HandleLabelsGetAll godoc @@ -13,6 +17,14 @@ import ( // @Security Bearer func (ctrl *V1Controller) HandleLabelsGetAll() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + user := services.UseUserCtx(r.Context()) + labels, err := ctrl.svc.Labels.GetAll(r.Context(), user.GroupID) + if err != nil { + ctrl.log.Error(err, nil) + server.RespondServerError(w) + return + } + server.Respond(w, http.StatusOK, server.Results{Items: labels}) } } @@ -26,6 +38,23 @@ func (ctrl *V1Controller) HandleLabelsGetAll() http.HandlerFunc { // @Security Bearer func (ctrl *V1Controller) HandleLabelsCreate() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + createData := types.LabelCreate{} + if err := server.Decode(r, &createData); err != nil { + ctrl.log.Error(err, nil) + server.RespondError(w, http.StatusInternalServerError, err) + return + } + + user := services.UseUserCtx(r.Context()) + label, err := ctrl.svc.Labels.Create(r.Context(), user.GroupID, createData) + if err != nil { + ctrl.log.Error(err, nil) + server.RespondServerError(w) + return + } + + server.Respond(w, http.StatusCreated, label) + } } @@ -39,6 +68,18 @@ func (ctrl *V1Controller) HandleLabelsCreate() http.HandlerFunc { // @Security Bearer func (ctrl *V1Controller) HandleLabelDelete() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + uid, user, err := ctrl.partialParseIdAndUser(w, r) + if err != nil { + return + } + + err = ctrl.svc.Labels.Delete(r.Context(), user.GroupID, uid) + if err != nil { + ctrl.log.Error(err, nil) + server.RespondServerError(w) + return + } + server.Respond(w, http.StatusNoContent, nil) } } @@ -52,6 +93,18 @@ func (ctrl *V1Controller) HandleLabelDelete() http.HandlerFunc { // @Security Bearer func (ctrl *V1Controller) HandleLabelGet() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + uid, user, err := ctrl.partialParseIdAndUser(w, r) + if err != nil { + return + } + + labels, err := ctrl.svc.Labels.Get(r.Context(), user.GroupID, uid) + if err != nil { + ctrl.log.Error(err, nil) + server.RespondServerError(w) + return + } + server.Respond(w, http.StatusOK, labels) } } @@ -65,5 +118,24 @@ func (ctrl *V1Controller) HandleLabelGet() http.HandlerFunc { // @Security Bearer func (ctrl *V1Controller) HandleLabelUpdate() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + body := types.LabelUpdate{} + if err := server.Decode(r, &body); err != nil { + ctrl.log.Error(err, nil) + server.RespondError(w, http.StatusInternalServerError, err) + return + } + uid, user, err := ctrl.partialParseIdAndUser(w, r) + if err != nil { + return + } + + body.ID = uid + result, err := ctrl.svc.Labels.Update(r.Context(), user.GroupID, body) + if err != nil { + ctrl.log.Error(err, nil) + server.RespondServerError(w) + return + } + server.Respond(w, http.StatusOK, result) } } diff --git a/backend/internal/repo/repo_labels.go b/backend/internal/repo/repo_labels.go new file mode 100644 index 0000000..d54c852 --- /dev/null +++ b/backend/internal/repo/repo_labels.go @@ -0,0 +1,60 @@ +package repo + +import ( + "context" + + "github.com/google/uuid" + "github.com/hay-kot/content/backend/ent" + "github.com/hay-kot/content/backend/ent/group" + "github.com/hay-kot/content/backend/ent/label" + "github.com/hay-kot/content/backend/internal/types" +) + +type EntLabelRepository struct { + db *ent.Client +} + +func (r *EntLabelRepository) Get(ctx context.Context, ID uuid.UUID) (*ent.Label, error) { + return r.db.Label.Query(). + Where(label.ID(ID)). + WithGroup(). + WithItems(). + Only(ctx) +} + +func (r *EntLabelRepository) GetAll(ctx context.Context, groupId uuid.UUID) ([]*ent.Label, error) { + return r.db.Label.Query(). + Where(label.HasGroupWith(group.ID(groupId))). + WithGroup(). + All(ctx) +} + +func (r *EntLabelRepository) Create(ctx context.Context, groupdId uuid.UUID, data types.LabelCreate) (*ent.Label, error) { + label, err := r.db.Label.Create(). + SetName(data.Name). + SetDescription(data.Description). + SetColor(data.Color). + SetGroupID(groupdId). + Save(ctx) + + label.Edges.Group = &ent.Group{ID: groupdId} // bootstrap group ID + return label, err +} + +func (r *EntLabelRepository) Update(ctx context.Context, data types.LabelUpdate) (*ent.Label, error) { + _, err := r.db.Label.UpdateOneID(data.ID). + SetName(data.Name). + SetDescription(data.Description). + SetColor(data.Color). + Save(ctx) + + if err != nil { + return nil, err + } + + return r.Get(ctx, data.ID) +} + +func (r *EntLabelRepository) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.Label.DeleteOneID(id).Exec(ctx) +} diff --git a/backend/internal/repo/repos_all.go b/backend/internal/repo/repos_all.go index 7de4e12..9fe144e 100644 --- a/backend/internal/repo/repos_all.go +++ b/backend/internal/repo/repos_all.go @@ -8,6 +8,7 @@ type AllRepos struct { AuthTokens *EntTokenRepository Groups *EntGroupRepository Locations *EntLocationRepository + Labels *EntLabelRepository } func EntAllRepos(db *ent.Client) *AllRepos { @@ -16,5 +17,6 @@ func EntAllRepos(db *ent.Client) *AllRepos { AuthTokens: &EntTokenRepository{db}, Groups: &EntGroupRepository{db}, Locations: &EntLocationRepository{db}, + Labels: &EntLabelRepository{db}, } } diff --git a/backend/internal/services/all.go b/backend/internal/services/all.go index cd4110a..4d26ae5 100644 --- a/backend/internal/services/all.go +++ b/backend/internal/services/all.go @@ -6,6 +6,7 @@ type AllServices struct { User *UserService Admin *AdminService Location *LocationService + Labels *LabelService } func NewServices(repos *repo.AllRepos) *AllServices { @@ -13,5 +14,6 @@ func NewServices(repos *repo.AllRepos) *AllServices { User: &UserService{repos}, Admin: &AdminService{repos}, Location: &LocationService{repos}, + Labels: &LabelService{repos}, } } diff --git a/backend/internal/services/mappers/labels.go b/backend/internal/services/mappers/labels.go new file mode 100644 index 0000000..a04e862 --- /dev/null +++ b/backend/internal/services/mappers/labels.go @@ -0,0 +1,32 @@ +package mappers + +import ( + "github.com/hay-kot/content/backend/ent" + "github.com/hay-kot/content/backend/internal/types" +) + +func ToLabelSummary(label *ent.Label) *types.LabelSummary { + return &types.LabelSummary{ + ID: label.ID, + GroupID: label.Edges.Group.ID, + Name: label.Name, + Description: label.Description, + CreatedAt: label.CreatedAt, + UpdatedAt: label.UpdatedAt, + } +} + +func ToLabelSummaryErr(label *ent.Label, err error) (*types.LabelSummary, error) { + return ToLabelSummary(label), err +} + +func ToLabelOut(label *ent.Label) *types.LabelOut { + return &types.LabelOut{ + LabelSummary: *ToLabelSummary(label), + Items: MapEach(label.Edges.Items, ToItemSummary), + } +} + +func ToLabelOutErr(label *ent.Label, err error) (*types.LabelOut, error) { + return ToLabelOut(label), err +} diff --git a/backend/internal/services/service_labels.go b/backend/internal/services/service_labels.go new file mode 100644 index 0000000..70bfa30 --- /dev/null +++ b/backend/internal/services/service_labels.go @@ -0,0 +1,63 @@ +package services + +import ( + "context" + + "github.com/google/uuid" + "github.com/hay-kot/content/backend/internal/repo" + "github.com/hay-kot/content/backend/internal/services/mappers" + "github.com/hay-kot/content/backend/internal/types" +) + +type LabelService struct { + repos *repo.AllRepos +} + +func (svc *LabelService) Create(ctx context.Context, groupId uuid.UUID, data types.LabelCreate) (*types.LabelSummary, error) { + label, err := svc.repos.Labels.Create(ctx, groupId, data) + return mappers.ToLabelSummaryErr(label, err) +} + +func (svc *LabelService) Update(ctx context.Context, groupId uuid.UUID, data types.LabelUpdate) (*types.LabelSummary, error) { + label, err := svc.repos.Labels.Update(ctx, data) + return mappers.ToLabelSummaryErr(label, err) +} + +func (svc *LabelService) Delete(ctx context.Context, groupId uuid.UUID, id uuid.UUID) error { + label, err := svc.repos.Labels.Get(ctx, id) + if err != nil { + return err + } + if label.Edges.Group.ID != groupId { + return ErrNotOwner + } + return svc.repos.Labels.Delete(ctx, id) +} + +func (svc *LabelService) Get(ctx context.Context, groupId uuid.UUID, id uuid.UUID) (*types.LabelOut, error) { + label, err := svc.repos.Labels.Get(ctx, id) + + if err != nil { + return nil, err + } + + if label.Edges.Group.ID != groupId { + return nil, ErrNotOwner + } + + return mappers.ToLabelOut(label), nil +} + +func (svc *LabelService) GetAll(ctx context.Context, groupId uuid.UUID) ([]*types.LabelSummary, error) { + labels, err := svc.repos.Labels.GetAll(ctx, groupId) + if err != nil { + return nil, err + } + + labelsOut := make([]*types.LabelSummary, len(labels)) + for i, label := range labels { + labelsOut[i] = mappers.ToLabelSummary(label) + } + + return labelsOut, nil +} diff --git a/backend/internal/types/label_types.go b/backend/internal/types/label_types.go index 029a375..301a3fd 100644 --- a/backend/internal/types/label_types.go +++ b/backend/internal/types/label_types.go @@ -1,7 +1,34 @@ package types -type LabelOut struct{} +import ( + "time" -type LabelCreate struct{} + "github.com/google/uuid" +) -type LabelSummary struct{} +type LabelCreate struct { + Name string `json:"name"` + Description string `json:"description"` + Color string `json:"color"` +} + +type LabelUpdate struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Color string `json:"color"` +} + +type LabelSummary struct { + ID uuid.UUID `json:"id"` + GroupID uuid.UUID `json:"groupId"` + Name string `json:"name"` + Description string `json:"description"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type LabelOut struct { + LabelSummary + Items []*ItemSummary `json:"items"` +} diff --git a/frontend/components/App/Header.vue b/frontend/components/App/Header.vue index b352e5b..49c67bd 100644 --- a/frontend/components/App/Header.vue +++ b/frontend/components/App/Header.vue @@ -25,79 +25,50 @@ }, ]; + const modals = reactive({ + location: false, + label: false, + item: false, + }); + const dropdown = [ { name: 'Location', action: () => { - modal.value = true; + modals.location = true; }, }, { name: 'Item / Asset', - action: () => {}, + action: () => { + modals.item = true; + }, }, { name: 'Label', - action: () => {}, + action: () => { + modals.label = true; + }, }, ]; - - // ---------------------------- - // Location Stuff - // Should move to own component - const locationLoading = ref(false); - const locationForm = reactive({ - name: '', - description: '', - }); - - const locationNameRef = ref(null); - const triggerFocus = ref(false); - const modal = ref(false); - - whenever( - () => modal.value, - () => { - triggerFocus.value = true; - } - ); - - async function createLocation() { - locationLoading.value = true; - const { data } = await api.locations.create(locationForm); - - if (data) { - navigateTo(`/location/${data.id}`); - } - - locationLoading.value = false; - modal.value = false; - locationForm.name = ''; - locationForm.description = ''; - triggerFocus.value = false; - }