diff --git a/client/web/compose/src/views/Admin/Pages/Edit.vue b/client/web/compose/src/views/Admin/Pages/Edit.vue index b53f7ba0ac..b73f1f3ef5 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 + + @@ -1246,6 +1257,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 }