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.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using DevExpress.DataAccess.ConnectionParameters;
|
using DevExpress.DataAccess.ConnectionParameters;
|
||||||
using DevExpress.DataAccess.Sql;
|
using DevExpress.DataAccess.Sql;
|
||||||
using DevExpress.Drawing;
|
using DevExpress.Drawing;
|
||||||
using DevExpress.XtraPrinting;
|
using DevExpress.XtraPrinting;
|
||||||
using DevExpress.XtraReports.UI;
|
using DevExpress.XtraReports.UI;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
using Sozsoft.Platform;
|
using Sozsoft.Platform;
|
||||||
|
|
@ -36,10 +38,10 @@ public class DynamicFormReport : XtraReport
|
||||||
_serviceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DynamicFormReport Create(IDictionary<string, string> parameters)
|
public static async Task<DynamicFormReport> CreateAsync(IDictionary<string, string> parameters)
|
||||||
{
|
{
|
||||||
var report = new DynamicFormReport();
|
var report = new DynamicFormReport();
|
||||||
report.Build(parameters);
|
await report.BuildAsync(parameters);
|
||||||
return report;
|
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)
|
if (_serviceProvider == null)
|
||||||
{
|
{
|
||||||
|
|
@ -73,23 +75,22 @@ public class DynamicFormReport : XtraReport
|
||||||
var selectQueryManager = scope.ServiceProvider.GetRequiredService<ISelectQueryManager>();
|
var selectQueryManager = scope.ServiceProvider.GetRequiredService<ISelectQueryManager>();
|
||||||
var defaultValueHelper = scope.ServiceProvider.GetRequiredService<DefaultValueHelper>();
|
var defaultValueHelper = scope.ServiceProvider.GetRequiredService<DefaultValueHelper>();
|
||||||
var localizer = scope.ServiceProvider.GetRequiredService<IStringLocalizer<PlatformResource>>();
|
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}'.");
|
throw new UnauthorizedAccessException($"No read permission for list form '{listFormCode}'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var listForm = listFormManager.GetUserListForm(listFormCode).GetAwaiter().GetResult();
|
var listForm = await listFormManager.GetUserListForm(listFormCode);
|
||||||
var fields = listFormFieldManager.GetUserListFormFields(listFormCode).GetAwaiter().GetResult()
|
var fields = (await listFormFieldManager.GetUserListFormFields(listFormCode))
|
||||||
.Where(x => x.CanRead)
|
.Where(x => x.CanRead)
|
||||||
.OrderBy(x => x.ListOrderNo ?? int.MaxValue)
|
.OrderBy(x => x.ListOrderNo ?? int.MaxValue)
|
||||||
.ThenBy(x => x.CaptionName)
|
.ThenBy(x => x.CaptionName)
|
||||||
.ToList();
|
.ToList();
|
||||||
var customizations = customizationManager.GetUsersServerCustomizations(listFormCode).GetAwaiter().GetResult();
|
var customizations = await customizationManager.GetUsersServerCustomizations(listFormCode);
|
||||||
var (dynamicDataRepository, connectionString, dataSourceType) = dynamicDataManager
|
var (dynamicDataRepository, connectionString, dataSourceType) = await dynamicDataManager
|
||||||
.GetAsync(listForm.IsTenant, listForm.DataSourceCode)
|
.GetAsync(listForm.IsTenant, listForm.DataSourceCode);
|
||||||
.GetAwaiter()
|
|
||||||
.GetResult();
|
|
||||||
|
|
||||||
var queryParams = new QueryParameters
|
var queryParams = new QueryParameters
|
||||||
{
|
{
|
||||||
|
|
@ -102,19 +103,18 @@ public class DynamicFormReport : XtraReport
|
||||||
selectQueryManager.PrepareQueries(listForm, fields, dataSourceType, customizations, queryParams);
|
selectQueryManager.PrepareQueries(listForm, fields, dataSourceType, customizations, queryParams);
|
||||||
|
|
||||||
var reportFields = GetReportFields(listForm, fields, selectQueryManager, defaultValueHelper);
|
var reportFields = GetReportFields(listForm, fields, selectQueryManager, defaultValueHelper);
|
||||||
var selectQuery = ApplyLookupQueryColumns(
|
var selectQuery = StripReportPaging(ApplyQueryParameters(selectQueryManager.SelectQuery, selectQueryManager.SelectQueryParameters));
|
||||||
selectQueryManager.SelectQuery,
|
selectQuery = DynamicReportSqlBuilder.ApplyLookupTextColumns(
|
||||||
listForm,
|
selectQuery,
|
||||||
reportFields,
|
reportFields,
|
||||||
defaultValueHelper);
|
defaultValueHelper,
|
||||||
selectQuery = ApplyQueryParameters(selectQuery, selectQueryManager.SelectQueryParameters);
|
dataSourceType);
|
||||||
selectQuery = StripReportPaging(selectQuery);
|
|
||||||
|
|
||||||
var row = GetFirstRow(dynamicDataRepository.QueryAsync(selectQuery, connectionString).GetAwaiter().GetResult());
|
var row = GetFirstRow(await dynamicDataRepository.QueryAsync(selectQuery, connectionString));
|
||||||
|
|
||||||
ConfigurePage();
|
ConfigurePage();
|
||||||
ConfigureDataSource(selectQuery, connectionString, dataSourceType);
|
ConfigureDataSource(selectQuery, connectionString, dataSourceType);
|
||||||
BuildLayout(listForm, reportFields, row, localizer);
|
await BuildLayoutAsync(listForm, reportFields, row, localizer, GetImageBaseUrl(configuration));
|
||||||
RequestParameters = false;
|
RequestParameters = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,11 +146,12 @@ public class DynamicFormReport : XtraReport
|
||||||
DataMember = QueryName;
|
DataMember = QueryName;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BuildLayout(
|
private async Task BuildLayoutAsync(
|
||||||
ListForm listForm,
|
ListForm listForm,
|
||||||
IReadOnlyList<FormReportField> fields,
|
IReadOnlyList<FormReportField> fields,
|
||||||
IDictionary<string, object> row,
|
IDictionary<string, object> row,
|
||||||
IStringLocalizer<PlatformResource> localizer)
|
IStringLocalizer<PlatformResource> localizer,
|
||||||
|
string imageBaseUrl)
|
||||||
{
|
{
|
||||||
Bands.Clear();
|
Bands.Clear();
|
||||||
|
|
||||||
|
|
@ -208,7 +209,14 @@ public class DynamicFormReport : XtraReport
|
||||||
var rowHeight = GetFieldRowHeight(field);
|
var rowHeight = GetFieldRowHeight(field);
|
||||||
var labelWidth = pageWidth * 0.28F;
|
var labelWidth = pageWidth * 0.28F;
|
||||||
detail.Controls.Add(CreateCaptionLabel(field, localizer, 0, y, labelWidth, rowHeight));
|
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;
|
y += rowHeight + 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -220,7 +228,7 @@ public class DynamicFormReport : XtraReport
|
||||||
detail.Controls.Add(new XRSubreport
|
detail.Controls.Add(new XRSubreport
|
||||||
{
|
{
|
||||||
BoundsF = new System.Drawing.RectangleF(0, y, pageWidth, 24),
|
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;
|
y += 28;
|
||||||
}
|
}
|
||||||
|
|
@ -251,7 +259,7 @@ public class DynamicFormReport : XtraReport
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static XRLabel CreateValueLabel(
|
private static XRControl CreateValueLabel(
|
||||||
FormReportField field,
|
FormReportField field,
|
||||||
IDictionary<string, object> row,
|
IDictionary<string, object> row,
|
||||||
float x,
|
float x,
|
||||||
|
|
@ -259,6 +267,11 @@ public class DynamicFormReport : XtraReport
|
||||||
float width,
|
float width,
|
||||||
float height)
|
float height)
|
||||||
{
|
{
|
||||||
|
if (IsHtmlEditor(field))
|
||||||
|
{
|
||||||
|
return CreateHtmlValueControl(field, row, x, y, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
var label = new XRLabel
|
var label = new XRLabel
|
||||||
{
|
{
|
||||||
BoundsF = new System.Drawing.RectangleF(x, y, width, height),
|
BoundsF = new System.Drawing.RectangleF(x, y, width, height),
|
||||||
|
|
@ -275,6 +288,7 @@ public class DynamicFormReport : XtraReport
|
||||||
if (TryGetFormattedValue(field, row, out var text))
|
if (TryGetFormattedValue(field, row, out var text))
|
||||||
{
|
{
|
||||||
label.Text = text;
|
label.Text = text;
|
||||||
|
label.Font = CreateValueFont(text);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -284,6 +298,115 @@ public class DynamicFormReport : XtraReport
|
||||||
return label;
|
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(
|
private static bool TryGetFormattedValue(
|
||||||
FormReportField field,
|
FormReportField field,
|
||||||
IDictionary<string, object> row,
|
IDictionary<string, object> row,
|
||||||
|
|
@ -327,6 +450,11 @@ public class DynamicFormReport : XtraReport
|
||||||
|
|
||||||
private static float GetFieldRowHeight(FormReportField field)
|
private static float GetFieldRowHeight(FormReportField field)
|
||||||
{
|
{
|
||||||
|
if (field.IsImage)
|
||||||
|
{
|
||||||
|
return DynamicReportImageHelper.GetFormImageHeight();
|
||||||
|
}
|
||||||
|
|
||||||
return field.EditorType is PlatformConsts.EditorTypes.dxTextArea or PlatformConsts.EditorTypes.dxHtmlEditor
|
return field.EditorType is PlatformConsts.EditorTypes.dxTextArea or PlatformConsts.EditorTypes.dxHtmlEditor
|
||||||
? 48
|
? 48
|
||||||
: RowHeight;
|
: RowHeight;
|
||||||
|
|
@ -489,25 +617,27 @@ public class DynamicFormReport : XtraReport
|
||||||
|
|
||||||
if (lookup == null || selectField?.JoinOptions != null)
|
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)
|
if (lookup.DataSourceType == UiLookupDataSourceTypeEnum.StaticData)
|
||||||
{
|
{
|
||||||
|
var staticLookupValues = GetStaticLookupValues(lookup, defaultValueHelper);
|
||||||
return new FormReportField(
|
return new FormReportField(
|
||||||
field,
|
field,
|
||||||
bindingName,
|
staticLookupValues?.Count > 0 ? DynamicReportSqlBuilder.GetLookupBindingName(field) : bindingName,
|
||||||
editorType,
|
editorType,
|
||||||
lookup,
|
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)
|
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)
|
private static string GetLocalizedReportTitle(ListForm listForm, IStringLocalizer<PlatformResource> localizer)
|
||||||
{
|
{
|
||||||
var title = string.IsNullOrWhiteSpace(listForm.Title) ? listForm.Name : listForm.Title;
|
var title = string.IsNullOrWhiteSpace(listForm.Title) ? listForm.Name : listForm.Title;
|
||||||
|
|
@ -863,6 +899,11 @@ public class DynamicFormReport : XtraReport
|
||||||
field.Field.SourceDbType == DbType.Boolean;
|
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)
|
private static bool TryFormatBoolean(string value, out string text)
|
||||||
{
|
{
|
||||||
if (bool.TryParse(value, out var boolean))
|
if (bool.TryParse(value, out var boolean))
|
||||||
|
|
@ -892,6 +933,11 @@ public class DynamicFormReport : XtraReport
|
||||||
return parameters.TryGetValue(name, out var value) ? value : null;
|
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 FieldWithEditor(ListFormField Field, string EditorType);
|
||||||
private sealed record FormGroup(string Caption, IReadOnlyList<FormReportField> Fields);
|
private sealed record FormGroup(string Caption, IReadOnlyList<FormReportField> Fields);
|
||||||
private sealed record FormReportField(
|
private sealed record FormReportField(
|
||||||
|
|
@ -899,5 +945,6 @@ public class DynamicFormReport : XtraReport
|
||||||
string BindingName,
|
string BindingName,
|
||||||
string EditorType,
|
string EditorType,
|
||||||
LookupDto Lookup,
|
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.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using DevExpress.DataAccess.ConnectionParameters;
|
using DevExpress.DataAccess.ConnectionParameters;
|
||||||
using DevExpress.DataAccess.Sql;
|
using DevExpress.DataAccess.Sql;
|
||||||
using DevExpress.Drawing;
|
using DevExpress.Drawing;
|
||||||
using DevExpress.XtraPrinting;
|
using DevExpress.XtraPrinting;
|
||||||
using DevExpress.XtraReports.UI;
|
using DevExpress.XtraReports.UI;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
|
using Sozsoft.Platform;
|
||||||
using Sozsoft.Platform.DynamicData;
|
using Sozsoft.Platform.DynamicData;
|
||||||
using Sozsoft.Platform.Entities;
|
using Sozsoft.Platform.Entities;
|
||||||
using Sozsoft.Platform.Enums;
|
using Sozsoft.Platform.Enums;
|
||||||
|
|
@ -36,10 +39,10 @@ public class DynamicGridReport : XtraReport
|
||||||
_serviceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DynamicGridReport Create(IDictionary<string, string> parameters)
|
public static async Task<DynamicGridReport> CreateAsync(IDictionary<string, string> parameters)
|
||||||
{
|
{
|
||||||
var report = new DynamicGridReport();
|
var report = new DynamicGridReport();
|
||||||
report.Build(parameters);
|
await report.BuildAsync(parameters);
|
||||||
return report;
|
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)
|
if (_serviceProvider == null)
|
||||||
{
|
{
|
||||||
|
|
@ -68,20 +71,21 @@ public class DynamicGridReport : XtraReport
|
||||||
var selectQueryManager = scope.ServiceProvider.GetRequiredService<ISelectQueryManager>();
|
var selectQueryManager = scope.ServiceProvider.GetRequiredService<ISelectQueryManager>();
|
||||||
var defaultValueHelper = scope.ServiceProvider.GetRequiredService<DefaultValueHelper>();
|
var defaultValueHelper = scope.ServiceProvider.GetRequiredService<DefaultValueHelper>();
|
||||||
var localizer = scope.ServiceProvider.GetRequiredService<IStringLocalizer<PlatformResource>>();
|
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}'.");
|
throw new UnauthorizedAccessException($"No read permission for list form '{listFormCode}'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var listForm = listFormManager.GetUserListForm(listFormCode).GetAwaiter().GetResult();
|
var listForm = await listFormManager.GetUserListForm(listFormCode);
|
||||||
var fields = listFormFieldManager.GetUserListFormFields(listFormCode).GetAwaiter().GetResult()
|
var fields = (await listFormFieldManager.GetUserListFormFields(listFormCode))
|
||||||
.Where(x => x.CanRead)
|
.Where(x => x.CanRead)
|
||||||
.OrderBy(x => x.ListOrderNo ?? int.MaxValue)
|
.OrderBy(x => x.ListOrderNo ?? int.MaxValue)
|
||||||
.ThenBy(x => x.CaptionName)
|
.ThenBy(x => x.CaptionName)
|
||||||
.ToList();
|
.ToList();
|
||||||
var customizations = customizationManager.GetUsersServerCustomizations(listFormCode).GetAwaiter().GetResult();
|
var customizations = await customizationManager.GetUsersServerCustomizations(listFormCode);
|
||||||
var (_, connectionString, dataSourceType) = dynamicDataManager.GetAsync(listForm.IsTenant, listForm.DataSourceCode).GetAwaiter().GetResult();
|
var (dynamicDataRepository, connectionString, dataSourceType) = await dynamicDataManager.GetAsync(listForm.IsTenant, listForm.DataSourceCode);
|
||||||
|
|
||||||
var queryParams = new QueryParameters
|
var queryParams = new QueryParameters
|
||||||
{
|
{
|
||||||
|
|
@ -117,24 +121,31 @@ public class DynamicGridReport : XtraReport
|
||||||
.Select(field =>
|
.Select(field =>
|
||||||
{
|
{
|
||||||
var selectField = selectQueryManager.SelectFields.First(s => s.FieldName == field.FieldName);
|
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")
|
.Where(x => !string.IsNullOrWhiteSpace(x.BindingName) && x.BindingName != "undefined")
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var selectQuery = ApplyLookupQueryColumns(
|
var selectQuery = StripReportPaging(ApplyQueryParameters(selectQueryManager.SelectQuery, selectQueryManager.SelectQueryParameters));
|
||||||
selectQueryManager.SelectQuery,
|
selectQuery = DynamicReportSqlBuilder.ApplyLookupTextColumns(
|
||||||
listForm,
|
selectQuery,
|
||||||
reportColumns,
|
reportColumns,
|
||||||
defaultValueHelper);
|
defaultValueHelper,
|
||||||
selectQuery = StripReportPaging(ApplyQueryParameters(selectQuery, selectQueryManager.SelectQueryParameters));
|
dataSourceType);
|
||||||
|
var imageBaseUrl = GetImageBaseUrl(configuration);
|
||||||
|
reportColumns = await ApplyImageCountsAsync(reportColumns, dynamicDataRepository, selectQuery, connectionString, imageBaseUrl);
|
||||||
|
|
||||||
ConfigurePage(reportColumns);
|
ConfigurePage(reportColumns);
|
||||||
ConfigureDataSource(
|
ConfigureDataSource(
|
||||||
selectQuery,
|
selectQuery,
|
||||||
connectionString,
|
connectionString,
|
||||||
dataSourceType);
|
dataSourceType);
|
||||||
BuildLayout(listForm, reportColumns, localizer);
|
BuildLayout(listForm, reportColumns, localizer, imageBaseUrl);
|
||||||
RequestParameters = false;
|
RequestParameters = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,7 +155,7 @@ public class DynamicGridReport : XtraReport
|
||||||
Margins = CreateReportMargins(HorizontalMargin, HorizontalMargin, 35, 35);
|
Margins = CreateReportMargins(HorizontalMargin, HorizontalMargin, 35, 35);
|
||||||
PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4;
|
PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4;
|
||||||
var portraitContentWidth = A4PortraitWidth - Margins.Left - Margins.Right;
|
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;
|
Landscape = visibleFields.Count >= LandscapeColumnThreshold || requestedContentWidth > portraitContentWidth;
|
||||||
Font = new DXFont("Arial", 8.25F);
|
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(
|
private void BuildLayout(
|
||||||
ListForm listForm,
|
ListForm listForm,
|
||||||
IReadOnlyList<ReportColumn> fields,
|
IReadOnlyList<ReportColumn> fields,
|
||||||
IStringLocalizer<PlatformResource> localizer)
|
IStringLocalizer<PlatformResource> localizer,
|
||||||
|
string imageBaseUrl)
|
||||||
{
|
{
|
||||||
Bands.Clear();
|
Bands.Clear();
|
||||||
|
|
||||||
|
|
@ -253,7 +334,12 @@ public class DynamicGridReport : XtraReport
|
||||||
var bottomMargin = new BottomMarginBand { HeightF = 32 };
|
var bottomMargin = new BottomMarginBand { HeightF = 32 };
|
||||||
var reportHeader = new ReportHeaderBand { HeightF = 32 };
|
var reportHeader = new ReportHeaderBand { HeightF = 32 };
|
||||||
var pageHeader = new PageHeaderBand { HeightF = 28 };
|
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 };
|
var reportFooter = new ReportFooterBand { HeightF = HasAnySummary(fields) ? 28 : 0 };
|
||||||
|
|
||||||
Bands.AddRange([topMargin, reportHeader, pageHeader, detail, reportFooter, bottomMargin]);
|
Bands.AddRange([topMargin, reportHeader, pageHeader, detail, reportFooter, bottomMargin]);
|
||||||
|
|
@ -288,17 +374,17 @@ public class DynamicGridReport : XtraReport
|
||||||
|
|
||||||
var widths = CalculateWidths(fields, pageWidth);
|
var widths = CalculateWidths(fields, pageWidth);
|
||||||
var headerTable = CreateTable(pageWidth, 28);
|
var headerTable = CreateTable(pageWidth, 28);
|
||||||
var headerRow = new XRTableRow();
|
var headerRow = new XRTableRow { HeightF = 28 };
|
||||||
var detailTable = CreateTable(pageWidth, 24);
|
var detailTable = CreateTable(pageWidth, detailHeight);
|
||||||
var detailRow = new XRTableRow();
|
var detailRow = new XRTableRow { HeightF = detailHeight };
|
||||||
var footerTable = CreateTable(pageWidth, 24);
|
var footerTable = CreateTable(pageWidth, 24);
|
||||||
var footerRow = new XRTableRow();
|
var footerRow = new XRTableRow { HeightF = 24 };
|
||||||
|
|
||||||
for (var i = 0; i < fields.Count; i++)
|
for (var i = 0; i < fields.Count; i++)
|
||||||
{
|
{
|
||||||
var field = fields[i];
|
var field = fields[i];
|
||||||
headerRow.Cells.Add(CreateHeaderCell(field, widths[i], localizer));
|
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]));
|
footerRow.Cells.Add(CreateFooterCell(field, widths[i]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -335,6 +421,7 @@ public class DynamicGridReport : XtraReport
|
||||||
{
|
{
|
||||||
Text = GetLocalizedFieldCaption(field, localizer),
|
Text = GetLocalizedFieldCaption(field, localizer),
|
||||||
WidthF = width,
|
WidthF = width,
|
||||||
|
Weight = width,
|
||||||
Font = new DXFont("Arial", 8F, DXFontStyle.Bold),
|
Font = new DXFont("Arial", 8F, DXFontStyle.Bold),
|
||||||
BackColor = System.Drawing.Color.FromArgb(238, 242, 247),
|
BackColor = System.Drawing.Color.FromArgb(238, 242, 247),
|
||||||
Padding = new PaddingInfo(4, 4, 2, 2),
|
Padding = new PaddingInfo(4, 4, 2, 2),
|
||||||
|
|
@ -368,13 +455,29 @@ public class DynamicGridReport : XtraReport
|
||||||
: localized.Value;
|
: 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;
|
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
|
var cell = new XRTableCell
|
||||||
{
|
{
|
||||||
WidthF = width,
|
WidthF = width,
|
||||||
Font = new DXFont("Arial", 8F),
|
Weight = width,
|
||||||
|
Font = new DXFont("Segoe UI Emoji", 8F),
|
||||||
Padding = new PaddingInfo(4, 4, 2, 2),
|
Padding = new PaddingInfo(4, 4, 2, 2),
|
||||||
TextAlignment = ToTextAlignment(field.Alignment, field.SourceDbType),
|
TextAlignment = ToTextAlignment(field.Alignment, field.SourceDbType),
|
||||||
Multiline = false,
|
Multiline = false,
|
||||||
|
|
@ -385,6 +488,29 @@ public class DynamicGridReport : XtraReport
|
||||||
return cell;
|
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)
|
private static XRTableCell CreateFooterCell(ReportColumn column, float width)
|
||||||
{
|
{
|
||||||
var field = column.Field;
|
var field = column.Field;
|
||||||
|
|
@ -392,6 +518,7 @@ public class DynamicGridReport : XtraReport
|
||||||
var cell = new XRTableCell
|
var cell = new XRTableCell
|
||||||
{
|
{
|
||||||
WidthF = width,
|
WidthF = width,
|
||||||
|
Weight = width,
|
||||||
Font = new DXFont("Arial", 8F, DXFontStyle.Bold),
|
Font = new DXFont("Arial", 8F, DXFontStyle.Bold),
|
||||||
Padding = new PaddingInfo(4, 4, 2, 2),
|
Padding = new PaddingInfo(4, 4, 2, 2),
|
||||||
TextAlignment = ToTextAlignment(field.Alignment, field.SourceDbType)
|
TextAlignment = ToTextAlignment(field.Alignment, field.SourceDbType)
|
||||||
|
|
@ -427,7 +554,7 @@ public class DynamicGridReport : XtraReport
|
||||||
|
|
||||||
private static float[] CalculateWidths(IReadOnlyList<ReportColumn> fields, float pageWidth)
|
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();
|
var total = raw.Sum();
|
||||||
if (total <= 0)
|
if (total <= 0)
|
||||||
{
|
{
|
||||||
|
|
@ -437,11 +564,64 @@ public class DynamicGridReport : XtraReport
|
||||||
return raw.Select(x => pageWidth * x / total).ToArray();
|
return raw.Select(x => pageWidth * x / total).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static float GetPreferredColumnWidth(ListFormField field)
|
||||||
|
{
|
||||||
|
if (field.Width > 0)
|
||||||
|
{
|
||||||
|
return Math.Max(field.Width.Value, 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
var caption = string.IsNullOrWhiteSpace(field.CaptionName) ? field.FieldName : field.CaptionName;
|
||||||
|
var name = field.FieldName ?? string.Empty;
|
||||||
|
var text = $"{caption} {name}".ToLowerInvariant();
|
||||||
|
|
||||||
|
if (field.SourceDbType == DbType.Boolean)
|
||||||
|
{
|
||||||
|
return 75;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (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)
|
private static bool HasAnySummary(IEnumerable<ReportColumn> fields)
|
||||||
{
|
{
|
||||||
return fields.Any(x => GetSummaryType(x.Field.TotalSummaryJson) != null);
|
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)
|
private static string GetReportFieldName(SelectField selectField)
|
||||||
{
|
{
|
||||||
if (selectField?.JoinOptions != null && !string.IsNullOrWhiteSpace(selectField.JoinOptions.FieldNameAlias))
|
if (selectField?.JoinOptions != null && !string.IsNullOrWhiteSpace(selectField.JoinOptions.FieldNameAlias))
|
||||||
|
|
@ -455,19 +635,21 @@ public class DynamicGridReport : XtraReport
|
||||||
private static ReportColumn CreateReportColumn(
|
private static ReportColumn CreateReportColumn(
|
||||||
ListFormField field,
|
ListFormField field,
|
||||||
SelectField selectField,
|
SelectField selectField,
|
||||||
DefaultValueHelper defaultValueHelper)
|
DefaultValueHelper defaultValueHelper,
|
||||||
|
bool isImage,
|
||||||
|
bool isHtml)
|
||||||
{
|
{
|
||||||
var bindingName = GetReportFieldName(selectField);
|
var bindingName = GetReportFieldName(selectField);
|
||||||
var lookup = GetLookup(field);
|
var lookup = GetLookup(field);
|
||||||
|
|
||||||
if (lookup == null || selectField?.JoinOptions != null)
|
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)
|
if (lookup.DataSourceType == UiLookupDataSourceTypeEnum.StaticData)
|
||||||
|
|
@ -475,20 +657,35 @@ public class DynamicGridReport : XtraReport
|
||||||
var staticLookupValues = GetStaticLookupValues(lookup, defaultValueHelper);
|
var staticLookupValues = GetStaticLookupValues(lookup, defaultValueHelper);
|
||||||
return new ReportColumn(
|
return new ReportColumn(
|
||||||
field,
|
field,
|
||||||
staticLookupValues?.Count > 0 ? GetLookupBindingName(field) : bindingName,
|
staticLookupValues?.Count > 0 ? DynamicReportSqlBuilder.GetLookupBindingName(field) : bindingName,
|
||||||
lookup,
|
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 &&
|
if (string.IsNullOrWhiteSpace(listForm?.EditingFormJson) || string.IsNullOrWhiteSpace(fieldName))
|
||||||
string.IsNullOrWhiteSpace(lookup.CascadeParentFields) &&
|
{
|
||||||
!string.IsNullOrWhiteSpace(lookup.LookupQuery) &&
|
return false;
|
||||||
!Regex.IsMatch(lookup.LookupQuery, @"@\s*param\d+", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
}
|
||||||
|
|
||||||
|
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)
|
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(
|
private sealed record ReportColumn(
|
||||||
ListFormField Field,
|
ListFormField Field,
|
||||||
string BindingName,
|
string BindingName,
|
||||||
LookupDto Lookup,
|
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)
|
private static SummaryFunc? GetSummaryType(string totalSummaryJson)
|
||||||
{
|
{
|
||||||
|
|
@ -749,4 +842,9 @@ public class DynamicGridReport : XtraReport
|
||||||
{
|
{
|
||||||
return parameters.TryGetValue(name, out var value) ? value : null;
|
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.Linq;
|
||||||
using System;
|
using System;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Sozsoft.Platform.ReportServices;
|
namespace Sozsoft.Platform.ReportServices;
|
||||||
|
|
@ -75,6 +76,79 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens
|
||||||
}
|
}
|
||||||
|
|
||||||
public override byte[] GetData(string url)
|
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.
|
// 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**.
|
// This method is called if the **IsValidUrl** method returns **true**.
|
||||||
|
|
@ -86,90 +160,23 @@ public class CustomReportStorageWebExtension : DevExpress.XtraReports.Web.Extens
|
||||||
|
|
||||||
if (IsDynamicGridReport(reportName))
|
if (IsDynamicGridReport(reportName))
|
||||||
{
|
{
|
||||||
XtraReport report = HasParameter(parameters, "id")
|
var report = HasParameter(parameters, "id")
|
||||||
? Sozsoft.Reports.PredefinedReports.DynamicFormReport.Create(parameters)
|
? (XtraReport)await Sozsoft.Reports.PredefinedReports.DynamicFormReport.CreateAsync(parameters)
|
||||||
: Sozsoft.Reports.PredefinedReports.DynamicGridReport.Create(parameters);
|
: await Sozsoft.Reports.PredefinedReports.DynamicGridReport.CreateAsync(parameters);
|
||||||
using MemoryStream ms = new MemoryStream();
|
|
||||||
report.SaveLayoutToXml(ms);
|
return SaveReportLayout(report);
|
||||||
return ms.ToArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsDynamicFormReport(reportName))
|
if (IsDynamicFormReport(reportName))
|
||||||
{
|
{
|
||||||
var report = Sozsoft.Reports.PredefinedReports.DynamicFormReport.Create(parameters);
|
var report = await Sozsoft.Reports.PredefinedReports.DynamicFormReport.CreateAsync(parameters);
|
||||||
using MemoryStream ms = new MemoryStream();
|
return SaveReportLayout(report);
|
||||||
report.SaveLayoutToXml(ms);
|
|
||||||
return ms.ToArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ö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();
|
var reportData = TryGetStoredReportData(reportName, parameters);
|
||||||
report.SaveLayoutToXml(ms);
|
if (reportData != null)
|
||||||
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 report = (XtraReport)Activator.CreateInstance(reportType);
|
return reportData;
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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");
|
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)
|
private static (string ReportName, Dictionary<string, string> Parameters) ParseReportUrl(string url)
|
||||||
{
|
{
|
||||||
var decodedUrl = DecodeRepeatedly(url);
|
var decodedUrl = DecodeRepeatedly(url);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import {
|
||||||
FaEye,
|
FaEye,
|
||||||
FaSave,
|
FaSave,
|
||||||
FaCog,
|
FaCog,
|
||||||
FaFileAlt,
|
FaSistrix,
|
||||||
} from 'react-icons/fa'
|
} from 'react-icons/fa'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { GridColumnData } from '../list/GridColumnData'
|
import { GridColumnData } from '../list/GridColumnData'
|
||||||
|
|
@ -160,8 +160,6 @@ const FormButtons = (props: {
|
||||||
|
|
||||||
{hasVisibleToolbarButtons && <Badge innerClass="bg-blue-500" />}
|
{hasVisibleToolbarButtons && <Badge innerClass="bg-blue-500" />}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{commandColumnData?.buttons
|
{commandColumnData?.buttons
|
||||||
?.filter((item) => typeof item !== 'string' && !commandExcludedNames.includes(item.name!))
|
?.filter((item) => typeof item !== 'string' && !commandExcludedNames.includes(item.name!))
|
||||||
.map((item: any, i) => {
|
.map((item: any, i) => {
|
||||||
|
|
@ -292,7 +290,7 @@ const FormButtons = (props: {
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="default"
|
variant="default"
|
||||||
className="dx-button dx-button-mode-contained dx-button-normal"
|
className="dx-button dx-button-mode-contained dx-button-normal"
|
||||||
icon={<FaFileAlt />}
|
icon={<FaSistrix />}
|
||||||
title={translate('::App.Reports.Form')}
|
title={translate('::App.Reports.Form')}
|
||||||
onClick={openDynamicGridReport}
|
onClick={openDynamicGridReport}
|
||||||
></Button>
|
></Button>
|
||||||
|
|
|
||||||
|
|
@ -6,18 +6,34 @@ import { useLocation, useParams } from 'react-router-dom'
|
||||||
import { RequestOptions } from 'devexpress-reporting-react/dx-report-designer'
|
import { RequestOptions } from 'devexpress-reporting-react/dx-report-designer'
|
||||||
import { APP_NAME } from '@/constants/app.constant'
|
import { APP_NAME } from '@/constants/app.constant'
|
||||||
import { createReportUrl } from './reportRouteParams'
|
import { createReportUrl } from './reportRouteParams'
|
||||||
|
import { MODE_DARK } from '@/constants/theme.constant'
|
||||||
|
import { useStoreState } from '@/store/store'
|
||||||
|
|
||||||
const ReportDesigner = lazy(() =>
|
const ReportDesigner = lazy(() =>
|
||||||
import('devexpress-reporting-react/dx-report-designer')
|
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 = [
|
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.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-webdocumentviewer.css', import.meta.url).href,
|
||||||
new URL('devexpress-reporting/dist/css/dx-reportdesigner.css', import.meta.url).href,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
styles.forEach((href) => {
|
styles.forEach((href) => {
|
||||||
|
|
@ -27,6 +43,45 @@ const loadDesignerCss = () => {
|
||||||
link.href = href
|
link.href = href
|
||||||
document.head.appendChild(link)
|
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 = () => {
|
const DevexpressReportDesigner: React.FC = () => {
|
||||||
|
|
@ -36,17 +91,18 @@ const DevexpressReportDesigner: React.FC = () => {
|
||||||
id: string
|
id: string
|
||||||
listFormCode: string
|
listFormCode: string
|
||||||
}>()
|
}>()
|
||||||
|
const themeMode = useStoreState((state) => state.theme.mode)
|
||||||
|
const isDarkMode = themeMode === MODE_DARK
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const reportUrlWithParams = createReportUrl({ report, id, listFormCode }, location.search, 'design')
|
const reportUrlWithParams = createReportUrl({ report, id, listFormCode }, location.search, 'design')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadDesignerCss()
|
loadViewerCss(isDarkMode)
|
||||||
}, [])
|
}, [isDarkMode])
|
||||||
|
|
||||||
if (!reportUrlWithParams) return null
|
if (!reportUrlWithParams) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container className={getViewerClassName(isDarkMode)}>
|
||||||
<Helmet
|
<Helmet
|
||||||
titleTemplate={`%s | ${APP_NAME}`}
|
titleTemplate={`%s | ${APP_NAME}`}
|
||||||
title={translate('::App.Reports')}
|
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 { Container } from '@/components/shared'
|
||||||
import { Helmet } from 'react-helmet'
|
import { Helmet } from 'react-helmet'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { useParams, useLocation } from 'react-router-dom'
|
import { useParams, useLocation } from 'react-router-dom'
|
||||||
import ReportViewer, {
|
import ReportViewer, {
|
||||||
|
Callbacks,
|
||||||
RequestOptions,
|
RequestOptions,
|
||||||
} from 'devexpress-reporting-react/dx-report-viewer'
|
} from 'devexpress-reporting-react/dx-report-viewer'
|
||||||
import { ajaxSetup } from '@devexpress/analytics-core/analytics-utils-native'
|
import { ajaxSetup } from '@devexpress/analytics-core/analytics-utils-native'
|
||||||
|
import { ZoomAutoBy } from 'devexpress-reporting/viewer/constants'
|
||||||
import { APP_NAME } from '@/constants/app.constant'
|
import { APP_NAME } from '@/constants/app.constant'
|
||||||
import { MODE_DARK } from '@/constants/theme.constant'
|
import { MODE_DARK } from '@/constants/theme.constant'
|
||||||
import { useStoreState } from '@/store'
|
import { useStoreState } from '@/store'
|
||||||
|
|
@ -14,6 +16,7 @@ import { createReportUrl } from './reportRouteParams'
|
||||||
|
|
||||||
const VIEWER_THEME_LINK_ID = 'devexpress-report-viewer-theme'
|
const VIEWER_THEME_LINK_ID = 'devexpress-report-viewer-theme'
|
||||||
const VIEWER_DARK_OVERRIDES_ID = 'devexpress-report-viewer-dark-overrides'
|
const VIEWER_DARK_OVERRIDES_ID = 'devexpress-report-viewer-dark-overrides'
|
||||||
|
const DEFAULT_REPORT_ZOOM = 1
|
||||||
|
|
||||||
const getViewerClassName = (isDarkMode: boolean) =>
|
const getViewerClassName = (isDarkMode: boolean) =>
|
||||||
[
|
[
|
||||||
|
|
@ -94,8 +97,43 @@ const DevexpressReportViewer: React.FC = () => {
|
||||||
const token = useStoreState((state) => state.auth.session.token)
|
const token = useStoreState((state) => state.auth.session.token)
|
||||||
const themeMode = useStoreState((state) => state.theme.mode)
|
const themeMode = useStoreState((state) => state.theme.mode)
|
||||||
const [isRequestAuthReady, setIsRequestAuthReady] = useState(false)
|
const [isRequestAuthReady, setIsRequestAuthReady] = useState(false)
|
||||||
|
const previewModelRef = useRef<any>(null)
|
||||||
const isDarkMode = themeMode === MODE_DARK
|
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(() => {
|
useEffect(() => {
|
||||||
loadViewerCss(isDarkMode)
|
loadViewerCss(isDarkMode)
|
||||||
}, [isDarkMode])
|
}, [isDarkMode])
|
||||||
|
|
@ -136,6 +174,10 @@ const DevexpressReportViewer: React.FC = () => {
|
||||||
|
|
||||||
{isRequestAuthReady && (
|
{isRequestAuthReady && (
|
||||||
<ReportViewer reportUrl={reportUrlWithParams}>
|
<ReportViewer reportUrl={reportUrlWithParams}>
|
||||||
|
<Callbacks
|
||||||
|
BeforeRender={setDefaultZoom}
|
||||||
|
DocumentReady={setDefaultZoom}
|
||||||
|
/>
|
||||||
<RequestOptions
|
<RequestOptions
|
||||||
host={import.meta.env.VITE_API_URL}
|
host={import.meta.env.VITE_API_URL}
|
||||||
invokeAction="/DXXRDV"
|
invokeAction="/DXXRDV"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue