diff --git a/client/web/compose/src/views/Admin/Pages/Edit.vue b/client/web/compose/src/views/Admin/Pages/Edit.vue
index b53f7ba0ac..0a6f505e50 100644
--- a/client/web/compose/src/views/Admin/Pages/Edit.vue
+++ b/client/web/compose/src/views/Admin/Pages/Edit.vue
@@ -785,17 +785,28 @@
v-else
class="d-flex flex-wrap"
>
-
+
+
+ X
+
+
@@ -1236,6 +1247,11 @@ export default {
const baseURL = this.$ComposeAPI.baseURL
this.attachments = []
+ // If there are no attachments, clear the icon from page config
+ if (attachments.length === 0) {
+ this.page.config.navItem.icon = {}
+ }
+
if (attachments) {
attachments.forEach(a => {
const src = !a.url.includes(baseURL) ? this.makeAttachmentUrl(a.url) : a.url
@@ -1246,6 +1262,13 @@ export default {
.catch(this.toastErrorHandler(this.$t('notification:page.iconFetchFailed')))
},
+ async deleteAttachment (attachmentID) {
+ return this.$ComposeAPI.iconDelete({ iconID: attachmentID })
+ .then(() => {
+ this.fetchAttachments()
+ })
+ },
+
addLayoutAction () {
this.layoutEditor.layout.addAction()
},
diff --git a/lib/js/src/api-clients/compose.ts b/lib/js/src/api-clients/compose.ts
index 15b6547d66..30377764cf 100644
--- a/lib/js/src/api-clients/compose.ts
+++ b/lib/js/src/api-clients/compose.ts
@@ -1337,6 +1337,44 @@ export default class Compose {
return '/icon/'
}
+ // Delete icon
+ async iconDelete (a: KV, extra: AxiosRequestConfig = {}): Promise {
+ const {
+ iconID,
+ } = (a as KV) || {}
+ if (!iconID) {
+ throw Error('field iconID is empty')
+ }
+ const cfg: AxiosRequestConfig = {
+ ...extra,
+ method: 'delete',
+ url: this.iconDeleteEndpoint({
+ iconID,
+ }),
+ }
+
+ return this.api().request(cfg).then(result => stdResolve(result))
+ }
+
+ iconDeleteCancellable (a: KV, extra: AxiosRequestConfig = {}): { response: (a: KV, extra?: AxiosRequestConfig) => Promise; cancel: () => void; } {
+ const cancelTokenSource = axios.CancelToken.source();
+ let options = {...extra, cancelToken: cancelTokenSource.token }
+
+ return {
+ response: () => this.iconDelete(a, options),
+ cancel: () => {
+ cancelTokenSource.cancel();
+ }
+ }
+ }
+
+ iconDeleteEndpoint (a: KV): string {
+ const {
+ iconID,
+ } = a || {}
+ return `/icon/${iconID}`
+ }
+
// List available page layouts
async pageLayoutListNamespace (a: KV, extra: AxiosRequestConfig = {}): Promise {
const {
diff --git a/server/compose/rest.yaml b/server/compose/rest.yaml
index 1247d6ac6d..d9811092a6 100644
--- a/server/compose/rest.yaml
+++ b/server/compose/rest.yaml
@@ -562,6 +562,16 @@ endpoints:
- name: icon
type: "*multipart.FileHeader"
title: Icon to upload
+ - name: delete
+ path: "/{iconID}"
+ method: DELETE
+ title: Delete icon
+ parameters:
+ path:
+ - type: uint64
+ name: iconID
+ required: true
+ title: Icon ID
- title: Page Layouts
description: Compose page layouts
diff --git a/server/compose/rest/handlers/icon.go b/server/compose/rest/handlers/icon.go
index a7c054907b..a32784f609 100644
--- a/server/compose/rest/handlers/icon.go
+++ b/server/compose/rest/handlers/icon.go
@@ -21,12 +21,14 @@ type (
IconAPI interface {
List(context.Context, *request.IconList) (interface{}, error)
Upload(context.Context, *request.IconUpload) (interface{}, error)
+ Delete(context.Context, *request.IconDelete) (interface{}, error)
}
// HTTP API interface
Icon struct {
List func(http.ResponseWriter, *http.Request)
Upload func(http.ResponseWriter, *http.Request)
+ Delete func(http.ResponseWriter, *http.Request)
}
)
@@ -62,6 +64,22 @@ func NewIcon(h IconAPI) *Icon {
return
}
+ api.Send(w, r, value)
+ },
+ Delete: func(w http.ResponseWriter, r *http.Request) {
+ defer r.Body.Close()
+ params := request.NewIconDelete()
+ if err := params.Fill(r); err != nil {
+ api.Send(w, r, err)
+ return
+ }
+
+ value, err := h.Delete(r.Context(), params)
+ if err != nil {
+ api.Send(w, r, err)
+ return
+ }
+
api.Send(w, r, value)
},
}
@@ -72,5 +90,6 @@ func (h Icon) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.H
r.Use(middlewares...)
r.Get("/icon/", h.List)
r.Post("/icon/", h.Upload)
+ r.Delete("/icon/{iconID}", h.Delete)
})
}
diff --git a/server/compose/rest/icon.go b/server/compose/rest/icon.go
index 6ab52ebf72..96c450ea32 100644
--- a/server/compose/rest/icon.go
+++ b/server/compose/rest/icon.go
@@ -2,6 +2,9 @@ package rest
import (
"context"
+ "github.com/cortezaproject/corteza/server/pkg/api"
+ "github.com/cortezaproject/corteza/server/pkg/auth"
+ "github.com/cortezaproject/corteza/server/pkg/errors"
"mime/multipart"
"github.com/cortezaproject/corteza/server/compose/rest/request"
@@ -56,6 +59,9 @@ func (ctrl *Icon) List(ctx context.Context, r *request.IconList) (interface{}, e
return nil, err
}
+ //Get only the undeleted icons
+ f.Deleted = filter.StateExcluded
+
set, f, err = ctrl.attachment.Find(ctx, f)
return ctrl.makeIconFilterPayload(ctx, set, f, err)
}
@@ -105,3 +111,16 @@ func (ctrl *Icon) makeIconFilterPayload(ctx context.Context, nn types.Attachment
return res, nil
}
+
+func (ctrl *Icon) Delete(ctx context.Context, r *request.IconDelete) (interface{}, error) {
+ if !auth.GetIdentityFromContext(ctx).Valid() {
+ return nil, errors.Unauthorized("cannot delete icon")
+ }
+
+ _, err := ctrl.attachment.FindByID(ctx, 0, r.IconID)
+ if err != nil {
+ return nil, err
+ }
+
+ return api.OK(), ctrl.attachment.DeleteByID(ctx, 0, r.IconID)
+}
diff --git a/server/compose/rest/request/icon.go b/server/compose/rest/request/icon.go
index a90d70f7d7..148b76713a 100644
--- a/server/compose/rest/request/icon.go
+++ b/server/compose/rest/request/icon.go
@@ -61,6 +61,13 @@ type (
// Icon to upload
Icon *multipart.FileHeader
}
+
+ IconDelete struct {
+ // IconID PATH parameter
+ //
+ // Icon ID
+ IconID uint64 `json:",string"`
+ }
)
// NewIconList request
@@ -191,3 +198,38 @@ func (r *IconUpload) Fill(req *http.Request) (err error) {
return err
}
+
+// NewIconDelete request
+func NewIconDelete() *IconDelete {
+ return &IconDelete{}
+}
+
+// Auditable returns all auditable/loggable parameters
+func (r IconDelete) Auditable() map[string]interface{} {
+ return map[string]interface{}{
+ "iconID": r.IconID,
+ }
+}
+
+// Auditable returns all auditable/loggable parameters
+func (r IconDelete) GetIconID() uint64 {
+ return r.IconID
+}
+
+// Fill processes request and fills internal variables
+func (r *IconDelete) Fill(req *http.Request) (err error) {
+
+ {
+ var val string
+ // path params
+
+ val = chi.URLParam(req, "iconID")
+ r.IconID, err = payload.ParseUint64(val), nil
+ if err != nil {
+ return err
+ }
+
+ }
+
+ return err
+}
diff --git a/server/compose/types/attachment.go b/server/compose/types/attachment.go
index 0af63f1426..6203cb7577 100644
--- a/server/compose/types/attachment.go
+++ b/server/compose/types/attachment.go
@@ -36,6 +36,8 @@ type (
FieldName string `json:"fieldName,omitempty"`
Filter string `json:"filter"`
+ Deleted filter.State `json:"deleted"`
+
// Check fn is called by store backend for each resource found function can
// modify the resource and return false if store should not return it
//
diff --git a/server/store/adapters/rdbms/filter.go b/server/store/adapters/rdbms/filter.go
index 3d845343e6..ea8b1572c6 100644
--- a/server/store/adapters/rdbms/filter.go
+++ b/server/store/adapters/rdbms/filter.go
@@ -120,6 +120,13 @@ func DefaultFilters() (f *extendedFilters) {
return
}
+ // Add a filter expression for deleted attachments
+ if f.Deleted == filter.StateExcluded {
+ ee = append(ee, goqu.C("deleted_at").IsNull())
+ } else if f.Deleted == filter.StateExclusive {
+ ee = append(ee, goqu.C("deleted_at").IsNotNull())
+ }
+
return ee, f, nil
}