diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs index 3adff87..54cac25 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs @@ -5,11 +5,13 @@ using System.Globalization; using System.Linq; using System.Text.Json; using System.Text.RegularExpressions; +using System.Threading.Tasks; using DevExpress.DataAccess.ConnectionParameters; using DevExpress.DataAccess.Sql; using DevExpress.Drawing; using DevExpress.XtraPrinting; using DevExpress.XtraReports.UI; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; using Sozsoft.Platform; @@ -36,10 +38,10 @@ public class DynamicFormReport : XtraReport _serviceProvider = serviceProvider; } - public static DynamicFormReport Create(IDictionary parameters) + public static async Task CreateAsync(IDictionary parameters) { var report = new DynamicFormReport(); - report.Build(parameters); + await report.BuildAsync(parameters); return report; } @@ -47,7 +49,7 @@ public class DynamicFormReport : XtraReport { } - private void Build(IDictionary parameters) + private async Task BuildAsync(IDictionary parameters) { if (_serviceProvider == null) { @@ -73,23 +75,22 @@ public class DynamicFormReport : XtraReport var selectQueryManager = scope.ServiceProvider.GetRequiredService(); var defaultValueHelper = scope.ServiceProvider.GetRequiredService(); var localizer = scope.ServiceProvider.GetRequiredService>(); + var configuration = scope.ServiceProvider.GetRequiredService(); - if (!authManager.CanAccess(listFormCode).GetAwaiter().GetResult()) + if (!await authManager.CanAccess(listFormCode)) { throw new UnauthorizedAccessException($"No read permission for list form '{listFormCode}'."); } - var listForm = listFormManager.GetUserListForm(listFormCode).GetAwaiter().GetResult(); - var fields = listFormFieldManager.GetUserListFormFields(listFormCode).GetAwaiter().GetResult() + var listForm = await listFormManager.GetUserListForm(listFormCode); + var fields = (await listFormFieldManager.GetUserListFormFields(listFormCode)) .Where(x => x.CanRead) .OrderBy(x => x.ListOrderNo ?? int.MaxValue) .ThenBy(x => x.CaptionName) .ToList(); - var customizations = customizationManager.GetUsersServerCustomizations(listFormCode).GetAwaiter().GetResult(); - var (dynamicDataRepository, connectionString, dataSourceType) = dynamicDataManager - .GetAsync(listForm.IsTenant, listForm.DataSourceCode) - .GetAwaiter() - .GetResult(); + var customizations = await customizationManager.GetUsersServerCustomizations(listFormCode); + var (dynamicDataRepository, connectionString, dataSourceType) = await dynamicDataManager + .GetAsync(listForm.IsTenant, listForm.DataSourceCode); var queryParams = new QueryParameters { @@ -102,19 +103,18 @@ public class DynamicFormReport : XtraReport selectQueryManager.PrepareQueries(listForm, fields, dataSourceType, customizations, queryParams); var reportFields = GetReportFields(listForm, fields, selectQueryManager, defaultValueHelper); - var selectQuery = ApplyLookupQueryColumns( - selectQueryManager.SelectQuery, - listForm, + var selectQuery = StripReportPaging(ApplyQueryParameters(selectQueryManager.SelectQuery, selectQueryManager.SelectQueryParameters)); + selectQuery = DynamicReportSqlBuilder.ApplyLookupTextColumns( + selectQuery, reportFields, - defaultValueHelper); - selectQuery = ApplyQueryParameters(selectQuery, selectQueryManager.SelectQueryParameters); - selectQuery = StripReportPaging(selectQuery); + defaultValueHelper, + dataSourceType); - var row = GetFirstRow(dynamicDataRepository.QueryAsync(selectQuery, connectionString).GetAwaiter().GetResult()); + var row = GetFirstRow(await dynamicDataRepository.QueryAsync(selectQuery, connectionString)); ConfigurePage(); ConfigureDataSource(selectQuery, connectionString, dataSourceType); - BuildLayout(listForm, reportFields, row, localizer); + await BuildLayoutAsync(listForm, reportFields, row, localizer, GetImageBaseUrl(configuration)); RequestParameters = false; } @@ -146,11 +146,12 @@ public class DynamicFormReport : XtraReport DataMember = QueryName; } - private void BuildLayout( + private async Task BuildLayoutAsync( ListForm listForm, IReadOnlyList fields, IDictionary row, - IStringLocalizer localizer) + IStringLocalizer localizer, + string imageBaseUrl) { Bands.Clear(); @@ -208,7 +209,14 @@ public class DynamicFormReport : XtraReport var rowHeight = GetFieldRowHeight(field); var labelWidth = pageWidth * 0.28F; detail.Controls.Add(CreateCaptionLabel(field, localizer, 0, y, labelWidth, rowHeight)); - detail.Controls.Add(CreateValueLabel(field, row, labelWidth, y, pageWidth - labelWidth, rowHeight)); + if (field.IsImage) + { + detail.Controls.Add(CreateImageValuePanel(field, row, imageBaseUrl, labelWidth, y, pageWidth - labelWidth, rowHeight)); + } + else + { + detail.Controls.Add(CreateValueLabel(field, row, labelWidth, y, pageWidth - labelWidth, rowHeight)); + } y += rowHeight + 3; } @@ -220,7 +228,7 @@ public class DynamicFormReport : XtraReport detail.Controls.Add(new XRSubreport { BoundsF = new System.Drawing.RectangleF(0, y, pageWidth, 24), - ReportSource = DynamicGridReport.Create(CreateSubReportParameters(subForm.Form, subForm.Filter)) + ReportSource = await DynamicGridReport.CreateAsync(CreateSubReportParameters(subForm.Form, subForm.Filter)) }); y += 28; } @@ -251,7 +259,7 @@ public class DynamicFormReport : XtraReport }; } - private static XRLabel CreateValueLabel( + private static XRControl CreateValueLabel( FormReportField field, IDictionary row, float x, @@ -259,6 +267,11 @@ public class DynamicFormReport : XtraReport float width, float height) { + if (IsHtmlEditor(field)) + { + return CreateHtmlValueControl(field, row, x, y, width, height); + } + var label = new XRLabel { BoundsF = new System.Drawing.RectangleF(x, y, width, height), @@ -275,6 +288,7 @@ public class DynamicFormReport : XtraReport if (TryGetFormattedValue(field, row, out var text)) { label.Text = text; + label.Font = CreateValueFont(text); } else { @@ -284,6 +298,115 @@ public class DynamicFormReport : XtraReport return label; } + private static XRRichText CreateHtmlValueControl( + FormReportField field, + IDictionary row, + float x, + float y, + float width, + float height) + { + var richText = new XRRichText + { + BoundsF = new System.Drawing.RectangleF(x, y, width, height), + Font = new DXFont("Arial", 8F), + Borders = BorderSide.All, + BorderColor = FieldBorderColor, + BorderWidth = 0.25F, + Padding = new PaddingInfo(5, 5, 2, 2), + CanGrow = true + }; + + if (TryGetFormattedValue(field, row, out var html)) + { + richText.Html = html ?? string.Empty; + } + else + { + richText.ExpressionBindings.Add(new ExpressionBinding("BeforePrint", "Html", $"[{field.BindingName}]")); + } + + return richText; + } + + private static DXFont CreateValueFont(string text) + { + return ContainsEmoji(text) + ? new DXFont("Segoe UI Emoji", 8F) + : new DXFont("Arial", 8F); + } + + private static bool ContainsEmoji(string text) + { + if (string.IsNullOrEmpty(text)) + { + return false; + } + + foreach (var rune in text.EnumerateRunes()) + { + if (rune.Value is >= 0x1F000 and <= 0x1FAFF or + >= 0x2600 and <= 0x27BF or + >= 0x2300 and <= 0x23FF) + { + return true; + } + } + + return false; + } + + private static XRPanel CreateImageValuePanel( + FormReportField field, + IDictionary row, + string imageBaseUrl, + float x, + float y, + float width, + float height) + { + var panel = new XRPanel + { + BoundsF = new System.Drawing.RectangleF(x, y, width, height), + Borders = BorderSide.All, + BorderColor = FieldBorderColor, + BorderWidth = 0.25F + }; + + var rawValue = GetImageRawValue(field, row); + panel.Controls.AddRange(DynamicReportImageHelper.CreateFormPictureBoxes( + rawValue, + imageBaseUrl, + 0, + 0, + width, + height).ToArray()); + + return panel; + } + + private static object GetImageRawValue(FormReportField field, IDictionary row) + { + if (row == null) + { + return null; + } + + if (TryGetRowValue(row, field.BindingName, out var value) && HasValue(value)) + { + return value; + } + + return TryGetRowValue(row, field.Field.FieldName, out value) ? value : null; + } + + private static bool HasValue(object value) + { + return value != null && + value != DBNull.Value && + !string.IsNullOrWhiteSpace(Convert.ToString(value, CultureInfo.InvariantCulture)); + } + private static bool TryGetFormattedValue( FormReportField field, IDictionary row, @@ -327,6 +450,11 @@ public class DynamicFormReport : XtraReport private static float GetFieldRowHeight(FormReportField field) { + if (field.IsImage) + { + return DynamicReportImageHelper.GetFormImageHeight(); + } + return field.EditorType is PlatformConsts.EditorTypes.dxTextArea or PlatformConsts.EditorTypes.dxHtmlEditor ? 48 : RowHeight; @@ -489,25 +617,27 @@ public class DynamicFormReport : XtraReport if (lookup == null || selectField?.JoinOptions != null) { - return new FormReportField(field, bindingName, editorType, null, null); + return new FormReportField(field, bindingName, editorType, null, null, DynamicReportImageHelper.IsImageEditor(editorType)); } - if (IsReportQueryableLookup(lookup)) + if (DynamicReportSqlBuilder.IsReportQueryableLookup(lookup)) { - return new FormReportField(field, GetLookupBindingName(field), editorType, lookup, null); + return new FormReportField(field, DynamicReportSqlBuilder.GetLookupBindingName(field), editorType, lookup, null, false); } if (lookup.DataSourceType == UiLookupDataSourceTypeEnum.StaticData) { + var staticLookupValues = GetStaticLookupValues(lookup, defaultValueHelper); return new FormReportField( field, - bindingName, + staticLookupValues?.Count > 0 ? DynamicReportSqlBuilder.GetLookupBindingName(field) : bindingName, editorType, lookup, - GetStaticLookupValues(lookup, defaultValueHelper)); + staticLookupValues, + false); } - return new FormReportField(field, bindingName, editorType, null, null); + return new FormReportField(field, bindingName, editorType, null, null, DynamicReportImageHelper.IsImageEditor(editorType)); } private static string CreateKeyFilter(string keyFieldName, string id) @@ -721,100 +851,6 @@ public class DynamicFormReport : XtraReport }; } - private static string ApplyLookupQueryColumns( - string selectQuery, - ListForm listForm, - IReadOnlyList reportFields, - DefaultValueHelper defaultValueHelper) - { - var lookupFields = reportFields - .Where(x => x.Lookup != null && IsReportQueryableLookup(x.Lookup)) - .ToList(); - - if (lookupFields.Count == 0 || string.IsNullOrWhiteSpace(selectQuery)) - { - return selectQuery; - } - - var tableAlias = string.IsNullOrWhiteSpace(listForm.TableName) - ? listForm.SelectCommand - : listForm.TableName; - - var selectParts = new List(); - var joinParts = new List(); - foreach (var field in lookupFields) - { - var lookup = field.Lookup; - var alias = GetLookupAlias(field.Field); - var valueExpr = QuoteSqlIdentifier(lookup.ValueExpr ?? "Key"); - var displayExpr = QuoteSqlIdentifier(lookup.DisplayExpr ?? "Name"); - var lookupQuery = NormalizeLookupQuery(defaultValueHelper.GetDefaultValue(lookup.LookupQuery)); - - selectParts.Add($"{alias}.{displayExpr} AS {QuoteSqlIdentifier(field.BindingName)}"); - joinParts.Add( - $"LEFT JOIN ({lookupQuery}) AS {alias} " + - $"ON {alias}.{valueExpr} = {QuoteSqlIdentifier(tableAlias)}.{QuoteSqlIdentifier(field.Field.FieldName)}"); - } - - var fromIndex = selectQuery.IndexOf(" FROM ", StringComparison.OrdinalIgnoreCase); - if (fromIndex < 0) - { - return selectQuery; - } - - selectQuery = selectQuery.Insert(fromIndex, "," + string.Join(',', selectParts)); - - var joinInsertIndex = FindClauseIndex(selectQuery, " WHERE "); - if (joinInsertIndex < 0) - { - joinInsertIndex = FindClauseIndex(selectQuery, " ORDER BY "); - } - if (joinInsertIndex < 0) - { - return selectQuery + " " + string.Join(' ', joinParts); - } - - return selectQuery.Insert(joinInsertIndex, " " + string.Join(' ', joinParts)); - } - - private static bool IsReportQueryableLookup(LookupDto lookup) - { - return lookup.DataSourceType == UiLookupDataSourceTypeEnum.Query && - string.IsNullOrWhiteSpace(lookup.CascadeParentFields) && - !string.IsNullOrWhiteSpace(lookup.LookupQuery) && - !Regex.IsMatch(lookup.LookupQuery, @"@\s*param\d+", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); - } - - private static int FindClauseIndex(string sql, string clause) - { - return sql.IndexOf(clause, StringComparison.OrdinalIgnoreCase); - } - - private static string NormalizeLookupQuery(string lookupQuery) - { - var query = lookupQuery.Trim().TrimEnd(';'); - return Regex.Replace( - query, - @"(? localizer) { var title = string.IsNullOrWhiteSpace(listForm.Title) ? listForm.Name : listForm.Title; @@ -863,6 +899,11 @@ public class DynamicFormReport : XtraReport field.Field.SourceDbType == DbType.Boolean; } + private static bool IsHtmlEditor(FormReportField field) + { + return field.EditorType == PlatformConsts.EditorTypes.dxHtmlEditor; + } + private static bool TryFormatBoolean(string value, out string text) { if (bool.TryParse(value, out var boolean)) @@ -892,6 +933,11 @@ public class DynamicFormReport : XtraReport return parameters.TryGetValue(name, out var value) ? value : null; } + private static string GetImageBaseUrl(IConfiguration configuration) + { + return configuration["App:CdnUrl"] ?? configuration["App:SelfUrl"]; + } + private sealed record FieldWithEditor(ListFormField Field, string EditorType); private sealed record FormGroup(string Caption, IReadOnlyList Fields); private sealed record FormReportField( @@ -899,5 +945,6 @@ public class DynamicFormReport : XtraReport string BindingName, string EditorType, LookupDto Lookup, - Dictionary StaticLookupValues); + Dictionary StaticLookupValues, + bool IsImage) : DynamicReportSqlBuilder.IReportLookupColumn; } diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs index 4fbff53..c10fc68 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs @@ -5,13 +5,16 @@ using System.Globalization; using System.Linq; using System.Text.Json; using System.Text.RegularExpressions; +using System.Threading.Tasks; using DevExpress.DataAccess.ConnectionParameters; using DevExpress.DataAccess.Sql; using DevExpress.Drawing; using DevExpress.XtraPrinting; using DevExpress.XtraReports.UI; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; +using Sozsoft.Platform; using Sozsoft.Platform.DynamicData; using Sozsoft.Platform.Entities; using Sozsoft.Platform.Enums; @@ -36,10 +39,10 @@ public class DynamicGridReport : XtraReport _serviceProvider = serviceProvider; } - public static DynamicGridReport Create(IDictionary parameters) + public static async Task CreateAsync(IDictionary parameters) { var report = new DynamicGridReport(); - report.Build(parameters); + await report.BuildAsync(parameters); return report; } @@ -47,7 +50,7 @@ public class DynamicGridReport : XtraReport { } - private void Build(IDictionary parameters) + private async Task BuildAsync(IDictionary parameters) { if (_serviceProvider == null) { @@ -68,20 +71,21 @@ public class DynamicGridReport : XtraReport var selectQueryManager = scope.ServiceProvider.GetRequiredService(); var defaultValueHelper = scope.ServiceProvider.GetRequiredService(); var localizer = scope.ServiceProvider.GetRequiredService>(); + var configuration = scope.ServiceProvider.GetRequiredService(); - if (!authManager.CanAccess(listFormCode).GetAwaiter().GetResult()) + if (!await authManager.CanAccess(listFormCode)) { throw new UnauthorizedAccessException($"No read permission for list form '{listFormCode}'."); } - var listForm = listFormManager.GetUserListForm(listFormCode).GetAwaiter().GetResult(); - var fields = listFormFieldManager.GetUserListFormFields(listFormCode).GetAwaiter().GetResult() + var listForm = await listFormManager.GetUserListForm(listFormCode); + var fields = (await listFormFieldManager.GetUserListFormFields(listFormCode)) .Where(x => x.CanRead) .OrderBy(x => x.ListOrderNo ?? int.MaxValue) .ThenBy(x => x.CaptionName) .ToList(); - var customizations = customizationManager.GetUsersServerCustomizations(listFormCode).GetAwaiter().GetResult(); - var (_, connectionString, dataSourceType) = dynamicDataManager.GetAsync(listForm.IsTenant, listForm.DataSourceCode).GetAwaiter().GetResult(); + var customizations = await customizationManager.GetUsersServerCustomizations(listFormCode); + var (dynamicDataRepository, connectionString, dataSourceType) = await dynamicDataManager.GetAsync(listForm.IsTenant, listForm.DataSourceCode); var queryParams = new QueryParameters { @@ -117,24 +121,31 @@ public class DynamicGridReport : XtraReport .Select(field => { var selectField = selectQueryManager.SelectFields.First(s => s.FieldName == field.FieldName); - return CreateReportColumn(field, selectField, defaultValueHelper); + return CreateReportColumn( + field, + selectField, + defaultValueHelper, + DynamicReportImageHelper.HasImageEditor(listForm, field.FieldName), + HasHtmlEditor(listForm, field.FieldName)); }) .Where(x => !string.IsNullOrWhiteSpace(x.BindingName) && x.BindingName != "undefined") .ToList(); - var selectQuery = ApplyLookupQueryColumns( - selectQueryManager.SelectQuery, - listForm, + var selectQuery = StripReportPaging(ApplyQueryParameters(selectQueryManager.SelectQuery, selectQueryManager.SelectQueryParameters)); + selectQuery = DynamicReportSqlBuilder.ApplyLookupTextColumns( + selectQuery, reportColumns, - defaultValueHelper); - selectQuery = StripReportPaging(ApplyQueryParameters(selectQuery, selectQueryManager.SelectQueryParameters)); + defaultValueHelper, + dataSourceType); + var imageBaseUrl = GetImageBaseUrl(configuration); + reportColumns = await ApplyImageCountsAsync(reportColumns, dynamicDataRepository, selectQuery, connectionString, imageBaseUrl); ConfigurePage(reportColumns); ConfigureDataSource( selectQuery, connectionString, dataSourceType); - BuildLayout(listForm, reportColumns, localizer); + BuildLayout(listForm, reportColumns, localizer, imageBaseUrl); RequestParameters = false; } @@ -144,7 +155,7 @@ public class DynamicGridReport : XtraReport Margins = CreateReportMargins(HorizontalMargin, HorizontalMargin, 35, 35); PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4; var portraitContentWidth = A4PortraitWidth - Margins.Left - Margins.Right; - var requestedContentWidth = visibleFields.Sum(x => Math.Max(x.Field.Width ?? 90, 60)); + var requestedContentWidth = visibleFields.Sum(x => x.IsImage ? GetImageColumnWidth(x) : GetPreferredColumnWidth(x.Field)); Landscape = visibleFields.Count >= LandscapeColumnThreshold || requestedContentWidth > portraitContentWidth; Font = new DXFont("Arial", 8.25F); } @@ -242,10 +253,80 @@ public class DynamicGridReport : XtraReport }; } + private static async Task> ApplyImageCountsAsync( + IReadOnlyList columns, + IDynamicDataRepository dynamicDataRepository, + string selectQuery, + string connectionString, + string imageBaseUrl) + { + var imageColumns = columns.Where(x => x.IsImage).ToList(); + if (imageColumns.Count == 0) + { + return columns.ToList(); + } + + var imageCounts = imageColumns.ToDictionary(x => x.BindingName, _ => 1, StringComparer.OrdinalIgnoreCase); + var rows = await dynamicDataRepository.QueryAsync(selectQuery, connectionString); + foreach (var row in rows.OfType>()) + { + foreach (var column in imageColumns) + { + var count = DynamicReportImageHelper.CountImageSources( + GetImageRawValue(row, column), + imageBaseUrl); + + if (count > imageCounts[column.BindingName]) + { + imageCounts[column.BindingName] = count; + } + } + } + + return columns + .Select(column => column.IsImage + ? column with { ImageCount = imageCounts[column.BindingName] } + : column) + .ToList(); + } + + private static object GetImageRawValue(IDictionary row, ReportColumn column) + { + if (TryGetRowValue(row, column.BindingName, out var value) && HasValue(value)) + { + return value; + } + + return TryGetRowValue(row, column.Field.FieldName, out value) ? value : null; + } + + private static bool HasValue(object value) + { + return value != null && + value != DBNull.Value && + !string.IsNullOrWhiteSpace(Convert.ToString(value, CultureInfo.InvariantCulture)); + } + + private static bool TryGetRowValue(IDictionary row, string name, out object value) + { + foreach (var item in row) + { + if (string.Equals(item.Key, name, StringComparison.OrdinalIgnoreCase)) + { + value = item.Value; + return true; + } + } + + value = null; + return false; + } + private void BuildLayout( ListForm listForm, IReadOnlyList fields, - IStringLocalizer localizer) + IStringLocalizer localizer, + string imageBaseUrl) { Bands.Clear(); @@ -253,7 +334,12 @@ public class DynamicGridReport : XtraReport var bottomMargin = new BottomMarginBand { HeightF = 32 }; var reportHeader = new ReportHeaderBand { HeightF = 32 }; var pageHeader = new PageHeaderBand { HeightF = 28 }; - var detail = new DetailBand { HeightF = 24 }; + var detailHeight = fields.Any(x => x.IsImage) + ? DynamicReportImageHelper.GetGridImageHeight() + : fields.Any(x => x.IsHtml) + ? 48 + : 24; + var detail = new DetailBand { HeightF = detailHeight }; var reportFooter = new ReportFooterBand { HeightF = HasAnySummary(fields) ? 28 : 0 }; Bands.AddRange([topMargin, reportHeader, pageHeader, detail, reportFooter, bottomMargin]); @@ -288,17 +374,17 @@ public class DynamicGridReport : XtraReport var widths = CalculateWidths(fields, pageWidth); var headerTable = CreateTable(pageWidth, 28); - var headerRow = new XRTableRow(); - var detailTable = CreateTable(pageWidth, 24); - var detailRow = new XRTableRow(); + var headerRow = new XRTableRow { HeightF = 28 }; + var detailTable = CreateTable(pageWidth, detailHeight); + var detailRow = new XRTableRow { HeightF = detailHeight }; var footerTable = CreateTable(pageWidth, 24); - var footerRow = new XRTableRow(); + var footerRow = new XRTableRow { HeightF = 24 }; for (var i = 0; i < fields.Count; i++) { var field = fields[i]; headerRow.Cells.Add(CreateHeaderCell(field, widths[i], localizer)); - detailRow.Cells.Add(CreateDetailCell(field, widths[i])); + detailRow.Cells.Add(CreateDetailCell(field, widths[i], imageBaseUrl, detailHeight)); footerRow.Cells.Add(CreateFooterCell(field, widths[i])); } @@ -335,6 +421,7 @@ public class DynamicGridReport : XtraReport { Text = GetLocalizedFieldCaption(field, localizer), WidthF = width, + Weight = width, Font = new DXFont("Arial", 8F, DXFontStyle.Bold), BackColor = System.Drawing.Color.FromArgb(238, 242, 247), Padding = new PaddingInfo(4, 4, 2, 2), @@ -368,13 +455,29 @@ public class DynamicGridReport : XtraReport : localized.Value; } - private static XRTableCell CreateDetailCell(ReportColumn column, float width) + private static XRTableCell CreateDetailCell(ReportColumn column, float width, string imageBaseUrl, float height) { var field = column.Field; + if (column.IsImage) + { + return DynamicReportImageHelper.CreateGridImageCell( + column.BindingName, + column.Field.FieldName, + width, + imageBaseUrl, + column.ImageCount); + } + + if (column.IsHtml) + { + return CreateHtmlDetailCell(column, width, height); + } + var cell = new XRTableCell { WidthF = width, - Font = new DXFont("Arial", 8F), + Weight = width, + Font = new DXFont("Segoe UI Emoji", 8F), Padding = new PaddingInfo(4, 4, 2, 2), TextAlignment = ToTextAlignment(field.Alignment, field.SourceDbType), Multiline = false, @@ -385,6 +488,29 @@ public class DynamicGridReport : XtraReport return cell; } + private static XRTableCell CreateHtmlDetailCell(ReportColumn column, float width, float height) + { + var cell = new XRTableCell + { + WidthF = width, + Weight = width, + Padding = new PaddingInfo(0, 0, 0, 0), + CanGrow = false + }; + + var richText = new XRRichText + { + BoundsF = new System.Drawing.RectangleF(0, 0, width, height), + Font = new DXFont("Arial", 8F), + Padding = new PaddingInfo(4, 4, 2, 2), + CanGrow = false + }; + richText.ExpressionBindings.Add(new ExpressionBinding("BeforePrint", "Html", $"[{column.BindingName}]")); + cell.Controls.Add(richText); + + return cell; + } + private static XRTableCell CreateFooterCell(ReportColumn column, float width) { var field = column.Field; @@ -392,6 +518,7 @@ public class DynamicGridReport : XtraReport var cell = new XRTableCell { WidthF = width, + Weight = width, Font = new DXFont("Arial", 8F, DXFontStyle.Bold), Padding = new PaddingInfo(4, 4, 2, 2), TextAlignment = ToTextAlignment(field.Alignment, field.SourceDbType) @@ -427,7 +554,7 @@ public class DynamicGridReport : XtraReport private static float[] CalculateWidths(IReadOnlyList fields, float pageWidth) { - var raw = fields.Select(x => (float)Math.Max(x.Field.Width ?? 90, 60)).ToArray(); + var raw = fields.Select(x => x.IsImage ? GetImageColumnWidth(x) : GetPreferredColumnWidth(x.Field)).ToArray(); var total = raw.Sum(); if (total <= 0) { @@ -437,11 +564,64 @@ public class DynamicGridReport : XtraReport 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 (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); } + private static float GetImageColumnWidth(ReportColumn column) + { + return DynamicReportImageHelper.GetGridImageColumnWidth(column.ImageCount); + } + private static string GetReportFieldName(SelectField selectField) { if (selectField?.JoinOptions != null && !string.IsNullOrWhiteSpace(selectField.JoinOptions.FieldNameAlias)) @@ -455,19 +635,21 @@ public class DynamicGridReport : XtraReport private static ReportColumn CreateReportColumn( ListFormField field, SelectField selectField, - DefaultValueHelper defaultValueHelper) + DefaultValueHelper defaultValueHelper, + bool isImage, + bool isHtml) { var bindingName = GetReportFieldName(selectField); var lookup = GetLookup(field); if (lookup == null || selectField?.JoinOptions != null) { - return new ReportColumn(field, bindingName, null, null); + return new ReportColumn(field, bindingName, null, null, isImage, isHtml); } - if (IsReportQueryableLookup(lookup)) + if (DynamicReportSqlBuilder.IsReportQueryableLookup(lookup)) { - return new ReportColumn(field, GetLookupBindingName(field), lookup, null); + return new ReportColumn(field, DynamicReportSqlBuilder.GetLookupBindingName(field), lookup, null, isImage, isHtml); } if (lookup.DataSourceType == UiLookupDataSourceTypeEnum.StaticData) @@ -475,20 +657,35 @@ public class DynamicGridReport : XtraReport var staticLookupValues = GetStaticLookupValues(lookup, defaultValueHelper); return new ReportColumn( field, - staticLookupValues?.Count > 0 ? GetLookupBindingName(field) : bindingName, + staticLookupValues?.Count > 0 ? DynamicReportSqlBuilder.GetLookupBindingName(field) : bindingName, lookup, - staticLookupValues); + staticLookupValues, + isImage, + isHtml); } - return new ReportColumn(field, bindingName, null, null); + return new ReportColumn(field, bindingName, null, null, isImage, isHtml); } - private static bool IsReportQueryableLookup(LookupDto lookup) + private static bool HasHtmlEditor(ListForm listForm, string fieldName) { - return lookup.DataSourceType == UiLookupDataSourceTypeEnum.Query && - string.IsNullOrWhiteSpace(lookup.CascadeParentFields) && - !string.IsNullOrWhiteSpace(lookup.LookupQuery) && - !Regex.IsMatch(lookup.LookupQuery, @"@\s*param\d+", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + if (string.IsNullOrWhiteSpace(listForm?.EditingFormJson) || string.IsNullOrWhiteSpace(fieldName)) + { + return false; + } + + try + { + var groups = JsonSerializer.Deserialize>(listForm.EditingFormJson) ?? []; + return groups + .SelectMany(x => x.Items ?? []) + .Any(x => string.Equals(x.DataField, fieldName, StringComparison.OrdinalIgnoreCase) && + x.EditorType2 == PlatformConsts.EditorTypes.dxHtmlEditor); + } + catch (JsonException) + { + return false; + } } private static LookupDto GetLookup(ListFormField field) @@ -582,119 +779,15 @@ public class DynamicGridReport : XtraReport }; } - private static string ApplyLookupQueryColumns( - string selectQuery, - ListForm listForm, - IReadOnlyList reportColumns, - DefaultValueHelper defaultValueHelper) - { - var queryLookupColumns = reportColumns - .Where(x => x.Lookup != null && IsReportQueryableLookup(x.Lookup)) - .ToList(); - var staticLookupColumns = reportColumns - .Where(x => x.StaticLookupValues?.Count > 0) - .ToList(); - - if ((queryLookupColumns.Count == 0 && staticLookupColumns.Count == 0) || - string.IsNullOrWhiteSpace(selectQuery)) - { - return selectQuery; - } - - var tableAlias = string.IsNullOrWhiteSpace(listForm.TableName) - ? listForm.SelectCommand - : listForm.TableName; - - var selectParts = new List(); - var joinParts = new List(); - foreach (var column in staticLookupColumns) - { - selectParts.Add(CreateStaticLookupSelectPart(column, tableAlias)); - } - - foreach (var column in queryLookupColumns) - { - var lookup = column.Lookup; - var alias = GetLookupAlias(column.Field); - var valueExpr = QuoteSqlIdentifier(lookup.ValueExpr ?? "Key"); - var displayExpr = QuoteSqlIdentifier(lookup.DisplayExpr ?? "Name"); - var lookupQuery = NormalizeLookupQuery(defaultValueHelper.GetDefaultValue(lookup.LookupQuery)); - - selectParts.Add($"{alias}.{displayExpr} AS {QuoteSqlIdentifier(column.BindingName)}"); - joinParts.Add( - $"LEFT JOIN ({lookupQuery}) AS {alias} " + - $"ON {alias}.{valueExpr} = {QuoteSqlIdentifier(tableAlias)}.{QuoteSqlIdentifier(column.Field.FieldName)}"); - } - - var fromIndex = selectQuery.IndexOf(" FROM ", StringComparison.OrdinalIgnoreCase); - if (fromIndex < 0) - { - return selectQuery; - } - - selectQuery = selectQuery.Insert(fromIndex, "," + string.Join(',', selectParts)); - - var joinInsertIndex = FindClauseIndex(selectQuery, " WHERE "); - if (joinInsertIndex < 0) - { - joinInsertIndex = FindClauseIndex(selectQuery, " ORDER BY "); - } - if (joinParts.Count == 0) - { - return selectQuery; - } - if (joinInsertIndex < 0) - { - return selectQuery + " " + string.Join(' ', joinParts); - } - - return selectQuery.Insert(joinInsertIndex, " " + string.Join(' ', joinParts)); - } - - private static string CreateStaticLookupSelectPart(ReportColumn column, string tableAlias) - { - var sourceField = $"{QuoteSqlIdentifier(tableAlias)}.{QuoteSqlIdentifier(column.Field.FieldName)}"; - var cases = column.StaticLookupValues - .Select(item => $"WHEN {ToSqlLiteral(item.Key)} THEN {ToSqlLiteral(item.Value)}"); - - return $"CASE {sourceField} {string.Join(" ", cases)} ELSE CAST({sourceField} AS VARCHAR(4000)) END AS {QuoteSqlIdentifier(column.BindingName)}"; - } - - private static int FindClauseIndex(string sql, string clause) - { - return sql.IndexOf(clause, StringComparison.OrdinalIgnoreCase); - } - - private static string NormalizeLookupQuery(string lookupQuery) - { - var query = lookupQuery.Trim().TrimEnd(';'); - return Regex.Replace( - query, - @"(? StaticLookupValues); + Dictionary StaticLookupValues, + bool IsImage, + bool IsHtml, + int ImageCount = 1) : DynamicReportSqlBuilder.IReportLookupColumn; private static SummaryFunc? GetSummaryType(string totalSummaryJson) { @@ -749,4 +842,9 @@ public class DynamicGridReport : XtraReport { return parameters.TryGetValue(name, out var value) ? value : null; } + + private static string GetImageBaseUrl(IConfiguration configuration) + { + return configuration["App:CdnUrl"] ?? configuration["App:SelfUrl"]; + } } diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportImageHelper.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportImageHelper.cs new file mode 100644 index 0000000..9790d93 --- /dev/null +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportImageHelper.cs @@ -0,0 +1,754 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using DevExpress.Data.Filtering; +using DevExpress.XtraPrinting; +using DevExpress.XtraPrinting.Drawing; +using DevExpress.XtraReports.UI; +using Sozsoft.Platform; +using Sozsoft.Platform.Entities; +using Sozsoft.Platform.ListForms; + +namespace Sozsoft.Reports.PredefinedReports; + +internal static class DynamicReportImageHelper +{ + private const float DefaultGridImageHeight = 82F; + private const float DefaultGridImageWidth = 86F; + private const float DefaultFormImageHeight = 86F; + private const float MaxFormImageWidth = 86F; + private const int MaxFormImages = 8; + private const string ImageUrlFunctionName = "SozReportImageUrl"; + private const string ImageSourceFunctionName = "SozReportImageSource"; + private static readonly HttpClient HttpClient = new(new HttpClientHandler + { + ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator + }) + { + Timeout = TimeSpan.FromSeconds(5) + }; + + static DynamicReportImageHelper() + { + RegisterImageFunction(new ReportImageUrlFunction()); + RegisterImageFunction(new ReportImageSourceFunction()); + } + + public static bool IsImageEditor(string editorType) + { + return editorType is PlatformConsts.EditorTypes.dxImageUpload or PlatformConsts.EditorTypes.dxImageViewer; + } + + public static bool HasImageEditor(ListForm listForm, string fieldName) + { + if (string.IsNullOrWhiteSpace(listForm?.EditingFormJson) || string.IsNullOrWhiteSpace(fieldName)) + { + return false; + } + + try + { + var groups = JsonSerializer.Deserialize>(listForm.EditingFormJson) ?? []; + return groups + .SelectMany(x => x.Items ?? []) + .Any(x => string.Equals(x.DataField, fieldName, StringComparison.OrdinalIgnoreCase) && + IsImageEditor(x.EditorType2)); + } + catch (JsonException) + { + return false; + } + } + + public static float GetGridImageHeight() => DefaultGridImageHeight; + + public static float GetFormImageHeight() => DefaultFormImageHeight; + + public static XRTableCell CreateGridImageCell( + string bindingName, + string fieldName, + float width, + string baseUrl, + int imageCount) + { + var safeImageCount = Math.Max(imageCount, 1); + var cell = new XRTableCell + { + WidthF = width, + Weight = width, + Padding = new PaddingInfo(3, 3, 3, 3), + CanGrow = false + }; + + for (var i = 0; i < safeImageCount; i++) + { + var pictureBox = CreatePictureBox(i, width, DefaultGridImageHeight - 6, safeImageCount); + pictureBox.Visible = true; + pictureBox.ExpressionBindings.Add(new ExpressionBinding( + "BeforePrint", + "ImageUrl", + $"{ImageUrlFunctionName}([{bindingName}], '{EscapeExpressionString(baseUrl)}', {i})")); + pictureBox.ExpressionBindings.Add(new ExpressionBinding( + "BeforePrint", + "ImageSource", + $"{ImageSourceFunctionName}([{bindingName}], '{EscapeExpressionString(baseUrl)}', {i})")); + cell.Controls.Add(pictureBox); + } + + return cell; + } + + public static float GetGridImageColumnWidth(int imageCount) + { + var safeImageCount = Math.Max(imageCount, 1); + return safeImageCount * DefaultGridImageWidth + (safeImageCount - 1) * 4F + 8F; + } + + private static object GetCurrentValue(XRControl control, string bindingName, string fieldName) + { + if (HasImageValue(control.Tag)) + { + return control.Tag; + } + + var report = control.Report; + if (report == null) + { + return null; + } + + var value = GetCurrentColumnValue(report, bindingName); + return HasImageValue(value) || string.Equals(bindingName, fieldName, StringComparison.OrdinalIgnoreCase) + ? value + : GetCurrentColumnValue(report, fieldName); + } + + private static object GetCurrentColumnValue(XtraReportBase report, string fieldName) + { + if (string.IsNullOrWhiteSpace(fieldName)) + { + return null; + } + + try + { + return report.GetCurrentColumnValue(fieldName); + } + catch (InvalidOperationException) + { + return null; + } + catch (ArgumentException) + { + return null; + } + } + + private static bool HasValue(object value) + { + return value != null && + value != DBNull.Value && + !string.IsNullOrWhiteSpace(Convert.ToString(value)); + } + + private static bool HasImageValue(object value) + { + return HasValue(value) && NormalizeImageValue(value).Any(IsPotentialImageSource); + } + + private static bool IsPotentialImageSource(string value) + { + if (string.IsNullOrWhiteSpace(value) || + value.StartsWith("System.", StringComparison.Ordinal)) + { + return false; + } + + return value.StartsWith("data:image/", StringComparison.OrdinalIgnoreCase) || + value.StartsWith("/", StringComparison.Ordinal) || + IsProbablyBase64Image(value) || + Uri.TryCreate(value, UriKind.Absolute, out _); + } + + public static IReadOnlyList CreateFormPictureBoxes( + object rawValue, + string baseUrl, + float x, + float y, + float width, + float height) + { + var innerWidth = Math.Max(width - 10, 1); + var innerHeight = Math.Max(height - 10, 1); + var imageSources = ResolveImageSources(rawValue, baseUrl, MaxFormImages).ToList(); + var pictureBoxes = CreateFormPictureBoxes(innerWidth, innerHeight, Math.Max(imageSources.Count, 1)) + .Select(picture => + { + picture.LeftF += x + 5; + picture.TopF += y + 5; + return picture; + }) + .ToList(); + + ApplyImages(pictureBoxes, imageSources); + return pictureBoxes; + } + + public static int CountImageSources(object rawValue, string baseUrl) + { + return ResolveImageSources(rawValue, baseUrl, int.MaxValue).Count(); + } + + private static XRPictureBox CreatePictureBox(int index, float width, float height, int count) + { + var gap = 4F; + var safeCount = Math.Max(count, 1); + var boxWidth = safeCount == 1 + ? width + : Math.Max((width - gap * (safeCount - 1)) / safeCount, 1); + + return CreatePictureBox(index * (boxWidth + gap), 0, boxWidth, height); + } + + private static XRPictureBox CreatePictureBox(float x, float y, float width, float height) + { + return new XRPictureBox + { + BoundsF = new System.Drawing.RectangleF(x, y, width, height), + Sizing = ImageSizeMode.ZoomImage, + Visible = false + }; + } + + private static IEnumerable CreateFormPictureBoxes(float width, float height, int count) + { + var safeCount = Math.Max(count, 1); + var gap = 8F; + var boxWidth = Math.Min(MaxFormImageWidth, Math.Max((width - gap * (safeCount - 1)) / safeCount, 1)); + + for (var i = 0; i < safeCount; i++) + { + yield return CreatePictureBox(i * (boxWidth + gap), 0, boxWidth, height); + } + } + + private static void ArrangePictureBoxes( + IReadOnlyList pictureBoxes, + float width, + float height, + int visibleCount) + { + var safeCount = Math.Max(visibleCount, 1); + var gap = 4F; + var boxWidth = safeCount == 1 + ? width + : Math.Max((width - gap * (safeCount - 1)) / safeCount, 1); + + for (var i = 0; i < pictureBoxes.Count; i++) + { + pictureBoxes[i].BoundsF = new System.Drawing.RectangleF( + i * (boxWidth + gap), + 0, + boxWidth, + height); + } + } + + private static string GetImageSourceAt(object rawValue, string baseUrl, int index) + { + if (index < 0) + { + return null; + } + + return ResolveImageSources(rawValue, baseUrl, index + 1).ElementAtOrDefault(index); + } + + private static string GetImageUrlAt(object rawValue, string baseUrl, int index) + { + var imageSource = GetImageSourceAt(rawValue, baseUrl, index); + return Uri.TryCreate(imageSource, UriKind.Absolute, out var uri) && + uri.Scheme is "http" or "https" + ? imageSource + : null; + } + + private static ImageSource GetImageSourceObjectAt(object rawValue, string baseUrl, int index) + { + var imageSource = GetImageSourceAt(rawValue, baseUrl, index); + if (string.IsNullOrWhiteSpace(imageSource) || + !TryGetImageBytes(imageSource, out var bytes)) + { + return null; + } + + try + { + return new ImageSource(false, bytes); + } + catch (ArgumentException) + { + return null; + } + catch (InvalidOperationException) + { + return null; + } + } + + private static void ApplyImages(IReadOnlyList pictureBoxes, IReadOnlyList imageSources) + { + for (var i = 0; i < pictureBoxes.Count; i++) + { + ApplyImage(pictureBoxes[i], i < imageSources.Count ? imageSources[i] : null); + } + } + + private static void ApplyImage(XRPictureBox picture, string imageSource) + { + picture.ImageSource = null; + picture.ImageUrl = null; + picture.Visible = false; + + if (string.IsNullOrWhiteSpace(imageSource)) + { + return; + } + + if (Uri.TryCreate(imageSource, UriKind.Absolute, out var uri) && + uri.Scheme is "http" or "https") + { + picture.ImageUrl = imageSource; + picture.Visible = true; + return; + } + + if (TryGetImageBytes(imageSource, out var bytes)) + { + try + { + picture.ImageSource = new ImageSource(false, bytes); + picture.Visible = true; + return; + } + catch (ArgumentException) + { + picture.ImageSource = null; + } + catch (InvalidOperationException) + { + picture.ImageSource = null; + } + } + } + + private static string ResolveImageSource(string imageSource, string baseUrl) + { + if (string.IsNullOrWhiteSpace(imageSource) || + imageSource.StartsWith("data:image/", StringComparison.OrdinalIgnoreCase) || + Uri.TryCreate(imageSource, UriKind.Absolute, out _)) + { + return imageSource; + } + + if (string.IsNullOrWhiteSpace(baseUrl)) + { + return imageSource; + } + + return $"{baseUrl.TrimEnd('/')}/{imageSource.TrimStart('/')}"; + } + + private static IEnumerable ResolveImageSources(object rawValue, string baseUrl, int take) + { + return NormalizeImageValue(rawValue) + .Select(x => ResolveImageSource(x, baseUrl)) + .Where(IsPotentialImageSource) + .Take(take); + } + + private static bool TryGetImageBytes(string imageSource, out byte[] bytes) + { + bytes = null; + + if (TryGetDataUrlBytes(imageSource, out bytes)) + { + return true; + } + + if (IsProbablyBase64Image(imageSource)) + { + try + { + bytes = Convert.FromBase64String(imageSource); + return bytes.Length > 0; + } + catch (FormatException) + { + bytes = null; + return false; + } + } + + if (!Uri.TryCreate(imageSource, UriKind.Absolute, out var uri) || + uri.Scheme is not ("http" or "https")) + { + return false; + } + + try + { + bytes = HttpClient.GetByteArrayAsync(uri).GetAwaiter().GetResult(); + return bytes?.Length > 0; + } + catch (HttpRequestException) + { + bytes = null; + return false; + } + catch (TaskCanceledException) + { + bytes = null; + return false; + } + catch (InvalidOperationException) + { + bytes = null; + return false; + } + } + + private static bool TryGetDataUrlBytes(string imageSource, out byte[] bytes) + { + bytes = null; + var match = Regex.Match( + imageSource, + @"^data:image/[^;]+;base64,(?.+)$", + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + if (!match.Success) + { + return false; + } + + try + { + bytes = Convert.FromBase64String(match.Groups["data"].Value); + return bytes.Length > 0; + } + catch (FormatException) + { + bytes = null; + return false; + } + } + + private static IEnumerable NormalizeImageValue(object value) + { + if (value == null || value == DBNull.Value) + { + yield break; + } + + if (value is JsonElement element) + { + foreach (var source in NormalizeJsonElement(element)) + { + yield return source; + } + + yield break; + } + + if (value is IEnumerable enumerable && value is not string) + { + foreach (var item in enumerable) + { + foreach (var source in NormalizeImageValue(item)) + { + yield return source; + } + } + + yield break; + } + + var text = Convert.ToString(value)?.Trim(); + if (string.IsNullOrWhiteSpace(text)) + { + yield break; + } + + if (text.StartsWith("[", StringComparison.Ordinal)) + { + foreach (var source in NormalizeJsonArray(text)) + { + yield return source; + } + + yield break; + } + + foreach (var source in SplitImageString(text)) + { + yield return source; + } + } + + private static IEnumerable NormalizeJsonArray(string value) + { + JsonDocument document = null; + string fallbackSource = null; + try + { + document = JsonDocument.Parse(value); + } + catch (JsonException) + { + fallbackSource = ToImageSource(value); + } + + if (document == null) + { + if (!string.IsNullOrWhiteSpace(fallbackSource)) + { + yield return fallbackSource; + } + + yield break; + } + + using (document) + { + if (document.RootElement.ValueKind != JsonValueKind.Array) + { + yield break; + } + + foreach (var item in document.RootElement.EnumerateArray()) + { + foreach (var source in NormalizeJsonElement(item)) + { + yield return source; + } + } + } + } + + private static IEnumerable NormalizeJsonElement(JsonElement element) + { + if (element.ValueKind == JsonValueKind.String) + { + foreach (var source in SplitImageString(element.GetString())) + { + yield return source; + } + + yield break; + } + + if (element.ValueKind != JsonValueKind.Object) + { + yield break; + } + + foreach (var propertyName in new[] { "url", "imageUrl", "src", "fileUrl", "fileName", "path", "value" }) + { + if (TryGetJsonProperty(element, propertyName, out var property) && + property.ValueKind == JsonValueKind.String) + { + foreach (var source in SplitImageString(property.GetString())) + { + yield return source; + } + + yield break; + } + } + } + + private static bool TryGetJsonProperty(JsonElement element, string propertyName, out JsonElement value) + { + foreach (var property in element.EnumerateObject()) + { + if (string.Equals(property.Name, propertyName, StringComparison.OrdinalIgnoreCase)) + { + value = property.Value; + return true; + } + } + + value = default; + return false; + } + + private static IEnumerable SplitImageString(string value) + { + var text = value?.Trim(); + if (string.IsNullOrWhiteSpace(text)) + { + yield break; + } + + if (text.StartsWith("data:image/", StringComparison.OrdinalIgnoreCase)) + { + yield return text; + yield break; + } + + foreach (var item in SplitDelimitedImageString(text) + .Select(x => ToImageSource(x)) + .Where(x => !string.IsNullOrWhiteSpace(x))) + { + yield return item; + } + } + + private static IEnumerable SplitDelimitedImageString(string text) + { + var start = 0; + for (var i = 0; i < text.Length; i++) + { + if (!IsImageDelimiter(text, i)) + { + continue; + } + + yield return text[start..i]; + start = text[i] == '\r' && i + 1 < text.Length && text[i + 1] == '\n' + ? i + 2 + : i + 1; + if (start == i + 2) + { + i++; + } + } + + yield return text[start..]; + } + + private static bool IsImageDelimiter(string text, int index) + { + return text[index] == '|' || + text[index] == '\n' || + text[index] == '\r' || + (text[index] == ',' && !IsDataImageComma(text, index)); + } + + private static bool IsDataImageComma(string text, int index) + { + var dataImageIndex = text.LastIndexOf("data:image/", index, StringComparison.OrdinalIgnoreCase); + if (dataImageIndex < 0) + { + return false; + } + + var previousDelimiterIndex = text.LastIndexOfAny(['|', '\r', '\n'], index); + return previousDelimiterIndex < dataImageIndex && + text.IndexOf(";base64,", dataImageIndex, index - dataImageIndex + 1, StringComparison.OrdinalIgnoreCase) >= 0; + } + + private static string EscapeExpressionString(string value) + { + return (value ?? string.Empty).Replace("'", "''"); + } + + private static string ToImageSource(string value) + { + var text = value?.Trim(); + if (string.IsNullOrWhiteSpace(text)) + { + return null; + } + + if (text.StartsWith("data:image/", StringComparison.OrdinalIgnoreCase) || !IsProbablyBase64Image(text)) + { + return text; + } + + return $"data:image/jpeg;base64,{text}"; + } + + private static bool IsProbablyBase64Image(string value) + { + return value.Length > 80 && Regex.IsMatch(value, @"^[A-Za-z0-9+/]+={0,2}$", RegexOptions.CultureInvariant); + } + + private static void RegisterImageFunction(ICustomFunctionOperator function) + { + if (CriteriaOperator.GetCustomFunction(function.Name) == null) + { + CriteriaOperator.RegisterCustomFunction(function); + } + } + + private abstract class ReportImageFunctionBase : ICustomFunctionOperatorBrowsable + { + public int MinOperandCount => 3; + public int MaxOperandCount => 3; + public FunctionCategory Category => FunctionCategory.Text; + public string Description => "Returns a report image value."; + + public abstract string Name { get; } + protected abstract Type FunctionResultType { get; } + protected abstract object GetValue(object rawValue, string baseUrl, int index); + + public object Evaluate(params object[] operands) + { + return GetValue( + operands.ElementAtOrDefault(0), + Convert.ToString(operands.ElementAtOrDefault(1), CultureInfo.InvariantCulture), + ToIndex(operands.ElementAtOrDefault(2))); + } + + public Type ResultType(params Type[] operands) + { + return FunctionResultType; + } + + public bool IsValidOperandCount(int count) + { + return count == 3; + } + + public bool IsValidOperandType(int operandIndex, int operandCount, Type type) + { + return true; + } + + private static int ToIndex(object value) + { + return value == null || value == DBNull.Value + ? -1 + : Convert.ToInt32(value, CultureInfo.InvariantCulture); + } + } + + private sealed class ReportImageUrlFunction : ReportImageFunctionBase + { + public override string Name => ImageUrlFunctionName; + protected override Type FunctionResultType => typeof(string); + + protected override object GetValue(object rawValue, string baseUrl, int index) + { + return GetImageUrlAt(rawValue, baseUrl, index); + } + } + + private sealed class ReportImageSourceFunction : ReportImageFunctionBase + { + public override string Name => ImageSourceFunctionName; + protected override Type FunctionResultType => typeof(ImageSource); + + protected override object GetValue(object rawValue, string baseUrl, int index) + { + return GetImageSourceObjectAt(rawValue, baseUrl, index); + } + } +} diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportSqlBuilder.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportSqlBuilder.cs new file mode 100644 index 0000000..7602f33 --- /dev/null +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportSqlBuilder.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using Sozsoft.Platform; +using Sozsoft.Platform.Entities; +using Sozsoft.Platform.Enums; +using Sozsoft.Platform.ListForms; + +namespace Sozsoft.Reports.PredefinedReports; + +internal static class DynamicReportSqlBuilder +{ + private const string SourceAlias = "ReportSource"; + private const string LookupAlias = "LookupSource"; + + internal interface IReportLookupColumn + { + ListFormField Field { get; } + string BindingName { get; } + LookupDto Lookup { get; } + Dictionary StaticLookupValues { get; } + } + + internal static string GetLookupBindingName(ListFormField field) + { + return $"{field.FieldName}__LookupText"; + } + + internal static bool IsReportQueryableLookup(LookupDto lookup) + { + return lookup.DataSourceType == UiLookupDataSourceTypeEnum.Query && + !string.IsNullOrWhiteSpace(lookup.LookupQuery); + } + + internal static string ApplyLookupTextColumns( + string selectQuery, + IReadOnlyList columns, + DefaultValueHelper defaultValueHelper, + DataSourceTypeEnum dataSourceType) + { + if (string.IsNullOrWhiteSpace(selectQuery)) + { + return selectQuery; + } + + var lookupSelectParts = columns + .Select(column => CreateLookupSelectPart(column, defaultValueHelper, dataSourceType)) + .Where(part => !string.IsNullOrWhiteSpace(part)) + .ToList(); + + if (lookupSelectParts.Count == 0) + { + return selectQuery; + } + + var sourceAlias = QuoteSqlIdentifier(SourceAlias); + return $"SELECT {sourceAlias}.*, {string.Join(", ", lookupSelectParts)} FROM ({NormalizeSql(selectQuery)}) AS {sourceAlias}"; + } + + private static string CreateLookupSelectPart( + IReportLookupColumn column, + DefaultValueHelper defaultValueHelper, + DataSourceTypeEnum dataSourceType) + { + if (column.StaticLookupValues?.Count > 0) + { + return CreateStaticLookupSelectPart(column); + } + + if (column.Lookup != null && IsReportQueryableLookup(column.Lookup)) + { + return CreateQueryLookupSelectPart(column, defaultValueHelper, dataSourceType); + } + + return null; + } + + private static string CreateStaticLookupSelectPart(IReportLookupColumn column) + { + var sourceField = GetSourceField(column.Field.FieldName); + var cases = column.StaticLookupValues + .Select(item => $"WHEN {ToSqlLiteral(item.Key)} THEN {ToSqlLiteral(item.Value)}"); + + return $"CASE {sourceField} {string.Join(" ", cases)} ELSE CAST({sourceField} AS VARCHAR(4000)) END AS {QuoteSqlIdentifier(column.BindingName)}"; + } + + private static string CreateQueryLookupSelectPart( + IReportLookupColumn column, + DefaultValueHelper defaultValueHelper, + DataSourceTypeEnum dataSourceType) + { + var lookupQuery = NormalizeLookupQuery(defaultValueHelper.GetDefaultValue(column.Lookup.LookupQuery)); + if (string.IsNullOrWhiteSpace(lookupQuery)) + { + return null; + } + + var lookupAlias = QuoteSqlIdentifier(LookupAlias); + var valueExpr = QuoteSqlIdentifier(column.Lookup.ValueExpr ?? "Key"); + var displayExpr = QuoteSqlIdentifier(column.Lookup.DisplayExpr ?? "Name"); + var sourceField = GetSourceField(column.Field.FieldName); + var resultAlias = QuoteSqlIdentifier(column.BindingName); + + var scalarQuery = dataSourceType == DataSourceTypeEnum.Mssql + ? $"SELECT TOP 1 {lookupAlias}.{displayExpr} FROM ({lookupQuery}) AS {lookupAlias} WHERE {lookupAlias}.{valueExpr} = {sourceField}" + : $"SELECT {lookupAlias}.{displayExpr} FROM ({lookupQuery}) AS {lookupAlias} WHERE {lookupAlias}.{valueExpr} = {sourceField} LIMIT 1"; + + return $"({scalarQuery}) AS {resultAlias}"; + } + + private static string GetSourceField(string fieldName) + { + return $"{QuoteSqlIdentifier(SourceAlias)}.{QuoteSqlIdentifier(fieldName)}"; + } + + private static string NormalizeLookupQuery(string lookupQuery) + { + if (string.IsNullOrWhiteSpace(lookupQuery)) + { + return null; + } + + var query = lookupQuery.Trim().TrimEnd(';'); + query = Regex.Replace( + query, + @"(? $"'{text.Replace("'", "''")}'", + Guid guid => $"'{guid}'", + DateTime date => $"'{date:yyyy-MM-dd HH:mm:ss.fff}'", + DateTimeOffset date => $"'{date:yyyy-MM-dd HH:mm:ss.fff zzz}'", + bool boolean => boolean ? "1" : "0", + byte or sbyte or short or ushort or int or uint or long or ulong or float or double or decimal + => Convert.ToString(value, CultureInfo.InvariantCulture), + _ => $"'{value.ToString()?.Replace("'", "''")}'" + }; + } +} diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs b/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs index c085135..aabb060 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/ReportServices/CustomReportStorageWebExtension.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System; using System.Reflection; +using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; namespace Sozsoft.Platform.ReportServices; @@ -75,6 +76,79 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens } public override byte[] GetData(string url) + { + return GetDataCoreAsync(url).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + public override Task GetDataAsync(string url) + { + return GetDataCoreAsync(url); + } + + private static byte[] SaveReportLayout(XtraReport report) + { + try + { + return SaveReportLayoutCore(report); + } + catch (Exception ex) when (IsGdiPlusError(ex.GetBaseException())) + { + ClearEmbeddedPictureSources(report); + return SaveReportLayoutCore(report); + } + } + + private static byte[] SaveReportLayoutCore(XtraReport report) + { + using var ms = new MemoryStream(); + report.SaveLayoutToXml(ms); + return ms.ToArray(); + } + + private static bool IsGdiPlusError(Exception ex) + { + return ex?.Message.Contains("GDI+", StringComparison.OrdinalIgnoreCase) == true; + } + + private static void ClearEmbeddedPictureSources(XtraReport report) + { + foreach (var picture in EnumerateControls(report).OfType()) + { + if (picture.ImageSource == null || !string.IsNullOrWhiteSpace(picture.ImageUrl)) + { + continue; + } + + picture.ImageSource = null; + picture.Visible = false; + } + } + + private static IEnumerable EnumerateControls(XtraReport report) + { + foreach (Band band in report.Bands) + { + foreach (var control in EnumerateControls(band.Controls)) + { + yield return control; + } + } + } + + private static IEnumerable EnumerateControls(XRControlCollection controls) + { + foreach (XRControl control in controls) + { + yield return control; + + foreach (var child in EnumerateControls(control.Controls)) + { + yield return child; + } + } + } + + private async Task GetDataCoreAsync(string url) { // Uses a specified URL to return report layout data stored within a report storage medium. // This method is called if the **IsValidUrl** method returns **true**. @@ -86,90 +160,23 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens if (IsDynamicGridReport(reportName)) { - XtraReport report = HasParameter(parameters, "id") - ? Sozsoft.Reports.PredefinedReports.DynamicFormReport.Create(parameters) - : Sozsoft.Reports.PredefinedReports.DynamicGridReport.Create(parameters); - using MemoryStream ms = new MemoryStream(); - report.SaveLayoutToXml(ms); - return ms.ToArray(); + var report = HasParameter(parameters, "id") + ? (XtraReport)await Sozsoft.Reports.PredefinedReports.DynamicFormReport.CreateAsync(parameters) + : await Sozsoft.Reports.PredefinedReports.DynamicGridReport.CreateAsync(parameters); + + return SaveReportLayout(report); } if (IsDynamicFormReport(reportName)) { - var report = Sozsoft.Reports.PredefinedReports.DynamicFormReport.Create(parameters); - using MemoryStream ms = new MemoryStream(); - report.SaveLayoutToXml(ms); - return ms.ToArray(); + var report = await Sozsoft.Reports.PredefinedReports.DynamicFormReport.CreateAsync(parameters); + return SaveReportLayout(report); } - - // Önce Reports klasöründen yüklemeyi dene - if (Directory.EnumerateFiles(reportDirectory).Select(Path.GetFileNameWithoutExtension).Contains(reportName)) - { - var report = XtraReport.FromFile(Path.Combine(reportDirectory, reportName + FileExtension)); - - // SQL DataSource'daki filtreyi güncelle - if (parameters.Count > 0) - { - var sqlDataSource = report.ComponentStorage.OfType().FirstOrDefault(); - if (sqlDataSource != null && sqlDataSource.Queries.Count > 0) - { - var query = sqlDataSource.Queries[0] as DevExpress.DataAccess.Sql.SelectQuery; - if (query != null) - { - // FilterString'i oluştur - sadece kolon adını kullan - var filters = new List(); - foreach (var param in parameters) - { - // Capitalize first letter for column name matching (Name, Id, etc.) - var columnName = char.ToUpper(param.Key[0]) + param.Key.Substring(1); - filters.Add($"[{columnName}] = '{param.Value.Replace("'", "''")}'"); - } - query.FilterString = string.Join(" AND ", filters); - } - } - } - using MemoryStream ms = new MemoryStream(); - report.SaveLayoutToXml(ms); - return ms.ToArray(); - } - - // Reports klasöründe yoksa PredefinedReports namespace'inden yükle - var reportType = Assembly.GetExecutingAssembly() - .GetTypes() - .FirstOrDefault(t => t.Namespace == "Sozsoft.Reports.PredefinedReports" && - t.Name == reportName && - typeof(XtraReport).IsAssignableFrom(t)); - - if (reportType != null) + var reportData = TryGetStoredReportData(reportName, parameters); + if (reportData != null) { - var report = (XtraReport)Activator.CreateInstance(reportType); - - // SQL DataSource'daki filtreyi güncelle - if (parameters.Count > 0) - { - var sqlDataSource = report.ComponentStorage.OfType().FirstOrDefault(); - if (sqlDataSource != null && sqlDataSource.Queries.Count > 0) - { - var query = sqlDataSource.Queries[0] as DevExpress.DataAccess.Sql.SelectQuery; - if (query != null) - { - // FilterString'i oluştur - sadece kolon adını kullan - var filters = new List(); - foreach (var param in parameters) - { - // Capitalize first letter for column name matching (Name, Id, etc.) - var columnName = char.ToUpper(param.Key[0]) + param.Key.Substring(1); - filters.Add($"[{columnName}] = '{param.Value.Replace("'", "''")}'"); - } - query.FilterString = string.Join(" AND ", filters); - } - } - } - - using MemoryStream ms = new MemoryStream(); - report.SaveLayoutToXml(ms); - return ms.ToArray(); + return reportData; } } catch (Exception ex) @@ -180,6 +187,64 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens throw new FaultException(new FaultReason(string.Format("Could not find report '{0}'.", url)), new FaultCode("Server"), "GetData"); } + private byte[] TryGetStoredReportData(string reportName, Dictionary parameters) + { + var report = TryLoadStoredReport(reportName); + if (report == null) + { + return null; + } + + ApplyReportParameters(report, parameters); + return SaveReportLayout(report); + } + + private XtraReport TryLoadStoredReport(string reportName) + { + if (Directory.EnumerateFiles(reportDirectory).Select(Path.GetFileNameWithoutExtension).Contains(reportName)) + { + return XtraReport.FromFile(Path.Combine(reportDirectory, reportName + FileExtension)); + } + + var reportType = Assembly.GetExecutingAssembly() + .GetTypes() + .FirstOrDefault(t => t.Namespace == "Sozsoft.Reports.PredefinedReports" && + t.Name == reportName && + typeof(XtraReport).IsAssignableFrom(t)); + + return reportType == null + ? null + : (XtraReport)Activator.CreateInstance(reportType); + } + + private static void ApplyReportParameters(XtraReport report, Dictionary parameters) + { + if (parameters.Count == 0) + { + return; + } + + var sqlDataSource = report.ComponentStorage.OfType().FirstOrDefault(); + if (sqlDataSource == null || sqlDataSource.Queries.Count == 0) + { + return; + } + + if (sqlDataSource.Queries[0] is not DevExpress.DataAccess.Sql.SelectQuery query) + { + return; + } + + var filters = new List(); + foreach (var param in parameters) + { + var columnName = char.ToUpper(param.Key[0]) + param.Key.Substring(1); + filters.Add($"[{columnName}] = '{param.Value.Replace("'", "''")}'"); + } + + query.FilterString = string.Join(" AND ", filters); + } + private static (string ReportName, Dictionary Parameters) ParseReportUrl(string url) { var decodedUrl = DecodeRepeatedly(url); diff --git a/ui/src/views/form/FormButtons.tsx b/ui/src/views/form/FormButtons.tsx index 5903350..d559cdc 100644 --- a/ui/src/views/form/FormButtons.tsx +++ b/ui/src/views/form/FormButtons.tsx @@ -11,7 +11,7 @@ import { FaEye, FaSave, FaCog, - FaFileAlt, + FaSistrix, } from 'react-icons/fa' import { useNavigate } from 'react-router-dom' import { GridColumnData } from '../list/GridColumnData' @@ -160,8 +160,6 @@ const FormButtons = (props: { {hasVisibleToolbarButtons && } - - {commandColumnData?.buttons ?.filter((item) => typeof item !== 'string' && !commandExcludedNames.includes(item.name!)) .map((item: any, i) => { @@ -292,7 +290,7 @@ const FormButtons = (props: { size="sm" variant="default" className="dx-button dx-button-mode-contained dx-button-normal" - icon={} + icon={} title={translate('::App.Reports.Form')} onClick={openDynamicGridReport} > diff --git a/ui/src/views/report/DevexpressReportDesigner.tsx b/ui/src/views/report/DevexpressReportDesigner.tsx index 6e5616a..41a3669 100644 --- a/ui/src/views/report/DevexpressReportDesigner.tsx +++ b/ui/src/views/report/DevexpressReportDesigner.tsx @@ -6,18 +6,34 @@ import { useLocation, useParams } from 'react-router-dom' import { RequestOptions } from 'devexpress-reporting-react/dx-report-designer' import { APP_NAME } from '@/constants/app.constant' import { createReportUrl } from './reportRouteParams' +import { MODE_DARK } from '@/constants/theme.constant' +import { useStoreState } from '@/store/store' const ReportDesigner = lazy(() => import('devexpress-reporting-react/dx-report-designer') ) -const loadDesignerCss = () => { +const VIEWER_THEME_LINK_ID = 'devexpress-report-viewer-theme' +const VIEWER_DARK_OVERRIDES_ID = 'devexpress-report-viewer-dark-overrides' + +const getViewerClassName = (isDarkMode: boolean) => + [ + 'dx-viewport', + 'dx-device-desktop', + 'dx-device-generic', + 'dx-theme-generic', + 'dx-theme-generic-typography', + isDarkMode ? 'dx-color-scheme-dark' : 'dx-color-scheme-light', + 'report-viewer-shell', + isDarkMode ? 'report-viewer-shell-dark' : '', + ] + .filter(Boolean) + .join(' ') + +const loadViewerCss = (isDarkMode: boolean) => { const styles = [ new URL('@devexpress/analytics-core/dist/css/dx-analytics.common.css', import.meta.url).href, - new URL('@devexpress/analytics-core/dist/css/dx-analytics.light.css', import.meta.url).href, - new URL('@devexpress/analytics-core/dist/css/dx-querybuilder.css', import.meta.url).href, new URL('devexpress-reporting/dist/css/dx-webdocumentviewer.css', import.meta.url).href, - new URL('devexpress-reporting/dist/css/dx-reportdesigner.css', import.meta.url).href, ] styles.forEach((href) => { @@ -27,6 +43,45 @@ const loadDesignerCss = () => { link.href = href document.head.appendChild(link) }) + + const themeHref = isDarkMode + ? new URL('@devexpress/analytics-core/dist/css/dx-analytics.dark.css', import.meta.url).href + : new URL('@devexpress/analytics-core/dist/css/dx-analytics.light.css', import.meta.url).href + + let themeLink = document.getElementById(VIEWER_THEME_LINK_ID) as HTMLLinkElement | null + if (!themeLink) { + themeLink = document.createElement('link') + themeLink.id = VIEWER_THEME_LINK_ID + themeLink.rel = 'stylesheet' + document.head.appendChild(themeLink) + } + if (themeLink.href !== themeHref) { + themeLink.href = themeHref + } + + if (!document.getElementById(VIEWER_DARK_OVERRIDES_ID)) { + const style = document.createElement('style') + style.id = VIEWER_DARK_OVERRIDES_ID + style.textContent = ` + .report-viewer-shell { + min-height: calc(100vh - 4rem); + } + + .report-viewer-shell-dark { + background: #111827; + color: #e5e7eb; + } + + .report-viewer-shell-dark .dxrd-preview, + .report-viewer-shell-dark .dxrd-preview-wrapper, + .report-viewer-shell-dark .dxrd-toolbar-wrapper, + .report-viewer-shell-dark .dxrd-right-panel, + .report-viewer-shell-dark .dxrd-search-wrapper { + background-color: #111827; + } + ` + document.head.appendChild(style) + } } const DevexpressReportDesigner: React.FC = () => { @@ -36,17 +91,18 @@ const DevexpressReportDesigner: React.FC = () => { id: string listFormCode: string }>() + const themeMode = useStoreState((state) => state.theme.mode) + const isDarkMode = themeMode === MODE_DARK const location = useLocation() const reportUrlWithParams = createReportUrl({ report, id, listFormCode }, location.search, 'design') - useEffect(() => { - loadDesignerCss() - }, []) + loadViewerCss(isDarkMode) + }, [isDarkMode]) if (!reportUrlWithParams) return null return ( - + [ @@ -94,8 +97,43 @@ const DevexpressReportViewer: React.FC = () => { const token = useStoreState((state) => state.auth.session.token) const themeMode = useStoreState((state) => state.theme.mode) const [isRequestAuthReady, setIsRequestAuthReady] = useState(false) + const previewModelRef = useRef(null) const isDarkMode = themeMode === MODE_DARK + const getReportPreview = useCallback((eventOrModel?: any) => { + const eventArgs = eventOrModel?.args + const sender = eventOrModel?.sender + const candidates = [ + eventArgs?.reportPreview, + eventArgs?.ReportPreview, + eventArgs?.previewModel?.reportPreview, + eventOrModel?.reportPreview, + eventOrModel?.ReportPreview, + eventOrModel?.previewModel?.reportPreview, + sender?.GetReportPreview?.(), + sender?.previewModel?.reportPreview, + previewModelRef.current, + ] + + return candidates.find( + (candidate) => + candidate && + ('zoom' in candidate || + 'originalZoom' in candidate || + 'autoFitBy' in candidate), + ) + }, []) + + const setDefaultZoom = useCallback((eventOrModel?: any) => { + const preview = getReportPreview(eventOrModel) + if (!preview) return + + previewModelRef.current = preview + preview.autoFitBy = ZoomAutoBy.None + preview.originalZoom = DEFAULT_REPORT_ZOOM + preview.zoom = DEFAULT_REPORT_ZOOM + }, [getReportPreview]) + useEffect(() => { loadViewerCss(isDarkMode) }, [isDarkMode]) @@ -136,6 +174,10 @@ const DevexpressReportViewer: React.FC = () => { {isRequestAuthReady && ( +