From f2ac90d748afd4213444c0b2710b589979593ccc Mon Sep 17 00:00:00 2001 From: David Bright Date: Wed, 22 Apr 2026 13:09:18 -0700 Subject: [PATCH 1/3] Rebuild of branch changes from previous PR New Configuration Menu that merges multiple configurations into a single location. Primarily uses invoking components from their original location where possible. --- .../Settings/AISettingGroup/Default.cshtml | 2 - .../Components/Scoresheet/Scoresheet.js | 2 +- .../WorksheetListWidget/WorksheetList.js | 4 +- .../NotificationsSettingGroup/Default.cshtml | 4 - .../NotificationsSettingViewComponent.cs | 3 + .../UX2/Components/Topbar/Default.cshtml | 16 +- .../Localization/GrantManager/en.json | 29 +- .../Menus/GrantManagerMenus.cs | 1 + .../ConfigurationManagement/Index.cshtml | 273 +++++++++++ .../ConfigurationManagement/Index.cshtml.cs | 76 ++++ .../Pages/ConfigurationManagement/Index.css | 117 +++++ .../Pages/ConfigurationManagement/Index.js | 122 +++++ .../NotificationSettings.css | 73 +++ .../PaymentConfigurations.css | 92 ++++ .../PaymentConfigurations.js | 429 ++++++++++++++++++ .../ScoresheetConfiguration.js | 103 +++++ .../Settings/TagManagement/TagManagement.js | 14 +- .../TagManagementViewComponent.cshtml | 2 - 18 files changed, 1320 insertions(+), 42 deletions(-) create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.cshtml create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.cshtml.cs create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.css create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.js create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/NotificationSettings.css create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/PaymentConfigurations.css create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/PaymentConfigurations.js create mode 100644 applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/ScoresheetConfiguration.js diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Settings/AISettingGroup/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Settings/AISettingGroup/Default.cshtml index c1f8b5071b..0a83cf001b 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Settings/AISettingGroup/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Settings/AISettingGroup/Default.cshtml @@ -1,7 +1,5 @@ @model Unity.AI.Web.Views.Settings.AISettingGroup.AISettingViewModel - -

AI Configuration

diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/Scoresheet/Scoresheet.js b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/Scoresheet/Scoresheet.js index 0bae6d71a5..38a4ec7844 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/Scoresheet/Scoresheet.js +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/Scoresheet/Scoresheet.js @@ -97,7 +97,7 @@ $(function () { } function updatePreviewAccordion(sortedItems) { - const previewDiv = document.getElementById('preview'); + const previewDiv = document.getElementById('scoresheet-preview') || document.getElementById('preview'); if (sortedItems.length === 0) { previewDiv.innerHTML = '

No sections to display.

'; diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js index 07f07e5e0d..9713fcff91 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetListWidget/WorksheetList.js @@ -143,7 +143,7 @@ $(function () { function updatePreview() { let worksheets = $('button.accordion-button[aria-expanded=true]'); - const previewPane = $('#preview'); + const previewPane = $('#worksheet-preview').length ? $('#worksheet-preview') : $('#preview'); if (worksheets?.length > 0) { let worksheetId = worksheets[0].dataset.worksheetId; @@ -158,7 +158,7 @@ $(function () { .then(response => response.text()) .then(data => { previewPane.html(data); - $("#preview :input").prop("readonly", true); + previewPane.find(':input').prop('readonly', true); PubSub.publish('worksheet_preview_datagrid_refresh'); }) .catch(error => { diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.cshtml index 081a16a1f6..b246bb27c1 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.cshtml @@ -1,9 +1,5 @@ @model Unity.Notifications.Web.Views.Settings.NotificationsSettingGroup.NotificationsSettingViewModel - - - -

Notifications

diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/NotificationsSettingViewComponent.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/NotificationsSettingViewComponent.cs index 7796a666de..ff9a8bda3e 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/NotificationsSettingViewComponent.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/NotificationsSettingViewComponent.cs @@ -37,6 +37,9 @@ public override void ConfigureBundle(BundleConfigurationContext context) context .Files .AddIfNotContains("/Views/Settings/NotificationsSettingGroup/Default.js"); + context + .Files + .AddIfNotContains("/Views/Settings/NotificationsSettingGroup/InternalEmailGroups.js"); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Components/Topbar/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Components/Topbar/Default.cshtml index 9383cf230e..bcd69a27dd 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Components/Topbar/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Components/Topbar/Default.cshtml @@ -45,24 +45,16 @@ @if (CurrentTenant.Id != null) { Applicant Portal Configuration - } - @if (await FeatureChecker.IsEnabledAsync("Unity.Payments") && isAuthorizedForPaymentConfiguration) - { - Payments Configuration - } - @if (await FeatureChecker.IsEnabledAsync("Unity.Flex")) + } + @if (CurrentUser.IsInRole("system_admin") && await FeatureChecker.IsEnabledAsync("SettingManagement.Enable")) { - Scoresheets Configuration - Custom Fields Configuration + Configuration Management } @if (CurrentUser.IsInRole("ITOperations")) { Unity Admin } - @if (CurrentUser.IsInRole("system_admin") && await FeatureChecker.IsEnabledAsync("SettingManagement.Enable")) - { - Settings - } + Logout diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json index 89d5df054e..560172e365 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json @@ -18,6 +18,7 @@ "Menu:TenantManagement": "Tenants", "Menu:EndpointManagement": "Endpoints", "Menu:Applicants": "Applicants", + "Menu:ConfigurationManagement": "Configuration Management", "Welcome": "Welcome", "LongWelcomeMessage": "Welcome to Unity Grant Manager", @@ -102,20 +103,20 @@ "ReviewerList:Subtotal": "Subtotal", "ReviewerList:CloneAssessment": "Clone Assessment", - "AssessmentResultAttachments:Id": "#", - "AssessmentResultAttachments:DocumentName": "Document Name", - "AssessmentResultAttachments:UploadedDate": "Date", - "AssessmentResultAttachments:AttachedBy": "Attached by", - "ChefsAttachments:Title": "Submission Attachments", - "ChefsAttachments:Filter": "Filter", - "ChefsAttachments:Download": "Download", - "ChefsAttachments:GenerateSummaries": "Generate AI Summaries", - "ChefsAttachments:GenerateSummary": "Generate Summary", - "ChefsAttachments:HideAISummaries": "Hide AI Summaries", - "ChefsAttachments:ShowAISummaries": "Show AI Summaries", - "ChefsAttachments:HideSummaries": "Hide Summaries", - "ChefsAttachments:ShowSummaries": "Show Summaries", - "ChefsAttachments:NoSummariesAvailable": "No summaries available", + "AssessmentResultAttachments:Id": "#", + "AssessmentResultAttachments:DocumentName": "Document Name", + "AssessmentResultAttachments:UploadedDate": "Date", + "AssessmentResultAttachments:AttachedBy": "Attached by", + "ChefsAttachments:Title": "Submission Attachments", + "ChefsAttachments:Filter": "Filter", + "ChefsAttachments:Download": "Download", + "ChefsAttachments:GenerateSummaries": "Generate AI Summaries", + "ChefsAttachments:GenerateSummary": "Generate Summary", + "ChefsAttachments:HideAISummaries": "Hide AI Summaries", + "ChefsAttachments:ShowAISummaries": "Show AI Summaries", + "ChefsAttachments:HideSummaries": "Hide Summaries", + "ChefsAttachments:ShowSummaries": "Show Summaries", + "ChefsAttachments:NoSummariesAvailable": "No summaries available", "Enum:AssessmentState.IN_PROGRESS": "In Progress", "Enum:AssessmentState.IN_REVIEW": "Under Review by Team Lead", diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenus.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenus.cs index 5151503b1b..872b27f475 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenus.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Menus/GrantManagerMenus.cs @@ -19,5 +19,6 @@ public static class GrantManagerMenus public const string EndpointManagement = Prefix + ".EndpointManagement"; public const string AIReporting = Prefix + ".AIReporting"; public const string Applicants = Prefix + ".Applicants"; + public const string ConfigurationManagement = Prefix + ".ConfigurationManagement"; public const string UnityAdmin = Prefix + ".UnityAdmin"; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.cshtml new file mode 100644 index 0000000000..08ab554893 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.cshtml @@ -0,0 +1,273 @@ +@page +@using Microsoft.AspNetCore.Mvc.Localization +@using Unity.Flex.Web.Views.Shared.Components.WorksheetList +@using Unity.GrantManager.Localization +@using Unity.GrantManager.Web.Pages.ConfigurationManagement +@using Volo.Abp.AspNetCore.Mvc.UI.Layout +@model IndexModel + +@inject IHtmlLocalizer L +@inject IPageLayout PageLayout + +@{ + PageLayout.Content.MenuItemName = Unity.GrantManager.Web.Menus.GrantManagerMenus.ConfigurationManagement; + ViewBag.PageTitle = "Configuration Management"; +} + +@section styles { + + + @if (Model.ShowPayments) + { + + } + @if (Model.ShowNotifications) + { + + } + +} + +@section scripts { + + + @if (Model.ShowPayments) + { + + } + + + @if (Model.ShowNotifications) + { + + + } + @if (Model.ShowTags) + { + + } + @if (Model.ShowAI) + { + + } + +} + +
+ +
+
    + @if (Model.ShowNotifications) + { + + } + @if (Model.ShowPayments) + { + + } + @if (Model.ShowCustomFields) + { + + } + @if (Model.ShowScoresheets) + { + + } + @if (Model.ShowTags) + { + + } + @if (Model.ShowAI) + { + + } +
+
+ +
+ + @if (Model.ShowNotifications) + { +
+ @await Component.InvokeAsync(typeof(Unity.Notifications.Web.Views.Settings.NotificationsSettingGroup.NotificationsSettingViewComponent)) +
+ } + + + @if (Model.ShowPayments) + { +
+ + +
+
+

Payments

+
+ + + +
+ + + @if (Model.ShowPaymentAccountCoding) + { + + } + + + @if (Model.ShowPaymentSettings) + { + + } + +
+
+
+ } + + + @if (Model.ShowCustomFields) + { +
+ +
+
+
+ @await Component.InvokeAsync(typeof(WorksheetListWidget)) +
+
+
+
+ + +

Preview

+
+

No sections to display.

+
+
+
+
+
+
+ } + + + @if (Model.ShowScoresheets) + { +
+
+
+
+ @await Component.InvokeAsync("Scoresheet") +
+
+
+
+ + +

Preview

+
+
+
+
+
+
+
+ } + + + @if (Model.ShowTags) + { +
+ @await Component.InvokeAsync(typeof(Unity.GrantManager.Web.Views.Settings.TagManagement.TagManagementViewComponent)) +
+ } + + + @if (Model.ShowAI) + { +
+ @await Component.InvokeAsync(typeof(Unity.AI.Web.Views.Settings.AISettingGroup.AISettingViewComponent)) +
+ } +
+
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.cshtml.cs new file mode 100644 index 0000000000..b60b82d183 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.cshtml.cs @@ -0,0 +1,76 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Configuration; + +using System; +using System.Threading.Tasks; + +using Unity.AI.Permissions; +using Unity.GrantManager.Permissions; +using Unity.Modules.Shared; +using Unity.Notifications.Permissions; +using Unity.Payments.PaymentConfigurations; +using Unity.Payments.Permissions; + +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.Features; + +namespace Unity.GrantManager.Web.Pages.ConfigurationManagement; + +[Authorize(UnitySettingManagementPermissions.UserInterface)] +public class IndexModel( + IPaymentConfigurationAppService paymentConfigurationAppService, + IConfiguration configuration, + IFeatureChecker featureChecker, + IPermissionChecker permissionChecker) : GrantManagerPageModel +{ + public Guid? AccountCodingId { get; set; } + public string? PaymentIdPrefix { get; set; } + public string MaxFileSize { get; set; } = string.Empty; + + // Visibility flags + public bool ShowNotifications { get; set; } + public bool ShowPayments { get; set; } + public bool ShowPaymentAccountCoding { get; set; } + public bool ShowPaymentSettings { get; set; } + public bool ShowCustomFields { get; set; } + public bool ShowScoresheets { get; set; } + public bool ShowTags { get; set; } + public bool ShowAI { get; set; } + + public async Task OnGetAsync() + { + MaxFileSize = configuration["S3:MaxFileSize"] ?? ""; + + // Resolve feature + permission flags + ShowNotifications = await featureChecker.IsEnabledAsync("Unity.Notifications") + && await permissionChecker.IsGrantedAsync(NotificationsPermissions.Settings); + + bool isPaymentsFeatureEnabled = await featureChecker.IsEnabledAsync("Unity.Payments"); + bool isAuthorizedForPaymentConfiguration = await permissionChecker.IsGrantedAsync(UnitySettingManagementPermissions.ConfigurePayments); + ShowPayments = isPaymentsFeatureEnabled && isAuthorizedForPaymentConfiguration; + + ShowPaymentAccountCoding = ShowPayments + && await permissionChecker.IsGrantedAsync(PaymentsPermissions.Payments.Default); + ShowPaymentSettings = ShowPayments + && await permissionChecker.IsGrantedAsync(UnitySelector.Payment.Summary.Default); + + ShowCustomFields = await featureChecker.IsEnabledAsync("Unity.Flex"); + ShowScoresheets = await featureChecker.IsEnabledAsync("Unity.Flex"); + + ShowTags = await permissionChecker.IsGrantedAsync(UnitySelector.SettingManagement.Tags.Default); + + ShowAI = await featureChecker.IsEnabledAsync("Unity.AI.Scoring") + && await permissionChecker.IsGrantedAsync(AIPermissions.Configuration.ConfigureAI); + + // Load payment data only if visible + if (ShowPayments) + { + var paymentConfiguration = await paymentConfigurationAppService.GetAsync(); + if (paymentConfiguration != null) + { + AccountCodingId = paymentConfiguration.DefaultAccountCodingId; + PaymentIdPrefix = paymentConfiguration.PaymentIdPrefix; + } + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.css new file mode 100644 index 0000000000..3e2796dc65 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.css @@ -0,0 +1,117 @@ +/* Page layout: side menu + content side-by-side */ +.config-page-layout { + display: flex; + align-items: flex-start; + width: 100%; + gap: 0; +} + +#ConfigurationManagementSideMenu.side-menu { + position: sticky; + margin-top: 20px; + top: 100px; + flex-shrink: 0; + z-index: 10; +} + +#ConfigurationManagementSideMenu ul { + padding-left: 0; +} + +#ConfigurationManagementSideMenu li { + height: 40px; + padding: 10px; + min-width: 200px; + margin-top: 2px; + border-radius: 0 100em 100em 0; +} + +#ConfigurationManagementSideMenu .nav-item { + justify-content: left; +} + +.config-content { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + align-items: center; +} + +.config-management { + padding: 1rem; + max-height: 80vh; + overflow: auto; + margin-top: 20px; + width: 95%; +} + +.unity-app-main-container { + margin: auto; + display: flex; + align-items: flex-start; +} + +.hide { + display: none !important; +} + +/* Resizable split layout for Worksheets and Scoresheets */ +.config-split-container { + display: flex; + height: calc(80vh - 60px); + width: 100%; +} + +.config-split-left, +.config-split-right { + overflow-y: auto; +} + +.config-split-right { + width: 67%; + padding-left: 0; +} + +.config-split-left { + width: 33%; + padding-right: 0; +} + +.config-split-divider { + width: 10px; + background-color: #ccc; + cursor: col-resize; + border-radius: 10px; + flex-shrink: 0; +} + + .config-split-divider:hover { + background-color: #aaa; + } + +/* Preview panel (shared by Worksheets and Scoresheets) */ +.sticky-preview { + position: sticky; + top: 0; +} + +.preview-section { + display: flex; + flex-wrap: wrap; + flex-direction: row; + justify-content: space-evenly; + align-items: center; +} + + .preview-section:has(> :nth-child(1)) .preview-field { + flex: 0 0 99%; + } + + .preview-section:has(> :nth-child(2)) .preview-field { + flex: 0 0 45%; + } + + .preview-section:has(> :nth-child(3)) .preview-field { + flex: 0 0 30%; + } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.js new file mode 100644 index 0000000000..45bd4463e4 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.js @@ -0,0 +1,122 @@ +$(function () { + const menuItems = $('#ConfigurationManagementSideMenu .nav-item'); + const configSections = $('.config-section'); + + init(); + + function init() { + menuItems.on('click', menuItemClick); + + // Adjust DataTables when Payments internal tabs are shown + $('button[data-bs-toggle="tab"]').on('shown.bs.tab', function () { + adjustDataTables(); + }); + + // Auto-select the first visible menu item + const firstMenuItem = menuItems.first(); + if (firstMenuItem.length) { + firstMenuItem.addClass('active'); + const targetId = firstMenuItem.data('target'); + $('#' + targetId).removeClass('hide'); + } + + // Auto-activate the first Payment tab (if rendered) + const firstPaymentTab = $('#payments-nav-tab .nav-link').first(); + if (firstPaymentTab.length) { + firstPaymentTab.addClass('active'); + const targetPane = $(firstPaymentTab.data('bs-target')); + if (targetPane.length) { + targetPane.addClass('show active'); + } + } + } + + const splitRestoreMap = { + 'custom-fields-div': initResizableSplit('worksheet-split-container', 'worksheet-left', 'worksheet-divider', 'worksheet-right', 'worksheetSplitWidth'), + 'scoresheets-div': initResizableSplit('scoresheet-split-container', 'scoresheet-left', 'scoresheet-divider', 'scoresheet-right', 'scoresheetSplitWidth') + }; + + function menuItemClick(e) { + const clickedItem = $(e.currentTarget); + const targetId = clickedItem.data('target'); + + // Update active menu item + menuItems.removeClass('active'); + clickedItem.addClass('active'); + + // Toggle content sections + configSections.addClass('hide'); + $('#' + targetId).removeClass('hide'); + + // Restore split widths now that the section is visible + if (splitRestoreMap[targetId]) { + splitRestoreMap[targetId](); + } + + adjustDataTables(); + } + + function adjustDataTables() { + // Adjust any visible DataTables after tab/section switch + if (typeof accountCodingDataTable !== 'undefined' && accountCodingDataTable) { + accountCodingDataTable.columns.adjust().draw(); + } + if (typeof paymentSettingsDataTable !== 'undefined' && paymentSettingsDataTable) { + paymentSettingsDataTable.columns.adjust().draw(); + } + $.fn.dataTable.tables({ visible: true, api: true }).columns.adjust(); + } +}); +function initResizableSplit(containerId, leftId, dividerId, rightId, storageKey) { + const container = document.getElementById(containerId); + const leftDiv = document.getElementById(leftId); + const divider = document.getElementById(dividerId); + const rightDiv = document.getElementById(rightId); + + if (!container || !leftDiv || !divider || !rightDiv) { + return null; + } + + function restoreSavedWidth() { + const saved = localStorage.getItem(storageKey); + if (saved && container.clientWidth > 0) { + const containerWidth = container.clientWidth; + const percentage = Number.parseFloat(saved); + const leftWidth = containerWidth * percentage; + const rightWidth = containerWidth - leftWidth - divider.offsetWidth; + leftDiv.style.width = leftWidth + 'px'; + rightDiv.style.width = rightWidth + 'px'; + } + } + + function resize(e) { + const containerRect = container.getBoundingClientRect(); + const leftWidth = e.clientX - containerRect.left; + const rightWidth = containerRect.right - e.clientX - divider.offsetWidth; + + if (leftWidth > 200 && rightWidth > 200) { + leftDiv.style.width = leftWidth + 'px'; + rightDiv.style.width = rightWidth + 'px'; + + // Save as percentage for responsive recalculation + localStorage.setItem(storageKey, (leftWidth / container.clientWidth).toString()); + } + } + + divider.addEventListener('mousedown', function (e) { + e.preventDefault(); + + function stopResize() { + document.removeEventListener('mousemove', resize); + document.removeEventListener('mouseup', stopResize); + } + + document.addEventListener('mousemove', resize); + document.addEventListener('mouseup', stopResize); + }); + + // Recalculate on window resize (guard prevents no-op on hidden sections) + globalThis.addEventListener('resize', restoreSavedWidth); + + return restoreSavedWidth; +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/NotificationSettings.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/NotificationSettings.css new file mode 100644 index 0000000000..41c1a36acd --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/NotificationSettings.css @@ -0,0 +1,73 @@ +.v-scroll { + max-height: 600px; + overflow-y: auto; + padding-right: 10px; + scroll-behavior: smooth; +} + +.tui-editor-body { + min-height: 250px; +} + +.note-text { + font-size: 12px; +} + +/* Group Users Table - Push remove button to far right */ +#groupUsersTable td:last-child, +#createGroupUsersTable td:last-child { + text-align: right !important; +} + +#groupUsersTable th:last-child, +#createGroupUsersTable th:last-child { + width: 30px !important; + min-width: 30px !important; + max-width: 30px !important; +} + +/* Fix DataTable empty message positioning */ +#groupUsersTable td.dataTables_empty, +#createGroupUsersTable td.dataTables_empty, +#groupUsersTable td.dt-empty, +#createGroupUsersTable td.dt-empty { + text-align: center !important; +} + +/* Ensure table maintains proper width */ +#createGroupUsersTable { + width: 100% !important; +} + +/* Add User Button Styling */ +.btn-add-user { + background-color: white !important; + border: 2px solid var(--bs-primary) !important; + color: var(--bs-primary) !important; + font-weight: 700; + padding: 0.25rem 0.5rem; + white-space: nowrap; + transition: all 0.15s ease-in-out; + border-radius: 4px; + font-size: 0.875rem; +} + + .btn-add-user:hover:not(:disabled) { + background-color: var(--bs-primary) !important; + color: white !important; + } + + .btn-add-user:disabled { + opacity: 0.5; + cursor: not-allowed; + border-color: var(--bs-secondary) !important; + color: var(--bs-secondary) !important; + } + + .btn-add-user i { + font-size: 0.8rem; + } + +.modal-footer { + margin-top: 10px; +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/PaymentConfigurations.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/PaymentConfigurations.css new file mode 100644 index 0000000000..07651b7b9e --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/PaymentConfigurations.css @@ -0,0 +1,92 @@ +/* PaymentConfigurations styles - local copy for ConfigurationManagement */ + +.readonly { + color: var(--bc-colors-grey-text-500); + background-color: var(--bc-colors-blue-100, #D8EAFD); + width: 332px !important; +} + +.currencyinput { + float: left; +} + +.currency { + text-align: right; + width: 100%; + margin-top: -31px; +} + +.dm-approval { + width: 100%; + max-width: 100%; +} + +.payment-form h4 { + padding: 4px; + font-weight: 700; +} + +.payment-form .mb-3:nth-child(-n+3) { + width: 30%; +} + +.payment-form .mb-3:nth-child(n+4) { + width: 45%; +} + +.payment-form .mb-3:nth-child(n+6) { + width: 100%; +} + +.submit-section { + display: inline-flex; + gap: 1rem; +} + +.note { + margin-left: 5px; +} + +.payment-prefix { + width: 200px; + margin-top: -7px; +} + +.field-validation-error { + float: inline-start; + display: block; + max-width: 100%; + width: 100%; +} + +#payments-div .modal-dialog { + max-width: 850px !important; + min-width: 850px !important; +} + +#payments-div input.form-control:focus, #payments-div input.form-control:active, #payments-div textarea.form-control:focus, #payments-div textarea.form-control:active, #payments-div .form-select:focus, #payments-div .form-select:active { + font-weight: inherit !important; +} + +#payment-settings-div { + width: 750px; +} + +#PaymentThreshold_Threshold { + text-align: right; +} + +label.display-input-label { + font-size: var(--bc-font-size-sm); + color: var(--bc-colors-grey-text-300); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-bottom: 0; +} + +@media only screen and (max-width: 850px) { + .field-validation-error { + float: none; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/PaymentConfigurations.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/PaymentConfigurations.js new file mode 100644 index 0000000000..8cd446f471 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/PaymentConfigurations.js @@ -0,0 +1,429 @@ +let accountCodingDataTable; +let paymentSettingsDataTable; + +$(function () { + let createModal = new abp.ModalManager(abp.appPath + 'AccountCoding/CreateModal'); + let updateModal = new abp.ModalManager(abp.appPath + 'AccountCoding/UpdateModal'); + let updateThresholdModal = new abp.ModalManager(abp.appPath + 'PaymentThresholds/UpdateModal'); + const formatter = createNumberFormatter(); + + const l = abp.localization.getResource('GrantManager'); + toastr.options.positionClass = 'toast-top-center'; + + const UIElements = { + accountCodingDT: $('#AccountCodesDataTable'), + paymentSettingsDT: $('#PaymentSettingsDataTable'), + accountCodingId: $('#AccountCodingId'), + paymentPrefixSaveButton: $('#PaymentPrefixSaveButton'), + paymentPrefixDiscardButton: $('#PaymentPrefixDiscardButton'), + paymentPrefixInput: $('#payment-id-prefix'), + originalPaymentPrefix: $('#payment-id-prefix-original') + }; + + init(); + + function init() { + const hasAccountCodesDataTable = UIElements.accountCodingDT.length > 0; + const hasPaymentSettingsDataTable = UIElements.paymentSettingsDT.length > 0; + if (!hasAccountCodesDataTable && !hasPaymentSettingsDataTable) { + return; + } + if (hasAccountCodesDataTable) { + accountCodingDataTable = initializeAccountCodesDataTable(); + } + if (hasPaymentSettingsDataTable) { + paymentSettingsDataTable = initializePaymentSettingsDataTable(); + } + bindUIElements(); + } + + function bindUIElements() { + UIElements.paymentPrefixSaveButton.on('click', updatePaymentPrefix); + UIElements.paymentPrefixDiscardButton.on('click', discardPaymentPrefix); + UIElements.paymentPrefixInput.on('keyup', checkEnableDiscard); + } + + function bindModalElements() { + const UIElements = { + inputMinistryClient: $('input[name="AccountCoding.MinistryClient"]'), + inputResponsibility: $('input[name="AccountCoding.Responsibility"]'), + inputServiceLine: $('input[name="AccountCoding.ServiceLine"]'), + inputStob: $('input[name="AccountCoding.Stob"]'), + inputProjectNumber: $('input[name="AccountCoding.ProjectNumber"]'), + inputPaymentThreshold: $('#PaymentThreshold_Threshold'), + readOnlyAccountCoding: $('#account-coding') + }; + + UIElements.inputMinistryClient.on('keyup', setAccountCodingDisplay); + UIElements.inputResponsibility.on('keyup', setAccountCodingDisplay); + UIElements.inputServiceLine.on('keyup', setAccountCodingDisplay); + UIElements.inputStob.on('keyup', setAccountCodingDisplay); + UIElements.inputProjectNumber.on('keyup', setAccountCodingDisplay); + + UIElements.inputPaymentThreshold.on('keyup', preventDecimalKeyUp); + UIElements.inputPaymentThreshold.on('keypress', preventNonCurrencyKeyPress); + + + + + function setAccountCodingDisplay() { + let currentAccount = $(UIElements.inputMinistryClient).val() + "." + + $(UIElements.inputResponsibility).val() + "." + + $(UIElements.inputServiceLine).val() + "." + + $(UIElements.inputStob).val() + "." + + $(UIElements.inputProjectNumber).val(); + + $(UIElements.readOnlyAccountCoding).val(currentAccount); + } + + setAccountCodingDisplay(); + } + + function initializePaymentSettingsDataTable() { + let actionButtons = []; + const listColumns = getPaymentSettingsColumns(); + + const defaultVisibleColumns = [ + 'userName', + 'paymentThreshold', + 'description' + ]; + + let dt = UIElements.paymentSettingsDT; + return initializeDataTable({ + dt, + defaultVisibleColumns, + listColumns, + maxRowsPerPage: 25, + defaultSortColumn: { + name: 'userName', + dir: 'asc' + }, + dataEndpoint: unity.grantManager.payments.paymentSettings.getL2ApproversThresholds, + data: {}, + responseCallback: paymentSettingsResponseCallback, + actionButtons, + pagingEnabled: true, + reorderEnabled: false, + languageSetValues: {}, + dataTableName: 'PaymentSettingsDataTable', + dynamicButtonContainerId: 'dynamicButtonContainerId', + useNullPlaceholder: true, + disableColumnSelect: true, + externalSearchId: 'search-data-table' + }); + + + } + + function initializeAccountCodesDataTable() { + $.fn.dataTable.Buttons.defaults.dom.button.className = 'btn flex-none'; + let actionButtons = [ + { + text: ' ' + l('Common:Command:Create') + '', + titleAttr: l('Common:Command:Create'), + id: 'CreateButton', + className: 'btn-light rounded-1', + action: (e, dt, node, config) => createAccountCodingBtn(e) + } + ]; + + const listColumns = getAccountCodingColumns(); + + const defaultVisibleColumns = [ + 'ministryClient', + 'responsibility', + 'serviceLine', + 'stob', + 'projectNumber', + 'description', + 'defaultRadio', + 'rowActions', + ]; + + let dt = UIElements.accountCodingDT; + return initializeDataTable({ + dt, + defaultVisibleColumns, + listColumns, + maxRowsPerPage: 25, + defaultSortColumn: { + name: 'ministryClient', + dir: 'asc' + }, + dataEndpoint: unity.grantManager.payments.accountCoding.getList, + data: {}, + responseCallback: accountCodesResponseCallback, + actionButtons, + pagingEnabled: true, + reorderEnabled: false, + languageSetValues: {}, + dataTableName: 'AccountCodesDataTable', + dynamicButtonContainerId: 'dynamicButtonContainerId', + useNullPlaceholder: true, + disableColumnSelect: true, + externalSearchId: 'search-data-table' + }); + } + + function getAccountCodingColumns() { + let index = 0; + return [ + { + title: 'Ministry Client', + name: "ministryClient", + data: "ministryClient", + visible: true, + index: index++ + }, + { + title: 'Responsibility', + name: "responsibility", + data: "responsibility", + visible: true, + index: index++ + }, + { + title: 'Service Line', + name: "serviceLine", + data: "serviceLine", + visible: true, + index: index++ + }, + { + title: 'Stob', + name: "stob", + data: "stob", + visible: true, + index: index++ + }, + { + title: 'Project #', + name: "projectNumber", + data: "projectNumber", + visible: true, + index: index++ + }, + { + title: 'Description', + name: "description", + data: "description", + visible: true, + index: index++ + }, + { + title: 'Default', + orderable: false, + visible: true, + className: 'notexport text-center', + name: 'defaultRadio', + index: index++, + data: 'id', + render: function (data, type, full, meta) { + let checked = UIElements.accountCodingId.val() == data ? 'checked' : ''; + return ``; + } + }, + { + title: 'Action', + orderable: false, + sortable: false, + data: 'id', + className: 'notexport text-center', + name: 'rowActions', + visible: true, + index: index++, + rowAction: { + items: + [ + { + text: 'Edit', + action: (data) => editAccountCodingBtn(data.record.id) + } + ] + } + } + ]; + } + + createModal.onResult(function () { + accountCodingDataTable.ajax.reload(); + }); + + updateModal.onResult(function () { + accountCodingDataTable.ajax.reload(); + }); + + updateThresholdModal.onResult(function () { + paymentSettingsDataTable.ajax.reload(); + }); + + function editThresholdBtn(id, userName) { + updateThresholdModal.open({ id: id, userName: userName }); + updateThresholdModal.onOpen(function () { + bindModalElements(); + }); + } + + function editAccountCodingBtn(id) { + updateModal.open({ id: id }); + updateModal.onOpen(function () { + bindModalElements(); + }); + } + + function createAccountCodingBtn(e) { + e.preventDefault(); + createModal.open(); + createModal.onOpen(function () { + bindModalElements(); + }); + } + + function updatePaymentPrefix() { + unity.payments.paymentConfigurations.paymentConfiguration.updatePaymentPrefix(UIElements.paymentPrefixInput.val()) + .done(function () { + toastr.success('Payment prefix updated successfully.'); + $('#payment-id-prefix-original').val(UIElements.paymentPrefixInput.val()); + checkEnableDiscard(); + }) + .fail(function () { + toastr.error('Failed to update payment prefix.'); + }); + } + + function checkEnableDiscard() { + const originalPrefix = UIElements.originalPaymentPrefix.val(); + const currentPrefix = UIElements.paymentPrefixInput.val(); + UIElements.paymentPrefixDiscardButton.prop('disabled', currentPrefix === originalPrefix); + } + + function discardPaymentPrefix() { + UIElements.paymentPrefixInput.val(UIElements.originalPaymentPrefix.val()); + toastr.info('Payment prefix changes discarded.'); + checkEnableDiscard(); + } + + function getPaymentSettingsColumns() { + let index = 0; + return [ + { + title: 'Id', + name: "id", + data: "id", + visible: false, + index: index++ + }, + { + title: 'User Id', + name: "userId", + data: "userId", + visible: false, + index: index++ + }, + { + title: 'Expense Authority', + name: "userName", + data: "userName", + visible: true, + index: index++ + }, + { + title: 'Approval Threshold', + name: "paymentThreshold", + className: 'dt-body-right', + data: "threshold", + visible: true, + index: index++, + render: function (data, type, row) { + if (data == null || data === '') return ''; + return formatter.format(data); + } + }, + { + title: 'Description', + name: "description", + data: "description", + visible: true, + index: index++ + }, + { + title: 'Action', + orderable: false, + sortable: false, + data: 'id', + className: 'notexport text-center', + name: 'rowActions', + visible: true, + index: index++, + rowAction: { + items: + [ + { + text: 'Edit', + action: (data) => editThresholdBtn(data.record.id, data.record.userName) + } + ] + } + } + ]; + } +}); + +function paymentSettingsResponseCallback(result) { + return { + recordsTotal: result.length, + recordsFiltered: result.length, + data: result + }; +} + +function accountCodesResponseCallback(result) { + return { + recordsTotal: result.totalCount, + recordsFiltered: result.items.length, + data: result.items + }; +} + +function clearFilter() { + $('#search-data-table').val(''); + $('#search-data-table').trigger("keyup"); +} + +function handleDefaultAccountCodeRadioClick(id) { + $('#AccountCodingId').val(id); + unity.payments.paymentConfigurations.paymentConfiguration.setDefaultAccountCode(id).done(function () { + toastr.success('Successfully set default account code. Reloading account codes.'); + clearAccountCodesSearchAndReload(); + }).fail(function () { + toastr.error('Failed to set default account code.'); + }); +} + +function clearAccountCodesSearchAndReload() { + clearFilter(); + accountCodingDataTable.search('').draw(); + + localStorage.removeItem('DataTables_AccountCodesDataTable_/ConfigurationManagement'); + localStorage.removeItem('DataTables_PaymentSettingsDataTable_/ConfigurationManagement'); + + accountCodingDataTable.ajax.reload(); +} + +function preventNonCurrencyKeyPress(e) { + if (/[a-zA-Z]/.test(e.key) || e.key === ' ' || e.key === '-' || e.keyCode === 45) { + e.preventDefault(); + } +} + +function preventDecimalKeyUp(e) { + const input = e.target; + const cursorPosition = input.selectionStart; + const decimalMatch = input.value.match(/\.(\d+)/); + + if (decimalMatch && decimalMatch[1].length > 2) { + input.value = input.value.replace(/\.(\d{2}).*/, '.$1'); + input.setSelectionRange(cursorPosition, cursorPosition); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/ScoresheetConfiguration.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/ScoresheetConfiguration.js new file mode 100644 index 0000000000..b60cbf7393 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/ScoresheetConfiguration.js @@ -0,0 +1,103 @@ +/* ScoresheetConfiguration JS - local copy for ConfigurationManagement */ + +(function ($) { + const scoresheetModal = new abp.ModalManager({ + viewUrl: 'ScoresheetConfiguration/ScoresheetModal' + }); + + const cloneScoresheetModal = new abp.ModalManager({ + viewUrl: 'ScoresheetConfiguration/CloneScoresheetModal' + }); + + const publishScoresheetModal = new abp.ModalManager({ + viewUrl: 'ScoresheetConfiguration/PublishScoresheetModal' + }); + + let scoresheetToEditId = null; + + scoresheetModal.onResult(function (response) { + const actionType = $(response.currentTarget).find('#ActionType').val(); + if (actionType.startsWith('Delete')) { + PubSub.publish('refresh_scoresheet_list', { scoresheetId: null }); + } else { + PubSub.publish('refresh_scoresheet_list', { scoresheetId: scoresheetToEditId }); + } + abp.notify.success( + actionType + ' is successful.', + 'Scoresheet' + ); + }); + + cloneScoresheetModal.onResult(function (response) { + PubSub.publish('refresh_scoresheet_list', { scoresheetId: null }); + abp.notify.success( + 'Scoresheet cloning is successful.', + 'Scoresheet' + ); + }); + + publishScoresheetModal.onResult(function (response) { + PubSub.publish('refresh_scoresheet_list', { scoresheetId: scoresheetToEditId }); + abp.notify.success( + 'Scoresheet publishing is successful.', + 'Scoresheet' + ); + }); + + // Exposed globally — called from inline onclick attributes in Scoresheet component HTML + window.openScoresheetModal = function (scoresheetId, actionType) { + scoresheetToEditId = scoresheetId; + scoresheetModal.open({ + scoresheetId: scoresheetId, + actionType: actionType + }); + }; + + window.openCloneScoresheetModal = function (scoresheetId) { + scoresheetToEditId = scoresheetId; + cloneScoresheetModal.open({ + scoresheetId: scoresheetId + }); + }; + + window.openPublishScoresheetModal = function (scoresheetId) { + scoresheetToEditId = scoresheetId; + publishScoresheetModal.open({ + scoresheetId: scoresheetId + }); + }; + + function showAccordion(scoresheetId) { + if (!scoresheetId) { + return; + } + const accordionId = 'collapse-' + scoresheetId; + const accordion = document.getElementById(accordionId); + accordion.classList.add('show'); + + const buttonId = 'accordion-button-' + scoresheetId; + const accordionButton = document.getElementById(buttonId); + accordionButton.classList.remove('collapsed'); + } + + function refreshScoresheetInfoWidget(scoresheetId) { + const url = `../Flex/Widget/Scoresheet/Refresh`; + fetch(url) + .then(response => response.text()) + .then(data => { + document.getElementById('scoresheet-info-widget').innerHTML = data; + showAccordion(scoresheetId); + PubSub.publish('refresh_scoresheet_configuration_page'); + }) + .catch(error => { + console.error('Error refreshing scoresheet-info-widget:', error); + }); + } + + PubSub.subscribe( + 'refresh_scoresheet_list', + (msg, data) => { + refreshScoresheetInfoWidget(data.scoresheetId); + } + ); +})(jQuery); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/TagManagement/TagManagement.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/TagManagement/TagManagement.js index 127d0257a0..3cfaf63254 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/TagManagement/TagManagement.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/TagManagement/TagManagement.js @@ -1,9 +1,7 @@ const TagTypes = {}; -let userCanUpdate = abp.auth.isGranted('Unity.GrantManager.SettingManagement.Tags.Update'); -let userCanDelete = abp.auth.isGranted('Unity.GrantManager.SettingManagement.Tags.Delete'); -let addNewTagModal = new abp.ModalManager({ - viewUrl: 'Tags/CreateTagsModal' -}); +let userCanUpdate; +let userCanDelete; +let addNewTagModal; function defineTagSummaryColumnDefs() { const columnDefs = [ @@ -162,6 +160,12 @@ function getUnifiedTagSummaryAjax(requestData, callback, settings) { }); } $(function () { + userCanUpdate = abp.auth.isGranted('Unity.GrantManager.SettingManagement.Tags.Update'); + userCanDelete = abp.auth.isGranted('Unity.GrantManager.SettingManagement.Tags.Delete'); + addNewTagModal = new abp.ModalManager({ + viewUrl: 'Tags/CreateTagsModal' + }); + abp.log.debug('TagManagement.js initialized!'); abp.modals.RenameTag = function () { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/TagManagement/TagManagementViewComponent.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/TagManagement/TagManagementViewComponent.cshtml index 6e0cb90dd0..861e5710d7 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/TagManagement/TagManagementViewComponent.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/TagManagement/TagManagementViewComponent.cshtml @@ -3,8 +3,6 @@ @inject IPermissionChecker PermissionChecker - -
From 5c7950be18357864cdeb60cffac1b02cd9287f64 Mon Sep 17 00:00:00 2001 From: David Bright Date: Wed, 22 Apr 2026 15:33:02 -0700 Subject: [PATCH 2/3] Implement consistent disabling of save buttons during async save to prevent duplicate operations, as well as additional re-enabling of buttons on failure or completion. Core method in zone-extensions.js for setSaving --- .../wwwroot/themes/ux2/zone-extensions.js | 20 +++++++++++++------ .../Pages/ApplicationForms/Mapping.js | 10 ++++++++++ .../Pages/GrantApplications/Details.js | 3 +++ .../Components/ApplicantContacts/Default.js | 2 ++ .../Components/ApplicantHistory/Default.js | 2 ++ .../Components/ApplicantInfo/Default.js | 3 +++ .../AssessmentScoresWidget/Default.js | 9 ++++++--- .../Shared/Components/EmailsWidget/Default.js | 2 ++ .../FundingAgreementInfo/Default.js | 4 ++++ .../PaymentConfiguration/Default.js | 5 +++++ .../Shared/Components/ProjectInfo/Default.js | 6 +++++- 11 files changed, 56 insertions(+), 10 deletions(-) diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/zone-extensions.js b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/zone-extensions.js index 1003da21fe..188fe000fc 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/zone-extensions.js +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/zone-extensions.js @@ -4,7 +4,7 @@ } /** - * Unflatten dot separated JSON objects into nested objects + * Unflatten dot separated JSON objects into nested objects */ $.fn.unflattenObject = function(flatObj) { const result = {}; @@ -268,6 +268,14 @@ class UnityChangeTrackingForm { this.saveButton.prop('disabled', this.modifiedFields.size === 0); } + setSaving(isSaving) { + if (isSaving) { + this.saveButton.prop('disabled', true); + } else { + this.updateSaveButtonState(); + } + } + /** * Reset tracking without changing values */ @@ -354,7 +362,7 @@ class UnityZoneForm extends UnityChangeTrackingForm { // NOTE Get field value by name or id isValid() { - return this.form.valid(); + return this.form.valid(); } initializeNumericFields() { @@ -441,10 +449,10 @@ class UnityZoneForm extends UnityChangeTrackingForm { let expandedProperties = { 'name': name, 'tag': this.tagName.toLowerCase(), - 'type': this.type - }; - - tableOutput = { ...tableOutput, ...expandedProperties }; + 'type': this.type + }; + + tableOutput = { ...tableOutput, ...expandedProperties }; } let changeProperties = { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.js index f24fa44924..1cc5e6af97 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.js @@ -136,6 +136,7 @@ let jsonText = $('#jsonText').val(); $.parseJSON(jsonText); let mappingJsonStr = jsonText.replace(/\s+/g, ' ').replace(/(\r\n|\n|\r)/gm, ""); + UIElements.btnSaveMapping.prop('disabled', true); handleSaveMapping($.parseJSON(mappingJsonStr)); handleCancelMapping(); @@ -150,6 +151,7 @@ } catch (err) { + UIElements.btnSaveMapping.prop('disabled', false); abp.notify.error( '', 'The JSON is not valid:' + err @@ -313,6 +315,7 @@ formData["availableChefsFields"] = document.getElementById('availableChefsFields').value; formData["ChefsApplicationFormGuid"] = document.getElementById('applicationFormId').value; + UIElements.btnSave.prop('disabled', true); $.ajax( { url: "/api/app/application-form-version/" + formVersionId, @@ -332,6 +335,9 @@ data.responseText, 'Mapping Not Saved Successful' ); + }, + complete: function () { + UIElements.btnSave.prop('disabled', false); } } ); @@ -640,6 +646,7 @@ }; btnSaveAIConfig.addEventListener('click', function () { + btnSaveAIConfig.disabled = true; abp.ajax({ url: `/api/app/application-form/${aiFormId}/ai-config`, type: 'PATCH', @@ -658,6 +665,9 @@ }) .fail(function () { abp.notify.error('Failed to save AI configuration.'); + }) + .always(function () { + btnSaveAIConfig.disabled = false; }); }); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js index 41ecd22540..d0d87af7c8 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js @@ -1305,6 +1305,9 @@ function updateCustomForm( .update(customFormUpdate) .done(function () { abp.notify.success('Information has been updated.'); + }) + .fail(function () { + $(`#${saveId}`).prop('disabled', false); }); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.js index de2d963e45..e59d9a1039 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantContacts/Default.js @@ -140,6 +140,7 @@ $(function () { } }; + zoneForm.setSaving(true); unity.grantManager.applicants.applicant .updateApplicantContactAddresses(applicantId, payload) .done(function () { @@ -149,6 +150,7 @@ $(function () { }) .fail(function () { abp.notify.error('Failed to update contact.'); + zoneForm.setSaving(false); }); }); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.js index 61450ef2d4..c9688e999e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.js @@ -23,6 +23,7 @@ $(function () { return; } + zoneForm.setSaving(true); unity.grantManager.applicantProfile.applicantHistory .saveNotes(applicantId, { fundingHistoryComments: $('#FundingHistoryComments').val(), @@ -35,6 +36,7 @@ $(function () { }) .fail(function () { abp.notify.error('Failed to save history notes.'); + zoneForm.setSaving(false); }); }); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.js index f85c5e42b4..b782da861c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.js @@ -90,6 +90,7 @@ abp.widgets.ApplicantInfo = function ($wrapper) { let applicationId = document.getElementById('ApplicantInfo_ApplicationId').value; let applicantInfoSubmission = self.getPartialUpdate(); + self.zoneForm.setSaving(true); try { unity.grantManager.grantApplications.applicationApplicant .updatePartialApplicantInfo(applicationId, applicantInfoSubmission) @@ -102,10 +103,12 @@ abp.widgets.ApplicantInfo = function ($wrapper) { .fail(function (error) { abp.notify.error('Failed to update Applicant Info.'); console.log(error); + self.zoneForm.setSaving(false); }); } catch (error) { abp.notify.error('An unexpected error occurred.'); console.log(error); + self.zoneForm.setSaving(false); } }); }, diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js index 428d691d21..fb6bcdc61c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js @@ -34,6 +34,8 @@ function saveScoresSection(formId, sectionId) { }; //Calls an enpoint and disabled buttons + secSaveButton.disabled = true; + secDiscardButton.disabled = true; unity.grantManager.assessments.assessment .saveScoresheetSectionAnswers(data) .done(function () { @@ -52,14 +54,15 @@ function saveScoresSection(formId, sectionId) { } } - secSaveButton.disabled = true; - secDiscardButton.disabled = true; - updateSubtotal(); PubSub.publish( 'refresh_review_list_without_sidepanel', assessmentId ); + }) + .fail(function () { + secSaveButton.disabled = false; + secDiscardButton.disabled = false; }); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js index fadcee3017..375a71bf30 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js @@ -334,6 +334,7 @@ templateName = $('#templateText').val(); } + UIElements.btnSave.prop('disabled', true); unity.grantManager.emails.email .saveDraft({ emailId: UIElements.inputEmailId[0].value, @@ -353,6 +354,7 @@ abp.notify.success('Your email has been saved.'); PubSub.publish('refresh_application_emails'); }).catch(function () { + UIElements.btnSave.prop('disabled', false); abp.notify.error('An error ocurred your email could not be saved.'); }); } else { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/FundingAgreementInfo/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/FundingAgreementInfo/Default.js index 285a05f607..f74ebbc6b9 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/FundingAgreementInfo/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/FundingAgreementInfo/Default.js @@ -49,6 +49,7 @@ } function updateFundingAgreementInfo(applicationId, fundingAgreementInfoObj) { + $('#saveFundingAgreementInfoBtn').prop('disabled', true); try { unity.grantManager.grantApplications.grantApplication .updateFundingAgreementInfo(applicationId, fundingAgreementInfoObj) @@ -59,6 +60,9 @@ $('#saveFundingAgreementInfoBtn').prop('disabled', true); PubSub.publish('funding_agreement_info_saved', fundingAgreementInfoObj); PubSub.publish('refresh_detail_panel_summary'); + }) + .fail(function () { + $('#saveFundingAgreementInfoBtn').prop('disabled', false); }); } catch (error) { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/PaymentConfiguration/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/PaymentConfiguration/Default.js index 44e7a40851..c9d3e5bbe8 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/PaymentConfiguration/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/PaymentConfiguration/Default.js @@ -197,6 +197,8 @@ return; } + UIElements.btnSave.prop('disabled', true); + const hierarchyValue = UIElements.formHierarchy.val(); const formHierarchy = hierarchyValue ? parseInt(hierarchyValue, 10) : null; const parentFormId = UIElements.parentFormSelect.val(); @@ -235,6 +237,9 @@ confirmButton: 'btn btn-primary' } }); + }) + .catch(() => { + UIElements.btnSave.prop('disabled', false); }); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.js index f2101f7c80..190f22e6fb 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.js @@ -90,6 +90,7 @@ abp.widgets.ProjectInfo = function ($wrapper) { data: modifiedFieldData }; + self.zoneForm.setSaving(true); try { unity.grantManager.grantApplications.grantApplication .updatePartialProjectInfo(applicationId, projectInfoSubmission) @@ -98,11 +99,14 @@ abp.widgets.ProjectInfo = function ($wrapper) { self.zoneForm.resetTracking(); PubSub.publish('project_info_saved', projectInfoObj); PubSub.publish('refresh_detail_panel_summary'); + }) + .fail(function () { + self.zoneForm.setSaving(false); }); } catch (error) { console.log(error); - self.zoneForm.resetTracking(); + self.zoneForm.setSaving(false); } }); From 77e148afaca5a89ff94ae8b425f3fbbae23c109a Mon Sep 17 00:00:00 2001 From: Jacob Smith Date: Wed, 22 Apr 2026 17:31:25 -0700 Subject: [PATCH 3/3] AB#32465 update OpenAI endpoint resolution config --- .../AI/Runtime/OpenAIConfigurationResolver.cs | 16 ++-------- .../Unity.GrantManager.Web/appsettings.json | 2 ++ .../OpenAIConfigurationResolverTests.cs | 29 +++++++++++++++++++ 3 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAIConfigurationResolverTests.cs diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs index 1f8da1b91b..25fb917d21 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AI/Runtime/OpenAIConfigurationResolver.cs @@ -139,18 +139,8 @@ private static string CombineEndpointAndPath(string endpoint, string profilePath { const char UrlPathSeparator = '/'; - if (Uri.TryCreate(profilePath, UriKind.Absolute, out var absoluteUri)) - { - return absoluteUri.ToString(); - } - - var trimmedEndpoint = endpoint.Trim().TrimEnd(UrlPathSeparator); - var trimmedPath = profilePath.Trim(); - if (!trimmedPath.StartsWith(UrlPathSeparator)) - { - trimmedPath = string.Concat(UrlPathSeparator, trimmedPath); - } - - return trimmedEndpoint + trimmedPath; + return endpoint.Trim().TrimEnd(UrlPathSeparator) + + UrlPathSeparator + + profilePath.Trim().TrimStart(UrlPathSeparator); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json index 50a4bfa585..b74c91d2f4 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/appsettings.json @@ -165,6 +165,8 @@ } }, "OpenAI": { + "ApiKey": "", + "Endpoint": "", "Profiles": { "Gpt4oMini": { "ApiUrl": "/openai/deployments/gpt-4o-mini/chat/completions?api-version=2024-02-01", diff --git a/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAIConfigurationResolverTests.cs b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAIConfigurationResolverTests.cs new file mode 100644 index 0000000000..aa95ac7b8a --- /dev/null +++ b/applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/AI/Runtime/OpenAIConfigurationResolverTests.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.Configuration; +using Shouldly; +using System.Collections.Generic; +using Unity.AI.Runtime; +using Xunit; + +namespace Unity.GrantManager.AI.Runtime; + +public class OpenAIConfigurationResolverTests +{ + [Fact] + public void ResolveApiUrl_Should_CombineEndpointWithLeadingSlashProfilePath() + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["Azure:Operations:Defaults:Provider"] = "OpenAI", + ["Azure:Operations:Defaults:Profile"] = "Gpt4oMini", + ["Azure:OpenAI:Endpoint"] = "https://d837ad-test-recap-webapp.azurewebsites.net", + ["Azure:OpenAI:Profiles:Gpt4oMini:ApiUrl"] = "/openai/deployments/gpt-4o-mini/chat/completions?api-version=2024-02-01" + }) + .Build(); + + var resolver = new OpenAIConfigurationResolver(configuration); + + resolver.ResolveApiUrl().ShouldBe( + "https://d837ad-test-recap-webapp.azurewebsites.net/openai/deployments/gpt-4o-mini/chat/completions?api-version=2024-02-01"); + } +}