From 7089e8063401a4a11bdc103f1f5f888d327fc7b7 Mon Sep 17 00:00:00 2001 From: Vasily Chefonov <81791194+vasiliy-chefonov@users.noreply.github.com> Date: Tue, 16 Nov 2021 13:48:57 +0300 Subject: [PATCH 1/9] Add help text --- .../packs/core/src/data/data_column.ts | 18 +++++ .../crud/src/form/entity_form_builder.ts | 60 ++++++++------- easydata.js/packs/ui/assets/css/easy-grid.css | 8 ++ easydata.js/packs/ui/src/grid/easy_grid.ts | 8 +- .../packs/ui/src/grid/easy_grid_columns.ts | 7 ++ .../src/EasyData.Core/EasyDataResultSet.cs | 74 +++++++++++++++++-- .../Services/EasyDataManagerEF.cs | 3 +- 7 files changed, 142 insertions(+), 36 deletions(-) diff --git a/easydata.js/packs/core/src/data/data_column.ts b/easydata.js/packs/core/src/data/data_column.ts index f21a5f65..f6952277 100644 --- a/easydata.js/packs/core/src/data/data_column.ts +++ b/easydata.js/packs/core/src/data/data_column.ts @@ -15,34 +15,51 @@ export interface DataColumnStyle } export interface DataColumnDescriptor { + /** Represents internal column id. */ id: string; + /** Represents original internal column id. */ originAttrId?: string; + /** The type of data represented by column. */ type?: DataType; + /** Name to use for this column in the UI. */ label: string; isAggr?: boolean; + /** The display format for the column. */ dfmt?: string; gfct?: string; + /** The style of the column to display in UI. */ style?: DataColumnStyle; + /** Detailed description of the column. */ + description: string | null; } export class DataColumn { + /** The type of data represented by column. */ public readonly type: DataType; + /** Represents internal column id. */ public readonly id: string; public readonly isAggr: boolean; + /** Represents original internal column id. */ public readonly originAttrId?: string; + /** Name to use for this column in the UI. */ public label: string; + /** The display format for the column. */ public displayFormat?: string; public groupFooterColumnTemplate?: string; + /** The style of the column to display in UI. */ public style?: DataColumnStyle; + /** Column description. */ + public description: string | null; + constructor(desc: DataColumnDescriptor) { if (!desc) throw Error("Options are required"); @@ -61,6 +78,7 @@ export class DataColumn { this.displayFormat = desc.dfmt; this.groupFooterColumnTemplate = desc.gfct; this.style = desc.style || {}; + this.description = desc.description; } } diff --git a/easydata.js/packs/crud/src/form/entity_form_builder.ts b/easydata.js/packs/crud/src/form/entity_form_builder.ts index df01f2f1..e247638c 100644 --- a/easydata.js/packs/crud/src/form/entity_form_builder.ts +++ b/easydata.js/packs/crud/src/form/entity_form_builder.ts @@ -394,35 +394,43 @@ export class EntityEditFormBuilder { .addChild('label', b => b .attr('for', attr.id) .addHtml(`${attr.caption} ${required ? '*' : ''}: `) - ); + ).addChild('div'); + + let fieldHolder = parent.lastChild as HTMLElement; if (attr.kind === EntityAttrKind.Lookup) { - this.setupLookupField(parent, attr, readOnly, value); - return; + this.setupLookupField(fieldHolder, attr, readOnly, value); + } + else { + switch (editor.tag) { + case EditorTag.DateTime: + this.setupDateTimeField(fieldHolder, attr, readOnly, value); + break; + + case EditorTag.List: + this.setupListField(fieldHolder, attr, readOnly, editor.values, value); + break; + + case EditorTag.File: + this.setupFileField(fieldHolder, attr, readOnly, editor.accept); + break; + + case EditorTag.Edit: + default: + if (editor.multiline) { + this.setupTextArea(fieldHolder, attr, readOnly, value); + } + else { + this.setupTextField(fieldHolder, attr, readOnly, value); + } + break; + } } - switch (editor.tag) { - case EditorTag.DateTime: - this.setupDateTimeField(parent, attr, readOnly, value); - break; - - case EditorTag.List: - this.setupListField(parent, attr, readOnly, editor.values, value); - break; - - case EditorTag.File: - this.setupFileField(parent, attr, readOnly, editor.accept); - break; - - case EditorTag.Edit: - default: - if (editor.multiline) { - this.setupTextArea(parent, attr, readOnly, value); - } - else { - this.setupTextField(parent, attr, readOnly, value); - } - break; + if (attr.description) { + domel(fieldHolder).addChild('small', b => b + .addText(attr.description) + ); } } @@ -501,4 +509,4 @@ export class EntityEditFormBuilder { return this.form; } -} +} \ No newline at end of file diff --git a/easydata.js/packs/ui/assets/css/easy-grid.css b/easydata.js/packs/ui/assets/css/easy-grid.css index 00691686..558ffcd8 100644 --- a/easydata.js/packs/ui/assets/css/easy-grid.css +++ b/easydata.js/packs/ui/assets/css/easy-grid.css @@ -330,4 +330,12 @@ .eqjs-chart-content canvas { max-height: 100%; +} + +.question-mark { + position: relative; + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAACE4AAAhOAYwxAOwAAAFgSURBVDhPbdK7K4dRHMfxn1sWUVhcihK/MBKlbMqGKAplMMglk7JJRpOy+R8sJotBiUUiSe6XWNxGpYT3++k5Tw/51Kvfc06/c/2enMzv2K5CCxpRgmecYB+vSJIX/5oCTGINnXjBG+owjXE84gzfSOLAZRxiKG6nU4QpnGIWyY79mIEDa+N2LirRhvK4Tx24Qx+iVOMarhhmHICr7MFJ22GcdB47KLajF8fIt0GcYBujcLur2EBIIa7Q5Eze6hY+ETKCdXzA838hxL4jZB1sOZ4Q4k0+wJ1sogtLSMdKlDnYOtbY8ycNsEzDOLAjFd+CZct04wKeLx3brfCM6ZTiFvWu7MvxTGMIt20slbWNbjWOj2oB54hWNv2wNNbRCU0FJhAGO3AQN/B/SRzgy7mHdfxvqytwYI8dJr1Nv635HFzVcnirXk4z3OoidhElPTjEbfrqsiiDZ7uMf98RJ5P5AUhxQvegD107AAAAAElFTkSuQmCC') no-repeat center; + width: 20px; + height: 20px; + margin-left: 5px; } \ No newline at end of file diff --git a/easydata.js/packs/ui/src/grid/easy_grid.ts b/easydata.js/packs/ui/src/grid/easy_grid.ts index 2a3b7c76..71e11363 100644 --- a/easydata.js/packs/ui/src/grid/easy_grid.ts +++ b/easydata.js/packs/ui/src/grid/easy_grid.ts @@ -393,6 +393,12 @@ export class EasyGrid { .text(column.label); } + if (column.description) { + domel('div', colDiv) + .addClass('question-mark') + .title(column.description); + } + if (this.options.allowDragDrop) { eqDragManager.registerDraggableItem({ element: colDiv, @@ -672,7 +678,7 @@ export class EasyGrid { let result: any = {} for (const colId of group.columns) { let keyVal = row.getValue(colId); - if (caseInsensitive && typeof(keyVal) === 'string') { + if (caseInsensitive) { keyVal = keyVal.toLowerCase(); } result[colId] = keyVal; 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 f46d490f..71babab4 100644 --- a/easydata.js/packs/ui/src/grid/easy_grid_columns.ts +++ b/easydata.js/packs/ui/src/grid/easy_grid_columns.ts @@ -39,6 +39,7 @@ function MapAlignment(alignment: ColumnAlignment): GridColumnAlign { export class GridColumn { private _label : string = null; private grid: EasyGrid; + private _description: string = null; public readonly dataColumn: DataColumn; @@ -77,6 +78,7 @@ export class GridColumn { } this.cellRenderer = this.grid.cellRendererStore.getDefaultRenderer(column.type); + this._description = column.description; } else if (isRowNum) { this.isRowNum = true; @@ -99,6 +101,11 @@ export class GridColumn { this._label = this.label; } + /** Get column description. */ + public get description(): string { + return this._description; + } + public get type(): DataType { return this.dataColumn ? this.dataColumn.type : null; } diff --git a/easydata.net/src/EasyData.Core/EasyDataResultSet.cs b/easydata.net/src/EasyData.Core/EasyDataResultSet.cs index 59f56845..3296c96c 100644 --- a/easydata.net/src/EasyData.Core/EasyDataResultSet.cs +++ b/easydata.net/src/EasyData.Core/EasyDataResultSet.cs @@ -7,8 +7,8 @@ namespace EasyData { - public enum ColumnAlignment - { + public enum ColumnAlignment + { None, Left, Center, @@ -23,22 +23,48 @@ public class EasyDataColStyle public class EasyDataColDesc { + /// + /// Represents internal property id. + /// public string Id { get; set; } + /// + /// Index of property. + /// public int Index { get; set; } public bool IsAggr { get; set; } + /// + /// Name to use for this property in the UI. + /// public string Label { get; set; } + /// + /// Detailed description of the property. + /// + public string Description { get; set; } + + /// + /// The type of data represented by property. + /// public DataType DataType { get; set; } + /// + /// Represents internal property id. + /// public string AttrId { get; set; } + /// + /// The display format for the property. + /// public string DisplayFormat { get; set; } public string GroupFooterColumnTemplate { get; set; } + /// + /// The style of the property to display in UI. + /// public EasyDataColStyle Style { get; set; } @@ -46,34 +72,64 @@ public class EasyDataColDesc public class EasyDataCol { + /// + /// Represents internal property id. + /// [JsonProperty("id")] - public string Id { get; } + public string Id { get; } + /// + /// Index of property. + /// [JsonIgnore] - public int Index { get; } + public int Index { get; } [JsonProperty("isAggr")] - public bool IsAggr { get; } + public bool IsAggr { get; } + /// + /// Name to use for this property in the UI. + /// [JsonProperty("label")] public string Label { get; set; } + /// + /// Detailed description of the property. + /// + [JsonProperty("description")] + public string Description { get; set; } + + /// + /// The type of data represented by property. + /// [Obsolete("Use DataType instead")] [JsonIgnore] public DataType Type => DataType; + /// + /// The type of data represented by property. + /// [JsonProperty("type")] public DataType DataType { get; } + /// + /// Represents original internal property id. + /// [JsonProperty("originAttrId")] public string OrginAttrId { get; } + /// + /// The display format for the property. + /// [JsonProperty("dfmt")] public string DisplayFormat { get; set; } [JsonProperty("gfct")] public string GroupFooterColumnTemplate { get; set; } + /// + /// The style of the property to display in UI. + /// [JsonProperty("style")] public EasyDataColStyle Style { get; } @@ -85,6 +141,7 @@ public EasyDataCol(EasyDataColDesc desc) IsAggr = desc.IsAggr; OrginAttrId = desc.AttrId; Label = desc.Label; + Description = desc.Description; DataType = desc.DataType; DisplayFormat = desc.DisplayFormat; GroupFooterColumnTemplate = desc.GroupFooterColumnTemplate; @@ -101,7 +158,8 @@ public EasyDataRow(IEnumerable collection) : base(collection) } } - public interface IEasyDataResultSet { + public interface IEasyDataResultSet + { /// /// Gets columns /// @@ -114,7 +172,7 @@ public interface IEasyDataResultSet { } - public class EasyDataResultSet: IEasyDataResultSet + public class EasyDataResultSet : IEasyDataResultSet { [JsonProperty("cols")] public List Cols { get; } = new List(); @@ -125,4 +183,4 @@ public class EasyDataResultSet: IEasyDataResultSet IEnumerable IEasyDataResultSet.Rows => Rows; } -} +} \ No newline at end of file diff --git a/easydata.net/src/EasyData.EntityFrameworkCore.Relational/Services/EasyDataManagerEF.cs b/easydata.net/src/EasyData.EntityFrameworkCore.Relational/Services/EasyDataManagerEF.cs index 3c02a2cb..4e8bf874 100644 --- a/easydata.net/src/EasyData.EntityFrameworkCore.Relational/Services/EasyDataManagerEF.cs +++ b/easydata.net/src/EasyData.EntityFrameworkCore.Relational/Services/EasyDataManagerEF.cs @@ -87,7 +87,8 @@ public override async Task GetEntitiesAsync(string modelId, Label = DataUtils.PrettifyName(prop.Name), AttrId = attr?.Id, DisplayFormat = dfmt, - DataType = dataType + DataType = dataType, + Description = attr.Description })); } From 2d89dd17b5cd8a6610e9383affdabc272730ad69 Mon Sep 17 00:00:00 2001 From: Vasily Chefonov <81791194+vasiliy-chefonov@users.noreply.github.com> Date: Tue, 16 Nov 2021 13:51:13 +0300 Subject: [PATCH 2/9] Add type checking --- easydata.js/packs/ui/src/grid/easy_grid.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easydata.js/packs/ui/src/grid/easy_grid.ts b/easydata.js/packs/ui/src/grid/easy_grid.ts index 71e11363..f3959d68 100644 --- a/easydata.js/packs/ui/src/grid/easy_grid.ts +++ b/easydata.js/packs/ui/src/grid/easy_grid.ts @@ -678,7 +678,7 @@ export class EasyGrid { let result: any = {} for (const colId of group.columns) { let keyVal = row.getValue(colId); - if (caseInsensitive) { + if (caseInsensitive && typeof(keyVal) === 'string') { keyVal = keyVal.toLowerCase(); } result[colId] = keyVal; From f7895a54be230bb122a615016c79c92410e6c876 Mon Sep 17 00:00:00 2001 From: Vasily Chefonov <81791194+vasiliy-chefonov@users.noreply.github.com> Date: Thu, 18 Nov 2021 17:30:00 +0300 Subject: [PATCH 3/9] Refactor and change description naming --- .../packs/core/src/data/data_column.ts | 4 ++-- .../crud/src/form/entity_form_builder.ts | 20 +++++++++---------- .../src/EasyData.Core/EasyDataResultSet.cs | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/easydata.js/packs/core/src/data/data_column.ts b/easydata.js/packs/core/src/data/data_column.ts index f6952277..f88f8c75 100644 --- a/easydata.js/packs/core/src/data/data_column.ts +++ b/easydata.js/packs/core/src/data/data_column.ts @@ -30,7 +30,7 @@ export interface DataColumnDescriptor { /** The style of the column to display in UI. */ style?: DataColumnStyle; /** Detailed description of the column. */ - description: string | null; + desc: string | null; } export class DataColumn { @@ -78,7 +78,7 @@ export class DataColumn { this.displayFormat = desc.dfmt; this.groupFooterColumnTemplate = desc.gfct; this.style = desc.style || {}; - this.description = desc.description; + this.description = desc.desc; } } diff --git a/easydata.js/packs/crud/src/form/entity_form_builder.ts b/easydata.js/packs/crud/src/form/entity_form_builder.ts index e247638c..4a1a5e2b 100644 --- a/easydata.js/packs/crud/src/form/entity_form_builder.ts +++ b/easydata.js/packs/crud/src/form/entity_form_builder.ts @@ -394,41 +394,41 @@ export class EntityEditFormBuilder { .addChild('label', b => b .attr('for', attr.id) .addHtml(`${attr.caption} ${required ? '*' : ''}: `) - ).addChild('div'); - - let fieldHolder = parent.lastChild as HTMLElement; + ); + + const fieldSlot = domel('div', parent).toDOM() if (attr.kind === EntityAttrKind.Lookup) { - this.setupLookupField(fieldHolder, attr, readOnly, value); + this.setupLookupField(fieldSlot, attr, readOnly, value); } else { switch (editor.tag) { case EditorTag.DateTime: - this.setupDateTimeField(fieldHolder, attr, readOnly, value); + this.setupDateTimeField(fieldSlot, attr, readOnly, value); break; case EditorTag.List: - this.setupListField(fieldHolder, attr, readOnly, editor.values, value); + this.setupListField(fieldSlot, attr, readOnly, editor.values, value); break; case EditorTag.File: - this.setupFileField(fieldHolder, attr, readOnly, editor.accept); + this.setupFileField(fieldSlot, attr, readOnly, editor.accept); break; case EditorTag.Edit: default: if (editor.multiline) { - this.setupTextArea(fieldHolder, attr, readOnly, value); + this.setupTextArea(fieldSlot, attr, readOnly, value); } else { - this.setupTextField(fieldHolder, attr, readOnly, value); + this.setupTextField(fieldSlot, attr, readOnly, value); } break; } } if (attr.description) { - domel(fieldHolder).addChild('small', b => b + domel(fieldSlot).addChild('small', b => b .addText(attr.description) ); } diff --git a/easydata.net/src/EasyData.Core/EasyDataResultSet.cs b/easydata.net/src/EasyData.Core/EasyDataResultSet.cs index 3296c96c..9e2afd5f 100644 --- a/easydata.net/src/EasyData.Core/EasyDataResultSet.cs +++ b/easydata.net/src/EasyData.Core/EasyDataResultSet.cs @@ -96,7 +96,7 @@ public class EasyDataCol /// /// Detailed description of the property. /// - [JsonProperty("description")] + [JsonProperty("desc")] public string Description { get; set; } /// From c13df6e705d76f41d09c7f62774572ccf29719b7 Mon Sep 17 00:00:00 2001 From: Vasily Chefonov <81791194+vasiliy-chefonov@users.noreply.github.com> Date: Mon, 6 Dec 2021 17:13:34 +0300 Subject: [PATCH 4/9] Fix dosctrings and add help icon to the CRUD popup. --- .../packs/core/src/data/data_column.ts | 34 ++++++--- .../crud/src/form/entity_form_builder.ts | 73 ++++++++++--------- .../src/EasyData.Core/EasyDataResultSet.cs | 34 +++++---- 3 files changed, 80 insertions(+), 61 deletions(-) diff --git a/easydata.js/packs/core/src/data/data_column.ts b/easydata.js/packs/core/src/data/data_column.ts index f88f8c75..16fa5cac 100644 --- a/easydata.js/packs/core/src/data/data_column.ts +++ b/easydata.js/packs/core/src/data/data_column.ts @@ -15,38 +15,48 @@ export interface DataColumnStyle } export interface DataColumnDescriptor { - /** Represents internal column id. */ + /** Represents the internal column ID. */ id: string; - /** Represents original internal column id. */ + + /** Represents the ID of the metadata attribute this column is based on. */ originAttrId?: string; - /** The type of data represented by column. */ + + /** The type of data represented by the column. */ type?: DataType; - /** Name to use for this column in the UI. */ + + /** The label that is used for this column in UI. */ label: string; + + /** Indicates whether this column is an aggregate one. */ isAggr?: boolean; + /** The display format for the column. */ dfmt?: string; + gfct?: string; + /** The style of the column to display in UI. */ style?: DataColumnStyle; - /** Detailed description of the column. */ - desc: string | null; + + /** The detailed column description. */ + description: string | null; } export class DataColumn { - /** The type of data represented by column. */ + /** The type of data represented by the column. */ public readonly type: DataType; - /** Represents internal column id. */ + /** Represents the internal column ID. */ public readonly id: string; + /** Indicates whether this column is an aggregate one. */ public readonly isAggr: boolean; - /** Represents original internal column id. */ + /** Represents the ID of the metadata attribute this column is based on. */ public readonly originAttrId?: string; - /** Name to use for this column in the UI. */ + /** The label that is used for this column in UI. */ public label: string; /** The display format for the column. */ @@ -57,7 +67,7 @@ export class DataColumn { /** The style of the column to display in UI. */ public style?: DataColumnStyle; - /** Column description. */ + /** The column description. */ public description: string | null; constructor(desc: DataColumnDescriptor) { @@ -78,7 +88,7 @@ export class DataColumn { this.displayFormat = desc.dfmt; this.groupFooterColumnTemplate = desc.gfct; this.style = desc.style || {}; - this.description = desc.desc; + this.description = desc.description; } } diff --git a/easydata.js/packs/crud/src/form/entity_form_builder.ts b/easydata.js/packs/crud/src/form/entity_form_builder.ts index 4a1a5e2b..bb71fa66 100644 --- a/easydata.js/packs/crud/src/form/entity_form_builder.ts +++ b/easydata.js/packs/crud/src/form/entity_form_builder.ts @@ -391,46 +391,49 @@ export class EntityEditFormBuilder { } domel(parent) - .addChild('label', b => b - .attr('for', attr.id) - .addHtml(`${attr.caption} ${required ? '*' : ''}: `) + .addChild('label', b => { + b.attr('for', attr.id); + b.addHtml(`${attr.caption} ${required ? '*' : ''}: `); + + if (attr.description) { + b.addChild('div', b => b + .attr('title', attr.description) + .addClass('question-mark') + .setStyle('vertical-align', 'middle') + .setStyle('display', 'inline-block') + ); + } + } + ); - const fieldSlot = domel('div', parent).toDOM() - if (attr.kind === EntityAttrKind.Lookup) { - this.setupLookupField(fieldSlot, attr, readOnly, value); - } - else { - switch (editor.tag) { - case EditorTag.DateTime: - this.setupDateTimeField(fieldSlot, attr, readOnly, value); - break; - - case EditorTag.List: - this.setupListField(fieldSlot, attr, readOnly, editor.values, value); - break; - - case EditorTag.File: - this.setupFileField(fieldSlot, attr, readOnly, editor.accept); - break; - - case EditorTag.Edit: - default: - if (editor.multiline) { - this.setupTextArea(fieldSlot, attr, readOnly, value); - } - else { - this.setupTextField(fieldSlot, attr, readOnly, value); - } - break; - } + this.setupLookupField(parent, attr, readOnly, value); + return; } - if (attr.description) { - domel(fieldSlot).addChild('small', b => b - .addText(attr.description) - ); + switch (editor.tag) { + case EditorTag.DateTime: + this.setupDateTimeField(parent, attr, readOnly, value); + break; + + case EditorTag.List: + this.setupListField(parent, attr, readOnly, editor.values, value); + break; + + case EditorTag.File: + this.setupFileField(parent, attr, readOnly, editor.accept); + break; + + case EditorTag.Edit: + default: + if (editor.multiline) { + this.setupTextArea(parent, attr, readOnly, value); + } + else { + this.setupTextField(parent, attr, readOnly, value); + } + break; } } diff --git a/easydata.net/src/EasyData.Core/EasyDataResultSet.cs b/easydata.net/src/EasyData.Core/EasyDataResultSet.cs index 9e2afd5f..137b36ab 100644 --- a/easydata.net/src/EasyData.Core/EasyDataResultSet.cs +++ b/easydata.net/src/EasyData.Core/EasyDataResultSet.cs @@ -24,34 +24,37 @@ public class EasyDataColStyle public class EasyDataColDesc { /// - /// Represents internal property id. + /// Represents the internal column ID. /// public string Id { get; set; } /// - /// Index of property. + /// Represents the order number of this column among all columns in the result set. /// public int Index { get; set; } + /// + /// Indicates whether this column is an aggregate one. + /// public bool IsAggr { get; set; } /// - /// Name to use for this property in the UI. + /// The label that is used for this column in UI. /// public string Label { get; set; } /// - /// Detailed description of the property. + /// The detailed column description. /// public string Description { get; set; } /// - /// The type of data represented by property. + /// The type of data represented by the property. /// public DataType DataType { get; set; } /// - /// Represents internal property id. + /// Represents internal property ID. /// public string AttrId { get; set; } @@ -73,47 +76,50 @@ public class EasyDataColDesc public class EasyDataCol { /// - /// Represents internal property id. + /// Represents the internal column ID. /// [JsonProperty("id")] public string Id { get; } /// - /// Index of property. + /// Represents the order number of this column among all columns in the result set. /// [JsonIgnore] public int Index { get; } + /// + /// Indicates whether this column is an aggregate one. + /// [JsonProperty("isAggr")] public bool IsAggr { get; } /// - /// Name to use for this property in the UI. + /// The label that is used for this column in UI. /// [JsonProperty("label")] public string Label { get; set; } /// - /// Detailed description of the property. + /// The detailed column description. /// - [JsonProperty("desc")] + [JsonProperty("description")] public string Description { get; set; } /// - /// The type of data represented by property. + /// The type of data represented by the property. /// [Obsolete("Use DataType instead")] [JsonIgnore] public DataType Type => DataType; /// - /// The type of data represented by property. + /// The type of data represented by the property. /// [JsonProperty("type")] public DataType DataType { get; } /// - /// Represents original internal property id. + /// Represents the ID of the metadata attribute this column is based on. /// [JsonProperty("originAttrId")] public string OrginAttrId { get; } From 699362683c8cef62d8e02245d31c2552d70faa25 Mon Sep 17 00:00:00 2001 From: Vasily Chefonov Date: Tue, 1 Mar 2022 17:41:40 +0300 Subject: [PATCH 5/9] Add bulk deleting API Handler and update data manager --- .../Middleware/EasyDataApiHandler.cs | 18 +++++++++ .../Middleware/EasyDataMiddleware.cs | 7 +++- .../EasyData.Core/Services/EasyDataManager.cs | 9 +++++ .../Services/EasyDataManagerEF.cs | 39 +++++++++++++++++++ 4 files changed, 72 insertions(+), 1 deletion(-) 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..994c8e81 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 pks, CancellationToken ct = default); + public abstract Task> GetDefaultSortersAsync(string modelId, string sourceId, CancellationToken ct = default); /// diff --git a/easydata.net/src/EasyData.EntityFrameworkCore.Relational/Services/EasyDataManagerEF.cs b/easydata.net/src/EasyData.EntityFrameworkCore.Relational/Services/EasyDataManagerEF.cs index ee951fa4..8cab9bfa 100644 --- a/easydata.net/src/EasyData.EntityFrameworkCore.Relational/Services/EasyDataManagerEF.cs +++ b/easydata.net/src/EasyData.EntityFrameworkCore.Relational/Services/EasyDataManagerEF.cs @@ -196,6 +196,45 @@ public override async Task DeleteRecordAsync(string modelId, string sourceId, JO await DbContext.SaveChangesAsync(ct); } + /// + public override async Task DeleteRecordsInBulkAsync(string modelId, string sourceId, JObject props, CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); + var entityType = GetCurrentEntityType(DbContext, sourceId); + var recordsPKs = GetRecordsPKs(props); + var recordsToDelete = new List(); + + foreach (var pk in recordsPKs) { + 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 GetRecordsPKs(JObject fields) + { + foreach (var keyValue in fields) { + if (keyValue.Key.Equals("pks")) { + return keyValue.Value.ToObject(); + } + } + + throw new Exception("Primary keys were not found."); + } + + private static IEntityType GetCurrentEntityType(DbContext dbContext, string sourceId) { var entityType = dbContext.Model.GetEntityTypes() From c005e9106947871fd30f7c4d88c11f6b39742510 Mon Sep 17 00:00:00 2001 From: Vasily Chefonov Date: Tue, 1 Mar 2022 17:44:13 +0300 Subject: [PATCH 6/9] Update grid ui to delete in bulk --- .../packs/crud/src/i18n/text_resources.ts | 5 +- .../packs/crud/src/main/data_context.ts | 27 +++- .../packs/crud/src/views/entity_data_view.ts | 65 ++++++++- easydata.js/packs/ui/assets/css/easy-grid.css | 15 +- easydata.js/packs/ui/src/grid/easy_grid.ts | 133 ++++++++++++++---- .../packs/ui/src/grid/easy_grid_columns.ts | 20 ++- .../packs/ui/src/grid/easy_grid_events.ts | 8 +- .../packs/ui/src/grid/easy_grid_options.ts | 6 +- 8 files changed, 239 insertions(+), 40 deletions(-) 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..3aa2f773 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: any, 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..bbf82a25 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,58 @@ 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) => { + rows.forEach(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; + }, {}); + + recordKeys.push(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..4a200739 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,83 @@ 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'); + + let indices: number[] = []; + + checkboxes.forEach(checkbox => { + indices.push(parseInt(checkbox.closest('div.keg-row').getAttribute('data-row-idx'))); + }); + return indices; } @@ -1372,11 +1457,11 @@ export class EasyGrid { maxWidth += 3; - const maxOption = column.isRowNum + const maxOption = column.isRowNum ? this.options.columnWidths.rowNumColumn.max || 500 : this.options.columnWidths[column.dataColumn.type].max || 2000; - const minOption = column.isRowNum + const minOption = column.isRowNum ? 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; From b1bc02e6d90706264662de412996c900f3a01570 Mon Sep 17 00:00:00 2001 From: Vasily Chefonov Date: Tue, 1 Mar 2022 17:55:58 +0300 Subject: [PATCH 7/9] Update tests with bulk delete --- .../test/EasyData.Core.Test/Services/EasyDataOptionsTests.cs | 5 +++++ 1 file changed, 5 insertions(+) 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(); From 1c4e9d09d7881054ccbb5fad84d293f08aae88ea Mon Sep 17 00:00:00 2001 From: Vasily Chefonov Date: Mon, 14 Mar 2022 17:36:34 +0300 Subject: [PATCH 8/9] Provide DTO to get primary keys --- .../EasyData.Core/Services/EasyDataManager.cs | 4 ++-- .../Models/BulkDeleteDTO.cs | 17 +++++++++++++++++ .../Services/EasyDataManagerEF.cs | 19 +++++++------------ 3 files changed, 26 insertions(+), 14 deletions(-) create mode 100644 easydata.net/src/EasyData.EntityFrameworkCore.Relational/Models/BulkDeleteDTO.cs diff --git a/easydata.net/src/EasyData.Core/Services/EasyDataManager.cs b/easydata.net/src/EasyData.Core/Services/EasyDataManager.cs index 994c8e81..d4133631 100644 --- a/easydata.net/src/EasyData.Core/Services/EasyDataManager.cs +++ b/easydata.net/src/EasyData.Core/Services/EasyDataManager.cs @@ -64,9 +64,9 @@ public abstract Task FetchDatasetAsync( /// /// Model Id. /// Entity type. - /// Primary keys of the records to delete in bulk. + /// Primary keys of the records to delete in bulk. /// Cancellation Token. - public abstract Task DeleteRecordsInBulkAsync(string modelId, string sourceId, JObject pks, CancellationToken ct = default); + 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 8cab9bfa..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 @@ -197,14 +199,14 @@ public override async Task DeleteRecordAsync(string modelId, string sourceId, JO } /// - public override async Task DeleteRecordsInBulkAsync(string modelId, string sourceId, JObject props, CancellationToken ct = default) + public override async Task DeleteRecordsInBulkAsync(string modelId, string sourceId, JObject primaryKeys, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); var entityType = GetCurrentEntityType(DbContext, sourceId); - var recordsPKs = GetRecordsPKs(props); + var recordsPrimaryKeys = GetRecordsPrimaryKeys(primaryKeys); var recordsToDelete = new List(); - foreach (var pk in recordsPKs) { + foreach (var pk in recordsPrimaryKeys) { var keys = GetKeys(entityType, pk); var record = FindRecord(DbContext, entityType.ClrType, keys.Values); @@ -223,18 +225,11 @@ public override async Task DeleteRecordsInBulkAsync(string modelId, string sourc /// /// Get primary keys of records from the request body. /// - private IEnumerable GetRecordsPKs(JObject fields) + private IEnumerable GetRecordsPrimaryKeys(JObject fields) { - foreach (var keyValue in fields) { - if (keyValue.Key.Equals("pks")) { - return keyValue.Value.ToObject(); - } - } - - throw new Exception("Primary keys were not found."); + return fields.ToObject().PrimaryKeys; } - private static IEntityType GetCurrentEntityType(DbContext dbContext, string sourceId) { var entityType = dbContext.Model.GetEntityTypes() From ebff4ab72fb851305eeece57fb44cfac60d115e0 Mon Sep 17 00:00:00 2001 From: Vasily Chefonov Date: Mon, 14 Mar 2022 17:37:50 +0300 Subject: [PATCH 9/9] Refactor by specifying types. --- easydata.js/packs/crud/src/main/data_context.ts | 2 +- .../packs/crud/src/views/entity_data_view.ts | 7 +++---- easydata.js/packs/ui/src/grid/easy_grid.ts | 13 +++++-------- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/easydata.js/packs/crud/src/main/data_context.ts b/easydata.js/packs/crud/src/main/data_context.ts index 3aa2f773..583dd95e 100644 --- a/easydata.js/packs/crud/src/main/data_context.ts +++ b/easydata.js/packs/crud/src/main/data_context.ts @@ -174,7 +174,7 @@ export class DataContext { * @param obj Instances primary keys. * @param sourceId Entity Id. */ - public bulkDeleteRecords(obj: any, sourceId?: string) { + public bulkDeleteRecords(obj: {[key: string]: object[]}, sourceId?: string) { const url = this.resolveEndpoint('BulkDeleteRecords', { sourceId: sourceId || this.activeEntity.id }); this.startProcess(); 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 bbf82a25..5761376d 100644 --- a/easydata.js/packs/crud/src/views/entity_data_view.ts +++ b/easydata.js/packs/crud/src/views/entity_data_view.ts @@ -264,16 +264,15 @@ export class EntityDataView { let recordKeys: object[] = []; Promise.all(promises).then((rows) => { - rows.forEach(row => { + 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; - }, {}); - - recordKeys.push(keys); + }, {}); + return keys; }); if (recordKeys.length == 0) { diff --git a/easydata.js/packs/ui/src/grid/easy_grid.ts b/easydata.js/packs/ui/src/grid/easy_grid.ts index 4a200739..112f2bb5 100644 --- a/easydata.js/packs/ui/src/grid/easy_grid.ts +++ b/easydata.js/packs/ui/src/grid/easy_grid.ts @@ -1281,12 +1281,9 @@ export class EasyGrid { */ private getSelectedRowsIds(): number[] { var checkboxes = document.querySelectorAll('div.keg-cell-value input[type="checkbox"]:checked'); - - let indices: number[] = []; - - checkboxes.forEach(checkbox => { - indices.push(parseInt(checkbox.closest('div.keg-row').getAttribute('data-row-idx'))); - }); + const indices: number[] = Array.from(checkboxes, checkbox => { + return parseInt(checkbox.closest('div.keg-row').getAttribute('data-row-idx')); + }) return indices; } @@ -1457,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;