From cac497103e16999fcb2b47d6804dddb641ab119d Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Thu, 25 Jun 2026 15:03:00 -0700 Subject: [PATCH 01/13] AB#32598 initial commit, still WIP --- .../Components/DataGridWidget/Default.js | 164 +++++++++++------- 1 file changed, 105 insertions(+), 59 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DataGridWidget/Default.js b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DataGridWidget/Default.js index 7fd001ce6c..f440b49e6c 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DataGridWidget/Default.js +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DataGridWidget/Default.js @@ -1,9 +1,61 @@ +function getDatagridActionsRowButtonTemplate(actions) { + if (actions.length === 0) return ''; + + if (actions.length === 1) { + if (actions.includes('EDIT')) + return ''; + if (actions.includes('DELETE')) + return ''; + return ''; + } + + let items = ''; + if (actions.includes('EDIT')) + items += '
  • '; + if (actions.includes('DELETE')) + items += '
  • '; + + return ``; +} + +// Function to set data attributes on the row +function setRowDataAttributes(row, rowIndex) { + row.attr('data-row-no', rowIndex); +} + +// Function to format currency as CAD +function formatDatagridCurrency(value) { + return new Intl.NumberFormat('en-CA', + { style: 'currency', currency: 'CAD', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value); +} + +// Function to check if a value is numeric +function isDatagridCellNumeric(value) { + return !Number.isNaN(value) && Number.isFinite(value); +} + +// Function to calculate sum for a specific column +function calculateDatagridColumnSum(table, columnIndex) { + let total = 0; + table.column(columnIndex).data().each(function (value) { + // Remove currency symbols and commas for numeric check + let cleanedValue = value.replace(/[^\d.-]/g, ''); + if (isNumeric(cleanedValue)) { + total += Number.parseFloat(cleanedValue); + } + }); + return total; +} + $(function () { const UIElements = { tables: $('.custom-dynamic-table'), tableSearches: $('.custom-tbl-search') }; - + let editDatagridRowModal = new abp.ModalManager({ viewUrl: '../Components/DataGrid/EditDataRowModal' }); @@ -19,23 +71,13 @@ $(function () { // Refresh any update table level attributes resetTableAttributes($(newRowNode), response); - // Create the edit button HTML and append it to the last cell of the new row - $(newRowNode).find('td:last').html(getEditRowButtonTemplate()); - - // Attach click event handler to the newly added button - $(newRowNode).find('.row-edit-btn').on('click', function () { - let button = this; // `this` refers to the button element - editDataRow(button); - }); + // Configure action buttons on the last cell of the new row + let fieldId = $(newRowNode).closest('table')[0].id; + configureActionButtonsForCell($(newRowNode).find('td:last')[0], getTableActions(fieldId)); abp.notify.success('Row added successfully.', 'New Row'); } - // Function to set data attributes on the row - function setRowDataAttributes(row, rowIndex) { - row.attr('data-row-no', rowIndex); - } - // Function to reset the table level attributes function resetTableAttributes(row, response) { let table = row.closest('table'); @@ -62,10 +104,10 @@ $(function () { function updateRow(table, dataToUpdate, rowIndex) { $.each(dataToUpdate, function (columnName, newValue) { let columnIndex = getColumnIndex(table, columnName); - if (columnIndex !== -1) { - table.cell(rowIndex, columnIndex).data(newValue); - } else { + if (columnIndex === -1) { console.warn('Column not found:', columnName); + } else { + table.cell(rowIndex, columnIndex).data(newValue); } }); @@ -105,18 +147,6 @@ $(function () { handleEditDatagridRowModalResult(response); }); - // Function to calculate sum for a specific column - function calculateColumnSum(table, columnIndex) { - let total = 0; - table.column(columnIndex).data().each(function (value) { - // Remove currency symbols and commas for numeric check - let cleanedValue = value.replace(/[^\d.-]/g, ''); - if (isNumeric(cleanedValue)) { - total += parseFloat(cleanedValue); - } - }); - return total; - } // Function to update totals function updateTotals(table, fieldId) { @@ -129,11 +159,11 @@ $(function () { let columnIndex = getColumnIndex(table, key); if (columnIndex !== -1) { - let total = calculateColumnSum(table, columnIndex); + let total = calculateDatagridColumnSum(table, columnIndex); // Update the input field with the calculated total if ($(this).data('field-type') === 'Currency') { - $(this).val(formatCurrency(total)); + $(this).val(formatDatagridCurrency(total)); } else { $(this).val(total); } @@ -141,17 +171,6 @@ $(function () { }); } - // Function to format currency as CAD - function formatCurrency(value) { - return new Intl.NumberFormat('en-CA', - { style: 'currency', currency: 'CAD', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value); - } - - // Function to check if a value is numeric - function isNumeric(value) { - return !isNaN(value) && isFinite(value); - } - // Function to get the index of a column by its key function getColumnIndex(table, key) { let headers = table.columns().header().toArray(); @@ -274,28 +293,37 @@ $(function () { } function configureButtons(fieldId) { - let options = ($(`#table-options-${fieldId}`).val()).split(','); + let options = new Set(($(`#table-options-${fieldId}`).val()).split(',')); // Always include ColumnVisibility button regardless of options - let availableOptions = actionButtons.filter(item => options.includes(item.id) || item.id === 'ColumnVisibility'); + let availableOptions = actionButtons.filter(item => options.has(item.id) || item.id === 'ColumnVisibility'); return availableOptions; } + function getTableActions(fieldId) { + let options = new Set(($(`#table-options-${fieldId}`).val()).split(',')); + let actions = ['EDIT']; + if (options.has('AddRecord')) actions.push('DELETE'); + return actions; + } + // Function to configure action buttons for a table cell - function configureActionButtonForCell(cell) { - cell.innerHTML = getEditRowButtonTemplate(); // Add edit button to each cell + function configureActionButtonsForCell(cell, actions) { + cell.innerHTML = getDatagridActionsRowButtonTemplate(actions); - // Attach click event handler to the newly added button $(cell).find('.row-edit-btn').on('click', function () { - let button = this; // `this` refers to the button element - editDataRow(button); + editDataRow(this); + }); + + $(cell).find('.row-delete-btn').on('click', function () { + deleteDataRow(this); }); } // Function to setup the actions column - function setupActionsColumn(table, columnIndex) { - table.column(columnIndex).header().innerHTML = 'Actions'; // Update column header if needed + function setupActionsColumn(table, columnIndex, actions) { + table.column(columnIndex).header().innerHTML = 'Actions'; table.column(columnIndex).nodes().each(function (cell) { - configureActionButtonForCell(cell); + configureActionButtonsForCell(cell, actions); }); } @@ -303,18 +331,15 @@ $(function () { // Move buttons to custom container table.buttons().container().prependTo(`#btn-container-${fieldId}`); - // Add edit buttons to the last column (Actions) + let actions = getTableActions(fieldId); + table.columns().every(function (index) { - if (index === table.columns().count() - 1) { // Check if it is the last column - setupActionsColumn(table, index); + if (index === table.columns().count() - 1) { + setupActionsColumn(table, index, actions); } }); } - function getEditRowButtonTemplate() { - return ''; - } - function editDataRow(button) { // Get the parent element of the button let row = $(button).closest('tr'); @@ -336,6 +361,27 @@ $(function () { }); } + function deleteDataRow(button) { + // Get the parent element of the button + let row = $(button).closest('tr'); + let rowDataSet = row[0].dataset; + + // Retrieve the data attributes from the element + let table = $(button).closest('table'); + let tableDataSet = table[0].dataset; + + abp.message.confirm( + 'Are you sure you want to delete this row?', + 'Delete Row', + function (confirmed) { + if (confirmed) { + // TODO: replace with real API call + console.log('Calling off to API to delete row', { fieldId: tableDataSet.fieldId, row: rowDataSet.rowNo }); + } + } + ); + } + PubSub.subscribe( 'worksheet_preview_datagrid_refresh', () => { From 2ce6700a4316cbae4df55291002f98f7f0f884ea Mon Sep 17 00:00:00 2001 From: Andre Goncalves Date: Fri, 26 Jun 2026 11:12:40 -0700 Subject: [PATCH 02/13] AB#32598 worksheet datagrid row delete for non-dynamic --- .../DataGrid/DataGridWriteService.cs | 16 +++ .../src/Unity.Flex.Web/Unity.Flex.Web.csproj | 4 + .../DataGridWidgetController.cs | 63 +++++++++- .../Components/DataGridWidget/Default.css | 38 ++++++ .../Components/DataGridWidget/Default.js | 71 +++++++++-- .../DataGrid/DataGridWriteServiceTests.cs | 119 ++++++++++++++++++ .../Unity.Flex.Application.Tests.csproj | 1 + 7 files changed, 298 insertions(+), 14 deletions(-) create mode 100644 applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Application.Tests/DataGrid/DataGridWriteServiceTests.cs diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/Components/DataGrid/DataGridWriteService.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/Components/DataGrid/DataGridWriteService.cs index 6a1440df97..7489f231e2 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/Components/DataGrid/DataGridWriteService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/Components/DataGrid/DataGridWriteService.cs @@ -279,6 +279,22 @@ internal async Task UpdateRowAsync(RowInputData rowInputDa }; } + internal async Task DeleteRowAsync(Guid valueId, uint row, Guid worksheetInstanceId) + { + var currentValue = await customFieldValueAppService.GetAsync(valueId); + var dataGridValue = DataGridServiceUtils.DeserializeDataGridValue(currentValue.CurrentValue); + if (dataGridValue == null) return; + + var dataGridRowsValue = DataGridServiceUtils.DeserializeDataGridRowsValue(dataGridValue.Value?.ToString()); + if (dataGridRowsValue == null || row >= (uint)dataGridRowsValue.Rows.Count) return; + + dataGridRowsValue.Rows.RemoveAt((int)row); + dataGridValue.Value = dataGridRowsValue; + + await customFieldValueAppService.ExplicitSetAsync(valueId, JsonSerializer.Serialize(dataGridValue)); + await customFieldValueAppService.SyncWorksheetInstanceValueAsync(worksheetInstanceId); + } + internal async Task>> GenerateKeyValueTypesAsync(Guid customFieldId, Dictionary? keyValuePairs) { var result = new List>(); diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Unity.Flex.Web.csproj b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Unity.Flex.Web.csproj index 349cb06a44..1b9333d47f 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Unity.Flex.Web.csproj +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Unity.Flex.Web.csproj @@ -43,6 +43,10 @@ + + + + true diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DataGridWidget/DataGridWidgetController.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DataGridWidget/DataGridWidgetController.cs index e128b73cdb..27e26c837f 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DataGridWidget/DataGridWidgetController.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DataGridWidget/DataGridWidgetController.cs @@ -1,14 +1,22 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; +using System.Threading.Tasks; +using Unity.Flex.Web.Pages.Flex; using Unity.Flex.Web.Views.Shared.Components.WorksheetInstanceWidget.ViewModels; +using Unity.Flex.Worksheets; +using Unity.Flex.WorksheetInstances; using Volo.Abp.AspNetCore.Mvc; namespace Unity.Flex.Web.Views.Shared.Components.DataGridWidget { [ApiExplorerSettings(IgnoreApi = true)] [Route("Flex/Widgets/DataGrid")] - public class DataGridWidgetController : AbpController + public class DataGridWidgetController( + ICustomFieldAppService customFieldAppService, + ICustomFieldValueAppService customFieldValueAppService, + DataGridWriteService dataGridWriteService) : AbpController { [HttpGet] [Route("Refresh")] @@ -33,5 +41,56 @@ public IActionResult Refresh(WorksheetFieldViewModel? fieldModel, worksheetInstanceId }); } + + [Authorize] + [HttpPost] + [Route("DeleteRow")] + public async Task DeleteRow( + Guid valueId, + Guid fieldId, + uint row, + Guid worksheetInstanceId, + Guid applicationId) + { + await dataGridWriteService.DeleteRowAsync(valueId, row, worksheetInstanceId); + return new OkObjectResult(new { fieldId, row, worksheetInstanceId }); + } + + [Authorize] + [HttpGet] + [Route("RefreshByField")] + public async Task RefreshByField( + Guid valueId, + Guid fieldId, + string modelName, + Guid worksheetId, + Guid worksheetInstanceId, + string uiAnchor) + { + var field = await customFieldAppService.GetAsync(fieldId); + var value = await customFieldValueAppService.GetAsync(valueId); + + var fieldModel = new WorksheetFieldViewModel + { + Id = field.Id, + Name = field.Name, + Label = field.Label, + Type = field.Type, + Order = field.Order, + Enabled = field.Enabled, + Definition = field.Definition, + CurrentValue = value.CurrentValue, + CurrentValueId = valueId, + UiAnchor = uiAnchor + }; + + return ViewComponent(typeof(DataGridWidget), new + { + fieldModel, + modelName, + worksheetId, + worksheetInstanceId + }); + } } } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DataGridWidget/Default.css b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DataGridWidget/Default.css index 856b7c5a22..99348aa925 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DataGridWidget/Default.css +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DataGridWidget/Default.css @@ -33,6 +33,17 @@ .custom-grid-container { display: flex; flex-direction: column; + position: relative; +} + +.grid-loading-overlay { + position: absolute; + inset: 0; + background: rgba(255, 255, 255, 0.75); + display: flex; + align-items: center; + justify-content: center; + z-index: 10; } .grid-position { @@ -61,4 +72,31 @@ color: #8a8886; margin-left: 4px; vertical-align: middle; +} + +.custom-dynamic-table .dropdown { + display: inline-block; +} + +.custom-dynamic-table .dropdown-content { + display: none; + position: fixed; + right: auto; + background-color: #f9f9f9; + min-width: 160px; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + z-index: 1039; + --bs-btn-active-color: var(--bc-colors-white-primary-500); + --bs-btn-active-bg: var(--bc-colors-blue-primary-500); +} + +.custom-dynamic-table .dropdown:hover .dropdown-content { + display: block; +} + +.custom-dynamic-table .dropdown-content .btn.fullWidth { + width: calc(100% - 16px); + display: block; + text-align: left; + margin: 10px 8px; } \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DataGridWidget/Default.js b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DataGridWidget/Default.js index f440b49e6c..32618ff9ed 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DataGridWidget/Default.js +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DataGridWidget/Default.js @@ -11,13 +11,13 @@ function getDatagridActionsRowButtonTemplate(actions) { let items = ''; if (actions.includes('EDIT')) - items += '
  • '; + items += ''; if (actions.includes('DELETE')) - items += '
  • '; + items += ''; - return `