diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/EditSectionDto.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/EditSectionDto.cs index 4da2682097..104c59d70b 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/EditSectionDto.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/EditSectionDto.cs @@ -6,5 +6,6 @@ namespace Unity.Flex.Worksheets public class EditSectionDto { public string Name { get; set; } = string.Empty; + public int? FieldWidth { get; set; } } } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/IWorksheetAppService.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/IWorksheetAppService.cs index 13f1168010..6b0c1dc8c3 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/IWorksheetAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/IWorksheetAppService.cs @@ -16,6 +16,7 @@ public interface IWorksheetAppService : IApplicationService Task EditAsync(Guid id, EditWorksheetDto dto); Task CloneAsync(Guid id); Task PublishAsync(Guid id); + Task ArchiveAsync(Guid id, bool archive); Task DeleteAsync(Guid id); Task GetLinkedFormsAsync(Guid worksheetId); Task ResequenceSectionsAsync(Guid id, uint oldIndex, uint newIndex); diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/WorksheetDto.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/WorksheetDto.cs index b4503bbd42..5dc6ffdccb 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/WorksheetDto.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/WorksheetDto.cs @@ -15,5 +15,6 @@ public class WorksheetDto : ExtensibleFullAuditedEntityDto public uint TotalSections { get; set; } = 0; public uint Version { get; set; } = 0; public bool Published { get; set; } = false; + public bool IsArchived { get; set; } = false; } } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/WorksheetSectionDto.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/WorksheetSectionDto.cs index 59f19bf6a5..87d5431352 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/WorksheetSectionDto.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/WorksheetSectionDto.cs @@ -9,6 +9,8 @@ public class WorksheetSectionDto : EntityDto { public string Name { get; set; } = string.Empty; public uint Order { get; set; } + public int? FieldWidth { get; set; } + public string? Definition { get; set; } public List Fields { get; set; } = []; } } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/Worksheet.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/Worksheet.cs index 34b24f6484..5a59baf8ad 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/Worksheet.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/Worksheet.cs @@ -16,6 +16,7 @@ public class Worksheet : FullAuditedAggregateRoot, IMultiTenant, IReportab public virtual string Title { get; private set; } = string.Empty; public virtual uint Version { get; private set; } = 1; public virtual bool Published { get; private set; } = false; + public virtual bool IsArchived { get; private set; } = false; public Guid? TenantId { get; set; } @@ -61,9 +62,10 @@ internal Worksheet CloneSection(WorksheetSection clonedSection) return this; } - public Worksheet UpdateSection(WorksheetSection section, string name) + public Worksheet UpdateSection(WorksheetSection section, string name, string? definition = null) { section.SetName(name); + section.SetDefinition(definition); return this; } @@ -88,6 +90,12 @@ public Worksheet SetPublished(bool published) return this; } + public Worksheet SetArchived(bool archived) + { + IsArchived = archived; + return this; + } + public Worksheet RemoveSection(WorksheetSection section) { Sections.Remove(section); diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/WorksheetSection.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/WorksheetSection.cs index be6fdef71b..b0770c0b20 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/WorksheetSection.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/Worksheets/WorksheetSection.cs @@ -1,5 +1,6 @@ using System; using System.Collections.ObjectModel; +using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Text.Json.Serialization; using Volo.Abp; @@ -26,6 +27,9 @@ public virtual Worksheet Worksheet public virtual string Name { get; private set; } = string.Empty; public virtual uint Order { get; private set; } + [Column(TypeName = "jsonb")] + public virtual string? Definition { get; private set; } + public virtual Collection Fields { get; private set; } = []; public Guid? TenantId { get; set; } @@ -75,6 +79,12 @@ public WorksheetSection SetOrder(uint order) return this; } + public WorksheetSection SetDefinition(string? definition) + { + Definition = definition; + return this; + } + public WorksheetSection RemoveField(CustomField field) { Fields.Remove(field); diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/FlexApplicationAutoMapperProfile.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/FlexApplicationAutoMapperProfile.cs index 1d972e8aa7..fbe39d8126 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/FlexApplicationAutoMapperProfile.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/FlexApplicationAutoMapperProfile.cs @@ -1,5 +1,6 @@ using AutoMapper; using System.Linq; +using System.Text.Json; using Unity.Flex.Domain.ScoresheetInstances; using Unity.Flex.Domain.Scoresheets; using Unity.Flex.Domain.WorksheetInstances; @@ -21,7 +22,9 @@ public FlexApplicationAutoMapperProfile() .ForMember(dest => dest.TotalFields, opt => opt.MapFrom(s => s.Sections.SelectMany(s => s.Fields).Count())); CreateMap(); - CreateMap(); + CreateMap() + .ForMember(dest => dest.FieldWidth, opt => opt.MapFrom(src => + ParseFieldWidth(src.Definition))); CreateMap(); CreateMap().ReverseMap(); CreateMap(); @@ -46,4 +49,17 @@ public FlexApplicationAutoMapperProfile() CreateMap(); CreateMap(); } + + private static int? ParseFieldWidth(string? definition) + { + if (string.IsNullOrEmpty(definition)) return null; + try + { + using var doc = JsonDocument.Parse(definition); + if (doc.RootElement.TryGetProperty("fieldWidth", out var prop) && prop.TryGetInt32(out var value)) + return value > 0 ? value : null; + } + catch { /* malformed JSON — fall through */ } + return null; + } } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/WorksheetAppService.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/WorksheetAppService.cs index 90b41405a2..3f1a07c05a 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/WorksheetAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/WorksheetAppService.cs @@ -124,6 +124,13 @@ public virtual async Task PublishAsync(Guid id) return await Task.FromResult(true); } + public virtual async Task ArchiveAsync(Guid id, bool archive) + { + var worksheet = await worksheetRepository.GetAsync(id); + _ = worksheet.SetArchived(archive); + return await Task.FromResult(true); + } + [Authorize(FlexPermissions.Worksheets.Delete)] public virtual async Task DeleteAsync(Guid id) { diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/WorksheetSectionAppService.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/WorksheetSectionAppService.cs index 7e8f4aa39f..ba7d54c466 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/WorksheetSectionAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/WorksheetSectionAppService.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Authorization; using System; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using Unity.Flex.Domain.Worksheets; using Volo.Abp; @@ -20,7 +21,11 @@ public virtual async Task EditAsync(Guid id, EditSectionDto { (Worksheet worksheet, WorksheetSection section) = await GetWorksheetAndSectionAsync(id); - _ = worksheet.UpdateSection(section, dto.Name.Trim()); + string? definition = dto.FieldWidth is > 0 + ? JsonSerializer.Serialize(new { fieldWidth = dto.FieldWidth }) + : null; + + _ = worksheet.UpdateSection(section, dto.Name.Trim(), definition); return ObjectMapper.Map(section); } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Localization/Flex/en.json b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Localization/Flex/en.json index c615f56148..be1208b9d2 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Localization/Flex/en.json +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Localization/Flex/en.json @@ -20,16 +20,16 @@ "Scoresheet:Configuration:DiscardChangesButtonText": "DISCARD CHANGES", "Worksheet:Configuration:SaveChangesButtonText": "SAVE CHANGES", "Worksheet:Configuration:DiscardChangesButtonText": "DISCARD CHANGES", - "Worksheet:Configuration:EditWorksheetButtonText": "Edit Worksheet", + "Worksheet:Configuration:EditWorksheetButtonText": "Edit", "Worksheet:Configuration:AddCustomFieldButtonText": "Add Field", "Worksheet:Configuration:AddSectionButtonText": "ADD SECTION", - "Worksheet:Configuration:AddWorksheetButtonText": "ADD WORKSHEET", - "Worksheet:Configuration:LinkWorksheetButtonText": "Link Worksheet", - "Worksheet:Configuration:CloneWorksheetButtonText": "Clone Worksheet", - "Worksheet:Configuration:PublishWorksheetButtonText": "Publish Worksheet", - "Worksheet:Configuration:DeleteWorksheetButtonText": "Delete Worksheet", + "Worksheet:Configuration:AddWorksheetButtonText": "ADD", + "Worksheet:Configuration:LinkWorksheetButtonText": "Link", + "Worksheet:Configuration:CloneWorksheetButtonText": "Clone", + "Worksheet:Configuration:PublishWorksheetButtonText": "Publish", + "Worksheet:Configuration:DeleteWorksheetButtonText": "Delete", "Worksheet:Configuration:AddCheckboxOptionText": "Add Option", - "Worksheet:Configuration:ExportWorksheetButtonText": "Export Worksheet", + "Worksheet:Configuration:ExportWorksheetButtonText": "Export", "Worksheet:Configuration:AddSelectListOptionText": "Add Option", "Worksheet:Configuration:AddColumnOptionText": "Add Column", "DataGrids:DynamicColumnsHeader": "Dynamic Columns", diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Worksheets/Definitions/CustomFieldDefinition.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Worksheets/Definitions/CustomFieldDefinition.cs index 8f188b66c9..3a68e06078 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Worksheets/Definitions/CustomFieldDefinition.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Worksheets/Definitions/CustomFieldDefinition.cs @@ -7,6 +7,36 @@ public class CustomFieldDefinition [JsonPropertyName("required")] public bool Required { get; set; } = false; + [JsonPropertyName("isHidden")] + public bool IsHidden { get; set; } = false; + + [JsonPropertyName("hideLabel")] + public bool HideLabel { get; set; } = false; + + [JsonPropertyName("isDisabled")] + public bool IsDisabled { get; set; } = false; + + [JsonPropertyName("labelPosition")] + public string LabelPosition { get; set; } = "Top"; + + [JsonPropertyName("style")] + public string? Style { get; set; } + + [JsonPropertyName("cssClass")] + public string? CssClass { get; set; } + + [JsonPropertyName("labelStyle")] + public string? LabelStyle { get; set; } + + [JsonPropertyName("labelCssClass")] + public string? LabelCssClass { get; set; } + + [JsonPropertyName("securityClassification")] + public string? SecurityClassification { get; set; } + + [JsonPropertyName("placeholder")] + public string? Placeholder { get; set; } + public CustomFieldDefinition() : base() { } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.cshtml index a1eeeb7708..06e56218b5 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.cshtml @@ -1,66 +1,6 @@ @page -@using Microsoft.Extensions.Localization; -@using Unity.Flex.Localization; @using Unity.Flex.Web.Pages.WorksheetConfiguration; -@using Unity.Flex.Web.Views.Shared.Components.WorksheetList -@using Volo.Abp.Features; +@using Unity.Flex.Web.Views.Shared.Components.WorksheetConfiguration @model Unity.Flex.Web.Pages.WorksheetConfiguration.IndexModel; -@inject IStringLocalizer L -@inject IFeatureChecker FeatureChecker - -@section styles -{ - -} -@section scripts -{ - - -} - -@if (await FeatureChecker.IsEnabledAsync("Unity.Flex")) -{ - -
- - - - -

Worksheets

-
-
- - - -
-
- @await Component.InvokeAsync(typeof(WorksheetListWidget)) -
-
- -
-
-
-
- - - -

Preview

-
-

No sections to display.

-
-
-
-
-
-
-} \ No newline at end of file +@await Component.InvokeAsync(typeof(WorksheetConfigurationViewComponent)) \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.css b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.css index 3ce40d4e41..5097e505fb 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.css +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.css @@ -3,8 +3,60 @@ flex-wrap: nowrap; } -.worksheet-configuration-container > div { - overflow-y: scroll; +#worksheet-left-col { + overflow: hidden; +} + +.worksheet-left-header { + position: sticky; + top: 0; + z-index: 10; + background-color: var(--bs-card-bg, #fff); + padding: 1rem 1rem 0; + border-bottom: 1px solid var(--bs-border-color, #dee2e6); + padding-bottom: 0.5rem; + margin-bottom: 0.5rem; +} + +.worksheet-left-body { + overflow-y: auto; + flex: 1 1 auto; + padding: 0 1rem; +} + + +.column-resizer { + width: 6px; + cursor: col-resize; + background-color: #dee2e6; + flex-shrink: 0; + transition: background-color 0.15s; + border-radius: 3px; +} + +.column-resizer:hover, +.column-resizer.dragging { + background-color: var(--bs-accordion-active-bg); +} + +.worksheet-filters { + flex-wrap: nowrap; +} + +#worksheet-accordion { + padding-top: 1px; +} + +.btn-label-position { + border: 2px solid #acb2b7; + background-color: transparent; + color: inherit; +} + +.btn-check:checked + .btn-label-position { + background-color: #acb2b7; + border: 2px solid var(--bc-colors-blue-primary); + color: inherit; } .worksheet-scrollable-content { @@ -36,4 +88,33 @@ .sticky-preview { position: sticky; top: 0; +} + +/* Custom field modal section headings */ +.cf-section-heading { + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--bs-secondary-color, #6c757d); + padding-bottom: 0.35rem; + border-bottom: 1px solid var(--bs-border-color, #dee2e6); + margin-top: 1.25rem; + margin-bottom: 0.75rem; +} + +.cf-section-heading:first-child { + margin-top: 0; +} + +/* Tab error indicator badge */ +.tab-error-badge { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + background-color: var(--bs-danger, #dc3545); + margin-left: 6px; + vertical-align: middle; + flex-shrink: 0; } \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.js b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.js index 0500b11825..3ce1b62450 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.js +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/Index.js @@ -1,7 +1,93 @@ $(function () { - $('#worksheet_import_upload_btn').click(function () { + $('.worksheet-import-btn').on('click', function () { $('#worksheet_import_upload').trigger('click'); }); + + // --- Column resizer --- + const resizer = document.getElementById('column-resizer'); + const leftCol = document.getElementById('worksheet-left-col'); + const rightCol = document.getElementById('worksheet-right-col'); + + if (resizer && leftCol && rightCol) { + let isResizing = false; + let startX = 0; + let startLeftWidth = 0; + + resizer.addEventListener('mousedown', function (e) { + isResizing = true; + startX = e.clientX; + startLeftWidth = leftCol.getBoundingClientRect().width; + resizer.classList.add('dragging'); + document.body.style.cursor = 'col-resize'; + document.body.style.userSelect = 'none'; + e.preventDefault(); + }); + + document.addEventListener('mousemove', function (e) { + if (!isResizing) return; + const container = resizer.parentElement; + const containerWidth = container.getBoundingClientRect().width; + const resizerWidth = resizer.getBoundingClientRect().width; + const dx = e.clientX - startX; + let newLeftWidth = startLeftWidth + dx; + newLeftWidth = Math.max(200, Math.min(containerWidth - resizerWidth - 200, newLeftWidth)); + leftCol.style.flex = `0 0 ${newLeftWidth}px`; + rightCol.style.flex = '1 1 0'; + }); + + document.addEventListener('mouseup', function () { + if (!isResizing) return; + isResizing = false; + resizer.classList.remove('dragging'); + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + }); + } + + // --- Worksheet name filter & published/archived toggle --- + function applyWorksheetFilters() { + const searchText = $('#worksheet-name-filter').val().toLowerCase(); + const publishedFilter = $('#worksheet-published-toggle .active').data('filter'); + + let visibleCount = 0; + $('#worksheet-accordion .accordion-item').each(function () { + const $item = $(this); + const title = $item.find('.worksheet-title').text().toLowerCase(); + const name = $item.find('.worksheet-name').text().toLowerCase(); + const isPublished = !$item.find('.worksheet-published-icon').hasClass('hidden'); + const isArchived = $item.data('is-archived') === true || $item.data('is-archived') === 'true'; + + const matchesText = !searchText || title.includes(searchText) || name.includes(searchText); + const matchesFilter = + publishedFilter === 'archived' + ? isArchived + : !isArchived && ( + publishedFilter === 'all' || + (publishedFilter === 'published' && isPublished) || + (publishedFilter === 'unpublished' && !isPublished) + ); + + const visible = matchesText && matchesFilter; + $item.toggle(visible); + if (visible) visibleCount++; + }); + + $('#worksheet-no-results').toggle(visibleCount === 0); + } + + $(document).on('input', '#worksheet-name-filter', applyWorksheetFilters); + + $(document).on('click', '#worksheet-published-toggle button', function () { + $('#worksheet-published-toggle button').removeClass('active'); + $(this).addClass('active'); + applyWorksheetFilters(); + }); + + PubSub.subscribe('worksheet_list_refreshed', function () { + applyWorksheetFilters(); + }); + + applyWorksheetFilters(); }); function importWorksheetFile(inputId) { diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.cshtml index e89d9c4d46..6f3ad46b7b 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.cshtml @@ -11,10 +11,6 @@ @{ Layout = null; } -@section scripts -{ - -}
@@ -22,72 +18,112 @@ + + - - - - - - - - - - - - - - - -
- @await Component.InvokeAsync(typeof(CustomFieldDefinitionWidget), new { type = Model.FieldType, definition = Model.Definition }) + +
+
+ + + + + + + + + + + +
+ Label position + + + + +
+
+
+ + + + + + + +
+ @await Component.InvokeAsync(typeof(CustomFieldDefinitionWidget), new { type = Model.FieldType, definition = Model.Definition }) +
+
+
+ +

Security

+ @{ + string? scNone = string.IsNullOrEmpty(Model.SecurityClassification) ? "selected" : null; + string? scA = Model.SecurityClassification == "ProtectedA" ? "selected" : null; + string? scB = Model.SecurityClassification == "ProtectedB" ? "selected" : null; + string? scC = Model.SecurityClassification == "ProtectedC" ? "selected" : null; + } +
+ + +
+
+ +

Visibility

+
+
+ +
+
+ +
+
+ +
+
+

Label

+
+ + +
+
+ + +
+ +
+
- - @if (Model.UpsertAction == WorksheetUpsertAction.Update && !Model.Published) { - + } + + - diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.cshtml.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.cshtml.cs index 9a1c7e55a2..e2b96b74f3 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.cshtml.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.cshtml.cs @@ -5,8 +5,10 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; +using Unity.Flex; using Unity.Flex.Web.Views.Shared.Components.CustomFieldDefinitionWidget; using Unity.Flex.Worksheets; +using Unity.Flex.Worksheets.Definitions; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; namespace Unity.Flex.Web.Pages.WorksheetConfiguration; @@ -52,6 +54,34 @@ public class UpsertCustomFieldModalModel(ICustomFieldAppService customFieldAppSe [BindProperty] public bool IsDelete { get; set; } + [BindProperty] + [DisplayName("Is Hidden")] + public bool IsHidden { get; set; } + + [BindProperty] + [DisplayName("Hide Label")] + public bool HideLabel { get; set; } + + [BindProperty] + [DisplayName("Is Disabled")] + public bool IsDisabled { get; set; } + + [BindProperty] + public string LabelPosition { get; set; } = "Top"; + + [BindProperty] + public string? LabelStyle { get; set; } + + [BindProperty] + public string? LabelCssClass { get; set; } + + [BindProperty] + [DisplayName("Security Classification")] + public string? SecurityClassification { get; set; } + + [BindProperty] + public string? Placeholder { get; set; } + [SelectItems(nameof(FieldTypes))] [Required] [BindProperty] @@ -77,6 +107,19 @@ public async Task OnGetAsync(Guid worksheetId, Guid sectionId, Guid fieldId, str Published = worksheet.Published; FieldType = customField.Type.ToString(); Definition = customField.Definition; + + if (customField.Definition != null) + { + var existingDef = customField.Definition.ConvertDefinition(customField.Type); + IsHidden = existingDef?.IsHidden ?? false; + HideLabel = existingDef?.HideLabel ?? false; + IsDisabled = existingDef?.IsDisabled ?? false; + LabelPosition = existingDef?.LabelPosition ?? "Top"; + LabelStyle = existingDef?.LabelStyle; + LabelCssClass = existingDef?.LabelCssClass; + SecurityClassification = existingDef?.SecurityClassification; + Placeholder = existingDef?.Placeholder; + } } } @@ -135,9 +178,39 @@ private async Task UpdateCustomField() private object? ExtractDefinition() { - var fieldType = Enum.TryParse(FieldType, out CustomFieldType type); - if (!fieldType) return null; - return CustomFieldDefinitionWidget.ParseFormValues(type, Request.Form); + if (!Enum.TryParse(FieldType, out CustomFieldType type)) return null; + + var definition = CustomFieldDefinitionWidget.ParseFormValues(type, Request.Form); + + // Types with a definition editor: ParseFormValues already built the typed object. + if (definition is CustomFieldDefinition def) + { + ApplyFieldOptions(def); + return definition; + } + + // Types without a definition editor (Date, Checkbox, YesNo, Email, Phone…): + // rehydrate the existing definition JSON so we preserve Required, then apply options. + var existingDef = (Definition ?? "{}").ConvertDefinition(type); + if (existingDef != null) + { + ApplyFieldOptions(existingDef); + return existingDef; + } + + return definition; + } + + private void ApplyFieldOptions(CustomFieldDefinition def) + { + def.IsHidden = IsHidden; + def.HideLabel = HideLabel; + def.IsDisabled = IsDisabled; + def.LabelPosition = LabelPosition; + def.LabelStyle = string.IsNullOrWhiteSpace(LabelStyle) ? null : LabelStyle.Trim(); + def.LabelCssClass = string.IsNullOrWhiteSpace(LabelCssClass) ? null : LabelCssClass.Trim(); + def.SecurityClassification = string.IsNullOrEmpty(SecurityClassification) ? null : SecurityClassification; + def.Placeholder = string.IsNullOrWhiteSpace(Placeholder) ? null : Placeholder.Trim(); } private OkObjectResult MapModalResponse(CustomFieldDto customFieldDto) diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.js b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.js new file mode 100644 index 0000000000..abb0f483ab --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertCustomFieldModal.js @@ -0,0 +1,108 @@ +abp.modals.UpsertCustomFieldModal = function () { + + const classificationHints = { + 'ProtectedA': 'If compromised, could cause limited or moderate injury to an individual or organisation — e.g. an exact salary figure or home address.', + 'ProtectedB': 'Could cause serious injury if disclosed — e.g. Social Insurance Numbers, employment equity data, or personal health records.', + 'ProtectedC': 'The most sensitive level — disclosure could cause extremely grave injury.' + }; + + const placeholderSupportedTypes = new Set(['Text', 'TextArea', 'Numeric', 'Currency', 'Email', 'Phone']); + + function updateClassificationHint(value) { + const hint = document.getElementById('classificationHint'); + if (hint) hint.textContent = classificationHints[value] || ''; + } + + function updatePlaceholderVisibility(type) { + const row = document.getElementById('placeholder-row'); + if (row) row.style.display = placeholderSupportedTypes.has(type) ? '' : 'none'; + } + + function initModal(modalManager, args) { + const initClassification = document.getElementById('InitSecurityClassification')?.value ?? ''; + const initFieldType = document.getElementById('InitFieldType')?.value ?? 'Text'; + + updateClassificationHint(initClassification); + updatePlaceholderVisibility(initFieldType); + + document.getElementById('fieldType')?.addEventListener('change', function () { + updatePlaceholderVisibility(this.value); + const customFieldWidget = new abp.WidgetManager({ + wrapper: '#definition-editor', + filterCallback: function () { + return { 'type': $('#fieldType').val() }; + } + }); + customFieldWidget.refresh(); + }); + + document.getElementById('SecurityClassification')?.addEventListener('change', function () { + updateClassificationHint(this.value); + }); + + document.querySelector('[name="deleteCustomFieldBtn"]')?.addEventListener('click', function () { + Swal.fire({ + title: "Delete Custom Field?", + text: 'Are you sure you want to delete this custom field?', + showCancelButton: true, + confirmButtonText: 'Confirm', + customClass: { + confirmButton: 'btn btn-primary', + cancelButton: 'btn btn-secondary' + } + }).then((result) => { + if (result.isConfirmed) { + $('#DeleteAction').val(true); + $('#customFieldInfo').submit(); + } + }); + }); + + function checkPaneErrors(activePaneId, tabButtons, paneId) { + if (paneId === activePaneId) return false; + let pane = document.getElementById(paneId); + let invalid = pane ? pane.querySelector(':invalid') : null; + let btn = document.getElementById(tabButtons[paneId]); + if (!btn) return false; + let badge = btn.querySelector('.tab-error-badge'); + if (invalid) { + if (!badge) { + badge = document.createElement('span'); + badge.className = 'tab-error-badge'; + badge.setAttribute('aria-label', 'This tab has errors'); + btn.appendChild(badge); + } + return true; + } + if (badge) badge.remove(); + return false; + } + + document.querySelector('[name="saveCustomFieldBtn"]')?.addEventListener('click', function () { + setTimeout(function () { + let panes = ['pane-display', 'pane-attributes']; + let tabButtons = { 'pane-display': 'tab-display', 'pane-attributes': 'tab-attributes' }; + let activePane = document.querySelector('#customFieldTabContent .tab-pane.show'); + let activePaneId = activePane ? activePane.id : null; + let hasOffTabErrors = false; + + for (const paneId of panes) { + if (checkPaneErrors(activePaneId, tabButtons, paneId)) hasOffTabErrors = true; + } + + if (hasOffTabErrors) { + abp.notify.warn('There are validation errors on another tab. Please review all tabs before saving.'); + } + }, 0); + }); + + document.querySelectorAll('#customFieldTabs [data-bs-toggle="tab"]').forEach(function (btn) { + btn.addEventListener('shown.bs.tab', function () { + let badge = btn.querySelector('.tab-error-badge'); + if (badge) badge.remove(); + }); + }); + } + + return { initModal: initModal }; +}; diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertSectionModal.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertSectionModal.cshtml index df0f763371..b46393d927 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertSectionModal.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Pages/WorksheetConfiguration/UpsertSectionModal.cshtml @@ -25,7 +25,37 @@ - + + + + + + +
+ Auto + + 100% + +
+
+ + +
+
+ Set to 0 to use automatic column layout based on number of fields. Otherwise fields in this section will be a fixed percentage width. +
@@ -42,6 +72,53 @@