Add ability to select and perform bulk operations on all records
This change affects the recordlist and the bulk operations are edit,delete and undelete
This commit is contained in:
committed by
Mumbi Francis
parent
0d47243e5d
commit
89f621c035
@@ -72,6 +72,7 @@
|
||||
:query="query"
|
||||
:prefilter="prefilter"
|
||||
:selection="selected"
|
||||
:selected-all-records-count="selectedAllRecords ? pagination.count : 0"
|
||||
:processing="processing"
|
||||
:preselected-fields="fields.map(({ moduleField }) => moduleField)"
|
||||
class="mr-1 float-left"
|
||||
@@ -164,37 +165,37 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="d-none flex-wrap align-items-center mt-1"
|
||||
:class="{ 'd-flex': options.selectable && selected.length }"
|
||||
v-if="options.selectable && selected.length"
|
||||
class="d-flex align-items-center flex-wrap align-items-center mt-2"
|
||||
>
|
||||
<div
|
||||
class="d-flex align-items-baseline my-auto pt-1 text-nowrap h-100"
|
||||
>
|
||||
{{ $t('recordList.selected', { count: selected.length, total: items.length }) }}
|
||||
<b-button
|
||||
variant="link"
|
||||
class="p-0 text-decoration-none"
|
||||
@click.prevent="handleSelectAllOnPage({ isChecked: false })"
|
||||
>
|
||||
({{ $t('recordList.cancelSelection') }})
|
||||
</b-button>
|
||||
<div class="mr-1">
|
||||
{{ selectedRecordsDisplayText }}
|
||||
</div>
|
||||
|
||||
<b-button
|
||||
size="sm"
|
||||
variant="outline-light"
|
||||
class="text-primary border-0"
|
||||
@click="selectAllRecords()"
|
||||
>
|
||||
{{ selectedAllRecords ? $t('recordList.unselectAllRecords') : $t('recordList.selectAllRecords') }}
|
||||
</b-button>
|
||||
|
||||
<div class="d-flex align-items-center ml-auto">
|
||||
<automation-buttons
|
||||
class="d-inline m-0 mr-2"
|
||||
:buttons="options.selectionButtons"
|
||||
:module="recordListModule"
|
||||
:extra-event-args="{ selected, filter }"
|
||||
:extra-event-args="{ selected, filter}"
|
||||
v-bind="$props"
|
||||
@refresh="refresh()"
|
||||
/>
|
||||
|
||||
<bulk-edit-modal
|
||||
v-show="options.bulkRecordEditEnabled && canUpdateSelectedRecords"
|
||||
v-show="options.bulkRecordEditEnabled && canUpdateSelectedRecords && !showingDeletedRecords"
|
||||
:module="recordListModule"
|
||||
:namespace="namespace"
|
||||
:selected-records="selected"
|
||||
:selected-records="selectedAllRecords ? [] : selected"
|
||||
@save="onBulkUpdate()"
|
||||
/>
|
||||
|
||||
@@ -433,7 +434,7 @@
|
||||
:extra-options="options"
|
||||
/>
|
||||
<div
|
||||
v-if="options.inlineRecordEditEnabled && field.canEdit"
|
||||
v-if="options.inlineRecordEditEnabled && field.canEdit && !showingDeletedRecords"
|
||||
class="inline-actions"
|
||||
>
|
||||
<b-button
|
||||
@@ -844,6 +845,7 @@ export default {
|
||||
customPresetFilters: [],
|
||||
currentCustomPresetFilter: undefined,
|
||||
showCustomPresetFilterModal: false,
|
||||
selectedAllRecords: false,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1020,6 +1022,14 @@ export default {
|
||||
authUserRoles () {
|
||||
return this.$auth.user.roles
|
||||
},
|
||||
|
||||
selectedRecordsDisplayText () {
|
||||
const count = this.selectedAllRecords ? (this.options.showTotalCount ? this.pagination.count : undefined) : this.selected.length
|
||||
const total = this.items.length
|
||||
const key = this.selectedAllRecords ? 'selectedFromAllPages' : 'selected'
|
||||
|
||||
return this.$t(`recordList.${key}`, { count, total })
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
@@ -1150,6 +1160,8 @@ export default {
|
||||
return
|
||||
}
|
||||
this.selected.splice(i, 1)
|
||||
|
||||
this.selectedAllRecords = false
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1174,6 +1186,7 @@ export default {
|
||||
|
||||
handleShowDeleted () {
|
||||
this.showingDeletedRecords = !this.showingDeletedRecords
|
||||
this.selectedAllRecords = false
|
||||
this.refresh(true)
|
||||
},
|
||||
|
||||
@@ -1381,7 +1394,7 @@ export default {
|
||||
query: {
|
||||
fields: e.fields,
|
||||
// url.Make already URL encodes the the values, so the filter shouldn't be encoded
|
||||
filter: filter,
|
||||
filter: this.selectedAllRecords ? '' : filter,
|
||||
jwt: this.$auth.accessToken,
|
||||
timezone: timezone ? timezone.tzCode : undefined,
|
||||
},
|
||||
@@ -1468,9 +1481,15 @@ export default {
|
||||
this.selected = this.items.map(({ id }) => id)
|
||||
} else {
|
||||
this.selected = []
|
||||
this.selectedAllRecords = isChecked
|
||||
}
|
||||
},
|
||||
|
||||
selectAllRecords () {
|
||||
this.selectedAllRecords = !this.selectedAllRecords
|
||||
this.handleSelectAllOnPage({ isChecked: this.selectedAllRecords })
|
||||
},
|
||||
|
||||
handleRestoreSelectedRecords () {
|
||||
if (this.inlineEditing) {
|
||||
const sel = new Set(this.selected)
|
||||
@@ -1482,15 +1501,19 @@ export default {
|
||||
} else {
|
||||
const { moduleID, namespaceID } = this.items[0].r
|
||||
|
||||
// filter undeletable records from the selected list
|
||||
const recordIDs = this.items
|
||||
.filter(({ id, r }) => r.canUndeleteRecord && this.selected.includes(id))
|
||||
.map(({ id }) => id)
|
||||
let query = ''
|
||||
|
||||
this.processing = true
|
||||
if (!this.selectedAllRecords) {
|
||||
// filter deletable records from the selected list
|
||||
const recordIDs = this.items
|
||||
.filter(({ id, r }) => r.canUndeleteRecord && this.selected.includes(id))
|
||||
.map(({ id }) => id)
|
||||
|
||||
query = recordIDs.map(r => `recordID='${r}'`).join(' OR ')
|
||||
}
|
||||
|
||||
this.$ComposeAPI
|
||||
.recordBulkUndelete({ moduleID, namespaceID, recordIDs })
|
||||
.recordBulkUndelete({ moduleID, namespaceID, query })
|
||||
.then(() => {
|
||||
this.refresh(true)
|
||||
this.toastSuccess(this.$t('notification:record.undeleteBulkSuccess'))
|
||||
@@ -1498,6 +1521,7 @@ export default {
|
||||
.catch(this.toastErrorHandler(this.$t('notification:record.undeleteBulkFailed')))
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
this.selectedAllRecords = false
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -1521,15 +1545,21 @@ export default {
|
||||
// same module so this should be safe to do.
|
||||
const { moduleID, namespaceID } = this.items[0].r
|
||||
|
||||
// filter deletable records from the selected list
|
||||
const recordIDs = this.items
|
||||
.filter(({ id, r }) => r.canDeleteRecord && selected.includes(id))
|
||||
.map(({ id }) => id)
|
||||
let query = ''
|
||||
|
||||
if (!this.selectedAllRecords) {
|
||||
// filter deletable records from the selected list
|
||||
const recordIDs = this.items
|
||||
.filter(({ id, r }) => r.canDeleteRecord && selected.includes(id))
|
||||
.map(({ id }) => id)
|
||||
|
||||
query = recordIDs.map(r => `recordID='${r}'`).join(' OR ')
|
||||
}
|
||||
|
||||
this.processing = true
|
||||
|
||||
this.$ComposeAPI
|
||||
.recordBulkDelete({ moduleID, namespaceID, recordIDs })
|
||||
.recordBulkDelete({ moduleID, namespaceID, query })
|
||||
.then(() => this.refresh(true))
|
||||
.then(() => {
|
||||
this.toastSuccess(this.$t('notification:record.deleteBulkSuccess'))
|
||||
@@ -1537,6 +1567,7 @@ export default {
|
||||
.catch(this.toastErrorHandler(this.$t('notification:record.deleteBulkFailed')))
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
this.selectedAllRecords = false
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -1762,6 +1793,7 @@ export default {
|
||||
},
|
||||
|
||||
onBulkUpdate () {
|
||||
this.selectedAllRecords = false
|
||||
this.refresh(true)
|
||||
},
|
||||
|
||||
|
||||
@@ -232,6 +232,11 @@ export default {
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
selectedAllRecordsCount: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
filterRangeType: {
|
||||
type: String,
|
||||
default: 'all',
|
||||
@@ -375,6 +380,10 @@ export default {
|
||||
getExportableCount () {
|
||||
// when exporting selection, only selected records are applicable
|
||||
if (this.rangeType === 'selection') {
|
||||
if (this.selectedAllRecordsCount !== 0) {
|
||||
return this.selectedAllRecordsCount
|
||||
}
|
||||
|
||||
return this.selection.length
|
||||
}
|
||||
|
||||
|
||||
@@ -281,8 +281,10 @@ export default {
|
||||
|
||||
const { moduleID, namespaceID } = this.module
|
||||
|
||||
const query = records.map(r => `recordID='${r}'`).join(' OR ')
|
||||
|
||||
return this
|
||||
.$ComposeAPI.recordPatch({ moduleID, namespaceID, records, values })
|
||||
.$ComposeAPI.recordPatch({ moduleID, namespaceID, values, query })
|
||||
.catch(err => {
|
||||
const { details = undefined } = err
|
||||
if (!!details && Array.isArray(details) && details.length > 0) {
|
||||
|
||||
@@ -2185,8 +2185,8 @@ export default class Compose {
|
||||
const {
|
||||
namespaceID,
|
||||
moduleID,
|
||||
records,
|
||||
values,
|
||||
query,
|
||||
} = (a as KV) || {}
|
||||
if (!namespaceID) {
|
||||
throw Error('field namespaceID is empty')
|
||||
@@ -2202,8 +2202,8 @@ export default class Compose {
|
||||
}),
|
||||
}
|
||||
cfg.data = {
|
||||
records,
|
||||
values,
|
||||
query,
|
||||
}
|
||||
return this.api().request(cfg).then(result => stdResolve(result))
|
||||
}
|
||||
@@ -2221,8 +2221,8 @@ export default class Compose {
|
||||
const {
|
||||
namespaceID,
|
||||
moduleID,
|
||||
recordIDs,
|
||||
truncate,
|
||||
query,
|
||||
} = (a as KV) || {}
|
||||
if (!namespaceID) {
|
||||
throw Error('field namespaceID is empty')
|
||||
@@ -2238,8 +2238,8 @@ export default class Compose {
|
||||
}),
|
||||
}
|
||||
cfg.data = {
|
||||
recordIDs,
|
||||
truncate,
|
||||
query,
|
||||
}
|
||||
return this.api().request(cfg).then(result => stdResolve(result))
|
||||
}
|
||||
@@ -2329,7 +2329,7 @@ export default class Compose {
|
||||
const {
|
||||
namespaceID,
|
||||
moduleID,
|
||||
recordIDs,
|
||||
query,
|
||||
} = (a as KV) || {}
|
||||
if (!namespaceID) {
|
||||
throw Error('field namespaceID is empty')
|
||||
@@ -2345,7 +2345,7 @@ export default class Compose {
|
||||
}),
|
||||
}
|
||||
cfg.data = {
|
||||
recordIDs,
|
||||
query,
|
||||
}
|
||||
return this.api().request(cfg).then(result => stdResolve(result))
|
||||
}
|
||||
|
||||
@@ -484,6 +484,9 @@ recordList:
|
||||
label: Parent field
|
||||
selectable: Users will be able to select records
|
||||
selected: '{{count}} of {{total}} records selected'
|
||||
selectedFromAllPages: 'All {{count}} records from all pages selected'
|
||||
selectAllRecords: Select all records on all pages
|
||||
unselectAllRecords: Clear selection
|
||||
sort:
|
||||
tooltip: Sort column
|
||||
showRecords:
|
||||
|
||||
+44
-33
@@ -81,9 +81,10 @@ func (a recordsLookupArgs) GetRecord() (bool, uint64, *types.Record) {
|
||||
// Lookup function Compose record lookup
|
||||
//
|
||||
// expects implementation of lookup function:
|
||||
// func (h recordsHandler) lookup(ctx context.Context, args *recordsLookupArgs) (results *recordsLookupResults, err error) {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// func (h recordsHandler) lookup(ctx context.Context, args *recordsLookupArgs) (results *recordsLookupResults, err error) {
|
||||
// return
|
||||
// }
|
||||
func (h recordsHandler) Lookup() *atypes.Function {
|
||||
return &atypes.Function{
|
||||
Ref: "composeRecordsLookup",
|
||||
@@ -255,9 +256,10 @@ func (a recordsSearchArgs) GetNamespace() (bool, uint64, string, *types.Namespac
|
||||
// Search function Compose records search
|
||||
//
|
||||
// expects implementation of search function:
|
||||
// func (h recordsHandler) search(ctx context.Context, args *recordsSearchArgs) (results *recordsSearchResults, err error) {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// func (h recordsHandler) search(ctx context.Context, args *recordsSearchArgs) (results *recordsSearchResults, err error) {
|
||||
// return
|
||||
// }
|
||||
func (h recordsHandler) Search() *atypes.Function {
|
||||
return &atypes.Function{
|
||||
Ref: "composeRecordsSearch",
|
||||
@@ -508,9 +510,10 @@ func (a recordsFirstArgs) GetNamespace() (bool, uint64, string, *types.Namespace
|
||||
// First function Compose record lookup (first created)
|
||||
//
|
||||
// expects implementation of first function:
|
||||
// func (h recordsHandler) first(ctx context.Context, args *recordsFirstArgs) (results *recordsFirstResults, err error) {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// func (h recordsHandler) first(ctx context.Context, args *recordsFirstArgs) (results *recordsFirstResults, err error) {
|
||||
// return
|
||||
// }
|
||||
func (h recordsHandler) First() *atypes.Function {
|
||||
return &atypes.Function{
|
||||
Ref: "composeRecordsFirst",
|
||||
@@ -637,9 +640,10 @@ func (a recordsLastArgs) GetNamespace() (bool, uint64, string, *types.Namespace)
|
||||
// Last function Compose record lookup (last created)
|
||||
//
|
||||
// expects implementation of last function:
|
||||
// func (h recordsHandler) last(ctx context.Context, args *recordsLastArgs) (results *recordsLastResults, err error) {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// func (h recordsHandler) last(ctx context.Context, args *recordsLastArgs) (results *recordsLastResults, err error) {
|
||||
// return
|
||||
// }
|
||||
func (h recordsHandler) Last() *atypes.Function {
|
||||
return &atypes.Function{
|
||||
Ref: "composeRecordsLast",
|
||||
@@ -792,9 +796,10 @@ func (a recordsEachArgs) GetNamespace() (bool, uint64, string, *types.Namespace)
|
||||
// Each function Compose records
|
||||
//
|
||||
// expects implementation of each function:
|
||||
// func (h recordsHandler) each(ctx context.Context, args *recordsEachArgs) (results *recordsEachResults, err error) {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// func (h recordsHandler) each(ctx context.Context, args *recordsEachArgs) (results *recordsEachResults, err error) {
|
||||
// return
|
||||
// }
|
||||
func (h recordsHandler) Each() *atypes.Function {
|
||||
return &atypes.Function{
|
||||
Ref: "composeRecordsEach",
|
||||
@@ -959,9 +964,10 @@ func (a recordsNewArgs) GetNamespace() (bool, uint64, string, *types.Namespace)
|
||||
// New function Compose record maker
|
||||
//
|
||||
// expects implementation of new function:
|
||||
// func (h recordsHandler) new(ctx context.Context, args *recordsNewArgs) (results *recordsNewResults, err error) {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// func (h recordsHandler) new(ctx context.Context, args *recordsNewArgs) (results *recordsNewResults, err error) {
|
||||
// return
|
||||
// }
|
||||
func (h recordsHandler) New() *atypes.Function {
|
||||
return &atypes.Function{
|
||||
Ref: "composeRecordsNew",
|
||||
@@ -1072,9 +1078,10 @@ type (
|
||||
// Validate function Compose record validator
|
||||
//
|
||||
// expects implementation of validate function:
|
||||
// func (h recordsHandler) validate(ctx context.Context, args *recordsValidateArgs) (results *recordsValidateResults, err error) {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// func (h recordsHandler) validate(ctx context.Context, args *recordsValidateArgs) (results *recordsValidateResults, err error) {
|
||||
// return
|
||||
// }
|
||||
func (h recordsHandler) Validate() *atypes.Function {
|
||||
return &atypes.Function{
|
||||
Ref: "composeRecordsValidate",
|
||||
@@ -1152,9 +1159,10 @@ type (
|
||||
// Create function Compose record create
|
||||
//
|
||||
// expects implementation of create function:
|
||||
// func (h recordsHandler) create(ctx context.Context, args *recordsCreateArgs) (results *recordsCreateResults, err error) {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// func (h recordsHandler) create(ctx context.Context, args *recordsCreateArgs) (results *recordsCreateResults, err error) {
|
||||
// return
|
||||
// }
|
||||
func (h recordsHandler) Create() *atypes.Function {
|
||||
return &atypes.Function{
|
||||
Ref: "composeRecordsCreate",
|
||||
@@ -1229,9 +1237,10 @@ type (
|
||||
// Update function Compose record update
|
||||
//
|
||||
// expects implementation of update function:
|
||||
// func (h recordsHandler) update(ctx context.Context, args *recordsUpdateArgs) (results *recordsUpdateResults, err error) {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// func (h recordsHandler) update(ctx context.Context, args *recordsUpdateArgs) (results *recordsUpdateResults, err error) {
|
||||
// return
|
||||
// }
|
||||
func (h recordsHandler) Update() *atypes.Function {
|
||||
return &atypes.Function{
|
||||
Ref: "composeRecordsUpdate",
|
||||
@@ -1328,9 +1337,10 @@ func (a recordsDeleteArgs) GetRecord() (bool, uint64, *types.Record) {
|
||||
// Delete function Compose record delete
|
||||
//
|
||||
// expects implementation of delete function:
|
||||
// func (h recordsHandler) delete(ctx context.Context, args *recordsDeleteArgs) (err error) {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// func (h recordsHandler) delete(ctx context.Context, args *recordsDeleteArgs) (err error) {
|
||||
// return
|
||||
// }
|
||||
func (h recordsHandler) Delete() *atypes.Function {
|
||||
return &atypes.Function{
|
||||
Ref: "composeRecordsDelete",
|
||||
@@ -1454,9 +1464,10 @@ func (a recordsReportArgs) GetNamespace() (bool, uint64, string, *types.Namespac
|
||||
// Report function Report
|
||||
//
|
||||
// expects implementation of report function:
|
||||
// func (h recordsHandler) report(ctx context.Context, args *recordsReportArgs) (results *recordsReportResults, err error) {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// func (h recordsHandler) report(ctx context.Context, args *recordsReportArgs) (results *recordsReportResults, err error) {
|
||||
// return
|
||||
// }
|
||||
func (h recordsHandler) Report() *atypes.Function {
|
||||
return &atypes.Function{
|
||||
Ref: "composeRecordsReport",
|
||||
|
||||
@@ -1167,22 +1167,22 @@ endpoints:
|
||||
path: "/"
|
||||
parameters:
|
||||
post:
|
||||
- { name: records, type: "[]string", title: Records to update }
|
||||
- { name: values, type: "types.RecordValueSet", title: Fields to update and their values }
|
||||
- { name: query, type: "string", title: Search query for records to operate on }
|
||||
- name: bulkDelete
|
||||
method: DELETE
|
||||
title: Delete record row from module section
|
||||
path: "/"
|
||||
parameters:
|
||||
post:
|
||||
- type: "[]string"
|
||||
name: recordIDs
|
||||
required: false
|
||||
title: IDs of records to delete
|
||||
- type: bool
|
||||
name: truncate
|
||||
required: false
|
||||
title: Remove ALL records of a specified module (pending implementation)
|
||||
- name: query
|
||||
type: string
|
||||
required: false
|
||||
title: Search query for records to operate on
|
||||
- name: delete
|
||||
method: DELETE
|
||||
title: Delete record row from module section
|
||||
@@ -1209,10 +1209,10 @@ endpoints:
|
||||
path: "/undelete"
|
||||
parameters:
|
||||
post:
|
||||
- type: "[]string"
|
||||
name: recordIDs
|
||||
- name: query
|
||||
type: string
|
||||
required: false
|
||||
title: IDs of records to undelete
|
||||
title: Search query for records to operate on
|
||||
- name: upload
|
||||
path: "/attachment"
|
||||
method: POST
|
||||
|
||||
@@ -11,9 +11,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/revisions"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/compose/rest/request"
|
||||
"github.com/cortezaproject/corteza/server/compose/service"
|
||||
"github.com/cortezaproject/corteza/server/compose/types"
|
||||
@@ -26,7 +23,7 @@ import (
|
||||
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
|
||||
estore "github.com/cortezaproject/corteza/server/pkg/envoy/store"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/pkg/payload"
|
||||
"github.com/cortezaproject/corteza/server/pkg/revisions"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
@@ -233,34 +230,29 @@ func (ctrl *Record) Create(ctx context.Context, r *request.RecordCreate) (interf
|
||||
|
||||
func (ctrl *Record) Patch(ctx context.Context, req *request.RecordPatch) (interface{}, error) {
|
||||
var (
|
||||
f = types.RecordFilter{
|
||||
Query: req.Query,
|
||||
NamespaceID: req.NamespaceID,
|
||||
ModuleID: req.ModuleID,
|
||||
Deleted: filter.State(0),
|
||||
}
|
||||
|
||||
err error
|
||||
)
|
||||
|
||||
oo := make([]*types.RecordBulkOperation, 0)
|
||||
|
||||
counters := make(map[string]uint)
|
||||
for _, v := range req.Values {
|
||||
v.Place = counters[v.Name]
|
||||
counters[v.Name]++
|
||||
}
|
||||
|
||||
for _, r := range req.Records {
|
||||
oo = append(oo, &types.RecordBulkOperation{
|
||||
RecordID: cast.ToUint64(r),
|
||||
NamespaceID: req.NamespaceID,
|
||||
ModuleID: req.ModuleID,
|
||||
err = ctrl.record.BulkModifyByFilter(ctx, f, req.Values, types.OperationTypePatch)
|
||||
|
||||
Record: &types.Record{Values: req.Values},
|
||||
Operation: types.OperationTypePatch,
|
||||
})
|
||||
}
|
||||
|
||||
results, err := ctrl.record.Bulk(ctx, true, oo...)
|
||||
if rve := types.IsRecordValueErrorSet(err); rve != nil {
|
||||
return ctrl.handleValidationError(rve), nil
|
||||
}
|
||||
|
||||
return ctrl.makeRecordBulkPatchPayload(ctx, results, err)
|
||||
return api.OK(), err
|
||||
}
|
||||
|
||||
func (ctrl *Record) Update(ctx context.Context, r *request.RecordUpdate) (interface{}, error) {
|
||||
@@ -329,15 +321,20 @@ func (ctrl *Record) Delete(ctx context.Context, r *request.RecordDelete) (interf
|
||||
}
|
||||
|
||||
func (ctrl *Record) BulkDelete(ctx context.Context, r *request.RecordBulkDelete) (interface{}, error) {
|
||||
var (
|
||||
f = types.RecordFilter{
|
||||
Query: r.Query,
|
||||
NamespaceID: r.NamespaceID,
|
||||
ModuleID: r.ModuleID,
|
||||
Deleted: filter.State(0),
|
||||
}
|
||||
)
|
||||
|
||||
if r.Truncate {
|
||||
return nil, fmt.Errorf("pending implementation")
|
||||
}
|
||||
|
||||
return api.OK(), ctrl.record.DeleteByID(ctx,
|
||||
r.NamespaceID,
|
||||
r.ModuleID,
|
||||
payload.ParseUint64s(r.RecordIDs)...,
|
||||
)
|
||||
return api.OK(), ctrl.record.BulkModifyByFilter(ctx, f, nil, types.OperationTypeDelete)
|
||||
}
|
||||
|
||||
func (ctrl *Record) Undelete(ctx context.Context, r *request.RecordUndelete) (interface{}, error) {
|
||||
@@ -345,11 +342,16 @@ func (ctrl *Record) Undelete(ctx context.Context, r *request.RecordUndelete) (in
|
||||
}
|
||||
|
||||
func (ctrl *Record) BulkUndelete(ctx context.Context, r *request.RecordBulkUndelete) (interface{}, error) {
|
||||
return api.OK(), ctrl.record.UndeleteByID(ctx,
|
||||
r.NamespaceID,
|
||||
r.ModuleID,
|
||||
payload.ParseUint64s(r.RecordIDs)...,
|
||||
var (
|
||||
f = types.RecordFilter{
|
||||
Query: r.Query,
|
||||
NamespaceID: r.NamespaceID,
|
||||
ModuleID: r.ModuleID,
|
||||
Deleted: filter.State(1),
|
||||
}
|
||||
)
|
||||
|
||||
return api.OK(), ctrl.record.BulkModifyByFilter(ctx, f, nil, types.OperationTypeUndelete)
|
||||
}
|
||||
|
||||
func (ctrl *Record) Upload(ctx context.Context, r *request.RecordUpload) (interface{}, error) {
|
||||
|
||||
@@ -336,15 +336,15 @@ type (
|
||||
// Module ID
|
||||
ModuleID uint64 `json:",string"`
|
||||
|
||||
// Records POST parameter
|
||||
//
|
||||
// Records to update
|
||||
Records []string
|
||||
|
||||
// Values POST parameter
|
||||
//
|
||||
// Fields to update and their values
|
||||
Values types.RecordValueSet
|
||||
|
||||
// Query POST parameter
|
||||
//
|
||||
// Search query for records to operate on
|
||||
Query string
|
||||
}
|
||||
|
||||
RecordBulkDelete struct {
|
||||
@@ -358,15 +358,15 @@ type (
|
||||
// Module ID
|
||||
ModuleID uint64 `json:",string"`
|
||||
|
||||
// RecordIDs POST parameter
|
||||
//
|
||||
// IDs of records to delete
|
||||
RecordIDs []string
|
||||
|
||||
// Truncate POST parameter
|
||||
//
|
||||
// Remove ALL records of a specified module (pending implementation)
|
||||
Truncate bool
|
||||
|
||||
// Query POST parameter
|
||||
//
|
||||
// Search query for records to operate on
|
||||
Query string
|
||||
}
|
||||
|
||||
RecordDelete struct {
|
||||
@@ -414,10 +414,10 @@ type (
|
||||
// Module ID
|
||||
ModuleID uint64 `json:",string"`
|
||||
|
||||
// RecordIDs POST parameter
|
||||
// Query POST parameter
|
||||
//
|
||||
// IDs of records to undelete
|
||||
RecordIDs []string
|
||||
// Search query for records to operate on
|
||||
Query string
|
||||
}
|
||||
|
||||
RecordUpload struct {
|
||||
@@ -1653,8 +1653,8 @@ func (r RecordPatch) Auditable() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"namespaceID": r.NamespaceID,
|
||||
"moduleID": r.ModuleID,
|
||||
"records": r.Records,
|
||||
"values": r.Values,
|
||||
"query": r.Query,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1669,13 +1669,13 @@ func (r RecordPatch) GetModuleID() uint64 {
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r RecordPatch) GetRecords() []string {
|
||||
return r.Records
|
||||
func (r RecordPatch) GetValues() types.RecordValueSet {
|
||||
return r.Values
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r RecordPatch) GetValues() types.RecordValueSet {
|
||||
return r.Values
|
||||
func (r RecordPatch) GetQuery() string {
|
||||
return r.Query
|
||||
}
|
||||
|
||||
// Fill processes request and fills internal variables
|
||||
@@ -1699,6 +1699,12 @@ func (r *RecordPatch) Fill(req *http.Request) (err error) {
|
||||
} else if err == nil {
|
||||
// Multipart params
|
||||
|
||||
if val, ok := req.MultipartForm.Value["query"]; ok && len(val) > 0 {
|
||||
r.Query, err = val[0], nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1709,19 +1715,19 @@ func (r *RecordPatch) Fill(req *http.Request) (err error) {
|
||||
|
||||
// POST params
|
||||
|
||||
//if val, ok := req.Form["records[]"]; ok && len(val) > 0 {
|
||||
// r.Records, err = val, nil
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//}
|
||||
|
||||
//if val, ok := req.Form["values[]"]; ok && len(val) > 0 {
|
||||
// r.Values, err = types.RecordValueSet(val), nil
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//}
|
||||
|
||||
if val, ok := req.Form["query"]; ok && len(val) > 0 {
|
||||
r.Query, err = val[0], nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
@@ -1755,8 +1761,8 @@ func (r RecordBulkDelete) Auditable() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"namespaceID": r.NamespaceID,
|
||||
"moduleID": r.ModuleID,
|
||||
"recordIDs": r.RecordIDs,
|
||||
"truncate": r.Truncate,
|
||||
"query": r.Query,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1771,13 +1777,13 @@ func (r RecordBulkDelete) GetModuleID() uint64 {
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r RecordBulkDelete) GetRecordIDs() []string {
|
||||
return r.RecordIDs
|
||||
func (r RecordBulkDelete) GetTruncate() bool {
|
||||
return r.Truncate
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r RecordBulkDelete) GetTruncate() bool {
|
||||
return r.Truncate
|
||||
func (r RecordBulkDelete) GetQuery() string {
|
||||
return r.Query
|
||||
}
|
||||
|
||||
// Fill processes request and fills internal variables
|
||||
@@ -1807,6 +1813,13 @@ func (r *RecordBulkDelete) Fill(req *http.Request) (err error) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if val, ok := req.MultipartForm.Value["query"]; ok && len(val) > 0 {
|
||||
r.Query, err = val[0], nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1817,19 +1830,19 @@ func (r *RecordBulkDelete) Fill(req *http.Request) (err error) {
|
||||
|
||||
// POST params
|
||||
|
||||
//if val, ok := req.Form["recordIDs[]"]; ok && len(val) > 0 {
|
||||
// r.RecordIDs, err = val, nil
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//}
|
||||
|
||||
if val, ok := req.Form["truncate"]; ok && len(val) > 0 {
|
||||
r.Truncate, err = payload.ParseBool(val[0]), nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if val, ok := req.Form["query"]; ok && len(val) > 0 {
|
||||
r.Query, err = val[0], nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
@@ -1981,7 +1994,7 @@ func (r RecordBulkUndelete) Auditable() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"namespaceID": r.NamespaceID,
|
||||
"moduleID": r.ModuleID,
|
||||
"recordIDs": r.RecordIDs,
|
||||
"query": r.Query,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1996,8 +2009,8 @@ func (r RecordBulkUndelete) GetModuleID() uint64 {
|
||||
}
|
||||
|
||||
// Auditable returns all auditable/loggable parameters
|
||||
func (r RecordBulkUndelete) GetRecordIDs() []string {
|
||||
return r.RecordIDs
|
||||
func (r RecordBulkUndelete) GetQuery() string {
|
||||
return r.Query
|
||||
}
|
||||
|
||||
// Fill processes request and fills internal variables
|
||||
@@ -2021,6 +2034,12 @@ func (r *RecordBulkUndelete) Fill(req *http.Request) (err error) {
|
||||
} else if err == nil {
|
||||
// Multipart params
|
||||
|
||||
if val, ok := req.MultipartForm.Value["query"]; ok && len(val) > 0 {
|
||||
r.Query, err = val[0], nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2031,12 +2050,12 @@ func (r *RecordBulkUndelete) Fill(req *http.Request) (err error) {
|
||||
|
||||
// POST params
|
||||
|
||||
//if val, ok := req.Form["recordIDs[]"]; ok && len(val) > 0 {
|
||||
// r.RecordIDs, err = val, nil
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//}
|
||||
if val, ok := req.Form["query"]; ok && len(val) > 0 {
|
||||
r.Query, err = val[0], nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -126,6 +126,8 @@ type (
|
||||
Update(ctx context.Context, record *types.Record) (*types.Record, *types.RecordValueErrorSet, error)
|
||||
Bulk(ctx context.Context, skipFailed bool, oo ...*types.RecordBulkOperation) ([]types.RecordBulkOperationResult, error)
|
||||
|
||||
BulkModifyByFilter(ctx context.Context, f types.RecordFilter, values types.RecordValueSet, operation types.OperationType) (err error)
|
||||
|
||||
Validate(ctx context.Context, rec *types.Record) error
|
||||
|
||||
DeleteByID(ctx context.Context, namespaceID, moduleID uint64, recordID ...uint64) error
|
||||
@@ -714,6 +716,82 @@ func (svc record) Bulk(ctx context.Context, skipFailed bool, oo ...*types.Record
|
||||
}
|
||||
}
|
||||
|
||||
// BulkModifyByFilter performs bulk record operations based on the provided filter query.
|
||||
// It's able to update, delete or undelete records in a single transaction.
|
||||
func (svc record) BulkModifyByFilter(ctx context.Context, f types.RecordFilter, values types.RecordValueSet, operation types.OperationType) (err error) {
|
||||
var (
|
||||
ns *types.Namespace
|
||||
m *types.Module
|
||||
r *types.Record
|
||||
records types.RecordSet
|
||||
recordFilter types.RecordFilter
|
||||
|
||||
aProps = &recordActionProps{
|
||||
namespace: &types.Namespace{ID: f.NamespaceID},
|
||||
module: &types.Module{ID: f.ModuleID},
|
||||
}
|
||||
action func(props ...*recordActionProps) *recordAction
|
||||
|
||||
valueError *types.RecordValueErrorSet
|
||||
)
|
||||
|
||||
return store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) error {
|
||||
// load both the namespace and module
|
||||
if ns, m, err = loadModuleCombo(ctx, s, f.NamespaceID, f.ModuleID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aProps.setNamespace(ns)
|
||||
aProps.setModule(m)
|
||||
|
||||
f.Limit = 500
|
||||
|
||||
// performing a batched search for IDs, processing them in batches of 500 for update.
|
||||
for {
|
||||
records, recordFilter, err = svc.Find(ctx, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, r = range records {
|
||||
aProps.setRecord(r)
|
||||
|
||||
switch operation {
|
||||
case types.OperationTypePatch:
|
||||
action = RecordActionPatch
|
||||
r, valueError, err = svc.patch(ctx, r, values)
|
||||
case types.OperationTypeDelete:
|
||||
action = RecordActionDelete
|
||||
r, err = svc.processDelete(ctx, r, ns, m)
|
||||
case types.OperationTypeUndelete:
|
||||
action = RecordActionUndelete
|
||||
r, err = svc.processUndelete(ctx, r, ns, m)
|
||||
}
|
||||
|
||||
aProps.setChanged(r)
|
||||
|
||||
if valueError != nil && !valueError.IsValid() {
|
||||
return RecordErrValueInput().Wrap(valueError)
|
||||
}
|
||||
|
||||
_ = svc.recordAction(ctx, aProps, action, err)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if recordFilter.NextPage == nil {
|
||||
break
|
||||
}
|
||||
|
||||
f.NextPage = recordFilter.NextPage
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Raw create function that is responsible for value validation, event dispatching
|
||||
// and creation.
|
||||
func (svc record) create(ctx context.Context, new *types.Record) (rec *types.Record, dd *types.RecordValueErrorSet, err error) {
|
||||
@@ -1316,8 +1394,6 @@ func (svc record) delete(ctx context.Context, namespaceID, moduleID, recordID ui
|
||||
var (
|
||||
ns *types.Namespace
|
||||
m *types.Module
|
||||
|
||||
invokerID = auth.GetIdentityFromContext(ctx).Identity()
|
||||
)
|
||||
|
||||
if namespaceID == 0 {
|
||||
@@ -1335,6 +1411,14 @@ func (svc record) delete(ctx context.Context, namespaceID, moduleID, recordID ui
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return svc.processDelete(ctx, del, ns, m)
|
||||
}
|
||||
|
||||
func (svc record) processDelete(ctx context.Context, del *types.Record, namespace *types.Namespace, module *types.Module) (record *types.Record, err error) {
|
||||
var (
|
||||
invokerID = auth.GetIdentityFromContext(ctx).Identity()
|
||||
)
|
||||
|
||||
if !svc.ac.CanDeleteRecord(ctx, del) {
|
||||
return nil, RecordErrNotAllowedToDelete()
|
||||
}
|
||||
@@ -1343,7 +1427,7 @@ func (svc record) delete(ctx context.Context, namespaceID, moduleID, recordID ui
|
||||
del.DeletedBy = invokerID
|
||||
|
||||
// ensure module ref is set before running through records workflows and scripts
|
||||
del.SetModule(m)
|
||||
del.SetModule(module)
|
||||
|
||||
// deleted, revision need to be set when RecordBeforeDelete is triggered
|
||||
del.DeletedAt = nowUTC()
|
||||
@@ -1352,27 +1436,27 @@ func (svc record) delete(ctx context.Context, namespaceID, moduleID, recordID ui
|
||||
|
||||
{
|
||||
// Calling before-record-delete scripts
|
||||
if err = svc.eventbus.WaitFor(ctx, event.RecordBeforeDelete(nil, del, m, ns, nil, nil)); err != nil {
|
||||
if err = svc.eventbus.WaitFor(ctx, event.RecordBeforeDelete(nil, del, module, namespace, nil, nil)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if m.Config.RecordRevisions.Enabled {
|
||||
if module.Config.RecordRevisions.Enabled {
|
||||
// Prepare record revision for update
|
||||
if err = svc.revisions.softDeleted(ctx, del); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = dalutils.ComposeRecordSoftDelete(ctx, svc.dal, m, del); err != nil {
|
||||
if err = dalutils.ComposeRecordSoftDelete(ctx, svc.dal, module, del); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ensure module ref is set before running through records workflows and scripts
|
||||
del.SetModule(m)
|
||||
del.SetModule(module)
|
||||
|
||||
{
|
||||
_ = svc.eventbus.WaitFor(ctx, event.RecordAfterDeleteImmutable(nil, del, m, ns, nil, nil))
|
||||
_ = svc.eventbus.WaitFor(ctx, event.RecordAfterDeleteImmutable(nil, del, module, namespace, nil, nil))
|
||||
}
|
||||
|
||||
return del, nil
|
||||
@@ -1389,6 +1473,14 @@ func (svc record) undelete(ctx context.Context, namespaceID, moduleID, recordID
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return svc.processUndelete(ctx, undel, ns, m)
|
||||
}
|
||||
|
||||
func (svc record) processUndelete(ctx context.Context, undel *types.Record, namespace *types.Namespace, module *types.Module) (record *types.Record, err error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !svc.ac.CanUndeleteRecord(ctx, undel) {
|
||||
return nil, RecordErrNotAllowedToUndelete()
|
||||
}
|
||||
@@ -1397,7 +1489,7 @@ func (svc record) undelete(ctx context.Context, namespaceID, moduleID, recordID
|
||||
undel.DeletedBy = 0
|
||||
|
||||
// ensure module ref is set before running through records workflows and scripts
|
||||
undel.SetModule(m)
|
||||
undel.SetModule(module)
|
||||
|
||||
undel.DeletedAt = nil
|
||||
undel.DeletedBy = 0
|
||||
@@ -1405,27 +1497,27 @@ func (svc record) undelete(ctx context.Context, namespaceID, moduleID, recordID
|
||||
|
||||
{
|
||||
// Calling before-record-undelete scripts
|
||||
if err = svc.eventbus.WaitFor(ctx, event.RecordBeforeUndelete(nil, undel, m, ns, nil, nil)); err != nil {
|
||||
if err = svc.eventbus.WaitFor(ctx, event.RecordBeforeUndelete(nil, undel, module, namespace, nil, nil)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if m.Config.RecordRevisions.Enabled {
|
||||
if module.Config.RecordRevisions.Enabled {
|
||||
// Prepare record revision for update
|
||||
if err = svc.revisions.undeleted(ctx, undel); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = dalutils.ComposeRecordUndelete(ctx, svc.dal, m, undel); err != nil {
|
||||
if err = dalutils.ComposeRecordUndelete(ctx, svc.dal, module, undel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ensure module ref is set before running through records workflows and scripts
|
||||
undel.SetModule(m)
|
||||
undel.SetModule(module)
|
||||
|
||||
{
|
||||
_ = svc.eventbus.WaitFor(ctx, event.RecordAfterUndeleteImmutable(nil, undel, m, ns, nil, nil))
|
||||
_ = svc.eventbus.WaitFor(ctx, event.RecordAfterUndeleteImmutable(nil, undel, module, namespace, nil, nil))
|
||||
}
|
||||
|
||||
return undel, nil
|
||||
|
||||
@@ -101,10 +101,11 @@ type (
|
||||
)
|
||||
|
||||
const (
|
||||
OperationTypeCreate OperationType = "create"
|
||||
OperationTypeUpdate OperationType = "update"
|
||||
OperationTypeDelete OperationType = "delete"
|
||||
OperationTypePatch OperationType = "patch"
|
||||
OperationTypeCreate OperationType = "create"
|
||||
OperationTypeUpdate OperationType = "update"
|
||||
OperationTypeDelete OperationType = "delete"
|
||||
OperationTypePatch OperationType = "patch"
|
||||
OperationTypeUndelete OperationType = "undelete"
|
||||
)
|
||||
|
||||
func (f RecordFilter) ToConstraintedFilter(c map[string][]any) filter.Filter {
|
||||
|
||||
@@ -12,8 +12,8 @@ type (
|
||||
)
|
||||
|
||||
func HasUint64(ss []uint64, s uint64) bool {
|
||||
for i := range ss {
|
||||
if ss[i] == s {
|
||||
for _, value := range ss {
|
||||
if value == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,8 @@ func TestHasUint64(t *testing.T) {
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
assert.EqualValues(t, HasUint64(c.ss, c.s), c.o)
|
||||
ok := HasUint64(c.ss, c.s)
|
||||
assert.EqualValues(t, ok, c.o)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user