3
0

Add an option to soft delete page icon

This commit is contained in:
Mumbi Francis 2023-10-12 17:30:36 +03:00 committed by Katrin Yordanova
parent 305b334d3e
commit 82636aefaf
10 changed files with 208 additions and 7 deletions

View File

@ -733,14 +733,39 @@
<b-modal
v-model="showIconModal"
:title="$t('icon.configure')"
:ok-title="$t('label.saveAndClose')"
size="lg"
label-class="text-primary"
cancel-variant="link"
footer-class="d-flex align-items-center"
no-fade
@close="closeIconModal"
@ok="saveIconModal"
>
<template #modal-footer>
<c-input-confirm
v-if="attachments && selectedAttachmentID"
:disabled="(attachments && !selectedAttachmentID) || processingIcon"
size="md"
variant="danger"
@confirmed="deleteIcon"
>
{{ $t('icon.delete') }}
</c-input-confirm>
<div class="ml-auto">
<b-button
variant="link"
class="text-primary"
@click="closeIconModal"
>
{{ $t('general:label.cancel') }}
</b-button>
<b-button
variant="primary"
class="ml-2"
@click="saveIconModal"
>
{{ $t('general:label.saveAndClose') }}
</b-button>
</div>
</template>
<b-form-group
:label="$t('icon.upload')"
label-class="text-primary"
@ -787,7 +812,7 @@
label-class="text-primary"
>
<div
v-if="processing"
v-if="processingIcon"
class="d-flex align-items-center justify-content-center h-100"
>
<b-spinner />
@ -915,6 +940,7 @@ export default {
data () {
return {
processing: false,
processingIcon: false,
page: new compose.Page(),
initialPageState: new compose.Page(),
@ -1243,12 +1269,16 @@ export default {
},
async fetchAttachments () {
this.processingIcon = true
return this.$ComposeAPI.iconList({ sort: 'id DESC' })
.then(({ set: attachments = [] }) => {
const baseURL = this.$ComposeAPI.baseURL
this.attachments = []
if (attachments) {
if (attachments.length === 0) {
this.icon = {}
} else {
attachments.forEach(a => {
const src = !a.url.includes(baseURL) ? this.makeAttachmentUrl(a.url) : a.url
this.attachments.push({ ...a, src })
@ -1256,6 +1286,9 @@ export default {
}
})
.catch(this.toastErrorHandler(this.$t('notification:page.iconFetchFailed')))
.finally(() => {
this.processingIcon = false
})
},
addLayoutAction () {
@ -1281,7 +1314,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 +1327,39 @@ export default {
}
this.icon = { type, src }
if (type === 'link' && !src) {
this.icon = {}
}
this.showIconModal = false
},
deleteIcon () {
this.processingIcon = true
return this.$ComposeAPI.iconDelete({ iconID: this.selectedAttachmentID }).then(() => {
return this.fetchAttachments().then(() => {
this.setCurrentIcon()
this.toastSuccess(this.$t('notification:page.iconDeleteSuccess'))
})
}).finally(() => {
this.processingIcon = false
}).catch(this.toastErrorHandler(this.$t('notification:page.iconDeleteFailed')))
},
closeIconModal () {
this.linkUrl = this.icon.type === 'link' ? this.icon.src : ''
this.setCurrentIcon()
this.showIconModal = false
},
setCurrentIcon () {
this.selectedAttachmentID = (this.attachments.find(a => a.url === this.icon.src) || {}).attachmentID
if (!this.selectedAttachmentID) {
this.icon = {}
}
},
makeAttachmentUrl (src) {

View File

@ -1337,6 +1337,44 @@ export default class Compose {
return '/icon/'
}
// Delete icon
async iconDelete (a: KV, extra: AxiosRequestConfig = {}): Promise<KV> {
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<KV>; 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<KV> {
const {

View File

@ -98,6 +98,8 @@ page:
cloneFailed: Could not clone this page
deleteFailed: Could not delete this page
iconFetchFailed: Could not fetch list of icons
iconDeleteFailed: Could not delete selected icon
iconDeleteSuccess: Icon deleted
loadFailed: Could not load the page tree
noPages: No pages found
saveFailed: Could not save this page

View File

@ -79,6 +79,7 @@ icon:
page: Page icon
upload: Upload icon
list: Uploaded icons
delete: Delete selected icon
url:
label: Or add URL to icon
import: 'Import page(s):'

View File

@ -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

View File

@ -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)
})
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
//

View File

@ -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
}