diff --git a/client/web/compose/src/themes/corteza-base/custom.scss b/client/web/compose/src/themes/corteza-base/custom.scss index 075703862b..6c13055b70 100644 --- a/client/web/compose/src/themes/corteza-base/custom.scss +++ b/client/web/compose/src/themes/corteza-base/custom.scss @@ -78,7 +78,7 @@ th { -ms-flex: 1 1 auto; flex: 1 1 auto; margin-bottom: 0; - font-size: .9rem; + font-size: 0.9rem; border-radius: 0.25rem; .vs__selected-options { diff --git a/client/web/compose/src/views/Admin/Pages/Edit.vue b/client/web/compose/src/views/Admin/Pages/Edit.vue index 82c8a2b56d..92798a7a87 100644 --- a/client/web/compose/src/views/Admin/Pages/Edit.vue +++ b/client/web/compose/src/views/Admin/Pages/Edit.vue @@ -733,14 +733,35 @@ + { const src = !a.url.includes(baseURL) ? this.makeAttachmentUrl(a.url) : a.url this.attachments.push({ ...a, src }) @@ -1281,7 +1304,7 @@ export default { openIconModal () { this.linkUrl = this.icon.type === 'link' ? this.icon.src : '' - this.selectedAttachmentID = (this.attachments.find(a => a.url === this.icon.src) || {}).attachmentID + this.setCurrentIcon() this.showIconModal = true }, @@ -1294,11 +1317,18 @@ export default { } this.icon = { type, src } + + if (type === 'link' && !src) { + this.icon = {} + } + + this.showIconModal = false }, closeIconModal () { this.linkUrl = this.icon.type === 'link' ? this.icon.src : '' - this.selectedAttachmentID = (this.attachments.find(a => a.url === this.icon.src) || {}).attachmentID + this.setCurrentIcon() + this.showIconModal = false }, makeAttachmentUrl (src) { @@ -1341,6 +1371,20 @@ export default { cancel() }) }, + + deleteIcon () { + return this.$ComposeAPI.iconDelete({ iconID: this.selectedAttachmentID }) + .then(() => { + this.fetchAttachments().then(() => { + this.setCurrentIcon() + }) + }) + .catch(this.toastErrorHandler(this.$t('notification:page.iconUndeleteFailed'))) + }, + + setCurrentIcon () { + this.selectedAttachmentID = (this.attachments.find(a => a.url === this.icon.src) || {}).attachmentID + }, }, } 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/locale/en/corteza-webapp-compose/notification.yaml b/locale/en/corteza-webapp-compose/notification.yaml index 6fe5723aea..29ed074675 100644 --- a/locale/en/corteza-webapp-compose/notification.yaml +++ b/locale/en/corteza-webapp-compose/notification.yaml @@ -86,6 +86,7 @@ page: cloneFailed: Could not clone this page deleteFailed: Could not delete this page iconFetchFailed: Could not fetch list of icons + iconUndeleteFailed: Could not delete selected icon loadFailed: Could not load the page tree noPages: No pages found saveFailed: Could not save this page diff --git a/locale/en/corteza-webapp-compose/page.yaml b/locale/en/corteza-webapp-compose/page.yaml index b4cb53e672..9cc53271f6 100644 --- a/locale/en/corteza-webapp-compose/page.yaml +++ b/locale/en/corteza-webapp-compose/page.yaml @@ -79,6 +79,8 @@ icon: page: Page icon upload: Upload icon list: Uploaded icons + tooltip: + delete: Delete selected icon url: label: Or add URL to icon import: 'Import page(s):' 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 }