diff --git a/easydata.js/packs/crud/src/i18n/text_resources.ts b/easydata.js/packs/crud/src/i18n/text_resources.ts index c205d223..ce2ee54f 100644 --- a/easydata.js/packs/crud/src/i18n/text_resources.ts +++ b/easydata.js/packs/crud/src/i18n/text_resources.ts @@ -21,13 +21,16 @@ function addEasyDataCRUDTexts() { AddDlgCaption: 'Create {entity}', EditDlgCaption: 'Edit {entity}', DeleteDlgCaption: 'Delete {entity}', + BulkDeleteDlgCaption: 'Delete {entity} records', DeleteDlgMessage: 'Are you sure you want to remove this record: {{recordId}}?', + BulkDeleteDlgMessage: 'Are you sure you want to remove these records: [recordIds]?', EntityMenuDesc: 'Click on an entity to view/edit its content', BackToEntities: 'Back to entities', SearchBtn: 'Search', SearchInputPlaceholder: 'Search...', RootViewTitle: 'Entities', - ModelIsEmpty: 'No entity was found.' + ModelIsEmpty: 'No entity was found.', + BulkDeleteBtnTitle: 'Bulk Delete' }); } diff --git a/easydata.js/packs/crud/src/main/data_context.ts b/easydata.js/packs/crud/src/main/data_context.ts index 6e14524c..583dd95e 100644 --- a/easydata.js/packs/crud/src/main/data_context.ts +++ b/easydata.js/packs/crud/src/main/data_context.ts @@ -10,12 +10,13 @@ import { TextDataFilter } from '../filter/text_data_filter'; import { EasyDataServerLoader } from './easy_data_server_loader'; type EasyDataEndpointKey = - 'GetMetaData' | - 'FetchDataset' | - 'FetchRecord' | - 'CreateRecord' | - 'UpdateRecord' | - 'DeleteRecord' ; + 'GetMetaData' | + 'FetchDataset' | + 'FetchRecord' | + 'CreateRecord' | + 'UpdateRecord' | + 'DeleteRecord' | + 'BulkDeleteRecords'; interface CompoundRecordKey { @@ -168,6 +169,19 @@ export class DataContext { .finally(() => this.endProcess()); } + /** + * Delete records in bulk. + * @param obj Instances primary keys. + * @param sourceId Entity Id. + */ + public bulkDeleteRecords(obj: {[key: string]: object[]}, sourceId?: string) { + const url = this.resolveEndpoint('BulkDeleteRecords', { sourceId: sourceId || this.activeEntity.id }); + + this.startProcess(); + return this.http.post(url, obj, { dataType: 'json'}) + .finally(() => this.endProcess()); + } + public setEndpoint(key: EasyDataEndpointKey, value: string) : void public setEndpoint(key: EasyDataEndpointKey | string, value: string) : void { this.endpoints.set(key, value); @@ -231,5 +245,6 @@ export class DataContext { this.setEnpointIfNotExist('CreateRecord', combinePath(endpointBase, 'models/{modelId}/sources/{sourceId}/create')); this.setEnpointIfNotExist('UpdateRecord', combinePath(endpointBase, 'models/{modelId}/sources/{sourceId}/update')); this.setEnpointIfNotExist('DeleteRecord', combinePath(endpointBase, 'models/{modelId}/sources/{sourceId}/delete')); + this.setEnpointIfNotExist('BulkDeleteRecords', combinePath(endpointBase, 'models/{modelId}/sources/{sourceId}/bulk-delete')); } } \ No newline at end of file diff --git a/easydata.js/packs/crud/src/views/entity_data_view.ts b/easydata.js/packs/crud/src/views/entity_data_view.ts index 490b8833..5761376d 100644 --- a/easydata.js/packs/crud/src/views/entity_data_view.ts +++ b/easydata.js/packs/crud/src/views/entity_data_view.ts @@ -3,7 +3,7 @@ import { DataRow, i18n, utils as dataUtils } from '@easydata/core'; import { DefaultDialogService, DialogService, domel, EasyGrid, - GridCellRenderer, GridColumn, RowClickEvent + GridCellRenderer, GridColumn, RowClickEvent, BulkDeleteClickEvent } from '@easydata/ui'; import { EntityEditFormBuilder } from '../form/entity_form_builder'; @@ -84,11 +84,14 @@ export class EntityDataView { }, showPlusButton: this.context.getActiveEntity().isEditable, plusButtonTitle: i18n.getText('AddRecordBtnTitle'), + showBulkDeleteButton: this.context.getActiveEntity().isEditable, + bulkDeleteButtonTitle: i18n.getText('BulkDeleteBtnTitle'), showActiveRow: false, onPlusButtonClick: this.addClickHandler.bind(this), onGetCellRenderer: this.manageCellRenderer.bind(this), onRowDbClick: this.rowDbClickHandler.bind(this), - onSyncGridColumn: this.syncGridColumnHandler.bind(this) + onSyncGridColumn: this.syncGridColumnHandler.bind(this), + onBulkDeleteButtonClick: this.bulkDeleteClickHandler.bind(this) }, this.options.grid || {})); if (this.options.showFilterBox) { @@ -133,6 +136,12 @@ export class EntityDataView { } } } + else if (column.isSelectCol) { + column.width = 110; + return (value: any, column: GridColumn, cell: HTMLElement, rowEl: HTMLElement) => { + domel('div', cell).addChild('input', b => b.attr('type', 'checkbox')); + } + } } private addClickHandler() { @@ -241,6 +250,57 @@ export class EntityDataView { }); } + private bulkDeleteClickHandler(ev: BulkDeleteClickEvent) { + const activeEntity = this.context.getActiveEntity(); + const keyAttrs = activeEntity.getPrimaryAttrs(); + + let promises: Promise[] = []; + + // Get record rows to delete in bulk. + ev.rowIndices.forEach(index => { + promises.push(this.context.getData().getRow(index)); + }) + + let recordKeys: object[] = []; + + Promise.all(promises).then((rows) => { + recordKeys = rows.map(row => { + if (!row) return; + let keyVals = keyAttrs.map(attr => row.getValue(attr.id)); + let keys = keyAttrs.reduce((val, attr, index) => { + const property = attr.id.substring(attr.id.lastIndexOf('.') + 1); + val[property] = keyVals[index]; + return val; + }, {}); + return keys; + }); + + if (recordKeys.length == 0) { + return; + } + + this.dlg.openConfirm( + i18n.getText('BulkDeleteDlgCaption') + .replace('{entity}', activeEntity.caption), + i18n.getText('BulkDeleteDlgMessage') + .replace('recordIds', recordKeys.map( + keys => '{' + Object.keys(keys).map(key => `${key}:${keys[key]}`).join('; ') + '}' + ).join("; ") + ), + ) + .then((result) => { + if (!result) return; + this.context.bulkDeleteRecords({'pks': recordKeys}) + .then(() => { + return this.refreshData(); + }) + .catch((error) => { + this.processError(error); + }); + }); + }); + } + private processError(error) { this.dlg.open({ title: 'Ooops, something went wrong', diff --git a/easydata.js/packs/ui/assets/css/easy-grid.css b/easydata.js/packs/ui/assets/css/easy-grid.css index f50c7907..b80c9255 100644 --- a/easydata.js/packs/ui/assets/css/easy-grid.css +++ b/easydata.js/packs/ui/assets/css/easy-grid.css @@ -164,7 +164,7 @@ font-size: 16px; } -.keg-header-btn-plus { +.keg-header-btn-plus, .keg-header-btn-delete { position: relative; height: 23px; width: 23px; @@ -181,6 +181,19 @@ background-position: -25px 0 !important; } +.keg-header-btn-delete a { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QAAKqNIzIAAAAJcEhZcwAAAGAAAABgAPBrQs8AAAAHdElNRQfmAwEEIQkBn0oRAAAA6UlEQVQoz73RLUuDYRTG8d997zY4XwZD2NCwJhO7n0KbdU0/gMlPYLUIFrUJFi2micF1v4BiFIQZfJuOhW2P4ZlDQbD5Txec6xzOdU6Qs27DKyg5cGZMMm1BsurUtYKBFWtu9N3rQrCvoGdWV3/UUvRmUtuOHsmSLc8CAsiQKdtW9kByac+tvkw0FA0FSd2Jdr5FzaGKOXUly0rq5lQdmc/LUWago6phwqakoaJj8JUijtMURVOiouQbcawyvxL9wb8ZoraWDxe6rjyObjqK9y6pudMUnAuaWJTyV5E8adnV+TF3xrGXXH4CCEs376+QugwAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjItMDMtMDFUMDQ6MzM6MDktMDU6MDDpgVkiAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIyLTAzLTAxVDA0OjMzOjA5LTA1OjAwmNzhngAAAABJRU5ErkJggg==') no-repeat; + width: 100%; + height: 100%; + display: block; + background-position: center; +} + +.keg-header-btn-delete a:hover { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAABgAAAAYADwa0LPAAAAB3RJTUUH5gMBBCAyqY+SdAAAAxFJREFUSMfVlUtIVVEUhr+9zzl6vSnmNcJeFE6iKEjKIhGCBk0qkAqqgTSwEiIIqdAoGkQRVNog6GFBBGYURFhBL+hFQV2FoEnUpFDKNHpc9d7u1XPObrC2jzSpoEkLLj9r77X/9fPvtc9VDMWZ020vAVBkANDodWWAwWwuBQwkPtjilRZvAwry50nd2XY5ebURCAgAqrctWsy4oeBMY1s81wNC9NQ3suYtkO3dlwWf1Am2Vgo6xwSD7YAiLK0BDE75XBF65BGgMcFSwEF1bJD1VAOoQ2YywNa3pTPAFcVH9ktjp1WI0+IF5qlgyXPB+VYI5yzeBQzaK5Jyc1R4qtPSMPoZCODjesDBHLoEZp/qSncCVSACDMxZCPiENc+ANP7Xd7ZBhRCqHTZfY9G3uMridyt4ufBxEYjgxqoAD13bBoSYglPC11kxeAVWwL0eQKFOdMmB17Ntnw5LvFpQHxQMLYFusfkue6MyA8pdIXxzysSB5hgQIatrD+AzMDwDLjBAXpOMXV/xWqCX5N4WIEpWcAvwcCcVAikynR1AFG9aCZAmeL8XiKCnNAE+/qdTgE+BGwKa/sO5QJrUtSVAgnS4G6rrFpUPC9B2FDeJNcFFmd5eX15BUTGQjVM5E4jguQ0iaOtEYCp57kYgm6zKeqCQ3KIDgCLWm2MdbBaHzU55JWNfgR6VOxbjQIBxrkgafSx3qPsln3AM0Cgd2P04oNDuWTsDraP4xg09zrqyv6TN+4ZWJRIW0xZ7fh7GPw/9twf+dfw3AtQf7v+ublwBGhmewTkNLQ5+cAbvOhiRmxH7KQFjh3SIx4zqMybkU2zMK6v+PODiqFrAQ3cvk7L77wCFSrYDBnNnCzBAkOwDDOGD04BPf7cHuAzok5b/AqBRxAHnV/64gEsmWQ8ojFsHxIjOuglonDeHxY07x4EIjvomAq5ngBS+isi52+XAd4xxgSifZ8eAEOMWApqcVP4IN8YI0KgvjYAifKiBEN2QkEa9sVH1+eLEkPX2z4x+a/MLcSSvQOqaqoEcehI3gCTTxwr4AeSNAVcYdmHUAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIyLTAzLTAxVDA0OjMyOjUwLTA1OjAw2zt5qwAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMi0wMy0wMVQwNDozMjo1MC0wNTowMKpmwRcAAAAASUVORK5CYII=') no-repeat; + background-position: center; +} + /* Pagination */ .keg-pagination-wrapper { display: inline-flex; diff --git a/easydata.js/packs/ui/src/grid/easy_grid.ts b/easydata.js/packs/ui/src/grid/easy_grid.ts index ab480c14..112f2bb5 100644 --- a/easydata.js/packs/ui/src/grid/easy_grid.ts +++ b/easydata.js/packs/ui/src/grid/easy_grid.ts @@ -17,7 +17,8 @@ import { PageChangedEvent, ColumnChangedEvent, ColumnDeletedEvent, - ActiveRowChangedEvent + ActiveRowChangedEvent, + BulkDeleteClickEvent } from './easy_grid_events'; import { GridColumnList, GridColumn, GridColumnAlign } from './easy_grid_columns'; @@ -110,7 +111,8 @@ export class EasyGrid { }, showPlusButton: false, viewportRowsCount: null, - showActiveRow: true + showActiveRow: true, + showBulkDeleteButton: false, } public readonly options: EasyGridOptions; @@ -241,6 +243,9 @@ export class EasyGrid { if (options.onActiveRowChanged) { this.addEventListener('activeRowChanged', options.onActiveRowChanged); } + if (options.onBulkDeleteButtonClick) { + this.addEventListener('bulkDeleteClick', options.onBulkDeleteButtonClick); + } this.addEventListener('pageChanged', ev => this.activeRowIndex = -1); @@ -450,6 +455,15 @@ export class EasyGrid { domel(hd) .addChildElement(this.renderHeaderButtons()); } + + if (column.isSelectCol) { + domel(hd) + .addChildElement(domel('span') + .setStyle("margin-left", "4px") + .setStyle("display", "flex") + .setStyle("align-items", "center") + .addChildElement(this.renderSelectAllCheckbox()).toDOM()); + } }); const containerWidth = this.getContainerWidth(); @@ -478,7 +492,7 @@ export class EasyGrid { domel('div', colDiv) .addClass(`${this.cssPrefix}-header-cell-resize`) - if (!column.isRowNum) { + if (!column.isRowNum && !column.isSelectCol) { domel('div', colDiv) .addClass(`${this.cssPrefix}-header-cell-label`) .text(column.label); @@ -957,8 +971,21 @@ export class EasyGrid { return; } - const colindex = column.isRowNum ? -1 : this.dataTable.columns.getIndex(column.dataColumn.id); - let val = column.isRowNum ? indexGlobal + 1 : row.getValue(colindex); + let colindex: number; + let val: any; + + if (column.isSelectCol) { + colindex = -2; + val = indexGlobal + 1; + } + else if (column.isRowNum) { + colindex = -1; + val = indexGlobal + 1; + } + else { + colindex = this.dataTable.columns.getIndex(column.dataColumn.id); + val = row.getValue(colindex) + } rowElement.appendChild(this.renderCell(column, index, val, rowElement)); }); @@ -1174,6 +1201,7 @@ export class EasyGrid { public addEventListener(eventType: 'columnMoved', handler: (ev: ColumnMovedEvent) => void): string; public addEventListener(eventType: 'columnDeleted', handler: (ev: ColumnDeletedEvent) => void): string; public addEventListener(eventType: 'activeRowChanged', handler: (ev: ActiveRowChangedEvent) => void): string; + public addEventListener(eventType: 'bulkDeleteClick', handler: (ev: BulkDeleteClickEvent) => void): string; public addEventListener(eventType: GridEventType | string, handler: (data: any) => void): string { return this.eventEmitter.subscribe(eventType, event => handler(event.data)); } @@ -1183,26 +1211,80 @@ export class EasyGrid { } protected renderHeaderButtons(): HTMLElement { - if (this.options.showPlusButton) { - return domel('div') - .addClass(`${this.cssPrefix}-header-btn-plus`) - .title(this.options.plusButtonTitle || 'Add') - .addChild('a', builder => builder - .attr('href', 'javascript:void(0)') - .on('click', (e) => { - e.preventDefault(); - this.fireEvent({ - type: 'plusButtonClick', - sourceEvent: e - } as PlusButtonClickEvent); - }) - ) + if (!this.options.showBulkDeleteButton && !this.options.showPlusButton) { + return domel('span') + .addText('#') .toDOM(); } - return domel('span') - .addText('#') - .toDOM(); + let buttonBlock = domel('div') + .setStyle("display", "flex") + .setStyle("justify-content", "center"); + + // Generate Add Record button. + if (this.options.showPlusButton) { + buttonBlock.addChildElement(domel('div') + .addClass(`${this.cssPrefix}-header-btn-plus`) + .title(this.options.plusButtonTitle || 'Add') + .addChild('a', builder => builder + .attr('href', 'javascript:void(0)') + .on('click', (e) => { + e.preventDefault(); + this.fireEvent({ + type: 'plusButtonClick', + sourceEvent: e + } as PlusButtonClickEvent); + }) + ).toDOM()); + } + + // Generate Bulk Delete button. + if (this.options.showBulkDeleteButton) { + buttonBlock.addChildElement(domel('div') + .addClass(`${this.cssPrefix}-header-btn-delete`) + .title(this.options.bulkDeleteButtonTitle || 'Bulk delete') + .addChild('a', builder => builder + .attr('href', 'javascript:void(0)')) + .on('click', (e) => { + e.preventDefault(); + this.fireEvent({ + type: 'bulkDeleteClick', + rowIndices: this.getSelectedRowsIds() + } as BulkDeleteClickEvent); + }).toDOM()); + } + + return buttonBlock.toDOM(); + } + + /** + * Render checkbox to select all checkboxes. + */ + private renderSelectAllCheckbox(): HTMLElement { + return domel('input') + .attr('type', 'checkbox') + .on('change', (e) => { + var checkboxes = document.querySelectorAll('input[type="checkbox"]'); + const targetCheckbox: HTMLInputElement = e.target as HTMLInputElement; + + checkboxes.forEach(checkbox => { + const input: HTMLInputElement = checkbox as HTMLInputElement; + + if (input != targetCheckbox) + input.checked = targetCheckbox.checked; + }) + }).toDOM(); + } + + /** + * Get indices of selected rows. + */ + private getSelectedRowsIds(): number[] { + var checkboxes = document.querySelectorAll('div.keg-cell-value input[type="checkbox"]:checked'); + const indices: number[] = Array.from(checkboxes, checkbox => { + return parseInt(checkbox.closest('div.keg-row').getAttribute('data-row-idx')); + }) + return indices; } @@ -1372,11 +1454,11 @@ export class EasyGrid { maxWidth += 3; - const maxOption = column.isRowNum + const maxOption = column.isRowNum || column.isSelectCol ? this.options.columnWidths.rowNumColumn.max || 500 : this.options.columnWidths[column.dataColumn.type].max || 2000; - const minOption = column.isRowNum + const minOption = column.isRowNum || column.isSelectCol ? this.options.columnWidths.rowNumColumn.min || 0 : this.options.columnWidths[column.dataColumn.type].min || 20; diff --git a/easydata.js/packs/ui/src/grid/easy_grid_columns.ts b/easydata.js/packs/ui/src/grid/easy_grid_columns.ts index c2f6e45b..029be8e0 100644 --- a/easydata.js/packs/ui/src/grid/easy_grid_columns.ts +++ b/easydata.js/packs/ui/src/grid/easy_grid_columns.ts @@ -45,11 +45,16 @@ export class GridColumn { public readonly isRowNum: boolean = false; + /** + * If column contains checkbox to select the row. + */ + public readonly isSelectCol: boolean = false; + public cellRenderer: GridCellRenderer; public calculatedWidth: number; - constructor(column: DataColumn, grid: EasyGrid, isRowNum: boolean = false) { + constructor(column: DataColumn, grid: EasyGrid, isRowNum: boolean = false, isSelectCol: boolean = false) { this.dataColumn = column; this.grid = grid; const widthOptions = grid.options.columnWidths || {}; @@ -64,8 +69,9 @@ export class GridColumn { this.cellRenderer = this.grid.cellRendererStore.getDefaultRenderer(column.type); this._description = column.description; } - else if (isRowNum) { - this.isRowNum = true; + else if (isRowNum || isSelectCol) { + this.isRowNum = isRowNum; + this.isSelectCol = isSelectCol; this.width = (widthOptions && widthOptions.rowNumColumn) ? widthOptions.rowNumColumn.default : ROW_NUM_WIDTH; this._label = ''; @@ -78,7 +84,10 @@ export class GridColumn { } public get label(): string { - return this._label ? this._label : this.isRowNum ? '' : this.dataColumn.label; + if (this.isSelectCol || this.isRowNum) { + return ''; + } + return this._label ? this._label : this.dataColumn.label; }; public set label(value: string) { @@ -108,6 +117,9 @@ export class GridColumnList { public sync(columnList: DataColumnList, hasRowNumCol = true) { this.clear(); + const selectColumn = new GridColumn(null, this.grid, false, true); + this.add(selectColumn); + const rowNumCol = new GridColumn(null, this.grid, true); this.add(rowNumCol); if (!hasRowNumCol) { diff --git a/easydata.js/packs/ui/src/grid/easy_grid_events.ts b/easydata.js/packs/ui/src/grid/easy_grid_events.ts index 5397fa20..a433766d 100644 --- a/easydata.js/packs/ui/src/grid/easy_grid_events.ts +++ b/easydata.js/packs/ui/src/grid/easy_grid_events.ts @@ -8,7 +8,8 @@ export type GridEventType = 'addColumnClick' | 'columnChanged' | 'columnDeleted' | - 'columnMoved'; + 'columnMoved' | + 'bulkDeleteClick'; export interface GridEvent { type: GridEventType | string; @@ -53,3 +54,8 @@ export interface ActiveRowChangedEvent extends GridEvent { newValue: number; rowIndex: number; } + +export interface BulkDeleteClickEvent extends GridEvent { + type: 'bulkDeleteClick'; + rowIndices: number[]; +} diff --git a/easydata.js/packs/ui/src/grid/easy_grid_options.ts b/easydata.js/packs/ui/src/grid/easy_grid_options.ts index 25cea075..1a476162 100644 --- a/easydata.js/packs/ui/src/grid/easy_grid_options.ts +++ b/easydata.js/packs/ui/src/grid/easy_grid_options.ts @@ -6,7 +6,8 @@ import { AddColumnClickEvent, PageChangedEvent, RowClickEvent, - ActiveRowChangedEvent + ActiveRowChangedEvent, + BulkDeleteClickEvent } from './easy_grid_events'; import { GridColumn } from './easy_grid_columns'; @@ -49,6 +50,8 @@ export interface EasyGridOptions { plusButtonTitle?: string; useRowNumeration?: boolean; allowDragDrop?: boolean; + showBulkDeleteButton?: boolean; + bulkDeleteButtonTitle?: string fixHeightOnFirstRender?: boolean; @@ -85,6 +88,7 @@ export interface EasyGridOptions { onColumnDeleted?: (ev: ColumnDeletedEvent) => void; onColumnMoved?: (ev: ColumnMovedEvent) => void; onActiveRowChanged?: (ev:ActiveRowChangedEvent) => void; + onBulkDeleteButtonClick?: (ev: BulkDeleteClickEvent) => void; onSyncGridColumn?: (column: GridColumn) => void; onGetCellRenderer?: (column: GridColumn, defaultRenderer: GridCellRenderer) => GridCellRenderer; diff --git a/easydata.net/src/EasyData.AspNetCore/Middleware/EasyDataApiHandler.cs b/easydata.net/src/EasyData.AspNetCore/Middleware/EasyDataApiHandler.cs index 1d3c6af2..0b751309 100644 --- a/easydata.net/src/EasyData.AspNetCore/Middleware/EasyDataApiHandler.cs +++ b/easydata.net/src/EasyData.AspNetCore/Middleware/EasyDataApiHandler.cs @@ -255,6 +255,24 @@ await WriteOkJsonResponseAsync(HttpContext, async (jsonWriter, cancellationToken } } + /// + /// Delete records in bulk. + /// + /// Id of the model. + /// Entity type. + /// Cancellation token. + public virtual async Task HandleDeleteRecordsInBulkAsync(string modelId, string sourceId, CancellationToken ct = default) + { + using (var reader = new HttpRequestStreamReader(HttpContext.Request.Body, Encoding.UTF8)) + using (var jsReader = new JsonTextReader(reader)) { + var props = await JObject.LoadAsync(jsReader, ct); + await Manager.DeleteRecordsInBulkAsync(modelId, sourceId, props, ct); + await WriteOkJsonResponseAsync(HttpContext, async (jsonWriter, cancellationToken) => { + await WriteDeleteRecordResponseAsync(jsonWriter, cancellationToken); + }, ct); + }; + } + protected virtual Task WriteDeleteRecordResponseAsync(JsonWriter jsonWriter, CancellationToken ct) { return Task.CompletedTask; diff --git a/easydata.net/src/EasyData.AspNetCore/Middleware/EasyDataMiddleware.cs b/easydata.net/src/EasyData.AspNetCore/Middleware/EasyDataMiddleware.cs index 5126979f..1f8338eb 100644 --- a/easydata.net/src/EasyData.AspNetCore/Middleware/EasyDataMiddleware.cs +++ b/easydata.net/src/EasyData.AspNetCore/Middleware/EasyDataMiddleware.cs @@ -19,6 +19,7 @@ public static class DataAction public const string CreateRecord = "CreateRecord"; public const string UpdateRecord = "UpdateRecord"; public const string DeleteRecord = "DeleteRecord"; + public const string DeleteRecordsInBulk = "DeleteRecordsInBulk"; } public class EasyDataMiddleware where THandler: EasyDataApiHandler @@ -47,7 +48,8 @@ public Endpoint(string action, string regex, string method) new Endpoint(DataAction.FetchRecord, @"^/models/([^/]+?)/sources/([^/]+?)/fetch$", "GET"), new Endpoint(DataAction.CreateRecord, @"^/models/([^/]+?)/sources/([^/]+?)/create$", "POST"), new Endpoint(DataAction.UpdateRecord,@"^/models/([^/]+?)/sources/([^/]+?)/update$", "POST"), - new Endpoint(DataAction.DeleteRecord, @"^/models/([^/]+?)/sources/([^/]+?)/delete$", "POST") + new Endpoint(DataAction.DeleteRecord, @"^/models/([^/]+?)/sources/([^/]+?)/delete$", "POST"), + new Endpoint(DataAction.DeleteRecordsInBulk, @"^/models/([^/]+?)/sources/([^/]+?)/bulk-delete", "POST") }; public EasyDataMiddleware(RequestDelegate next, EasyDataOptions options) @@ -110,6 +112,9 @@ public async Task InvokeAsync(HttpContext context) case DataAction.DeleteRecord: await handler.HandleDeleteRecordAsync(modelId, entityTypeName, ct); return; + case DataAction.DeleteRecordsInBulk: + await handler.HandleDeleteRecordsInBulkAsync(modelId, entityTypeName, ct); + return; } } catch (Exception ex) { diff --git a/easydata.net/src/EasyData.Core/Services/EasyDataManager.cs b/easydata.net/src/EasyData.Core/Services/EasyDataManager.cs index c8d2045b..d4133631 100644 --- a/easydata.net/src/EasyData.Core/Services/EasyDataManager.cs +++ b/easydata.net/src/EasyData.Core/Services/EasyDataManager.cs @@ -59,6 +59,15 @@ public abstract Task FetchDatasetAsync( public abstract Task DeleteRecordAsync(string modelId, string sourceId, JObject props, CancellationToken ct = default); + /// + /// Delete entities in bulk. + /// + /// Model Id. + /// Entity type. + /// Primary keys of the records to delete in bulk. + /// Cancellation Token. + public abstract Task DeleteRecordsInBulkAsync(string modelId, string sourceId, JObject primaryKeys, CancellationToken ct = default); + public abstract Task> GetDefaultSortersAsync(string modelId, string sourceId, CancellationToken ct = default); /// diff --git a/easydata.net/src/EasyData.EntityFrameworkCore.Relational/Models/BulkDeleteDTO.cs b/easydata.net/src/EasyData.EntityFrameworkCore.Relational/Models/BulkDeleteDTO.cs new file mode 100644 index 00000000..ac0e4b13 --- /dev/null +++ b/easydata.net/src/EasyData.EntityFrameworkCore.Relational/Models/BulkDeleteDTO.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace EasyData.EntityFrameworkCore.Models +{ + /// + /// Store information used for deletion in bulk. + /// + public class BulkDeleteDTO + { + /// + /// Gets or sets Primary Keys of the Records. + /// + [JsonProperty("pks")] + public JObject[] PrimaryKeys { get; set; } + } +} diff --git a/easydata.net/src/EasyData.EntityFrameworkCore.Relational/Services/EasyDataManagerEF.cs b/easydata.net/src/EasyData.EntityFrameworkCore.Relational/Services/EasyDataManagerEF.cs index ee951fa4..aa3f7d94 100644 --- a/easydata.net/src/EasyData.EntityFrameworkCore.Relational/Services/EasyDataManagerEF.cs +++ b/easydata.net/src/EasyData.EntityFrameworkCore.Relational/Services/EasyDataManagerEF.cs @@ -11,6 +11,8 @@ using Newtonsoft.Json.Linq; using EasyData.EntityFrameworkCore; +using EasyData.EntityFrameworkCore.Models; + namespace EasyData.Services { public class EasyDataManagerEF : EasyDataManager where TDbContext : DbContext @@ -196,6 +198,38 @@ public override async Task DeleteRecordAsync(string modelId, string sourceId, JO await DbContext.SaveChangesAsync(ct); } + /// + public override async Task DeleteRecordsInBulkAsync(string modelId, string sourceId, JObject primaryKeys, CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); + var entityType = GetCurrentEntityType(DbContext, sourceId); + var recordsPrimaryKeys = GetRecordsPrimaryKeys(primaryKeys); + var recordsToDelete = new List(); + + foreach (var pk in recordsPrimaryKeys) { + var keys = GetKeys(entityType, pk); + var record = FindRecord(DbContext, entityType.ClrType, keys.Values); + + if (record == null) { + throw new RecordNotFoundException(sourceId, + $"({string.Join(";", keys.Select(kv => $"{kv.Key.Name}: {kv.Value}"))})"); + } + + recordsToDelete.Add(record); + } + + DbContext.RemoveRange(recordsToDelete); + await DbContext.SaveChangesAsync(ct); + } + + /// + /// Get primary keys of records from the request body. + /// + private IEnumerable GetRecordsPrimaryKeys(JObject fields) + { + return fields.ToObject().PrimaryKeys; + } + private static IEntityType GetCurrentEntityType(DbContext dbContext, string sourceId) { var entityType = dbContext.Model.GetEntityTypes() diff --git a/easydata.net/test/EasyData.Core.Test/Services/EasyDataOptionsTests.cs b/easydata.net/test/EasyData.Core.Test/Services/EasyDataOptionsTests.cs index 10c37e37..6dbc96ad 100644 --- a/easydata.net/test/EasyData.Core.Test/Services/EasyDataOptionsTests.cs +++ b/easydata.net/test/EasyData.Core.Test/Services/EasyDataOptionsTests.cs @@ -123,6 +123,11 @@ public override Task DeleteRecordAsync(string modelId, string sourceId, JObject throw new NotImplementedException(); } + public override Task DeleteRecordsInBulkAsync(string modelId, string sourceId, JObject pks, CancellationToken ct = default) + { + throw new NotImplementedException(); + } + public override Task FetchDatasetAsync(string modelId, string sourceId, IEnumerable filters = null, IEnumerable sorters = null, bool isLookup = false, int? offset = null, int? fetch = null, CancellationToken ct = default) { throw new NotImplementedException();