diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs index 216d2b6..cda0ce4 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs @@ -307,10 +307,17 @@ public class DynamicFormReport : XtraReport if (TryGetFormattedValue(field, row, out var text)) { label.Text = text; + label.Font = new DXFont( + DynamicReportFontHelper.GetFontFamilyForText(text), + DynamicReportFontHelper.DataFontSize); } else { label.ExpressionBindings.Add(new ExpressionBinding("BeforePrint", "Text", $"[{field.BindingName}]")); + label.ExpressionBindings.Add(new ExpressionBinding( + "BeforePrint", + "Font.Name", + DynamicReportImageHelper.CreateFontFamilyExpression($"[{field.BindingName}]"))); } return label; diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs index bb1a8e1..96e6bfc 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs @@ -115,6 +115,9 @@ public class DynamicGridReport : XtraReport var visibleFields = DynamicReportGridStateHelper.GetVisibleFields( availableFields, gridState?.CustomizationData); + var stateWidths = DynamicReportGridStateHelper.GetColumnWidths( + gridState?.CustomizationData, + GetParameter(parameters, "columnWidths")); if (visibleFields.Count == 0) { @@ -174,12 +177,12 @@ public class DynamicGridReport : XtraReport localizer); reportColumns = await ApplyImageCountsAsync(reportColumns, dynamicDataRepository, selectQuery, connectionString, imageBaseUrl); - ConfigurePage(reportColumns, isSubReport); + ConfigurePage(reportColumns, localizer, stateWidths, isSubReport); ConfigureDataSource( selectQuery, connectionString, dataSourceType); - BuildLayout(listForm, reportColumns, groupColumns, localizer, imageBaseUrl, currentUser.UserName, criteriaText, isSubReport); + BuildLayout(listForm, reportColumns, groupColumns, localizer, imageBaseUrl, currentUser.UserName, criteriaText, isSubReport, stateWidths); RequestParameters = false; } @@ -207,12 +210,18 @@ public class DynamicGridReport : XtraReport } #pragma warning disable CA1416 - private void ConfigurePage(IReadOnlyCollection visibleFields, bool isSubReport) + private void ConfigurePage( + IReadOnlyCollection visibleFields, + IStringLocalizer localizer, + IReadOnlyDictionary stateWidths, + bool isSubReport) { Margins = CreateReportMargins(HorizontalMargin, HorizontalMargin, 35, 35); PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4; var portraitContentWidth = A4PortraitWidth - Margins.Left - Margins.Right; - var requestedContentWidth = visibleFields.Sum(x => x.IsImage ? GetImageColumnWidth(x) : GetPreferredColumnWidth(x.Field)); + var requestedContentWidth = visibleFields.Sum(x => x.IsImage + ? GetImageColumnWidth(x) + : DynamicReportGridStateHelper.GetPreferredColumnWidth(x.Field, localizer, stateWidths)); Landscape = !isSubReport && (visibleFields.Count >= LandscapeColumnThreshold || requestedContentWidth > portraitContentWidth); Font = DynamicReportFontHelper.CreateDataFont(); @@ -294,7 +303,8 @@ public class DynamicGridReport : XtraReport string imageBaseUrl, string currentUserName, string criteriaText, - bool isSubReport) + bool isSubReport, + IReadOnlyDictionary stateWidths) { Bands.Clear(); @@ -352,8 +362,7 @@ public class DynamicGridReport : XtraReport var detailIndent = GetGroupDetailIndent(groupFields.Count, pageWidth); var tableWidth = pageWidth - detailIndent; - var detailWidths = CalculateWidths(fields, tableWidth); - var headerWidths = CreateIndentedHeaderWidths(detailWidths, detailIndent); + var detailWidths = DynamicReportGridStateHelper.CalculateWidths(fields, tableWidth, localizer, stateWidths); var headerTable = CreateTable(pageWidth, 28); var headerRow = new XRTableRow { HeightF = 28 }; var detailTable = CreateTable(tableWidth, detailHeight, detailIndent); @@ -362,10 +371,15 @@ public class DynamicGridReport : XtraReport var footerTable = CreateTable(tableWidth, 24, detailIndent); var footerRow = new XRTableRow { HeightF = 24 }; + if (detailIndent > 0) + { + headerRow.Cells.Add(CreateIndentHeaderCell(detailIndent)); + } + for (var i = 0; i < fields.Count; i++) { var field = fields[i]; - headerRow.Cells.Add(CreateHeaderCell(field, headerWidths[i], localizer)); + headerRow.Cells.Add(CreateHeaderCell(field, detailWidths[i], localizer)); detailRow.Cells.Add(CreateDetailCell(field, detailWidths[i], imageBaseUrl, detailHeight)); footerRow.Cells.Add(CreateFooterCell(field, detailWidths[i])); } @@ -457,19 +471,6 @@ public class DynamicGridReport : XtraReport return Math.Min(groupCount * GroupIndentWidth, pageWidth * 0.25F); } - private static float[] CreateIndentedHeaderWidths(float[] detailWidths, float detailIndent) - { - if (detailWidths.Length == 0 || detailIndent <= 0) - { - return detailWidths; - } - - var headerWidths = detailWidths.ToArray(); - headerWidths[0] += detailIndent; - - return headerWidths; - } - private static XRTable CreateTable(float width, float height, float left = 0) { return new XRTable @@ -481,6 +482,17 @@ public class DynamicGridReport : XtraReport }; } + private static XRTableCell CreateIndentHeaderCell(float width) + { + return new XRTableCell + { + WidthF = width, + Weight = width, + BackColor = System.Drawing.Color.FromArgb(238, 242, 247), + Padding = new PaddingInfo(0, 0, 0, 0) + }; + } + private static XRTableCell CreateHeaderCell( ReportColumn column, float width, @@ -532,10 +544,15 @@ public class DynamicGridReport : XtraReport Multiline = true, CanGrow = true }; + var valueExpression = DynamicReportImageHelper.CreateFormatValueExpression($"[{column.BindingName}]", field, column.EditorType); cell.ExpressionBindings.Add(new ExpressionBinding( "BeforePrint", "Text", - DynamicReportImageHelper.CreateFormatValueExpression($"[{column.BindingName}]", field, column.EditorType))); + valueExpression)); + cell.ExpressionBindings.Add(new ExpressionBinding( + "BeforePrint", + "Font.Name", + DynamicReportImageHelper.CreateFontFamilyExpression(valueExpression))); return cell; } @@ -614,66 +631,6 @@ public class DynamicGridReport : XtraReport }; } - private static float[] CalculateWidths(IReadOnlyList fields, float pageWidth) - { - var raw = fields.Select(x => x.IsImage ? GetImageColumnWidth(x) : GetPreferredColumnWidth(x.Field)).ToArray(); - var total = raw.Sum(); - if (total <= 0) - { - return fields.Select(_ => pageWidth / fields.Count).ToArray(); - } - - return raw.Select(x => pageWidth * x / total).ToArray(); - } - - private static float GetPreferredColumnWidth(ListFormField field) - { - if (field.Width > 0) - { - return Math.Max(field.Width.Value, 60); - } - - var caption = string.IsNullOrWhiteSpace(field.CaptionName) ? field.FieldName : field.CaptionName; - var name = field.FieldName ?? string.Empty; - var text = $"{caption} {name}".ToLowerInvariant(); - - if (field.SourceDbType == DbType.Boolean) - { - return 75; - } - - if (DynamicReportImageHelper.IsNumeric(field.SourceDbType)) - { - return 70; - } - - if (field.SourceDbType is DbType.Date or DbType.DateTime or DbType.DateTime2 or DbType.DateTimeOffset) - { - return 115; - } - - if (text.Contains("description") || - text.Contains("explanation") || - text.Contains("note") || - text.Contains("comment") || - text.Contains("address") || - text.Contains("content") || - text.Contains("html") || - text.Contains("text")) - { - return 220; - } - - if (text.Contains("name") || - text.Contains("title") || - text.Contains("subject")) - { - return 160; - } - - return Math.Clamp((caption?.Length ?? 0) * 9F + 36F, 85F, 180F); - } - private static bool HasAnySummary(IEnumerable fields) { return fields.Any(x => GetSummaryType(x.Field.TotalSummaryJson) != null); @@ -737,7 +694,10 @@ public class DynamicGridReport : XtraReport bool IsImage, bool IsHtml, string EditorType, - int ImageCount = 1) : DynamicReportSqlBuilder.IReportLookupColumn; + int ImageCount = 1) : DynamicReportSqlBuilder.IReportLookupColumn, DynamicReportGridStateHelper.IReportColumnWidth + { + public float ImageWidth => GetImageColumnWidth(this); + } private static SummaryFunc? GetSummaryType(string totalSummaryJson) { diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportFontHelper.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportFontHelper.cs index 97f05a3..9d40abc 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportFontHelper.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportFontHelper.cs @@ -9,6 +9,7 @@ internal static class DynamicReportFontHelper public const float DataFontSize = 8.5F; private const string WindowsFontFamily = "Arial"; + private const string WindowsEmojiFontFamily = "Segoe UI Emoji"; private const string LinuxFontFamily = "Liberation Sans"; public static DXFont CreateHeaderFont(DXFontStyle style = DXFontStyle.Bold) => @@ -19,10 +20,48 @@ internal static class DynamicReportFontHelper public static DXFont CreateValueFont(float size, DXFontStyle style = DXFontStyle.Regular) { - var fontFamily = OperatingSystem.IsLinux() + return new DXFont(GetDefaultFontFamily(), size, style); + } + + public static string GetFontFamilyForText(string text) + { + return OperatingSystem.IsWindows() && ContainsEmojiOrSymbol(text) + ? WindowsEmojiFontFamily + : GetDefaultFontFamily(); + } + + private static bool ContainsEmojiOrSymbol(string text) + { + if (string.IsNullOrEmpty(text)) + { + return false; + } + + for (var i = 0; i < text.Length; i++) + { + int codePoint; + if (char.IsHighSurrogate(text[i]) && i + 1 < text.Length && char.IsLowSurrogate(text[i + 1])) + { + codePoint = char.ConvertToUtf32(text, i); + i++; + } + else + { + codePoint = text[i]; + } + + if (codePoint is >= 0x1F000 and <= 0x1FAFF || + codePoint is >= 0x2600 and <= 0x27BF) + { + return true; + } + } + + return false; + } + + private static string GetDefaultFontFamily() => + OperatingSystem.IsLinux() ? LinuxFontFamily : WindowsFontFamily; - - return new DXFont(fontFamily, size, style); - } } diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportGridStateHelper.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportGridStateHelper.cs index 25e5992..d4f0c0d 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportGridStateHelper.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportGridStateHelper.cs @@ -1,15 +1,25 @@ using System; using System.Collections.Generic; +using System.Data; using System.Linq; using System.Text.Json; +using Microsoft.Extensions.Localization; using Sozsoft.Platform; using Sozsoft.Platform.Entities; using Sozsoft.Platform.ListForms; +using Sozsoft.Platform.Localization; namespace Sozsoft.Reports.PredefinedReports; internal static class DynamicReportGridStateHelper { + public interface IReportColumnWidth + { + ListFormField Field { get; } + bool IsImage { get; } + float ImageWidth { get; } + } + public static string GetFilterName(ListForm listForm, string prefix) => $"{prefix}-{GetStateStorageKey(listForm)}"; @@ -79,6 +89,74 @@ internal static class DynamicReportGridStateHelper } } + public static Dictionary GetColumnWidths(string gridStateJson, string columnWidthsJson = null) + { + var widths = TryGetColumnStates(gridStateJson, out var statesByField) + ? statesByField + .Where(x => x.Value.Width.HasValue && x.Value.Width.Value > 0) + .ToDictionary(x => x.Key, x => x.Value.Width.Value, StringComparer.OrdinalIgnoreCase) + : new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var item in ParseColumnWidths(columnWidthsJson)) + { + widths[item.Key] = item.Value; + } + + return widths; + } + + public static float[] CalculateWidths( + IReadOnlyList columns, + float pageWidth, + IStringLocalizer localizer, + IReadOnlyDictionary stateWidths = null) + where TColumn : IReportColumnWidth + { + if (columns.Count == 0) + { + return []; + } + + var ideal = columns + .Select(column => column.IsImage + ? column.ImageWidth + : GetPreferredColumnWidth(column.Field, localizer, stateWidths)) + .ToArray(); + var total = ideal.Sum(); + if (total <= 0) + { + return columns.Select(_ => pageWidth / columns.Count).ToArray(); + } + + return ideal.Select(width => pageWidth * width / total).ToArray(); + } + + public static float GetPreferredColumnWidth( + ListFormField field, + IStringLocalizer localizer = null, + IReadOnlyDictionary stateWidths = null) + { + var caption = localizer == null + ? (string.IsNullOrWhiteSpace(field.CaptionName) ? field.FieldName : field.CaptionName) + : DynamicReportImageHelper.GetLocalizedFieldCaption(field, localizer); + var name = field.FieldName ?? string.Empty; + var calculatedWidth = CalculateTextColumnWidth(field, caption, name); + + if (!string.IsNullOrWhiteSpace(field.FieldName) && + stateWidths?.TryGetValue(field.FieldName, out var stateWidth) == true && + stateWidth > 0) + { + return Math.Max(stateWidth, 40F); + } + + if (field.Width > 0) + { + return Math.Max(field.Width.Value, 40F); + } + + return calculatedWidth; + } + private static string GetStateStorageKey(ListForm listForm) { if (!string.IsNullOrWhiteSpace(listForm.StateStoringJson)) @@ -151,10 +229,14 @@ internal static class DynamicReportGridStateHelper groupIndexProperty.TryGetInt32(out var groupValue) ? groupValue : (int?)null; + var width = TryGetProperty(column, "width", out var widthProperty) && + TryGetFloat(widthProperty, out var widthValue) + ? widthValue + : (float?)null; if (!string.IsNullOrWhiteSpace(fieldName)) { - states.Add(new GridColumnState(fieldName, visible, visibleIndex, groupIndex)); + states.Add(new GridColumnState(fieldName, visible, visibleIndex, groupIndex, width)); } if (TryGetProperty(column, "columns", out var childColumns) && @@ -182,6 +264,114 @@ internal static class DynamicReportGridStateHelper return null; } + private static bool TryGetFloat(JsonElement element, out float value) + { + if (element.ValueKind == JsonValueKind.Number && element.TryGetSingle(out value)) + { + return true; + } + + if (element.ValueKind == JsonValueKind.String && + float.TryParse(element.GetString(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out value)) + { + return true; + } + + value = 0; + return false; + } + + private static float CalculateTextColumnWidth(ListFormField field, string caption, string name) + { + if (field.SourceDbType == DbType.Boolean) + { + return Math.Max(75F, MeasureTextWidth(caption, 7.2F, 30F, 68F, 100F)); + } + + if (DynamicReportImageHelper.IsNumeric(field.SourceDbType)) + { + return Math.Max(70F, MeasureTextWidth(caption, 7.2F, 30F, 68F, 120F)); + } + + if (field.SourceDbType is DbType.Date or DbType.DateTime or DbType.DateTime2 or DbType.DateTimeOffset) + { + return Math.Max(115F, MeasureTextWidth(caption, 7.2F, 30F, 90F, 145F)); + } + + return Math.Max( + MeasureTextWidth(caption, 7.2F, 30F, 85F, 220F), + MeasureTextWidth(name, 5.6F, 28F, 85F, 180F)); + } + + private static float MeasureTextWidth( + string value, + float characterWidth, + float padding, + float minimum, + float maximum) + { + if (string.IsNullOrWhiteSpace(value)) + { + return 0; + } + + return Math.Clamp(value.Trim().Length * characterWidth + padding, minimum, maximum); + } + + private static Dictionary ParseColumnWidths(string columnWidthsJson) + { + var widths = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (string.IsNullOrWhiteSpace(columnWidthsJson)) + { + return widths; + } + + try + { + using var document = JsonDocument.Parse(columnWidthsJson); + if (document.RootElement.ValueKind == JsonValueKind.Object) + { + foreach (var property in document.RootElement.EnumerateObject()) + { + if (TryGetFloat(property.Value, out var width) && width > 0) + { + widths[property.Name] = width; + } + } + } + else if (document.RootElement.ValueKind == JsonValueKind.Array) + { + AddColumnWidths(document.RootElement, widths); + } + } + catch (JsonException) + { + } + + return widths; + } + + private static void AddColumnWidths(JsonElement columns, IDictionary widths) + { + foreach (var column in columns.EnumerateArray()) + { + var fieldName = GetColumnFieldName(column); + if (!string.IsNullOrWhiteSpace(fieldName) && + TryGetProperty(column, "width", out var widthProperty) && + TryGetFloat(widthProperty, out var width) && + width > 0) + { + widths[fieldName] = width; + } + + if (TryGetProperty(column, "columns", out var childColumns) && + childColumns.ValueKind == JsonValueKind.Array) + { + AddColumnWidths(childColumns, widths); + } + } + } + private static List GetGroupFieldsFromParameter( IReadOnlyList availableFields, string groupParameter) @@ -249,5 +439,5 @@ internal static class DynamicReportGridStateHelper return false; } - private sealed record GridColumnState(string FieldName, bool Visible, int VisibleIndex, int? GroupIndex); + private sealed record GridColumnState(string FieldName, bool Visible, int VisibleIndex, int? GroupIndex, float? Width); } diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportImageHelper.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportImageHelper.cs index 4a138ff..c767177 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportImageHelper.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportImageHelper.cs @@ -35,6 +35,7 @@ internal static class DynamicReportImageHelper private const string ImageUrlFunctionName = "SozReportImageUrl"; private const string ImageSourceFunctionName = "SozReportImageSource"; private const string FormatValueFunctionName = "SozReportFormatValue"; + private const string FontFamilyFunctionName = "SozReportFontFamily"; private static readonly Regex ReportPagingRegex = new( @"\s+OFFSET\s+\d+\s+ROWS(?:\s+FETCH\s+NEXT\s+\d+\s+ROWS\s+ONLY)?\s*$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled); @@ -55,6 +56,7 @@ internal static class DynamicReportImageHelper RegisterImageFunction(new ReportImageUrlFunction()); RegisterImageFunction(new ReportImageSourceFunction()); RegisterImageFunction(new ReportFormatValueFunction()); + RegisterImageFunction(new ReportFontFamilyFunction()); } public static Dictionary CreateSelectFieldMap(IEnumerable selectFields) @@ -487,13 +489,13 @@ internal static class DynamicReportImageHelper { WidthF = width, Weight = width, - Padding = new PaddingInfo(3, 3, 3, 3), + Padding = new PaddingInfo(0, 0, 0, 0), CanGrow = false }; for (var i = 0; i < safeImageCount; i++) { - var pictureBox = CreatePictureBox(i, width, DefaultGridImageHeight - 6, safeImageCount); + var pictureBox = CreatePictureBox(i, width, DefaultGridImageHeight, safeImageCount); pictureBox.Visible = true; pictureBox.ExpressionBindings.Add(new ExpressionBinding( "BeforePrint", @@ -602,6 +604,11 @@ internal static class DynamicReportImageHelper return $"{FormatValueFunctionName}({valueExpression}, '{field.SourceDbType}', '{EscapeExpressionString(GetEffectiveFormat(field))}', '{EscapeExpressionString(editorType)}')"; } + public static string CreateFontFamilyExpression(string valueExpression) + { + return $"{FontFamilyFunctionName}({valueExpression})"; + } + public static string FormatReportDate(DateTime value) { var culture = CultureInfo.CurrentCulture; @@ -2006,4 +2013,34 @@ internal static class DynamicReportImageHelper : DbType.String; } } + + private sealed class ReportFontFamilyFunction : ICustomFunctionOperatorBrowsable + { + public string Name => FontFamilyFunctionName; + public int MinOperandCount => 1; + public int MaxOperandCount => 1; + public FunctionCategory Category => FunctionCategory.Text; + public string Description => "Returns a report font family with emoji fallback."; + + public object Evaluate(params object[] operands) + { + var text = Convert.ToString(operands.ElementAtOrDefault(0), CultureInfo.CurrentCulture); + return DynamicReportFontHelper.GetFontFamilyForText(text); + } + + public Type ResultType(params Type[] operands) + { + return typeof(string); + } + + public bool IsValidOperandCount(int count) + { + return count == 1; + } + + public bool IsValidOperandType(int operandIndex, int operandCount, Type type) + { + return true; + } + } } diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicTreeReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicTreeReport.cs index 1658474..2bad38c 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicTreeReport.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicTreeReport.cs @@ -124,6 +124,9 @@ public class DynamicTreeReport : XtraReport var visibleFields = DynamicReportGridStateHelper.GetVisibleFields( availableFields, gridState?.CustomizationData); + var stateWidths = DynamicReportGridStateHelper.GetColumnWidths( + gridState?.CustomizationData, + GetParameter(parameters, "columnWidths")); if (visibleFields.Count == 0) { @@ -169,19 +172,24 @@ public class DynamicTreeReport : XtraReport var orderedRows = OrderTreeRows(sourceRows, treeOptions).ToList(); reportColumns = ApplyImageCounts(reportColumns, orderedRows, imageBaseUrl); - ConfigurePage(reportColumns); + ConfigurePage(reportColumns, localizer, stateWidths); ConfigureDataSource(orderedRows, reportColumns); - BuildLayout(listForm, reportColumns, localizer, imageBaseUrl, currentUser.UserName, criteriaText, isSubReport); + BuildLayout(listForm, reportColumns, localizer, imageBaseUrl, currentUser.UserName, criteriaText, isSubReport, stateWidths); RequestParameters = false; } #pragma warning disable CA1416 - private void ConfigurePage(IReadOnlyCollection visibleFields) + private void ConfigurePage( + IReadOnlyCollection visibleFields, + IStringLocalizer localizer, + IReadOnlyDictionary stateWidths) { Margins = CreateReportMargins(HorizontalMargin, HorizontalMargin, 35, 35); PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4; var portraitContentWidth = A4PortraitWidth - Margins.Left - Margins.Right; - var requestedContentWidth = visibleFields.Sum(x => x.IsImage ? GetImageColumnWidth(x) : GetPreferredColumnWidth(x.Field)); + var requestedContentWidth = visibleFields.Sum(x => x.IsImage + ? GetImageColumnWidth(x) + : DynamicReportGridStateHelper.GetPreferredColumnWidth(x.Field, localizer, stateWidths)); Landscape = visibleFields.Count >= LandscapeColumnThreshold || requestedContentWidth > portraitContentWidth; Font = DynamicReportFontHelper.CreateDataFont(); } @@ -423,7 +431,8 @@ public class DynamicTreeReport : XtraReport string imageBaseUrl, string currentUserName, string criteriaText, - bool isSubReport) + bool isSubReport, + IReadOnlyDictionary stateWidths) { Bands.Clear(); @@ -466,7 +475,7 @@ public class DynamicTreeReport : XtraReport TextAlignment = TextAlignment.MiddleRight }); - var widths = CalculateWidths(fields, pageWidth); + var widths = DynamicReportGridStateHelper.CalculateWidths(fields, pageWidth, localizer, stateWidths); var headerTable = CreateTable(pageWidth, 28); var headerRow = new XRTableRow { HeightF = 28 }; var detailTable = CreateTable(pageWidth, detailHeight); @@ -560,12 +569,17 @@ public class DynamicTreeReport : XtraReport }; var valueExpression = DynamicReportImageHelper.CreateFormatValueExpression($"[{column.BindingName}]", column.Field, column.EditorType); + var textExpression = isTreeColumn + ? $"Concat([{TreePrefixColumn}], {valueExpression})" + : valueExpression; cell.ExpressionBindings.Add(new ExpressionBinding( "BeforePrint", "Text", - isTreeColumn - ? $"Concat([{TreePrefixColumn}], {valueExpression})" - : valueExpression)); + textExpression)); + cell.ExpressionBindings.Add(new ExpressionBinding( + "BeforePrint", + "Font.Name", + DynamicReportImageHelper.CreateFontFamilyExpression(textExpression))); if (isTreeColumn) { @@ -635,66 +649,6 @@ public class DynamicTreeReport : XtraReport return cell; } - private static float[] CalculateWidths(IReadOnlyList fields, float pageWidth) - { - var raw = fields.Select(x => x.IsImage ? GetImageColumnWidth(x) : GetPreferredColumnWidth(x.Field)).ToArray(); - var total = raw.Sum(); - if (total <= 0) - { - return fields.Select(_ => pageWidth / fields.Count).ToArray(); - } - - return raw.Select(x => pageWidth * x / total).ToArray(); - } - - private static float GetPreferredColumnWidth(ListFormField field) - { - if (field.Width > 0) - { - return Math.Max(field.Width.Value, 60); - } - - var caption = string.IsNullOrWhiteSpace(field.CaptionName) ? field.FieldName : field.CaptionName; - var name = field.FieldName ?? string.Empty; - var text = $"{caption} {name}".ToLowerInvariant(); - - if (field.SourceDbType == DbType.Boolean) - { - return 75; - } - - if (DynamicReportImageHelper.IsNumeric(field.SourceDbType)) - { - return 70; - } - - if (field.SourceDbType is DbType.Date or DbType.DateTime or DbType.DateTime2 or DbType.DateTimeOffset) - { - return 115; - } - - if (text.Contains("description") || - text.Contains("explanation") || - text.Contains("note") || - text.Contains("comment") || - text.Contains("address") || - text.Contains("content") || - text.Contains("html") || - text.Contains("text")) - { - return 220; - } - - if (text.Contains("name") || - text.Contains("title") || - text.Contains("subject")) - { - return 160; - } - - return Math.Clamp((caption?.Length ?? 0) * 9F + 36F, 85F, 180F); - } - private static bool HasAnySummary(IEnumerable fields) { return fields.Any(x => GetSummaryType(x.Field.TotalSummaryJson) != null); @@ -778,7 +732,10 @@ public class DynamicTreeReport : XtraReport bool IsImage, bool IsHtml, string EditorType, - int ImageCount = 1) : DynamicReportSqlBuilder.IReportLookupColumn; + int ImageCount = 1) : DynamicReportSqlBuilder.IReportLookupColumn, DynamicReportGridStateHelper.IReportColumnWidth + { + public float ImageWidth => GetImageColumnWidth(this); + } private static SummaryFunc? GetSummaryType(string totalSummaryJson) { diff --git a/ui/src/views/list/useFilters.tsx b/ui/src/views/list/useFilters.tsx index bd83eb1..ed246f8 100644 --- a/ui/src/views/list/useFilters.tsx +++ b/ui/src/views/list/useFilters.tsx @@ -209,6 +209,63 @@ const getReportSort = (grid: GridInstance) => { .join(MULTIVALUE_DELIMITER) } +const getReportColumnWidths = (grid: GridInstance) => { + const getVisibleColumns = (grid as any).getVisibleColumns + if (typeof getVisibleColumns !== 'function') { + return undefined + } + + const stateColumnWidths: Record = {} + const addStateColumnWidths = (columns: any[] | undefined) => { + columns?.forEach((column) => { + const fieldName = column?.dataField || column?.name + const width = Number(column?.width) + if (fieldName && Number.isFinite(width) && width > 0) { + stateColumnWidths[fieldName] = width + } + + if (Array.isArray(column?.columns)) { + addStateColumnWidths(column.columns) + } + }) + } + + if (supportsState(grid)) { + const state = grid.state() + if (Array.isArray(state?.columns)) { + addStateColumnWidths(state.columns) + } + } + + const visibleColumns = getVisibleColumns.call(grid) as any[] + const widths = visibleColumns.reduce((result: Record, column: any) => { + const fieldName = column?.dataField || column?.name + if (!fieldName || column?.type === 'buttons') { + return result + } + + const stateWidth = stateColumnWidths[fieldName] + const columnWidth = Number(column?.width) + const visibleWidth = Number(column?.visibleWidth) + const width = + Number.isFinite(stateWidth) && stateWidth > 0 + ? stateWidth + : Number.isFinite(columnWidth) && columnWidth > 0 + ? columnWidth + : Number.isFinite(visibleWidth) && visibleWidth > 0 + ? visibleWidth + : undefined + + if (width) { + result[fieldName] = Math.round(width) + } + + return result + }, {}) + + return Object.keys(widths).length ? widths : undefined +} + const createDynamicListReportUrl = ( grid: GridInstance | undefined, listFormCode: string, @@ -222,6 +279,7 @@ const createDynamicListReportUrl = ( const searchParams = new URLSearchParams() const filter = getReportFilter(grid) const sort = getReportSort(grid) + const columnWidths = getReportColumnWidths(grid) if (filter) { searchParams.set('filter', JSON.stringify(filter)) @@ -231,6 +289,10 @@ const createDynamicListReportUrl = ( searchParams.set('sort', sort) } + if (columnWidths) { + searchParams.set('columnWidths', JSON.stringify(columnWidths)) + } + const queryString = createReportQueryString(searchParams) return queryString ? `${reportUrl}?${queryString}` : reportUrl }