Devexpress Grid ve Form raporlarını Richtext ve Image gösterilmesi
This commit is contained in:
parent
1997a8aacb
commit
2376eb641a
8 changed files with 1589 additions and 361 deletions
|
|
@ -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<string, string> parameters)
|
||||
public static async Task<DynamicFormReport> CreateAsync(IDictionary<string, string> 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<string, string> parameters)
|
||||
private async Task BuildAsync(IDictionary<string, string> parameters)
|
||||
{
|
||||
if (_serviceProvider == null)
|
||||
{
|
||||
|
|
@ -73,23 +75,22 @@ public class DynamicFormReport : XtraReport
|
|||
var selectQueryManager = scope.ServiceProvider.GetRequiredService<ISelectQueryManager>();
|
||||
var defaultValueHelper = scope.ServiceProvider.GetRequiredService<DefaultValueHelper>();
|
||||
var localizer = scope.ServiceProvider.GetRequiredService<IStringLocalizer<PlatformResource>>();
|
||||
var configuration = scope.ServiceProvider.GetRequiredService<IConfiguration>();
|
||||
|
||||
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<FormReportField> fields,
|
||||
IDictionary<string, object> row,
|
||||
IStringLocalizer<PlatformResource> localizer)
|
||||
IStringLocalizer<PlatformResource> 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<string, object> 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<string, object> 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<string, object> 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<string, object> 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<string, object> 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<FormReportField> 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<string>();
|
||||
var joinParts = new List<string>();
|
||||
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,
|
||||
@"(?<![A-Za-z0-9_])ORDER\s+BY\b[\s\S]*$",
|
||||
string.Empty,
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
}
|
||||
|
||||
private static string GetLookupBindingName(ListFormField field)
|
||||
{
|
||||
return $"{field.FieldName}__LookupText";
|
||||
}
|
||||
|
||||
private static string GetLookupAlias(ListFormField field)
|
||||
{
|
||||
return QuoteSqlIdentifier($"Lookup_{Regex.Replace(field.FieldName, @"\W+", "_")}");
|
||||
}
|
||||
|
||||
private static string QuoteSqlIdentifier(string identifier)
|
||||
{
|
||||
return $"\"{identifier.Replace("\"", "\"\"")}\"";
|
||||
}
|
||||
|
||||
private static string GetLocalizedReportTitle(ListForm listForm, IStringLocalizer<PlatformResource> 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<FormReportField> Fields);
|
||||
private sealed record FormReportField(
|
||||
|
|
@ -899,5 +945,6 @@ public class DynamicFormReport : XtraReport
|
|||
string BindingName,
|
||||
string EditorType,
|
||||
LookupDto Lookup,
|
||||
Dictionary<string, string> StaticLookupValues);
|
||||
Dictionary<string, string> StaticLookupValues,
|
||||
bool IsImage) : DynamicReportSqlBuilder.IReportLookupColumn;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, string> parameters)
|
||||
public static async Task<DynamicGridReport> CreateAsync(IDictionary<string, string> 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<string, string> parameters)
|
||||
private async Task BuildAsync(IDictionary<string, string> parameters)
|
||||
{
|
||||
if (_serviceProvider == null)
|
||||
{
|
||||
|
|
@ -68,20 +71,21 @@ public class DynamicGridReport : XtraReport
|
|||
var selectQueryManager = scope.ServiceProvider.GetRequiredService<ISelectQueryManager>();
|
||||
var defaultValueHelper = scope.ServiceProvider.GetRequiredService<DefaultValueHelper>();
|
||||
var localizer = scope.ServiceProvider.GetRequiredService<IStringLocalizer<PlatformResource>>();
|
||||
var configuration = scope.ServiceProvider.GetRequiredService<IConfiguration>();
|
||||
|
||||
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<List<ReportColumn>> ApplyImageCountsAsync(
|
||||
IReadOnlyList<ReportColumn> 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<IDictionary<string, object>>())
|
||||
{
|
||||
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<string, object> 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<string, object> 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<ReportColumn> fields,
|
||||
IStringLocalizer<PlatformResource> localizer)
|
||||
IStringLocalizer<PlatformResource> 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<ReportColumn> 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<ReportColumn> 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<List<EditingFormDto>>(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<ReportColumn> 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<string>();
|
||||
var joinParts = new List<string>();
|
||||
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,
|
||||
@"(?<![A-Za-z0-9_])ORDER\s+BY\b[\s\S]*$",
|
||||
string.Empty,
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
}
|
||||
|
||||
private static string GetLookupBindingName(ListFormField field)
|
||||
{
|
||||
return $"{field.FieldName}__LookupText";
|
||||
}
|
||||
|
||||
private static string GetLookupAlias(ListFormField field)
|
||||
{
|
||||
return QuoteSqlIdentifier($"Lookup_{Regex.Replace(field.FieldName, @"\W+", "_")}");
|
||||
}
|
||||
|
||||
private static string QuoteSqlIdentifier(string identifier)
|
||||
{
|
||||
return $"\"{identifier.Replace("\"", "\"\"")}\"";
|
||||
}
|
||||
|
||||
private sealed record ReportColumn(
|
||||
ListFormField Field,
|
||||
string BindingName,
|
||||
LookupDto Lookup,
|
||||
Dictionary<string, string> StaticLookupValues);
|
||||
Dictionary<string, string> 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"];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<List<EditingFormDto>>(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<XRPictureBox> 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<XRPictureBox> 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<XRPictureBox> 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<XRPictureBox> pictureBoxes, IReadOnlyList<string> 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<string> 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,(?<data>.+)$",
|
||||
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<string> 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<string> 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<string> 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<string> 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<string> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string, string> 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<IReportLookupColumn> 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,
|
||||
@"(?<![A-Za-z0-9_])ORDER\s+BY\b[\s\S]*$",
|
||||
string.Empty,
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
|
||||
return Regex.Replace(
|
||||
query,
|
||||
@"@\s*param\d+",
|
||||
"NULL",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
}
|
||||
|
||||
private static string NormalizeSql(string sql)
|
||||
{
|
||||
return sql.Trim().TrimEnd(';');
|
||||
}
|
||||
|
||||
private static string QuoteSqlIdentifier(string identifier)
|
||||
{
|
||||
return $"\"{identifier.Replace("\"", "\"\"")}\"";
|
||||
}
|
||||
|
||||
private static string ToSqlLiteral(object value)
|
||||
{
|
||||
if (value == null || value == DBNull.Value)
|
||||
{
|
||||
return "NULL";
|
||||
}
|
||||
|
||||
return value switch
|
||||
{
|
||||
string text => $"'{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("'", "''")}'"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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<byte[]> 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<XRPictureBox>())
|
||||
{
|
||||
if (picture.ImageSource == null || !string.IsNullOrWhiteSpace(picture.ImageUrl))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
picture.ImageSource = null;
|
||||
picture.Visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<XRControl> EnumerateControls(XtraReport report)
|
||||
{
|
||||
foreach (Band band in report.Bands)
|
||||
{
|
||||
foreach (var control in EnumerateControls(band.Controls))
|
||||
{
|
||||
yield return control;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<XRControl> EnumerateControls(XRControlCollection controls)
|
||||
{
|
||||
foreach (XRControl control in controls)
|
||||
{
|
||||
yield return control;
|
||||
|
||||
foreach (var child in EnumerateControls(control.Controls))
|
||||
{
|
||||
yield return child;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<byte[]> 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<DevExpress.DataAccess.Sql.SqlDataSource>().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<string>();
|
||||
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<DevExpress.DataAccess.Sql.SqlDataSource>().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<string>();
|
||||
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<string, string> 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<string, string> parameters)
|
||||
{
|
||||
if (parameters.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sqlDataSource = report.ComponentStorage.OfType<DevExpress.DataAccess.Sql.SqlDataSource>().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<string>();
|
||||
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<string, string> Parameters) ParseReportUrl(string url)
|
||||
{
|
||||
var decodedUrl = DecodeRepeatedly(url);
|
||||
|
|
|
|||
|
|
@ -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 && <Badge innerClass="bg-blue-500" />}
|
||||
|
||||
|
||||
|
||||
{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={<FaFileAlt />}
|
||||
icon={<FaSistrix />}
|
||||
title={translate('::App.Reports.Form')}
|
||||
onClick={openDynamicGridReport}
|
||||
></Button>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Container>
|
||||
<Container className={getViewerClassName(isDarkMode)}>
|
||||
<Helmet
|
||||
titleTemplate={`%s | ${APP_NAME}`}
|
||||
title={translate('::App.Reports')}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import React, { useMemo, useEffect, useState } from 'react'
|
||||
import React, { useMemo, useEffect, useState, useCallback, useRef } from 'react'
|
||||
import { Container } from '@/components/shared'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { useParams, useLocation } from 'react-router-dom'
|
||||
import ReportViewer, {
|
||||
Callbacks,
|
||||
RequestOptions,
|
||||
} from 'devexpress-reporting-react/dx-report-viewer'
|
||||
import { ajaxSetup } from '@devexpress/analytics-core/analytics-utils-native'
|
||||
import { ZoomAutoBy } from 'devexpress-reporting/viewer/constants'
|
||||
import { APP_NAME } from '@/constants/app.constant'
|
||||
import { MODE_DARK } from '@/constants/theme.constant'
|
||||
import { useStoreState } from '@/store'
|
||||
|
|
@ -14,6 +16,7 @@ import { createReportUrl } from './reportRouteParams'
|
|||
|
||||
const VIEWER_THEME_LINK_ID = 'devexpress-report-viewer-theme'
|
||||
const VIEWER_DARK_OVERRIDES_ID = 'devexpress-report-viewer-dark-overrides'
|
||||
const DEFAULT_REPORT_ZOOM = 1
|
||||
|
||||
const getViewerClassName = (isDarkMode: boolean) =>
|
||||
[
|
||||
|
|
@ -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<any>(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 && (
|
||||
<ReportViewer reportUrl={reportUrlWithParams}>
|
||||
<Callbacks
|
||||
BeforeRender={setDefaultZoom}
|
||||
DocumentReady={setDefaultZoom}
|
||||
/>
|
||||
<RequestOptions
|
||||
host={import.meta.env.VITE_API_URL}
|
||||
invokeAction="/DXXRDV"
|
||||
|
|
|
|||
Loading…
Reference in a new issue