Calude güncellemesi Grid, Tree, Rapor ve diğerleri
This commit is contained in:
parent
ac4ccd76d6
commit
0224f4b3e1
55 changed files with 4815 additions and 6528 deletions
|
|
@ -1,11 +1,14 @@
|
||||||
namespace Sozsoft.Platform.Enums;
|
namespace Sozsoft.Platform.Enums;
|
||||||
|
|
||||||
public static class ListFormTabTypeEnum
|
public static class ListFormTabTypeEnum
|
||||||
{
|
{
|
||||||
public const string List = "List";
|
public const string List = "List";
|
||||||
|
public const string Card = "Card";
|
||||||
public const string Tree = "Tree";
|
public const string Tree = "Tree";
|
||||||
|
public const string Gantt = "Gantt";
|
||||||
|
public const string Scheduler = "Scheduler";
|
||||||
|
public const string Todo = "Todo";
|
||||||
public const string Form = "Form";
|
public const string Form = "Form";
|
||||||
public const string Chart = "Chart";
|
public const string Chart = "Chart";
|
||||||
public const string Pivot = "Pivot";
|
public const string Pivot = "Pivot";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ public class DynamicFormReport : XtraReport
|
||||||
private const string QueryName = "DynamicForm";
|
private const string QueryName = "DynamicForm";
|
||||||
private const int HorizontalMargin = 50;
|
private const int HorizontalMargin = 50;
|
||||||
private const int LandscapeColumnThreshold = 4;
|
private const int LandscapeColumnThreshold = 4;
|
||||||
|
private const int VerticalMargin = 35;
|
||||||
private const float RowHeight = 24;
|
private const float RowHeight = 24;
|
||||||
private const float ColumnGap = 10;
|
private const float ColumnGap = 10;
|
||||||
private static readonly System.Drawing.Color FieldBorderColor = System.Drawing.Color.FromArgb(226, 232, 240);
|
private static readonly System.Drawing.Color FieldBorderColor = System.Drawing.Color.FromArgb(226, 232, 240);
|
||||||
|
|
@ -106,7 +107,7 @@ public class DynamicFormReport : XtraReport
|
||||||
|
|
||||||
await selectQueryManager.PrepareQueriesAsync(listForm, fields, dataSourceType, customizations, queryParams);
|
await selectQueryManager.PrepareQueriesAsync(listForm, fields, dataSourceType, customizations, queryParams);
|
||||||
|
|
||||||
var reportFields = GetReportFields(listForm, fields, selectQueryManager, defaultValueHelper);
|
var reportFields = GetReportFields(fields, selectQueryManager, defaultValueHelper, localizer);
|
||||||
var selectQuery = DynamicReportImageHelper.StripReportPaging(DynamicReportImageHelper.ApplyQueryParameters(
|
var selectQuery = DynamicReportImageHelper.StripReportPaging(DynamicReportImageHelper.ApplyQueryParameters(
|
||||||
selectQueryManager.SelectQuery,
|
selectQueryManager.SelectQuery,
|
||||||
selectQueryManager.SelectQueryParameters,
|
selectQueryManager.SelectQueryParameters,
|
||||||
|
|
@ -133,23 +134,12 @@ public class DynamicFormReport : XtraReport
|
||||||
|
|
||||||
private void ConfigurePage(bool landscape)
|
private void ConfigurePage(bool landscape)
|
||||||
{
|
{
|
||||||
Margins = CreateReportMargins(HorizontalMargin, HorizontalMargin, 35, 35);
|
Margins = DynamicReportLayoutHelper.CreateMargins(HorizontalMargin, VerticalMargin);
|
||||||
PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4;
|
PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4;
|
||||||
Landscape = landscape;
|
Landscape = landscape;
|
||||||
Font = DynamicReportFontHelper.CreateDataFont();
|
Font = DynamicReportFontHelper.CreateDataFont();
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma warning disable CA1416
|
|
||||||
private static System.Drawing.Printing.Margins CreateReportMargins(
|
|
||||||
int left,
|
|
||||||
int right,
|
|
||||||
int top,
|
|
||||||
int bottom)
|
|
||||||
{
|
|
||||||
return new System.Drawing.Printing.Margins(left, right, top, bottom);
|
|
||||||
}
|
|
||||||
#pragma warning restore CA1416
|
|
||||||
|
|
||||||
private void ConfigureDataSource(string selectQuery, string connectionString, DataSourceTypeEnum dataSourceType)
|
private void ConfigureDataSource(string selectQuery, string connectionString, DataSourceTypeEnum dataSourceType)
|
||||||
{
|
{
|
||||||
var dataSource = new SqlDataSource(new CustomStringConnectionParameters(
|
var dataSource = new SqlDataSource(new CustomStringConnectionParameters(
|
||||||
|
|
@ -175,7 +165,8 @@ public class DynamicFormReport : XtraReport
|
||||||
var topMargin = new TopMarginBand { HeightF = Margins.Top };
|
var topMargin = new TopMarginBand { HeightF = Margins.Top };
|
||||||
var bottomMargin = new BottomMarginBand { HeightF = 32 };
|
var bottomMargin = new BottomMarginBand { HeightF = 32 };
|
||||||
var reportHeader = new ReportHeaderBand { HeightF = 38 };
|
var reportHeader = new ReportHeaderBand { HeightF = 38 };
|
||||||
var detail = new DetailBand { HeightF = CalculateDetailHeight(formGroups, subReports.Count) };
|
// Nihai yükseklik yerleşim tamamlandıktan sonra hesaplanıp atanır.
|
||||||
|
var detail = new DetailBand { HeightF = RowHeight };
|
||||||
|
|
||||||
Bands.AddRange([topMargin, reportHeader, detail, bottomMargin]);
|
Bands.AddRange([topMargin, reportHeader, detail, bottomMargin]);
|
||||||
|
|
||||||
|
|
@ -195,13 +186,7 @@ public class DynamicFormReport : XtraReport
|
||||||
DynamicReportImageHelper.AddFooterTopLine(bottomMargin.Controls, pageWidth);
|
DynamicReportImageHelper.AddFooterTopLine(bottomMargin.Controls, pageWidth);
|
||||||
DynamicReportImageHelper.AddCurrentUserNameToFooter(bottomMargin.Controls, currentUserName, pageWidth);
|
DynamicReportImageHelper.AddCurrentUserNameToFooter(bottomMargin.Controls, currentUserName, pageWidth);
|
||||||
|
|
||||||
bottomMargin.Controls.Add(new XRPageInfo
|
DynamicReportLayoutHelper.AddPageInfoToFooter(bottomMargin.Controls, pageWidth);
|
||||||
{
|
|
||||||
TextFormatString = "Sayfa {0} / {1}",
|
|
||||||
BoundsF = new System.Drawing.RectangleF(pageWidth / 2, 8, pageWidth / 2, 18),
|
|
||||||
Font = DynamicReportFontHelper.CreateValueFont(8F),
|
|
||||||
TextAlignment = TextAlignment.MiddleRight
|
|
||||||
});
|
|
||||||
|
|
||||||
var y = 0F;
|
var y = 0F;
|
||||||
foreach (var group in formGroups)
|
foreach (var group in formGroups)
|
||||||
|
|
@ -451,25 +436,6 @@ public class DynamicFormReport : XtraReport
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static float CalculateDetailHeight(
|
|
||||||
IReadOnlyList<FormGroup> groups,
|
|
||||||
int subFormCount)
|
|
||||||
{
|
|
||||||
var groupHeight = groups
|
|
||||||
.Sum(group => (string.IsNullOrWhiteSpace(group.Caption) ? 0 : 26) +
|
|
||||||
CalculateGroupFieldsHeight(group) + 8);
|
|
||||||
|
|
||||||
var subFormHeight = subFormCount * 32;
|
|
||||||
return Math.Max(groupHeight + subFormHeight, RowHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static float CalculateGroupFieldsHeight(FormGroup group)
|
|
||||||
{
|
|
||||||
var columnCount = GetSafeColumnCount(group.ColumnCount);
|
|
||||||
return CreateLayoutRows(group.Fields, columnCount)
|
|
||||||
.Sum(row => row.Max(x => GetFieldRowHeight(x.Field)) + 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static float GetCellWidth(float pageWidth, int columnCount)
|
private static float GetCellWidth(float pageWidth, int columnCount)
|
||||||
{
|
{
|
||||||
return (pageWidth - ColumnGap * (columnCount - 1)) / columnCount;
|
return (pageWidth - ColumnGap * (columnCount - 1)) / columnCount;
|
||||||
|
|
@ -532,10 +498,10 @@ public class DynamicFormReport : XtraReport
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<FormReportField> GetReportFields(
|
private static List<FormReportField> GetReportFields(
|
||||||
ListForm listForm,
|
|
||||||
List<ListFormField> fields,
|
List<ListFormField> fields,
|
||||||
ISelectQueryManager selectQueryManager,
|
ISelectQueryManager selectQueryManager,
|
||||||
DefaultValueHelper defaultValueHelper)
|
DefaultValueHelper defaultValueHelper,
|
||||||
|
IStringLocalizer<PlatformResource> localizer)
|
||||||
{
|
{
|
||||||
var fieldsByName = fields
|
var fieldsByName = fields
|
||||||
.Where(x => !string.IsNullOrWhiteSpace(x.FieldName) && x.FieldName != "undefined")
|
.Where(x => !string.IsNullOrWhiteSpace(x.FieldName) && x.FieldName != "undefined")
|
||||||
|
|
@ -565,7 +531,13 @@ public class DynamicFormReport : XtraReport
|
||||||
.Select(x =>
|
.Select(x =>
|
||||||
{
|
{
|
||||||
var selectField = selectFieldsByName[x.Field.FieldName];
|
var selectField = selectFieldsByName[x.Field.FieldName];
|
||||||
return CreateFormReportField(x.Field, selectField, x.EditorType, x.ColumnSpan, defaultValueHelper);
|
return CreateFormReportField(
|
||||||
|
x.Field,
|
||||||
|
selectField,
|
||||||
|
x.EditorType,
|
||||||
|
x.ColumnSpan,
|
||||||
|
defaultValueHelper,
|
||||||
|
localizer);
|
||||||
})
|
})
|
||||||
.Where(x => !string.IsNullOrWhiteSpace(x.BindingName) && x.BindingName != "undefined")
|
.Where(x => !string.IsNullOrWhiteSpace(x.BindingName) && x.BindingName != "undefined")
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
@ -573,7 +545,6 @@ public class DynamicFormReport : XtraReport
|
||||||
|
|
||||||
private static IEnumerable<FormGroup> GetFormGroups(ListForm listForm, IReadOnlyList<FormReportField> fields)
|
private static IEnumerable<FormGroup> GetFormGroups(ListForm listForm, IReadOnlyList<FormReportField> fields)
|
||||||
{
|
{
|
||||||
var fieldsByName = fields.ToDictionary(x => x.Field.FieldName, StringComparer.OrdinalIgnoreCase);
|
|
||||||
var groups = GetEditingFormGroups(listForm).ToList();
|
var groups = GetEditingFormGroups(listForm).ToList();
|
||||||
if (groups.Count == 0)
|
if (groups.Count == 0)
|
||||||
{
|
{
|
||||||
|
|
@ -645,7 +616,7 @@ public class DynamicFormReport : XtraReport
|
||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var subForm in subForms.Where(x => string.Equals(x.TabType, ListFormTypeEnum.List, StringComparison.OrdinalIgnoreCase)))
|
foreach (var subForm in subForms.Where(x => string.Equals(x.TabType, ListFormTabTypeEnum.List, StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
var relation = subForm.Relation?
|
var relation = subForm.Relation?
|
||||||
.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.ParentFieldName) &&
|
.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.ParentFieldName) &&
|
||||||
|
|
@ -679,7 +650,8 @@ public class DynamicFormReport : XtraReport
|
||||||
SelectField selectField,
|
SelectField selectField,
|
||||||
string editorType,
|
string editorType,
|
||||||
int columnSpan,
|
int columnSpan,
|
||||||
DefaultValueHelper defaultValueHelper)
|
DefaultValueHelper defaultValueHelper,
|
||||||
|
IStringLocalizer<PlatformResource> localizer)
|
||||||
{
|
{
|
||||||
var bindingName = DynamicReportImageHelper.GetReportFieldName(selectField);
|
var bindingName = DynamicReportImageHelper.GetReportFieldName(selectField);
|
||||||
var lookup = DynamicReportImageHelper.GetLookup(field);
|
var lookup = DynamicReportImageHelper.GetLookup(field);
|
||||||
|
|
@ -697,7 +669,10 @@ public class DynamicFormReport : XtraReport
|
||||||
|
|
||||||
if (lookup.DataSourceType == UiLookupDataSourceTypeEnum.StaticData)
|
if (lookup.DataSourceType == UiLookupDataSourceTypeEnum.StaticData)
|
||||||
{
|
{
|
||||||
var staticLookupValues = DynamicReportImageHelper.GetStaticLookupValues(lookup, defaultValueHelper);
|
var staticLookupValues = DynamicReportImageHelper.GetStaticLookupValues(
|
||||||
|
lookup,
|
||||||
|
defaultValueHelper,
|
||||||
|
localizer);
|
||||||
return new FormReportField(
|
return new FormReportField(
|
||||||
field,
|
field,
|
||||||
staticLookupValues?.Count > 0 ? DynamicReportSqlBuilder.GetLookupBindingName(field) : bindingName,
|
staticLookupValues?.Count > 0 ? DynamicReportSqlBuilder.GetLookupBindingName(field) : bindingName,
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,8 @@ public class DynamicGridReport : XtraReport
|
||||||
private const int A4PortraitWidth = 827;
|
private const int A4PortraitWidth = 827;
|
||||||
private const int HorizontalMargin = 50;
|
private const int HorizontalMargin = 50;
|
||||||
private const int LandscapeColumnThreshold = 5;
|
private const int LandscapeColumnThreshold = 5;
|
||||||
|
private const int VerticalMargin = 35;
|
||||||
private const float GroupIndentWidth = 18F;
|
private const float GroupIndentWidth = 18F;
|
||||||
private static readonly System.Drawing.Color TableBorderColor = System.Drawing.Color.FromArgb(226, 232, 240);
|
|
||||||
private static IServiceScopeFactory _scopeFactory;
|
private static IServiceScopeFactory _scopeFactory;
|
||||||
|
|
||||||
public bool RequiresLandscapeLayout { get; private set; }
|
public bool RequiresLandscapeLayout { get; private set; }
|
||||||
|
|
@ -149,12 +149,14 @@ public class DynamicGridReport : XtraReport
|
||||||
groupedFields,
|
groupedFields,
|
||||||
selectFieldsByName,
|
selectFieldsByName,
|
||||||
editorTypesByField,
|
editorTypesByField,
|
||||||
defaultValueHelper);
|
defaultValueHelper,
|
||||||
|
localizer);
|
||||||
var reportColumns = CreateReportColumns(
|
var reportColumns = CreateReportColumns(
|
||||||
reportFields,
|
reportFields,
|
||||||
selectFieldsByName,
|
selectFieldsByName,
|
||||||
editorTypesByField,
|
editorTypesByField,
|
||||||
defaultValueHelper);
|
defaultValueHelper,
|
||||||
|
localizer);
|
||||||
|
|
||||||
var allColumns = groupColumns
|
var allColumns = groupColumns
|
||||||
.Concat(reportColumns)
|
.Concat(reportColumns)
|
||||||
|
|
@ -188,40 +190,38 @@ public class DynamicGridReport : XtraReport
|
||||||
RequestParameters = false;
|
RequestParameters = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<ReportColumn> CreateReportColumns(
|
private static List<DynamicReportColumn> CreateReportColumns(
|
||||||
IEnumerable<ListFormField> fields,
|
IEnumerable<ListFormField> fields,
|
||||||
IReadOnlyDictionary<string, SelectField> selectFieldsByName,
|
IReadOnlyDictionary<string, SelectField> selectFieldsByName,
|
||||||
IReadOnlyDictionary<string, string> editorTypesByField,
|
IReadOnlyDictionary<string, string> editorTypesByField,
|
||||||
DefaultValueHelper defaultValueHelper)
|
DefaultValueHelper defaultValueHelper,
|
||||||
|
IStringLocalizer<PlatformResource> localizer)
|
||||||
{
|
{
|
||||||
return fields
|
return fields
|
||||||
.Select(field =>
|
.Select(field =>
|
||||||
{
|
{
|
||||||
var selectField = selectFieldsByName[field.FieldName];
|
|
||||||
editorTypesByField.TryGetValue(field.FieldName, out var editorType);
|
editorTypesByField.TryGetValue(field.FieldName, out var editorType);
|
||||||
return CreateReportColumn(
|
return DynamicReportColumnFactory.Create(
|
||||||
field,
|
field,
|
||||||
selectField,
|
selectFieldsByName[field.FieldName],
|
||||||
defaultValueHelper,
|
defaultValueHelper,
|
||||||
editorType,
|
editorType,
|
||||||
DynamicReportImageHelper.IsImageEditor(editorType),
|
localizer);
|
||||||
editorType == PlatformConsts.EditorTypes.dxHtmlEditor);
|
|
||||||
})
|
})
|
||||||
.Where(x => !string.IsNullOrWhiteSpace(x.BindingName) && x.BindingName != "undefined")
|
.Where(x => x.HasBindingName)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma warning disable CA1416
|
|
||||||
private void ConfigurePage(
|
private void ConfigurePage(
|
||||||
IReadOnlyCollection<ReportColumn> visibleFields,
|
IReadOnlyCollection<DynamicReportColumn> visibleFields,
|
||||||
IStringLocalizer<PlatformResource> localizer,
|
IStringLocalizer<PlatformResource> localizer,
|
||||||
IReadOnlyDictionary<string, float> stateWidths)
|
IReadOnlyDictionary<string, float> stateWidths)
|
||||||
{
|
{
|
||||||
Margins = CreateReportMargins(HorizontalMargin, HorizontalMargin, 35, 35);
|
Margins = DynamicReportLayoutHelper.CreateMargins(HorizontalMargin, VerticalMargin);
|
||||||
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 => x.IsImage
|
var requestedContentWidth = visibleFields.Sum(x => x.IsImage
|
||||||
? GetImageColumnWidth(x)
|
? x.ImageWidth
|
||||||
: DynamicReportGridStateHelper.GetPreferredColumnWidth(x.Field, localizer, stateWidths));
|
: DynamicReportGridStateHelper.GetPreferredColumnWidth(x.Field, localizer, stateWidths));
|
||||||
RequiresLandscapeLayout = visibleFields.Count >= LandscapeColumnThreshold ||
|
RequiresLandscapeLayout = visibleFields.Count >= LandscapeColumnThreshold ||
|
||||||
requestedContentWidth > portraitContentWidth;
|
requestedContentWidth > portraitContentWidth;
|
||||||
|
|
@ -229,16 +229,6 @@ public class DynamicGridReport : XtraReport
|
||||||
Font = DynamicReportFontHelper.CreateDataFont();
|
Font = DynamicReportFontHelper.CreateDataFont();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static System.Drawing.Printing.Margins CreateReportMargins(
|
|
||||||
int left,
|
|
||||||
int right,
|
|
||||||
int top,
|
|
||||||
int bottom)
|
|
||||||
{
|
|
||||||
return new System.Drawing.Printing.Margins(left, right, top, bottom);
|
|
||||||
}
|
|
||||||
#pragma warning restore CA1416
|
|
||||||
|
|
||||||
private void ConfigureDataSource(string selectQuery, string connectionString, DataSourceTypeEnum dataSourceType)
|
private void ConfigureDataSource(string selectQuery, string connectionString, DataSourceTypeEnum dataSourceType)
|
||||||
{
|
{
|
||||||
var dataSource = new SqlDataSource(new CustomStringConnectionParameters(
|
var dataSource = new SqlDataSource(new CustomStringConnectionParameters(
|
||||||
|
|
@ -249,8 +239,8 @@ public class DynamicGridReport : XtraReport
|
||||||
DataMember = QueryName;
|
DataMember = QueryName;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<List<ReportColumn>> ApplyImageCountsAsync(
|
private static async Task<List<DynamicReportColumn>> ApplyImageCountsAsync(
|
||||||
IReadOnlyList<ReportColumn> columns,
|
IReadOnlyList<DynamicReportColumn> columns,
|
||||||
IDynamicDataRepository dynamicDataRepository,
|
IDynamicDataRepository dynamicDataRepository,
|
||||||
string selectQuery,
|
string selectQuery,
|
||||||
string connectionString,
|
string connectionString,
|
||||||
|
|
@ -286,7 +276,7 @@ public class DynamicGridReport : XtraReport
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static object GetImageRawValue(IDictionary<string, object> row, ReportColumn column)
|
private static object GetImageRawValue(IDictionary<string, object> row, DynamicReportColumn column)
|
||||||
{
|
{
|
||||||
if (DynamicReportImageHelper.TryGetRowValue(row, column.BindingName, out var value) &&
|
if (DynamicReportImageHelper.TryGetRowValue(row, column.BindingName, out var value) &&
|
||||||
DynamicReportImageHelper.HasValue(value))
|
DynamicReportImageHelper.HasValue(value))
|
||||||
|
|
@ -299,8 +289,8 @@ public class DynamicGridReport : XtraReport
|
||||||
|
|
||||||
private void BuildLayout(
|
private void BuildLayout(
|
||||||
ListForm listForm,
|
ListForm listForm,
|
||||||
IReadOnlyList<ReportColumn> fields,
|
IReadOnlyList<DynamicReportColumn> fields,
|
||||||
IReadOnlyList<ReportColumn> groupFields,
|
IReadOnlyList<DynamicReportColumn> groupFields,
|
||||||
IStringLocalizer<PlatformResource> localizer,
|
IStringLocalizer<PlatformResource> localizer,
|
||||||
string imageBaseUrl,
|
string imageBaseUrl,
|
||||||
string currentUserName,
|
string currentUserName,
|
||||||
|
|
@ -317,14 +307,11 @@ public class DynamicGridReport : XtraReport
|
||||||
{
|
{
|
||||||
HeightF = !isSubReport && !string.IsNullOrWhiteSpace(criteriaText) ? 50 : 32
|
HeightF = !isSubReport && !string.IsNullOrWhiteSpace(criteriaText) ? 50 : 32
|
||||||
};
|
};
|
||||||
var pageHeader = new PageHeaderBand { HeightF = 28 };
|
var pageHeader = new PageHeaderBand { HeightF = DynamicReportLayoutHelper.HeaderRowHeight };
|
||||||
var detailHeight = fields.Any(x => x.IsImage)
|
var detailHeight = DynamicReportLayoutHelper.GetDetailHeight(fields);
|
||||||
? DynamicReportImageHelper.GetGridImageHeight()
|
|
||||||
: fields.Any(x => x.IsHtml)
|
|
||||||
? 48
|
|
||||||
: 24;
|
|
||||||
var detail = new DetailBand { HeightF = detailHeight };
|
var detail = new DetailBand { HeightF = detailHeight };
|
||||||
var reportFooter = new ReportFooterBand { HeightF = HasAnySummary(fields) ? 28 : 0 };
|
var hasSummary = DynamicReportLayoutHelper.HasAnySummary(fields);
|
||||||
|
var reportFooter = new ReportFooterBand { HeightF = hasSummary ? 28 : 0 };
|
||||||
var groupHeaderBands = CreateGroupHeaderBands(groupFields, localizer, pageWidth);
|
var groupHeaderBands = CreateGroupHeaderBands(groupFields, localizer, pageWidth);
|
||||||
|
|
||||||
Bands.AddRange([topMargin, reportHeader, pageHeader]);
|
Bands.AddRange([topMargin, reportHeader, pageHeader]);
|
||||||
|
|
@ -344,34 +331,23 @@ public class DynamicGridReport : XtraReport
|
||||||
if (!isSubReport)
|
if (!isSubReport)
|
||||||
{
|
{
|
||||||
DynamicReportImageHelper.AddGeneratedAtToHeader(reportHeader.Controls, pageWidth);
|
DynamicReportImageHelper.AddGeneratedAtToHeader(reportHeader.Controls, pageWidth);
|
||||||
}
|
|
||||||
if (!isSubReport)
|
|
||||||
{
|
|
||||||
DynamicReportImageHelper.AddReportCriteriaToHeader(reportHeader.Controls, criteriaText, pageWidth);
|
DynamicReportImageHelper.AddReportCriteriaToHeader(reportHeader.Controls, criteriaText, pageWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
DynamicReportImageHelper.AddFooterTopLine(bottomMargin.Controls, pageWidth);
|
DynamicReportImageHelper.AddFooterTopLine(bottomMargin.Controls, pageWidth);
|
||||||
DynamicReportImageHelper.AddCurrentUserNameToFooter(bottomMargin.Controls, currentUserName, pageWidth);
|
DynamicReportImageHelper.AddCurrentUserNameToFooter(bottomMargin.Controls, currentUserName, pageWidth);
|
||||||
|
DynamicReportLayoutHelper.AddPageInfoToFooter(bottomMargin.Controls, pageWidth);
|
||||||
|
|
||||||
var pageInfo = new XRPageInfo
|
var detailIndent = GetGroupIndent(groupFields.Count, pageWidth);
|
||||||
{
|
|
||||||
TextFormatString = "Sayfa {0} / {1}",
|
|
||||||
BoundsF = new System.Drawing.RectangleF(pageWidth / 2, 8, pageWidth / 2, 18),
|
|
||||||
Font = DynamicReportFontHelper.CreateValueFont(8F),
|
|
||||||
TextAlignment = TextAlignment.MiddleRight
|
|
||||||
};
|
|
||||||
bottomMargin.Controls.Add(pageInfo);
|
|
||||||
|
|
||||||
var detailIndent = GetGroupDetailIndent(groupFields.Count, pageWidth);
|
|
||||||
var tableWidth = pageWidth - detailIndent;
|
var tableWidth = pageWidth - detailIndent;
|
||||||
var detailWidths = DynamicReportGridStateHelper.CalculateWidths(fields, tableWidth, localizer, stateWidths);
|
var detailWidths = DynamicReportGridStateHelper.CalculateWidths(fields, tableWidth, localizer, stateWidths);
|
||||||
var headerTable = CreateTable(pageWidth, 28);
|
var headerTable = DynamicReportLayoutHelper.CreateTable(pageWidth, DynamicReportLayoutHelper.HeaderRowHeight);
|
||||||
var headerRow = new XRTableRow { HeightF = 28 };
|
var headerRow = new XRTableRow { HeightF = DynamicReportLayoutHelper.HeaderRowHeight };
|
||||||
var detailTable = CreateTable(tableWidth, detailHeight, detailIndent);
|
var detailTable = DynamicReportLayoutHelper.CreateTable(tableWidth, detailHeight, detailIndent);
|
||||||
detailTable.CanGrow = true;
|
detailTable.CanGrow = true;
|
||||||
var detailRow = new XRTableRow { HeightF = detailHeight, CanGrow = true };
|
var detailRow = new XRTableRow { HeightF = detailHeight, CanGrow = true };
|
||||||
var footerTable = CreateTable(tableWidth, 24, detailIndent);
|
var footerTable = DynamicReportLayoutHelper.CreateTable(tableWidth, DynamicReportLayoutHelper.FooterRowHeight, detailIndent);
|
||||||
var footerRow = new XRTableRow { HeightF = 24 };
|
var footerRow = new XRTableRow { HeightF = DynamicReportLayoutHelper.FooterRowHeight };
|
||||||
|
|
||||||
if (detailIndent > 0)
|
if (detailIndent > 0)
|
||||||
{
|
{
|
||||||
|
|
@ -381,9 +357,9 @@ public class DynamicGridReport : XtraReport
|
||||||
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, detailWidths[i], localizer));
|
headerRow.Cells.Add(DynamicReportLayoutHelper.CreateHeaderCell(field, detailWidths[i], localizer));
|
||||||
detailRow.Cells.Add(CreateDetailCell(field, detailWidths[i], imageBaseUrl, detailHeight));
|
detailRow.Cells.Add(DynamicReportLayoutHelper.CreateDetailCell(field, detailWidths[i], imageBaseUrl, detailHeight));
|
||||||
footerRow.Cells.Add(CreateFooterCell(field, detailWidths[i]));
|
footerRow.Cells.Add(DynamicReportLayoutHelper.CreateFooterCell(field, detailWidths[i]));
|
||||||
}
|
}
|
||||||
|
|
||||||
headerTable.Rows.Add(headerRow);
|
headerTable.Rows.Add(headerRow);
|
||||||
|
|
@ -392,14 +368,14 @@ public class DynamicGridReport : XtraReport
|
||||||
|
|
||||||
pageHeader.Controls.Add(headerTable);
|
pageHeader.Controls.Add(headerTable);
|
||||||
detail.Controls.Add(detailTable);
|
detail.Controls.Add(detailTable);
|
||||||
if (HasAnySummary(fields))
|
if (hasSummary)
|
||||||
{
|
{
|
||||||
reportFooter.Controls.Add(footerTable);
|
reportFooter.Controls.Add(footerTable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<GroupHeaderBand> CreateGroupHeaderBands(
|
private static List<GroupHeaderBand> CreateGroupHeaderBands(
|
||||||
IReadOnlyList<ReportColumn> groupFields,
|
IReadOnlyList<DynamicReportColumn> groupFields,
|
||||||
IStringLocalizer<PlatformResource> localizer,
|
IStringLocalizer<PlatformResource> localizer,
|
||||||
float pageWidth)
|
float pageWidth)
|
||||||
{
|
{
|
||||||
|
|
@ -418,7 +394,7 @@ public class DynamicGridReport : XtraReport
|
||||||
};
|
};
|
||||||
band.GroupFields.Add(new GroupField(groupField.BindingName, XRColumnSortOrder.Ascending));
|
band.GroupFields.Add(new GroupField(groupField.BindingName, XRColumnSortOrder.Ascending));
|
||||||
|
|
||||||
var indent = GetGroupHeaderIndent(i, pageWidth);
|
var indent = GetGroupIndent(i, pageWidth);
|
||||||
var caption = DynamicReportImageHelper.GetLocalizedFieldCaption(groupField.Field, localizer);
|
var caption = DynamicReportImageHelper.GetLocalizedFieldCaption(groupField.Field, localizer);
|
||||||
var availableWidth = pageWidth - indent;
|
var availableWidth = pageWidth - indent;
|
||||||
var captionWidth = Math.Clamp(caption.Length * 6F + 28F, 90F, Math.Min(220F, availableWidth / 2));
|
var captionWidth = Math.Clamp(caption.Length * 6F + 28F, 90F, Math.Min(220F, availableWidth / 2));
|
||||||
|
|
@ -432,7 +408,7 @@ public class DynamicGridReport : XtraReport
|
||||||
TextAlignment = TextAlignment.MiddleLeft,
|
TextAlignment = TextAlignment.MiddleLeft,
|
||||||
BackColor = System.Drawing.Color.FromArgb(226, 232, 240),
|
BackColor = System.Drawing.Color.FromArgb(226, 232, 240),
|
||||||
Borders = BorderSide.Left | BorderSide.Top | BorderSide.Bottom,
|
Borders = BorderSide.Left | BorderSide.Top | BorderSide.Bottom,
|
||||||
BorderColor = TableBorderColor,
|
BorderColor = DynamicReportLayoutHelper.TableBorderColor,
|
||||||
BorderWidth = 0.25F
|
BorderWidth = 0.25F
|
||||||
};
|
};
|
||||||
var valueLabel = new XRLabel
|
var valueLabel = new XRLabel
|
||||||
|
|
@ -443,7 +419,7 @@ public class DynamicGridReport : XtraReport
|
||||||
TextAlignment = TextAlignment.MiddleLeft,
|
TextAlignment = TextAlignment.MiddleLeft,
|
||||||
BackColor = System.Drawing.Color.FromArgb(241, 245, 249),
|
BackColor = System.Drawing.Color.FromArgb(241, 245, 249),
|
||||||
Borders = BorderSide.All,
|
Borders = BorderSide.All,
|
||||||
BorderColor = TableBorderColor,
|
BorderColor = DynamicReportLayoutHelper.TableBorderColor,
|
||||||
BorderWidth = 0.25F,
|
BorderWidth = 0.25F,
|
||||||
CanGrow = true,
|
CanGrow = true,
|
||||||
Multiline = true
|
Multiline = true
|
||||||
|
|
@ -463,25 +439,9 @@ public class DynamicGridReport : XtraReport
|
||||||
return bands;
|
return bands;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static float GetGroupHeaderIndent(int groupIndex, float pageWidth)
|
private static float GetGroupIndent(int level, float pageWidth)
|
||||||
{
|
{
|
||||||
return Math.Min(groupIndex * GroupIndentWidth, pageWidth * 0.25F);
|
return Math.Min(level * GroupIndentWidth, pageWidth * 0.25F);
|
||||||
}
|
|
||||||
|
|
||||||
private static float GetGroupDetailIndent(int groupCount, float pageWidth)
|
|
||||||
{
|
|
||||||
return Math.Min(groupCount * GroupIndentWidth, pageWidth * 0.25F);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static XRTable CreateTable(float width, float height, float left = 0)
|
|
||||||
{
|
|
||||||
return new XRTable
|
|
||||||
{
|
|
||||||
BoundsF = new System.Drawing.RectangleF(left, 0, width, height),
|
|
||||||
Borders = BorderSide.All,
|
|
||||||
BorderColor = TableBorderColor,
|
|
||||||
BorderWidth = 0.25F
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static XRTableCell CreateIndentHeaderCell(float width)
|
private static XRTableCell CreateIndentHeaderCell(float width)
|
||||||
|
|
@ -495,244 +455,6 @@ public class DynamicGridReport : XtraReport
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static XRTableCell CreateHeaderCell(
|
|
||||||
ReportColumn column,
|
|
||||||
float width,
|
|
||||||
IStringLocalizer<PlatformResource> localizer)
|
|
||||||
{
|
|
||||||
var field = column.Field;
|
|
||||||
return new XRTableCell
|
|
||||||
{
|
|
||||||
Text = DynamicReportImageHelper.GetLocalizedFieldCaption(field, localizer),
|
|
||||||
WidthF = width,
|
|
||||||
Weight = width,
|
|
||||||
Font = DynamicReportFontHelper.CreateHeaderFont(),
|
|
||||||
BackColor = System.Drawing.Color.FromArgb(238, 242, 247),
|
|
||||||
Padding = new PaddingInfo(4, 4, 2, 2),
|
|
||||||
TextAlignment = TextAlignment.MiddleCenter,
|
|
||||||
Multiline = true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static XRTableCell CreateDetailCell(
|
|
||||||
ReportColumn column,
|
|
||||||
float width,
|
|
||||||
string imageBaseUrl,
|
|
||||||
float height)
|
|
||||||
{
|
|
||||||
var field = column.Field;
|
|
||||||
if (column.IsImage)
|
|
||||||
{
|
|
||||||
return DynamicReportImageHelper.CreateGridImageCell(
|
|
||||||
column.BindingName,
|
|
||||||
column.Field.FieldName,
|
|
||||||
width,
|
|
||||||
imageBaseUrl,
|
|
||||||
column.ImageCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.IsHtml)
|
|
||||||
{
|
|
||||||
return CreateHtmlDetailCell(column, width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
var cell = new XRTableCell
|
|
||||||
{
|
|
||||||
WidthF = width,
|
|
||||||
Weight = width,
|
|
||||||
Font = DynamicReportFontHelper.CreateDataFont(),
|
|
||||||
Padding = new PaddingInfo(4, 4, 2, 2),
|
|
||||||
TextAlignment = DynamicReportImageHelper.ToTextAlignment(field.Alignment, field.SourceDbType),
|
|
||||||
Multiline = true,
|
|
||||||
CanGrow = true
|
|
||||||
};
|
|
||||||
var valueExpression = DynamicReportImageHelper.CreateFormatValueExpression($"[{column.BindingName}]", field, column.EditorType);
|
|
||||||
cell.ExpressionBindings.Add(new ExpressionBinding(
|
|
||||||
"BeforePrint",
|
|
||||||
"Text",
|
|
||||||
valueExpression));
|
|
||||||
cell.ExpressionBindings.Add(new ExpressionBinding(
|
|
||||||
"BeforePrint",
|
|
||||||
"Font.Name",
|
|
||||||
DynamicReportImageHelper.CreateFontFamilyExpression(valueExpression)));
|
|
||||||
|
|
||||||
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 = true
|
|
||||||
};
|
|
||||||
|
|
||||||
var richText = new XRRichText
|
|
||||||
{
|
|
||||||
BoundsF = new System.Drawing.RectangleF(0, 0, width, height),
|
|
||||||
Font = DynamicReportFontHelper.CreateDataFont(),
|
|
||||||
Padding = new PaddingInfo(4, 4, 2, 2),
|
|
||||||
CanGrow = true
|
|
||||||
};
|
|
||||||
richText.ExpressionBindings.Add(new ExpressionBinding("BeforePrint", "Html", $"[{column.BindingName}]"));
|
|
||||||
cell.Controls.Add(richText);
|
|
||||||
|
|
||||||
return cell;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static XRTableCell CreateFooterCell(ReportColumn column, float width)
|
|
||||||
{
|
|
||||||
var field = column.Field;
|
|
||||||
var summaryType = GetSummaryType(field.TotalSummaryJson);
|
|
||||||
var cell = new XRTableCell
|
|
||||||
{
|
|
||||||
WidthF = width,
|
|
||||||
Weight = width,
|
|
||||||
Font = DynamicReportFontHelper.CreateDataFont(DXFontStyle.Bold),
|
|
||||||
Padding = new PaddingInfo(4, 4, 2, 2),
|
|
||||||
TextAlignment = DynamicReportImageHelper.ToTextAlignment(field.Alignment, field.SourceDbType)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (summaryType == null)
|
|
||||||
{
|
|
||||||
return cell;
|
|
||||||
}
|
|
||||||
|
|
||||||
cell.ExpressionBindings.Add(new ExpressionBinding(
|
|
||||||
"BeforePrint",
|
|
||||||
"Text",
|
|
||||||
DynamicReportImageHelper.CreateFormatValueExpression(
|
|
||||||
CreateSummaryExpression(summaryType.Value, column.BindingName),
|
|
||||||
field,
|
|
||||||
column.EditorType)));
|
|
||||||
cell.Summary = new XRSummary
|
|
||||||
{
|
|
||||||
Running = SummaryRunning.Report,
|
|
||||||
Func = summaryType.Value,
|
|
||||||
IgnoreNullValues = true
|
|
||||||
};
|
|
||||||
|
|
||||||
return cell;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string CreateSummaryExpression(SummaryFunc summaryType, string bindingName)
|
|
||||||
{
|
|
||||||
return summaryType switch
|
|
||||||
{
|
|
||||||
SummaryFunc.Sum => $"sumSum([{bindingName}])",
|
|
||||||
SummaryFunc.Avg => $"sumAvg([{bindingName}])",
|
|
||||||
SummaryFunc.Min => $"sumMin([{bindingName}])",
|
|
||||||
SummaryFunc.Max => $"sumMax([{bindingName}])",
|
|
||||||
SummaryFunc.Count => "sumCount()",
|
|
||||||
_ => $"sumSum([{bindingName}])"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool HasAnySummary(IEnumerable<ReportColumn> fields)
|
|
||||||
{
|
|
||||||
return fields.Any(x => GetSummaryType(x.Field.TotalSummaryJson) != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static float GetImageColumnWidth(ReportColumn column)
|
|
||||||
{
|
|
||||||
return DynamicReportImageHelper.GetGridImageColumnWidth(column.ImageCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ReportColumn CreateReportColumn(
|
|
||||||
ListFormField field,
|
|
||||||
SelectField selectField,
|
|
||||||
DefaultValueHelper defaultValueHelper,
|
|
||||||
string editorType,
|
|
||||||
bool isImage,
|
|
||||||
bool isHtml)
|
|
||||||
{
|
|
||||||
var bindingName = DynamicReportImageHelper.GetReportFieldName(selectField);
|
|
||||||
var lookup = DynamicReportImageHelper.GetLookup(field);
|
|
||||||
var isMultiValue = IsMultiValueLookupEditor(editorType);
|
|
||||||
|
|
||||||
if (lookup == null || selectField?.JoinOptions != null)
|
|
||||||
{
|
|
||||||
return new ReportColumn(field, bindingName, null, null, isMultiValue, isImage, isHtml, editorType);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DynamicReportSqlBuilder.IsReportQueryableLookup(lookup))
|
|
||||||
{
|
|
||||||
return new ReportColumn(field, DynamicReportSqlBuilder.GetLookupBindingName(field), lookup, null, isMultiValue, isImage, isHtml, editorType);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lookup.DataSourceType == UiLookupDataSourceTypeEnum.StaticData)
|
|
||||||
{
|
|
||||||
var staticLookupValues = DynamicReportImageHelper.GetStaticLookupValues(lookup, defaultValueHelper);
|
|
||||||
return new ReportColumn(
|
|
||||||
field,
|
|
||||||
staticLookupValues?.Count > 0 ? DynamicReportSqlBuilder.GetLookupBindingName(field) : bindingName,
|
|
||||||
lookup,
|
|
||||||
staticLookupValues,
|
|
||||||
isMultiValue,
|
|
||||||
isImage,
|
|
||||||
isHtml,
|
|
||||||
editorType);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ReportColumn(field, bindingName, null, null, isMultiValue, isImage, isHtml, editorType);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsMultiValueLookupEditor(string editorType)
|
|
||||||
{
|
|
||||||
return editorType is PlatformConsts.EditorTypes.dxTagBox or "dxGridBox";
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed record ReportColumn(
|
|
||||||
ListFormField Field,
|
|
||||||
string BindingName,
|
|
||||||
LookupDto Lookup,
|
|
||||||
Dictionary<string, string> StaticLookupValues,
|
|
||||||
bool IsMultiValue,
|
|
||||||
bool IsImage,
|
|
||||||
bool IsHtml,
|
|
||||||
string EditorType,
|
|
||||||
int ImageCount = 1) : DynamicReportSqlBuilder.IReportLookupColumn, DynamicReportGridStateHelper.IReportColumnWidth
|
|
||||||
{
|
|
||||||
public float ImageWidth => GetImageColumnWidth(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static SummaryFunc? GetSummaryType(string totalSummaryJson)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(totalSummaryJson))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var value = totalSummaryJson.ToLowerInvariant();
|
|
||||||
if (value.Contains("\"summarytype\":\"sum\""))
|
|
||||||
{
|
|
||||||
return SummaryFunc.Sum;
|
|
||||||
}
|
|
||||||
if (value.Contains("\"summarytype\":\"avg\""))
|
|
||||||
{
|
|
||||||
return SummaryFunc.Avg;
|
|
||||||
}
|
|
||||||
if (value.Contains("\"summarytype\":\"min\""))
|
|
||||||
{
|
|
||||||
return SummaryFunc.Min;
|
|
||||||
}
|
|
||||||
if (value.Contains("\"summarytype\":\"max\""))
|
|
||||||
{
|
|
||||||
return SummaryFunc.Max;
|
|
||||||
}
|
|
||||||
if (value.Contains("\"summarytype\":\"count\""))
|
|
||||||
{
|
|
||||||
return SummaryFunc.Count;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetParameter(IDictionary<string, string> parameters, string name) =>
|
private static string GetParameter(IDictionary<string, string> parameters, string name) =>
|
||||||
DynamicReportImageHelper.GetParameter(parameters, name);
|
DynamicReportImageHelper.GetParameter(parameters, name);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -112,8 +112,7 @@ public partial class DynamicReport : XtraReport
|
||||||
using var scope = _scopeFactory.CreateScope();
|
using var scope = _scopeFactory.CreateScope();
|
||||||
var dbContext = scope.ServiceProvider.GetRequiredService<PlatformDbContext>();
|
var dbContext = scope.ServiceProvider.GetRequiredService<PlatformDbContext>();
|
||||||
|
|
||||||
var language = dbContext.Set<Language>()
|
var language = GetCurrentLanguage(dbContext);
|
||||||
.FirstOrDefault(l => l.DisplayName == "Türkçe");
|
|
||||||
|
|
||||||
if (language != null)
|
if (language != null)
|
||||||
{
|
{
|
||||||
|
|
@ -129,6 +128,24 @@ public partial class DynamicReport : XtraReport
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rapor isteğiyle gelen kültüre karşılık gelen dil kaydını döner.
|
||||||
|
/// Eşleşme yoksa etkin dillerden ilki kullanılır.
|
||||||
|
/// </summary>
|
||||||
|
private static Language GetCurrentLanguage(PlatformDbContext dbContext)
|
||||||
|
{
|
||||||
|
var cultureName = System.Globalization.CultureInfo.CurrentUICulture.Name;
|
||||||
|
var twoLetterName = System.Globalization.CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;
|
||||||
|
var languages = dbContext.Set<Language>().ToList();
|
||||||
|
|
||||||
|
return languages.FirstOrDefault(l =>
|
||||||
|
string.Equals(l.CultureName, cultureName, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(l.UiCultureName, cultureName, StringComparison.OrdinalIgnoreCase)) ??
|
||||||
|
languages.FirstOrDefault(l =>
|
||||||
|
string.Equals(l.TwoLetterISOLanguageName, twoLetterName, StringComparison.OrdinalIgnoreCase)) ??
|
||||||
|
languages.FirstOrDefault(l => l.IsEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
private void XrRichText1_BeforePrint(object sender, CancelEventArgs e)
|
private void XrRichText1_BeforePrint(object sender, CancelEventArgs e)
|
||||||
{
|
{
|
||||||
var rt = (XRRichText)sender;
|
var rt = (XRRichText)sender;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
|
using Sozsoft.Platform;
|
||||||
|
using Sozsoft.Platform.Localization;
|
||||||
|
using Sozsoft.Platform.Entities;
|
||||||
|
using Sozsoft.Platform.Enums;
|
||||||
|
using Sozsoft.Platform.ListForms;
|
||||||
|
using Sozsoft.Platform.Queries;
|
||||||
|
|
||||||
|
namespace Sozsoft.Reports.PredefinedReports;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Grid ve Tree raporlarının ortak kolon tanımı.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed record DynamicReportColumn(
|
||||||
|
ListFormField Field,
|
||||||
|
string BindingName,
|
||||||
|
LookupDto Lookup,
|
||||||
|
Dictionary<string, string> StaticLookupValues,
|
||||||
|
bool IsMultiValue,
|
||||||
|
bool IsImage,
|
||||||
|
bool IsHtml,
|
||||||
|
string EditorType,
|
||||||
|
int ImageCount = 1) : DynamicReportSqlBuilder.IReportLookupColumn, DynamicReportGridStateHelper.IReportColumnWidth
|
||||||
|
{
|
||||||
|
public float ImageWidth => DynamicReportImageHelper.GetGridImageColumnWidth(ImageCount);
|
||||||
|
|
||||||
|
public bool HasBindingName =>
|
||||||
|
!string.IsNullOrWhiteSpace(BindingName) && BindingName != "undefined";
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class DynamicReportColumnFactory
|
||||||
|
{
|
||||||
|
public static DynamicReportColumn Create(
|
||||||
|
ListFormField field,
|
||||||
|
SelectField selectField,
|
||||||
|
DefaultValueHelper defaultValueHelper,
|
||||||
|
string editorType,
|
||||||
|
IStringLocalizer<PlatformResource> localizer)
|
||||||
|
{
|
||||||
|
var bindingName = DynamicReportImageHelper.GetReportFieldName(selectField);
|
||||||
|
var lookup = DynamicReportImageHelper.GetLookup(field);
|
||||||
|
var isMultiValue = IsMultiValueLookupEditor(editorType);
|
||||||
|
var isImage = DynamicReportImageHelper.IsImageEditor(editorType);
|
||||||
|
var isHtml = editorType == PlatformConsts.EditorTypes.dxHtmlEditor;
|
||||||
|
|
||||||
|
if (lookup == null || selectField?.JoinOptions != null)
|
||||||
|
{
|
||||||
|
return new DynamicReportColumn(field, bindingName, null, null, isMultiValue, isImage, isHtml, editorType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DynamicReportSqlBuilder.IsReportQueryableLookup(lookup))
|
||||||
|
{
|
||||||
|
return new DynamicReportColumn(
|
||||||
|
field,
|
||||||
|
DynamicReportSqlBuilder.GetLookupBindingName(field),
|
||||||
|
lookup,
|
||||||
|
null,
|
||||||
|
isMultiValue,
|
||||||
|
isImage,
|
||||||
|
isHtml,
|
||||||
|
editorType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lookup.DataSourceType == UiLookupDataSourceTypeEnum.StaticData)
|
||||||
|
{
|
||||||
|
var staticLookupValues = DynamicReportImageHelper.GetStaticLookupValues(
|
||||||
|
lookup,
|
||||||
|
defaultValueHelper,
|
||||||
|
localizer);
|
||||||
|
return new DynamicReportColumn(
|
||||||
|
field,
|
||||||
|
staticLookupValues?.Count > 0 ? DynamicReportSqlBuilder.GetLookupBindingName(field) : bindingName,
|
||||||
|
lookup,
|
||||||
|
staticLookupValues,
|
||||||
|
isMultiValue,
|
||||||
|
isImage,
|
||||||
|
isHtml,
|
||||||
|
editorType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DynamicReportColumn(field, bindingName, null, null, isMultiValue, isImage, isHtml, editorType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsMultiValueLookupEditor(string editorType)
|
||||||
|
{
|
||||||
|
return editorType is PlatformConsts.EditorTypes.dxTagBox or "dxGridBox";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -136,12 +136,6 @@ internal static class DynamicReportGridStateHelper
|
||||||
IStringLocalizer<PlatformResource> localizer = null,
|
IStringLocalizer<PlatformResource> localizer = null,
|
||||||
IReadOnlyDictionary<string, float> stateWidths = null)
|
IReadOnlyDictionary<string, float> stateWidths = null)
|
||||||
{
|
{
|
||||||
var caption = localizer == null
|
|
||||||
? (string.IsNullOrWhiteSpace(field.CaptionName) ? field.FieldName : field.CaptionName)
|
|
||||||
: DynamicReportImageHelper.GetLocalizedFieldCaption(field, localizer);
|
|
||||||
var name = field.FieldName ?? string.Empty;
|
|
||||||
var calculatedWidth = CalculateTextColumnWidth(field, caption, name);
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(field.FieldName) &&
|
if (!string.IsNullOrWhiteSpace(field.FieldName) &&
|
||||||
stateWidths?.TryGetValue(field.FieldName, out var stateWidth) == true &&
|
stateWidths?.TryGetValue(field.FieldName, out var stateWidth) == true &&
|
||||||
stateWidth > 0)
|
stateWidth > 0)
|
||||||
|
|
@ -154,7 +148,11 @@ internal static class DynamicReportGridStateHelper
|
||||||
return Math.Max(field.Width.Value, 40F);
|
return Math.Max(field.Width.Value, 40F);
|
||||||
}
|
}
|
||||||
|
|
||||||
return calculatedWidth;
|
var caption = localizer == null
|
||||||
|
? (string.IsNullOrWhiteSpace(field.CaptionName) ? field.FieldName : field.CaptionName)
|
||||||
|
: DynamicReportImageHelper.GetLocalizedFieldCaption(field, localizer);
|
||||||
|
|
||||||
|
return CalculateTextColumnWidth(field, caption, field.FieldName ?? string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetStateStorageKey(ListForm listForm)
|
private static string GetStateStorageKey(ListForm listForm)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
@ -51,6 +52,11 @@ internal static class DynamicReportImageHelper
|
||||||
Timeout = TimeSpan.FromSeconds(5)
|
Timeout = TimeSpan.FromSeconds(5)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Aynı görsel bir raporda çok sayıda satırda ve hem ImageUrl hem ImageSource ifadelerinde
|
||||||
|
// kullanılabildiği için indirilen içerik süreç boyunca önbelleklenir.
|
||||||
|
private const int MaxCachedRemoteImages = 256;
|
||||||
|
private static readonly ConcurrentDictionary<string, byte[]> RemoteImageCache = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
static DynamicReportImageHelper()
|
static DynamicReportImageHelper()
|
||||||
{
|
{
|
||||||
RegisterImageFunction(new ReportImageUrlFunction());
|
RegisterImageFunction(new ReportImageUrlFunction());
|
||||||
|
|
@ -209,9 +215,15 @@ internal static class DynamicReportImageHelper
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Statik lookup tanımlarını okur. Görüntü metinleri arayüzdeki gibi
|
||||||
|
/// (<c>translate('::' + name)</c>) yerelleştirme anahtarı olabildiği için
|
||||||
|
/// <paramref name="localizer"/> verildiğinde çevrilerek döner.
|
||||||
|
/// </summary>
|
||||||
public static Dictionary<string, string> GetStaticLookupValues(
|
public static Dictionary<string, string> GetStaticLookupValues(
|
||||||
LookupDto lookup,
|
LookupDto lookup,
|
||||||
DefaultValueHelper defaultValueHelper)
|
DefaultValueHelper defaultValueHelper,
|
||||||
|
IStringLocalizer<PlatformResource> localizer = null)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(lookup.LookupQuery))
|
if (string.IsNullOrWhiteSpace(lookup.LookupQuery))
|
||||||
{
|
{
|
||||||
|
|
@ -244,7 +256,8 @@ internal static class DynamicReportImageHelper
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
values[value] = GetJsonScalar(displayElement) ?? value;
|
var display = GetJsonScalar(displayElement) ?? value;
|
||||||
|
values[value] = localizer == null ? display : Localize(localizer, display, display);
|
||||||
}
|
}
|
||||||
|
|
||||||
return values.Count == 0 ? null : values;
|
return values.Count == 0 ? null : values;
|
||||||
|
|
@ -440,18 +453,6 @@ internal static class DynamicReportImageHelper
|
||||||
return editorType is PlatformConsts.EditorTypes.dxImageUpload or PlatformConsts.EditorTypes.dxImageViewer;
|
return editorType is PlatformConsts.EditorTypes.dxImageUpload or PlatformConsts.EditorTypes.dxImageViewer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool HasImageEditor(IEnumerable<ListFormField> fields, string fieldName)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(fieldName))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (fields ?? []).Any(x =>
|
|
||||||
string.Equals(x.FieldName, fieldName, StringComparison.OrdinalIgnoreCase) &&
|
|
||||||
IsImageEditor(x.EditorType2));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static float GetGridImageHeight() => DefaultGridImageHeight;
|
public static float GetGridImageHeight() => DefaultGridImageHeight;
|
||||||
|
|
||||||
public static float GetFormImageHeight() => DefaultFormImageHeight;
|
public static float GetFormImageHeight() => DefaultFormImageHeight;
|
||||||
|
|
@ -496,46 +497,6 @@ internal static class DynamicReportImageHelper
|
||||||
return safeImageCount * DefaultGridImageWidth + (safeImageCount - 1) * 4F + 8F;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool HasValue(object value)
|
public static bool HasValue(object value)
|
||||||
{
|
{
|
||||||
return value != null &&
|
return value != null &&
|
||||||
|
|
@ -1070,8 +1031,8 @@ internal static class DynamicReportImageHelper
|
||||||
{
|
{
|
||||||
JsonValueKind.String => value.GetString(),
|
JsonValueKind.String => value.GetString(),
|
||||||
JsonValueKind.Number => value.GetRawText(),
|
JsonValueKind.Number => value.GetRawText(),
|
||||||
JsonValueKind.True => "Evet",
|
JsonValueKind.True => FormatBoolean(true),
|
||||||
JsonValueKind.False => "Hayır",
|
JsonValueKind.False => FormatBoolean(false),
|
||||||
JsonValueKind.Null => string.Empty,
|
JsonValueKind.Null => string.Empty,
|
||||||
JsonValueKind.Array => string.Join(", ", value.EnumerateArray().Select(FormatJsonValue)),
|
JsonValueKind.Array => string.Join(", ", value.EnumerateArray().Select(FormatJsonValue)),
|
||||||
_ => value.ToString()
|
_ => value.ToString()
|
||||||
|
|
@ -1107,20 +1068,6 @@ internal static class DynamicReportImageHelper
|
||||||
dbType == DbType.Boolean;
|
dbType == DbType.Boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetCustomFormat(string format)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(format))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var customFormat = TryGetFormatOption(format, "displayFormat") ??
|
|
||||||
TryGetFormatOption(format, "format") ??
|
|
||||||
format.Trim();
|
|
||||||
|
|
||||||
return NormalizeDateTimeFormat(customFormat);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetEffectiveFormat(ListFormField field)
|
private static string GetEffectiveFormat(ListFormField field)
|
||||||
{
|
{
|
||||||
return TryGetFormatOption(field?.EditorOptions, "displayFormat") ??
|
return TryGetFormatOption(field?.EditorOptions, "displayFormat") ??
|
||||||
|
|
@ -1355,11 +1302,6 @@ internal static class DynamicReportImageHelper
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool HasImageValue(object value)
|
|
||||||
{
|
|
||||||
return HasValue(value) && NormalizeImageValue(value).Any(IsPotentialImageSource);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsPotentialImageSource(string value)
|
private static bool IsPotentialImageSource(string value)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(value) ||
|
if (string.IsNullOrWhiteSpace(value) ||
|
||||||
|
|
@ -1436,28 +1378,6 @@ internal static class DynamicReportImageHelper
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
private static string GetImageSourceAt(object rawValue, string baseUrl, int index)
|
||||||
{
|
{
|
||||||
if (index < 0)
|
if (index < 0)
|
||||||
|
|
@ -1600,25 +1520,44 @@ internal static class DynamicReportImageHelper
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bytes = GetRemoteImageBytes(uri);
|
||||||
|
return bytes?.Length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] GetRemoteImageBytes(Uri uri)
|
||||||
|
{
|
||||||
|
var cacheKey = uri.AbsoluteUri;
|
||||||
|
if (RemoteImageCache.TryGetValue(cacheKey, out var cached))
|
||||||
|
{
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = DownloadImageBytes(uri);
|
||||||
|
if (RemoteImageCache.Count < MaxCachedRemoteImages)
|
||||||
|
{
|
||||||
|
RemoteImageCache.TryAdd(cacheKey, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] DownloadImageBytes(Uri uri)
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
bytes = HttpClient.GetByteArrayAsync(uri).GetAwaiter().GetResult();
|
return HttpClient.GetByteArrayAsync(uri).GetAwaiter().GetResult();
|
||||||
return bytes?.Length > 0;
|
|
||||||
}
|
}
|
||||||
catch (HttpRequestException)
|
catch (HttpRequestException)
|
||||||
{
|
{
|
||||||
bytes = null;
|
return null;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
catch (TaskCanceledException)
|
catch (TaskCanceledException)
|
||||||
{
|
{
|
||||||
bytes = null;
|
return null;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
catch (InvalidOperationException)
|
catch (InvalidOperationException)
|
||||||
{
|
{
|
||||||
bytes = null;
|
return null;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,277 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using DevExpress.Drawing;
|
||||||
|
using DevExpress.XtraPrinting;
|
||||||
|
using DevExpress.XtraReports.UI;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
|
using Sozsoft.Platform.Localization;
|
||||||
|
|
||||||
|
namespace Sozsoft.Reports.PredefinedReports;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Grid ve Tree raporlarının ortak tablo/hücre/özet üretimi.
|
||||||
|
/// </summary>
|
||||||
|
internal static class DynamicReportLayoutHelper
|
||||||
|
{
|
||||||
|
public const float HeaderRowHeight = 28F;
|
||||||
|
public const float FooterRowHeight = 24F;
|
||||||
|
public const float DefaultDetailHeight = 24F;
|
||||||
|
public const float HtmlDetailHeight = 48F;
|
||||||
|
|
||||||
|
public static readonly System.Drawing.Color TableBorderColor = System.Drawing.Color.FromArgb(226, 232, 240);
|
||||||
|
private static readonly System.Drawing.Color HeaderBackColor = System.Drawing.Color.FromArgb(238, 242, 247);
|
||||||
|
|
||||||
|
#pragma warning disable CA1416
|
||||||
|
public static System.Drawing.Printing.Margins CreateMargins(int horizontal, int vertical)
|
||||||
|
{
|
||||||
|
return new System.Drawing.Printing.Margins(horizontal, horizontal, vertical, vertical);
|
||||||
|
}
|
||||||
|
#pragma warning restore CA1416
|
||||||
|
|
||||||
|
public static float GetDetailHeight(IEnumerable<DynamicReportColumn> columns)
|
||||||
|
{
|
||||||
|
var materialized = columns as IReadOnlyCollection<DynamicReportColumn> ?? columns.ToList();
|
||||||
|
if (materialized.Any(x => x.IsImage))
|
||||||
|
{
|
||||||
|
return DynamicReportImageHelper.GetGridImageHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
return materialized.Any(x => x.IsHtml) ? HtmlDetailHeight : DefaultDetailHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static XRTable CreateTable(float width, float height, float left = 0)
|
||||||
|
{
|
||||||
|
return new XRTable
|
||||||
|
{
|
||||||
|
BoundsF = new System.Drawing.RectangleF(left, 0, width, height),
|
||||||
|
Borders = BorderSide.All,
|
||||||
|
BorderColor = TableBorderColor,
|
||||||
|
BorderWidth = 0.25F
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static XRTableCell CreateHeaderCell(
|
||||||
|
DynamicReportColumn column,
|
||||||
|
float width,
|
||||||
|
IStringLocalizer<PlatformResource> localizer)
|
||||||
|
{
|
||||||
|
return new XRTableCell
|
||||||
|
{
|
||||||
|
Text = DynamicReportImageHelper.GetLocalizedFieldCaption(column.Field, localizer),
|
||||||
|
WidthF = width,
|
||||||
|
Weight = width,
|
||||||
|
Font = DynamicReportFontHelper.CreateHeaderFont(),
|
||||||
|
BackColor = HeaderBackColor,
|
||||||
|
Padding = new PaddingInfo(4, 4, 2, 2),
|
||||||
|
TextAlignment = TextAlignment.MiddleCenter,
|
||||||
|
Multiline = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Detay hücresi üretir. <paramref name="prefixColumnName"/> verildiğinde metnin başına
|
||||||
|
/// ilgili kolonun değeri eklenir (Tree raporundaki girinti için kullanılır).
|
||||||
|
/// </summary>
|
||||||
|
public static XRTableCell CreateDetailCell(
|
||||||
|
DynamicReportColumn column,
|
||||||
|
float width,
|
||||||
|
string imageBaseUrl,
|
||||||
|
float height,
|
||||||
|
string prefixColumnName = null,
|
||||||
|
string boldColumnName = null)
|
||||||
|
{
|
||||||
|
if (column.IsImage)
|
||||||
|
{
|
||||||
|
return DynamicReportImageHelper.CreateGridImageCell(
|
||||||
|
column.BindingName,
|
||||||
|
column.Field.FieldName,
|
||||||
|
width,
|
||||||
|
imageBaseUrl,
|
||||||
|
column.ImageCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (column.IsHtml)
|
||||||
|
{
|
||||||
|
return CreateHtmlDetailCell(column, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasPrefix = !string.IsNullOrWhiteSpace(prefixColumnName);
|
||||||
|
var cell = new XRTableCell
|
||||||
|
{
|
||||||
|
WidthF = width,
|
||||||
|
Weight = width,
|
||||||
|
Font = DynamicReportFontHelper.CreateDataFont(),
|
||||||
|
Padding = new PaddingInfo(4, 4, 2, 2),
|
||||||
|
TextAlignment = hasPrefix
|
||||||
|
? TextAlignment.MiddleLeft
|
||||||
|
: DynamicReportImageHelper.ToTextAlignment(column.Field.Alignment, column.Field.SourceDbType),
|
||||||
|
Multiline = true,
|
||||||
|
CanGrow = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var valueExpression = DynamicReportImageHelper.CreateFormatValueExpression(
|
||||||
|
$"[{column.BindingName}]",
|
||||||
|
column.Field,
|
||||||
|
column.EditorType);
|
||||||
|
var textExpression = hasPrefix
|
||||||
|
? $"Concat([{prefixColumnName}], {valueExpression})"
|
||||||
|
: valueExpression;
|
||||||
|
|
||||||
|
cell.ExpressionBindings.Add(new ExpressionBinding("BeforePrint", "Text", textExpression));
|
||||||
|
cell.ExpressionBindings.Add(new ExpressionBinding(
|
||||||
|
"BeforePrint",
|
||||||
|
"Font.Name",
|
||||||
|
DynamicReportImageHelper.CreateFontFamilyExpression(textExpression)));
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(boldColumnName))
|
||||||
|
{
|
||||||
|
cell.ExpressionBindings.Add(new ExpressionBinding("BeforePrint", "Font.Bold", $"[{boldColumnName}]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static XRTableCell CreateHtmlDetailCell(DynamicReportColumn column, float width, float height)
|
||||||
|
{
|
||||||
|
var cell = new XRTableCell
|
||||||
|
{
|
||||||
|
WidthF = width,
|
||||||
|
Weight = width,
|
||||||
|
Padding = new PaddingInfo(0, 0, 0, 0),
|
||||||
|
CanGrow = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var richText = new XRRichText
|
||||||
|
{
|
||||||
|
BoundsF = new System.Drawing.RectangleF(0, 0, width, height),
|
||||||
|
Font = DynamicReportFontHelper.CreateDataFont(),
|
||||||
|
Padding = new PaddingInfo(4, 4, 2, 2),
|
||||||
|
CanGrow = true
|
||||||
|
};
|
||||||
|
richText.ExpressionBindings.Add(new ExpressionBinding("BeforePrint", "Html", $"[{column.BindingName}]"));
|
||||||
|
cell.Controls.Add(richText);
|
||||||
|
|
||||||
|
return cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static XRTableCell CreateFooterCell(DynamicReportColumn column, float width)
|
||||||
|
{
|
||||||
|
var summaryType = GetSummaryType(column.Field.TotalSummaryJson);
|
||||||
|
var cell = new XRTableCell
|
||||||
|
{
|
||||||
|
WidthF = width,
|
||||||
|
Weight = width,
|
||||||
|
Font = DynamicReportFontHelper.CreateDataFont(DXFontStyle.Bold),
|
||||||
|
Padding = new PaddingInfo(4, 4, 2, 2),
|
||||||
|
TextAlignment = DynamicReportImageHelper.ToTextAlignment(column.Field.Alignment, column.Field.SourceDbType)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (summaryType == null)
|
||||||
|
{
|
||||||
|
return cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
cell.ExpressionBindings.Add(new ExpressionBinding(
|
||||||
|
"BeforePrint",
|
||||||
|
"Text",
|
||||||
|
DynamicReportImageHelper.CreateFormatValueExpression(
|
||||||
|
CreateSummaryExpression(summaryType.Value, column.BindingName),
|
||||||
|
column.Field,
|
||||||
|
column.EditorType)));
|
||||||
|
cell.Summary = new XRSummary
|
||||||
|
{
|
||||||
|
Running = SummaryRunning.Report,
|
||||||
|
Func = summaryType.Value,
|
||||||
|
IgnoreNullValues = true
|
||||||
|
};
|
||||||
|
|
||||||
|
return cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool HasAnySummary(IEnumerable<DynamicReportColumn> columns)
|
||||||
|
{
|
||||||
|
return columns.Any(x => GetSummaryType(x.Field.TotalSummaryJson) != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AddPageInfoToFooter(XRControlCollection controls, float pageWidth)
|
||||||
|
{
|
||||||
|
controls.Add(new XRPageInfo
|
||||||
|
{
|
||||||
|
TextFormatString = "Sayfa {0} / {1}",
|
||||||
|
BoundsF = new System.Drawing.RectangleF(pageWidth / 2, 8, pageWidth / 2, 18),
|
||||||
|
Font = DynamicReportFontHelper.CreateValueFont(8F),
|
||||||
|
TextAlignment = TextAlignment.MiddleRight
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string CreateSummaryExpression(SummaryFunc summaryType, string bindingName)
|
||||||
|
{
|
||||||
|
return summaryType switch
|
||||||
|
{
|
||||||
|
SummaryFunc.Avg => $"sumAvg([{bindingName}])",
|
||||||
|
SummaryFunc.Min => $"sumMin([{bindingName}])",
|
||||||
|
SummaryFunc.Max => $"sumMax([{bindingName}])",
|
||||||
|
SummaryFunc.Count => "sumCount()",
|
||||||
|
_ => $"sumSum([{bindingName}])"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ListForm alanındaki toplam özeti tanımını okur. JSON tek bir nesne veya dizi olabilir.
|
||||||
|
/// </summary>
|
||||||
|
public static SummaryFunc? GetSummaryType(string totalSummaryJson)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(totalSummaryJson))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var document = JsonDocument.Parse(totalSummaryJson);
|
||||||
|
return GetSummaryType(document.RootElement);
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SummaryFunc? GetSummaryType(JsonElement element)
|
||||||
|
{
|
||||||
|
if (element.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
return element.EnumerateArray()
|
||||||
|
.Select(GetSummaryType)
|
||||||
|
.FirstOrDefault(x => x != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.ValueKind != JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var property in element.EnumerateObject())
|
||||||
|
{
|
||||||
|
if (!string.Equals(property.Name, "summaryType", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
property.Value.ValueKind != JsonValueKind.String)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return property.Value.GetString()?.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"sum" => SummaryFunc.Sum,
|
||||||
|
"avg" => SummaryFunc.Avg,
|
||||||
|
"min" => SummaryFunc.Min,
|
||||||
|
"max" => SummaryFunc.Max,
|
||||||
|
"count" => SummaryFunc.Count,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,9 +13,6 @@ internal static class DynamicReportSqlBuilder
|
||||||
{
|
{
|
||||||
private const string SourceAlias = "ReportSource";
|
private const string SourceAlias = "ReportSource";
|
||||||
private const string LookupAlias = "LookupSource";
|
private const string LookupAlias = "LookupSource";
|
||||||
private static readonly Regex OrderByRegex = new(
|
|
||||||
@"(?<![A-Za-z0-9_])ORDER\s+BY\b[\s\S]*$",
|
|
||||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
|
||||||
private static readonly Regex LookupParameterRegex = new(
|
private static readonly Regex LookupParameterRegex = new(
|
||||||
@"@\s*param\d+",
|
@"@\s*param\d+",
|
||||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||||
|
|
@ -66,11 +63,17 @@ internal static class DynamicReportSqlBuilder
|
||||||
var orderBy = ExtractOrderBy(normalizedSql);
|
var orderBy = ExtractOrderBy(normalizedSql);
|
||||||
if (!string.IsNullOrWhiteSpace(orderBy))
|
if (!string.IsNullOrWhiteSpace(orderBy))
|
||||||
{
|
{
|
||||||
normalizedSql = OrderByRegex.Replace(normalizedSql, string.Empty).Trim();
|
normalizedSql = RemoveOrderBy(normalizedSql);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A derived table cannot contain its own WITH clause, so the source CTEs are
|
||||||
|
// promoted to the outer query together with the lookup CTEs.
|
||||||
|
var (sourceCommonTableExpression, sourceSelectQuery) = SplitCommonTableExpression(normalizedSql);
|
||||||
|
normalizedSql = sourceSelectQuery;
|
||||||
|
|
||||||
var commonTableExpressions = lookupSelectParts
|
var commonTableExpressions = lookupSelectParts
|
||||||
.Select(part => part.CommonTableExpression)
|
.Select(part => part.CommonTableExpression)
|
||||||
|
.Prepend(sourceCommonTableExpression)
|
||||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
@ -292,8 +295,7 @@ internal static class DynamicReportSqlBuilder
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var query = lookupQuery.Trim().TrimEnd(';');
|
var query = RemoveOrderBy(NormalizeSql(lookupQuery));
|
||||||
query = OrderByRegex.Replace(query, string.Empty);
|
|
||||||
|
|
||||||
return LookupParameterRegex.Replace(query, "NULL");
|
return LookupParameterRegex.Replace(query, "NULL");
|
||||||
}
|
}
|
||||||
|
|
@ -305,8 +307,83 @@ internal static class DynamicReportSqlBuilder
|
||||||
|
|
||||||
private static string ExtractOrderBy(string sql)
|
private static string ExtractOrderBy(string sql)
|
||||||
{
|
{
|
||||||
var match = OrderByRegex.Match(sql);
|
var index = FindTrailingOrderByIndex(sql);
|
||||||
return match.Success ? " " + match.Value.Trim() : string.Empty;
|
return index < 0 ? string.Empty : " " + sql[index..].Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RemoveOrderBy(string sql)
|
||||||
|
{
|
||||||
|
var index = FindTrailingOrderByIndex(sql);
|
||||||
|
return index < 0 ? sql : sql[..index].TrimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sorgunun sonundaki ORDER BY ifadesini bulur. Alt sorgular ve OVER (ORDER BY ...)
|
||||||
|
/// gibi parantez içindeki sıralamalar dikkate alınmaz.
|
||||||
|
/// </summary>
|
||||||
|
private static int FindTrailingOrderByIndex(string sql)
|
||||||
|
{
|
||||||
|
var depth = 0;
|
||||||
|
var quote = '\0';
|
||||||
|
var result = -1;
|
||||||
|
|
||||||
|
for (var index = 0; index < sql.Length; index++)
|
||||||
|
{
|
||||||
|
var current = sql[index];
|
||||||
|
if (quote != '\0')
|
||||||
|
{
|
||||||
|
if (current == quote)
|
||||||
|
{
|
||||||
|
if (index + 1 < sql.Length && sql[index + 1] == quote)
|
||||||
|
{
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
quote = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current is '\'' or '"')
|
||||||
|
{
|
||||||
|
quote = current;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current == '(')
|
||||||
|
{
|
||||||
|
depth++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current == ')')
|
||||||
|
{
|
||||||
|
depth--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depth != 0 || !IsKeywordAt(sql, index, "ORDER"))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var next = index + "ORDER".Length;
|
||||||
|
while (next < sql.Length && char.IsWhiteSpace(sql[next]))
|
||||||
|
{
|
||||||
|
next++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsKeywordAt(sql, next, "BY"))
|
||||||
|
{
|
||||||
|
result = index;
|
||||||
|
index = next + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string QuoteSqlIdentifier(string identifier)
|
private static string QuoteSqlIdentifier(string identifier)
|
||||||
|
|
|
||||||
|
|
@ -29,11 +29,11 @@ public class DynamicTreeReport : XtraReport
|
||||||
private const int A4PortraitWidth = 827;
|
private const int A4PortraitWidth = 827;
|
||||||
private const int HorizontalMargin = 50;
|
private const int HorizontalMargin = 50;
|
||||||
private const int LandscapeColumnThreshold = 5;
|
private const int LandscapeColumnThreshold = 5;
|
||||||
|
private const int VerticalMargin = 35;
|
||||||
private const string TreeLevelColumn = "__TreeLevel";
|
private const string TreeLevelColumn = "__TreeLevel";
|
||||||
private const string TreeHasChildrenColumn = "__TreeHasChildren";
|
private const string TreeHasChildrenColumn = "__TreeHasChildren";
|
||||||
private const string TreePrefixColumn = "__TreePrefix";
|
private const string TreePrefixColumn = "__TreePrefix";
|
||||||
private const string TreeDataSourceName = "DynamicTree";
|
private const string TreeDataSourceName = "DynamicTree";
|
||||||
private static readonly System.Drawing.Color TableBorderColor = System.Drawing.Color.FromArgb(226, 232, 240);
|
|
||||||
private static IServiceScopeFactory _scopeFactory;
|
private static IServiceScopeFactory _scopeFactory;
|
||||||
|
|
||||||
public static void SetScopeFactory(IServiceScopeFactory scopeFactory)
|
public static void SetScopeFactory(IServiceScopeFactory scopeFactory)
|
||||||
|
|
@ -139,17 +139,15 @@ public class DynamicTreeReport : XtraReport
|
||||||
var reportColumns = visibleFields
|
var reportColumns = visibleFields
|
||||||
.Select(field =>
|
.Select(field =>
|
||||||
{
|
{
|
||||||
var selectField = selectFieldsByName[field.FieldName];
|
|
||||||
editorTypesByField.TryGetValue(field.FieldName, out var editorType);
|
editorTypesByField.TryGetValue(field.FieldName, out var editorType);
|
||||||
return CreateReportColumn(
|
return DynamicReportColumnFactory.Create(
|
||||||
field,
|
field,
|
||||||
selectField,
|
selectFieldsByName[field.FieldName],
|
||||||
defaultValueHelper,
|
defaultValueHelper,
|
||||||
editorType,
|
editorType,
|
||||||
DynamicReportImageHelper.IsImageEditor(editorType),
|
localizer);
|
||||||
editorType == PlatformConsts.EditorTypes.dxHtmlEditor);
|
|
||||||
})
|
})
|
||||||
.Where(x => !string.IsNullOrWhiteSpace(x.BindingName) && x.BindingName != "undefined")
|
.Where(x => x.HasBindingName)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var selectQuery = DynamicReportImageHelper.StripReportPaging(DynamicReportImageHelper.ApplyQueryParameters(
|
var selectQuery = DynamicReportImageHelper.StripReportPaging(DynamicReportImageHelper.ApplyQueryParameters(
|
||||||
|
|
@ -178,32 +176,21 @@ public class DynamicTreeReport : XtraReport
|
||||||
RequestParameters = false;
|
RequestParameters = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma warning disable CA1416
|
|
||||||
private void ConfigurePage(
|
private void ConfigurePage(
|
||||||
IReadOnlyCollection<ReportColumn> visibleFields,
|
IReadOnlyCollection<DynamicReportColumn> visibleFields,
|
||||||
IStringLocalizer<PlatformResource> localizer,
|
IStringLocalizer<PlatformResource> localizer,
|
||||||
IReadOnlyDictionary<string, float> stateWidths)
|
IReadOnlyDictionary<string, float> stateWidths)
|
||||||
{
|
{
|
||||||
Margins = CreateReportMargins(HorizontalMargin, HorizontalMargin, 35, 35);
|
Margins = DynamicReportLayoutHelper.CreateMargins(HorizontalMargin, VerticalMargin);
|
||||||
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 => x.IsImage
|
var requestedContentWidth = visibleFields.Sum(x => x.IsImage
|
||||||
? GetImageColumnWidth(x)
|
? x.ImageWidth
|
||||||
: DynamicReportGridStateHelper.GetPreferredColumnWidth(x.Field, localizer, stateWidths));
|
: DynamicReportGridStateHelper.GetPreferredColumnWidth(x.Field, localizer, stateWidths));
|
||||||
Landscape = visibleFields.Count >= LandscapeColumnThreshold || requestedContentWidth > portraitContentWidth;
|
Landscape = visibleFields.Count >= LandscapeColumnThreshold || requestedContentWidth > portraitContentWidth;
|
||||||
Font = DynamicReportFontHelper.CreateDataFont();
|
Font = DynamicReportFontHelper.CreateDataFont();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static System.Drawing.Printing.Margins CreateReportMargins(
|
|
||||||
int left,
|
|
||||||
int right,
|
|
||||||
int top,
|
|
||||||
int bottom)
|
|
||||||
{
|
|
||||||
return new System.Drawing.Printing.Margins(left, right, top, bottom);
|
|
||||||
}
|
|
||||||
#pragma warning restore CA1416
|
|
||||||
|
|
||||||
private static async Task<List<Dictionary<string, object>>> ReadRowsAsync(
|
private static async Task<List<Dictionary<string, object>>> ReadRowsAsync(
|
||||||
IDynamicDataRepository dynamicDataRepository,
|
IDynamicDataRepository dynamicDataRepository,
|
||||||
string selectQuery,
|
string selectQuery,
|
||||||
|
|
@ -312,7 +299,7 @@ public class DynamicTreeReport : XtraReport
|
||||||
|
|
||||||
private void ConfigureDataSource(
|
private void ConfigureDataSource(
|
||||||
IEnumerable<Dictionary<string, object>> rows,
|
IEnumerable<Dictionary<string, object>> rows,
|
||||||
IReadOnlyList<ReportColumn> columns)
|
IReadOnlyList<DynamicReportColumn> columns)
|
||||||
{
|
{
|
||||||
var normalizedRows = rows
|
var normalizedRows = rows
|
||||||
.Select(row => NormalizeJsonRow(row, columns))
|
.Select(row => NormalizeJsonRow(row, columns))
|
||||||
|
|
@ -334,7 +321,7 @@ public class DynamicTreeReport : XtraReport
|
||||||
|
|
||||||
private static Dictionary<string, object> NormalizeJsonRow(
|
private static Dictionary<string, object> NormalizeJsonRow(
|
||||||
IDictionary<string, object> row,
|
IDictionary<string, object> row,
|
||||||
IReadOnlyList<ReportColumn> columns)
|
IReadOnlyList<DynamicReportColumn> columns)
|
||||||
{
|
{
|
||||||
var normalized = row.ToDictionary(
|
var normalized = row.ToDictionary(
|
||||||
x => x.Key,
|
x => x.Key,
|
||||||
|
|
@ -382,8 +369,8 @@ public class DynamicTreeReport : XtraReport
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<ReportColumn> ApplyImageCounts(
|
private static List<DynamicReportColumn> ApplyImageCounts(
|
||||||
IReadOnlyList<ReportColumn> columns,
|
IReadOnlyList<DynamicReportColumn> columns,
|
||||||
IReadOnlyList<Dictionary<string, object>> rows,
|
IReadOnlyList<Dictionary<string, object>> rows,
|
||||||
string imageBaseUrl)
|
string imageBaseUrl)
|
||||||
{
|
{
|
||||||
|
|
@ -413,7 +400,7 @@ public class DynamicTreeReport : XtraReport
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static object GetImageRawValue(IDictionary<string, object> row, ReportColumn column)
|
private static object GetImageRawValue(IDictionary<string, object> row, DynamicReportColumn column)
|
||||||
{
|
{
|
||||||
if (DynamicReportImageHelper.TryGetRowValue(row, column.BindingName, out var value) &&
|
if (DynamicReportImageHelper.TryGetRowValue(row, column.BindingName, out var value) &&
|
||||||
DynamicReportImageHelper.HasValue(value))
|
DynamicReportImageHelper.HasValue(value))
|
||||||
|
|
@ -426,7 +413,7 @@ public class DynamicTreeReport : XtraReport
|
||||||
|
|
||||||
private void BuildLayout(
|
private void BuildLayout(
|
||||||
ListForm listForm,
|
ListForm listForm,
|
||||||
IReadOnlyList<ReportColumn> fields,
|
IReadOnlyList<DynamicReportColumn> fields,
|
||||||
IStringLocalizer<PlatformResource> localizer,
|
IStringLocalizer<PlatformResource> localizer,
|
||||||
string imageBaseUrl,
|
string imageBaseUrl,
|
||||||
string currentUserName,
|
string currentUserName,
|
||||||
|
|
@ -439,14 +426,11 @@ public class DynamicTreeReport : XtraReport
|
||||||
var topMargin = new TopMarginBand { HeightF = Margins.Top };
|
var topMargin = new TopMarginBand { HeightF = Margins.Top };
|
||||||
var bottomMargin = new BottomMarginBand { HeightF = 32 };
|
var bottomMargin = new BottomMarginBand { HeightF = 32 };
|
||||||
var reportHeader = new ReportHeaderBand { HeightF = string.IsNullOrWhiteSpace(criteriaText) ? 32 : 50 };
|
var reportHeader = new ReportHeaderBand { HeightF = string.IsNullOrWhiteSpace(criteriaText) ? 32 : 50 };
|
||||||
var pageHeader = new PageHeaderBand { HeightF = 28 };
|
var pageHeader = new PageHeaderBand { HeightF = DynamicReportLayoutHelper.HeaderRowHeight };
|
||||||
var detailHeight = fields.Any(x => x.IsImage)
|
var detailHeight = DynamicReportLayoutHelper.GetDetailHeight(fields);
|
||||||
? DynamicReportImageHelper.GetGridImageHeight()
|
|
||||||
: fields.Any(x => x.IsHtml)
|
|
||||||
? 48
|
|
||||||
: 24;
|
|
||||||
var detail = new DetailBand { HeightF = detailHeight };
|
var detail = new DetailBand { HeightF = detailHeight };
|
||||||
var reportFooter = new ReportFooterBand { HeightF = HasAnySummary(fields) ? 28 : 0 };
|
var hasSummary = DynamicReportLayoutHelper.HasAnySummary(fields);
|
||||||
|
var reportFooter = new ReportFooterBand { HeightF = hasSummary ? 28 : 0 };
|
||||||
|
|
||||||
Bands.AddRange([topMargin, reportHeader, pageHeader, detail, reportFooter, bottomMargin]);
|
Bands.AddRange([topMargin, reportHeader, pageHeader, detail, reportFooter, bottomMargin]);
|
||||||
|
|
||||||
|
|
@ -467,29 +451,30 @@ public class DynamicTreeReport : XtraReport
|
||||||
DynamicReportImageHelper.AddFooterTopLine(bottomMargin.Controls, pageWidth);
|
DynamicReportImageHelper.AddFooterTopLine(bottomMargin.Controls, pageWidth);
|
||||||
DynamicReportImageHelper.AddCurrentUserNameToFooter(bottomMargin.Controls, currentUserName, pageWidth);
|
DynamicReportImageHelper.AddCurrentUserNameToFooter(bottomMargin.Controls, currentUserName, pageWidth);
|
||||||
|
|
||||||
bottomMargin.Controls.Add(new XRPageInfo
|
DynamicReportLayoutHelper.AddPageInfoToFooter(bottomMargin.Controls, pageWidth);
|
||||||
{
|
|
||||||
TextFormatString = "Sayfa {0} / {1}",
|
|
||||||
BoundsF = new System.Drawing.RectangleF(pageWidth / 2, 8, pageWidth / 2, 18),
|
|
||||||
Font = DynamicReportFontHelper.CreateValueFont(8F),
|
|
||||||
TextAlignment = TextAlignment.MiddleRight
|
|
||||||
});
|
|
||||||
|
|
||||||
var widths = DynamicReportGridStateHelper.CalculateWidths(fields, pageWidth, localizer, stateWidths);
|
var widths = DynamicReportGridStateHelper.CalculateWidths(fields, pageWidth, localizer, stateWidths);
|
||||||
var headerTable = CreateTable(pageWidth, 28);
|
var headerTable = DynamicReportLayoutHelper.CreateTable(pageWidth, DynamicReportLayoutHelper.HeaderRowHeight);
|
||||||
var headerRow = new XRTableRow { HeightF = 28 };
|
var headerRow = new XRTableRow { HeightF = DynamicReportLayoutHelper.HeaderRowHeight };
|
||||||
var detailTable = CreateTable(pageWidth, detailHeight);
|
var detailTable = DynamicReportLayoutHelper.CreateTable(pageWidth, detailHeight);
|
||||||
detailTable.CanGrow = true;
|
detailTable.CanGrow = true;
|
||||||
var detailRow = new XRTableRow { HeightF = detailHeight, CanGrow = true };
|
var detailRow = new XRTableRow { HeightF = detailHeight, CanGrow = true };
|
||||||
var footerTable = CreateTable(pageWidth, 24);
|
var footerTable = DynamicReportLayoutHelper.CreateTable(pageWidth, DynamicReportLayoutHelper.FooterRowHeight);
|
||||||
var footerRow = new XRTableRow { HeightF = 24 };
|
var footerRow = new XRTableRow { HeightF = DynamicReportLayoutHelper.FooterRowHeight };
|
||||||
|
|
||||||
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));
|
var isTreeColumn = i == 0;
|
||||||
detailRow.Cells.Add(CreateDetailCell(field, widths[i], imageBaseUrl, detailHeight, i == 0));
|
headerRow.Cells.Add(DynamicReportLayoutHelper.CreateHeaderCell(field, widths[i], localizer));
|
||||||
footerRow.Cells.Add(CreateFooterCell(field, widths[i]));
|
detailRow.Cells.Add(DynamicReportLayoutHelper.CreateDetailCell(
|
||||||
|
field,
|
||||||
|
widths[i],
|
||||||
|
imageBaseUrl,
|
||||||
|
detailHeight,
|
||||||
|
isTreeColumn ? TreePrefixColumn : null,
|
||||||
|
isTreeColumn ? TreeHasChildrenColumn : null));
|
||||||
|
footerRow.Cells.Add(DynamicReportLayoutHelper.CreateFooterCell(field, widths[i]));
|
||||||
}
|
}
|
||||||
|
|
||||||
headerTable.Rows.Add(headerRow);
|
headerTable.Rows.Add(headerRow);
|
||||||
|
|
@ -498,211 +483,12 @@ public class DynamicTreeReport : XtraReport
|
||||||
|
|
||||||
pageHeader.Controls.Add(headerTable);
|
pageHeader.Controls.Add(headerTable);
|
||||||
detail.Controls.Add(detailTable);
|
detail.Controls.Add(detailTable);
|
||||||
if (HasAnySummary(fields))
|
if (hasSummary)
|
||||||
{
|
{
|
||||||
reportFooter.Controls.Add(footerTable);
|
reportFooter.Controls.Add(footerTable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static XRTable CreateTable(float width, float height)
|
|
||||||
{
|
|
||||||
return new XRTable
|
|
||||||
{
|
|
||||||
BoundsF = new System.Drawing.RectangleF(0, 0, width, height),
|
|
||||||
Borders = BorderSide.All,
|
|
||||||
BorderColor = TableBorderColor,
|
|
||||||
BorderWidth = 0.25F
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static XRTableCell CreateHeaderCell(
|
|
||||||
ReportColumn column,
|
|
||||||
float width,
|
|
||||||
IStringLocalizer<PlatformResource> localizer)
|
|
||||||
{
|
|
||||||
return new XRTableCell
|
|
||||||
{
|
|
||||||
Text = DynamicReportImageHelper.GetLocalizedFieldCaption(column.Field, localizer),
|
|
||||||
WidthF = width,
|
|
||||||
Weight = width,
|
|
||||||
Font = DynamicReportFontHelper.CreateHeaderFont(),
|
|
||||||
BackColor = System.Drawing.Color.FromArgb(238, 242, 247),
|
|
||||||
Padding = new PaddingInfo(4, 4, 2, 2),
|
|
||||||
TextAlignment = TextAlignment.MiddleCenter,
|
|
||||||
Multiline = true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static XRTableCell CreateDetailCell(
|
|
||||||
ReportColumn column,
|
|
||||||
float width,
|
|
||||||
string imageBaseUrl,
|
|
||||||
float height,
|
|
||||||
bool isTreeColumn)
|
|
||||||
{
|
|
||||||
if (column.IsImage)
|
|
||||||
{
|
|
||||||
return DynamicReportImageHelper.CreateGridImageCell(
|
|
||||||
column.BindingName,
|
|
||||||
column.Field.FieldName,
|
|
||||||
width,
|
|
||||||
imageBaseUrl,
|
|
||||||
column.ImageCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.IsHtml)
|
|
||||||
{
|
|
||||||
return CreateHtmlDetailCell(column, width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
var cell = new XRTableCell
|
|
||||||
{
|
|
||||||
WidthF = width,
|
|
||||||
Weight = width,
|
|
||||||
Font = DynamicReportFontHelper.CreateDataFont(),
|
|
||||||
Padding = new PaddingInfo(4, 4, 2, 2),
|
|
||||||
TextAlignment = isTreeColumn
|
|
||||||
? TextAlignment.MiddleLeft
|
|
||||||
: DynamicReportImageHelper.ToTextAlignment(column.Field.Alignment, column.Field.SourceDbType),
|
|
||||||
Multiline = true,
|
|
||||||
CanGrow = true
|
|
||||||
};
|
|
||||||
|
|
||||||
var valueExpression = DynamicReportImageHelper.CreateFormatValueExpression($"[{column.BindingName}]", column.Field, column.EditorType);
|
|
||||||
var textExpression = isTreeColumn
|
|
||||||
? $"Concat([{TreePrefixColumn}], {valueExpression})"
|
|
||||||
: valueExpression;
|
|
||||||
cell.ExpressionBindings.Add(new ExpressionBinding(
|
|
||||||
"BeforePrint",
|
|
||||||
"Text",
|
|
||||||
textExpression));
|
|
||||||
cell.ExpressionBindings.Add(new ExpressionBinding(
|
|
||||||
"BeforePrint",
|
|
||||||
"Font.Name",
|
|
||||||
DynamicReportImageHelper.CreateFontFamilyExpression(textExpression)));
|
|
||||||
|
|
||||||
if (isTreeColumn)
|
|
||||||
{
|
|
||||||
cell.ExpressionBindings.Add(new ExpressionBinding(
|
|
||||||
"BeforePrint",
|
|
||||||
"Font.Bold",
|
|
||||||
$"[{TreeHasChildrenColumn}]"));
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = true
|
|
||||||
};
|
|
||||||
|
|
||||||
var richText = new XRRichText
|
|
||||||
{
|
|
||||||
BoundsF = new System.Drawing.RectangleF(0, 0, width, height),
|
|
||||||
Font = DynamicReportFontHelper.CreateDataFont(),
|
|
||||||
Padding = new PaddingInfo(4, 4, 2, 2),
|
|
||||||
CanGrow = true
|
|
||||||
};
|
|
||||||
richText.ExpressionBindings.Add(new ExpressionBinding("BeforePrint", "Html", $"[{column.BindingName}]"));
|
|
||||||
cell.Controls.Add(richText);
|
|
||||||
|
|
||||||
return cell;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static XRTableCell CreateFooterCell(ReportColumn column, float width)
|
|
||||||
{
|
|
||||||
var summaryType = GetSummaryType(column.Field.TotalSummaryJson);
|
|
||||||
var cell = new XRTableCell
|
|
||||||
{
|
|
||||||
WidthF = width,
|
|
||||||
Weight = width,
|
|
||||||
Font = DynamicReportFontHelper.CreateDataFont(DXFontStyle.Bold),
|
|
||||||
Padding = new PaddingInfo(4, 4, 2, 2),
|
|
||||||
TextAlignment = DynamicReportImageHelper.ToTextAlignment(column.Field.Alignment, column.Field.SourceDbType)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (summaryType == null)
|
|
||||||
{
|
|
||||||
return cell;
|
|
||||||
}
|
|
||||||
|
|
||||||
cell.ExpressionBindings.Add(new ExpressionBinding(
|
|
||||||
"BeforePrint",
|
|
||||||
"Text",
|
|
||||||
DynamicReportImageHelper.CreateFormatValueExpression($"sumSum([{column.BindingName}])", column.Field, column.EditorType)));
|
|
||||||
cell.Summary = new XRSummary
|
|
||||||
{
|
|
||||||
Running = SummaryRunning.Report,
|
|
||||||
Func = summaryType.Value,
|
|
||||||
IgnoreNullValues = true
|
|
||||||
};
|
|
||||||
|
|
||||||
return cell;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool HasAnySummary(IEnumerable<ReportColumn> fields)
|
|
||||||
{
|
|
||||||
return fields.Any(x => GetSummaryType(x.Field.TotalSummaryJson) != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static float GetImageColumnWidth(ReportColumn column)
|
|
||||||
{
|
|
||||||
return DynamicReportImageHelper.GetGridImageColumnWidth(column.ImageCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ReportColumn CreateReportColumn(
|
|
||||||
ListFormField field,
|
|
||||||
SelectField selectField,
|
|
||||||
DefaultValueHelper defaultValueHelper,
|
|
||||||
string editorType,
|
|
||||||
bool isImage,
|
|
||||||
bool isHtml)
|
|
||||||
{
|
|
||||||
var bindingName = DynamicReportImageHelper.GetReportFieldName(selectField);
|
|
||||||
var lookup = DynamicReportImageHelper.GetLookup(field);
|
|
||||||
var isMultiValue = IsMultiValueLookupEditor(editorType);
|
|
||||||
|
|
||||||
if (lookup == null || selectField?.JoinOptions != null)
|
|
||||||
{
|
|
||||||
return new ReportColumn(field, bindingName, null, null, isMultiValue, isImage, isHtml, editorType);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DynamicReportSqlBuilder.IsReportQueryableLookup(lookup))
|
|
||||||
{
|
|
||||||
return new ReportColumn(field, DynamicReportSqlBuilder.GetLookupBindingName(field), lookup, null, isMultiValue, isImage, isHtml, editorType);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lookup.DataSourceType == UiLookupDataSourceTypeEnum.StaticData)
|
|
||||||
{
|
|
||||||
var staticLookupValues = DynamicReportImageHelper.GetStaticLookupValues(lookup, defaultValueHelper);
|
|
||||||
return new ReportColumn(
|
|
||||||
field,
|
|
||||||
staticLookupValues?.Count > 0 ? DynamicReportSqlBuilder.GetLookupBindingName(field) : bindingName,
|
|
||||||
lookup,
|
|
||||||
staticLookupValues,
|
|
||||||
isMultiValue,
|
|
||||||
isImage,
|
|
||||||
isHtml,
|
|
||||||
editorType);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ReportColumn(field, bindingName, null, null, isMultiValue, isImage, isHtml, editorType);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsMultiValueLookupEditor(string editorType)
|
|
||||||
{
|
|
||||||
return editorType is PlatformConsts.EditorTypes.dxTagBox or "dxGridBox";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static TreeOptionDto GetTreeOptions(ListForm listForm)
|
private static TreeOptionDto GetTreeOptions(ListForm listForm)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(listForm.TreeOptionJson))
|
if (string.IsNullOrWhiteSpace(listForm.TreeOptionJson))
|
||||||
|
|
@ -723,52 +509,6 @@ public class DynamicTreeReport : XtraReport
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record ReportColumn(
|
|
||||||
ListFormField Field,
|
|
||||||
string BindingName,
|
|
||||||
LookupDto Lookup,
|
|
||||||
Dictionary<string, string> StaticLookupValues,
|
|
||||||
bool IsMultiValue,
|
|
||||||
bool IsImage,
|
|
||||||
bool IsHtml,
|
|
||||||
string EditorType,
|
|
||||||
int ImageCount = 1) : DynamicReportSqlBuilder.IReportLookupColumn, DynamicReportGridStateHelper.IReportColumnWidth
|
|
||||||
{
|
|
||||||
public float ImageWidth => GetImageColumnWidth(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static SummaryFunc? GetSummaryType(string totalSummaryJson)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(totalSummaryJson))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var value = totalSummaryJson.ToLowerInvariant();
|
|
||||||
if (value.Contains("\"summarytype\":\"sum\""))
|
|
||||||
{
|
|
||||||
return SummaryFunc.Sum;
|
|
||||||
}
|
|
||||||
if (value.Contains("\"summarytype\":\"avg\""))
|
|
||||||
{
|
|
||||||
return SummaryFunc.Avg;
|
|
||||||
}
|
|
||||||
if (value.Contains("\"summarytype\":\"min\""))
|
|
||||||
{
|
|
||||||
return SummaryFunc.Min;
|
|
||||||
}
|
|
||||||
if (value.Contains("\"summarytype\":\"max\""))
|
|
||||||
{
|
|
||||||
return SummaryFunc.Max;
|
|
||||||
}
|
|
||||||
if (value.Contains("\"summarytype\":\"count\""))
|
|
||||||
{
|
|
||||||
return SummaryFunc.Count;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetParameter(IDictionary<string, string> parameters, string name) =>
|
private static string GetParameter(IDictionary<string, string> parameters, string name) =>
|
||||||
DynamicReportImageHelper.GetParameter(parameters, name);
|
DynamicReportImageHelper.GetParameter(parameters, name);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -839,9 +839,11 @@ export enum ColumnRowTypeListOptions {
|
||||||
|
|
||||||
export enum SubFormTabTypeEnum {
|
export enum SubFormTabTypeEnum {
|
||||||
List = 'List',
|
List = 'List',
|
||||||
|
Card = 'Card',
|
||||||
Tree = 'Tree',
|
Tree = 'Tree',
|
||||||
Gantt = 'Gantt',
|
Gantt = 'Gantt',
|
||||||
Scheduler = 'Scheduler',
|
Scheduler = 'Scheduler',
|
||||||
|
Todo = 'Todo',
|
||||||
Form = 'Form',
|
Form = 'Form',
|
||||||
Chart = 'Chart',
|
Chart = 'Chart',
|
||||||
Pivot = 'Pivot',
|
Pivot = 'Pivot',
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
SimpleItem as SimpleItemDx,
|
SimpleItem as SimpleItemDx,
|
||||||
} from 'devextreme-react/form'
|
} from 'devextreme-react/form'
|
||||||
import type { FieldDataChangedEvent, GroupItem } from 'devextreme/ui/form'
|
import type { FieldDataChangedEvent, GroupItem } from 'devextreme/ui/form'
|
||||||
import { Dispatch, RefObject, useEffect, useRef, useState } from 'react'
|
import { Dispatch, ReactNode, RefObject, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
|
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
|
||||||
import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent'
|
import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent'
|
||||||
import { ImageViewerEditorComponent } from './editors/ImageViewerEditorComponent'
|
import { ImageViewerEditorComponent } from './editors/ImageViewerEditorComponent'
|
||||||
|
|
@ -16,135 +16,21 @@ import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent'
|
||||||
import { RowMode, SimpleItemWithColData } from './types'
|
import { RowMode, SimpleItemWithColData } from './types'
|
||||||
import { PlatformEditorTypes } from '@/proxy/form/models'
|
import { PlatformEditorTypes } from '@/proxy/form/models'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
import {
|
||||||
|
getValueByField,
|
||||||
|
setFormEditorReadOnly,
|
||||||
|
shouldRunEditorScriptOnContentReady,
|
||||||
|
} from '../list/shared/editingForm'
|
||||||
|
import { isTouchLikeDevice } from '../list/shared/editPopup'
|
||||||
|
import { flattenFormGroups, getCascadeParentFields } from './shared/formItems'
|
||||||
|
|
||||||
const flattenFormItems = (items: any[] = []): SimpleItemWithColData[] =>
|
/** Kendi render'ını yapan platform editörleri. */
|
||||||
items.flatMap((item) => [
|
const CUSTOM_EDITOR_TYPES = [
|
||||||
...(item?.dataField ? [item] : []),
|
PlatformEditorTypes.dxTagBox,
|
||||||
...flattenFormItems(item?.items || []),
|
PlatformEditorTypes.dxGridBox,
|
||||||
...(item?.tabs || []).flatMap((tab: any) => flattenFormItems(tab?.items || [])),
|
PlatformEditorTypes.dxImageUpload,
|
||||||
])
|
PlatformEditorTypes.dxImageViewer,
|
||||||
|
] as const
|
||||||
const updateReadOnlyInFormItems = (items: any[] = [], field: string, readOnly: boolean) => {
|
|
||||||
let changed = false
|
|
||||||
const expected = String(field || '').toLowerCase()
|
|
||||||
|
|
||||||
const nextItems = items.map((item) => {
|
|
||||||
const key = item?.dataField || item?.name
|
|
||||||
let nextItem = item
|
|
||||||
|
|
||||||
if (key && String(key).toLowerCase() === expected) {
|
|
||||||
const editorOptions = nextItem.editorOptions || {}
|
|
||||||
if (editorOptions.readOnly !== readOnly) {
|
|
||||||
changed = true
|
|
||||||
nextItem = {
|
|
||||||
...nextItem,
|
|
||||||
editorOptions: {
|
|
||||||
...editorOptions,
|
|
||||||
readOnly,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextItem?.items?.length) {
|
|
||||||
const childResult = updateReadOnlyInFormItems(nextItem.items, field, readOnly)
|
|
||||||
if (childResult.changed) {
|
|
||||||
changed = true
|
|
||||||
nextItem = { ...nextItem, items: childResult.items }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextItem?.tabs?.length) {
|
|
||||||
const tabs = nextItem.tabs.map((tab: any) => {
|
|
||||||
const tabResult = updateReadOnlyInFormItems(tab.items, field, readOnly)
|
|
||||||
if (tabResult.changed) {
|
|
||||||
changed = true
|
|
||||||
return { ...tab, items: tabResult.items }
|
|
||||||
}
|
|
||||||
return tab
|
|
||||||
})
|
|
||||||
nextItem = tabs === nextItem.tabs ? nextItem : { ...nextItem, tabs }
|
|
||||||
}
|
|
||||||
|
|
||||||
return nextItem
|
|
||||||
})
|
|
||||||
|
|
||||||
return { items: nextItems, changed }
|
|
||||||
}
|
|
||||||
|
|
||||||
const findFormFieldKey = (items: any[] = [], field: string): string => {
|
|
||||||
const expected = String(field || '').toLowerCase()
|
|
||||||
|
|
||||||
for (const item of items || []) {
|
|
||||||
const key = item?.dataField || item?.name
|
|
||||||
if (key && String(key).toLowerCase() === expected) {
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
const childKey = findFormFieldKey(item?.items || [], field)
|
|
||||||
if (childKey) {
|
|
||||||
return childKey
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const tab of item?.tabs || []) {
|
|
||||||
const tabKey = findFormFieldKey(tab?.items || [], field)
|
|
||||||
if (tabKey) {
|
|
||||||
return tabKey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return field
|
|
||||||
}
|
|
||||||
|
|
||||||
const setFormEditorReadOnly = (form: any, field: string, readOnly: boolean) => {
|
|
||||||
if (!form?.option) return false
|
|
||||||
|
|
||||||
const apply = () => {
|
|
||||||
const formItems = form.option('items') || []
|
|
||||||
const resolvedField = findFormFieldKey(formItems, field)
|
|
||||||
const editor = form.getEditor?.(resolvedField) ?? form.getEditor?.(field)
|
|
||||||
const result = updateReadOnlyInFormItems(formItems, resolvedField, readOnly)
|
|
||||||
|
|
||||||
if (result.changed) {
|
|
||||||
try {
|
|
||||||
const item = form.itemOption?.(resolvedField) ?? form.itemOption?.(field)
|
|
||||||
if (item) {
|
|
||||||
form.itemOption?.(resolvedField, 'editorOptions', {
|
|
||||||
...(item.editorOptions || {}),
|
|
||||||
readOnly,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
form.option('items', result.items)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
form.option('items', result.items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeEditor = editor ?? form.getEditor?.(resolvedField) ?? form.getEditor?.(field)
|
|
||||||
if (activeEditor?.option?.('readOnly') !== readOnly) {
|
|
||||||
activeEditor?.option?.('readOnly', readOnly)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const getValueByField = (data: Record<string, any> = {}, field?: string) => {
|
|
||||||
if (!field) return undefined
|
|
||||||
if (Object.prototype.hasOwnProperty.call(data, field)) return data[field]
|
|
||||||
const key = Object.keys(data).find(
|
|
||||||
(itemKey) => itemKey.toLowerCase() === String(field).toLowerCase(),
|
|
||||||
)
|
|
||||||
return key ? data[key] : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldRunEditorScriptOnContentReady = (script?: string) =>
|
|
||||||
Boolean(
|
|
||||||
script && (script.includes('setEditorReadOnly') || script.includes('runtimeSetEditorReadOnly')),
|
|
||||||
)
|
|
||||||
|
|
||||||
const FormDevExpress = (props: {
|
const FormDevExpress = (props: {
|
||||||
listFormCode: string
|
listFormCode: string
|
||||||
|
|
@ -166,18 +52,8 @@ const FormDevExpress = (props: {
|
||||||
const [runtimeReadOnlyFields, setRuntimeReadOnlyFields] = useState<Record<string, boolean>>({})
|
const [runtimeReadOnlyFields, setRuntimeReadOnlyFields] = useState<Record<string, boolean>>({})
|
||||||
const runtimeReadOnlyFieldsRef = useRef<Record<string, boolean>>({})
|
const runtimeReadOnlyFieldsRef = useRef<Record<string, boolean>>({})
|
||||||
|
|
||||||
const isTouchLikeDevice = () =>
|
formDataRef.current = formData
|
||||||
typeof window !== 'undefined' &&
|
formItemsRef.current = formItems
|
||||||
(window.matchMedia?.('(pointer: coarse)').matches ||
|
|
||||||
window.matchMedia?.('(hover: none)').matches)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
formDataRef.current = formData
|
|
||||||
}, [formData])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
formItemsRef.current = formItems
|
|
||||||
}, [formItems])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
runtimeReadOnlyFieldsRef.current = runtimeReadOnlyFields
|
runtimeReadOnlyFieldsRef.current = runtimeReadOnlyFields
|
||||||
|
|
@ -187,24 +63,21 @@ const FormDevExpress = (props: {
|
||||||
didAutoFocusRef.current = false
|
didAutoFocusRef.current = false
|
||||||
}, [listFormCode, mode])
|
}, [listFormCode, mode])
|
||||||
|
|
||||||
|
const getFieldKey = (formItem: SimpleItemWithColData) =>
|
||||||
|
String(formItem.dataField || formItem.name || '').toLowerCase()
|
||||||
|
|
||||||
const setRuntimeEditorReadOnly = (field: string, readOnly: boolean) => {
|
const setRuntimeEditorReadOnly = (field: string, readOnly: boolean) => {
|
||||||
const resolvedField = findFormFieldKey(formItemsRef.current, field)
|
const key = String(field || '').toLowerCase()
|
||||||
const key = String(resolvedField || field || '').toLowerCase()
|
|
||||||
if (!key || runtimeReadOnlyFieldsRef.current[key] === readOnly) {
|
if (!key || runtimeReadOnlyFieldsRef.current[key] === readOnly) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
runtimeReadOnlyFieldsRef.current = {
|
runtimeReadOnlyFieldsRef.current = { ...runtimeReadOnlyFieldsRef.current, [key]: readOnly }
|
||||||
...runtimeReadOnlyFieldsRef.current,
|
|
||||||
[key]: readOnly,
|
|
||||||
}
|
|
||||||
setRuntimeReadOnlyFields(runtimeReadOnlyFieldsRef.current)
|
setRuntimeReadOnlyFields(runtimeReadOnlyFieldsRef.current)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRuntimeEditorReadOnly = (formItem: SimpleItemWithColData) => {
|
const getRuntimeEditorReadOnly = (formItem: SimpleItemWithColData) => {
|
||||||
const field = formItem.dataField || formItem.name
|
const key = getFieldKey(formItem)
|
||||||
const resolvedField = findFormFieldKey(formItemsRef.current, field || '')
|
|
||||||
const key = String(resolvedField || field || '').toLowerCase()
|
|
||||||
return Object.prototype.hasOwnProperty.call(runtimeReadOnlyFields, key)
|
return Object.prototype.hasOwnProperty.call(runtimeReadOnlyFields, key)
|
||||||
? runtimeReadOnlyFields[key]
|
? runtimeReadOnlyFields[key]
|
||||||
: undefined
|
: undefined
|
||||||
|
|
@ -212,32 +85,31 @@ const FormDevExpress = (props: {
|
||||||
|
|
||||||
const applyEditorReadOnly = (form: any, field: string, readOnly: boolean) => {
|
const applyEditorReadOnly = (form: any, field: string, readOnly: boolean) => {
|
||||||
setRuntimeEditorReadOnly(field, readOnly)
|
setRuntimeEditorReadOnly(field, readOnly)
|
||||||
setFormEditorReadOnly(form, field, readOnly)
|
setFormEditorReadOnly(formInstanceRef.current ?? form, field, readOnly)
|
||||||
setTimeout(() => setFormEditorReadOnly(formInstanceRef.current ?? form, field, readOnly), 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const applyEditorScriptFormData = (form: any, newData: any) => {
|
/** Editör script'i / özel editör değişikliklerini forma ve state'e yazar. */
|
||||||
const nextFormData = {
|
const commitFormData = useCallback(
|
||||||
...(formDataRef.current || {}),
|
(patch: Record<string, any>, form?: any) => {
|
||||||
...(newData || {}),
|
const nextFormData = { ...(formDataRef.current || {}), ...patch }
|
||||||
}
|
formDataRef.current = nextFormData
|
||||||
|
|
||||||
formDataRef.current = nextFormData
|
const targetForm = form ?? formInstanceRef.current
|
||||||
form?.option?.('formData', nextFormData)
|
targetForm?.option?.('formData', nextFormData)
|
||||||
|
Object.keys(patch).forEach((field) => {
|
||||||
|
targetForm?.getEditor?.(field)?.option?.('value', patch[field])
|
||||||
|
})
|
||||||
|
|
||||||
Object.keys(newData || {}).forEach((field) => {
|
setFormData(nextFormData)
|
||||||
form?.getEditor?.(field)?.option?.('value', newData[field])
|
return nextFormData
|
||||||
})
|
},
|
||||||
|
[setFormData],
|
||||||
|
)
|
||||||
|
|
||||||
setFormData(nextFormData)
|
const runEditorScript = (formItem: SimpleItemWithColData, eventValue: any) => {
|
||||||
}
|
if (!formItem?.editorScript) return
|
||||||
|
|
||||||
const runEditorScript = (formItem: SimpleItemWithColData, eventValue: any, component?: any) => {
|
const form = formInstanceRef.current
|
||||||
if (!formItem?.editorScript) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const form = formInstanceRef.current ?? component
|
|
||||||
const dataField = formItem.dataField
|
const dataField = formItem.dataField
|
||||||
const nextFormData = {
|
const nextFormData = {
|
||||||
...(formDataRef.current || {}),
|
...(formDataRef.current || {}),
|
||||||
|
|
@ -247,46 +119,38 @@ const FormDevExpress = (props: {
|
||||||
formDataRef.current = nextFormData
|
formDataRef.current = nextFormData
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const editor = {
|
|
||||||
dataField,
|
|
||||||
component: form,
|
|
||||||
}
|
|
||||||
const formData = nextFormData
|
|
||||||
const e = {
|
|
||||||
component: form,
|
|
||||||
dataField,
|
|
||||||
scriptEvent: 'valueChanged',
|
|
||||||
value: eventValue,
|
|
||||||
}
|
|
||||||
const runtimeSetEditorReadOnly = (field: string, readOnly: boolean) =>
|
|
||||||
applyEditorReadOnly(form, field, readOnly)
|
|
||||||
|
|
||||||
executeEditorScript(formItem.editorScript, {
|
executeEditorScript(formItem.editorScript, {
|
||||||
formData,
|
formData: nextFormData,
|
||||||
e,
|
e: { component: form, dataField, scriptEvent: 'valueChanged', value: eventValue },
|
||||||
editor,
|
editor: { dataField, component: form },
|
||||||
runtimeSetEditorReadOnly,
|
runtimeSetEditorReadOnly: (field: string, readOnly: boolean) =>
|
||||||
setFormData: (newData: any) => applyEditorScriptFormData(form, newData),
|
applyEditorReadOnly(form, field, readOnly),
|
||||||
|
setFormData: (newData: any) => commitFormData(newData || {}, form),
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Script execution failed for', formItem.name, err)
|
console.error('Script execution failed for', formItem.name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Özel editörlerin ortak değer değişimi akışı. */
|
||||||
|
const handleCustomEditorChange = (formItem: SimpleItemWithColData, value: any) => {
|
||||||
|
if (!formItem.dataField) return
|
||||||
|
formDataRef.current = { ...formDataRef.current, [formItem.dataField]: value }
|
||||||
|
setFormData(formDataRef.current)
|
||||||
|
runEditorScript(formItem, value)
|
||||||
|
}
|
||||||
|
|
||||||
const getEditorOptions = (formItem: SimpleItemWithColData, index?: number) => {
|
const getEditorOptions = (formItem: SimpleItemWithColData, index?: number) => {
|
||||||
const runtimeReadOnly = getRuntimeEditorReadOnly(formItem)
|
const runtimeReadOnly = getRuntimeEditorReadOnly(formItem)
|
||||||
const prevOnValueChanged = formItem.editorOptions?.onValueChanged
|
const previousOnValueChanged = formItem.editorOptions?.onValueChanged
|
||||||
|
const configuredButtons = formItem.editorOptions?.buttons
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...(index !== undefined && mode !== 'view' && !isTouchLikeDevice()
|
...(index !== undefined && mode !== 'view' && !isTouchLikeDevice()
|
||||||
? { autoFocus: index === 1 }
|
? { autoFocus: index === 1 }
|
||||||
: {}),
|
: {}),
|
||||||
...(formItem.editorType === 'dxDateBox'
|
...(formItem.editorType === 'dxDateBox'
|
||||||
? {
|
? { useMaskBehavior: true, openOnFieldClick: true, showClearButton: true }
|
||||||
useMaskBehavior: true,
|
|
||||||
openOnFieldClick: true,
|
|
||||||
showClearButton: true,
|
|
||||||
}
|
|
||||||
: {}),
|
: {}),
|
||||||
...(formItem.colData?.placeHolder
|
...(formItem.colData?.placeHolder
|
||||||
? { placeholder: translate('::' + formItem.colData.placeHolder) }
|
? { placeholder: translate('::' + formItem.colData.placeHolder) }
|
||||||
|
|
@ -297,350 +161,303 @@ const FormDevExpress = (props: {
|
||||||
...(formItem.editorScript
|
...(formItem.editorScript
|
||||||
? {
|
? {
|
||||||
onValueChanged: (e: any) => {
|
onValueChanged: (e: any) => {
|
||||||
if (typeof prevOnValueChanged === 'function') {
|
previousOnValueChanged?.(e)
|
||||||
prevOnValueChanged(e)
|
|
||||||
}
|
|
||||||
if (formItem.dataField) {
|
if (formItem.dataField) {
|
||||||
const nextFormData = {
|
commitFormData({ [formItem.dataField]: e?.value })
|
||||||
...(formDataRef.current || {}),
|
|
||||||
...(formInstanceRef.current?.option?.('formData') || {}),
|
|
||||||
[formItem.dataField]: e?.value,
|
|
||||||
}
|
|
||||||
formDataRef.current = nextFormData
|
|
||||||
formInstanceRef.current?.option?.('formData', nextFormData)
|
|
||||||
setFormData(nextFormData)
|
|
||||||
}
|
}
|
||||||
runEditorScript(formItem, e?.value, formInstanceRef.current)
|
runEditorScript(formItem, e?.value)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
buttons: (formItem.editorOptions?.buttons || []).map((btn: any) => {
|
...(configuredButtons?.length
|
||||||
if (btn?.options?.onClick && typeof btn.options.onClick === 'string') {
|
? {
|
||||||
const origClick = eval(`(${btn.options.onClick})`)
|
buttons: configuredButtons.map((button: any) => {
|
||||||
btn.options.onClick = (e: any) => {
|
// Yapılandırmadan string olarak gelen onClick bir kez fonksiyona çevrilir.
|
||||||
origClick({
|
if (typeof button?.options?.onClick !== 'string') return button
|
||||||
...e,
|
|
||||||
formData: formDataRef.current,
|
const originalClick = eval(`(${button.options.onClick})`)
|
||||||
fieldName: formItem.dataField,
|
return {
|
||||||
mode,
|
...button,
|
||||||
})
|
options: {
|
||||||
|
...button.options,
|
||||||
|
onClick: (e: any) =>
|
||||||
|
originalClick({
|
||||||
|
...e,
|
||||||
|
formData: formDataRef.current,
|
||||||
|
fieldName: formItem.dataField,
|
||||||
|
mode,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
: {}),
|
||||||
return btn
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFormItemKey = (formItem: SimpleItemWithColData, index: number) => {
|
const getFormItemKey = (formItem: SimpleItemWithColData, index: number) =>
|
||||||
const runtimeReadOnly = getRuntimeEditorReadOnly(formItem)
|
`formItem-${formItem.dataField || formItem.name || index}-${String(
|
||||||
return `formItem-${formItem.dataField || formItem.name || index}-${String(runtimeReadOnly)}`
|
getRuntimeEditorReadOnly(formItem),
|
||||||
}
|
)}`
|
||||||
|
|
||||||
// formItems değiştiğinde (özellikle cascading alanlar için) editörlerin dataSource'larını güncelle
|
// Cascading alanlarda formItems yenilendiğinde editör dataSource'larını güncelle.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!refForm.current?.instance()) return
|
const form = refForm.current?.instance()
|
||||||
|
|
||||||
const allItems = formItems.flatMap((group) => flattenFormItems([group]))
|
|
||||||
|
|
||||||
allItems.forEach((item) => {
|
|
||||||
if (item.colData?.lookupDto?.dataSourceType && item.editorOptions?.dataSource) {
|
|
||||||
try {
|
|
||||||
const editor = refForm.current?.instance().getEditor(item.dataField!)
|
|
||||||
if (editor) {
|
|
||||||
editor.option('dataSource', item.editorOptions.dataSource)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// Editor henüz oluşmamış olabilir, sessizce devam et
|
|
||||||
console.debug('Editor update skipped for', item.dataField, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [formItems])
|
|
||||||
|
|
||||||
// Cascade fieldlerin disabled durumunu güncelle
|
|
||||||
const updateCascadeDisabledStates = () => {
|
|
||||||
if (!refForm.current?.instance()) return
|
|
||||||
|
|
||||||
const allItems = formItemsRef.current.flatMap((group) => flattenFormItems([group]))
|
|
||||||
|
|
||||||
allItems.forEach((item) => {
|
|
||||||
const cascadeParentFields = item.colData?.lookupDto?.cascadeParentFields
|
|
||||||
if (cascadeParentFields) {
|
|
||||||
const parentFields = cascadeParentFields.split(',').map((f: string) => f.trim())
|
|
||||||
|
|
||||||
try {
|
|
||||||
const editor = refForm.current?.instance().getEditor(item.dataField!)
|
|
||||||
if (editor && mode !== 'view') {
|
|
||||||
if (item.editorOptions?.disabled === true) {
|
|
||||||
editor.option('disabled', true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parent fieldlerden en az biri boşsa disabled olmalı
|
|
||||||
const shouldDisable = parentFields.some((parentField: string) => {
|
|
||||||
return !formDataRef.current || !formDataRef.current[parentField]
|
|
||||||
})
|
|
||||||
|
|
||||||
editor.option('disabled', shouldDisable)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.debug('Cascade disabled update skipped for', item.dataField, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// formData değiştiğinde cascade disabled durumlarını güncelle
|
|
||||||
useEffect(() => {
|
|
||||||
updateCascadeDisabledStates()
|
|
||||||
}, [formData, mode])
|
|
||||||
|
|
||||||
const runReadOnlyScripts = (form: any) => {
|
|
||||||
if (!form) return
|
if (!form) return
|
||||||
|
|
||||||
const currentFormData = {
|
flattenFormGroups(formItems).forEach((item) => {
|
||||||
...(formDataRef.current || {}),
|
if (!item.colData?.lookupDto?.dataSourceType || !item.editorOptions?.dataSource) return
|
||||||
...(form?.option?.('formData') || {}),
|
|
||||||
}
|
|
||||||
formDataRef.current = currentFormData
|
|
||||||
|
|
||||||
formItemsRef.current
|
try {
|
||||||
.flatMap((group) => flattenFormItems([group]))
|
form.getEditor(item.dataField!)?.option('dataSource', item.editorOptions.dataSource)
|
||||||
.filter((formItem) => shouldRunEditorScriptOnContentReady(formItem.editorScript))
|
} catch (err) {
|
||||||
.forEach((formItem) => {
|
// Editör henüz oluşmamış olabilir.
|
||||||
try {
|
console.debug('Editor update skipped for', item.dataField, err)
|
||||||
const editor = {
|
}
|
||||||
dataField: formItem.dataField,
|
})
|
||||||
component: form,
|
}, [formItems, refForm])
|
||||||
}
|
|
||||||
const formData = currentFormData
|
|
||||||
const e = {
|
|
||||||
component: form,
|
|
||||||
dataField: formItem.dataField,
|
|
||||||
scriptEvent: 'contentReady',
|
|
||||||
value: getValueByField(currentFormData, formItem.dataField),
|
|
||||||
}
|
|
||||||
const runtimeSetEditorReadOnly = (field: string, readOnly: boolean) =>
|
|
||||||
applyEditorReadOnly(form, field, readOnly)
|
|
||||||
|
|
||||||
executeEditorScript(formItem.editorScript!, {
|
/** Parent alanı boş olan cascade alanlarını devre dışı bırakır. */
|
||||||
formData,
|
const updateCascadeDisabledStates = useCallback(() => {
|
||||||
e,
|
const form = refForm.current?.instance()
|
||||||
editor,
|
if (!form || mode === 'view') return
|
||||||
runtimeSetEditorReadOnly,
|
|
||||||
setFormData: undefined,
|
flattenFormGroups(formItemsRef.current).forEach((item) => {
|
||||||
})
|
const parentFields = getCascadeParentFields(item)
|
||||||
} catch (err) {
|
if (!parentFields.length) return
|
||||||
console.error('Script execution failed on contentReady for', formItem.name, err)
|
|
||||||
}
|
try {
|
||||||
})
|
const editor = form.getEditor(item.dataField!)
|
||||||
}
|
if (!editor) return
|
||||||
|
|
||||||
|
editor.option(
|
||||||
|
'disabled',
|
||||||
|
item.editorOptions?.disabled === true ||
|
||||||
|
parentFields.some((parentField) => !formDataRef.current?.[parentField]),
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
console.debug('Cascade disabled update skipped for', item.dataField, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [mode, refForm])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateCascadeDisabledStates()
|
||||||
|
}, [formData, updateCascadeDisabledStates])
|
||||||
|
|
||||||
|
/** contentReady anında salt-okunur davranışını belirleyen script'leri çalıştırır. */
|
||||||
|
const runReadOnlyScripts = useCallback(
|
||||||
|
(form: any) => {
|
||||||
|
if (!form) return
|
||||||
|
|
||||||
|
const currentFormData = {
|
||||||
|
...(formDataRef.current || {}),
|
||||||
|
...(form?.option?.('formData') || {}),
|
||||||
|
}
|
||||||
|
formDataRef.current = currentFormData
|
||||||
|
|
||||||
|
flattenFormGroups(formItemsRef.current)
|
||||||
|
.filter((formItem) => shouldRunEditorScriptOnContentReady(formItem.editorScript))
|
||||||
|
.forEach((formItem) => {
|
||||||
|
try {
|
||||||
|
executeEditorScript(formItem.editorScript!, {
|
||||||
|
formData: currentFormData,
|
||||||
|
e: {
|
||||||
|
component: form,
|
||||||
|
dataField: formItem.dataField,
|
||||||
|
scriptEvent: 'contentReady',
|
||||||
|
value: getValueByField(currentFormData, formItem.dataField),
|
||||||
|
},
|
||||||
|
editor: { dataField: formItem.dataField, component: form },
|
||||||
|
runtimeSetEditorReadOnly: (field: string, readOnly: boolean) =>
|
||||||
|
applyEditorReadOnly(form, field, readOnly),
|
||||||
|
setFormData: undefined,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Script execution failed on contentReady for', formItem.name, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// applyEditorReadOnly ref tabanlıdır; her render'da yeniden oluşması sorun değil.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const form = formInstanceRef.current
|
const form = formInstanceRef.current
|
||||||
if (!form || mode === 'view' || !formData || !formItems?.length) {
|
if (!form || mode === 'view' || !formData || !formItems?.length) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const scriptFields = formItemsRef.current
|
const scriptFields = flattenFormGroups(formItemsRef.current)
|
||||||
.flatMap((group) => flattenFormItems([group]))
|
|
||||||
.filter((formItem) => shouldRunEditorScriptOnContentReady(formItem.editorScript))
|
.filter((formItem) => shouldRunEditorScriptOnContentReady(formItem.editorScript))
|
||||||
.map((formItem) => formItem.dataField)
|
.map((formItem) => formItem.dataField)
|
||||||
.join(MULTIVALUE_DELIMITER)
|
.join(MULTIVALUE_DELIMITER)
|
||||||
|
|
||||||
if (!scriptFields) {
|
if (!scriptFields) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const scriptKey = `${mode}|${scriptFields}|${JSON.stringify(formData)}`
|
const scriptKey = `${mode}|${scriptFields}|${JSON.stringify(formData)}`
|
||||||
if (lastContentReadyScriptKeyRef.current === scriptKey) {
|
if (lastContentReadyScriptKeyRef.current === scriptKey) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
lastContentReadyScriptKeyRef.current = scriptKey
|
lastContentReadyScriptKeyRef.current = scriptKey
|
||||||
setTimeout(() => runReadOnlyScripts(form), 0)
|
setTimeout(() => runReadOnlyScripts(form), 0)
|
||||||
}, [formData, formItems, mode])
|
}, [formData, formItems, mode, runReadOnlyScripts])
|
||||||
|
|
||||||
|
/** Platform editörünün render fonksiyonunu döner. */
|
||||||
|
const renderCustomEditor = (formItem: SimpleItemWithColData): ReactNode => {
|
||||||
|
const value = formData[formItem.dataField!]
|
||||||
|
const editorOptions = getEditorOptions(formItem)
|
||||||
|
const onChange = (next: any) => handleCustomEditorChange(formItem, next)
|
||||||
|
|
||||||
|
switch (formItem.editorType2) {
|
||||||
|
case PlatformEditorTypes.dxTagBox:
|
||||||
|
return (
|
||||||
|
<TagBoxEditorComponent
|
||||||
|
value={value || []}
|
||||||
|
setDefaultValue={false}
|
||||||
|
values={formData}
|
||||||
|
options={formItem.tagBoxOptions}
|
||||||
|
col={formItem.colData}
|
||||||
|
onValueChanged={onChange}
|
||||||
|
editorOptions={editorOptions}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
case PlatformEditorTypes.dxGridBox:
|
||||||
|
return (
|
||||||
|
<GridBoxEditorComponent
|
||||||
|
value={value || []}
|
||||||
|
values={formData}
|
||||||
|
options={formItem.gridBoxOptions}
|
||||||
|
col={formItem.colData}
|
||||||
|
onValueChanged={onChange}
|
||||||
|
editorOptions={editorOptions}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
case PlatformEditorTypes.dxImageUpload:
|
||||||
|
return (
|
||||||
|
<ImageUploadEditorComponent
|
||||||
|
value={value}
|
||||||
|
options={formItem.imageUploadOptions}
|
||||||
|
onValueChanged={onChange}
|
||||||
|
editorOptions={editorOptions}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
case PlatformEditorTypes.dxImageViewer:
|
||||||
|
return (
|
||||||
|
<ImageViewerEditorComponent
|
||||||
|
value={value}
|
||||||
|
options={formItem.imageUploadOptions}
|
||||||
|
editorOptions={editorOptions}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderFormItem = (formItem: SimpleItemWithColData, index: number) => {
|
||||||
|
const key = getFormItemKey(formItem, index)
|
||||||
|
const label = {
|
||||||
|
text: translate('::' + formItem.colData?.captionName),
|
||||||
|
className: 'font-semibold',
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CUSTOM_EDITOR_TYPES.includes(formItem.editorType2 as any)) {
|
||||||
|
return (
|
||||||
|
<SimpleItemDx
|
||||||
|
cssClass="font-semibold"
|
||||||
|
key={key}
|
||||||
|
{...formItem}
|
||||||
|
editorOptions={getEditorOptions(formItem, index)}
|
||||||
|
label={{ text: label.text }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Görsel editörleri DevExtreme'in bilmediği alan tiplerini yaymamak için
|
||||||
|
// yalnızca gerekli alanlarla oluşturulur.
|
||||||
|
const isImageEditor =
|
||||||
|
formItem.editorType2 === PlatformEditorTypes.dxImageUpload ||
|
||||||
|
formItem.editorType2 === PlatformEditorTypes.dxImageViewer
|
||||||
|
const itemProps = isImageEditor
|
||||||
|
? {
|
||||||
|
dataField: formItem.dataField,
|
||||||
|
name: formItem.name,
|
||||||
|
colSpan: formItem.colSpan,
|
||||||
|
isRequired: formItem.isRequired,
|
||||||
|
}
|
||||||
|
: formItem
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SimpleItemDx
|
||||||
|
cssClass="font-semibold"
|
||||||
|
key={key}
|
||||||
|
{...itemProps}
|
||||||
|
render={() => renderCustomEditor(formItem)}
|
||||||
|
label={label}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormDx
|
<FormDx
|
||||||
ref={refForm}
|
ref={refForm}
|
||||||
className={`${DX_CLASSNAMES} ${!isSubForm ? 'px-2' : ''} pb-2`}
|
className={`${DX_CLASSNAMES} ${!isSubForm ? 'px-2' : ''} pb-2`}
|
||||||
formData={formData}
|
formData={formData}
|
||||||
onFieldDataChanged={async (e: FieldDataChangedEvent) => {
|
onFieldDataChanged={(e: FieldDataChangedEvent) => {
|
||||||
if (!e.dataField) {
|
if (!e.dataField) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const newFormData = { ...formData, [e.dataField]: e.value }
|
const newFormData = { ...formData, [e.dataField]: e.value }
|
||||||
let hasChanges = !Object.is(formData?.[e.dataField], e.value)
|
let hasChanges = !Object.is(formData?.[e.dataField], e.value)
|
||||||
|
|
||||||
// Cascading child field'leri temizle (parent field değiştiğinde)
|
// Parent alan değiştiğinde bağımlı (cascade) alanları temizle.
|
||||||
const allItems = formItemsRef.current.flatMap((group) => flattenFormItems([group]))
|
flattenFormGroups(formItemsRef.current)
|
||||||
const cascadingChildren = allItems.filter((item) => {
|
.filter((item) => getCascadeParentFields(item).includes(e.dataField!))
|
||||||
const parentFields = item.colData?.lookupDto?.cascadeParentFields?.split(',') || []
|
.forEach((child) => {
|
||||||
return parentFields.some((field) => field.trim() === e.dataField)
|
if (!Object.is(newFormData[child.dataField!], null)) {
|
||||||
})
|
newFormData[child.dataField!] = null
|
||||||
|
hasChanges = true
|
||||||
// Parent field değiştiğinde child field'leri temizle
|
}
|
||||||
cascadingChildren.forEach((child) => {
|
})
|
||||||
if (!Object.is(newFormData[child.dataField!], null)) {
|
|
||||||
newFormData[child.dataField!] = null
|
|
||||||
hasChanges = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (hasChanges) {
|
if (hasChanges) {
|
||||||
formDataRef.current = newFormData
|
formDataRef.current = newFormData
|
||||||
setFormData(newFormData)
|
setFormData(newFormData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cascade disabled durumlarını güncelle (setTimeout ile editor güncellemesinden sonra çalışsın)
|
// Editör güncellemesi tamamlandıktan sonra devre dışı durumlarını yenile.
|
||||||
setTimeout(() => {
|
setTimeout(updateCascadeDisabledStates, 0)
|
||||||
updateCascadeDisabledStates()
|
|
||||||
}, 0)
|
|
||||||
}}
|
}}
|
||||||
onContentReady={(e) => {
|
onContentReady={(e) => {
|
||||||
formInstanceRef.current = e.component
|
formInstanceRef.current = e.component
|
||||||
|
runReadOnlyScripts(e.component)
|
||||||
|
|
||||||
const form = e.component
|
const firstItem = (e.component.option('items') as any[])?.[0]?.items?.[0]
|
||||||
|
if (
|
||||||
runReadOnlyScripts(form)
|
!didAutoFocusRef.current &&
|
||||||
|
firstItem?.dataField &&
|
||||||
const groupItems = e.component.option('items') as any[]
|
mode !== 'view' &&
|
||||||
const firstItem = groupItems?.[0]?.items?.[0]
|
!isTouchLikeDevice()
|
||||||
|
) {
|
||||||
if (!didAutoFocusRef.current && firstItem?.dataField && !isTouchLikeDevice()) {
|
|
||||||
didAutoFocusRef.current = true
|
didAutoFocusRef.current = true
|
||||||
const editor = e.component.getEditor(firstItem.dataField)
|
e.component.getEditor(firstItem.dataField)?.focus()
|
||||||
if (mode !== 'view') editor?.focus()
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{formItems.map((formGroupItem, i) => {
|
{formItems.map((formGroupItem, groupIndex) => (
|
||||||
return (
|
<GroupItemDx
|
||||||
<GroupItemDx
|
key={'formGroupItem-' + groupIndex}
|
||||||
key={'formGroupItem-' + i}
|
colCount={formGroupItem.colCount}
|
||||||
colCount={formGroupItem.colCount}
|
colSpan={formGroupItem.colSpan}
|
||||||
colSpan={formGroupItem.colSpan}
|
caption={formGroupItem.caption}
|
||||||
caption={formGroupItem.caption}
|
>
|
||||||
>
|
{(formGroupItem.items as SimpleItemWithColData[])
|
||||||
{(formGroupItem.items as SimpleItemWithColData[])
|
?.filter((formItem) => {
|
||||||
?.filter((formItem) => {
|
if (mode === 'edit') return formItem.allowEditing !== false
|
||||||
if (mode === 'edit') return formItem.allowEditing !== false
|
if (mode === 'new') return formItem.allowAdding !== false
|
||||||
if (mode === 'new') return formItem.allowAdding !== false
|
return true
|
||||||
return true
|
})
|
||||||
})
|
.map(renderFormItem)}
|
||||||
.map((formItem, i) => {
|
</GroupItemDx>
|
||||||
return formItem.editorType2 === PlatformEditorTypes.dxTagBox ? (
|
))}
|
||||||
<SimpleItemDx
|
|
||||||
cssClass="font-semibold"
|
|
||||||
key={getFormItemKey(formItem, i)}
|
|
||||||
{...formItem}
|
|
||||||
render={() => (
|
|
||||||
<TagBoxEditorComponent
|
|
||||||
value={formData[formItem.dataField!] || []}
|
|
||||||
setDefaultValue={false}
|
|
||||||
values={formData}
|
|
||||||
options={formItem.tagBoxOptions}
|
|
||||||
col={formItem.colData}
|
|
||||||
onValueChanged={(e: any) => {
|
|
||||||
const newData = { ...formDataRef.current, [formItem.dataField!]: e }
|
|
||||||
formDataRef.current = newData
|
|
||||||
setFormData(newData)
|
|
||||||
runEditorScript(formItem, e, formInstanceRef.current)
|
|
||||||
}}
|
|
||||||
editorOptions={getEditorOptions(formItem)}
|
|
||||||
></TagBoxEditorComponent>
|
|
||||||
)}
|
|
||||||
label={{
|
|
||||||
text: translate('::' + formItem.colData?.captionName),
|
|
||||||
className: 'font-semibold',
|
|
||||||
}}
|
|
||||||
></SimpleItemDx>
|
|
||||||
) : formItem.editorType2 === PlatformEditorTypes.dxGridBox ? (
|
|
||||||
<SimpleItemDx
|
|
||||||
cssClass="font-semibold"
|
|
||||||
key={getFormItemKey(formItem, i)}
|
|
||||||
{...formItem}
|
|
||||||
render={() => (
|
|
||||||
<GridBoxEditorComponent
|
|
||||||
value={formData[formItem.dataField!] || []}
|
|
||||||
values={formData}
|
|
||||||
options={formItem.gridBoxOptions}
|
|
||||||
col={formItem.colData}
|
|
||||||
onValueChanged={(e: any) => {
|
|
||||||
const newData = { ...formDataRef.current, [formItem.dataField!]: e }
|
|
||||||
formDataRef.current = newData
|
|
||||||
setFormData(newData)
|
|
||||||
runEditorScript(formItem, e, formInstanceRef.current)
|
|
||||||
}}
|
|
||||||
editorOptions={getEditorOptions(formItem)}
|
|
||||||
></GridBoxEditorComponent>
|
|
||||||
)}
|
|
||||||
label={{
|
|
||||||
text: translate('::' + formItem.colData?.captionName),
|
|
||||||
className: 'font-semibold',
|
|
||||||
}}
|
|
||||||
></SimpleItemDx>
|
|
||||||
) : formItem.editorType2 === PlatformEditorTypes.dxImageUpload ? (
|
|
||||||
<SimpleItemDx
|
|
||||||
cssClass="font-semibold"
|
|
||||||
key={getFormItemKey(formItem, i)}
|
|
||||||
dataField={formItem.dataField}
|
|
||||||
name={formItem.name}
|
|
||||||
colSpan={formItem.colSpan}
|
|
||||||
isRequired={formItem.isRequired}
|
|
||||||
render={() => (
|
|
||||||
<ImageUploadEditorComponent
|
|
||||||
value={formData[formItem.dataField!]}
|
|
||||||
options={formItem.imageUploadOptions}
|
|
||||||
onValueChanged={(val: any) => {
|
|
||||||
const newData = { ...formDataRef.current, [formItem.dataField!]: val }
|
|
||||||
formDataRef.current = newData
|
|
||||||
setFormData(newData)
|
|
||||||
runEditorScript(formItem, val, formInstanceRef.current)
|
|
||||||
}}
|
|
||||||
editorOptions={getEditorOptions(formItem)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
label={{
|
|
||||||
text: translate('::' + formItem.colData?.captionName),
|
|
||||||
className: 'font-semibold',
|
|
||||||
}}
|
|
||||||
></SimpleItemDx>
|
|
||||||
) : formItem.editorType2 === PlatformEditorTypes.dxImageViewer ? (
|
|
||||||
<SimpleItemDx
|
|
||||||
cssClass="font-semibold"
|
|
||||||
key={getFormItemKey(formItem, i)}
|
|
||||||
dataField={formItem.dataField}
|
|
||||||
name={formItem.name}
|
|
||||||
colSpan={formItem.colSpan}
|
|
||||||
isRequired={formItem.isRequired}
|
|
||||||
render={() => (
|
|
||||||
<ImageViewerEditorComponent
|
|
||||||
value={formData[formItem.dataField!]}
|
|
||||||
options={formItem.imageUploadOptions}
|
|
||||||
editorOptions={getEditorOptions(formItem)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
label={{
|
|
||||||
text: translate('::' + formItem.colData?.captionName),
|
|
||||||
className: 'font-semibold',
|
|
||||||
}}
|
|
||||||
></SimpleItemDx>
|
|
||||||
) : (
|
|
||||||
<SimpleItemDx
|
|
||||||
cssClass="font-semibold"
|
|
||||||
key={getFormItemKey(formItem, i)}
|
|
||||||
{...formItem}
|
|
||||||
editorOptions={getEditorOptions(formItem, i)}
|
|
||||||
label={{ text: translate('::' + formItem.colData?.captionName) }}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</GroupItemDx>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</FormDx>
|
</FormDx>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,16 @@
|
||||||
import { Container, Loading } from '@/components/shared'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
import { Helmet } from 'react-helmet'
|
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import FormButtons from './FormButtons'
|
import FormButtons from './FormButtons'
|
||||||
import FormDevExpress from './FormDevExpress'
|
|
||||||
import SubForms from './SubForms'
|
|
||||||
import { FormProps } from './types'
|
import { FormProps } from './types'
|
||||||
import { useGridData } from './useFormData'
|
import { useGridData } from './useFormData'
|
||||||
import { useCurrentMenuIcon } from '@/utils/hooks/useCurrentMenuIcon'
|
import { FormPageShell } from './shared/FormPageShell'
|
||||||
import { Badge } from '@/components/ui'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { NotePanel } from './notes/NotePanel'
|
|
||||||
import { usePermission } from '@/utils/hooks/usePermission'
|
|
||||||
import { APP_NAME } from '@/constants/app.constant'
|
|
||||||
|
|
||||||
const FormEdit = (
|
const FormEdit = (props: FormProps = { isSubForm: false }) => {
|
||||||
props: FormProps = {
|
|
||||||
isSubForm: false,
|
|
||||||
onActionView: () => {},
|
|
||||||
onActionNew: () => {},
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
const mode = 'edit'
|
const mode = 'edit'
|
||||||
const { isSubForm, level } = props
|
const { isSubForm, level, onActionView, onActionNew } = props
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const listFormCode = props?.listFormCode ?? params?.listFormCode ?? ''
|
const listFormCode = props?.listFormCode ?? params?.listFormCode ?? ''
|
||||||
const id = props?.id ?? params?.id ?? ''
|
const id = props?.id ?? params?.id ?? ''
|
||||||
const MenuIcon = useCurrentMenuIcon('w-5 h-5')
|
|
||||||
const { checkPermission } = usePermission()
|
|
||||||
|
|
||||||
const [isNotePanelVisible, setIsNotePanelVisible] = useState(false)
|
|
||||||
const isNotePanel = () => {
|
|
||||||
return (
|
|
||||||
listFormCode &&
|
|
||||||
id &&
|
|
||||||
!isSubForm &&
|
|
||||||
gridDto?.gridOptions?.showNote &&
|
|
||||||
checkPermission(gridDto?.gridOptions.permissionDto.n)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { translate } = useLocalization()
|
|
||||||
const {
|
const {
|
||||||
fetchData,
|
fetchData,
|
||||||
setFormData,
|
setFormData,
|
||||||
|
|
@ -55,97 +25,49 @@ const FormEdit = (
|
||||||
refForm,
|
refForm,
|
||||||
permissionResults,
|
permissionResults,
|
||||||
} = useGridData({
|
} = useGridData({
|
||||||
mode: 'edit',
|
mode,
|
||||||
listFormCode,
|
listFormCode,
|
||||||
id,
|
id,
|
||||||
level,
|
level,
|
||||||
isSubForm,
|
isSubForm,
|
||||||
onSubmitAction: props?.onActionView,
|
onSubmitAction: onActionView,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!listFormCode) {
|
|
||||||
return <></>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <Loading type="default" loading={loading}></Loading>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData) {
|
|
||||||
return <>{translate('::App.NoResults')}</>
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<FormPageShell
|
||||||
<Container
|
mode={mode}
|
||||||
className={`${isNotePanelVisible && !isSubForm ? 'lg:mr-[375px]' : ''} transition-all duration-300`}
|
listFormCode={listFormCode}
|
||||||
>
|
isSubForm={isSubForm}
|
||||||
{!isSubForm && (
|
level={level}
|
||||||
<Helmet
|
loading={loading}
|
||||||
titleTemplate={`%s | ${APP_NAME}`}
|
gridDto={gridDto}
|
||||||
title={translate('::' + gridDto?.gridOptions.title)}
|
formData={formData}
|
||||||
defaultTitle={APP_NAME}
|
formItems={formItems}
|
||||||
></Helmet>
|
refForm={refForm}
|
||||||
)}
|
setFormData={setFormData}
|
||||||
|
noteEntityId={id}
|
||||||
<div
|
actions={
|
||||||
className={`flex items-center pb-2 px-2 ${isSubForm ? 'justify-end' : 'justify-between'}`}
|
permissionResults && (
|
||||||
>
|
<FormButtons
|
||||||
<div className="flex items-center gap-2">
|
isSubForm={isSubForm}
|
||||||
{MenuIcon}
|
|
||||||
{!isSubForm && (
|
|
||||||
<>
|
|
||||||
<h4 className="text-slate-700 text-sm font-medium leading-none">
|
|
||||||
{translate('::' + gridDto?.gridOptions?.title)}
|
|
||||||
</h4>
|
|
||||||
●
|
|
||||||
<Badge content={mode} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{permissionResults && (
|
|
||||||
<FormButtons
|
|
||||||
isSubForm={isSubForm}
|
|
||||||
mode={mode}
|
|
||||||
listFormCode={listFormCode}
|
|
||||||
id={formData?.Id}
|
|
||||||
gridDto={gridDto!}
|
|
||||||
commandColumnData={commandColumnData!}
|
|
||||||
dataSource={dataSource!}
|
|
||||||
permissions={permissionResults}
|
|
||||||
handleSubmit={handleSubmit}
|
|
||||||
refreshData={fetchData}
|
|
||||||
getSelectedRowKeys={() => [id]}
|
|
||||||
getSelectedRowsData={() => [formData]}
|
|
||||||
getFilter={() => filter}
|
|
||||||
onActionView={props?.onActionView}
|
|
||||||
onActionNew={props?.onActionNew}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="px-2">
|
|
||||||
<FormDevExpress
|
|
||||||
mode={mode}
|
mode={mode}
|
||||||
refForm={refForm}
|
|
||||||
formData={formData}
|
|
||||||
formItems={formItems}
|
|
||||||
setFormData={setFormData}
|
|
||||||
listFormCode={listFormCode}
|
listFormCode={listFormCode}
|
||||||
|
id={formData?.Id}
|
||||||
|
gridDto={gridDto!}
|
||||||
|
commandColumnData={commandColumnData!}
|
||||||
|
dataSource={dataSource!}
|
||||||
|
permissions={permissionResults}
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
|
refreshData={fetchData}
|
||||||
|
getSelectedRowKeys={() => [id]}
|
||||||
|
getSelectedRowsData={() => [formData]}
|
||||||
|
getFilter={() => filter}
|
||||||
|
onActionView={onActionView}
|
||||||
|
onActionNew={onActionNew}
|
||||||
/>
|
/>
|
||||||
</div>
|
)
|
||||||
<SubForms gridDto={gridDto!} formData={formData} level={level ?? 0} />
|
}
|
||||||
</Container>
|
/>
|
||||||
|
|
||||||
{/* Note Panel - sadece ana formda göster */}
|
|
||||||
{isNotePanel() && (
|
|
||||||
<NotePanel
|
|
||||||
entityName={listFormCode}
|
|
||||||
entityId={id}
|
|
||||||
isVisible={isNotePanelVisible}
|
|
||||||
onToggle={() => setIsNotePanelVisible(!isNotePanelVisible)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,23 @@
|
||||||
import { Container, Loading } from '@/components/shared'
|
import { Loading } from '@/components/shared'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { lazy, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { useParams, useSearchParams } from 'react-router-dom'
|
||||||
import type { DataType } from 'devextreme/common'
|
import type { DataType } from 'devextreme/common'
|
||||||
import type { SimpleItem } from 'devextreme/ui/form'
|
import type { SimpleItem } from 'devextreme/ui/form'
|
||||||
import { lazy, useEffect, useRef, useState } from 'react'
|
|
||||||
import { Helmet } from 'react-helmet'
|
|
||||||
import { useParams, useSearchParams } from 'react-router-dom'
|
|
||||||
import FormButtons from './FormButtons'
|
import FormButtons from './FormButtons'
|
||||||
import FormDevExpress from './FormDevExpress'
|
|
||||||
import { FormProps } from './types'
|
import { FormProps } from './types'
|
||||||
import { useGridData } from './useFormData'
|
import { useGridData } from './useFormData'
|
||||||
import { useCurrentMenuIcon } from '@/utils/hooks/useCurrentMenuIcon'
|
import { getList } from '@/services/form.service'
|
||||||
import { Badge } from '@/components/ui'
|
import { applyNewRowDefaults, castValueByDataType } from '../list/shared/editingForm'
|
||||||
import { APP_NAME } from '@/constants/app.constant'
|
import { FormPageShell } from './shared/FormPageShell'
|
||||||
import { FieldCustomValueTypeEnum } from '@/proxy/form/models'
|
|
||||||
import { getList, getNextSequenceValue } from '@/services/form.service'
|
|
||||||
import { autoNumber } from '../list/Utils'
|
|
||||||
|
|
||||||
const FormView = lazy(() => import('./FormView'))
|
const FormView = lazy(() => import('./FormView'))
|
||||||
|
|
||||||
export const FormNewContent = (
|
export const FormNewContent = (props: FormProps = { isSubForm: false }) => {
|
||||||
props: FormProps = {
|
|
||||||
isSubForm: false,
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
const mode = 'new'
|
const mode = 'new'
|
||||||
const { isSubForm, level, sParams } = props
|
const { isSubForm, level, sParams, onActionView } = props
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const listFormCode = props?.listFormCode ?? params?.listFormCode ?? ''
|
const listFormCode = props?.listFormCode ?? params?.listFormCode ?? ''
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
const { translate } = useLocalization()
|
|
||||||
const MenuIcon = useCurrentMenuIcon('w-5 h-5')
|
|
||||||
const initializedListFormCodeRef = useRef<string>()
|
const initializedListFormCodeRef = useRef<string>()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -48,220 +36,107 @@ export const FormNewContent = (
|
||||||
listFormCode,
|
listFormCode,
|
||||||
level,
|
level,
|
||||||
isSubForm,
|
isSubForm,
|
||||||
onSubmitAction: (props?.onActionView),
|
onSubmitAction: onActionView,
|
||||||
})
|
})
|
||||||
const [newParams, setNewParams] = useState<URLSearchParams>(new URLSearchParams())
|
|
||||||
|
|
||||||
useEffect(() => {
|
// URL parametreleri + üst formdan gelen parametreler.
|
||||||
setNewParams(
|
const newParams = useMemo(
|
||||||
|
() =>
|
||||||
new URLSearchParams({
|
new URLSearchParams({
|
||||||
...Object.fromEntries(searchParams),
|
...Object.fromEntries(searchParams),
|
||||||
...Object.fromEntries(sParams ?? []),
|
...Object.fromEntries(sParams ?? []),
|
||||||
}),
|
}),
|
||||||
)
|
[searchParams, sParams],
|
||||||
}, [searchParams, sParams])
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initializedListFormCodeRef.current = undefined
|
initializedListFormCodeRef.current = undefined
|
||||||
}, [listFormCode])
|
}, [listFormCode])
|
||||||
|
|
||||||
|
/** Form ilk açıldığında varsayılan değerleri kurar. */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!formItems?.length || !gridDto?.columnFormats) {
|
if (!formItems?.length || !gridDto?.columnFormats) return
|
||||||
return
|
if (initializedListFormCodeRef.current === listFormCode) return
|
||||||
}
|
|
||||||
|
|
||||||
if (initializedListFormCodeRef.current === listFormCode) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
initializedListFormCodeRef.current = listFormCode
|
initializedListFormCodeRef.current = listFormCode
|
||||||
let isActive = true
|
let isActive = true
|
||||||
|
|
||||||
const initializeFormData = async () => {
|
// Tanımlı tüm alanlar önce boşaltılır, ardından kolon varsayılanları uygulanır.
|
||||||
const initialFormData = await createInitialFormData()
|
const initialData: Record<string, any> = {}
|
||||||
if (isActive) {
|
formItems.forEach((group) =>
|
||||||
setFormData(initialFormData)
|
(group.items as SimpleItem[] | undefined)?.forEach((item) => {
|
||||||
}
|
if (item.dataField) initialData[item.dataField] = undefined
|
||||||
}
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
initializeFormData()
|
applyNewRowDefaults(gridDto, initialData).then((data) => {
|
||||||
|
if (isActive) setFormData(data)
|
||||||
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
isActive = false
|
isActive = false
|
||||||
}
|
}
|
||||||
}, [listFormCode, formItems?.length, gridDto?.columnFormats])
|
}, [listFormCode, formItems?.length, gridDto?.columnFormats])
|
||||||
|
|
||||||
// newParams değiştiğinde sadece newParams'tan gelen alanları güncelle
|
/** URL parametrelerinden gelen alanları forma yazar. */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!formItems || !formData || !gridDto?.columnFormats) {
|
if (!formItems?.length || !formData || !gridDto?.columnFormats) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedData = { ...formData }
|
const updatedData = { ...formData }
|
||||||
let hasChanges = false
|
let hasChanges = false
|
||||||
|
|
||||||
for (const colFormat of gridDto.columnFormats) {
|
for (const colFormat of gridDto.columnFormats) {
|
||||||
if (!colFormat.fieldName || !newParams.has(colFormat.fieldName)) {
|
const fieldName = colFormat.fieldName
|
||||||
continue
|
if (!fieldName || !newParams.has(fieldName)) continue
|
||||||
}
|
|
||||||
|
|
||||||
const dType = colFormat.dataType as DataType
|
const nextValue = castValueByDataType(
|
||||||
let newValue: any
|
newParams.get(fieldName),
|
||||||
|
colFormat.dataType as DataType,
|
||||||
switch (dType) {
|
)
|
||||||
case 'date':
|
if (updatedData[fieldName] !== nextValue) {
|
||||||
case 'datetime':
|
updatedData[fieldName] = nextValue
|
||||||
newValue = new Date(newParams.get(colFormat.fieldName)!)
|
|
||||||
break
|
|
||||||
case 'number':
|
|
||||||
newValue = Number(newParams.get(colFormat.fieldName))
|
|
||||||
break
|
|
||||||
case 'boolean':
|
|
||||||
if (newParams.get(colFormat.fieldName) === 'true') {
|
|
||||||
newValue = true
|
|
||||||
} else if (newParams.get(colFormat.fieldName) === 'false') {
|
|
||||||
newValue = false
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'object':
|
|
||||||
try {
|
|
||||||
newValue = JSON.parse(newParams.get(colFormat.fieldName) as string)
|
|
||||||
} catch {
|
|
||||||
newValue = updatedData[colFormat.fieldName]
|
|
||||||
}
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
newValue = newParams.get(colFormat.fieldName)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updatedData[colFormat.fieldName] !== newValue) {
|
|
||||||
updatedData[colFormat.fieldName] = newValue
|
|
||||||
hasChanges = true
|
hasChanges = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasChanges) {
|
if (hasChanges) setFormData(updatedData)
|
||||||
setFormData(updatedData)
|
}, [newParams, formData, gridDto?.columnFormats])
|
||||||
}
|
|
||||||
}, [newParams, gridDto?.columnFormats])
|
|
||||||
|
|
||||||
// newParams'tan gelen alanları readonly yap
|
/** URL'den gelen alanlar kullanıcı tarafından değiştirilemez. */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!formItems || !gridDto?.columnFormats || !newParams) {
|
if (!formItems?.length || !gridDto?.columnFormats) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const colFormat of gridDto.columnFormats) {
|
const lockedFields = new Set(
|
||||||
if (!colFormat.fieldName || !newParams.has(colFormat.fieldName)) {
|
gridDto.columnFormats
|
||||||
continue
|
.map((colFormat) => colFormat.fieldName)
|
||||||
}
|
.filter((fieldName): fieldName is string => !!fieldName && newParams.has(fieldName)),
|
||||||
|
)
|
||||||
|
if (!lockedFields.size) return
|
||||||
|
|
||||||
// Eğer URL'den değer geliyorsa, bu editörü readonly yapıyoruz
|
formItems.forEach((group) =>
|
||||||
formItems.forEach((form) => {
|
(group.items as SimpleItem[] | undefined)?.forEach((item) => {
|
||||||
form?.items?.forEach((item: SimpleItem) => {
|
if (item.dataField && lockedFields.has(item.dataField)) {
|
||||||
if (item.dataField === colFormat.fieldName) {
|
item.editorOptions = { ...item.editorOptions, readOnly: true }
|
||||||
item.editorOptions = { ...item.editorOptions, readOnly: true }
|
}
|
||||||
}
|
}),
|
||||||
})
|
)
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [formItems, newParams, gridDto?.columnFormats])
|
}, [formItems, newParams, gridDto?.columnFormats])
|
||||||
|
|
||||||
async function createInitialFormData() {
|
|
||||||
const data: any = {}
|
|
||||||
if (!formItems?.length) {
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default tüm alanlar undefined olsun
|
|
||||||
for (const formGroupItem of formItems) {
|
|
||||||
if (formGroupItem.items) {
|
|
||||||
//SimpleItem
|
|
||||||
for (const formItem of formGroupItem.items) {
|
|
||||||
const f = formItem as SimpleItem
|
|
||||||
data[f.dataField!] = undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!gridDto?.columnFormats) {
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grid'den gelen columnFormat'ları kullanarak default değerleri set et
|
|
||||||
for (const colFormat of gridDto?.columnFormats || []) {
|
|
||||||
if (!colFormat.fieldName) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (colFormat.defaultValue !== null && colFormat.defaultValue !== undefined) {
|
|
||||||
if (
|
|
||||||
typeof colFormat.defaultValue === 'string' &&
|
|
||||||
colFormat.defaultValue === '@AUTONUMBER'
|
|
||||||
) {
|
|
||||||
data[colFormat.fieldName] = autoNumber()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (colFormat.defaultValueType === FieldCustomValueTypeEnum.Sequence) {
|
|
||||||
try {
|
|
||||||
const response = await getNextSequenceValue(String(colFormat.defaultValue))
|
|
||||||
data[colFormat.fieldName] = response.data
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Sequence default value alınamadı:', {
|
|
||||||
fieldName: colFormat.fieldName,
|
|
||||||
defaultValue: colFormat.defaultValue,
|
|
||||||
error,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
data[colFormat.fieldName] = colFormat.defaultValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!listFormCode) {
|
|
||||||
return <></>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <Loading type="default" loading={loading}></Loading>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData) {
|
|
||||||
return <>{translate('::App.NoResults')}</>
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<FormPageShell
|
||||||
{!isSubForm && (
|
mode={mode}
|
||||||
<Helmet
|
listFormCode={listFormCode}
|
||||||
titleTemplate={`%s | ${APP_NAME}`}
|
isSubForm={isSubForm}
|
||||||
title={translate('::' + gridDto?.gridOptions.title)}
|
level={level}
|
||||||
defaultTitle={APP_NAME}
|
loading={loading}
|
||||||
></Helmet>
|
gridDto={gridDto}
|
||||||
)}
|
formData={formData}
|
||||||
<div
|
formItems={formItems}
|
||||||
className={`flex items-center pb-2 px-2 ${isSubForm ? 'justify-end' : 'justify-between'}`}
|
refForm={refForm}
|
||||||
>
|
setFormData={setFormData}
|
||||||
<div className="flex items-center gap-2">
|
showSubForms={false}
|
||||||
{MenuIcon}
|
actions={
|
||||||
{!isSubForm && (
|
permissionResults && (
|
||||||
<>
|
|
||||||
<h4 className="text-slate-700 text-sm font-medium leading-none">
|
|
||||||
{translate('::' + gridDto?.gridOptions?.title)}
|
|
||||||
</h4>
|
|
||||||
●
|
|
||||||
<Badge content={mode} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{permissionResults && (
|
|
||||||
<FormButtons
|
<FormButtons
|
||||||
isSubForm={isSubForm}
|
isSubForm={isSubForm}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
|
|
@ -276,57 +151,42 @@ export const FormNewContent = (
|
||||||
getSelectedRowKeys={() => []}
|
getSelectedRowKeys={() => []}
|
||||||
getSelectedRowsData={() => [formData]}
|
getSelectedRowsData={() => [formData]}
|
||||||
getFilter={() => []}
|
getFilter={() => []}
|
||||||
onActionView={props?.onActionView}
|
onActionView={onActionView}
|
||||||
/>
|
/>
|
||||||
)}
|
)
|
||||||
</div>
|
}
|
||||||
<div className="px-2">
|
/>
|
||||||
<FormDevExpress
|
|
||||||
mode={mode}
|
|
||||||
refForm={refForm}
|
|
||||||
formData={formData}
|
|
||||||
formItems={formItems}
|
|
||||||
setFormData={setFormData}
|
|
||||||
listFormCode={listFormCode}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const FormNew = (
|
/**
|
||||||
props: FormProps = {
|
* `listFormType === 'Form'` olan liste formlarında "yeni kayıt" ekranı yerine
|
||||||
isSubForm: false,
|
* doğrudan görüntüleme ekranı açılır (tekil kayıtlı formlar).
|
||||||
},
|
*/
|
||||||
) => {
|
const FormNew = (props: FormProps = { isSubForm: false }) => {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const listFormCode = props.listFormCode ?? params.listFormCode ?? ''
|
const listFormCode = props.listFormCode ?? params.listFormCode ?? ''
|
||||||
const [listFormType, setListFormType] = useState<string>()
|
const [listFormType, setListFormType] = useState<string>()
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isActive = true
|
if (!listFormCode || props.isSubForm) {
|
||||||
|
|
||||||
const resolveFormType = async () => {
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
const response = await getList({ listFormCode })
|
|
||||||
if (isActive) {
|
|
||||||
setListFormType(response?.data?.gridOptions?.listFormType)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (isActive) {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (listFormCode && !props.isSubForm) {
|
|
||||||
resolveFormType()
|
|
||||||
} else {
|
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isActive = true
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
getList({ listFormCode })
|
||||||
|
.then((response: any) => {
|
||||||
|
if (isActive) setListFormType(response?.data?.gridOptions?.listFormType)
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => console.error('ListForm tipi alınamadı:', error))
|
||||||
|
.finally(() => {
|
||||||
|
if (isActive) setLoading(false)
|
||||||
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
isActive = false
|
isActive = false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,15 @@
|
||||||
import { Container, Loading } from '@/components/shared'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
import { Helmet } from 'react-helmet'
|
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import FormButtons from './FormButtons'
|
import FormButtons from './FormButtons'
|
||||||
import FormDevExpress from './FormDevExpress'
|
|
||||||
import SubForms from './SubForms'
|
|
||||||
import { FormProps } from './types'
|
import { FormProps } from './types'
|
||||||
import { useGridData } from './useFormData'
|
import { useGridData } from './useFormData'
|
||||||
import { useCurrentMenuIcon } from '@/utils/hooks/useCurrentMenuIcon'
|
import { FormPageShell } from './shared/FormPageShell'
|
||||||
import { Badge } from '@/components/ui'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { NotePanel } from './notes/NotePanel'
|
|
||||||
import { usePermission } from '@/utils/hooks/usePermission'
|
|
||||||
import { APP_NAME } from '@/constants/app.constant'
|
|
||||||
|
|
||||||
const FormView = (
|
const FormView = (props: FormProps = { isSubForm: false }) => {
|
||||||
props: FormProps = {
|
|
||||||
isSubForm: false,
|
|
||||||
onActionEdit: () => {},
|
|
||||||
onActionNew: () => {},
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
const mode = 'view'
|
const mode = 'view'
|
||||||
const { isSubForm, level } = props
|
const { isSubForm, level, onActionEdit, onActionNew } = props
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const listFormCode = props?.listFormCode ?? params?.listFormCode ?? ''
|
const listFormCode = props?.listFormCode ?? params?.listFormCode ?? ''
|
||||||
const id = props?.id ?? params?.id ?? ''
|
const id = props?.id ?? params?.id ?? ''
|
||||||
const { translate } = useLocalization()
|
|
||||||
const MenuIcon = useCurrentMenuIcon('w-5 h-5')
|
|
||||||
const { checkPermission } = usePermission()
|
|
||||||
|
|
||||||
const [isNotePanelVisible, setIsNotePanelVisible] = useState(false)
|
|
||||||
const isNotePanel = () => {
|
|
||||||
return (
|
|
||||||
listFormCode &&
|
|
||||||
formData &&
|
|
||||||
!isSubForm &&
|
|
||||||
gridDto?.gridOptions?.showNote &&
|
|
||||||
checkPermission(gridDto?.gridOptions.permissionDto.n)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
loading,
|
loading,
|
||||||
|
|
@ -52,102 +22,49 @@ const FormView = (
|
||||||
refForm,
|
refForm,
|
||||||
permissionResults,
|
permissionResults,
|
||||||
fetchData,
|
fetchData,
|
||||||
} = useGridData({
|
} = useGridData({ mode, listFormCode, id, isSubForm })
|
||||||
mode,
|
|
||||||
listFormCode,
|
|
||||||
id,
|
|
||||||
isSubForm,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!listFormCode) {
|
// Form tipi liste formları ID olmadan da açılabildiği için kayıt anahtarı
|
||||||
return <></>
|
// veriden okunur.
|
||||||
}
|
const recordId = id || formData?.[gridDto?.gridOptions.keyFieldName ?? 'Id']
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <Loading type="default" loading={loading}></Loading>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData) {
|
|
||||||
return <>{translate('::App.NoResults')}</>
|
|
||||||
}
|
|
||||||
|
|
||||||
const recordId = id || formData[gridDto?.gridOptions.keyFieldName ?? 'Id']
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<FormPageShell
|
||||||
<Container
|
mode={mode}
|
||||||
className={`${isNotePanelVisible && !isSubForm ? 'lg:mr-[375px]' : ''} transition-all duration-300`}
|
listFormCode={listFormCode}
|
||||||
>
|
isSubForm={isSubForm}
|
||||||
{!isSubForm && (
|
level={level}
|
||||||
<Helmet
|
loading={loading}
|
||||||
titleTemplate={`%s | ${APP_NAME}`}
|
gridDto={gridDto}
|
||||||
title={translate('::' + gridDto?.gridOptions.title)}
|
formData={formData}
|
||||||
defaultTitle={APP_NAME}
|
formItems={formItems}
|
||||||
></Helmet>
|
refForm={refForm}
|
||||||
)}
|
setFormData={() => {}}
|
||||||
<div
|
noteEntityId={recordId}
|
||||||
className={`flex items-center pb-2 px-2 ${isSubForm ? 'justify-end' : 'justify-between'}`}
|
refreshData={fetchData}
|
||||||
>
|
contentClassName={isSubForm ? 'px-2' : ''}
|
||||||
<div className="flex items-center gap-2">
|
actions={
|
||||||
{MenuIcon}
|
permissionResults && (
|
||||||
{!isSubForm && (
|
<FormButtons
|
||||||
<>
|
isSubForm={isSubForm}
|
||||||
<h4 className="text-slate-700 text-sm font-medium leading-none">
|
|
||||||
{translate('::' + gridDto?.gridOptions?.title)}
|
|
||||||
</h4>
|
|
||||||
●
|
|
||||||
<Badge content={mode} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{permissionResults && (
|
|
||||||
<FormButtons
|
|
||||||
isSubForm={isSubForm}
|
|
||||||
mode={mode}
|
|
||||||
listFormCode={listFormCode}
|
|
||||||
id={recordId}
|
|
||||||
gridDto={gridDto!}
|
|
||||||
commandColumnData={commandColumnData!}
|
|
||||||
dataSource={dataSource!}
|
|
||||||
permissions={permissionResults}
|
|
||||||
handleSubmit={() => {}}
|
|
||||||
refreshData={fetchData}
|
|
||||||
getSelectedRowKeys={() => [recordId]}
|
|
||||||
getSelectedRowsData={() => [formData]}
|
|
||||||
getFilter={() => filter}
|
|
||||||
onActionEdit={props.onActionEdit}
|
|
||||||
onActionNew={props.onActionNew}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className={`${isSubForm ? 'px-2' : ''}`}>
|
|
||||||
<FormDevExpress
|
|
||||||
mode={mode}
|
mode={mode}
|
||||||
refForm={refForm}
|
|
||||||
formData={formData}
|
|
||||||
formItems={formItems}
|
|
||||||
setFormData={() => {}}
|
|
||||||
listFormCode={listFormCode}
|
listFormCode={listFormCode}
|
||||||
|
id={recordId}
|
||||||
|
gridDto={gridDto!}
|
||||||
|
commandColumnData={commandColumnData!}
|
||||||
|
dataSource={dataSource!}
|
||||||
|
permissions={permissionResults}
|
||||||
|
handleSubmit={() => {}}
|
||||||
|
refreshData={fetchData}
|
||||||
|
getSelectedRowKeys={() => [recordId]}
|
||||||
|
getSelectedRowsData={() => [formData]}
|
||||||
|
getFilter={() => filter}
|
||||||
|
onActionEdit={onActionEdit}
|
||||||
|
onActionNew={onActionNew}
|
||||||
/>
|
/>
|
||||||
</div>
|
)
|
||||||
<SubForms
|
}
|
||||||
gridDto={gridDto!}
|
/>
|
||||||
formData={formData}
|
|
||||||
level={level ?? 0}
|
|
||||||
refreshData={fetchData}
|
|
||||||
/>
|
|
||||||
</Container>
|
|
||||||
|
|
||||||
{/* Note Panel - sadece ana formda göster */}
|
|
||||||
{isNotePanel() && (
|
|
||||||
<NotePanel
|
|
||||||
entityName={listFormCode}
|
|
||||||
entityId={recordId}
|
|
||||||
isVisible={isNotePanelVisible}
|
|
||||||
onToggle={() => setIsNotePanelVisible(!isNotePanelVisible)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,26 @@ import { Tabs } from '@/components/ui'
|
||||||
import TabContent from '@/components/ui/Tabs/TabContent'
|
import TabContent from '@/components/ui/Tabs/TabContent'
|
||||||
import TabList from '@/components/ui/Tabs/TabList'
|
import TabList from '@/components/ui/Tabs/TabList'
|
||||||
import TabNav from '@/components/ui/Tabs/TabNav'
|
import TabNav from '@/components/ui/Tabs/TabNav'
|
||||||
import { useEffect, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { FaChartBar, FaList } from 'react-icons/fa'
|
import type { ReactNode } from 'react'
|
||||||
|
import {
|
||||||
|
FaCalendarAlt,
|
||||||
|
FaChartArea,
|
||||||
|
FaChartBar,
|
||||||
|
FaList,
|
||||||
|
FaProjectDiagram,
|
||||||
|
FaSitemap,
|
||||||
|
FaTable,
|
||||||
|
FaTasks,
|
||||||
|
FaThLarge,
|
||||||
|
FaWpforms,
|
||||||
|
} from 'react-icons/fa'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
import Grid from '../list/Grid'
|
import Grid from '../list/Grid'
|
||||||
|
import CardView from '../list/CardView'
|
||||||
import Chart from '../list/Chart'
|
import Chart from '../list/Chart'
|
||||||
|
import Pivot from '../list/Pivot'
|
||||||
|
import TodoBoard from '../list/TodoBoard'
|
||||||
import { GridDto, SubFormDto, SubFormTabTypeEnum } from '@/proxy/form/models'
|
import { GridDto, SubFormDto, SubFormTabTypeEnum } from '@/proxy/form/models'
|
||||||
import FormEdit from './FormEdit'
|
import FormEdit from './FormEdit'
|
||||||
import { FormNewContent as FormNew } from './FormNew'
|
import { FormNewContent as FormNew } from './FormNew'
|
||||||
|
|
@ -16,6 +31,74 @@ import Tree from '../list/Tree'
|
||||||
import GanttView from '../list/GanttView'
|
import GanttView from '../list/GanttView'
|
||||||
import SchedulerView from '../list/SchedulerView'
|
import SchedulerView from '../list/SchedulerView'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aynı props sözleşmesini paylaşan liste tipi alt form görünümleri.
|
||||||
|
*
|
||||||
|
* DİKKAT: Bu eşleme modül seviyesinde sabit olarak tutulamaz. Grid/CardView/Tree/
|
||||||
|
* GanttView/SchedulerView/TodoBoard/Pivot bu dosyayı geri import ettiği için
|
||||||
|
* döngüsel bağımlılık vardır ve modül değerlendirme anında bileşenlere erişmek
|
||||||
|
* "Cannot access 'Grid' before initialization" hatası verir. Bu yüzden çözüm
|
||||||
|
* render anında yapılır.
|
||||||
|
*/
|
||||||
|
const resolveListView = (tabType: SubFormTabTypeEnum) => {
|
||||||
|
switch (tabType) {
|
||||||
|
case SubFormTabTypeEnum.List:
|
||||||
|
return Grid
|
||||||
|
case SubFormTabTypeEnum.Card:
|
||||||
|
return CardView
|
||||||
|
case SubFormTabTypeEnum.Tree:
|
||||||
|
return Tree
|
||||||
|
case SubFormTabTypeEnum.Gantt:
|
||||||
|
return GanttView
|
||||||
|
case SubFormTabTypeEnum.Scheduler:
|
||||||
|
return SchedulerView
|
||||||
|
case SubFormTabTypeEnum.Todo:
|
||||||
|
return TodoBoard
|
||||||
|
case SubFormTabTypeEnum.Pivot:
|
||||||
|
return Pivot
|
||||||
|
default:
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sekme başlığındaki ikonlar. */
|
||||||
|
const TAB_ICONS: Partial<Record<SubFormTabTypeEnum, ReactNode>> = {
|
||||||
|
[SubFormTabTypeEnum.List]: <FaList />,
|
||||||
|
[SubFormTabTypeEnum.Card]: <FaThLarge />,
|
||||||
|
[SubFormTabTypeEnum.Tree]: <FaSitemap />,
|
||||||
|
[SubFormTabTypeEnum.Gantt]: <FaProjectDiagram />,
|
||||||
|
[SubFormTabTypeEnum.Scheduler]: <FaCalendarAlt />,
|
||||||
|
[SubFormTabTypeEnum.Todo]: <FaTasks />,
|
||||||
|
[SubFormTabTypeEnum.Form]: <FaWpforms />,
|
||||||
|
[SubFormTabTypeEnum.Chart]: <FaChartArea />,
|
||||||
|
[SubFormTabTypeEnum.Pivot]: <FaTable />,
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubFormTabMode = 'view' | 'edit' | 'new'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alt form ile üst formun ilişkili alanlarından filtre ve kayıt anahtarı üretir.
|
||||||
|
* (Üst kayıt seçili değilse alt form filtresiz kalır.)
|
||||||
|
*/
|
||||||
|
const withParentRelation = (subForm: SubFormDto, formData: any): SubFormDto => {
|
||||||
|
const relation = subForm.relation?.find((item) => formData?.[item.parentFieldName])
|
||||||
|
if (!relation) {
|
||||||
|
return subForm
|
||||||
|
}
|
||||||
|
|
||||||
|
const filter: [string, string, string] = [
|
||||||
|
relation.childFieldName,
|
||||||
|
'=',
|
||||||
|
formData[relation.parentFieldName],
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
...subForm,
|
||||||
|
searchParams: new URLSearchParams({ filter: JSON.stringify(filter) }),
|
||||||
|
id: formData[relation.parentFieldName],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const SubForms = (props: {
|
const SubForms = (props: {
|
||||||
gridDto: GridDto
|
gridDto: GridDto
|
||||||
formData: any
|
formData: any
|
||||||
|
|
@ -25,206 +108,137 @@ const SubForms = (props: {
|
||||||
}) => {
|
}) => {
|
||||||
const { gridDto, formData, level, refreshData, refreshGridDto } = props
|
const { gridDto, formData, level, refreshData, refreshGridDto } = props
|
||||||
const { hash } = useLocation()
|
const { hash } = useLocation()
|
||||||
const [currentTab, setCurrentTab] = useState<string>()
|
|
||||||
const [subForms, setSubForms] = useState<SubFormDto[]>()
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
|
|
||||||
|
const [currentTab, setCurrentTab] = useState<string>()
|
||||||
|
// Form tipi sekmelerin görüntüle/düzenle/yeni modu; kod bazında tutulur.
|
||||||
|
const [tabModes, setTabModes] = useState<Record<string, SubFormTabMode>>({})
|
||||||
|
|
||||||
|
const subForms = useMemo(
|
||||||
|
() => (gridDto?.gridOptions?.subFormsDto ?? []).map((sub) => withParentRelation(sub, formData)),
|
||||||
|
[gridDto, formData],
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hash && level === 0) {
|
if (hash && level === 0) {
|
||||||
setCurrentTab(hash.replace('#', ''))
|
setCurrentTab(hash.replace('#', ''))
|
||||||
} else if (gridDto?.gridOptions?.subFormsDto?.length) {
|
|
||||||
setCurrentTab(gridDto?.gridOptions?.subFormsDto[0].code)
|
|
||||||
} else {
|
} else {
|
||||||
setCurrentTab(undefined)
|
setCurrentTab(subForms[0]?.code)
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [hash, gridDto])
|
}, [hash, gridDto])
|
||||||
|
|
||||||
useEffect(() => {
|
const setTabMode = useCallback(
|
||||||
if (!gridDto?.gridOptions?.subFormsDto?.length) {
|
(code: string, tabMode: SubFormTabMode) =>
|
||||||
return
|
setTabModes((current) => ({ ...current, [code]: tabMode })),
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!formData || !subForms.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderTabContent = (subForm: SubFormDto) => {
|
||||||
|
const ListView = resolveListView(subForm.tabType)
|
||||||
|
|
||||||
|
if (ListView) {
|
||||||
|
return (
|
||||||
|
<ListView
|
||||||
|
listFormCode={subForm.code}
|
||||||
|
searchParams={subForm.searchParams}
|
||||||
|
isSubForm
|
||||||
|
level={level + 1}
|
||||||
|
refreshData={subForm.isRefresh ? refreshData : undefined}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const subForm of gridDto.gridOptions.subFormsDto) {
|
if (subForm.tabType === SubFormTabTypeEnum.Chart) {
|
||||||
if (formData && subForm.relation?.length) {
|
return (
|
||||||
// sadece ilk eşleşmeyi al
|
<Chart
|
||||||
const rel = subForm.relation.find((a) => formData?.[a.parentFieldName])
|
id={gridDto?.gridOptions.id as string}
|
||||||
|
listFormCode={subForm.code}
|
||||||
if (rel) {
|
isSubForm
|
||||||
const filter: [string, string, string] = [
|
level={level + 1}
|
||||||
rel.childFieldName,
|
refreshData={subForm.isRefresh ? refreshData : undefined}
|
||||||
'=',
|
gridDto={gridDto}
|
||||||
formData[rel.parentFieldName],
|
refreshGridDto={refreshGridDto}
|
||||||
]
|
/>
|
||||||
|
)
|
||||||
subForm.searchParams = new URLSearchParams({
|
|
||||||
filter: JSON.stringify(filter),
|
|
||||||
})
|
|
||||||
|
|
||||||
subForm.id = formData[rel.parentFieldName]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setSubForms([...gridDto.gridOptions.subFormsDto])
|
if (subForm.tabType !== SubFormTabTypeEnum.Form) {
|
||||||
}, [gridDto, formData])
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
if (!formData || !subForms?.length) {
|
const tabMode = tabModes[subForm.code] ?? (subForm.tabMode as SubFormTabMode) ?? 'view'
|
||||||
return <></>
|
const commonProps = {
|
||||||
|
listFormCode: subForm.code,
|
||||||
|
level: level + 1,
|
||||||
|
isSubForm: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabMode === 'edit') {
|
||||||
|
return (
|
||||||
|
<FormEdit
|
||||||
|
{...commonProps}
|
||||||
|
id={subForm.id}
|
||||||
|
onActionNew={() => setTabMode(subForm.code, 'new')}
|
||||||
|
onActionView={() => setTabMode(subForm.code, 'view')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabMode === 'new') {
|
||||||
|
return (
|
||||||
|
<FormNew
|
||||||
|
{...commonProps}
|
||||||
|
sParams={subForm.searchParams}
|
||||||
|
onActionView={() => setTabMode(subForm.code, 'view')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormView
|
||||||
|
{...commonProps}
|
||||||
|
id={subForm.id}
|
||||||
|
onActionNew={() => setTabMode(subForm.code, 'new')}
|
||||||
|
onActionEdit={() => setTabMode(subForm.code, 'edit')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
value={currentTab}
|
value={currentTab}
|
||||||
onChange={(val: string) => {
|
onChange={(value: string) => {
|
||||||
if (subForms.some((a) => a.code === val)) {
|
if (!subForms.some((subForm) => subForm.code === value)) return
|
||||||
setCurrentTab(val)
|
|
||||||
if (level === 0) {
|
setCurrentTab(value)
|
||||||
navigate(`#${val}`)
|
if (level === 0) {
|
||||||
}
|
navigate(`#${value}`)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
>
|
>
|
||||||
<TabList>
|
<TabList>
|
||||||
{subForms.map((subForm) => {
|
{subForms.map((subForm) => (
|
||||||
return (
|
<TabNav
|
||||||
<TabNav
|
key={subForm.code}
|
||||||
key={subForm.code}
|
value={subForm.code}
|
||||||
value={subForm.code}
|
icon={TAB_ICONS[subForm.tabType] ?? <FaChartBar />}
|
||||||
icon={subForm.tabType == SubFormTabTypeEnum.List ? <FaList /> : <FaChartBar />}
|
>
|
||||||
>
|
{translate('::' + subForm.tabTitle)}
|
||||||
{translate('::' + subForm.tabTitle)}
|
</TabNav>
|
||||||
</TabNav>
|
))}
|
||||||
)
|
|
||||||
})}
|
|
||||||
</TabList>
|
</TabList>
|
||||||
{subForms.map((subForm) => {
|
{subForms.map((subForm) => (
|
||||||
return (
|
<TabContent key={subForm.code} value={subForm.code} className="pt-1">
|
||||||
<TabContent key={subForm.code} value={subForm.code} className="pt-1">
|
{renderTabContent(subForm)}
|
||||||
{subForm.tabType == SubFormTabTypeEnum.List && (
|
</TabContent>
|
||||||
<Grid
|
))}
|
||||||
listFormCode={subForm.code}
|
|
||||||
searchParams={subForm.searchParams}
|
|
||||||
isSubForm={true}
|
|
||||||
level={level + 1}
|
|
||||||
refreshData={subForm.isRefresh ? refreshData : undefined}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{subForm.tabType == SubFormTabTypeEnum.Tree && (
|
|
||||||
<Tree
|
|
||||||
listFormCode={subForm.code}
|
|
||||||
searchParams={subForm.searchParams}
|
|
||||||
isSubForm={true}
|
|
||||||
level={level + 1}
|
|
||||||
refreshData={subForm.isRefresh ? refreshData : undefined}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{subForm.tabType == SubFormTabTypeEnum.Gantt && (
|
|
||||||
<GanttView
|
|
||||||
listFormCode={subForm.code}
|
|
||||||
searchParams={subForm.searchParams}
|
|
||||||
isSubForm={true}
|
|
||||||
level={level + 1}
|
|
||||||
refreshData={subForm.isRefresh ? refreshData : undefined}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{subForm.tabType == SubFormTabTypeEnum.Scheduler && (
|
|
||||||
<SchedulerView
|
|
||||||
listFormCode={subForm.code}
|
|
||||||
searchParams={subForm.searchParams}
|
|
||||||
isSubForm={true}
|
|
||||||
level={level + 1}
|
|
||||||
refreshData={subForm.isRefresh ? refreshData : undefined}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{subForm.tabType == SubFormTabTypeEnum.Form &&
|
|
||||||
(subForm.tabMode == 'edit' ? (
|
|
||||||
<FormEdit
|
|
||||||
onActionNew={() => {
|
|
||||||
setSubForms(
|
|
||||||
subForms.map((a) => {
|
|
||||||
if (a.code == subForm.code) {
|
|
||||||
a.tabMode = 'new'
|
|
||||||
}
|
|
||||||
return a
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
onActionView={() => {
|
|
||||||
setSubForms(
|
|
||||||
subForms.map((a) => {
|
|
||||||
if (a.code == subForm.code) {
|
|
||||||
a.tabMode = 'view'
|
|
||||||
}
|
|
||||||
return a
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
listFormCode={subForm.code}
|
|
||||||
id={subForm.id}
|
|
||||||
level={level + 1}
|
|
||||||
isSubForm={true}
|
|
||||||
/>
|
|
||||||
) : subForm.tabMode == 'new' ? (
|
|
||||||
<FormNew
|
|
||||||
listFormCode={subForm.code}
|
|
||||||
sParams={subForm.searchParams}
|
|
||||||
level={level + 1}
|
|
||||||
isSubForm={true}
|
|
||||||
onActionView={() => {
|
|
||||||
setSubForms(
|
|
||||||
subForms.map((a) => {
|
|
||||||
if (a.code == subForm.code) {
|
|
||||||
a.tabMode = 'view'
|
|
||||||
}
|
|
||||||
return a
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<FormView
|
|
||||||
onActionNew={() => {
|
|
||||||
setSubForms(
|
|
||||||
subForms.map((a) => {
|
|
||||||
if (a.code == subForm.code) {
|
|
||||||
a.tabMode = 'new'
|
|
||||||
}
|
|
||||||
return a
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
onActionEdit={() => {
|
|
||||||
setSubForms(
|
|
||||||
subForms.map((a) => {
|
|
||||||
if (a.code == subForm.code) {
|
|
||||||
a.tabMode = 'edit'
|
|
||||||
}
|
|
||||||
return a
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
listFormCode={subForm.code}
|
|
||||||
id={subForm.id}
|
|
||||||
level={level + 1}
|
|
||||||
isSubForm={true}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{subForm.tabType == SubFormTabTypeEnum.Chart && (
|
|
||||||
<Chart
|
|
||||||
id={gridDto?.gridOptions.id as string}
|
|
||||||
listFormCode={subForm.code}
|
|
||||||
isSubForm={true}
|
|
||||||
level={level + 1}
|
|
||||||
refreshData={subForm.isRefresh ? refreshData : undefined}
|
|
||||||
gridDto={gridDto}
|
|
||||||
refreshGridDto={refreshGridDto}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</TabContent>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ImageUploadOptionsDto } from '@/proxy/form/models'
|
import { ImageUploadOptionsDto } from '@/proxy/form/models'
|
||||||
import { ReactElement, useRef, useState } from 'react'
|
import { ReactElement, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { FaSpinner, FaUpload } from 'react-icons/fa'
|
import { FaSpinner, FaUpload } from 'react-icons/fa'
|
||||||
import { Button } from '@/components/ui'
|
import { Button } from '@/components/ui'
|
||||||
import {
|
import {
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
openImageInNewTab,
|
openImageInNewTab,
|
||||||
showImageHoverPreview,
|
showImageHoverPreview,
|
||||||
} from './imageHoverPreview'
|
} from './imageHoverPreview'
|
||||||
|
import { normalizeImageSize } from '../../list/shared/imageValue'
|
||||||
|
|
||||||
const ImageUploadEditorComponent = ({
|
const ImageUploadEditorComponent = ({
|
||||||
value,
|
value,
|
||||||
|
|
@ -28,19 +29,24 @@ const ImageUploadEditorComponent = ({
|
||||||
const isMultiple: boolean =
|
const isMultiple: boolean =
|
||||||
resolvedOptions.multiple === true || (resolvedOptions.multiple as any) === 'true'
|
resolvedOptions.multiple === true || (resolvedOptions.multiple as any) === 'true'
|
||||||
|
|
||||||
const thumbW: number = resolvedOptions.width ?? 40
|
const thumbW = normalizeImageSize(resolvedOptions.width, 40)
|
||||||
const thumbH: number = resolvedOptions.height ?? 40
|
const thumbH = normalizeImageSize(resolvedOptions.height, 40)
|
||||||
|
|
||||||
const initialUrls: string[] = value
|
const valueUrls = useMemo<string[]>(
|
||||||
? Array.isArray(value)
|
() => (Array.isArray(value) ? value : [value]).filter(Boolean) as string[],
|
||||||
? (value as string[]).filter(Boolean)
|
[value],
|
||||||
: [value as string].filter(Boolean)
|
)
|
||||||
: []
|
|
||||||
|
|
||||||
const [urls, setUrls] = useState<string[]>(initialUrls)
|
const [urls, setUrls] = useState<string[]>(valueUrls)
|
||||||
const [uploading, setUploading] = useState(false)
|
const [uploading, setUploading] = useState(false)
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
// Değer dışarıdan (editör script'i, veri yenileme…) değiştiğinde önizlemeler
|
||||||
|
// güncellenmiyordu; state yalnızca ilk render'da kuruluyordu.
|
||||||
|
useEffect(() => {
|
||||||
|
setUrls(valueUrls)
|
||||||
|
}, [valueUrls])
|
||||||
|
|
||||||
const removeImage = (index: number) => {
|
const removeImage = (index: number) => {
|
||||||
const newUrls = urls.filter((_, i) => i !== index)
|
const newUrls = urls.filter((_, i) => i !== index)
|
||||||
setUrls(newUrls)
|
setUrls(newUrls)
|
||||||
|
|
@ -76,8 +82,7 @@ const ImageUploadEditorComponent = ({
|
||||||
const res = await fetch(resolvedOptions.uploadUrl, { method: 'POST', body: formData })
|
const res = await fetch(resolvedOptions.uploadUrl, { method: 'POST', body: formData })
|
||||||
if (!res.ok) throw new Error(`Upload failed: ${res.status}`)
|
if (!res.ok) throw new Error(`Upload failed: ${res.status}`)
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
const url: string = data?.url ?? data?.fileUrl ?? data?.path ?? data
|
uploadedUrls.push(String(data?.url ?? data?.fileUrl ?? data?.path ?? data ?? '').trim())
|
||||||
uploadedUrls.push(url)
|
|
||||||
} else {
|
} else {
|
||||||
// uploadUrl yoksa base64 olarak sakla
|
// uploadUrl yoksa base64 olarak sakla
|
||||||
const base64 = await toBase64(file)
|
const base64 = await toBase64(file)
|
||||||
|
|
|
||||||
|
|
@ -6,91 +6,12 @@ import {
|
||||||
openImageInNewTab,
|
openImageInNewTab,
|
||||||
showImageHoverPreview,
|
showImageHoverPreview,
|
||||||
} from './imageHoverPreview'
|
} from './imageHoverPreview'
|
||||||
|
import {
|
||||||
const parseJsonObject = (value: unknown) => {
|
NO_IMAGE,
|
||||||
if (!value) return undefined
|
normalizeImageSize,
|
||||||
if (typeof value === 'object') return value as Record<string, any>
|
normalizeImageValue,
|
||||||
if (typeof value !== 'string') return undefined
|
parseJsonObject,
|
||||||
|
} from '../../list/shared/imageValue'
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(value)
|
|
||||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
||||||
? (parsed as Record<string, any>)
|
|
||||||
: undefined
|
|
||||||
} catch {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizeImageSize = (value: unknown, fallback: number) => {
|
|
||||||
const size = Number(value)
|
|
||||||
return Number.isFinite(size) && size > 0 ? size : fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
const getImageSource = (value: unknown) => {
|
|
||||||
if (!value) return ''
|
|
||||||
if (typeof value === 'string') return value.trim()
|
|
||||||
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
const item = value as Record<string, unknown>
|
|
||||||
return String(item.url ?? item.src ?? item.fileUrl ?? item.path ?? item.value ?? '').trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
return String(value).trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
const isProbablyBase64Image = (value: string) =>
|
|
||||||
value.length > 80 && /^[A-Za-z0-9+/]+={0,2}$/.test(value)
|
|
||||||
|
|
||||||
const toImageSource = (value: string) => {
|
|
||||||
const trimmed = value.trim()
|
|
||||||
if (!trimmed) return ''
|
|
||||||
if (trimmed.startsWith('data:image/') || !isProbablyBase64Image(trimmed)) return trimmed
|
|
||||||
return `data:image/jpeg;base64,${trimmed}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const splitImageString = (value: string) => {
|
|
||||||
const trimmed = value.trim()
|
|
||||||
if (!trimmed) return []
|
|
||||||
if (trimmed.startsWith('data:image/')) return [trimmed]
|
|
||||||
|
|
||||||
return trimmed
|
|
||||||
.split(/\r?\n|\|\s*/)
|
|
||||||
.flatMap((part) => {
|
|
||||||
const text = part.trim()
|
|
||||||
if (!text || text.startsWith('data:image/')) return text ? [text] : []
|
|
||||||
return text.split(',').map((item) => item.trim())
|
|
||||||
})
|
|
||||||
.map(toImageSource)
|
|
||||||
.filter(Boolean)
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizeImageValue = (value: unknown): string[] => {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value.flatMap((item) => normalizeImageValue(item))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
const trimmed = value.trim()
|
|
||||||
if (!trimmed) return []
|
|
||||||
|
|
||||||
if (trimmed.startsWith('[')) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(trimmed)
|
|
||||||
if (Array.isArray(parsed)) {
|
|
||||||
return parsed.flatMap((item) => normalizeImageValue(item))
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return [toImageSource(trimmed)]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return splitImageString(trimmed)
|
|
||||||
}
|
|
||||||
|
|
||||||
const source = getImageSource(value)
|
|
||||||
return source ? [toImageSource(source)] : []
|
|
||||||
}
|
|
||||||
|
|
||||||
const ImageViewerEditorComponent = ({
|
const ImageViewerEditorComponent = ({
|
||||||
value,
|
value,
|
||||||
|
|
@ -137,7 +58,7 @@ const ImageViewerEditorComponent = ({
|
||||||
}}
|
}}
|
||||||
onError={({ currentTarget }) => {
|
onError={({ currentTarget }) => {
|
||||||
currentTarget.onerror = null
|
currentTarget.onerror = null
|
||||||
currentTarget.src = '/img/others/no-image.png'
|
currentTarget.src = NO_IMAGE
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
const NO_IMAGE = '/img/others/no-image.png'
|
const NO_IMAGE = '/img/others/no-image.png'
|
||||||
|
|
||||||
|
/** Hem React SyntheticEvent hem de native MouseEvent ile çalışabilmesi için minimum sözleşme. */
|
||||||
|
type HoverPreviewEvent = {
|
||||||
|
clientX: number
|
||||||
|
clientY: number
|
||||||
|
currentTarget: EventTarget | null
|
||||||
|
}
|
||||||
|
|
||||||
let previewElement: HTMLDivElement | null = null
|
let previewElement: HTMLDivElement | null = null
|
||||||
let previewTarget: HTMLElement | null = null
|
let previewTarget: HTMLElement | null = null
|
||||||
let previewListenersBound = false
|
let previewListenersBound = false
|
||||||
|
|
@ -71,9 +76,9 @@ const getPreviewElement = () => {
|
||||||
return previewElement
|
return previewElement
|
||||||
}
|
}
|
||||||
|
|
||||||
export const showImageHoverPreview = (src: string, event: React.MouseEvent<HTMLElement>) => {
|
export const showImageHoverPreview = (src: string, event: HoverPreviewEvent) => {
|
||||||
const element = getPreviewElement()
|
const element = getPreviewElement()
|
||||||
previewTarget = event.currentTarget
|
previewTarget = event.currentTarget instanceof HTMLElement ? event.currentTarget : null
|
||||||
const image = element.querySelector('img') as HTMLImageElement
|
const image = element.querySelector('img') as HTMLImageElement
|
||||||
image.onerror = () => {
|
image.onerror = () => {
|
||||||
image.onerror = null
|
image.onerror = null
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,8 @@ import { FaFileAlt, FaFileUpload, FaPlus, FaTrash } from 'react-icons/fa'
|
||||||
import { Field, FieldProps, Form, Formik } from 'formik'
|
import { Field, FieldProps, Form, Formik } from 'formik'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
import { noteService } from '@/services/note.service'
|
import { noteService } from '@/services/note.service'
|
||||||
import {
|
import { HtmlEditor, ImageUpload, MediaResizing } from 'devextreme-react/html-editor'
|
||||||
sizeValues,
|
import { HtmlEditorToolbar } from '@/views/shared/HtmlEditorToolbarItems'
|
||||||
fontSizeOptions,
|
|
||||||
fontValues,
|
|
||||||
fontFamilyOptions,
|
|
||||||
headerValues,
|
|
||||||
headerOptions,
|
|
||||||
} from '@/proxy/reports/data'
|
|
||||||
import { HtmlEditor, ImageUpload, Item, MediaResizing, Toolbar } from 'devextreme-react/html-editor'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
|
||||||
const validationSchema = Yup.object({
|
const validationSchema = Yup.object({
|
||||||
|
|
@ -161,36 +154,7 @@ function NoteModalContent({
|
||||||
>
|
>
|
||||||
<MediaResizing enabled={true} />
|
<MediaResizing enabled={true} />
|
||||||
<ImageUpload fileUploadMode="base64" />
|
<ImageUpload fileUploadMode="base64" />
|
||||||
<Toolbar multiline>
|
<HtmlEditorToolbar />
|
||||||
<Item name="undo" />
|
|
||||||
<Item name="redo" />
|
|
||||||
<Item name="separator" />
|
|
||||||
<Item name="size" acceptedValues={sizeValues} options={fontSizeOptions} />
|
|
||||||
<Item name="font" acceptedValues={fontValues} options={fontFamilyOptions} />
|
|
||||||
<Item name="separator" />
|
|
||||||
<Item name="bold" />
|
|
||||||
<Item name="italic" />
|
|
||||||
<Item name="underline" />
|
|
||||||
<Item name="strike" />
|
|
||||||
<Item name="separator" />
|
|
||||||
<Item name="orderedList" />
|
|
||||||
<Item name="bulletList" />
|
|
||||||
<Item name="separator" />
|
|
||||||
<Item name="header" acceptedValues={headerValues} options={headerOptions} />
|
|
||||||
<Item name="separator" />
|
|
||||||
<Item name="color" />
|
|
||||||
<Item name="background" />
|
|
||||||
<Item name="separator" />
|
|
||||||
<Item name="alignLeft" />
|
|
||||||
<Item name="alignCenter" />
|
|
||||||
<Item name="alignRight" />
|
|
||||||
<Item name="alignJustify" />
|
|
||||||
<Item name="separator" />
|
|
||||||
<Item name="link" />
|
|
||||||
<Item name="image" />
|
|
||||||
<Item name="separator" />
|
|
||||||
<Item name="clear" />
|
|
||||||
</Toolbar>
|
|
||||||
</HtmlEditor>
|
</HtmlEditor>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
||||||
149
ui/src/views/form/shared/FormPageShell.tsx
Normal file
149
ui/src/views/form/shared/FormPageShell.tsx
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
/**
|
||||||
|
* FormView / FormEdit / FormNew ekranlarının ortak sayfa iskeleti.
|
||||||
|
*
|
||||||
|
* Üç bileşen de aynı düzeni (Helmet + başlık + FormButtons + FormDevExpress +
|
||||||
|
* SubForms + NotePanel) satır satır kopyalıyordu. Tek noktadan yönetilir.
|
||||||
|
*/
|
||||||
|
import type { ReactNode, RefObject } from 'react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Container, Loading } from '@/components/shared'
|
||||||
|
import { Badge } from '@/components/ui'
|
||||||
|
import { APP_NAME } from '@/constants/app.constant'
|
||||||
|
import type { GridDto } from '@/proxy/form/models'
|
||||||
|
import { useCurrentMenuIcon } from '@/utils/hooks/useCurrentMenuIcon'
|
||||||
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
import { usePermission } from '@/utils/hooks/usePermission'
|
||||||
|
import type { FormRef } from 'devextreme-react/form'
|
||||||
|
import type { GroupItem } from 'devextreme/ui/form'
|
||||||
|
import { Helmet } from 'react-helmet'
|
||||||
|
import FormDevExpress from '../FormDevExpress'
|
||||||
|
import SubForms from '../SubForms'
|
||||||
|
import { NotePanel } from '../notes/NotePanel'
|
||||||
|
import type { RowMode } from '../types'
|
||||||
|
|
||||||
|
type FormPageShellProps = {
|
||||||
|
mode: RowMode
|
||||||
|
listFormCode: string
|
||||||
|
isSubForm?: boolean
|
||||||
|
level?: number
|
||||||
|
loading: boolean
|
||||||
|
gridDto?: GridDto
|
||||||
|
formData: any
|
||||||
|
formItems: GroupItem[]
|
||||||
|
refForm: RefObject<FormRef>
|
||||||
|
setFormData: (data: any) => void
|
||||||
|
/** Başlığın sağındaki aksiyon butonları (FormButtons). */
|
||||||
|
actions?: ReactNode
|
||||||
|
/** Not paneli kimliği; verilmezse panel gösterilmez. */
|
||||||
|
noteEntityId?: string
|
||||||
|
/** Alt form sekmelerini yenileme geri çağrısı. */
|
||||||
|
refreshData?: () => Promise<void>
|
||||||
|
/** FormNew'de alt form gösterilmez. */
|
||||||
|
showSubForms?: boolean
|
||||||
|
/** Form gövdesinin sarmalayıcı sınıfı (FormView farklı dolgu kullanır). */
|
||||||
|
contentClassName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FormPageShell = ({
|
||||||
|
mode,
|
||||||
|
listFormCode,
|
||||||
|
isSubForm,
|
||||||
|
level,
|
||||||
|
loading,
|
||||||
|
gridDto,
|
||||||
|
formData,
|
||||||
|
formItems,
|
||||||
|
refForm,
|
||||||
|
setFormData,
|
||||||
|
actions,
|
||||||
|
noteEntityId,
|
||||||
|
refreshData,
|
||||||
|
showSubForms = true,
|
||||||
|
contentClassName = 'px-2',
|
||||||
|
}: FormPageShellProps) => {
|
||||||
|
const { translate } = useLocalization()
|
||||||
|
const { checkPermission } = usePermission()
|
||||||
|
const MenuIcon = useCurrentMenuIcon('w-5 h-5')
|
||||||
|
const [isNotePanelVisible, setIsNotePanelVisible] = useState(false)
|
||||||
|
|
||||||
|
const showNotePanel = Boolean(
|
||||||
|
listFormCode &&
|
||||||
|
noteEntityId &&
|
||||||
|
!isSubForm &&
|
||||||
|
gridDto?.gridOptions?.showNote &&
|
||||||
|
checkPermission(gridDto?.gridOptions.permissionDto.n),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!listFormCode) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Loading type="default" loading />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData) {
|
||||||
|
return <>{translate('::App.NoResults')}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = translate('::' + gridDto?.gridOptions.title)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Container
|
||||||
|
className={`${
|
||||||
|
isNotePanelVisible && !isSubForm ? 'lg:mr-[375px]' : ''
|
||||||
|
} transition-all duration-300`}
|
||||||
|
>
|
||||||
|
{!isSubForm && (
|
||||||
|
<Helmet titleTemplate={`%s | ${APP_NAME}`} title={title} defaultTitle={APP_NAME} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`flex items-center pb-2 px-2 ${isSubForm ? 'justify-end' : 'justify-between'}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{MenuIcon}
|
||||||
|
{!isSubForm && (
|
||||||
|
<>
|
||||||
|
<h4 className="text-slate-700 text-sm font-medium leading-none">{title}</h4>●
|
||||||
|
<Badge content={mode} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={contentClassName}>
|
||||||
|
<FormDevExpress
|
||||||
|
mode={mode}
|
||||||
|
refForm={refForm}
|
||||||
|
formData={formData}
|
||||||
|
formItems={formItems}
|
||||||
|
setFormData={setFormData}
|
||||||
|
listFormCode={listFormCode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showSubForms && gridDto && (
|
||||||
|
<SubForms
|
||||||
|
gridDto={gridDto}
|
||||||
|
formData={formData}
|
||||||
|
level={level ?? 0}
|
||||||
|
refreshData={refreshData}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
{/* Not paneli yalnızca ana formda gösterilir. */}
|
||||||
|
{showNotePanel && (
|
||||||
|
<NotePanel
|
||||||
|
entityName={listFormCode}
|
||||||
|
entityId={noteEntityId!}
|
||||||
|
isVisible={isNotePanelVisible}
|
||||||
|
onToggle={() => setIsNotePanelVisible((visible) => !visible)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
126
ui/src/views/form/shared/formItems.ts
Normal file
126
ui/src/views/form/shared/formItems.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
/**
|
||||||
|
* Form (FormNew/FormEdit/FormView) ekranlarının item üretimi ve gezinme
|
||||||
|
* yardımcıları. Daha önce `useFormData.tsx` ve `FormDevExpress.tsx` içinde
|
||||||
|
* ayrı ayrı tanımlıydı.
|
||||||
|
*/
|
||||||
|
import { captionize } from 'devextreme/core/utils/inflector'
|
||||||
|
import type { GroupItem } from 'devextreme/ui/form'
|
||||||
|
import type { ColumnFormatDto, GridDto } from '@/proxy/form/models'
|
||||||
|
import { getEditingFormGroups } from '../../list/Utils'
|
||||||
|
import { isFormItemVisible, resolveEditorType } from '../../list/shared/editingForm'
|
||||||
|
import type { GridColumnData } from '../../list/GridColumnData'
|
||||||
|
import type { RowMode, SimpleItemWithColData } from '../types'
|
||||||
|
|
||||||
|
/** Grup / tab ağacındaki tüm veri alanlarını düz listeye indirger. */
|
||||||
|
export const flattenFormItems = (items: any[] = []): SimpleItemWithColData[] =>
|
||||||
|
items.flatMap((item) => [
|
||||||
|
...(item?.dataField ? [item] : []),
|
||||||
|
...flattenFormItems(item?.items || []),
|
||||||
|
...(item?.tabs || []).flatMap((tab: any) => flattenFormItems(tab?.items || [])),
|
||||||
|
])
|
||||||
|
|
||||||
|
/** Bir grup listesindeki tüm alanları tek seferde düzleştirir. */
|
||||||
|
export const flattenFormGroups = (groups: GroupItem[] = []): SimpleItemWithColData[] =>
|
||||||
|
groups.flatMap((group) => flattenFormItems([group]))
|
||||||
|
|
||||||
|
/** Bir alanın cascade parent alan adlarını döner. */
|
||||||
|
export const getCascadeParentFields = (item: SimpleItemWithColData): string[] =>
|
||||||
|
item.colData?.lookupDto?.cascadeParentFields
|
||||||
|
?.split(',')
|
||||||
|
.map((field: string) => field.trim())
|
||||||
|
.filter(Boolean) ?? []
|
||||||
|
|
||||||
|
type BuildFormItemsParams = {
|
||||||
|
gridDto: GridDto
|
||||||
|
mode: RowMode
|
||||||
|
columns?: GridColumnData[]
|
||||||
|
/** Lookup alanları için DevExtreme dataSource üretici. */
|
||||||
|
getLookupDataSource: (options: any, colData: ColumnFormatDto, formData: any) => any
|
||||||
|
formData: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tek bir ColumnFormatDto'yu DevExtreme form item'ına çevirir. */
|
||||||
|
const toFormItem = (
|
||||||
|
colFormat: ColumnFormatDto,
|
||||||
|
{ mode, columns, getLookupDataSource, formData }: Omit<BuildFormItemsParams, 'gridDto'>,
|
||||||
|
): SimpleItemWithColData => {
|
||||||
|
let parsedEditorOptions: Record<string, any> = {}
|
||||||
|
try {
|
||||||
|
parsedEditorOptions = colFormat.editorOptions ? JSON.parse(colFormat.editorOptions) : {}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('EditorOptions parse error:', colFormat.fieldName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const lookupEditorOptions = colFormat.lookupDto?.dataSourceType
|
||||||
|
? {
|
||||||
|
dataSource: getLookupDataSource(colFormat.editorOptions, colFormat, formData),
|
||||||
|
valueExpr: colFormat.lookupDto?.valueExpr?.toLowerCase(),
|
||||||
|
displayExpr: colFormat.lookupDto?.displayExpr?.toLowerCase(),
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
|
||||||
|
const item: SimpleItemWithColData = {
|
||||||
|
canRead: colFormat.canRead ?? false,
|
||||||
|
canUpdate: colFormat.canUpdate ?? false,
|
||||||
|
canCreate: colFormat.canCreate ?? false,
|
||||||
|
canExport: colFormat.canExport ?? false,
|
||||||
|
allowEditing: colFormat.allowEditing ?? true,
|
||||||
|
allowAdding: colFormat.allowAdding ?? true,
|
||||||
|
dataField: colFormat.fieldName,
|
||||||
|
name: colFormat.fieldName,
|
||||||
|
editorType2: colFormat.editorType2,
|
||||||
|
editorType: resolveEditorType(colFormat.editorType2),
|
||||||
|
colSpan: colFormat.colSpan,
|
||||||
|
editorOptions: { ...lookupEditorOptions, ...parsedEditorOptions },
|
||||||
|
colData: colFormat,
|
||||||
|
tagBoxOptions: colFormat.tagBoxOptions,
|
||||||
|
gridBoxOptions: colFormat.gridBoxOptions,
|
||||||
|
imageUploadOptions: colFormat.imageUploadOptions,
|
||||||
|
editorScript: colFormat.editorScript,
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Options:TemplateName" biçimindeki JSON alt alanları için etiket üret.
|
||||||
|
if (colFormat.fieldName?.includes(':')) {
|
||||||
|
item.label = { text: captionize(colFormat.fieldName.split(':')[1]) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((mode === 'edit' && !item.canUpdate) || (mode === 'new' && !item.canCreate)) {
|
||||||
|
item.editorOptions = { ...item.editorOptions, readOnly: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
item.isRequired =
|
||||||
|
columns?.some(
|
||||||
|
(column) =>
|
||||||
|
column.dataField === colFormat.fieldName &&
|
||||||
|
colFormat.validationRuleDto?.some((rule) => rule.type === 'required'),
|
||||||
|
) ?? false
|
||||||
|
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EditingFormDto gruplarını DevExtreme `GroupItem` listesine çevirir.
|
||||||
|
* Alanlar `editOrderNo`'ya göre sıralanır ve moda göre yetki filtresinden geçer.
|
||||||
|
*/
|
||||||
|
export const buildFormGroupItems = ({
|
||||||
|
gridDto,
|
||||||
|
mode,
|
||||||
|
columns,
|
||||||
|
getLookupDataSource,
|
||||||
|
formData,
|
||||||
|
}: BuildFormItemsParams): GroupItem[] =>
|
||||||
|
getEditingFormGroups(gridDto).map(
|
||||||
|
(group: any) =>
|
||||||
|
({
|
||||||
|
itemType: group.itemType,
|
||||||
|
colCount: group.colCount,
|
||||||
|
colSpan: group.colSpan,
|
||||||
|
caption: group.caption,
|
||||||
|
items: [...(group.items ?? [])]
|
||||||
|
.sort((left, right) => (left.editOrderNo ?? 0) - (right.editOrderNo ?? 0))
|
||||||
|
.map((colFormat: ColumnFormatDto) =>
|
||||||
|
toFormItem(colFormat, { mode, columns, getLookupDataSource, formData }),
|
||||||
|
)
|
||||||
|
.filter((item) => isFormItemVisible(item, mode)),
|
||||||
|
}) as GroupItem,
|
||||||
|
)
|
||||||
|
|
@ -3,28 +3,25 @@ import { getList } from '@/services/form.service'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { usePermission } from '@/utils/hooks/usePermission'
|
import { usePermission } from '@/utils/hooks/usePermission'
|
||||||
import type { FormRef } from 'devextreme-react/form'
|
import type { FormRef } from 'devextreme-react/form'
|
||||||
import { captionize } from 'devextreme/core/utils/inflector'
|
|
||||||
import CustomStore from 'devextreme/data/custom_store'
|
import CustomStore from 'devextreme/data/custom_store'
|
||||||
import type { GroupItem } from 'devextreme/ui/form'
|
import type { GroupItem } from 'devextreme/ui/form'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
|
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { GridColumnData } from '../list/GridColumnData'
|
import { GridColumnData } from '../list/GridColumnData'
|
||||||
import { addCss, addJs, getEditingFormGroups } from '../list/Utils'
|
|
||||||
import { PermissionResults, RowMode, SimpleItemWithColData } from './types'
|
import { PermissionResults, RowMode, SimpleItemWithColData } from './types'
|
||||||
import { ColumnFormatDto, GridDto, PlatformEditorTypes } from '@/proxy/form/models'
|
import { GridDto } from '@/proxy/form/models'
|
||||||
import { getAccessDeniedPath } from '@/utils/routing'
|
import { getAccessDeniedPath } from '@/utils/routing'
|
||||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||||
import { useLookupDataSource } from './useLookupDataSource'
|
import { useLookupDataSource } from './useLookupDataSource'
|
||||||
import { layoutTypes } from '../admin/listForm/edit/types'
|
import { layoutTypes } from '../admin/listForm/edit/types'
|
||||||
import { useListFormCustomDataSource } from '../list/useListFormCustomDataSource'
|
import { useListFormCustomDataSource } from '../list/useListFormCustomDataSource'
|
||||||
import { useListFormColumns } from '../list/useListFormColumns'
|
import { useListFormColumns } from '../list/useListFormColumns'
|
||||||
|
import { useListFormCustomSources } from '../list/shared/hooks'
|
||||||
const flattenFormItems = (items: any[] = []): SimpleItemWithColData[] =>
|
import {
|
||||||
items.flatMap((item) => [
|
buildFormGroupItems,
|
||||||
...(item?.dataField ? [item] : []),
|
flattenFormGroups,
|
||||||
...flattenFormItems(item?.items || []),
|
getCascadeParentFields,
|
||||||
...(item?.tabs || []).flatMap((tab: any) => flattenFormItems(tab?.items || [])),
|
} from './shared/formItems'
|
||||||
])
|
|
||||||
|
|
||||||
const useGridData = (props: {
|
const useGridData = (props: {
|
||||||
mode: RowMode
|
mode: RowMode
|
||||||
|
|
@ -63,21 +60,15 @@ const useGridData = (props: {
|
||||||
const { createSelectDataSource } = useListFormCustomDataSource({} as any)
|
const { createSelectDataSource } = useListFormCustomDataSource({} as any)
|
||||||
const { getLookupDataSource } = useLookupDataSource({ listFormCode, isSubForm })
|
const { getLookupDataSource } = useLookupDataSource({ listFormCode, isSubForm })
|
||||||
|
|
||||||
|
useListFormCustomSources(gridDto)
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const response: any = await dataSource?.load({
|
const response: any = await dataSource?.load({ filter, skip: 0, take: 1 })
|
||||||
filter,
|
const record = response?.data?.[0]
|
||||||
skip: 0,
|
setFormData(record)
|
||||||
take: 1,
|
setFormDataOld(record ? { ...record } : undefined)
|
||||||
})
|
|
||||||
if (response?.data?.length) {
|
|
||||||
setFormData(response.data[0])
|
|
||||||
setFormDataOld({ ...response.data[0] })
|
|
||||||
} else {
|
|
||||||
setFormData(undefined)
|
|
||||||
setFormDataOld(undefined)
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(<Notification title={error.message} type="danger" />, {
|
toast.push(<Notification title={error.message} type="danger" />, {
|
||||||
placement: 'bottom-end',
|
placement: 'bottom-end',
|
||||||
|
|
@ -87,77 +78,71 @@ const useGridData = (props: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = async (e: any) => {
|
const showSaveSuccess = () =>
|
||||||
e.preventDefault()
|
toast.push(
|
||||||
if (!dataSource) {
|
<Notification type="success" duration={2000}>
|
||||||
return
|
{translate('::ListForms.FormBilgileriKaydedildi')}
|
||||||
|
</Notification>,
|
||||||
|
{ placement: 'bottom-end' },
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Kaydetme sonrası yönlendirme; alt formda üst bileşene haber verilir. */
|
||||||
|
const finishSubmit = (viewPath: string) => {
|
||||||
|
if (isSubForm) {
|
||||||
|
props.onSubmitAction?.()
|
||||||
|
} else {
|
||||||
|
navigate(viewPath)
|
||||||
|
}
|
||||||
|
showSaveSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Güncelleme isteğinin gövdesini hazırlar. */
|
||||||
|
const buildUpdateData = (formValues: Record<string, any>) => {
|
||||||
|
const sendOnlyChanged = gridDto?.gridOptions.editingOptionDto?.sendOnlyChangedFormValuesUpdate
|
||||||
|
const data =
|
||||||
|
sendOnlyChanged && formDataOld
|
||||||
|
? Object.fromEntries(
|
||||||
|
Object.keys(formValues)
|
||||||
|
.filter((key) => !Object.is(formValues[key], formDataOld[key]))
|
||||||
|
.map((key) => [key, formValues[key]]),
|
||||||
|
)
|
||||||
|
: { ...formValues }
|
||||||
|
|
||||||
|
if (gridDto?.gridOptions.keyFieldName) {
|
||||||
|
delete data[gridDto.gridOptions.keyFieldName]
|
||||||
}
|
}
|
||||||
|
|
||||||
const validationResult = refForm.current?.instance().validate()
|
return data
|
||||||
if (!validationResult?.isValid) {
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (e: any) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!dataSource) return
|
||||||
|
|
||||||
|
if (!refForm.current?.instance().validate()?.isValid) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const formValues = { ...formData }
|
const formValues = { ...formData }
|
||||||
|
|
||||||
if (mode === 'new') {
|
if (mode === 'new') {
|
||||||
const result = await dataSource.insert(formValues)
|
const result = await dataSource.insert(formValues)
|
||||||
if (result.data) {
|
if (!result?.data) {
|
||||||
if (!isSubForm) {
|
|
||||||
navigate(result.data)
|
|
||||||
} else if (props.onSubmitAction) {
|
|
||||||
props.onSubmitAction()
|
|
||||||
}
|
|
||||||
toast.push(
|
|
||||||
<Notification type="success" duration={2000}>
|
|
||||||
{translate('::ListForms.FormBilgileriKaydedildi')}
|
|
||||||
</Notification>,
|
|
||||||
{
|
|
||||||
placement: 'bottom-end',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
throw new Error(translate('::ListForms.FormBilgileriKaydedilemedi'))
|
throw new Error(translate('::ListForms.FormBilgileriKaydedilemedi'))
|
||||||
}
|
}
|
||||||
|
finishSubmit(result.data)
|
||||||
} else if (mode === 'edit') {
|
} else if (mode === 'edit') {
|
||||||
let data: any = {}
|
const result = await dataSource.update(id, buildUpdateData(formValues))
|
||||||
if (gridDto?.gridOptions.editingOptionDto?.sendOnlyChangedFormValuesUpdate) {
|
if (!(result?.data > 0)) {
|
||||||
Object.keys(formValues).forEach((key) => {
|
|
||||||
if (formValues[key] !== formDataOld[key]) {
|
|
||||||
data[key] = formValues[key]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
data = { ...formValues }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gridDto?.gridOptions.keyFieldName) {
|
|
||||||
delete data[gridDto?.gridOptions.keyFieldName]
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await dataSource.update(id, data)
|
|
||||||
if (result.data > 0) {
|
|
||||||
if (!isSubForm) {
|
|
||||||
navigate(
|
|
||||||
ROUTES_ENUM.protected.admin.formView
|
|
||||||
.replace(':listFormCode', encodeURIComponent(listFormCode))
|
|
||||||
.replace(':id', encodeURIComponent(id!)),
|
|
||||||
)
|
|
||||||
} else if (props.onSubmitAction) {
|
|
||||||
props.onSubmitAction()
|
|
||||||
}
|
|
||||||
toast.push(
|
|
||||||
<Notification type="success" duration={2000}>
|
|
||||||
{translate('::ListForms.FormBilgileriKaydedildi')}
|
|
||||||
</Notification>,
|
|
||||||
{
|
|
||||||
placement: 'bottom-end',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
throw new Error(translate('::ListForms.FormBilgileriKaydedilemedi'))
|
throw new Error(translate('::ListForms.FormBilgileriKaydedilemedi'))
|
||||||
}
|
}
|
||||||
|
finishSubmit(
|
||||||
|
ROUTES_ENUM.protected.admin.formView
|
||||||
|
.replace(':listFormCode', encodeURIComponent(listFormCode))
|
||||||
|
.replace(':id', encodeURIComponent(id!)),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(<Notification title={error.message} type="danger" />, {
|
toast.push(<Notification title={error.message} type="danger" />, {
|
||||||
|
|
@ -170,142 +155,64 @@ const useGridData = (props: {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setGridReady(false)
|
setGridReady(false)
|
||||||
const initializeGrid = async () => {
|
|
||||||
const response = await getList({ listFormCode })
|
|
||||||
setGridDto(response.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeGrid()
|
let isActive = true
|
||||||
|
getList({ listFormCode })
|
||||||
|
.then((response: any) => {
|
||||||
|
if (isActive) setGridDto(response.data)
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => console.error('GridDto load error:', error))
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isActive = false
|
||||||
|
}
|
||||||
}, [listFormCode])
|
}, [listFormCode])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setGridReady(false)
|
setGridReady(false)
|
||||||
if (!gridDto) {
|
if (!gridDto) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const { editingOptionDto, permissionDto } = gridDto.gridOptions
|
||||||
setPermissionResults({
|
setPermissionResults({
|
||||||
c:
|
c: editingOptionDto.allowAdding === true && checkPermission(permissionDto.c),
|
||||||
gridDto?.gridOptions.editingOptionDto.allowAdding === true &&
|
r: checkPermission(permissionDto.r),
|
||||||
checkPermission(gridDto?.gridOptions.permissionDto.c),
|
u: editingOptionDto.allowUpdating === true && checkPermission(permissionDto.u),
|
||||||
r: checkPermission(gridDto?.gridOptions.permissionDto.r),
|
d: editingOptionDto.allowDeleting === true && checkPermission(permissionDto.d),
|
||||||
u:
|
e: checkPermission(permissionDto.e),
|
||||||
gridDto?.gridOptions.editingOptionDto.allowUpdating === true &&
|
i: checkPermission(permissionDto.i),
|
||||||
checkPermission(gridDto?.gridOptions.permissionDto.u),
|
|
||||||
d:
|
|
||||||
gridDto?.gridOptions.editingOptionDto.allowDeleting === true &&
|
|
||||||
checkPermission(gridDto?.gridOptions.permissionDto.d),
|
|
||||||
e: checkPermission(gridDto?.gridOptions.permissionDto.e),
|
|
||||||
i: checkPermission(gridDto?.gridOptions.permissionDto.i),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Set js and css
|
const columns = getBandedColumns()
|
||||||
const grdOpt = gridDto.gridOptions
|
setCommandColumnData(columns?.find((column) => column.type === 'buttons'))
|
||||||
grdOpt.customJsSources.forEach(addJs)
|
|
||||||
grdOpt.customStyleSources.forEach(addCss)
|
|
||||||
|
|
||||||
// Set columns
|
setDataSource(
|
||||||
const cols = getBandedColumns()
|
createSelectDataSource(
|
||||||
setCommandColumnData(cols?.find((a) => a.type == 'buttons'))
|
gridDto.gridOptions,
|
||||||
|
listFormCode,
|
||||||
// Set data source
|
searchParams,
|
||||||
const dataSource: CustomStore<any, any> = createSelectDataSource(
|
layoutTypes.grid,
|
||||||
gridDto.gridOptions,
|
columns,
|
||||||
listFormCode,
|
),
|
||||||
searchParams,
|
|
||||||
layoutTypes.grid,
|
|
||||||
cols,
|
|
||||||
)
|
)
|
||||||
setDataSource(dataSource)
|
|
||||||
|
|
||||||
const items = getEditingFormGroups(gridDto)
|
setFormItems(
|
||||||
.map((e: any) => {
|
buildFormGroupItems({
|
||||||
return {
|
gridDto,
|
||||||
itemType: e.itemType,
|
mode,
|
||||||
colCount: e.colCount,
|
columns,
|
||||||
colSpan: e.colSpan,
|
getLookupDataSource,
|
||||||
caption: e.caption,
|
formData,
|
||||||
items: e.items
|
}),
|
||||||
?.sort((a: any, b: any) => {
|
)
|
||||||
return (a.editOrderNo ?? 0) >= (b.editOrderNo ?? 0) ? 1 : -1
|
|
||||||
})
|
|
||||||
.map((i: ColumnFormatDto) => {
|
|
||||||
let editorOptions: Record<string, any> = {}
|
|
||||||
let parsedEditorOptions: Record<string, any> = {}
|
|
||||||
const colData = i
|
|
||||||
|
|
||||||
parsedEditorOptions = i.editorOptions ? JSON.parse(i.editorOptions) : {}
|
|
||||||
const lookupEditorOptions = colData?.lookupDto?.dataSourceType
|
|
||||||
? {
|
|
||||||
dataSource: getLookupDataSource(colData?.editorOptions, colData, formData),
|
|
||||||
valueExpr: colData?.lookupDto?.valueExpr?.toLowerCase(),
|
|
||||||
displayExpr: colData?.lookupDto?.displayExpr?.toLowerCase(),
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
editorOptions = {
|
|
||||||
...lookupEditorOptions,
|
|
||||||
...parsedEditorOptions,
|
|
||||||
}
|
|
||||||
const item: SimpleItemWithColData = {
|
|
||||||
canRead: i.canRead ?? false,
|
|
||||||
canUpdate: i.canUpdate ?? false,
|
|
||||||
canCreate: i.canCreate ?? false,
|
|
||||||
canExport: i.canExport ?? false,
|
|
||||||
allowEditing: i.allowEditing ?? true,
|
|
||||||
allowAdding: i.allowAdding ?? true,
|
|
||||||
dataField: i.fieldName,
|
|
||||||
name: i.fieldName,
|
|
||||||
editorType2: i.editorType2,
|
|
||||||
editorType:
|
|
||||||
i.editorType2 == PlatformEditorTypes.dxGridBox
|
|
||||||
? 'dxDropDownBox'
|
|
||||||
: i.editorType2 == PlatformEditorTypes.dxImageUpload ||
|
|
||||||
i.editorType2 == PlatformEditorTypes.dxImageViewer
|
|
||||||
? undefined
|
|
||||||
: i.editorType2,
|
|
||||||
colSpan: i.colSpan,
|
|
||||||
editorOptions,
|
|
||||||
colData,
|
|
||||||
tagBoxOptions: i.tagBoxOptions,
|
|
||||||
gridBoxOptions: i.gridBoxOptions,
|
|
||||||
imageUploadOptions: i.imageUploadOptions,
|
|
||||||
editorScript: i.editorScript,
|
|
||||||
}
|
|
||||||
if (i.fieldName?.indexOf(':') >= 0) {
|
|
||||||
item.label = { text: captionize(i.fieldName.split(':')[1]) }
|
|
||||||
}
|
|
||||||
if ((mode == 'edit' && !item.canUpdate) || (mode == 'new' && !item.canCreate)) {
|
|
||||||
item.editorOptions = {
|
|
||||||
...item.editorOptions,
|
|
||||||
readOnly: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
item.isRequired =
|
|
||||||
cols?.some(
|
|
||||||
(c) =>
|
|
||||||
c.dataField === item.colData?.fieldName &&
|
|
||||||
item.colData?.validationRuleDto?.some((a) => a.type === 'required'),
|
|
||||||
) ?? false
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
.filter((a: any) => {
|
|
||||||
if (mode === 'view') {
|
|
||||||
return a.canRead
|
|
||||||
} else if (mode === 'new') {
|
|
||||||
return a.canCreate && a.allowAdding
|
|
||||||
} else if (mode === 'edit') {
|
|
||||||
return a.canUpdate && a.allowEditing
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
} as GroupItem
|
|
||||||
})
|
|
||||||
setFormItems(items)
|
|
||||||
setGridReady(true)
|
setGridReady(true)
|
||||||
|
// getBandedColumns/getLookupDataSource her render'da yeniden oluşabilir.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [gridDto])
|
}, [gridDto])
|
||||||
|
|
||||||
// formData değiştiğinde sadece etkilenen cascading lookup datasource'ları güncelle
|
/**
|
||||||
|
* formData değiştiğinde yalnızca etkilenen cascading lookup dataSource'larını
|
||||||
|
* yeniden üretir; tüm form item ağacını gereksiz yere yenilemez.
|
||||||
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!gridDto || !formItems.length) {
|
if (!gridDto || !formItems.length) {
|
||||||
previousFormDataRef.current = formData
|
previousFormDataRef.current = formData
|
||||||
|
|
@ -319,23 +226,26 @@ const useGridData = (props: {
|
||||||
)
|
)
|
||||||
: []
|
: []
|
||||||
|
|
||||||
const shouldRefreshLookup = (item: SimpleItemWithColData) => {
|
const shouldRefreshLookup = (item: SimpleItemWithColData) =>
|
||||||
const cascadeParentFields = item.colData?.lookupDto?.cascadeParentFields
|
!previousFormData ||
|
||||||
?.split(',')
|
getCascadeParentFields(item).some((field) => changedFields.includes(field))
|
||||||
.map((field: string) => field.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
|
|
||||||
return (
|
const hasAffectedLookup = flattenFormGroups(formItems).some(
|
||||||
!previousFormData ||
|
(item) => item.colData?.lookupDto?.dataSourceType && shouldRefreshLookup(item),
|
||||||
cascadeParentFields?.some((field: string) => changedFields.includes(field))
|
)
|
||||||
)
|
|
||||||
|
if (!hasAffectedLookup) {
|
||||||
|
previousFormDataRef.current = formData
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateItems = (items: any[] = []): any[] =>
|
const updateItems = (items: any[] = []): any[] =>
|
||||||
items.map((item) => {
|
items.map((item) => {
|
||||||
const colData = item.colData
|
const colData = item.colData
|
||||||
|
|
||||||
if (colData?.lookupDto?.dataSourceType && shouldRefreshLookup(item)) {
|
if (colData?.lookupDto?.dataSourceType && shouldRefreshLookup(item)) {
|
||||||
const currentDataSource = item.editorOptions?.dataSource
|
const currentDataSource = item.editorOptions?.dataSource
|
||||||
|
// Kullanıcı tarafından elle verilmiş (statik dizi vb.) dataSource korunur.
|
||||||
const keepCustomDataSource =
|
const keepCustomDataSource =
|
||||||
currentDataSource !== undefined && typeof currentDataSource?.load !== 'function'
|
currentDataSource !== undefined && typeof currentDataSource?.load !== 'function'
|
||||||
|
|
||||||
|
|
@ -343,7 +253,6 @@ const useGridData = (props: {
|
||||||
...item,
|
...item,
|
||||||
editorOptions: {
|
editorOptions: {
|
||||||
...item.editorOptions,
|
...item.editorOptions,
|
||||||
// formData null bile olsa getLookupDataSource çağrılmalı (null parametrelerle API çağrısı yapılacak)
|
|
||||||
dataSource: keepCustomDataSource
|
dataSource: keepCustomDataSource
|
||||||
? currentDataSource
|
? currentDataSource
|
||||||
: getLookupDataSource(colData?.editorOptions, colData, formData || null),
|
: getLookupDataSource(colData?.editorOptions, colData, formData || null),
|
||||||
|
|
@ -356,82 +265,66 @@ const useGridData = (props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item?.items?.length) {
|
if (item?.items?.length) {
|
||||||
return {
|
return { ...item, items: updateItems(item.items) }
|
||||||
...item,
|
|
||||||
items: updateItems(item.items),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item?.tabs?.length) {
|
if (item?.tabs?.length) {
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
tabs: item.tabs.map((tab: any) => ({
|
tabs: item.tabs.map((tab: any) => ({ ...tab, items: updateItems(tab.items) })),
|
||||||
...tab,
|
|
||||||
items: updateItems(tab.items),
|
|
||||||
})),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return item
|
return item
|
||||||
})
|
})
|
||||||
|
|
||||||
const hasAffectedLookup =
|
|
||||||
!previousFormData ||
|
|
||||||
formItems
|
|
||||||
.flatMap((group) => flattenFormItems([group]))
|
|
||||||
.some((item) => item.colData?.lookupDto?.dataSourceType && shouldRefreshLookup(item))
|
|
||||||
|
|
||||||
if (!hasAffectedLookup) {
|
|
||||||
previousFormDataRef.current = formData
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedItems = formItems.map((groupItem) => ({
|
|
||||||
...groupItem,
|
|
||||||
items: updateItems(groupItem.items as any[]),
|
|
||||||
tabs: (groupItem as any).tabs?.map((tab: any) => ({
|
|
||||||
...tab,
|
|
||||||
items: updateItems(tab.items),
|
|
||||||
})),
|
|
||||||
}))
|
|
||||||
|
|
||||||
previousFormDataRef.current = formData
|
previousFormDataRef.current = formData
|
||||||
setFormItems(updatedItems)
|
setFormItems((currentItems) =>
|
||||||
|
currentItems.map((groupItem) => ({
|
||||||
|
...groupItem,
|
||||||
|
items: updateItems(groupItem.items as any[]),
|
||||||
|
tabs: (groupItem as any).tabs?.map((tab: any) => ({
|
||||||
|
...tab,
|
||||||
|
items: updateItems(tab.items),
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [formData, gridDto])
|
}, [formData, gridDto])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!gridReady) {
|
if (!gridReady || mode === 'new') return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode !== 'new') {
|
if (id) {
|
||||||
if (id) {
|
setFilter([gridDto?.gridOptions.keyFieldName ?? 'Id', '=', id])
|
||||||
setFilter([gridDto?.gridOptions.keyFieldName ?? 'Id', '=', id])
|
} else {
|
||||||
} else {
|
// Form tipi liste formlar ID olmadan açılabilir; kayıt seçimi sunucudaki
|
||||||
// Form tipi listformlar ID olmadan açılabilir. Bu durumda kayıt seçimini
|
// DefaultFilter'a bırakılır.
|
||||||
// sunucudaki DefaultFilter'a bırakıyoruz.
|
fetchData()
|
||||||
fetchData()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [id, gridReady])
|
}, [id, gridReady])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (filter?.length) {
|
if (filter?.length) {
|
||||||
fetchData()
|
fetchData()
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [filter])
|
}, [filter])
|
||||||
|
|
||||||
// Auth check
|
// Yetki kontrolü
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!permissionResults) return
|
if (!permissionResults) return
|
||||||
|
|
||||||
const noCreate = mode === 'new' && !permissionResults.c
|
const denied =
|
||||||
const noUpdate = mode === 'edit' && !permissionResults.u
|
(mode === 'new' && !permissionResults.c) ||
|
||||||
const noRead = mode === 'view' && !permissionResults.r
|
(mode === 'edit' && !permissionResults.u) ||
|
||||||
|
(mode === 'view' && !permissionResults.r)
|
||||||
|
|
||||||
if (noCreate || noUpdate || noRead) {
|
if (denied) {
|
||||||
navigate(getAccessDeniedPath(location.pathname), { replace: true, state: { from: location } })
|
navigate(getAccessDeniedPath(location.pathname), { replace: true, state: { from: location } })
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [permissionResults])
|
}, [permissionResults])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,16 @@ import { UiLookupDataSourceTypeEnum } from '@/proxy/form/models'
|
||||||
import CustomStore from 'devextreme/data/custom_store'
|
import CustomStore from 'devextreme/data/custom_store'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
import { cachedLookupLoad } from '../shared/lookupCache'
|
||||||
|
|
||||||
const __lookupCache = new Map<string, Promise<any[]>>()
|
export { clearLookupCache } from '../shared/lookupCache'
|
||||||
|
|
||||||
const cachedLoader = (key: string, loader: () => Promise<any[]>) => {
|
/**
|
||||||
if (__lookupCache.has(key)) return __lookupCache.get(key)!
|
* Lookup sorgusu yalnızca ilgili liste formu ekranı açıkken çalışmalı.
|
||||||
const p = Promise.resolve()
|
* (SubForm'larda bu kontrol uygulanmaz.)
|
||||||
.then(() => loader())
|
*/
|
||||||
.then((res) => res ?? [])
|
const isLookupAllowed = (listFormCode?: string, isSubForm?: boolean) =>
|
||||||
.catch((err) => {
|
isSubForm || !listFormCode || window.location.pathname.includes(listFormCode)
|
||||||
__lookupCache.delete(key)
|
|
||||||
throw err
|
|
||||||
})
|
|
||||||
__lookupCache.set(key, p)
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
const createLookupStaticDataSource = (
|
const createLookupStaticDataSource = (
|
||||||
load: () => any,
|
load: () => any,
|
||||||
|
|
@ -28,13 +23,12 @@ const createLookupStaticDataSource = (
|
||||||
store: new CustomStore({
|
store: new CustomStore({
|
||||||
key,
|
key,
|
||||||
loadMode: 'raw',
|
loadMode: 'raw',
|
||||||
load: async () => {
|
load: () =>
|
||||||
const cacheKey = `static:${key}`
|
cachedLookupLoad(`static:${key}`, async () => {
|
||||||
return cachedLoader(cacheKey, async () => {
|
const result = await Promise.resolve(load())
|
||||||
const res = await Promise.resolve(load())
|
if (result === undefined || result === null) return []
|
||||||
return Array.isArray(res) ? res : [res]
|
return Array.isArray(result) ? result : [result]
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
sort,
|
sort,
|
||||||
filter,
|
filter,
|
||||||
|
|
@ -45,31 +39,38 @@ const createLookupQueryDataSource = (
|
||||||
listFormFieldName?: string,
|
listFormFieldName?: string,
|
||||||
filters?: any[],
|
filters?: any[],
|
||||||
isSubForm?: boolean,
|
isSubForm?: boolean,
|
||||||
) => {
|
) =>
|
||||||
return new CustomStore({
|
new CustomStore({
|
||||||
loadMode: 'raw',
|
loadMode: 'raw',
|
||||||
load: async () => {
|
load: async () => {
|
||||||
if (!isSubForm && listFormCode && !window.location.pathname.includes(listFormCode)) {
|
if (!isLookupAllowed(listFormCode, isSubForm)) {
|
||||||
return
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = `query:${listFormCode}:${listFormFieldName}:${JSON.stringify(filters ?? null)}`
|
try {
|
||||||
return cachedLoader(cacheKey, async () => {
|
const cacheKey = `query:${listFormCode}:${listFormFieldName}:${JSON.stringify(filters ?? null)}`
|
||||||
const response = await dynamicFetch('list-form-select/lookup', 'POST', null, {
|
return await cachedLookupLoad(cacheKey, async () => {
|
||||||
listFormCode,
|
const response = await dynamicFetch('list-form-select/lookup', 'POST', null, {
|
||||||
listFormFieldName,
|
listFormCode,
|
||||||
filters,
|
listFormFieldName,
|
||||||
|
filters,
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = response?.data ?? []
|
||||||
|
return (Array.isArray(data) ? data : [data]).map((item: any) => ({
|
||||||
|
key: item.Key,
|
||||||
|
name: item.Name,
|
||||||
|
group: item.Group,
|
||||||
|
|
||||||
|
...item,
|
||||||
|
}))
|
||||||
})
|
})
|
||||||
const data = response?.data ?? []
|
} catch (error) {
|
||||||
return (Array.isArray(data) ? data : [data]).map((a: any) => ({
|
console.error('Lookup query data could not be loaded:', error)
|
||||||
key: a.Key,
|
return []
|
||||||
name: a.Name,
|
}
|
||||||
group: a.Group,
|
|
||||||
}))
|
|
||||||
}).catch(() => null as any)
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
const createLookupApiDataSource = (
|
const createLookupApiDataSource = (
|
||||||
listFormCode?: string,
|
listFormCode?: string,
|
||||||
|
|
@ -77,19 +78,15 @@ const createLookupApiDataSource = (
|
||||||
filters?: any[],
|
filters?: any[],
|
||||||
keyName?: string,
|
keyName?: string,
|
||||||
isSubForm?: boolean,
|
isSubForm?: boolean,
|
||||||
) => {
|
) =>
|
||||||
return new CustomStore({
|
new CustomStore({
|
||||||
key: keyName,
|
key: keyName,
|
||||||
loadMode: 'raw',
|
loadMode: 'raw',
|
||||||
load: async () => {
|
load: async () => {
|
||||||
if (!isSubForm && listFormCode && !window.location.pathname.includes(listFormCode)) {
|
if (!isLookupAllowed(listFormCode, isSubForm) || !lookupQuery) {
|
||||||
return
|
return []
|
||||||
}
|
|
||||||
if (!lookupQuery) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = lookupQuery.split(';')
|
|
||||||
const [
|
const [
|
||||||
method = 'GET',
|
method = 'GET',
|
||||||
url = '',
|
url = '',
|
||||||
|
|
@ -97,30 +94,40 @@ const createLookupApiDataSource = (
|
||||||
keySelector = 'a=>a.id',
|
keySelector = 'a=>a.id',
|
||||||
nameSelector = 'a=>a.name',
|
nameSelector = 'a=>a.name',
|
||||||
groupSelector = 'a=>a.group',
|
groupSelector = 'a=>a.group',
|
||||||
] = parts
|
] = lookupQuery.split(';')
|
||||||
|
|
||||||
|
// Gövdedeki @param0, @param1… yer tutucularını cascade filtreleriyle doldur.
|
||||||
let resolvedBody = bodyTemplate ?? ''
|
let resolvedBody = bodyTemplate ?? ''
|
||||||
if (filters?.length) {
|
filters?.forEach((filterValue, index) => {
|
||||||
for (let i = 0; i < filters.length; i++) {
|
resolvedBody = resolvedBody.replace(new RegExp(`@param${index}`, 'g'), String(filterValue))
|
||||||
resolvedBody = resolvedBody.replace(new RegExp(`@param${i}`, 'g'), String(filters[i]))
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cacheKey = `api:${lookupQuery}:${JSON.stringify(filters ?? null)}`
|
try {
|
||||||
return cachedLoader(cacheKey, async () => {
|
const cacheKey = `api:${lookupQuery}:${JSON.stringify(filters ?? null)}`
|
||||||
const response = await dynamicFetch(url, method, null, resolvedBody)
|
return await cachedLookupLoad(cacheKey, async () => {
|
||||||
let { data } = response
|
const response = await dynamicFetch(url, method, null, resolvedBody)
|
||||||
if (!data) return []
|
let { data } = response
|
||||||
if (!Array.isArray(data)) data = [data]
|
if (!data) return []
|
||||||
return data.map(() => ({
|
if (!Array.isArray(data)) data = [data]
|
||||||
key: eval(keySelector),
|
|
||||||
name: eval(nameSelector),
|
// NOT: Selector'lar `a=>a.id` biçiminde yapılandırmadan gelir; bu yüzden
|
||||||
group: eval(groupSelector),
|
// map parametresi `a` olarak adlandırılmalıdır. Önceden parametresiz
|
||||||
}))
|
// `() => ...` kullanıldığı için eval fonksiyonun kendisini döndürüyor ve
|
||||||
}).catch(() => [] as any)
|
// key/name/group alanları bozuk geliyordu.
|
||||||
|
return data.map((a: any) => ({
|
||||||
|
key: eval(keySelector),
|
||||||
|
name: eval(nameSelector),
|
||||||
|
group: eval(groupSelector),
|
||||||
|
|
||||||
|
...a,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Lookup API data could not be loaded:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
export const useLookupDataSource = ({
|
export const useLookupDataSource = ({
|
||||||
listFormCode,
|
listFormCode,
|
||||||
|
|
@ -136,7 +143,7 @@ export const useLookupDataSource = ({
|
||||||
const { lookupDto } = colData
|
const { lookupDto } = colData
|
||||||
const filters: any[] = []
|
const filters: any[] = []
|
||||||
|
|
||||||
// Cascading field'ler için - data yoksa veya parent field değeri yoksa null parametreler ile API çağrısı yap
|
// Cascading alanlar: parent değeri yoksa null gönderilir, istek yine de yapılır.
|
||||||
if (lookupDto.cascadeParentFields) {
|
if (lookupDto.cascadeParentFields) {
|
||||||
if (lookupDto.dataSourceType == UiLookupDataSourceTypeEnum.StaticData) {
|
if (lookupDto.dataSourceType == UiLookupDataSourceTypeEnum.StaticData) {
|
||||||
filters.push([
|
filters.push([
|
||||||
|
|
@ -146,34 +153,34 @@ export const useLookupDataSource = ({
|
||||||
])
|
])
|
||||||
} else {
|
} else {
|
||||||
for (const cascadeParentField of lookupDto.cascadeParentFields.split(',')) {
|
for (const cascadeParentField of lookupDto.cascadeParentFields.split(',')) {
|
||||||
filters.push(data?.[cascadeParentField] ?? null)
|
filters.push(data?.[cascadeParentField.trim()] ?? null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lookupDto.dataSourceType == UiLookupDataSourceTypeEnum.StaticData) {
|
switch (lookupDto.dataSourceType) {
|
||||||
const staticKey = `static:${cultureName}:${listFormCode}:${colData.fieldName}`
|
case UiLookupDataSourceTypeEnum.StaticData:
|
||||||
return createLookupStaticDataSource(
|
return createLookupStaticDataSource(
|
||||||
() =>
|
() =>
|
||||||
JSON.parse(lookupDto?.lookupQuery).map((item: any) => ({
|
JSON.parse(lookupDto?.lookupQuery).map((item: any) => ({
|
||||||
...item,
|
...item,
|
||||||
name: typeof item.name === 'string' ? translate(`::${item.name}`) : item.name,
|
name: typeof item.name === 'string' ? translate(`::${item.name}`) : item.name,
|
||||||
})),
|
})),
|
||||||
filters.length ? filters : null,
|
filters.length ? filters : null,
|
||||||
staticKey,
|
`static:${cultureName}:${listFormCode}:${colData.fieldName}`,
|
||||||
)
|
)
|
||||||
} else if (lookupDto.dataSourceType == UiLookupDataSourceTypeEnum.Query) {
|
case UiLookupDataSourceTypeEnum.Query:
|
||||||
return createLookupQueryDataSource(listFormCode, colData.fieldName, filters, isSubForm)
|
return createLookupQueryDataSource(listFormCode, colData.fieldName, filters, isSubForm)
|
||||||
} else if (lookupDto.dataSourceType == UiLookupDataSourceTypeEnum.WebService) {
|
case UiLookupDataSourceTypeEnum.WebService:
|
||||||
return createLookupApiDataSource(
|
return createLookupApiDataSource(
|
||||||
listFormCode,
|
listFormCode,
|
||||||
lookupDto?.lookupQuery,
|
lookupDto?.lookupQuery,
|
||||||
filters,
|
filters,
|
||||||
colData.lookupDto?.valueExpr?.toLowerCase(),
|
colData.lookupDto?.valueExpr?.toLowerCase(),
|
||||||
isSubForm,
|
isSubForm,
|
||||||
)
|
)
|
||||||
} else {
|
default:
|
||||||
return { store: [] }
|
return { store: [] }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[listFormCode, isSubForm, translate, cultureName],
|
[listFormCode, isSubForm, translate, cultureName],
|
||||||
|
|
@ -181,9 +188,3 @@ export const useLookupDataSource = ({
|
||||||
|
|
||||||
return { getLookupDataSource }
|
return { getLookupDataSource }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Opsiyonel: dışarıdan cache temizlemek istersen export et */
|
|
||||||
export const clearLookupCache = (key?: string) => {
|
|
||||||
if (!key) __lookupCache.clear()
|
|
||||||
else __lookupCache.delete(key)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,16 @@ import {
|
||||||
GridExtraFilterState,
|
GridExtraFilterState,
|
||||||
safeJsonParse,
|
safeJsonParse,
|
||||||
} from './Utils'
|
} from './Utils'
|
||||||
|
import {
|
||||||
|
buildEditPopupToolbarItems,
|
||||||
|
getEditPopupHeight,
|
||||||
|
getEditPopupMaxHeight,
|
||||||
|
getMobilePopupPosition,
|
||||||
|
shouldUseMobileEditPopup,
|
||||||
|
} from './shared/editPopup'
|
||||||
|
import { getValueByField, resolveEditorType } from './shared/editingForm'
|
||||||
|
import { flattenColumns } from './shared/columns'
|
||||||
|
import { getInitialExtraFilters, mergeFilterTriplets } from './shared/hooks'
|
||||||
import { useFilters } from './useFilters'
|
import { useFilters } from './useFilters'
|
||||||
import { useListFormColumns } from './useListFormColumns'
|
import { useListFormColumns } from './useListFormColumns'
|
||||||
import { useListFormCustomDataSource } from './useListFormCustomDataSource'
|
import { useListFormCustomDataSource } from './useListFormCustomDataSource'
|
||||||
|
|
@ -71,28 +81,6 @@ interface CardViewProps {
|
||||||
const cardColumnCountOptions = [1, 2, 3, 4, 5] as const
|
const cardColumnCountOptions = [1, 2, 3, 4, 5] as const
|
||||||
const defaultCardColumnCount = 3
|
const defaultCardColumnCount = 3
|
||||||
|
|
||||||
const flattenColumns = (columns: any[] = []): any[] =>
|
|
||||||
columns.flatMap((column) => (column?.columns?.length ? flattenColumns(column.columns) : [column]))
|
|
||||||
|
|
||||||
const isTouchLikeDevice = () =>
|
|
||||||
typeof window !== 'undefined' &&
|
|
||||||
(window.matchMedia?.('(pointer: coarse)').matches || window.matchMedia?.('(hover: none)').matches)
|
|
||||||
|
|
||||||
const isMobileViewport = () =>
|
|
||||||
typeof window !== 'undefined' && window.matchMedia?.('(max-width: 767px)').matches
|
|
||||||
|
|
||||||
const getPopupHeight = (useMobilePopup: boolean, fullScreen: boolean) =>
|
|
||||||
useMobilePopup && fullScreen ? '100%' : 'auto'
|
|
||||||
|
|
||||||
const getPopupMaxHeight = (
|
|
||||||
useMobilePopup: boolean,
|
|
||||||
fullScreen: boolean,
|
|
||||||
configuredHeight?: number,
|
|
||||||
) => {
|
|
||||||
if (useMobilePopup && fullScreen) return '100%'
|
|
||||||
return configuredHeight && configuredHeight > 0 ? configuredHeight : '90vh'
|
|
||||||
}
|
|
||||||
|
|
||||||
const toCardColumn = (
|
const toCardColumn = (
|
||||||
column: any,
|
column: any,
|
||||||
lookupDisplayValuesRef: { current: Record<string, Map<string, string>> },
|
lookupDisplayValuesRef: { current: Record<string, Map<string, string>> },
|
||||||
|
|
@ -171,13 +159,6 @@ const toCardColumn = (
|
||||||
) as CardViewTypes.ColumnProperties
|
) as CardViewTypes.ColumnProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
const getValueByField = (data: Record<string, any>, field?: string | null) => {
|
|
||||||
if (!field) return undefined
|
|
||||||
if (Object.prototype.hasOwnProperty.call(data, field)) return data[field]
|
|
||||||
const key = Object.keys(data).find((item) => item.toLowerCase() === field.toLowerCase())
|
|
||||||
return key ? data[key] : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const getEditingFieldName = (dataField?: string | null) =>
|
const getEditingFieldName = (dataField?: string | null) =>
|
||||||
String(dataField ?? '')
|
String(dataField ?? '')
|
||||||
.split(':')[0]
|
.split(':')[0]
|
||||||
|
|
@ -202,14 +183,6 @@ const lookupDisplayValuesEqual = (
|
||||||
const isGridDtoForList = (gridDto: GridDto | undefined, listFormCode: string) =>
|
const isGridDtoForList = (gridDto: GridDto | undefined, listFormCode: string) =>
|
||||||
gridDto?.gridOptions.listFormCode?.toLowerCase() === listFormCode.toLowerCase()
|
gridDto?.gridOptions.listFormCode?.toLowerCase() === listFormCode.toLowerCase()
|
||||||
|
|
||||||
const getInitialExtraFilters = (gridDto?: GridDto): GridExtraFilterState[] =>
|
|
||||||
(gridDto?.gridOptions.extraFilterDto ?? []).map((filter) => ({
|
|
||||||
fieldName: filter.fieldName,
|
|
||||||
operator: filter.operator,
|
|
||||||
controlType: filter.controlType,
|
|
||||||
value: filter.defaultValue ?? '',
|
|
||||||
}))
|
|
||||||
|
|
||||||
const loadCascadeLookupDisplayValues = async (
|
const loadCascadeLookupDisplayValues = async (
|
||||||
columns: any[],
|
columns: any[],
|
||||||
rows: Record<string, any>[],
|
rows: Record<string, any>[],
|
||||||
|
|
@ -384,7 +357,7 @@ const CardView = (props: CardViewProps) => {
|
||||||
(state) => state.abpConfig.config?.localization.currentCulture.cultureName,
|
(state) => state.abpConfig.config?.localization.currentCulture.cultureName,
|
||||||
)
|
)
|
||||||
const localizationTexts = useStoreState((state) => state.abpConfig.texts)
|
const localizationTexts = useStoreState((state) => state.abpConfig.texts)
|
||||||
const useMobileEditPopup = isMobileViewport() || isTouchLikeDevice()
|
const useMobileEditPopup = shouldUseMobileEditPopup()
|
||||||
const cardViewRef = useRef<CardViewRef>()
|
const cardViewRef = useRef<CardViewRef>()
|
||||||
const searchParamsRef = useRef(searchParams)
|
const searchParamsRef = useRef(searchParams)
|
||||||
const defaultSearchFilterRef = useRef(searchParams?.get('filter') ?? null)
|
const defaultSearchFilterRef = useRef(searchParams?.get('filter') ?? null)
|
||||||
|
|
@ -418,31 +391,20 @@ const CardView = (props: CardViewProps) => {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const mergedExtraFilter = useMemo(() => {
|
const mergedExtraFilter = useMemo(
|
||||||
const base = safeJsonParse(defaultSearchFilterRef.current, null, 'Search filter parse error:')
|
() =>
|
||||||
const triplets = [
|
mergeFilterTriplets([
|
||||||
...extractSearchParamsFields(base),
|
...extractSearchParamsFields(
|
||||||
...extraFilters
|
safeJsonParse(defaultSearchFilterRef.current, null, 'Search filter parse error:'),
|
||||||
.filter(
|
|
||||||
(filter) => filter.value !== undefined && filter.value !== null && filter.value !== '',
|
|
||||||
)
|
|
||||||
.map(
|
|
||||||
(filter) =>
|
|
||||||
[filter.fieldName, filter.operator, filter.value] as [string, string, unknown],
|
|
||||||
),
|
),
|
||||||
]
|
...extraFilters
|
||||||
const unique = triplets.reduce(
|
.filter((filter) => !!filter.value)
|
||||||
(result, current) => {
|
.map(
|
||||||
const index = result.findIndex((item) => item[0] === current[0] && item[1] === current[1])
|
(filter) => [filter.fieldName, filter.operator, filter.value] as [string, string, any],
|
||||||
if (index >= 0) result[index] = current
|
),
|
||||||
else result.push(current)
|
]),
|
||||||
return result
|
[extraFilters],
|
||||||
},
|
)
|
||||||
[] as [string, string, unknown][],
|
|
||||||
)
|
|
||||||
|
|
||||||
return unique.reduce<any>((result, filter) => (result ? [result, 'and', filter] : filter), null)
|
|
||||||
}, [extraFilters])
|
|
||||||
|
|
||||||
if (!dataSourceSearchParamsRef.current) {
|
if (!dataSourceSearchParamsRef.current) {
|
||||||
dataSourceSearchParamsRef.current = new URLSearchParams(searchParams?.toString())
|
dataSourceSearchParamsRef.current = new URLSearchParams(searchParams?.toString())
|
||||||
|
|
@ -552,7 +514,7 @@ const CardView = (props: CardViewProps) => {
|
||||||
listColumnsCacheRef.current = {
|
listColumnsCacheRef.current = {
|
||||||
gridDto,
|
gridDto,
|
||||||
localizationRevision,
|
localizationRevision,
|
||||||
columns: flattenColumns(getBandedColumns()),
|
columns: flattenColumns(getBandedColumns() ?? []),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const listColumns = listColumnsCacheRef.current.columns
|
const listColumns = listColumnsCacheRef.current.columns
|
||||||
|
|
@ -869,13 +831,7 @@ const CardView = (props: CardViewProps) => {
|
||||||
? item.fieldName!.split(':')[1]
|
? item.fieldName!.split(':')[1]
|
||||||
: translate('::' + (column.captionName || column.fieldName)),
|
: translate('::' + (column.captionName || column.fieldName)),
|
||||||
},
|
},
|
||||||
editorType:
|
editorType: resolveEditorType(item.editorType2),
|
||||||
item.editorType2 === PlatformEditorTypes.dxGridBox
|
|
||||||
? 'dxDropDownBox'
|
|
||||||
: item.editorType2 === PlatformEditorTypes.dxImageUpload ||
|
|
||||||
item.editorType2 === PlatformEditorTypes.dxImageViewer
|
|
||||||
? undefined
|
|
||||||
: item.editorType2,
|
|
||||||
editorOptions,
|
editorOptions,
|
||||||
template:
|
template:
|
||||||
item.editorType2 === PlatformEditorTypes.dxImageUpload
|
item.editorType2 === PlatformEditorTypes.dxImageUpload
|
||||||
|
|
@ -1219,57 +1175,24 @@ const CardView = (props: CardViewProps) => {
|
||||||
? editingOptions?.popup?.width
|
? editingOptions?.popup?.width
|
||||||
: 'auto',
|
: 'auto',
|
||||||
maxWidth: useMobileEditPopup ? '100%' : '95vw',
|
maxWidth: useMobileEditPopup ? '100%' : '95vw',
|
||||||
height: getPopupHeight(useMobileEditPopup, isPopupFullScreen),
|
height: getEditPopupHeight(useMobileEditPopup, isPopupFullScreen),
|
||||||
maxHeight: getPopupMaxHeight(
|
maxHeight: getEditPopupMaxHeight(
|
||||||
useMobileEditPopup,
|
useMobileEditPopup,
|
||||||
isPopupFullScreen,
|
isPopupFullScreen,
|
||||||
editingOptions?.popup?.height,
|
editingOptions?.popup?.height,
|
||||||
),
|
),
|
||||||
fullScreen: isPopupFullScreen,
|
fullScreen: isPopupFullScreen,
|
||||||
position: useMobileEditPopup
|
position: useMobileEditPopup
|
||||||
? {
|
? getMobilePopupPosition()
|
||||||
my: 'top center',
|
|
||||||
at: 'top center',
|
|
||||||
of: typeof window !== 'undefined' ? window : undefined,
|
|
||||||
}
|
|
||||||
: editingOptions?.popup?.position,
|
: editingOptions?.popup?.position,
|
||||||
resizeEnabled: editingOptions?.popup?.resizeEnabled,
|
resizeEnabled: editingOptions?.popup?.resizeEnabled,
|
||||||
dragEnabled: editingOptions?.popup?.dragEnabled,
|
dragEnabled: editingOptions?.popup?.dragEnabled,
|
||||||
restorePosition: editingOptions?.popup?.restorePosition,
|
restorePosition: editingOptions?.popup?.restorePosition,
|
||||||
toolbarItems: [
|
toolbarItems: buildEditPopupToolbarItems(translate, isPopupFullScreen, {
|
||||||
{
|
onSave: () => cardViewRef.current?.instance().saveEditData(),
|
||||||
widget: 'dxButton',
|
onCancel: () => cardViewRef.current?.instance().cancelEditData(),
|
||||||
toolbar: 'bottom',
|
onToggleFullScreen: () => setIsPopupFullScreen((current) => !current),
|
||||||
location: 'after',
|
}),
|
||||||
options: {
|
|
||||||
text: translate('::Save'),
|
|
||||||
type: 'default',
|
|
||||||
onClick: () => cardViewRef.current?.instance().saveEditData(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
widget: 'dxButton',
|
|
||||||
toolbar: 'bottom',
|
|
||||||
location: 'after',
|
|
||||||
options: {
|
|
||||||
text: translate('::Cancel'),
|
|
||||||
onClick: () => cardViewRef.current?.instance().cancelEditData(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
widget: 'dxButton',
|
|
||||||
toolbar: 'top',
|
|
||||||
location: 'after',
|
|
||||||
options: {
|
|
||||||
icon: isPopupFullScreen ? 'collapse' : 'fullscreen',
|
|
||||||
hint: isPopupFullScreen
|
|
||||||
? translate('::Normal Boyut')
|
|
||||||
: translate('::Tam Ekran'),
|
|
||||||
stylingMode: 'text',
|
|
||||||
onClick: () => setIsPopupFullScreen((current) => !current),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Toolbar visible>
|
<Toolbar visible>
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,7 @@ import Gantt, {
|
||||||
} from 'devextreme-react/gantt'
|
} from 'devextreme-react/gantt'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { Helmet } from 'react-helmet'
|
import { Helmet } from 'react-helmet'
|
||||||
import { getList } from '@/services/form.service'
|
|
||||||
import { useListFormCustomDataSource } from './useListFormCustomDataSource'
|
import { useListFormCustomDataSource } from './useListFormCustomDataSource'
|
||||||
import { addCss, addJs } from './Utils'
|
|
||||||
import { layoutTypes } from '../admin/listForm/edit/types'
|
import { layoutTypes } from '../admin/listForm/edit/types'
|
||||||
import WidgetGroup from '@/components/ui/Widget/WidgetGroup'
|
import WidgetGroup from '@/components/ui/Widget/WidgetGroup'
|
||||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||||
|
|
@ -34,6 +32,11 @@ import { useListFormStateStoring } from './useListFormStateStoring'
|
||||||
import TreeList from 'devextreme/ui/tree_list'
|
import TreeList from 'devextreme/ui/tree_list'
|
||||||
import { postListFormCustomization } from '@/services/list-form-customization.service'
|
import { postListFormCustomization } from '@/services/list-form-customization.service'
|
||||||
import { useFilters } from './useFilters'
|
import { useFilters } from './useFilters'
|
||||||
|
import {
|
||||||
|
useListFormCustomSources,
|
||||||
|
useListFormGridDto,
|
||||||
|
useWidgetGroupHeight,
|
||||||
|
} from './shared/hooks'
|
||||||
import GridFilterDialogs from './GridFilterDialogs'
|
import GridFilterDialogs from './GridFilterDialogs'
|
||||||
|
|
||||||
type GanttInstance = ReturnType<GanttRef['instance']>
|
type GanttInstance = ReturnType<GanttRef['instance']>
|
||||||
|
|
@ -68,20 +71,21 @@ const GanttView = (props: GanttViewProps) => {
|
||||||
const isPwaMode = usePWA()
|
const isPwaMode = usePWA()
|
||||||
|
|
||||||
const gridRef = useRef<GanttRef>()
|
const gridRef = useRef<GanttRef>()
|
||||||
const refListFormCode = useRef('')
|
|
||||||
const widgetGroupRef = useRef<HTMLDivElement>(null)
|
const widgetGroupRef = useRef<HTMLDivElement>(null)
|
||||||
const { checkPermission } = usePermission()
|
const { checkPermission } = usePermission()
|
||||||
|
|
||||||
const [ganttDataSource, setGanttDataSource] = useState<CustomStore<any, any>>()
|
const [ganttDataSource, setGanttDataSource] = useState<CustomStore<any, any>>()
|
||||||
const [columnData, setColumnData] = useState<GridColumnData[]>()
|
const [columnData, setColumnData] = useState<GridColumnData[]>()
|
||||||
const [gridDto, setGridDto] = useState<GridDto>()
|
const gridDto = useListFormGridDto(listFormCode, extGridDto)
|
||||||
const [widgetGroupHeight, setWidgetGroupHeight] = useState(0)
|
|
||||||
const [scaleType, setScaleType] = useState<GanttScaleType>('weeks')
|
const [scaleType, setScaleType] = useState<GanttScaleType>('weeks')
|
||||||
const [taskListWidth, setTaskListWidth] = useState(500)
|
const [taskListWidth, setTaskListWidth] = useState(500)
|
||||||
const layout = layoutTypes.gantt || 'gantt'
|
const layout = layoutTypes.gantt || 'gantt'
|
||||||
const pendingStateRef = useRef<Record<string, any>>()
|
const pendingStateRef = useRef<Record<string, any>>()
|
||||||
const loadedStateKeyRef = useRef('')
|
const loadedStateKeyRef = useRef('')
|
||||||
|
|
||||||
|
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, gridDto?.widgets)
|
||||||
|
useListFormCustomSources(gridDto)
|
||||||
|
|
||||||
const { customSaveState, customLoadState, storageKey } = useListFormStateStoring({
|
const { customSaveState, customLoadState, storageKey } = useListFormStateStoring({
|
||||||
listFormCode,
|
listFormCode,
|
||||||
storageKey: gridDto?.gridOptions.stateStoringDto?.storageKey,
|
storageKey: gridDto?.gridOptions.stateStoringDto?.storageKey,
|
||||||
|
|
@ -103,31 +107,12 @@ const GanttView = (props: GanttViewProps) => {
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initializeGantt = async () => {
|
setScaleType(gridDto?.gridOptions.ganttOptionDto?.scaleType || 'weeks')
|
||||||
const response = await getList({ listFormCode })
|
}, [gridDto])
|
||||||
setGridDto(response.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (extGridDto === undefined) {
|
|
||||||
initializeGantt()
|
|
||||||
} else {
|
|
||||||
setGridDto(extGridDto)
|
|
||||||
}
|
|
||||||
|
|
||||||
setScaleType(extGridDto?.gridOptions.ganttOptionDto?.scaleType || 'weeks')
|
|
||||||
}, [listFormCode, extGridDto])
|
|
||||||
|
|
||||||
|
// listFormCode değişiminde eski veri kaynağını temizle.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (gridRef?.current) {
|
gridRef.current?.instance()?.option('dataSource', undefined)
|
||||||
const instance = gridRef?.current?.instance()
|
|
||||||
if (instance) {
|
|
||||||
instance.option('dataSource', undefined)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (refListFormCode.current !== listFormCode) {
|
|
||||||
// Reset state if needed
|
|
||||||
}
|
|
||||||
}, [listFormCode])
|
}, [listFormCode])
|
||||||
|
|
||||||
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef })
|
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef })
|
||||||
|
|
@ -137,24 +122,6 @@ const GanttView = (props: GanttViewProps) => {
|
||||||
isSubForm,
|
isSubForm,
|
||||||
gridRef,
|
gridRef,
|
||||||
})
|
})
|
||||||
useEffect(() => {
|
|
||||||
if (!gridDto) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set js and css
|
|
||||||
const grdOpt = gridDto.gridOptions
|
|
||||||
if (grdOpt.customJsSources.length) {
|
|
||||||
for (const js of grdOpt.customJsSources) {
|
|
||||||
addJs(js)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (grdOpt.customStyleSources.length) {
|
|
||||||
for (const css of grdOpt.customStyleSources) {
|
|
||||||
addCss(css)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [gridDto])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!gridDto) return
|
if (!gridDto) return
|
||||||
|
|
@ -173,31 +140,6 @@ const GanttView = (props: GanttViewProps) => {
|
||||||
setGanttDataSource(dataSource)
|
setGanttDataSource(dataSource)
|
||||||
}, [createSelectDataSource, getBandedColumns, gridDto, layout, listFormCode, searchParams])
|
}, [createSelectDataSource, getBandedColumns, gridDto, layout, listFormCode, searchParams])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
refListFormCode.current = listFormCode
|
|
||||||
}, [listFormCode])
|
|
||||||
|
|
||||||
// WidgetGroup yüksekliğini hesapla
|
|
||||||
useEffect(() => {
|
|
||||||
const calculateWidgetHeight = () => {
|
|
||||||
if (widgetGroupRef.current) {
|
|
||||||
const height = widgetGroupRef.current.offsetHeight
|
|
||||||
setWidgetGroupHeight(height)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateWidgetHeight()
|
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(calculateWidgetHeight)
|
|
||||||
if (widgetGroupRef.current) {
|
|
||||||
resizeObserver.observe(widgetGroupRef.current)
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
resizeObserver.disconnect()
|
|
||||||
}
|
|
||||||
}, [gridDto?.widgets])
|
|
||||||
|
|
||||||
const settingButtonClick = useCallback(() => {
|
const settingButtonClick = useCallback(() => {
|
||||||
window.open(
|
window.open(
|
||||||
ROUTES_ENUM.protected.saas.listFormManagement.edit.replace(':listFormCode', listFormCode),
|
ROUTES_ENUM.protected.saas.listFormManagement.edit.replace(':listFormCode', listFormCode),
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -11,7 +11,7 @@ export function GridExtraFilterToolbar({
|
||||||
extraFilters: GridExtraFilterState[]
|
extraFilters: GridExtraFilterState[]
|
||||||
setExtraFilters: React.Dispatch<React.SetStateAction<GridExtraFilterState[]>>
|
setExtraFilters: React.Dispatch<React.SetStateAction<GridExtraFilterState[]>>
|
||||||
}) {
|
}) {
|
||||||
// input için geçici state tutuyoruz
|
// Metin girişleri yazarken değil, Enter/blur anında uygulanır.
|
||||||
const [inputValues, setInputValues] = useState<Record<string, string>>({})
|
const [inputValues, setInputValues] = useState<Record<string, string>>({})
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
|
|
||||||
|
|
@ -21,8 +21,11 @@ export function GridExtraFilterToolbar({
|
||||||
const current = extraFilters.find((f) => f.fieldName === fs.fieldName)
|
const current = extraFilters.find((f) => f.fieldName === fs.fieldName)
|
||||||
|
|
||||||
const handleSave = (value: string) => {
|
const handleSave = (value: string) => {
|
||||||
|
// Değer değişmediyse gereksiz yeniden yükleme tetikleme.
|
||||||
|
if ((current?.value ?? '') === value) return
|
||||||
|
|
||||||
setExtraFilters((prev) => {
|
setExtraFilters((prev) => {
|
||||||
const exists = prev.find((f) => f.fieldName === fs.fieldName)
|
const exists = prev.some((f) => f.fieldName === fs.fieldName)
|
||||||
if (exists) {
|
if (exists) {
|
||||||
return prev.map((f) => (f.fieldName === fs.fieldName ? { ...f, value } : f))
|
return prev.map((f) => (f.fieldName === fs.fieldName ? { ...f, value } : f))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,14 +25,11 @@ import type { Field } from 'devextreme/ui/pivot_grid/data_source'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Helmet } from 'react-helmet'
|
import { Helmet } from 'react-helmet'
|
||||||
import { GridColumnData } from './GridColumnData'
|
import { GridColumnData } from './GridColumnData'
|
||||||
import {
|
import { pivotFieldConvertDataType, setGridPanelColor } from './Utils'
|
||||||
addCss,
|
import { createConditionalCellStyleHandler } from './shared/cellStyles'
|
||||||
addJs,
|
import { useListFormCustomSources, useListFormGridDto } from './shared/hooks'
|
||||||
controlStyleCondition,
|
import { exportPivotView } from './shared/exportListView'
|
||||||
pivotFieldConvertDataType,
|
import WidgetGroup from '@/components/ui/Widget/WidgetGroup'
|
||||||
setGridPanelColor,
|
|
||||||
} from './Utils'
|
|
||||||
import WidgetGroup from '@/components/common/WidgetGroup'
|
|
||||||
import { Notification, toast } from '@/components/ui'
|
import { Notification, toast } from '@/components/ui'
|
||||||
import { usePermission } from '@/utils/hooks/usePermission'
|
import { usePermission } from '@/utils/hooks/usePermission'
|
||||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||||
|
|
@ -52,20 +49,21 @@ interface PivotProps {
|
||||||
level?: number
|
level?: number
|
||||||
refreshData?: () => Promise<void>
|
refreshData?: () => Promise<void>
|
||||||
gridDto?: GridDto
|
gridDto?: GridDto
|
||||||
refreshGridDto: () => Promise<void>
|
refreshGridDto?: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
type LookupDisplayValues = Record<string, Map<string, string>>
|
type LookupDisplayValues = Record<string, Map<string, string>>
|
||||||
|
|
||||||
const Pivot = (props: PivotProps) => {
|
const Pivot = (props: PivotProps) => {
|
||||||
const { listFormCode, searchParams, isSubForm, gridDto } = props
|
const { listFormCode, searchParams, isSubForm, gridDto: extGridDto } = props
|
||||||
|
// Alt form olarak açıldığında tanım dışarıdan gelmez; kendisi yükler.
|
||||||
|
const gridDto = useListFormGridDto(listFormCode, extGridDto)
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
const { checkPermission } = usePermission()
|
const { checkPermission } = usePermission()
|
||||||
const isPwaMode = usePWA()
|
const isPwaMode = usePWA()
|
||||||
|
|
||||||
const gridRef = useRef<PivotGridRef>()
|
const gridRef = useRef<PivotGridRef>()
|
||||||
const chartRef = useRef<ChartRef>(null)
|
const chartRef = useRef<ChartRef>(null)
|
||||||
const refListFormCode = useRef('')
|
|
||||||
const widgetGroupRef = useRef<HTMLDivElement>(null)
|
const widgetGroupRef = useRef<HTMLDivElement>(null)
|
||||||
const config = useStoreState((state) => state.abpConfig.config)
|
const config = useStoreState((state) => state.abpConfig.config)
|
||||||
|
|
||||||
|
|
@ -81,6 +79,8 @@ const Pivot = (props: PivotProps) => {
|
||||||
showSaveToast: false,
|
showSaveToast: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useListFormCustomSources(gridDto)
|
||||||
|
|
||||||
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef })
|
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef })
|
||||||
const { getBandedColumns, loadLookupDisplayValues } = useListFormColumns({
|
const { getBandedColumns, loadLookupDisplayValues } = useListFormColumns({
|
||||||
gridDto,
|
gridDto,
|
||||||
|
|
@ -89,45 +89,7 @@ const Pivot = (props: PivotProps) => {
|
||||||
gridRef,
|
gridRef,
|
||||||
})
|
})
|
||||||
|
|
||||||
const onCellPrepared = useCallback(
|
const onCellPrepared = useMemo(() => createConditionalCellStyleHandler(gridDto), [gridDto])
|
||||||
(e: any) => {
|
|
||||||
const columnFormats = gridDto?.columnFormats
|
|
||||||
if (!columnFormats) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// satir, hucre yada header vb. kisimlara conditional style uygulamak icin
|
|
||||||
for (let indxCol = 0; indxCol < columnFormats.length; indxCol++) {
|
|
||||||
const colFormat = columnFormats[indxCol]
|
|
||||||
for (let indxStyl = 0; indxStyl < colFormat.columnStylingDto.length; indxStyl++) {
|
|
||||||
const colStyle = colFormat.columnStylingDto[indxStyl] // uygulanacak style
|
|
||||||
if (e.rowType == colStyle.rowType) {
|
|
||||||
// header, filter, data, group, summaries ..her birisine style uygulanabilir
|
|
||||||
// style bütün satıra uygulansın olarak seçili ise yada sadece ilgili field üzerinde ise
|
|
||||||
if (colStyle.useRow || e.column?.dataField == colFormat.fieldName) {
|
|
||||||
if (
|
|
||||||
!colStyle.conditionValue ||
|
|
||||||
controlStyleCondition(e.data, colFormat.fieldName, colStyle)
|
|
||||||
) {
|
|
||||||
// css sınıf ismi var ise uygula
|
|
||||||
if (colStyle.cssClassName) {
|
|
||||||
e.cellElement.addClass(colStyle.cssClassName)
|
|
||||||
}
|
|
||||||
// css inline style var ise uygula
|
|
||||||
if (colStyle.cssStyles) {
|
|
||||||
e.cellElement.attr(
|
|
||||||
'style',
|
|
||||||
e.cellElement.attr('style') + ';' + colStyle.cssStyles,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[gridDto],
|
|
||||||
)
|
|
||||||
|
|
||||||
const clearPivotFilters = useCallback(() => {
|
const clearPivotFilters = useCallback(() => {
|
||||||
const grid = gridRef.current?.instance()
|
const grid = gridRef.current?.instance()
|
||||||
|
|
@ -175,42 +137,23 @@ const Pivot = (props: PivotProps) => {
|
||||||
}, [listFormCode, storageKey, clearPivotFilters, translate])
|
}, [listFormCode, storageKey, clearPivotFilters, translate])
|
||||||
|
|
||||||
const onExporting = useCallback(
|
const onExporting = useCallback(
|
||||||
async (e: PivotGridTypes.ExportingEvent) => {
|
(e: PivotGridTypes.ExportingEvent) => {
|
||||||
e.cancel = true
|
e.cancel = true
|
||||||
|
|
||||||
const pivot = gridRef?.current?.instance()
|
const pivot = gridRef.current?.instance()
|
||||||
if (!pivot) return
|
if (!pivot) return
|
||||||
|
|
||||||
try {
|
void exportPivotView({
|
||||||
// PivotGrid sadece Excel export destekliyor
|
component: pivot,
|
||||||
const [{ Workbook }, { saveAs }, { exportPivotGrid }] = await Promise.all([
|
fileName: listFormCode,
|
||||||
import('devextreme-exceljs-fork'),
|
onError: () =>
|
||||||
import('file-saver'),
|
toast.push(
|
||||||
import('devextreme/excel_exporter'),
|
<Notification type="danger" duration={2500}>
|
||||||
])
|
{translate('::App.Common.ExportError') ?? 'Dışa aktarma sırasında hata oluştu.'}
|
||||||
|
</Notification>,
|
||||||
const workbook = new Workbook()
|
{ placement: 'bottom-end' },
|
||||||
const worksheet = workbook.addWorksheet(`${listFormCode}_pivot`)
|
),
|
||||||
|
})
|
||||||
await exportPivotGrid({
|
|
||||||
component: pivot as any,
|
|
||||||
worksheet,
|
|
||||||
})
|
|
||||||
|
|
||||||
const buffer = await workbook.xlsx.writeBuffer()
|
|
||||||
saveAs(
|
|
||||||
new Blob([buffer], { type: 'application/octet-stream' }),
|
|
||||||
`${listFormCode}_pivot_export.xlsx`,
|
|
||||||
)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Pivot export error:', err)
|
|
||||||
toast.push(
|
|
||||||
<Notification type="danger" duration={2500}>
|
|
||||||
{translate('::App.Common.ExportError') ?? 'Dışa aktarma sırasında hata oluştu.'}
|
|
||||||
</Notification>,
|
|
||||||
{ placement: 'bottom-end' },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[listFormCode, translate],
|
[listFormCode, translate],
|
||||||
)
|
)
|
||||||
|
|
@ -253,30 +196,9 @@ const Pivot = (props: PivotProps) => {
|
||||||
)
|
)
|
||||||
}, [isPwaMode, listFormCode])
|
}, [isPwaMode, listFormCode])
|
||||||
|
|
||||||
|
// Grafiğin başlangıç görünürlüğü
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refListFormCode.current = listFormCode
|
setShowChart(gridDto?.gridOptions.pivotOptionDto.showChart ?? false)
|
||||||
}, [listFormCode])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!gridDto) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set js and css
|
|
||||||
const grdOpt = gridDto.gridOptions
|
|
||||||
if (grdOpt.customJsSources.length) {
|
|
||||||
for (const js of grdOpt.customJsSources) {
|
|
||||||
addJs(js)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (grdOpt.customStyleSources.length) {
|
|
||||||
for (const css of grdOpt.customStyleSources) {
|
|
||||||
addCss(css)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set initial chart visibility
|
|
||||||
setShowChart(gridDto.gridOptions.pivotOptionDto.showChart ?? false)
|
|
||||||
}, [gridDto])
|
}, [gridDto])
|
||||||
|
|
||||||
// Kolonları memoize et
|
// Kolonları memoize et
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import {
|
||||||
DbTypeEnum,
|
DbTypeEnum,
|
||||||
FieldCustomValueTypeEnum,
|
FieldCustomValueTypeEnum,
|
||||||
GridDto,
|
GridDto,
|
||||||
PlatformEditorTypes,
|
|
||||||
} from '@/proxy/form/models'
|
} from '@/proxy/form/models'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import Scheduler, {
|
import Scheduler, {
|
||||||
|
|
@ -19,9 +18,9 @@ import Scheduler, {
|
||||||
} from 'devextreme-react/scheduler'
|
} from 'devextreme-react/scheduler'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Helmet } from 'react-helmet'
|
import { Helmet } from 'react-helmet'
|
||||||
import { getList, getNextSequenceValue } from '@/services/form.service'
|
import { getNextSequenceValue } from '@/services/form.service'
|
||||||
import { useListFormCustomDataSource } from './useListFormCustomDataSource'
|
import { useListFormCustomDataSource } from './useListFormCustomDataSource'
|
||||||
import { addCss, addJs, autoNumber, getEditingFormGroups } from './Utils'
|
import { autoNumber, getEditingFormGroups } from './Utils'
|
||||||
import { layoutTypes } from '../admin/listForm/edit/types'
|
import { layoutTypes } from '../admin/listForm/edit/types'
|
||||||
import WidgetGroup from '@/components/ui/Widget/WidgetGroup'
|
import WidgetGroup from '@/components/ui/Widget/WidgetGroup'
|
||||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||||
|
|
@ -38,7 +37,19 @@ import { FaAlignLeft, FaClock, FaHeading } from 'react-icons/fa'
|
||||||
import { Avatar } from '@/components/ui'
|
import { Avatar } from '@/components/ui'
|
||||||
import { getUsers } from '@/services/identity.service'
|
import { getUsers } from '@/services/identity.service'
|
||||||
import { useListFormColumns } from './useListFormColumns'
|
import { useListFormColumns } from './useListFormColumns'
|
||||||
import type { GridColumnData } from './GridColumnData'
|
import {
|
||||||
|
getEditPopupHeight,
|
||||||
|
getEditPopupMaxHeight,
|
||||||
|
getMobilePopupPosition,
|
||||||
|
shouldUseMobileEditPopup,
|
||||||
|
} from './shared/editPopup'
|
||||||
|
import { getValueByField, resolveEditorType } from './shared/editingForm'
|
||||||
|
import { flattenGridColumns } from './shared/columns'
|
||||||
|
import {
|
||||||
|
useListFormCustomSources,
|
||||||
|
useListFormGridDto,
|
||||||
|
useWidgetGroupHeight,
|
||||||
|
} from './shared/hooks'
|
||||||
|
|
||||||
interface SchedulerViewProps {
|
interface SchedulerViewProps {
|
||||||
listFormCode: string
|
listFormCode: string
|
||||||
|
|
@ -49,63 +60,25 @@ interface SchedulerViewProps {
|
||||||
gridDto?: GridDto
|
gridDto?: GridDto
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTouchLikeDevice = () =>
|
|
||||||
typeof window !== 'undefined' &&
|
|
||||||
(window.matchMedia?.('(pointer: coarse)').matches || window.matchMedia?.('(hover: none)').matches)
|
|
||||||
|
|
||||||
const isMobileViewport = () =>
|
|
||||||
typeof window !== 'undefined' && window.matchMedia?.('(max-width: 767px)').matches
|
|
||||||
|
|
||||||
const getEditPopupHeight = (useMobileEditPopup: boolean, isPopupFullScreen: boolean) =>
|
|
||||||
useMobileEditPopup && isPopupFullScreen ? '100%' : 'auto'
|
|
||||||
|
|
||||||
const getEditPopupMaxHeight = (
|
|
||||||
useMobileEditPopup: boolean,
|
|
||||||
isPopupFullScreen: boolean,
|
|
||||||
configuredHeight?: number,
|
|
||||||
) => {
|
|
||||||
if (useMobileEditPopup && isPopupFullScreen) {
|
|
||||||
return '100%'
|
|
||||||
}
|
|
||||||
|
|
||||||
return configuredHeight && configuredHeight > 0 ? configuredHeight : '90vh'
|
|
||||||
}
|
|
||||||
|
|
||||||
const getValueByField = (data: Record<string, any>, field?: string) => {
|
|
||||||
if (!field) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolvedField = Object.keys(data).find(
|
|
||||||
(dataField) => dataField.toLowerCase() === field.toLowerCase(),
|
|
||||||
)
|
|
||||||
|
|
||||||
return resolvedField ? data[resolvedField] : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const flattenColumns = (columns: GridColumnData[] = []): GridColumnData[] =>
|
|
||||||
columns.flatMap((column) =>
|
|
||||||
column.columns?.length ? flattenColumns(column.columns as GridColumnData[]) : [column],
|
|
||||||
)
|
|
||||||
|
|
||||||
const SchedulerView = (props: SchedulerViewProps) => {
|
const SchedulerView = (props: SchedulerViewProps) => {
|
||||||
const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props
|
const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
const isPwaMode = usePWA()
|
const isPwaMode = usePWA()
|
||||||
const useMobileEditPopup = isMobileViewport() || isTouchLikeDevice()
|
const useMobileEditPopup = shouldUseMobileEditPopup()
|
||||||
const schedulerRef = useRef<SchedulerRef>()
|
const schedulerRef = useRef<SchedulerRef>()
|
||||||
const refListFormCode = useRef('')
|
|
||||||
const widgetGroupRef = useRef<HTMLDivElement>(null)
|
const widgetGroupRef = useRef<HTMLDivElement>(null)
|
||||||
const { checkPermission } = usePermission()
|
const { checkPermission } = usePermission()
|
||||||
|
|
||||||
const [schedulerDataSource, setSchedulerDataSource] = useState<CustomStore<any, any>>()
|
const [schedulerDataSource, setSchedulerDataSource] = useState<CustomStore<any, any>>()
|
||||||
const [gridDto, setGridDto] = useState<GridDto>()
|
const gridDto = useListFormGridDto(listFormCode, extGridDto)
|
||||||
const [widgetGroupHeight, setWidgetGroupHeight] = useState(0)
|
|
||||||
const [currentView, setCurrentView] = useState<string>('week')
|
const [currentView, setCurrentView] = useState<string>('week')
|
||||||
const [isPopupFullScreen, setIsPopupFullScreen] = useState(false)
|
const [isPopupFullScreen, setIsPopupFullScreen] = useState(false)
|
||||||
const [userAvatarUrls, setUserAvatarUrls] = useState<Record<string, string>>({})
|
const [userAvatarUrls, setUserAvatarUrls] = useState<Record<string, string>>({})
|
||||||
const layout = layoutTypes.scheduler || 'scheduler'
|
const layout = layoutTypes.scheduler || 'scheduler'
|
||||||
|
|
||||||
|
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, gridDto?.widgets)
|
||||||
|
useListFormCustomSources(gridDto)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
|
|
||||||
|
|
@ -136,31 +109,12 @@ const SchedulerView = (props: SchedulerViewProps) => {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initializeScheduler = async () => {
|
setCurrentView(gridDto?.gridOptions.schedulerOptionDto?.defaultView || 'week')
|
||||||
const response = await getList({ listFormCode })
|
}, [gridDto])
|
||||||
setGridDto(response.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (extGridDto === undefined) {
|
|
||||||
initializeScheduler()
|
|
||||||
} else {
|
|
||||||
setGridDto(extGridDto)
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentView(extGridDto?.gridOptions.schedulerOptionDto?.defaultView || 'week')
|
|
||||||
}, [listFormCode, extGridDto])
|
|
||||||
|
|
||||||
|
// listFormCode değişiminde eski veri kaynağını temizle.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (schedulerRef?.current) {
|
schedulerRef.current?.instance()?.option('dataSource', undefined)
|
||||||
const instance = schedulerRef?.current?.instance()
|
|
||||||
if (instance) {
|
|
||||||
instance.option('dataSource', undefined)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (refListFormCode.current !== listFormCode) {
|
|
||||||
// Reset state if needed
|
|
||||||
}
|
|
||||||
}, [listFormCode])
|
}, [listFormCode])
|
||||||
|
|
||||||
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef: schedulerRef })
|
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef: schedulerRef })
|
||||||
|
|
@ -170,26 +124,7 @@ const SchedulerView = (props: SchedulerViewProps) => {
|
||||||
isSubForm,
|
isSubForm,
|
||||||
gridRef: schedulerRef,
|
gridRef: schedulerRef,
|
||||||
})
|
})
|
||||||
const listFormColumns = useMemo(() => flattenColumns(getBandedColumns()), [getBandedColumns])
|
const listFormColumns = useMemo(() => flattenGridColumns(getBandedColumns()), [getBandedColumns])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!gridDto) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set js and css
|
|
||||||
const grdOpt = gridDto.gridOptions
|
|
||||||
if (grdOpt.customJsSources.length) {
|
|
||||||
for (const js of grdOpt.customJsSources) {
|
|
||||||
addJs(js)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (grdOpt.customStyleSources.length) {
|
|
||||||
for (const css of grdOpt.customStyleSources) {
|
|
||||||
addCss(css)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [gridDto])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!gridDto) return
|
if (!gridDto) return
|
||||||
|
|
@ -205,31 +140,6 @@ const SchedulerView = (props: SchedulerViewProps) => {
|
||||||
setSchedulerDataSource(dataSource)
|
setSchedulerDataSource(dataSource)
|
||||||
}, [gridDto, searchParams, createSelectDataSource])
|
}, [gridDto, searchParams, createSelectDataSource])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
refListFormCode.current = listFormCode
|
|
||||||
}, [listFormCode])
|
|
||||||
|
|
||||||
// WidgetGroup yüksekliğini hesapla
|
|
||||||
useEffect(() => {
|
|
||||||
const calculateWidgetHeight = () => {
|
|
||||||
if (widgetGroupRef.current) {
|
|
||||||
const height = widgetGroupRef.current.offsetHeight
|
|
||||||
setWidgetGroupHeight(height)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateWidgetHeight()
|
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(calculateWidgetHeight)
|
|
||||||
if (widgetGroupRef.current) {
|
|
||||||
resizeObserver.observe(widgetGroupRef.current)
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
resizeObserver.disconnect()
|
|
||||||
}
|
|
||||||
}, [gridDto?.widgets])
|
|
||||||
|
|
||||||
const settingButtonClick = useCallback(() => {
|
const settingButtonClick = useCallback(() => {
|
||||||
window.open(
|
window.open(
|
||||||
ROUTES_ENUM.protected.saas.listFormManagement.edit.replace(':listFormCode', listFormCode),
|
ROUTES_ENUM.protected.saas.listFormManagement.edit.replace(':listFormCode', listFormCode),
|
||||||
|
|
@ -522,13 +432,7 @@ const SchedulerView = (props: SchedulerViewProps) => {
|
||||||
hideOnOutsideClick: popupOptions?.hideOnOutsideClick,
|
hideOnOutsideClick: popupOptions?.hideOnOutsideClick,
|
||||||
maxHeight: getEditPopupMaxHeight(useMobileEditPopup, popupFullScreen, popupOptions?.height),
|
maxHeight: getEditPopupMaxHeight(useMobileEditPopup, popupFullScreen, popupOptions?.height),
|
||||||
maxWidth: useMobileEditPopup ? '100%' : popupOptions?.width,
|
maxWidth: useMobileEditPopup ? '100%' : popupOptions?.width,
|
||||||
position: useMobileEditPopup
|
position: useMobileEditPopup ? getMobilePopupPosition() : popupOptions?.position,
|
||||||
? {
|
|
||||||
my: 'top center',
|
|
||||||
at: 'top center',
|
|
||||||
of: typeof window !== 'undefined' ? window : undefined,
|
|
||||||
}
|
|
||||||
: popupOptions?.position,
|
|
||||||
resizeEnabled: popupOptions?.resizeEnabled,
|
resizeEnabled: popupOptions?.resizeEnabled,
|
||||||
restorePosition: popupOptions?.restorePosition,
|
restorePosition: popupOptions?.restorePosition,
|
||||||
showTitle: popupOptions?.showTitle,
|
showTitle: popupOptions?.showTitle,
|
||||||
|
|
@ -685,14 +589,6 @@ const SchedulerView = (props: SchedulerViewProps) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// EditorType belirleme
|
|
||||||
let editorType: any = i.editorType2
|
|
||||||
if (i.editorType2 === PlatformEditorTypes.dxGridBox) {
|
|
||||||
editorType = 'dxDropDownBox'
|
|
||||||
} else if (i.editorType2) {
|
|
||||||
editorType = i.editorType2
|
|
||||||
}
|
|
||||||
|
|
||||||
const lookup = listFormColumn?.lookup
|
const lookup = listFormColumn?.lookup
|
||||||
if (lookup) {
|
if (lookup) {
|
||||||
editorOptions.dataSource =
|
editorOptions.dataSource =
|
||||||
|
|
@ -713,13 +609,7 @@ const SchedulerView = (props: SchedulerViewProps) => {
|
||||||
dataField: i.fieldName,
|
dataField: i.fieldName,
|
||||||
name: i.fieldName,
|
name: i.fieldName,
|
||||||
editorType2: i.editorType2,
|
editorType2: i.editorType2,
|
||||||
editorType:
|
editorType: resolveEditorType(i.editorType2),
|
||||||
i.editorType2 == PlatformEditorTypes.dxGridBox
|
|
||||||
? 'dxDropDownBox'
|
|
||||||
: i.editorType2 == PlatformEditorTypes.dxImageUpload ||
|
|
||||||
i.editorType2 == PlatformEditorTypes.dxImageViewer
|
|
||||||
? undefined
|
|
||||||
: i.editorType2,
|
|
||||||
colSpan: i.colSpan,
|
colSpan: i.colSpan,
|
||||||
editorOptions,
|
editorOptions,
|
||||||
editorScript: i.editorScript,
|
editorScript: i.editorScript,
|
||||||
|
|
@ -827,11 +717,7 @@ const SchedulerView = (props: SchedulerViewProps) => {
|
||||||
),
|
),
|
||||||
maxWidth: schedulerPopupWidth,
|
maxWidth: schedulerPopupWidth,
|
||||||
position: useMobileEditPopup
|
position: useMobileEditPopup
|
||||||
? {
|
? getMobilePopupPosition()
|
||||||
my: 'top center',
|
|
||||||
at: 'top center',
|
|
||||||
of: typeof window !== 'undefined' ? window : undefined,
|
|
||||||
}
|
|
||||||
: configuredEditPopup.position,
|
: configuredEditPopup.position,
|
||||||
resizeEnabled: configuredEditPopup.resizeEnabled,
|
resizeEnabled: configuredEditPopup.resizeEnabled,
|
||||||
restorePosition: configuredEditPopup.restorePosition,
|
restorePosition: configuredEditPopup.restorePosition,
|
||||||
|
|
|
||||||
|
|
@ -2,25 +2,12 @@ import type { GridDto, TodoOptionDto } from '@/proxy/form/models'
|
||||||
import type { IdentityUserDto } from '@/proxy/admin/models'
|
import type { IdentityUserDto } from '@/proxy/admin/models'
|
||||||
import type { DataGridRef } from 'devextreme-react/data-grid'
|
import type { DataGridRef } from 'devextreme-react/data-grid'
|
||||||
import type CustomStore from 'devextreme/data/custom_store'
|
import type CustomStore from 'devextreme/data/custom_store'
|
||||||
import {
|
import { HtmlEditor, ImageUpload, MediaResizing } from 'devextreme-react/html-editor'
|
||||||
HtmlEditor,
|
import { HtmlEditorToolbar } from '@/views/shared/HtmlEditorToolbarItems'
|
||||||
ImageUpload,
|
|
||||||
Item as HtmlEditorItem,
|
|
||||||
MediaResizing,
|
|
||||||
Toolbar as HtmlEditorToolbar,
|
|
||||||
} from 'devextreme-react/html-editor'
|
|
||||||
import Toolbar, { Item as ToolbarItem } from 'devextreme-react/toolbar'
|
import Toolbar, { Item as ToolbarItem } from 'devextreme-react/toolbar'
|
||||||
import TagBox from 'devextreme-react/tag-box'
|
import TagBox from 'devextreme-react/tag-box'
|
||||||
import { AVATAR_URL } from '@/constants/app.constant'
|
import { AVATAR_URL } from '@/constants/app.constant'
|
||||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||||
import {
|
|
||||||
fontFamilyOptions,
|
|
||||||
fontSizeOptions,
|
|
||||||
fontValues,
|
|
||||||
headerOptions,
|
|
||||||
headerValues,
|
|
||||||
sizeValues,
|
|
||||||
} from '@/proxy/reports/data'
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
|
@ -39,6 +26,7 @@ import {
|
||||||
} from 'react-icons/fa'
|
} from 'react-icons/fa'
|
||||||
import { useListFormCustomDataSource } from './useListFormCustomDataSource'
|
import { useListFormCustomDataSource } from './useListFormCustomDataSource'
|
||||||
import { useListFormStateStoring } from './useListFormStateStoring'
|
import { useListFormStateStoring } from './useListFormStateStoring'
|
||||||
|
import { useListFormGridDto } from './shared/hooks'
|
||||||
import { Avatar, Rate } from '@/components/ui'
|
import { Avatar, Rate } from '@/components/ui'
|
||||||
import { getUsers } from '@/services/identity.service'
|
import { getUsers } from '@/services/identity.service'
|
||||||
import { getTenants } from '@/services/tenant.service'
|
import { getTenants } from '@/services/tenant.service'
|
||||||
|
|
@ -47,6 +35,7 @@ import { usePermission } from '@/utils/hooks/usePermission'
|
||||||
import { usePWA } from '@/utils/hooks/usePWA'
|
import { usePWA } from '@/utils/hooks/usePWA'
|
||||||
import { useListFormColumns } from './useListFormColumns'
|
import { useListFormColumns } from './useListFormColumns'
|
||||||
import type { GridColumnData } from './GridColumnData'
|
import type { GridColumnData } from './GridColumnData'
|
||||||
|
import { flattenGridColumns } from './shared/columns'
|
||||||
import { useStoreState } from '@/store/store'
|
import { useStoreState } from '@/store/store'
|
||||||
|
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
@ -55,7 +44,9 @@ interface TodoBoardProps {
|
||||||
listFormCode: string
|
listFormCode: string
|
||||||
searchParams?: URLSearchParams
|
searchParams?: URLSearchParams
|
||||||
isSubForm?: boolean
|
isSubForm?: boolean
|
||||||
gridDto: GridDto
|
level?: number
|
||||||
|
refreshData?: () => Promise<void>
|
||||||
|
gridDto?: GridDto
|
||||||
}
|
}
|
||||||
|
|
||||||
type TodoRow = Record<string, any>
|
type TodoRow = Record<string, any>
|
||||||
|
|
@ -92,11 +83,6 @@ const TODO_STATUS_LANGUAGE_KEYS: Record<string, string> = {
|
||||||
staging: 'App.StaticLookup.Staging',
|
staging: 'App.StaticLookup.Staging',
|
||||||
}
|
}
|
||||||
|
|
||||||
const flattenColumns = (columns: GridColumnData[] = []): GridColumnData[] =>
|
|
||||||
columns.flatMap((column) =>
|
|
||||||
column.columns?.length ? flattenColumns(column.columns as GridColumnData[]) : [column],
|
|
||||||
)
|
|
||||||
|
|
||||||
const fieldValue = (row: TodoRow, field?: string) => (field ? row[field] : undefined)
|
const fieldValue = (row: TodoRow, field?: string) => (field ? row[field] : undefined)
|
||||||
|
|
||||||
const tenantIdValue = (row: TodoRow) => row.TenantId ?? row.tenantId
|
const tenantIdValue = (row: TodoRow) => row.TenantId ?? row.tenantId
|
||||||
|
|
@ -176,7 +162,22 @@ const priorityLabel = (
|
||||||
: translate('::ListForms.TodoBoard.NoPriority')
|
: translate('::ListForms.TodoBoard.NoPriority')
|
||||||
}
|
}
|
||||||
|
|
||||||
const TodoBoard = ({ listFormCode, searchParams, isSubForm, gridDto }: TodoBoardProps) => {
|
const TodoBoard = (props: TodoBoardProps) => {
|
||||||
|
const { listFormCode, searchParams, isSubForm, gridDto: extGridDto } = props
|
||||||
|
// Alt form olarak açıldığında tanım dışarıdan gelmez; kendisi yükler.
|
||||||
|
const gridDto = useListFormGridDto(listFormCode, extGridDto)
|
||||||
|
|
||||||
|
return gridDto ? (
|
||||||
|
<TodoBoardContent {...props} gridDto={gridDto} />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const TodoBoardContent = ({
|
||||||
|
listFormCode,
|
||||||
|
searchParams,
|
||||||
|
isSubForm,
|
||||||
|
gridDto,
|
||||||
|
}: TodoBoardProps & { gridDto: GridDto }) => {
|
||||||
const options = gridDto.gridOptions.todoOptionDto as TodoOptionDto
|
const options = gridDto.gridOptions.todoOptionDto as TodoOptionDto
|
||||||
const editingOptions = gridDto.gridOptions.editingOptionDto
|
const editingOptions = gridDto.gridOptions.editingOptionDto
|
||||||
const canAdd =
|
const canAdd =
|
||||||
|
|
@ -216,7 +217,7 @@ const TodoBoard = ({ listFormCode, searchParams, isSubForm, gridDto }: TodoBoard
|
||||||
listFormColumnsCacheRef.current = {
|
listFormColumnsCacheRef.current = {
|
||||||
gridDto,
|
gridDto,
|
||||||
listFormCode,
|
listFormCode,
|
||||||
columns: flattenColumns(getBandedColumnsRef.current()),
|
columns: flattenGridColumns(getBandedColumnsRef.current()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const listFormColumns = listFormColumnsCacheRef.current.columns
|
const listFormColumns = listFormColumnsCacheRef.current.columns
|
||||||
|
|
@ -904,48 +905,7 @@ const TodoBoard = ({ listFormCode, searchParams, isSubForm, gridDto }: TodoBoard
|
||||||
>
|
>
|
||||||
<MediaResizing enabled={true} />
|
<MediaResizing enabled={true} />
|
||||||
<ImageUpload fileUploadMode="base64" />
|
<ImageUpload fileUploadMode="base64" />
|
||||||
<HtmlEditorToolbar multiline>
|
<HtmlEditorToolbar />
|
||||||
<HtmlEditorItem name="undo" />
|
|
||||||
<HtmlEditorItem name="redo" />
|
|
||||||
<HtmlEditorItem name="separator" />
|
|
||||||
<HtmlEditorItem
|
|
||||||
name="size"
|
|
||||||
acceptedValues={sizeValues}
|
|
||||||
options={fontSizeOptions}
|
|
||||||
/>
|
|
||||||
<HtmlEditorItem
|
|
||||||
name="font"
|
|
||||||
acceptedValues={fontValues}
|
|
||||||
options={fontFamilyOptions}
|
|
||||||
/>
|
|
||||||
<HtmlEditorItem name="separator" />
|
|
||||||
<HtmlEditorItem name="bold" />
|
|
||||||
<HtmlEditorItem name="italic" />
|
|
||||||
<HtmlEditorItem name="underline" />
|
|
||||||
<HtmlEditorItem name="strike" />
|
|
||||||
<HtmlEditorItem name="separator" />
|
|
||||||
<HtmlEditorItem name="orderedList" />
|
|
||||||
<HtmlEditorItem name="bulletList" />
|
|
||||||
<HtmlEditorItem name="separator" />
|
|
||||||
<HtmlEditorItem
|
|
||||||
name="header"
|
|
||||||
acceptedValues={headerValues}
|
|
||||||
options={headerOptions}
|
|
||||||
/>
|
|
||||||
<HtmlEditorItem name="separator" />
|
|
||||||
<HtmlEditorItem name="color" />
|
|
||||||
<HtmlEditorItem name="background" />
|
|
||||||
<HtmlEditorItem name="separator" />
|
|
||||||
<HtmlEditorItem name="alignLeft" />
|
|
||||||
<HtmlEditorItem name="alignCenter" />
|
|
||||||
<HtmlEditorItem name="alignRight" />
|
|
||||||
<HtmlEditorItem name="alignJustify" />
|
|
||||||
<HtmlEditorItem name="separator" />
|
|
||||||
<HtmlEditorItem name="link" />
|
|
||||||
<HtmlEditorItem name="image" />
|
|
||||||
<HtmlEditorItem name="separator" />
|
|
||||||
<HtmlEditorItem name="clear" />
|
|
||||||
</HtmlEditorToolbar>
|
|
||||||
</HtmlEditor>
|
</HtmlEditor>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -332,16 +332,14 @@ export function buildSeriesDto(seriesList: ChartSeriesDto[]) {
|
||||||
|
|
||||||
export function autoNumber() {
|
export function autoNumber() {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const pad = (n: number, width: number) => n.toString().padStart(width, '0')
|
const pad = (value: number, width: number) => value.toString().padStart(width, '0')
|
||||||
|
|
||||||
const shortYear = pad(now.getFullYear() % 100, 2); // 2025 → 25
|
return [
|
||||||
|
pad(now.getFullYear() % 100, 2), // 2025 -> 25
|
||||||
return (
|
pad(now.getMonth() + 1, 2),
|
||||||
`${shortYear}` +
|
pad(now.getDate(), 2),
|
||||||
`${pad(now.getMonth() + 1, 2)}` +
|
pad(now.getHours(), 2),
|
||||||
`${pad(now.getDate(), 2)}` +
|
pad(now.getMinutes(), 2),
|
||||||
`${pad(now.getHours(), 2)}` +
|
pad(now.getSeconds(), 2),
|
||||||
`${pad(now.getMinutes(), 2)}` +
|
].join('')
|
||||||
`${pad(now.getSeconds(), 2)}`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
openImageInNewTab,
|
openImageInNewTab,
|
||||||
showImageHoverPreview,
|
showImageHoverPreview,
|
||||||
} from '../../form/editors/imageHoverPreview'
|
} from '../../form/editors/imageHoverPreview'
|
||||||
|
import { normalizeImageSize, parseJsonObject } from '../shared/imageValue'
|
||||||
|
|
||||||
type ImageUploadOptions = {
|
type ImageUploadOptions = {
|
||||||
accept?: string
|
accept?: string
|
||||||
|
|
@ -17,24 +18,14 @@ type ImageUploadOptions = {
|
||||||
width?: number | string
|
width?: number | string
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseJsonObject = (value: unknown) => {
|
/**
|
||||||
if (!value) return undefined
|
* Yüklenen değerin ham biçimini korur; ayraçlara göre bölmez.
|
||||||
if (typeof value === 'object' && !Array.isArray(value)) return value as Record<string, unknown>
|
* (Görüntüleme tarafındaki `normalizeImageValue` ile bilinçli olarak farklıdır:
|
||||||
if (typeof value !== 'string') return undefined
|
* burada kullanıcı tarafından yazılmış virgüllü URL'ler bozulmamalıdır.)
|
||||||
|
*/
|
||||||
try {
|
const normalizeUploadValue = (value: unknown): string[] => {
|
||||||
const parsed = JSON.parse(value)
|
|
||||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
||||||
? (parsed as Record<string, unknown>)
|
|
||||||
: undefined
|
|
||||||
} catch {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizeImageValue = (value: unknown): string[] => {
|
|
||||||
if (!value) return []
|
if (!value) return []
|
||||||
if (Array.isArray(value)) return value.flatMap(normalizeImageValue)
|
if (Array.isArray(value)) return value.flatMap(normalizeUploadValue)
|
||||||
|
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
const trimmed = value.trim()
|
const trimmed = value.trim()
|
||||||
|
|
@ -43,7 +34,7 @@ const normalizeImageValue = (value: unknown): string[] => {
|
||||||
if (trimmed.startsWith('[')) {
|
if (trimmed.startsWith('[')) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(trimmed)
|
const parsed = JSON.parse(trimmed)
|
||||||
if (Array.isArray(parsed)) return parsed.flatMap(normalizeImageValue)
|
if (Array.isArray(parsed)) return parsed.flatMap(normalizeUploadValue)
|
||||||
} catch {
|
} catch {
|
||||||
return [trimmed]
|
return [trimmed]
|
||||||
}
|
}
|
||||||
|
|
@ -55,11 +46,6 @@ const normalizeImageValue = (value: unknown): string[] => {
|
||||||
return [String(value)].filter(Boolean)
|
return [String(value)].filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeSize = (value: unknown, fallback: number) => {
|
|
||||||
const size = Number(value)
|
|
||||||
return Number.isFinite(size) && size > 0 ? size : fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolveOptions = (column: any): ImageUploadOptions => {
|
const resolveOptions = (column: any): ImageUploadOptions => {
|
||||||
const configuredOptions = column?.extras?.imageUploadOptions
|
const configuredOptions = column?.extras?.imageUploadOptions
|
||||||
if (configuredOptions && Object.keys(configuredOptions).length > 0) {
|
if (configuredOptions && Object.keys(configuredOptions).length > 0) {
|
||||||
|
|
@ -90,10 +76,10 @@ const ImageUploadEditorComponent = (cellElement: any): ReactElement => {
|
||||||
const col = cellElement.column
|
const col = cellElement.column
|
||||||
const [uploading, setUploading] = useState(false)
|
const [uploading, setUploading] = useState(false)
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
const [urls, setUrls] = useState<string[]>(() => normalizeImageValue(cellElement.value))
|
const [urls, setUrls] = useState<string[]>(() => normalizeUploadValue(cellElement.value))
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setUrls(normalizeImageValue(cellElement.value))
|
setUrls(normalizeUploadValue(cellElement.value))
|
||||||
}, [cellElement.value])
|
}, [cellElement.value])
|
||||||
|
|
||||||
const options = useMemo(() => resolveOptions(col), [col])
|
const options = useMemo(() => resolveOptions(col), [col])
|
||||||
|
|
@ -105,8 +91,8 @@ const ImageUploadEditorComponent = (cellElement: any): ReactElement => {
|
||||||
// JSON'dan string olarak gelebilir: "true" / "false"
|
// JSON'dan string olarak gelebilir: "true" / "false"
|
||||||
const isMultiple: boolean = options.multiple === true || options.multiple === 'true'
|
const isMultiple: boolean = options.multiple === true || options.multiple === 'true'
|
||||||
|
|
||||||
const thumbW = normalizeSize(options.width, 40)
|
const thumbW = normalizeImageSize(options.width, 40)
|
||||||
const thumbH = normalizeSize(options.height, 40)
|
const thumbH = normalizeImageSize(options.height, 40)
|
||||||
|
|
||||||
const removeImage = (index: number) => {
|
const removeImage = (index: number) => {
|
||||||
const newUrls = urls.filter((_, i) => i !== index)
|
const newUrls = urls.filter((_, i) => i !== index)
|
||||||
|
|
|
||||||
|
|
@ -5,91 +5,12 @@ import {
|
||||||
openImageInNewTab,
|
openImageInNewTab,
|
||||||
showImageHoverPreview,
|
showImageHoverPreview,
|
||||||
} from '../../form/editors/imageHoverPreview'
|
} from '../../form/editors/imageHoverPreview'
|
||||||
|
import {
|
||||||
const parseJsonObject = (value: unknown) => {
|
NO_IMAGE,
|
||||||
if (!value) return undefined
|
normalizeImageSize,
|
||||||
if (typeof value === 'object') return value as Record<string, any>
|
normalizeImageValue,
|
||||||
if (typeof value !== 'string') return undefined
|
parseJsonObject,
|
||||||
|
} from '../shared/imageValue'
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(value)
|
|
||||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
||||||
? (parsed as Record<string, any>)
|
|
||||||
: undefined
|
|
||||||
} catch {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizeImageSize = (value: unknown, fallback: number) => {
|
|
||||||
const size = Number(value)
|
|
||||||
return Number.isFinite(size) && size > 0 ? size : fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
const getImageSource = (value: unknown) => {
|
|
||||||
if (!value) return ''
|
|
||||||
if (typeof value === 'string') return value.trim()
|
|
||||||
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
const item = value as Record<string, unknown>
|
|
||||||
return String(item.url ?? item.src ?? item.fileUrl ?? item.path ?? item.value ?? '').trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
return String(value).trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
const isProbablyBase64Image = (value: string) =>
|
|
||||||
value.length > 80 && /^[A-Za-z0-9+/]+={0,2}$/.test(value)
|
|
||||||
|
|
||||||
const toImageSource = (value: string) => {
|
|
||||||
const trimmed = value.trim()
|
|
||||||
if (!trimmed) return ''
|
|
||||||
if (trimmed.startsWith('data:image/') || !isProbablyBase64Image(trimmed)) return trimmed
|
|
||||||
return `data:image/jpeg;base64,${trimmed}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const splitImageString = (value: string) => {
|
|
||||||
const trimmed = value.trim()
|
|
||||||
if (!trimmed) return []
|
|
||||||
if (trimmed.startsWith('data:image/')) return [trimmed]
|
|
||||||
|
|
||||||
return trimmed
|
|
||||||
.split(/\r?\n|\|\s*/)
|
|
||||||
.flatMap((part) => {
|
|
||||||
const text = part.trim()
|
|
||||||
if (!text || text.startsWith('data:image/')) return text ? [text] : []
|
|
||||||
return text.split(',').map((item) => item.trim())
|
|
||||||
})
|
|
||||||
.map(toImageSource)
|
|
||||||
.filter(Boolean)
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizeImageValue = (value: unknown): string[] => {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value.flatMap((item) => normalizeImageValue(item))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
const trimmed = value.trim()
|
|
||||||
if (!trimmed) return []
|
|
||||||
|
|
||||||
if (trimmed.startsWith('[')) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(trimmed)
|
|
||||||
if (Array.isArray(parsed)) {
|
|
||||||
return parsed.flatMap((item) => normalizeImageValue(item))
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return [toImageSource(trimmed)]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return splitImageString(trimmed)
|
|
||||||
}
|
|
||||||
|
|
||||||
const source = getImageSource(value)
|
|
||||||
return source ? [toImageSource(source)] : []
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolveTemplateValue = (templateData: any) => {
|
const resolveTemplateValue = (templateData: any) => {
|
||||||
const dataField =
|
const dataField =
|
||||||
|
|
@ -181,7 +102,7 @@ const ImageViewerEditorComponent = (templateData: any): ReactElement => {
|
||||||
}}
|
}}
|
||||||
onError={({ currentTarget }) => {
|
onError={({ currentTarget }) => {
|
||||||
currentTarget.onerror = null
|
currentTarget.onerror = null
|
||||||
currentTarget.src = '/img/others/no-image.png'
|
currentTarget.src = NO_IMAGE
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
124
ui/src/views/list/shared/cascade.ts
Normal file
124
ui/src/views/list/shared/cascade.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
/**
|
||||||
|
* Lookup cascade (bağımlı açılır liste) davranışı.
|
||||||
|
* Grid ve Tree aynı kuralları paylaşır.
|
||||||
|
*/
|
||||||
|
import type { GridDto } from '@/proxy/form/models'
|
||||||
|
|
||||||
|
export type CascadeInfo = {
|
||||||
|
parentFields: string[]
|
||||||
|
childFields?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CascadeMaps = {
|
||||||
|
/** fieldName -> cascade bilgisi */
|
||||||
|
cascadeFieldsMap: Map<string, CascadeInfo>
|
||||||
|
/** parentField -> etkilediği child alan adları */
|
||||||
|
parentToChildrenMap: Map<string, Set<string>>
|
||||||
|
}
|
||||||
|
|
||||||
|
const splitFields = (value?: string | null) =>
|
||||||
|
(value ?? '')
|
||||||
|
.split(',')
|
||||||
|
.map((field) => field.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
export const createCascadeMaps = (gridDto?: GridDto): CascadeMaps => {
|
||||||
|
const cascadeFieldsMap = new Map<string, CascadeInfo>()
|
||||||
|
const parentToChildrenMap = new Map<string, Set<string>>()
|
||||||
|
|
||||||
|
gridDto?.columnFormats.forEach((col) => {
|
||||||
|
if (!col.fieldName || !col.lookupDto?.cascadeParentFields) return
|
||||||
|
|
||||||
|
const parentFields = splitFields(col.lookupDto.cascadeParentFields)
|
||||||
|
const childFields = col.lookupDto.cascadeEmptyFields
|
||||||
|
? splitFields(col.lookupDto.cascadeEmptyFields)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
cascadeFieldsMap.set(col.fieldName, { parentFields, childFields })
|
||||||
|
|
||||||
|
parentFields.forEach((parentField) => {
|
||||||
|
const children = parentToChildrenMap.get(parentField) ?? new Set<string>()
|
||||||
|
children.add(col.fieldName!)
|
||||||
|
parentToChildrenMap.set(parentField, children)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return { cascadeFieldsMap, parentToChildrenMap }
|
||||||
|
}
|
||||||
|
|
||||||
|
type AttachCascadeParams = {
|
||||||
|
editor: any
|
||||||
|
gridDto: GridDto
|
||||||
|
cascadeInfo: CascadeInfo
|
||||||
|
maps: CascadeMaps
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `onEditorPreparing` içinde cascade davranışını editöre bağlar:
|
||||||
|
* - parent değiştiğinde child alanları temizler,
|
||||||
|
* - etkilenen child editörlerin `disabled` durumunu günceller,
|
||||||
|
* - form ilk açıldığında parent'ı boş olan alanı devre dışı bırakır.
|
||||||
|
*/
|
||||||
|
export const attachCascadeBehavior = ({
|
||||||
|
editor,
|
||||||
|
gridDto,
|
||||||
|
cascadeInfo,
|
||||||
|
maps,
|
||||||
|
}: AttachCascadeParams) => {
|
||||||
|
const { cascadeFieldsMap, parentToChildrenMap } = maps
|
||||||
|
const affectedChildren = parentToChildrenMap.get(editor.dataField)
|
||||||
|
const previousHandler = editor.editorOptions.onValueChanged
|
||||||
|
|
||||||
|
editor.editorOptions.onValueChanged = (e: any) => {
|
||||||
|
previousHandler?.(e)
|
||||||
|
|
||||||
|
const grid = editor.component
|
||||||
|
const rowKey = grid.option('editing.editRowKey')
|
||||||
|
const rowIndex = grid.getRowIndexByKey(rowKey)
|
||||||
|
|
||||||
|
// Parent değiştiğinde bağımlı alanları temizle.
|
||||||
|
if (cascadeInfo.childFields && rowIndex >= 0) {
|
||||||
|
cascadeInfo.childFields.forEach((childField) => grid.cellValue(rowIndex, childField, null))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!affectedChildren?.size) return
|
||||||
|
|
||||||
|
const formInstance = grid.option('editing.form') as any
|
||||||
|
if (!formInstance?.getEditor) return
|
||||||
|
|
||||||
|
const rowData = grid.getVisibleRows().find((row: any) => row.key === rowKey)?.data
|
||||||
|
if (!rowData) return
|
||||||
|
|
||||||
|
affectedChildren.forEach((childFieldName) => {
|
||||||
|
const childInfo = cascadeFieldsMap.get(childFieldName)
|
||||||
|
if (!childInfo) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const childColumn = gridDto.columnFormats.find(
|
||||||
|
(column) => column.fieldName === childFieldName,
|
||||||
|
)
|
||||||
|
const childEditorOptions = childColumn?.editorOptions
|
||||||
|
? JSON.parse(childColumn.editorOptions)
|
||||||
|
: {}
|
||||||
|
const childEditor = formInstance.getEditor(childFieldName)
|
||||||
|
|
||||||
|
childEditor?.option(
|
||||||
|
'disabled',
|
||||||
|
childEditorOptions?.disabled === true ||
|
||||||
|
childInfo.parentFields.some((parentField) => !rowData[parentField]),
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Cascade child editor güncellenemedi: "${childFieldName}"`, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// İlk açılışta parent'ı dolu değilse editörü kilitle.
|
||||||
|
const grid = editor.component
|
||||||
|
const rowKey = grid.option('editing.editRowKey')
|
||||||
|
const rowData = grid.getVisibleRows().find((row: any) => row.key === rowKey)?.data
|
||||||
|
|
||||||
|
if (rowData && cascadeInfo.parentFields.some((parentField) => !rowData[parentField])) {
|
||||||
|
editor.editorOptions.disabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
39
ui/src/views/list/shared/cellStyles.ts
Normal file
39
ui/src/views/list/shared/cellStyles.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
/**
|
||||||
|
* ColumnStylingDto tabanlı koşullu hücre/satır stilleri.
|
||||||
|
* Grid, Tree ve Pivot aynı handler'ı kullanır.
|
||||||
|
*/
|
||||||
|
import type { GridDto } from '@/proxy/form/models'
|
||||||
|
import { controlStyleCondition } from '../Utils'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `onCellPrepared` handler'ı üretir. Yapılandırılmış stil bulunmayan
|
||||||
|
* liste formlarında hiç iş yapmaması için önceden filtreleme yapılır.
|
||||||
|
*/
|
||||||
|
export const createConditionalCellStyleHandler = (gridDto?: GridDto) => {
|
||||||
|
const styledColumns = (gridDto?.columnFormats ?? [])
|
||||||
|
.filter((colFormat) => colFormat.columnStylingDto?.length)
|
||||||
|
.map((colFormat) => ({ fieldName: colFormat.fieldName, styles: colFormat.columnStylingDto }))
|
||||||
|
|
||||||
|
if (!styledColumns.length) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return (e: any) => {
|
||||||
|
styledColumns.forEach(({ fieldName, styles }) => {
|
||||||
|
styles.forEach((colStyle) => {
|
||||||
|
// header, filter, data, group, summaries… her satır tipine stil verilebilir.
|
||||||
|
if (e.rowType !== colStyle.rowType) return
|
||||||
|
// Stil tüm satıra mı yoksa yalnızca ilgili kolona mı uygulanacak?
|
||||||
|
if (!colStyle.useRow && e.column?.dataField !== fieldName) return
|
||||||
|
if (colStyle.conditionValue && !controlStyleCondition(e.data, fieldName, colStyle)) return
|
||||||
|
|
||||||
|
if (colStyle.cssClassName) {
|
||||||
|
e.cellElement.addClass(colStyle.cssClassName)
|
||||||
|
}
|
||||||
|
if (colStyle.cssStyles) {
|
||||||
|
e.cellElement.attr('style', `${e.cellElement.attr('style')};${colStyle.cssStyles}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
31
ui/src/views/list/shared/columnFormatters.ts
Normal file
31
ui/src/views/list/shared/columnFormatters.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* Kolon düzeyinde özel görüntüleme biçimleri.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 10 haneli yerel numarayı `(XXX) XXX-XXXX` biçiminde gösterir.
|
||||||
|
* Ülke kodu (+90 / 0090 / 0) ön ekleri temizlenir; 10 hane değilse boş döner.
|
||||||
|
*/
|
||||||
|
export const formatGlobalPhone = (value: unknown) => {
|
||||||
|
if (!value) return ''
|
||||||
|
|
||||||
|
let digits = String(value).replace(/\D/g, '')
|
||||||
|
if (digits.length > 10) digits = digits.slice(-10)
|
||||||
|
if (digits.length !== 10) return ''
|
||||||
|
|
||||||
|
const match = digits.match(/^(\d{3})(\d{3})(\d{4})$/)
|
||||||
|
return match ? `(${match[1]}) ${match[2]}-${match[3]}` : digits
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `editorOptions.format === 'phoneGlobal'` olan kolonlara telefon biçimini uygular.
|
||||||
|
* DevExtreme bazı durumlarda `format.formatter`'ı çağırmadığı için
|
||||||
|
* `customizeText` de yedek olarak eklenir.
|
||||||
|
*/
|
||||||
|
export const applyPhoneGlobalFormat = (column: any) => {
|
||||||
|
if (column?.editorOptions?.format !== 'phoneGlobal') return
|
||||||
|
|
||||||
|
column.dataType = 'string'
|
||||||
|
column.format = { formatter: formatGlobalPhone }
|
||||||
|
column.customizeText = (cellInfo: any) => formatGlobalPhone(cellInfo?.value)
|
||||||
|
}
|
||||||
16
ui/src/views/list/shared/columns.ts
Normal file
16
ui/src/views/list/shared/columns.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
/**
|
||||||
|
* Kolon yapısıyla ilgili paylaşılan yardımcılar.
|
||||||
|
*/
|
||||||
|
import type { GridColumnData } from '../GridColumnData'
|
||||||
|
|
||||||
|
/** Banded (iç içe) kolon ağacını düz listeye indirger. */
|
||||||
|
export const flattenColumns = <T extends { columns?: any[] }>(columns: T[] = []): T[] =>
|
||||||
|
columns.flatMap((column) => (column?.columns?.length ? flattenColumns(column.columns) : [column]))
|
||||||
|
|
||||||
|
export const flattenGridColumns = (columns: GridColumnData[] = []): GridColumnData[] =>
|
||||||
|
flattenColumns(columns as { columns?: any[] }[]) as GridColumnData[]
|
||||||
|
|
||||||
|
/** Kullanıcı filtresi uygulanmış grid panelinin rengi. */
|
||||||
|
export const FILTERED_GRID_PANEL_COLOR = 'rgba(10, 200, 10, 0.5)'
|
||||||
|
/** Kayıtlı grid state'i yüklenmiş panelin rengi. */
|
||||||
|
export const STATED_GRID_PANEL_COLOR = 'rgba(50, 200, 200, 0.5)'
|
||||||
123
ui/src/views/list/shared/editPopup.ts
Normal file
123
ui/src/views/list/shared/editPopup.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
/**
|
||||||
|
* Liste görünümlerinin düzenleme popup'ı için ortak cihaz tespiti ve popup
|
||||||
|
* yapılandırması. Grid, Tree, CardView ve SchedulerView aynı kuralları kullanır.
|
||||||
|
*/
|
||||||
|
import type { ToolbarItem as PopupToolbarItem } from 'devextreme/ui/popup'
|
||||||
|
import type { RowMode } from '../../form/types'
|
||||||
|
|
||||||
|
export const isTouchLikeDevice = () =>
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
(window.matchMedia?.('(pointer: coarse)').matches || window.matchMedia?.('(hover: none)').matches)
|
||||||
|
|
||||||
|
export const isMobileViewport = () =>
|
||||||
|
typeof window !== 'undefined' && window.matchMedia?.('(max-width: 767px)').matches
|
||||||
|
|
||||||
|
/** Popup'ın mobil davranış kurallarıyla açılıp açılmayacağını belirler. */
|
||||||
|
export const shouldUseMobileEditPopup = () => isMobileViewport() || isTouchLikeDevice()
|
||||||
|
|
||||||
|
export const getEditPopupHeight = (useMobileEditPopup: boolean, isPopupFullScreen: boolean) =>
|
||||||
|
useMobileEditPopup && isPopupFullScreen ? '100%' : 'auto'
|
||||||
|
|
||||||
|
export const getEditPopupMaxHeight = (
|
||||||
|
useMobileEditPopup: boolean,
|
||||||
|
isPopupFullScreen: boolean,
|
||||||
|
configuredHeight?: number,
|
||||||
|
) => {
|
||||||
|
if (useMobileEditPopup && isPopupFullScreen) {
|
||||||
|
return '100%'
|
||||||
|
}
|
||||||
|
|
||||||
|
return configuredHeight && configuredHeight > 0 ? configuredHeight : '90vh'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMobilePopupPosition = () => ({
|
||||||
|
my: 'top center',
|
||||||
|
at: 'top center',
|
||||||
|
of: typeof window !== 'undefined' ? window : undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
type EditPopupToolbarHandlers = {
|
||||||
|
onSave: () => void
|
||||||
|
onCancel: () => void
|
||||||
|
onToggleFullScreen: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kaydet / İptal / Tam ekran butonlarını üretir. */
|
||||||
|
export const buildEditPopupToolbarItems = (
|
||||||
|
translate: (key: string) => string,
|
||||||
|
isPopupFullScreen: boolean,
|
||||||
|
{ onSave, onCancel, onToggleFullScreen }: EditPopupToolbarHandlers,
|
||||||
|
): PopupToolbarItem[] => [
|
||||||
|
{
|
||||||
|
widget: 'dxButton',
|
||||||
|
toolbar: 'bottom',
|
||||||
|
location: 'after',
|
||||||
|
options: {
|
||||||
|
text: translate('::Save'),
|
||||||
|
type: 'default',
|
||||||
|
onClick: onSave,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
widget: 'dxButton',
|
||||||
|
toolbar: 'bottom',
|
||||||
|
location: 'after',
|
||||||
|
options: {
|
||||||
|
text: translate('::Cancel'),
|
||||||
|
onClick: onCancel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
widget: 'dxButton',
|
||||||
|
toolbar: 'top',
|
||||||
|
location: 'after',
|
||||||
|
options: {
|
||||||
|
icon: isPopupFullScreen ? 'collapse' : 'fullscreen',
|
||||||
|
hint: isPopupFullScreen ? translate('::Normal Boyut') : translate('::Tam Ekran'),
|
||||||
|
stylingMode: 'text',
|
||||||
|
onClick: onToggleFullScreen,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
type BuildEditPopupParams = {
|
||||||
|
/** gridOptions.editingOptionDto.popup */
|
||||||
|
popupOptions: any
|
||||||
|
mode: RowMode
|
||||||
|
isPopupFullScreen: boolean
|
||||||
|
useMobileEditPopup: boolean
|
||||||
|
translate: (key: string) => string
|
||||||
|
handlers: EditPopupToolbarHandlers
|
||||||
|
/** Scheduler gibi ek sınıf gerektiren yerler için. */
|
||||||
|
mobileWrapperClass?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grid / Tree / CardView düzenleme popup'ının tek noktadan üretimi.
|
||||||
|
* Daha önce her bir bileşende ~70 satır olarak tekrarlanıyordu.
|
||||||
|
*/
|
||||||
|
export const buildEditPopupOptions = ({
|
||||||
|
popupOptions,
|
||||||
|
mode,
|
||||||
|
isPopupFullScreen,
|
||||||
|
useMobileEditPopup,
|
||||||
|
translate,
|
||||||
|
handlers,
|
||||||
|
mobileWrapperClass = 'mobile-edit-popup',
|
||||||
|
}: BuildEditPopupParams) => ({
|
||||||
|
animation: {},
|
||||||
|
deferRendering: true,
|
||||||
|
wrapperAttr: useMobileEditPopup ? { class: mobileWrapperClass } : undefined,
|
||||||
|
title: (mode === 'new' ? '✚ ' : '🖊️ ') + translate('::' + popupOptions?.title),
|
||||||
|
showTitle: popupOptions?.showTitle,
|
||||||
|
hideOnOutsideClick: popupOptions?.hideOnOutsideClick,
|
||||||
|
width: useMobileEditPopup ? '100%' : popupOptions?.width,
|
||||||
|
height: getEditPopupHeight(useMobileEditPopup, isPopupFullScreen),
|
||||||
|
maxHeight: getEditPopupMaxHeight(useMobileEditPopup, isPopupFullScreen, popupOptions?.height),
|
||||||
|
position: useMobileEditPopup ? getMobilePopupPosition() : popupOptions?.position,
|
||||||
|
resizeEnabled: popupOptions?.resizeEnabled,
|
||||||
|
fullScreen: isPopupFullScreen,
|
||||||
|
dragEnabled: popupOptions?.dragEnabled,
|
||||||
|
restorePosition: popupOptions?.restorePosition,
|
||||||
|
toolbarItems: buildEditPopupToolbarItems(translate, isPopupFullScreen, handlers),
|
||||||
|
})
|
||||||
569
ui/src/views/list/shared/editingForm.ts
Normal file
569
ui/src/views/list/shared/editingForm.ts
Normal file
|
|
@ -0,0 +1,569 @@
|
||||||
|
/**
|
||||||
|
* Grid / Tree / CardView düzenleme formlarının ortak altyapısı.
|
||||||
|
*
|
||||||
|
* Bu dosyadan önce aynı fonksiyonlar Grid.tsx ve Tree.tsx içinde satır satır
|
||||||
|
* kopyalanmış durumdaydı. Tek noktadan yönetim için buraya taşındı.
|
||||||
|
*/
|
||||||
|
import { captionize } from 'devextreme/core/utils/inflector'
|
||||||
|
import type { DataType } from 'devextreme/common'
|
||||||
|
import {
|
||||||
|
ColumnFormatDto,
|
||||||
|
DbTypeEnum,
|
||||||
|
FieldCustomValueTypeEnum,
|
||||||
|
GridDto,
|
||||||
|
PlatformEditorTypes,
|
||||||
|
} from '@/proxy/form/models'
|
||||||
|
import { getNextSequenceValue } from '@/services/form.service'
|
||||||
|
import type { EditorType2, RowMode, SimpleItemWithColData } from '../../form/types'
|
||||||
|
import {
|
||||||
|
autoNumber,
|
||||||
|
extractSearchParamsFields,
|
||||||
|
getEditingFormGroups,
|
||||||
|
GridExtraFilterState,
|
||||||
|
safeJsonParse,
|
||||||
|
setFormEditingExtraItemValues,
|
||||||
|
} from '../Utils'
|
||||||
|
|
||||||
|
export type EditorOptionsWithButtons = { buttons?: any[] } & Record<string, any>
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ *
|
||||||
|
* Satır anahtarı yardımcıları
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
const isTemporaryDxKey = (key: unknown) => typeof key === 'string' && key.startsWith('_DX_KEY_')
|
||||||
|
|
||||||
|
/** DevExtreme'in geçici `_DX_KEY_` değerlerini eleyerek gerçek insert anahtarını bulur. */
|
||||||
|
export const getPersistedInsertedKey = (e: any, keyFieldName?: string) => {
|
||||||
|
const dataKey = keyFieldName ? e?.data?.[keyFieldName] : undefined
|
||||||
|
if (dataKey !== undefined && dataKey !== null && !isTemporaryDxKey(dataKey)) {
|
||||||
|
return dataKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e?.key !== undefined && e?.key !== null && !isTemporaryDxKey(e.key)) {
|
||||||
|
return e.key
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ *
|
||||||
|
* Alan erişim yardımcıları
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/** Alan adını önce birebir, bulunamazsa büyük/küçük harf duyarsız arar. */
|
||||||
|
export const getValueByField = (data: Record<string, any> = {}, field?: string | null) => {
|
||||||
|
if (!field) return undefined
|
||||||
|
if (Object.prototype.hasOwnProperty.call(data, field)) return data[field]
|
||||||
|
const key = Object.keys(data).find(
|
||||||
|
(itemKey) => itemKey.toLowerCase() === String(field).toLowerCase(),
|
||||||
|
)
|
||||||
|
return key ? data[key] : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const findFormFieldKey = (items: any[] = [], field: string): string => {
|
||||||
|
const expected = String(field || '').toLowerCase()
|
||||||
|
|
||||||
|
for (const item of items || []) {
|
||||||
|
const key = item?.dataField || item?.name
|
||||||
|
if (key && String(key).toLowerCase() === expected) {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
const childKey = findFormFieldKey(item?.items || [], field)
|
||||||
|
if (childKey) {
|
||||||
|
return childKey
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const tab of item?.tabs || []) {
|
||||||
|
const tabKey = findFormFieldKey(tab?.items || [], field)
|
||||||
|
if (tabKey) {
|
||||||
|
return tabKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return field
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateReadOnlyInFormItems = (items: any[] = [], field: string, readOnly: boolean) => {
|
||||||
|
let changed = false
|
||||||
|
const expected = String(field || '').toLowerCase()
|
||||||
|
|
||||||
|
const nextItems = items.map((item) => {
|
||||||
|
const key = item?.dataField || item?.name
|
||||||
|
let nextItem = item
|
||||||
|
|
||||||
|
if (key && String(key).toLowerCase() === expected) {
|
||||||
|
const editorOptions = nextItem.editorOptions || {}
|
||||||
|
if (editorOptions.readOnly !== readOnly) {
|
||||||
|
changed = true
|
||||||
|
nextItem = { ...nextItem, editorOptions: { ...editorOptions, readOnly } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextItem?.items?.length) {
|
||||||
|
const childResult = updateReadOnlyInFormItems(nextItem.items, field, readOnly)
|
||||||
|
if (childResult.changed) {
|
||||||
|
changed = true
|
||||||
|
nextItem = { ...nextItem, items: childResult.items }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextItem?.tabs?.length) {
|
||||||
|
let tabsChanged = false
|
||||||
|
const tabs = nextItem.tabs.map((tab: any) => {
|
||||||
|
const tabResult = updateReadOnlyInFormItems(tab.items, field, readOnly)
|
||||||
|
if (tabResult.changed) {
|
||||||
|
changed = true
|
||||||
|
tabsChanged = true
|
||||||
|
return { ...tab, items: tabResult.items }
|
||||||
|
}
|
||||||
|
return tab
|
||||||
|
})
|
||||||
|
if (tabsChanged) {
|
||||||
|
nextItem = { ...nextItem, tabs }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextItem
|
||||||
|
})
|
||||||
|
|
||||||
|
return { items: nextItems, changed }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Editör script'lerinin `setEditorReadOnly` çağrılarını çalışma anında uygular. */
|
||||||
|
export const setFormEditorReadOnly = (form: any, field: string, readOnly: boolean) => {
|
||||||
|
if (!form?.option) return false
|
||||||
|
|
||||||
|
const apply = () => {
|
||||||
|
const formItems = form.option('items') || []
|
||||||
|
const resolvedField = findFormFieldKey(formItems, field)
|
||||||
|
const editor = form.getEditor?.(resolvedField) ?? form.getEditor?.(field)
|
||||||
|
const result = updateReadOnlyInFormItems(formItems, resolvedField, readOnly)
|
||||||
|
|
||||||
|
if (result.changed) {
|
||||||
|
try {
|
||||||
|
const item = form.itemOption?.(resolvedField) ?? form.itemOption?.(field)
|
||||||
|
if (item) {
|
||||||
|
form.itemOption?.(resolvedField, 'editorOptions', {
|
||||||
|
...(item.editorOptions || {}),
|
||||||
|
readOnly,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
form.option('items', result.items)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
form.option('items', result.items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeEditor = editor ?? form.getEditor?.(resolvedField) ?? form.getEditor?.(field)
|
||||||
|
if (activeEditor?.option?.('readOnly') !== readOnly) {
|
||||||
|
activeEditor?.option?.('readOnly', readOnly)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply()
|
||||||
|
// DevExtreme editörü ilk apply anında henüz oluşmamış olabilir.
|
||||||
|
setTimeout(apply, 0)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getActiveEditingForm = (component: any) => {
|
||||||
|
const editForm = component?.getView?.('editingView')?._editForm
|
||||||
|
return editForm?.getEditor || editForm?.itemOption ? editForm : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shouldRunEditorScriptOnContentReady = (script?: string) =>
|
||||||
|
Boolean(
|
||||||
|
script && (script.includes('setEditorReadOnly') || script.includes('runtimeSetEditorReadOnly')),
|
||||||
|
)
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ *
|
||||||
|
* Yeni satır varsayılan değerleri
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/** URL/filtre üzerinden gelen ham değeri kolonun veri tipine göre dönüştürür. */
|
||||||
|
export const castValueByDataType = (value: any, dataType?: DataType) => {
|
||||||
|
switch (dataType) {
|
||||||
|
case 'date':
|
||||||
|
case 'datetime':
|
||||||
|
return new Date(value)
|
||||||
|
case 'number':
|
||||||
|
return Number(value)
|
||||||
|
case 'boolean':
|
||||||
|
return value === true || value === 'true'
|
||||||
|
case 'object':
|
||||||
|
return safeJsonParse(value, value)
|
||||||
|
default:
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yeni kayıt için varsayılan değerleri uygular:
|
||||||
|
* 1) kolon defaultValue (@AUTONUMBER / Sequence / sabit)
|
||||||
|
* 2) URL veya prop üzerinden gelen filtre değerleri (üzerine yazar)
|
||||||
|
*
|
||||||
|
* ExtraFilter ile yönetilen alanlar atlanır.
|
||||||
|
*/
|
||||||
|
export const applyNewRowDefaults = async (
|
||||||
|
gridDto: GridDto,
|
||||||
|
data: Record<string, any>,
|
||||||
|
rawFilter?: string | null,
|
||||||
|
extraFilters: GridExtraFilterState[] = [],
|
||||||
|
) => {
|
||||||
|
const filters = rawFilter
|
||||||
|
? extractSearchParamsFields(safeJsonParse<any>(rawFilter, null, 'Filter parse edilemedi:'))
|
||||||
|
: []
|
||||||
|
const extraFilterFields = new Set(extraFilters.map((filter) => filter.fieldName))
|
||||||
|
|
||||||
|
for (const colFormat of gridDto.columnFormats ?? []) {
|
||||||
|
const fieldName = colFormat.fieldName
|
||||||
|
if (!fieldName || extraFilterFields.has(fieldName)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colFormat.defaultValue !== null && colFormat.defaultValue !== undefined) {
|
||||||
|
if (typeof colFormat.defaultValue === 'string' && colFormat.defaultValue === '@AUTONUMBER') {
|
||||||
|
data[fieldName] = autoNumber()
|
||||||
|
} else if (colFormat.defaultValueType === FieldCustomValueTypeEnum.Sequence) {
|
||||||
|
try {
|
||||||
|
const response = await getNextSequenceValue(String(colFormat.defaultValue))
|
||||||
|
data[fieldName] = response.data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Sequence default value alınamadı:', {
|
||||||
|
fieldName,
|
||||||
|
defaultValue: colFormat.defaultValue,
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data[fieldName] = colFormat.defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldMatch = filters.find(([field]) => field === fieldName)
|
||||||
|
if (fieldMatch) {
|
||||||
|
data[fieldName] = castValueByDataType(fieldMatch[2], colFormat.dataType as DataType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ *
|
||||||
|
* Insert / Update payload üretimi
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/** Sadece eklemeye izin verilen alanları gönderir (yetki filtresi). */
|
||||||
|
export const buildInsertPayload = (gridDto: GridDto | undefined, data: Record<string, any>) => {
|
||||||
|
if (!gridDto?.columnFormats) {
|
||||||
|
return setFormEditingExtraItemValues(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowedFields = new Set(
|
||||||
|
gridDto.columnFormats.filter((field) => field.allowAdding).map((field) => field.fieldName),
|
||||||
|
)
|
||||||
|
const filtered: Record<string, any> = {}
|
||||||
|
|
||||||
|
for (const key of Object.keys(data)) {
|
||||||
|
// "Options:TemplateName" gibi JSON alt alanları isim kökü üzerinden kontrol edilir.
|
||||||
|
if (allowedFields.has(key) || allowedFields.has(key.split(':')[0])) {
|
||||||
|
filtered[key] = data[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return setFormEditingExtraItemValues(filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Güncelleme payload'ı. `sendOnlyChangedFormValuesUpdate` açıksa yalnızca
|
||||||
|
* değişen alanlar, kapalıysa eski+yeni birleşimi gönderilir.
|
||||||
|
*/
|
||||||
|
export const buildUpdatePayload = (
|
||||||
|
gridDto: GridDto | undefined,
|
||||||
|
oldData: Record<string, any>,
|
||||||
|
newData: Record<string, any>,
|
||||||
|
) => {
|
||||||
|
const keyFieldName = gridDto?.gridOptions.keyFieldName
|
||||||
|
|
||||||
|
if (gridDto?.gridOptions.editingOptionDto?.sendOnlyChangedFormValuesUpdate) {
|
||||||
|
const payload = { ...newData }
|
||||||
|
if (Object.keys(payload).some((key) => key.includes(':'))) {
|
||||||
|
Object.keys(oldData).forEach((key) => {
|
||||||
|
if (key.includes(':')) {
|
||||||
|
payload[key] = payload[key] ?? oldData[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const result = setFormEditingExtraItemValues(payload)
|
||||||
|
if (keyFieldName) delete result[keyFieldName]
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowedFields = gridDto?.columnFormats
|
||||||
|
? new Set(
|
||||||
|
gridDto.columnFormats.filter((field) => field.allowEditing).map((field) => field.fieldName),
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const merged: Record<string, any> = { ...oldData, ...newData }
|
||||||
|
if (allowedFields) {
|
||||||
|
Object.keys(merged).forEach((key) => {
|
||||||
|
if (!allowedFields.has(key) && !allowedFields.has(key.split(':')[0])) {
|
||||||
|
delete merged[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = setFormEditingExtraItemValues(merged)
|
||||||
|
Object.keys(result).forEach((key) => {
|
||||||
|
if (key.includes(':')) delete result[key]
|
||||||
|
})
|
||||||
|
if (keyFieldName) delete result[keyFieldName]
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Options:TemplateName" biçimindeki sanal alanları, JSON kolonundan
|
||||||
|
* okuyup düzenleme formuna doldurur.
|
||||||
|
*/
|
||||||
|
export const expandJsonFormFields = (columns: { dataField?: string }[] = [], data: any) => {
|
||||||
|
columns.forEach((col) => {
|
||||||
|
if (!col.dataField?.includes(':')) return
|
||||||
|
|
||||||
|
const [jsonField, jsonProperty] = col.dataField.split(':')
|
||||||
|
const rawValue = data?.[jsonField]
|
||||||
|
if (!rawValue) return
|
||||||
|
|
||||||
|
const parsed = safeJsonParse<Record<string, any> | null>(
|
||||||
|
rawValue,
|
||||||
|
null,
|
||||||
|
`JSON alanı çözümlenemedi: ${jsonField}`,
|
||||||
|
)
|
||||||
|
if (parsed) {
|
||||||
|
data[col.dataField] = parsed[jsonProperty]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ *
|
||||||
|
* Form item üretimi
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
const dateEditorDefaults = {
|
||||||
|
type: 'date',
|
||||||
|
dateSerializationFormat: 'yyyy-MM-dd',
|
||||||
|
displayFormat: 'shortDate',
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateTimeEditorDefaults = {
|
||||||
|
type: 'datetime',
|
||||||
|
dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss',
|
||||||
|
displayFormat: 'shortDateShortTime',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kolonun DB tipine göre tarih editörü varsayılanlarını döner. */
|
||||||
|
const getDateEditorDefaults = (sourceDbType?: DbTypeEnum) => {
|
||||||
|
if (sourceDbType === DbTypeEnum.Date) return dateEditorDefaults
|
||||||
|
if (
|
||||||
|
sourceDbType === DbTypeEnum.DateTime ||
|
||||||
|
sourceDbType === DbTypeEnum.DateTime2 ||
|
||||||
|
sourceDbType === DbTypeEnum.DateTimeOffset
|
||||||
|
) {
|
||||||
|
return dateTimeEditorDefaults
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/** PlatformEditorTypes -> DevExtreme editorType eşlemesi. */
|
||||||
|
export const resolveEditorType = (editorType2?: EditorType2) => {
|
||||||
|
if (editorType2 === PlatformEditorTypes.dxGridBox) return 'dxDropDownBox'
|
||||||
|
if (
|
||||||
|
editorType2 === PlatformEditorTypes.dxImageUpload ||
|
||||||
|
editorType2 === PlatformEditorTypes.dxImageViewer
|
||||||
|
) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return editorType2
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormItemMapperParams = {
|
||||||
|
mode: RowMode
|
||||||
|
translate: (key: string) => string
|
||||||
|
/** URL/prop filtresinin ham JSON değeri. */
|
||||||
|
rawFilter?: string | null
|
||||||
|
extraFilters?: GridExtraFilterState[]
|
||||||
|
/** ImageViewer editörünün değerini okuyabilmesi için aktif form verisi. */
|
||||||
|
getEditingFormData?: () => Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ColumnFormatDto -> DevExtreme form item dönüşümü.
|
||||||
|
* Grid ve Tree bu tek uygulamayı paylaşır.
|
||||||
|
*/
|
||||||
|
export const createFormItemMapper = ({
|
||||||
|
mode,
|
||||||
|
translate,
|
||||||
|
rawFilter,
|
||||||
|
extraFilters = [],
|
||||||
|
getEditingFormData,
|
||||||
|
}: FormItemMapperParams) => {
|
||||||
|
// Filtreyi item başına değil, bir kez çözümle.
|
||||||
|
const filteredFields = new Set(
|
||||||
|
rawFilter
|
||||||
|
? extractSearchParamsFields(
|
||||||
|
safeJsonParse<any>(rawFilter, null, 'EditorOptions filter parse error:'),
|
||||||
|
).map(([field]) => field)
|
||||||
|
: [],
|
||||||
|
)
|
||||||
|
const overriddenByExtraFilter = new Set(
|
||||||
|
extraFilters.filter((filter) => !!filter.value).map((filter) => filter.fieldName),
|
||||||
|
)
|
||||||
|
|
||||||
|
return (colFormat: ColumnFormatDto): SimpleItemWithColData => {
|
||||||
|
const fieldName = colFormat.fieldName
|
||||||
|
let parsedEditorOptions: EditorOptionsWithButtons = {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
parsedEditorOptions = colFormat.editorOptions ? JSON.parse(colFormat.editorOptions) : {}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('EditorOptions parse error:', fieldName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultEditorOptions: EditorOptionsWithButtons = {
|
||||||
|
...(getDateEditorDefaults(colFormat.sourceDbType) ?? {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colFormat.placeHolder) {
|
||||||
|
defaultEditorOptions.placeholder = translate('::' + colFormat.placeHolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof colFormat.defaultValue === 'string' && colFormat.defaultValue === '@AUTONUMBER' && mode === 'new') {
|
||||||
|
defaultEditorOptions.value = autoNumber()
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL filtresi ile sabitlenmiş alanlar (extra filter ile değiştirilemiyorsa) salt okunur.
|
||||||
|
const forcedEditorOptions: EditorOptionsWithButtons =
|
||||||
|
fieldName && filteredFields.has(fieldName) && !overriddenByExtraFilter.has(fieldName)
|
||||||
|
? { readOnly: true }
|
||||||
|
: {}
|
||||||
|
|
||||||
|
const editorOptions: EditorOptionsWithButtons = {
|
||||||
|
...defaultEditorOptions,
|
||||||
|
...parsedEditorOptions,
|
||||||
|
...forcedEditorOptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editorOptions.buttons) {
|
||||||
|
editorOptions.buttons = editorOptions.buttons.map((btn: any) => {
|
||||||
|
if (typeof btn?.options?.onClick === 'string') {
|
||||||
|
// Yapılandırmadan gelen fonksiyon tanımı (platform tasarımı gereği).
|
||||||
|
btn.options.onClick = eval(`(${btn.options.onClick})`)
|
||||||
|
}
|
||||||
|
return btn
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const item: SimpleItemWithColData = {
|
||||||
|
canRead: colFormat.canRead ?? false,
|
||||||
|
canUpdate: colFormat.canUpdate ?? false,
|
||||||
|
canCreate: colFormat.canCreate ?? false,
|
||||||
|
canExport: colFormat.canExport ?? false,
|
||||||
|
allowEditing: colFormat.allowEditing ?? true,
|
||||||
|
allowAdding: colFormat.allowAdding ?? true,
|
||||||
|
dataField: fieldName,
|
||||||
|
name: fieldName,
|
||||||
|
editorType2: colFormat.editorType2,
|
||||||
|
editorType: resolveEditorType(colFormat.editorType2),
|
||||||
|
colSpan: colFormat.colSpan,
|
||||||
|
editorOptions,
|
||||||
|
editorScript: colFormat.editorScript,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colFormat.editorType2 === PlatformEditorTypes.dxImageViewer) {
|
||||||
|
item.template = 'cellEditImageViewer'
|
||||||
|
item.editorOptions = {
|
||||||
|
...item.editorOptions,
|
||||||
|
value: getValueByField(getEditingFormData?.() ?? {}, fieldName),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldName && fieldName.includes(':')) {
|
||||||
|
item.label = { text: captionize(fieldName.split(':')[1]) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((mode === 'edit' && !item.canUpdate) || (mode === 'new' && !item.canCreate)) {
|
||||||
|
item.editorOptions = { ...item.editorOptions, readOnly: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Aktif moda göre form alanının görünür olup olmayacağını belirler. */
|
||||||
|
export const isFormItemVisible = (item: SimpleItemWithColData, mode: RowMode) => {
|
||||||
|
if (mode === 'view') return item.canRead
|
||||||
|
if (mode === 'new') return item.canCreate && item.allowAdding
|
||||||
|
if (mode === 'edit') return item.canUpdate && item.allowEditing
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EditingFormDto gruplarını DevExtreme form item ağacına çevirir.
|
||||||
|
* Tüm `tabbed` gruplar tek bir `tabbed` item altında toplanır.
|
||||||
|
*/
|
||||||
|
export const buildEditingFormItems = (
|
||||||
|
gridDto: GridDto | undefined,
|
||||||
|
mapFormItem: (colFormat: ColumnFormatDto) => SimpleItemWithColData,
|
||||||
|
mode: RowMode,
|
||||||
|
{ singleColumn = false }: { singleColumn?: boolean } = {},
|
||||||
|
) => {
|
||||||
|
if (!gridDto) return undefined
|
||||||
|
|
||||||
|
const groups = getEditingFormGroups(gridDto)
|
||||||
|
if (!groups.length) return undefined
|
||||||
|
|
||||||
|
const tabbedGroups = groups.filter((group: any) => group.itemType === 'tabbed')
|
||||||
|
const result: any[] = []
|
||||||
|
|
||||||
|
const mapGroupItems = (items: ColumnFormatDto[] = []) =>
|
||||||
|
[...items]
|
||||||
|
.sort((left, right) => (left.editOrderNo ?? 0) - (right.editOrderNo ?? 0))
|
||||||
|
.map(mapFormItem)
|
||||||
|
.filter((item) => isFormItemVisible(item, mode))
|
||||||
|
|
||||||
|
const colCountOf = (group: any) => (singleColumn ? 1 : group.colCount || 1)
|
||||||
|
|
||||||
|
groups.forEach((group: any) => {
|
||||||
|
if (group.itemType !== 'tabbed') {
|
||||||
|
result.push({
|
||||||
|
itemType: group.itemType,
|
||||||
|
colCount: colCountOf(group),
|
||||||
|
colSpan: group.colSpan || 1,
|
||||||
|
caption: group.caption,
|
||||||
|
items: mapGroupItems(group.items),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tabbed gruplar yalnızca ilk tabbed grup üzerinden tek seferde eklenir.
|
||||||
|
if (group !== tabbedGroups[0]) return
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
itemType: 'tabbed',
|
||||||
|
colCount: 1,
|
||||||
|
colSpan: 1,
|
||||||
|
// Tabbed item'da caption kullanılmaz; başlıklar tabs içindeki title'lardır.
|
||||||
|
tabs: tabbedGroups.map((tabbedGroup: any) => ({
|
||||||
|
title: tabbedGroup.caption,
|
||||||
|
colCount: colCountOf(tabbedGroup),
|
||||||
|
items: mapGroupItems(tabbedGroup.items),
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
221
ui/src/views/list/shared/editingFormScripts.ts
Normal file
221
ui/src/views/list/shared/editingFormScripts.ts
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
/**
|
||||||
|
* Düzenleme formu (DataGrid/TreeList `editing.form`) olay yönetimi.
|
||||||
|
*
|
||||||
|
* `onContentReady` ve `onFieldDataChanged` mantığı Grid.tsx ve Tree.tsx içinde
|
||||||
|
* birebir tekrar ediyordu; buradan tek noktadan sağlanır.
|
||||||
|
*/
|
||||||
|
import type { MutableRefObject } from 'react'
|
||||||
|
import { executeEditorScript } from '@/utils/editorScriptRuntime'
|
||||||
|
import type { GridDto } from '@/proxy/form/models'
|
||||||
|
import { getEditingFormFields } from '../Utils'
|
||||||
|
import {
|
||||||
|
getValueByField,
|
||||||
|
setFormEditorReadOnly,
|
||||||
|
shouldRunEditorScriptOnContentReady,
|
||||||
|
} from './editingForm'
|
||||||
|
|
||||||
|
type EditingFormHandlerParams = {
|
||||||
|
getGridDto: () => GridDto | undefined
|
||||||
|
/** DataGrid / TreeList instance'ı. */
|
||||||
|
getGridInstance: () => any
|
||||||
|
editingFormDataRef: MutableRefObject<Record<string, any>>
|
||||||
|
editingFormInstanceRef: MutableRefObject<any>
|
||||||
|
}
|
||||||
|
|
||||||
|
const getEditingRowIndex = (grid: any) => {
|
||||||
|
const rowKey = grid?.option?.('editing.editRowKey')
|
||||||
|
return {
|
||||||
|
rowKey,
|
||||||
|
rowIndex: rowKey !== undefined ? (grid?.getRowIndexByKey?.(rowKey) ?? -1) : -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Grid hücrelerine script tarafından yazılan değerleri uygular. */
|
||||||
|
const createGridCellWriter = (grid: any, rowIndex: number) => (newData: Record<string, any>) => {
|
||||||
|
if (!grid || rowIndex < 0) return
|
||||||
|
Object.keys(newData).forEach((field) => grid.cellValue(rowIndex, field, newData[field]))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createEditingFormHandlers = ({
|
||||||
|
getGridDto,
|
||||||
|
getGridInstance,
|
||||||
|
editingFormDataRef,
|
||||||
|
editingFormInstanceRef,
|
||||||
|
}: EditingFormHandlerParams) => {
|
||||||
|
/**
|
||||||
|
* Form ilk render olduğunda salt-okunur davranışını belirleyen editör
|
||||||
|
* script'lerini çalıştırır.
|
||||||
|
*/
|
||||||
|
const onContentReady = (e: any) => {
|
||||||
|
editingFormInstanceRef.current = e.component
|
||||||
|
|
||||||
|
const gridDto = getGridDto()
|
||||||
|
if (!gridDto) return
|
||||||
|
|
||||||
|
const form = e.component
|
||||||
|
const grid = getGridInstance()
|
||||||
|
const formFields = getEditingFormFields(gridDto)
|
||||||
|
const scriptFields = formFields.filter((field) =>
|
||||||
|
shouldRunEditorScriptOnContentReady(field.editorScript),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!scriptFields.length) return
|
||||||
|
|
||||||
|
const editorValues = formFields.reduce<Record<string, any>>((values, field) => {
|
||||||
|
const editorInstance = form?.getEditor?.(field.fieldName)
|
||||||
|
if (editorInstance?.option) {
|
||||||
|
values[field.fieldName!] = editorInstance.option('value')
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const formData = {
|
||||||
|
...editingFormDataRef.current,
|
||||||
|
...(form?.option?.('formData') || {}),
|
||||||
|
...editorValues,
|
||||||
|
}
|
||||||
|
editingFormDataRef.current = { ...formData }
|
||||||
|
|
||||||
|
const runtimeSetEditorReadOnly = (field: string, readOnly: boolean) =>
|
||||||
|
setFormEditorReadOnly(form, field, readOnly)
|
||||||
|
|
||||||
|
scriptFields.forEach((field) => {
|
||||||
|
try {
|
||||||
|
const editorValue =
|
||||||
|
form?.getEditor?.(field.fieldName)?.option?.('value') ??
|
||||||
|
getValueByField(formData, field.fieldName)
|
||||||
|
|
||||||
|
executeEditorScript(field.editorScript!, {
|
||||||
|
formData,
|
||||||
|
e: {
|
||||||
|
component: form,
|
||||||
|
dataField: field.fieldName,
|
||||||
|
scriptEvent: 'contentReady',
|
||||||
|
value: editorValue,
|
||||||
|
},
|
||||||
|
editor: { dataField: field.fieldName, component: grid },
|
||||||
|
runtimeSetEditorReadOnly,
|
||||||
|
setFormData: undefined,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Script exec error on contentReady', field.fieldName, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bir alan değiştiğinde ilgili editör script'ini çalıştırır. */
|
||||||
|
const onFieldDataChanged = (e: any) => {
|
||||||
|
if (!e.dataField) return
|
||||||
|
|
||||||
|
editingFormDataRef.current = { ...(e.component?.option?.('formData') || {}) }
|
||||||
|
|
||||||
|
const gridDto = getGridDto()
|
||||||
|
if (!gridDto) return
|
||||||
|
|
||||||
|
const formField = getEditingFormFields(gridDto).find(
|
||||||
|
(field) =>
|
||||||
|
String(field.fieldName || '').toLowerCase() === String(e.dataField || '').toLowerCase(),
|
||||||
|
)
|
||||||
|
if (!formField?.editorScript) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const grid = getGridInstance()
|
||||||
|
const { rowIndex } = getEditingRowIndex(grid)
|
||||||
|
const formData = {
|
||||||
|
...(e.component?.option?.('formData') || {}),
|
||||||
|
[e.dataField]: e.value,
|
||||||
|
}
|
||||||
|
editingFormDataRef.current = { ...formData }
|
||||||
|
|
||||||
|
const writeToGrid = createGridCellWriter(grid, rowIndex)
|
||||||
|
|
||||||
|
executeEditorScript(formField.editorScript, {
|
||||||
|
formData,
|
||||||
|
e: { ...e, scriptEvent: 'valueChanged' },
|
||||||
|
editor: { dataField: e.dataField, component: grid },
|
||||||
|
runtimeSetEditorReadOnly: (field: string, readOnly: boolean) =>
|
||||||
|
setFormEditorReadOnly(e.component, field, readOnly),
|
||||||
|
setFormData: (newData: any) => {
|
||||||
|
e.component?.option?.('formData', newData)
|
||||||
|
editingFormDataRef.current = { ...newData }
|
||||||
|
writeToGrid(newData)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Script exec error', formField.fieldName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { onContentReady, onFieldDataChanged }
|
||||||
|
}
|
||||||
|
|
||||||
|
type EditorValueChangedScriptParams = {
|
||||||
|
editor: any
|
||||||
|
editorScript: string
|
||||||
|
fieldName?: string
|
||||||
|
editingFormInstanceRef: MutableRefObject<any>
|
||||||
|
getActiveEditingForm: (component: any) => any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `onEditorPreparing` içinde editör script'ini `onValueChanged` zincirine ekler.
|
||||||
|
*/
|
||||||
|
export const attachEditorValueChangedScript = ({
|
||||||
|
editor,
|
||||||
|
editorScript,
|
||||||
|
fieldName,
|
||||||
|
editingFormInstanceRef,
|
||||||
|
getActiveEditingForm,
|
||||||
|
}: EditorValueChangedScriptParams) => {
|
||||||
|
const previousHandler = editor.editorOptions.onValueChanged
|
||||||
|
const editorDataField = editor.dataField
|
||||||
|
|
||||||
|
editor.editorOptions.onValueChanged = (e: any) => {
|
||||||
|
previousHandler?.(e)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const grid = editor.component
|
||||||
|
const { rowKey, rowIndex } = getEditingRowIndex(grid)
|
||||||
|
const formData = {
|
||||||
|
...(grid.getVisibleRows().find((row: any) => row.key === rowKey)?.data || {}),
|
||||||
|
[editorDataField]: e.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
executeEditorScript(editorScript, {
|
||||||
|
formData,
|
||||||
|
e: { ...e, scriptEvent: 'valueChanged' },
|
||||||
|
editor,
|
||||||
|
runtimeSetEditorReadOnly: (field: string, readOnly: boolean) =>
|
||||||
|
setFormEditorReadOnly(
|
||||||
|
editingFormInstanceRef.current ?? getActiveEditingForm(grid),
|
||||||
|
field,
|
||||||
|
readOnly,
|
||||||
|
),
|
||||||
|
setFormData: createGridCellWriter(grid, rowIndex),
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Script exec error', fieldName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Editör butonlarının `onClick` çağrılarına satır bağlamını (formData, rowKey…) ekler.
|
||||||
|
*/
|
||||||
|
export const attachEditorButtonContext = (editor: any) => {
|
||||||
|
if (!editor.editorOptions?.buttons) return
|
||||||
|
|
||||||
|
editor.editorOptions.buttons = editor.editorOptions.buttons.map((btn: any) => {
|
||||||
|
if (typeof btn?.options?.onClick !== 'function') return btn
|
||||||
|
|
||||||
|
const originalClick = btn.options.onClick
|
||||||
|
btn.options.onClick = (e: any) => {
|
||||||
|
const grid = editor.component
|
||||||
|
const { rowKey, rowIndex } = getEditingRowIndex(grid)
|
||||||
|
const formData = grid.getVisibleRows().find((row: any) => row.key === rowKey)?.data || {}
|
||||||
|
|
||||||
|
originalClick({ ...e, formData, fieldName: editor.dataField, rowKey, rowIndex })
|
||||||
|
}
|
||||||
|
return btn
|
||||||
|
})
|
||||||
|
}
|
||||||
103
ui/src/views/list/shared/exportListView.ts
Normal file
103
ui/src/views/list/shared/exportListView.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
/**
|
||||||
|
* DataGrid / PivotGrid dışa aktarma işlemleri.
|
||||||
|
* Ağır kütüphaneler (exceljs, jspdf, file-saver) yalnızca ihtiyaç anında yüklenir.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type ExportFormat = 'xlsx' | 'csv' | 'pdf' | string
|
||||||
|
|
||||||
|
type ExportParams = {
|
||||||
|
component: any
|
||||||
|
format: ExportFormat
|
||||||
|
fileName: string
|
||||||
|
onError?: (error: unknown) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const createWorkbook = async (fileName: string, sheetSuffix: string) => {
|
||||||
|
const [{ Workbook }, { saveAs }] = await Promise.all([
|
||||||
|
import('devextreme-exceljs-fork'),
|
||||||
|
import('file-saver'),
|
||||||
|
])
|
||||||
|
|
||||||
|
const workbook = new Workbook()
|
||||||
|
const worksheet = workbook.addWorksheet(`${fileName}_${sheetSuffix}`)
|
||||||
|
|
||||||
|
return { workbook, worksheet, saveAs }
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportXlsx = async (component: any, fileName: string) => {
|
||||||
|
const { workbook, worksheet, saveAs } = await createWorkbook(fileName, 'sheet')
|
||||||
|
const { exportDataGrid } = await import('devextreme/excel_exporter')
|
||||||
|
|
||||||
|
await exportDataGrid({ component, worksheet, autoFilterEnabled: true })
|
||||||
|
|
||||||
|
const buffer = await workbook.xlsx.writeBuffer()
|
||||||
|
saveAs(new Blob([buffer], { type: 'application/octet-stream' }), `${fileName}_export.xlsx`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportCsv = async (component: any, fileName: string) => {
|
||||||
|
const { workbook, worksheet, saveAs } = await createWorkbook(fileName, 'sheet')
|
||||||
|
|
||||||
|
const items = component.getDataSource()?.items() ?? []
|
||||||
|
const columns = component.getVisibleColumns().filter((column: any) => column.dataField)
|
||||||
|
|
||||||
|
worksheet.addRow(columns.map((column: any) => column.caption || column.dataField))
|
||||||
|
items.forEach((item: any) => {
|
||||||
|
worksheet.addRow(columns.map((column: any) => item[column.dataField]))
|
||||||
|
})
|
||||||
|
|
||||||
|
const buffer = await workbook.csv.writeBuffer()
|
||||||
|
saveAs(new Blob([buffer], { type: 'text/csv' }), `${fileName}_export.csv`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportPdf = async (component: any, fileName: string) => {
|
||||||
|
const [jspdfModule, { exportDataGrid }] = await Promise.all([
|
||||||
|
import('jspdf'),
|
||||||
|
import('devextreme/pdf_exporter'),
|
||||||
|
])
|
||||||
|
|
||||||
|
// jsPDF bazı paketlemelerde default, bazılarında named export olarak gelir.
|
||||||
|
const JsPDFCtor = (jspdfModule as any).default ?? (jspdfModule as any).jsPDF
|
||||||
|
const doc = new JsPDFCtor({})
|
||||||
|
|
||||||
|
await exportDataGrid({ jsPDFDocument: doc, component, indent: 5 })
|
||||||
|
doc.save(`${fileName}_export.pdf`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** DataGrid tabanlı görünümlerin dışa aktarımı. */
|
||||||
|
export const exportListView = async ({ component, format, fileName, onError }: ExportParams) => {
|
||||||
|
try {
|
||||||
|
if (format === 'xlsx') {
|
||||||
|
await exportXlsx(component, fileName)
|
||||||
|
} else if (format === 'csv') {
|
||||||
|
await exportCsv(component, fileName)
|
||||||
|
} else if (format === 'pdf') {
|
||||||
|
await exportPdf(component, fileName)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Export error:', error)
|
||||||
|
onError?.(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** PivotGrid yalnızca Excel dışa aktarımı destekler. */
|
||||||
|
export const exportPivotView = async ({
|
||||||
|
component,
|
||||||
|
fileName,
|
||||||
|
onError,
|
||||||
|
}: Omit<ExportParams, 'format'>) => {
|
||||||
|
try {
|
||||||
|
const { workbook, worksheet, saveAs } = await createWorkbook(fileName, 'pivot')
|
||||||
|
const { exportPivotGrid } = await import('devextreme/excel_exporter')
|
||||||
|
|
||||||
|
await exportPivotGrid({ component, worksheet })
|
||||||
|
|
||||||
|
const buffer = await workbook.xlsx.writeBuffer()
|
||||||
|
saveAs(
|
||||||
|
new Blob([buffer], { type: 'application/octet-stream' }),
|
||||||
|
`${fileName}_pivot_export.xlsx`,
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Pivot export error:', error)
|
||||||
|
onError?.(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
185
ui/src/views/list/shared/hooks.ts
Normal file
185
ui/src/views/list/shared/hooks.ts
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
/**
|
||||||
|
* Liste görünümlerinin paylaştığı küçük React hook'ları.
|
||||||
|
*/
|
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import type { RefObject } from 'react'
|
||||||
|
import type { GridDto } from '@/proxy/form/models'
|
||||||
|
import { getList } from '@/services/form.service'
|
||||||
|
import {
|
||||||
|
addCss,
|
||||||
|
addJs,
|
||||||
|
extractSearchParamsFields,
|
||||||
|
GridExtraFilterState,
|
||||||
|
safeJsonParse,
|
||||||
|
} from '../Utils'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liste formu tanımını çözer: dışarıdan verilmişse onu kullanır, verilmemişse
|
||||||
|
* (örn. alt form olarak açıldığında) servisten yükler.
|
||||||
|
*
|
||||||
|
* Grid, Tree, Gantt, Scheduler, Pivot ve TodoBoard aynı akışı kopyalıyordu.
|
||||||
|
*/
|
||||||
|
export const useListFormGridDto = (listFormCode: string, externalGridDto?: GridDto) => {
|
||||||
|
const [gridDto, setGridDto] = useState<GridDto | undefined>(externalGridDto)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (externalGridDto !== undefined) {
|
||||||
|
setGridDto(externalGridDto)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!listFormCode) {
|
||||||
|
setGridDto(undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let isActive = true
|
||||||
|
getList({ listFormCode })
|
||||||
|
.then((response: any) => {
|
||||||
|
if (isActive) setGridDto(response.data)
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => console.error('GridDto load error:', error))
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isActive = false
|
||||||
|
}
|
||||||
|
}, [listFormCode, externalGridDto])
|
||||||
|
|
||||||
|
return gridDto
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WidgetGroup alanının yüksekliğini izler; `fullHeight` grid'lerinin
|
||||||
|
* `calc(100vh - …)` hesabında kullanılır.
|
||||||
|
*/
|
||||||
|
export const useWidgetGroupHeight = (
|
||||||
|
widgetGroupRef: RefObject<HTMLElement>,
|
||||||
|
widgets: unknown,
|
||||||
|
) => {
|
||||||
|
const [widgetGroupHeight, setWidgetGroupHeight] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const element = widgetGroupRef.current
|
||||||
|
if (!element) {
|
||||||
|
setWidgetGroupHeight(0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculateHeight = () => setWidgetGroupHeight(element.offsetHeight)
|
||||||
|
calculateHeight()
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(calculateHeight)
|
||||||
|
resizeObserver.observe(element)
|
||||||
|
|
||||||
|
return () => resizeObserver.disconnect()
|
||||||
|
}, [widgetGroupRef, widgets])
|
||||||
|
|
||||||
|
return widgetGroupHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Liste formuna tanımlı özel JS/CSS kaynaklarını sayfaya ekler. */
|
||||||
|
export const useListFormCustomSources = (gridDto?: GridDto) => {
|
||||||
|
const customJsSources = gridDto?.gridOptions.customJsSources
|
||||||
|
const customStyleSources = gridDto?.gridOptions.customStyleSources
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
customJsSources?.forEach(addJs)
|
||||||
|
}, [customJsSources])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
customStyleSources?.forEach(addCss)
|
||||||
|
}, [customStyleSources])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getInitialExtraFilters = (gridDto?: GridDto): GridExtraFilterState[] =>
|
||||||
|
(gridDto?.gridOptions.extraFilterDto ?? []).map((filter) => ({
|
||||||
|
fieldName: filter.fieldName,
|
||||||
|
operator: filter.operator,
|
||||||
|
controlType: filter.controlType,
|
||||||
|
value: filter.defaultValue ?? '',
|
||||||
|
}))
|
||||||
|
|
||||||
|
/** [field, operator, value] üçlülerini `and` ile zincirlenmiş DevExtreme filtresine çevirir. */
|
||||||
|
export const mergeFilterTriplets = (triplets: [string, string, any][]) => {
|
||||||
|
const unique = triplets.reduce<[string, string, any][]>((result, current) => {
|
||||||
|
const index = result.findIndex((item) => item[0] === current[0] && item[1] === current[1])
|
||||||
|
if (index >= 0) {
|
||||||
|
result[index] = current
|
||||||
|
} else {
|
||||||
|
result.push(current)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return unique.reduce<any>((result, filter) => (result ? [result, 'and', filter] : filter), null)
|
||||||
|
}
|
||||||
|
|
||||||
|
type UseExtraFiltersParams = {
|
||||||
|
gridDto?: GridDto
|
||||||
|
listFormCode: string
|
||||||
|
searchParams?: URLSearchParams
|
||||||
|
/** Filtre değişince görünümü yenilemek için. */
|
||||||
|
onFilterChanged: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toolbar'daki ek filtre alanlarının state'i, URL filtresiyle birleştirilmesi
|
||||||
|
* ve `searchParams` senkronizasyonu. Grid ve Tree bu hook'u paylaşır.
|
||||||
|
*/
|
||||||
|
export const useExtraFilters = ({
|
||||||
|
gridDto,
|
||||||
|
listFormCode,
|
||||||
|
searchParams,
|
||||||
|
onFilterChanged,
|
||||||
|
}: UseExtraFiltersParams) => {
|
||||||
|
const [extraFilters, setExtraFilters] = useState<GridExtraFilterState[]>([])
|
||||||
|
// İlk render'daki URL filtresi, ek filtrelerin birleştirileceği temeldir.
|
||||||
|
const defaultFilterRef = useRef<string | null>(null)
|
||||||
|
const isMountedRef = useRef(false)
|
||||||
|
|
||||||
|
if (defaultFilterRef.current === null) {
|
||||||
|
defaultFilterRef.current = searchParams?.get('filter') ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setExtraFilters([])
|
||||||
|
isMountedRef.current = false
|
||||||
|
}, [listFormCode])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setExtraFilters(getInitialExtraFilters(gridDto))
|
||||||
|
}, [gridDto])
|
||||||
|
|
||||||
|
const mergedFilter = useMemo(() => {
|
||||||
|
const base = safeJsonParse<any>(defaultFilterRef.current, null, 'Search filter parse error:')
|
||||||
|
|
||||||
|
return mergeFilterTriplets([
|
||||||
|
...extractSearchParamsFields(base),
|
||||||
|
...extraFilters
|
||||||
|
.filter((filter) => !!filter.value)
|
||||||
|
.map(
|
||||||
|
(filter) => [filter.fieldName, filter.operator, filter.value] as [string, string, any],
|
||||||
|
),
|
||||||
|
])
|
||||||
|
}, [extraFilters])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// İlk render'da URL zaten doğru; gereksiz refresh yapmamak için atlanır.
|
||||||
|
if (!isMountedRef.current) {
|
||||||
|
isMountedRef.current = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mergedFilter) {
|
||||||
|
searchParams?.set('filter', JSON.stringify(mergedFilter))
|
||||||
|
} else {
|
||||||
|
searchParams?.delete('filter')
|
||||||
|
}
|
||||||
|
|
||||||
|
onFilterChanged()
|
||||||
|
// onFilterChanged her render'da yeniden oluşabileceği için bağımlılığa alınmaz.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [mergedFilter])
|
||||||
|
|
||||||
|
return { extraFilters, setExtraFilters, mergedFilter }
|
||||||
|
}
|
||||||
115
ui/src/views/list/shared/imageValue.ts
Normal file
115
ui/src/views/list/shared/imageValue.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
/**
|
||||||
|
* Liste görünümlerinde (grid hücresi, image upload/viewer editörleri) kullanılan
|
||||||
|
* ortak görsel değer normalizasyon yardımcıları.
|
||||||
|
*
|
||||||
|
* Daha önce useListFormColumns.ts, ImageViewerEditorComponent.tsx ve
|
||||||
|
* ImageUploadEditorComponent.tsx içinde birebir tekrarlanıyordu.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const NO_IMAGE = '/img/others/no-image.png'
|
||||||
|
|
||||||
|
/** String ya da obje olabilen bir değeri düz objeye çevirir; dizi/geçersiz JSON için undefined döner. */
|
||||||
|
export const parseJsonObject = (value: unknown): Record<string, any> | undefined => {
|
||||||
|
if (!value) return undefined
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
return Array.isArray(value) ? undefined : (value as Record<string, any>)
|
||||||
|
}
|
||||||
|
if (typeof value !== 'string') return undefined
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value)
|
||||||
|
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
||||||
|
? (parsed as Record<string, any>)
|
||||||
|
: undefined
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pozitif sayıya çevirir, aksi halde varsayılanı döner. */
|
||||||
|
export const normalizeImageSize = (value: unknown, fallback: number) => {
|
||||||
|
const size = Number(value)
|
||||||
|
return Number.isFinite(size) && size > 0 ? size : fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Obje formatındaki görsel değerinden (url/src/fileUrl/path/value) kaynağı çıkarır. */
|
||||||
|
export const getImageSource = (value: unknown) => {
|
||||||
|
if (!value) return ''
|
||||||
|
if (typeof value === 'string') return value.trim()
|
||||||
|
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
const item = value as Record<string, unknown>
|
||||||
|
return String(item.url ?? item.src ?? item.fileUrl ?? item.path ?? item.value ?? '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(value).trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
const isProbablyBase64Image = (value: string) =>
|
||||||
|
value.length > 80 && /^[A-Za-z0-9+/]+={0,2}$/.test(value)
|
||||||
|
|
||||||
|
/** Ham base64 içeriğini data URL'e çevirir; zaten URL ise dokunmaz. */
|
||||||
|
export const toImageSource = (value: string) => {
|
||||||
|
const trimmed = value.trim()
|
||||||
|
if (!trimmed) return ''
|
||||||
|
if (trimmed.startsWith('data:image/') || !isProbablyBase64Image(trimmed)) return trimmed
|
||||||
|
return `data:image/jpeg;base64,${trimmed}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const splitImageString = (value: string) => {
|
||||||
|
const trimmed = value.trim()
|
||||||
|
if (!trimmed) return []
|
||||||
|
if (trimmed.startsWith('data:image/')) return [trimmed]
|
||||||
|
|
||||||
|
return trimmed
|
||||||
|
.split(/\r?\n|\|\s*/)
|
||||||
|
.flatMap((part) => {
|
||||||
|
const text = part.trim()
|
||||||
|
if (!text || text.startsWith('data:image/')) return text ? [text] : []
|
||||||
|
return text.split(',').map((item) => item.trim())
|
||||||
|
})
|
||||||
|
.map(toImageSource)
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tek görsel, çoklu görsel, JSON dizi ya da ayraçlı string olabilen değeri
|
||||||
|
* gösterilebilir URL listesine dönüştürür.
|
||||||
|
*/
|
||||||
|
export const normalizeImageValue = (value: unknown): string[] => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.flatMap((item) => normalizeImageValue(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const trimmed = value.trim()
|
||||||
|
if (!trimmed) return []
|
||||||
|
|
||||||
|
if (trimmed.startsWith('[')) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(trimmed)
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
return parsed.flatMap((item) => normalizeImageValue(item))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return [toImageSource(trimmed)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return splitImageString(trimmed)
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = getImageSource(value)
|
||||||
|
return source ? [toImageSource(source)] : []
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kolon yapılandırmasından image upload/viewer seçeneklerini çözer. */
|
||||||
|
export const resolveImageColumnOptions = (column: unknown): Record<string, any> => {
|
||||||
|
const col = column as any
|
||||||
|
const configured = col?.extras?.imageUploadOptions
|
||||||
|
if (configured && Object.keys(configured).length > 0) {
|
||||||
|
return configured
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseJsonObject(col?.extras?.editorOptions) ?? parseJsonObject(col?.editorOptions) ?? {}
|
||||||
|
}
|
||||||
49
ui/src/views/list/shared/workflowRows.tsx
Normal file
49
ui/src/views/list/shared/workflowRows.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
/**
|
||||||
|
* Yeni eklenen kayıt için workflow başlatma.
|
||||||
|
* Grid ve Tree `onRowInserted` içinde aynı akışı kullanır.
|
||||||
|
*/
|
||||||
|
import { Notification, toast } from '@/components/ui'
|
||||||
|
import type { GridDto } from '@/proxy/form/models'
|
||||||
|
import { workflowService } from '@/services/workflow.service'
|
||||||
|
import { getPersistedInsertedKey } from './editingForm'
|
||||||
|
|
||||||
|
export const showWorkflowMessages = (messages: string[] = []) => {
|
||||||
|
if (!messages.length) return
|
||||||
|
|
||||||
|
toast.push(
|
||||||
|
<Notification type="info" duration={7000}>
|
||||||
|
{messages.map((message, messageIndex) => (
|
||||||
|
<div key={messageIndex} className={messageIndex > 0 ? 'mt-2 border-t pt-2' : undefined}>
|
||||||
|
{message.split('\n').map((line, lineIndex) => (
|
||||||
|
<div key={lineIndex}>{line}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Notification>,
|
||||||
|
{ placement: 'bottom-end' },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liste formunda workflow onay alanı tanımlıysa eklenen kayıt için
|
||||||
|
* workflow'u başlatır ve görünümü yeniler.
|
||||||
|
*/
|
||||||
|
export const startWorkflowForInsertedRow = (
|
||||||
|
event: any,
|
||||||
|
gridDto: GridDto,
|
||||||
|
listFormCode: string,
|
||||||
|
refresh: () => void,
|
||||||
|
) => {
|
||||||
|
if (!gridDto.gridOptions.workflowDto?.approvalStatusFieldName) return
|
||||||
|
|
||||||
|
const insertedKey = getPersistedInsertedKey(event, gridDto.gridOptions.keyFieldName)
|
||||||
|
if (insertedKey === undefined) return
|
||||||
|
|
||||||
|
workflowService
|
||||||
|
.startWorkflow(listFormCode, [insertedKey])
|
||||||
|
.then((result) => {
|
||||||
|
showWorkflowMessages(result.toastMessages ?? [])
|
||||||
|
refresh()
|
||||||
|
})
|
||||||
|
.catch((error) => console.error('Workflow başlatılamadı:', error))
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,7 @@ import dxGantt from 'devextreme/ui/gantt'
|
||||||
import dxCardView from 'devextreme/ui/card_view'
|
import dxCardView from 'devextreme/ui/card_view'
|
||||||
import { Dispatch, MutableRefObject, SetStateAction, useEffect, useState } from 'react'
|
import { Dispatch, MutableRefObject, SetStateAction, useEffect, useState } from 'react'
|
||||||
import { setGridPanelColor } from './Utils'
|
import { setGridPanelColor } from './Utils'
|
||||||
|
import { FILTERED_GRID_PANEL_COLOR } from './shared/columns'
|
||||||
import { usePermission } from '@/utils/hooks/usePermission'
|
import { usePermission } from '@/utils/hooks/usePermission'
|
||||||
import { usePWA } from '@/utils/hooks/usePWA'
|
import { usePWA } from '@/utils/hooks/usePWA'
|
||||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||||
|
|
@ -305,9 +306,6 @@ const useFilters = ({
|
||||||
const [isImportModalOpen, setIsImportModalOpen] = useState(false)
|
const [isImportModalOpen, setIsImportModalOpen] = useState(false)
|
||||||
const [isPermissionUsersModalOpen, setIsPermissionUsersModalOpen] = useState(false)
|
const [isPermissionUsersModalOpen, setIsPermissionUsersModalOpen] = useState(false)
|
||||||
|
|
||||||
const filteredGridPanelColor = 'rgba(10, 200, 10, 0.5)' // kullanici tanimli filtre ile filtrelenmis gridin paneline ait renk
|
|
||||||
//const statedGridPanelColor = 'rgba(50, 200, 200, 0.5)' // kullanici tanimli gridState ile islem gormus gridin paneline ait renk
|
|
||||||
|
|
||||||
const grdOpt = gridDto?.gridOptions
|
const grdOpt = gridDto?.gridOptions
|
||||||
const config = useStoreState((state) => state.abpConfig.config)
|
const config = useStoreState((state) => state.abpConfig.config)
|
||||||
const toToolbarText = (text: string) => text
|
const toToolbarText = (text: string) => text
|
||||||
|
|
@ -558,7 +556,7 @@ const useFilters = ({
|
||||||
if (filter) {
|
if (filter) {
|
||||||
setFilterValue(grid, JSON.parse(filter.customizationData))
|
setFilterValue(grid, JSON.parse(filter.customizationData))
|
||||||
setFilterPanelVisible(grid, true)
|
setFilterPanelVisible(grid, true)
|
||||||
setGridPanelColor(filteredGridPanelColor)
|
setGridPanelColor(FILTERED_GRID_PANEL_COLOR)
|
||||||
} else {
|
} else {
|
||||||
clearGridFilter(grid)
|
clearGridFilter(grid)
|
||||||
setFilterPanelVisible(grid, false)
|
setFilterPanelVisible(grid, false)
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,18 @@ import {
|
||||||
UiLookupDataSourceTypeEnum,
|
UiLookupDataSourceTypeEnum,
|
||||||
} from '@/proxy/form/models'
|
} from '@/proxy/form/models'
|
||||||
import { addCss, getEditingFormFields } from './Utils'
|
import { addCss, getEditingFormFields } from './Utils'
|
||||||
|
import { cachedLookupLoad, clearLookupCache } from '../shared/lookupCache'
|
||||||
const NO_IMAGE = '/img/others/no-image.png'
|
import {
|
||||||
|
NO_IMAGE,
|
||||||
|
normalizeImageSize,
|
||||||
|
normalizeImageValue,
|
||||||
|
resolveImageColumnOptions,
|
||||||
|
} from './shared/imageValue'
|
||||||
|
import {
|
||||||
|
hideImageHoverPreview,
|
||||||
|
openImageInNewTab,
|
||||||
|
showImageHoverPreview,
|
||||||
|
} from '../form/editors/imageHoverPreview'
|
||||||
|
|
||||||
const cellTemplateMultiValue = (
|
const cellTemplateMultiValue = (
|
||||||
cellElement: HTMLElement,
|
cellElement: HTMLElement,
|
||||||
|
|
@ -65,260 +75,41 @@ const cellTemplateMultiValue = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hover preview overlay — singleton, tüm grid hücreleri tarafından paylaşılır
|
// Görsel hücre şablonu. Normalizasyon ve hover önizleme ortak modüllerden gelir.
|
||||||
let __imgPreviewEl: HTMLDivElement | null = null
|
|
||||||
let __imgPreviewTarget: HTMLElement | null = null
|
|
||||||
let __imgPreviewListenersBound = false
|
|
||||||
|
|
||||||
function hideImgPreview() {
|
|
||||||
__imgPreviewTarget = null
|
|
||||||
if (__imgPreviewEl) {
|
|
||||||
__imgPreviewEl.style.opacity = '0'
|
|
||||||
__imgPreviewEl.style.display = 'none'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindImgPreviewDismissListeners() {
|
|
||||||
if (__imgPreviewListenersBound) return
|
|
||||||
__imgPreviewListenersBound = true
|
|
||||||
|
|
||||||
document.addEventListener(
|
|
||||||
'pointermove',
|
|
||||||
(event) => {
|
|
||||||
const target = event.target as Node | null
|
|
||||||
if (
|
|
||||||
__imgPreviewTarget &&
|
|
||||||
(!__imgPreviewTarget.isConnected || !target || !__imgPreviewTarget.contains(target))
|
|
||||||
) {
|
|
||||||
hideImgPreview()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ passive: true },
|
|
||||||
)
|
|
||||||
document.addEventListener('scroll', hideImgPreview, true)
|
|
||||||
window.addEventListener('blur', hideImgPreview)
|
|
||||||
document.addEventListener('visibilitychange', () => {
|
|
||||||
if (document.hidden) hideImgPreview()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function getImgPreview(): HTMLDivElement {
|
|
||||||
if (!__imgPreviewEl) {
|
|
||||||
const el = document.createElement('div')
|
|
||||||
el.id = '__cellImgPreview'
|
|
||||||
el.style.cssText = [
|
|
||||||
'position:fixed',
|
|
||||||
'z-index:99999',
|
|
||||||
'display:none',
|
|
||||||
'pointer-events:none',
|
|
||||||
'background:#fff',
|
|
||||||
'border:1px solid #d1d5db',
|
|
||||||
'border-radius:8px',
|
|
||||||
'box-shadow:0 8px 32px rgba(0,0,0,0.22)',
|
|
||||||
'padding:4px',
|
|
||||||
'max-width:320px',
|
|
||||||
'max-height:320px',
|
|
||||||
'overflow:hidden',
|
|
||||||
'transition:opacity 0.15s ease',
|
|
||||||
'opacity:0',
|
|
||||||
].join(';')
|
|
||||||
const img = document.createElement('img')
|
|
||||||
img.onerror = null
|
|
||||||
img.src = NO_IMAGE
|
|
||||||
img.style.cssText =
|
|
||||||
'display:block;max-width:312px;max-height:312px;object-fit:contain;border-radius:4px;'
|
|
||||||
el.appendChild(img)
|
|
||||||
document.body.appendChild(el)
|
|
||||||
__imgPreviewEl = el
|
|
||||||
bindImgPreviewDismissListeners()
|
|
||||||
}
|
|
||||||
return __imgPreviewEl
|
|
||||||
}
|
|
||||||
|
|
||||||
function showImgPreview(src: string, e: MouseEvent) {
|
|
||||||
const el = getImgPreview()
|
|
||||||
__imgPreviewTarget = e.currentTarget instanceof HTMLElement ? e.currentTarget : null
|
|
||||||
const imgEl = el.querySelector('img') as HTMLImageElement
|
|
||||||
imgEl.onerror = () => {
|
|
||||||
imgEl.onerror = null
|
|
||||||
imgEl.src = NO_IMAGE
|
|
||||||
}
|
|
||||||
if (imgEl.src !== src) imgEl.src = src
|
|
||||||
|
|
||||||
const GAP = 12
|
|
||||||
const vw = window.innerWidth
|
|
||||||
const vh = window.innerHeight
|
|
||||||
|
|
||||||
el.style.opacity = '0'
|
|
||||||
el.style.display = 'block'
|
|
||||||
|
|
||||||
const pw = el.offsetWidth || 320
|
|
||||||
const ph = el.offsetHeight || 320
|
|
||||||
let left = e.clientX + GAP
|
|
||||||
let top = e.clientY + GAP
|
|
||||||
|
|
||||||
if (left + pw > vw - 8) left = e.clientX - pw - GAP
|
|
||||||
if (top + ph > vh - 8) top = e.clientY - ph - GAP
|
|
||||||
if (left < 8) left = 8
|
|
||||||
if (top < 8) top = 8
|
|
||||||
|
|
||||||
el.style.left = `${left}px`
|
|
||||||
el.style.top = `${top}px`
|
|
||||||
el.style.opacity = '1'
|
|
||||||
}
|
|
||||||
|
|
||||||
const cellTemplateImage = (
|
const cellTemplateImage = (
|
||||||
cellElement: HTMLElement,
|
cellElement: HTMLElement,
|
||||||
cellInfo: DataGridTypes.ColumnCellTemplateData<any, any>,
|
cellInfo: DataGridTypes.ColumnCellTemplateData<any, any>,
|
||||||
) => {
|
) => {
|
||||||
if (cellInfo?.value) {
|
if (!cellInfo?.value) return
|
||||||
const urls = normalizeImageCellValue(cellInfo.value)
|
|
||||||
const imgOptions = getImageColumnOptions(cellInfo.column)
|
|
||||||
const w = normalizeImageSize(imgOptions.width, 40)
|
|
||||||
const h = normalizeImageSize(imgOptions.height, 40)
|
|
||||||
|
|
||||||
cellElement.style.cssText += 'display:flex;flex-wrap:wrap;align-items:center;gap:4px;'
|
const urls = normalizeImageValue(cellInfo.value)
|
||||||
cellElement.innerHTML = ''
|
const imageOptions = resolveImageColumnOptions(cellInfo.column)
|
||||||
//cellElement.title = urls.join(', ')
|
const width = normalizeImageSize(imageOptions.width, 40)
|
||||||
|
const height = normalizeImageSize(imageOptions.height, 40)
|
||||||
|
|
||||||
urls.forEach((url) => {
|
cellElement.style.cssText += 'display:flex;flex-wrap:wrap;align-items:center;gap:4px;'
|
||||||
const img = document.createElement('img')
|
cellElement.innerHTML = ''
|
||||||
img.onerror = () => {
|
|
||||||
img.onerror = null
|
|
||||||
img.src = NO_IMAGE
|
|
||||||
}
|
|
||||||
img.src = url
|
|
||||||
img.alt = ''
|
|
||||||
img.style.cssText = `width:${w}px;height:${h}px;object-fit:cover;border-radius:4px;margin:2px;vertical-align:middle;display:inline-block;cursor:zoom-in;`
|
|
||||||
|
|
||||||
img.addEventListener('mouseenter', (e) => showImgPreview(url, e as MouseEvent))
|
urls.forEach((url) => {
|
||||||
img.addEventListener('mousemove', (e) => showImgPreview(url, e as MouseEvent))
|
const img = document.createElement('img')
|
||||||
img.addEventListener('mouseleave', hideImgPreview)
|
img.onerror = () => {
|
||||||
img.addEventListener('click', () => {
|
img.onerror = null
|
||||||
hideImgPreview()
|
img.src = NO_IMAGE
|
||||||
openImageSource(url)
|
|
||||||
})
|
|
||||||
|
|
||||||
cellElement.appendChild(img)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizeImageSize = (value: unknown, fallback: number) => {
|
|
||||||
const size = Number(value)
|
|
||||||
return Number.isFinite(size) && size > 0 ? size : fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseJsonObject = (value: unknown) => {
|
|
||||||
if (!value) return undefined
|
|
||||||
if (typeof value === 'object') return value as Record<string, any>
|
|
||||||
if (typeof value !== 'string') return undefined
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(value)
|
|
||||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
||||||
? (parsed as Record<string, any>)
|
|
||||||
: undefined
|
|
||||||
} catch {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getImageColumnOptions = (column: unknown) => {
|
|
||||||
const col = column as any
|
|
||||||
return (
|
|
||||||
col?.extras?.imageUploadOptions ??
|
|
||||||
parseJsonObject(col?.extras?.editorOptions) ??
|
|
||||||
parseJsonObject(col?.editorOptions) ??
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getImageSource = (value: unknown) => {
|
|
||||||
if (!value) return ''
|
|
||||||
if (typeof value === 'string') return value.trim()
|
|
||||||
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
const item = value as Record<string, unknown>
|
|
||||||
return String(item.url ?? item.src ?? item.fileUrl ?? item.path ?? item.value ?? '').trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
return String(value).trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
const isProbablyBase64Image = (value: string) =>
|
|
||||||
value.length > 80 && /^[A-Za-z0-9+/]+={0,2}$/.test(value)
|
|
||||||
|
|
||||||
const toImageSource = (value: string) => {
|
|
||||||
const trimmed = value.trim()
|
|
||||||
if (!trimmed) return ''
|
|
||||||
if (trimmed.startsWith('data:image/') || !isProbablyBase64Image(trimmed)) return trimmed
|
|
||||||
return `data:image/jpeg;base64,${trimmed}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const openImageSource = (url: string) => {
|
|
||||||
if (!url.startsWith('data:image/')) {
|
|
||||||
window.open(url, '_blank', 'noopener,noreferrer')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const match = url.match(/^data:(image\/[^;]+);base64,(.*)$/)
|
|
||||||
if (!match) return
|
|
||||||
|
|
||||||
const binary = atob(match[2])
|
|
||||||
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0))
|
|
||||||
const blobUrl = URL.createObjectURL(new Blob([bytes], { type: match[1] }))
|
|
||||||
window.open(blobUrl, '_blank', 'noopener,noreferrer')
|
|
||||||
window.setTimeout(() => URL.revokeObjectURL(blobUrl), 60000)
|
|
||||||
} catch {
|
|
||||||
window.open(url, '_blank', 'noopener,noreferrer')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const splitImageString = (value: string) => {
|
|
||||||
const trimmed = value.trim()
|
|
||||||
if (!trimmed) return []
|
|
||||||
if (trimmed.startsWith('data:image/')) return [trimmed]
|
|
||||||
|
|
||||||
return trimmed
|
|
||||||
.split(/\r?\n|\|\s*/)
|
|
||||||
.flatMap((part) => {
|
|
||||||
const text = part.trim()
|
|
||||||
if (!text || text.startsWith('data:image/')) return text ? [text] : []
|
|
||||||
return text.split(',').map((item) => item.trim())
|
|
||||||
})
|
|
||||||
.map(toImageSource)
|
|
||||||
.filter(Boolean)
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizeImageCellValue = (value: unknown): string[] => {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value.flatMap((item) => normalizeImageCellValue(item))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
const trimmed = value.trim()
|
|
||||||
if (!trimmed) return []
|
|
||||||
|
|
||||||
if (trimmed.startsWith('[')) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(trimmed)
|
|
||||||
if (Array.isArray(parsed)) {
|
|
||||||
return parsed.flatMap((item) => normalizeImageCellValue(item))
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return [toImageSource(trimmed)]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
img.src = url
|
||||||
|
img.alt = ''
|
||||||
|
img.style.cssText = `width:${width}px;height:${height}px;object-fit:cover;border-radius:4px;margin:2px;vertical-align:middle;display:inline-block;cursor:zoom-in;`
|
||||||
|
|
||||||
return splitImageString(trimmed)
|
img.addEventListener('mouseenter', (event) => showImageHoverPreview(url, event))
|
||||||
}
|
img.addEventListener('mousemove', (event) => showImageHoverPreview(url, event))
|
||||||
|
img.addEventListener('mouseleave', hideImageHoverPreview)
|
||||||
|
img.addEventListener('click', () => openImageInNewTab(url))
|
||||||
|
|
||||||
const source = getImageSource(value)
|
cellElement.appendChild(img)
|
||||||
return source ? [toImageSource(source)] : []
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function calculateFilterExpressionMultiValue(
|
function calculateFilterExpressionMultiValue(
|
||||||
this: DataGridTypes.Column,
|
this: DataGridTypes.Column,
|
||||||
filterValue: any,
|
filterValue: any,
|
||||||
|
|
@ -360,40 +151,6 @@ function calculateFilterExpressionMultiValue(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// lookup cache (module scope) - cache süresini ve boyutunu yönet
|
|
||||||
const __lookupCache = new Map<string, { promise: Promise<any[]>; timestamp: number }>()
|
|
||||||
const CACHE_DURATION = 5 * 60 * 1000 // 5 dakika
|
|
||||||
const MAX_CACHE_SIZE = 100 // Maksimum cache entry sayısı
|
|
||||||
|
|
||||||
const cachedLoader = (key: string, loader: () => Promise<any[]>) => {
|
|
||||||
const now = Date.now()
|
|
||||||
const cached = __lookupCache.get(key)
|
|
||||||
|
|
||||||
// Cache'de var ve süresi dolmamışsa kullan
|
|
||||||
if (cached && now - cached.timestamp < CACHE_DURATION) {
|
|
||||||
return cached.promise
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache boyutu limitini aşarsa en eskiyi temizle
|
|
||||||
if (__lookupCache.size >= MAX_CACHE_SIZE) {
|
|
||||||
const oldestKey = Array.from(__lookupCache.entries()).sort(
|
|
||||||
(a, b) => a[1].timestamp - b[1].timestamp,
|
|
||||||
)[0][0]
|
|
||||||
__lookupCache.delete(oldestKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
const p = Promise.resolve()
|
|
||||||
.then(() => loader())
|
|
||||||
.then((res) => res ?? [])
|
|
||||||
.catch((err) => {
|
|
||||||
__lookupCache.delete(key) // hata olursa tekrar denenebilsin
|
|
||||||
throw err
|
|
||||||
})
|
|
||||||
|
|
||||||
__lookupCache.set(key, { promise: p, timestamp: now })
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
const useListFormColumns = ({
|
const useListFormColumns = ({
|
||||||
gridDto,
|
gridDto,
|
||||||
listFormCode,
|
listFormCode,
|
||||||
|
|
@ -414,7 +171,7 @@ const useListFormColumns = ({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// listFormCode değişince lookup cache temizlensin (farklı form farklı lookuplar)
|
// listFormCode değişince lookup cache temizlensin (farklı form farklı lookuplar)
|
||||||
__lookupCache.clear()
|
clearLookupCache()
|
||||||
}, [listFormCode])
|
}, [listFormCode])
|
||||||
|
|
||||||
const lookupDataSource = useCallback(
|
const lookupDataSource = useCallback(
|
||||||
|
|
@ -523,7 +280,7 @@ const useListFormColumns = ({
|
||||||
loadMode: 'raw',
|
loadMode: 'raw',
|
||||||
load: async () => {
|
load: async () => {
|
||||||
// load fonksiyonu sync sonuç döndürüyor olabilir, o yüzden Promise.resolve ile sar
|
// load fonksiyonu sync sonuç döndürüyor olabilir, o yüzden Promise.resolve ile sar
|
||||||
return cachedLoader(`static:${key}`, async () => {
|
return cachedLookupLoad(`static:${key}`, async () => {
|
||||||
const result = await Promise.resolve(load())
|
const result = await Promise.resolve(load())
|
||||||
if (result === undefined || result === null) return []
|
if (result === undefined || result === null) return []
|
||||||
return Array.isArray(result) ? result : [result]
|
return Array.isArray(result) ? result : [result]
|
||||||
|
|
@ -548,7 +305,7 @@ const useListFormColumns = ({
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cacheKey = `query:${listFormCode}:${listFormFieldName}:${JSON.stringify(filters ?? null)}`
|
const cacheKey = `query:${listFormCode}:${listFormFieldName}:${JSON.stringify(filters ?? null)}`
|
||||||
return cachedLoader(cacheKey, async () => {
|
return cachedLookupLoad(cacheKey, async () => {
|
||||||
const response = await dynamicFetch('list-form-select/lookup', 'POST', null, {
|
const response = await dynamicFetch('list-form-select/lookup', 'POST', null, {
|
||||||
listFormCode,
|
listFormCode,
|
||||||
listFormFieldName,
|
listFormFieldName,
|
||||||
|
|
@ -600,7 +357,7 @@ const useListFormColumns = ({
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cacheKey = `api:${lookupQuery}:${JSON.stringify(filters ?? null)}`
|
const cacheKey = `api:${lookupQuery}:${JSON.stringify(filters ?? null)}`
|
||||||
return cachedLoader(cacheKey, async () => {
|
return cachedLookupLoad(cacheKey, async () => {
|
||||||
const response = await dynamicFetch(url, method, null, resolvedBody)
|
const response = await dynamicFetch(url, method, null, resolvedBody)
|
||||||
let { data } = response
|
let { data } = response
|
||||||
if (!data) return []
|
if (!data) return []
|
||||||
|
|
@ -1056,12 +813,14 @@ const useListFormColumns = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const col of gridDto.columnFormats) {
|
for (const col of gridDto.columnFormats) {
|
||||||
|
// NOT: Buradaki atlamalar `return` değil `continue` olmalı; aksi halde
|
||||||
|
// band'e dahil ilk tekrar eden kolonda tüm kolon listesi kayboluyordu.
|
||||||
if (!col.fieldName) {
|
if (!col.fieldName) {
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
if (insertedColumns.some((a) => a === col.fieldName)) {
|
if (insertedColumns.includes(col.fieldName)) {
|
||||||
// kolon zaten eklenmis ise islem yapma
|
// kolon zaten eklenmis ise islem yapma
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (col.bandName) {
|
if (col.bandName) {
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,33 @@ import { GridOptionsDto } from '@/proxy/form/models'
|
||||||
import { GridColumnData } from './GridColumnData'
|
import { GridColumnData } from './GridColumnData'
|
||||||
import { dynamicFetch } from '@/services/form.service'
|
import { dynamicFetch } from '@/services/form.service'
|
||||||
import { MULTIVALUE_DELIMITER } from '@/constants/app.constant'
|
import { MULTIVALUE_DELIMITER } from '@/constants/app.constant'
|
||||||
|
import { FILTERED_GRID_PANEL_COLOR } from './shared/columns'
|
||||||
import type { TreeListRef } from 'devextreme-react/cjs/tree-list'
|
import type { TreeListRef } from 'devextreme-react/cjs/tree-list'
|
||||||
import type { GanttRef } from 'devextreme-react/cjs/gantt'
|
import type { GanttRef } from 'devextreme-react/cjs/gantt'
|
||||||
import type { SchedulerRef } from 'devextreme-react/cjs/scheduler'
|
import type { SchedulerRef } from 'devextreme-react/cjs/scheduler'
|
||||||
import type { CardViewRef, CardViewTypes } from 'devextreme-react/cjs/card-view'
|
import type { CardViewRef, CardViewTypes } from 'devextreme-react/cjs/card-view'
|
||||||
|
|
||||||
const filteredGridPanelColor = 'rgba(10, 200, 10, 0.5)' // kullanici tanimli filtre ile filtrelenmis gridin paneline ait renk
|
|
||||||
|
/** URL'den gelen varsayılan filtre ile grid filtresini `and` ile birleştirir. */
|
||||||
|
const mergeWithDefaultFilter = (gridFilter: any, searchParams?: URLSearchParams) => {
|
||||||
|
const defaultFilter = safeJsonParse(
|
||||||
|
searchParams?.get('filter'),
|
||||||
|
null,
|
||||||
|
'Search filter parse error:',
|
||||||
|
)
|
||||||
|
|
||||||
|
if (defaultFilter && gridFilter) return [defaultFilter, 'and', gridFilter]
|
||||||
|
return defaultFilter ?? gridFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Filtre doluysa serialize eder, boşsa parametreyi hiç göndermez. */
|
||||||
|
const applyCombinedFilter = (parameters: any, combinedFilter: any) => {
|
||||||
|
if (combinedFilter && combinedFilter.length > 0) {
|
||||||
|
parameters.filter = JSON.stringify(combinedFilter)
|
||||||
|
} else {
|
||||||
|
delete parameters.filter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const toInsertedRowData = (values: any, responseData: any, keyFieldName?: string | null) => {
|
const toInsertedRowData = (values: any, responseData: any, keyFieldName?: string | null) => {
|
||||||
if (!keyFieldName) {
|
if (!keyFieldName) {
|
||||||
|
|
@ -120,21 +141,6 @@ const useListFormCustomDataSource = ({
|
||||||
|
|
||||||
Object.assign(parameters, chartParameters)
|
Object.assign(parameters, chartParameters)
|
||||||
}
|
}
|
||||||
// 1. Default filter'ı al
|
|
||||||
const defaultFilter = safeJsonParse(
|
|
||||||
searchParams?.get('filter'),
|
|
||||||
null,
|
|
||||||
'Search filter parse error:',
|
|
||||||
)
|
|
||||||
|
|
||||||
let combinedFilter: any = parameters.filter
|
|
||||||
|
|
||||||
// 2. Eğer hem default hem de grid filter varsa merge et
|
|
||||||
if (defaultFilter && combinedFilter) {
|
|
||||||
combinedFilter = [defaultFilter, 'and', combinedFilter]
|
|
||||||
} else if (defaultFilter) {
|
|
||||||
combinedFilter = defaultFilter
|
|
||||||
}
|
|
||||||
//editing asamasinda her bir field de yapilan degisiklik load istegi olarak buraya dusuyor.
|
//editing asamasinda her bir field de yapilan degisiklik load istegi olarak buraya dusuyor.
|
||||||
//TODO: bu bug halen devam ediyor!!
|
//TODO: bu bug halen devam ediyor!!
|
||||||
//Bunu engellemek icin eklendi.
|
//Bunu engellemek icin eklendi.
|
||||||
|
|
@ -176,13 +182,13 @@ const useListFormCustomDataSource = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (combinedFilter && combinedFilter.length > 0) {
|
// NOT: Birleştirme, yukarıdaki kolon bazlı URL filtreleri parameters.filter'a
|
||||||
parameters.filter = JSON.stringify(combinedFilter)
|
// eklendikten SONRA yapılmalı; aksi halde bu filtreler sessizce kayboluyordu.
|
||||||
} else {
|
applyCombinedFilter(
|
||||||
delete parameters.filter // hiç göndermesin
|
parameters,
|
||||||
}
|
mergeWithDefaultFilter(parameters.filter, searchParams),
|
||||||
|
)
|
||||||
|
|
||||||
//parameters.filter = JSON.stringify(parameters.filter)
|
|
||||||
const response = await dynamicFetch('list-form-select/select', 'GET', parameters)
|
const response = await dynamicFetch('list-form-select/select', 'GET', parameters)
|
||||||
|
|
||||||
// Chart için grouped data'yı chart formatına çevir
|
// Chart için grouped data'yı chart formatına çevir
|
||||||
|
|
@ -276,7 +282,7 @@ const useListFormCustomDataSource = ({
|
||||||
// gride filtre uygulanmis ise renklendir
|
// gride filtre uygulanmis ise renklendir
|
||||||
setGridPanelColor(
|
setGridPanelColor(
|
||||||
response.data.queryInfos?.isAppliedGridFilter
|
response.data.queryInfos?.isAppliedGridFilter
|
||||||
? filteredGridPanelColor
|
? FILTERED_GRID_PANEL_COLOR
|
||||||
: 'transparent',
|
: 'transparent',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -315,27 +321,9 @@ const useListFormCustomDataSource = ({
|
||||||
group: '',
|
group: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
// 1. Default filter'ı al
|
const combinedFilter = mergeWithDefaultFilter(parameters.filter, searchParams)
|
||||||
const defaultFilter = safeJsonParse(
|
|
||||||
searchParams?.get('filter'),
|
|
||||||
null,
|
|
||||||
'Search filter parse error:',
|
|
||||||
)
|
|
||||||
|
|
||||||
let combinedFilter: any = parameters.filter
|
applyCombinedFilter(parameters, combinedFilter)
|
||||||
|
|
||||||
// 2. Eğer hem default hem de grid filter varsa merge et
|
|
||||||
if (defaultFilter && combinedFilter) {
|
|
||||||
combinedFilter = [defaultFilter, 'and', combinedFilter]
|
|
||||||
} else if (defaultFilter) {
|
|
||||||
combinedFilter = defaultFilter
|
|
||||||
}
|
|
||||||
|
|
||||||
if (combinedFilter && combinedFilter.length > 0) {
|
|
||||||
parameters.filter = JSON.stringify(combinedFilter)
|
|
||||||
} else {
|
|
||||||
delete parameters.filter // hiç göndermesin
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await dynamicFetch('list-form-select/select', 'GET', parameters)
|
const response = await dynamicFetch('list-form-select/select', 'GET', parameters)
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ import {
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { MutableRefObject, useCallback, useEffect, useMemo, useRef } from 'react'
|
import { MutableRefObject, useCallback, useEffect, useMemo, useRef } from 'react'
|
||||||
import { setGridPanelColor } from './Utils'
|
import { setGridPanelColor } from './Utils'
|
||||||
|
import { STATED_GRID_PANEL_COLOR } from './shared/columns'
|
||||||
|
|
||||||
const statedGridPanelColor = 'rgba(50, 200, 200, 0.5)'
|
|
||||||
const defaultSaveDebounceMs = 500
|
const defaultSaveDebounceMs = 500
|
||||||
|
|
||||||
type PendingSave = {
|
type PendingSave = {
|
||||||
|
|
@ -139,7 +139,7 @@ export const useListFormStateStoring = ({
|
||||||
customizationData,
|
customizationData,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setGridPanelColor(statedGridPanelColor)
|
setGridPanelColor(STATED_GRID_PANEL_COLOR)
|
||||||
showSaveSuccessToast()
|
showSaveSuccessToast()
|
||||||
pendingSave?.resolve()
|
pendingSave?.resolve()
|
||||||
})
|
})
|
||||||
|
|
@ -178,7 +178,7 @@ export const useListFormStateStoring = ({
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const state = JSON.parse(customizationData)
|
const state = JSON.parse(customizationData)
|
||||||
setGridPanelColor(statedGridPanelColor)
|
setGridPanelColor(STATED_GRID_PANEL_COLOR)
|
||||||
return state
|
return state
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Grid state parse error:', err)
|
console.error('Grid state parse error:', err)
|
||||||
|
|
|
||||||
|
|
@ -1,111 +1,52 @@
|
||||||
import React, { lazy, Suspense, useEffect } from 'react'
|
import { lazy, Suspense } from 'react'
|
||||||
|
import type { FC } 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 { useLocation, useParams } from 'react-router-dom'
|
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 { useReportRequestSetup, useReportShellTheme, useReportUrl } from './shared/reportShell'
|
||||||
import { MODE_DARK } from '@/constants/theme.constant'
|
|
||||||
import { useStoreState } from '@/store/store'
|
|
||||||
|
|
||||||
const ReportDesigner = lazy(() => import('devexpress-reporting-react/dx-report-designer'))
|
const ReportDesigner = lazy(() => import('devexpress-reporting-react/dx-report-designer'))
|
||||||
|
|
||||||
const VIEWER_THEME_LINK_ID = 'devexpress-report-viewer-theme'
|
const DevexpressReportDesigner: FC = () => {
|
||||||
const REPORT_DESIGNER_CSS_ID = 'devexpress-report-designer-css'
|
|
||||||
|
|
||||||
const getViewerClassName = (isDarkMode: boolean) =>
|
|
||||||
[
|
|
||||||
'dx-viewport',
|
|
||||||
'dx-device-desktop',
|
|
||||||
'dx-device-generic',
|
|
||||||
'dx-theme-generic',
|
|
||||||
'dx-theme-generic-typography',
|
|
||||||
isDarkMode ? 'dx-color-scheme-dark' : 'dx-color-scheme-light',
|
|
||||||
'report-viewer-shell',
|
|
||||||
isDarkMode ? 'report-viewer-shell-dark' : '',
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ')
|
|
||||||
|
|
||||||
const loadViewerCss = (isDarkMode: boolean) => {
|
|
||||||
const styles = [
|
|
||||||
new URL('@devexpress/analytics-core/dist/css/dx-analytics.common.css', import.meta.url).href,
|
|
||||||
new URL('devexpress-reporting/dist/css/dx-webdocumentviewer.css', import.meta.url).href,
|
|
||||||
]
|
|
||||||
|
|
||||||
styles.forEach((href) => {
|
|
||||||
if (document.querySelector(`link[href="${href}"]`)) return
|
|
||||||
const link = document.createElement('link')
|
|
||||||
link.rel = 'stylesheet'
|
|
||||||
link.href = href
|
|
||||||
document.head.appendChild(link)
|
|
||||||
})
|
|
||||||
|
|
||||||
const themeHref = isDarkMode
|
|
||||||
? new URL('@devexpress/analytics-core/dist/css/dx-analytics.dark.css', import.meta.url).href
|
|
||||||
: new URL('@devexpress/analytics-core/dist/css/dx-analytics.light.css', import.meta.url).href
|
|
||||||
|
|
||||||
let themeLink = document.getElementById(VIEWER_THEME_LINK_ID) as HTMLLinkElement | null
|
|
||||||
if (!themeLink) {
|
|
||||||
themeLink = document.createElement('link')
|
|
||||||
themeLink.id = VIEWER_THEME_LINK_ID
|
|
||||||
themeLink.rel = 'stylesheet'
|
|
||||||
document.head.appendChild(themeLink)
|
|
||||||
}
|
|
||||||
if (themeLink.href !== themeHref) {
|
|
||||||
themeLink.href = themeHref
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!document.getElementById(REPORT_DESIGNER_CSS_ID)) {
|
|
||||||
const designerLink = document.createElement('link')
|
|
||||||
designerLink.id = REPORT_DESIGNER_CSS_ID
|
|
||||||
designerLink.rel = 'stylesheet'
|
|
||||||
designerLink.href = new URL(
|
|
||||||
'devexpress-reporting/dist/css/dx-reportdesigner.css',
|
|
||||||
import.meta.url,
|
|
||||||
).href
|
|
||||||
document.head.appendChild(designerLink)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const DevexpressReportDesigner: React.FC = () => {
|
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
const { report, id, listFormCode } = useParams<{
|
const { report, id, listFormCode } = useParams<{
|
||||||
report: string
|
report: string
|
||||||
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(
|
const { shellClassName } = useReportShellTheme(true)
|
||||||
|
const { cultureName, isRequestConfigured } = useReportRequestSetup()
|
||||||
|
const reportUrlWithParams = useReportUrl(
|
||||||
{ report, id, listFormCode },
|
{ report, id, listFormCode },
|
||||||
location.search,
|
location.search,
|
||||||
|
cultureName,
|
||||||
'design',
|
'design',
|
||||||
)
|
)
|
||||||
useEffect(() => {
|
|
||||||
loadViewerCss(isDarkMode)
|
|
||||||
}, [isDarkMode])
|
|
||||||
|
|
||||||
if (!reportUrlWithParams) return null
|
if (!reportUrlWithParams) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className={getViewerClassName(isDarkMode)}>
|
<Container className={shellClassName}>
|
||||||
<Helmet
|
<Helmet
|
||||||
titleTemplate={`%s | ${APP_NAME}`}
|
titleTemplate={`%s | ${APP_NAME}`}
|
||||||
title={translate('::App.Reports')}
|
title={translate('::App.Reports')}
|
||||||
defaultTitle={APP_NAME}
|
defaultTitle={APP_NAME}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Suspense fallback={<div>{translate('::App.Loading')}</div>}>
|
{isRequestConfigured && (
|
||||||
<ReportDesigner reportUrl={reportUrlWithParams}>
|
<Suspense fallback={<div>{translate('::App.Loading')}</div>}>
|
||||||
<RequestOptions
|
<ReportDesigner key={reportUrlWithParams} reportUrl={reportUrlWithParams}>
|
||||||
host={`${import.meta.env.VITE_API_URL}/`}
|
<RequestOptions
|
||||||
getDesignerModelAction="DXXRD/GetDesignerModel"
|
host={`${import.meta.env.VITE_API_URL}/`}
|
||||||
/>
|
getDesignerModelAction="DXXRD/GetDesignerModel"
|
||||||
</ReportDesigner>
|
/>
|
||||||
</Suspense>
|
</ReportDesigner>
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useRef } from 'react'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import { Container } from '@/components/shared'
|
import { Container } from '@/components/shared'
|
||||||
import { Helmet } from 'react-helmet'
|
import { Helmet } from 'react-helmet'
|
||||||
|
|
@ -8,14 +8,10 @@ import ReportViewer, {
|
||||||
Callbacks,
|
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 { ZoomAutoBy } from 'devexpress-reporting/viewer/constants'
|
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 { useReportRequestSetup, useReportShellTheme, useReportUrl } from './shared/reportShell'
|
||||||
import { useStoreState } from '@/store'
|
|
||||||
import { createReportQueryString, createReportUrl } from './reportRouteParams'
|
|
||||||
|
|
||||||
const VIEWER_THEME_LINK_ID = 'devexpress-report-viewer-theme'
|
|
||||||
const DEFAULT_REPORT_ZOOM = 1
|
const DEFAULT_REPORT_ZOOM = 1
|
||||||
|
|
||||||
interface ReportPreviewModel {
|
interface ReportPreviewModel {
|
||||||
|
|
@ -34,50 +30,6 @@ interface ReportPreviewContainer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getViewerClassName = (isDarkMode: boolean) =>
|
|
||||||
[
|
|
||||||
'dx-viewport',
|
|
||||||
'dx-device-desktop',
|
|
||||||
'dx-device-generic',
|
|
||||||
'dx-theme-generic',
|
|
||||||
'dx-theme-generic-typography',
|
|
||||||
isDarkMode ? 'dx-color-scheme-dark' : 'dx-color-scheme-light',
|
|
||||||
'report-viewer-shell',
|
|
||||||
isDarkMode ? 'report-viewer-shell-dark' : '',
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ')
|
|
||||||
|
|
||||||
const loadViewerCss = (isDarkMode: boolean) => {
|
|
||||||
const styles = [
|
|
||||||
new URL('@devexpress/analytics-core/dist/css/dx-analytics.common.css', import.meta.url).href,
|
|
||||||
new URL('devexpress-reporting/dist/css/dx-webdocumentviewer.css', import.meta.url).href,
|
|
||||||
]
|
|
||||||
|
|
||||||
styles.forEach((href) => {
|
|
||||||
if (document.querySelector(`link[href="${href}"]`)) return
|
|
||||||
const link = document.createElement('link')
|
|
||||||
link.rel = 'stylesheet'
|
|
||||||
link.href = href
|
|
||||||
document.head.appendChild(link)
|
|
||||||
})
|
|
||||||
|
|
||||||
const themeHref = isDarkMode
|
|
||||||
? new URL('@devexpress/analytics-core/dist/css/dx-analytics.dark.css', import.meta.url).href
|
|
||||||
: new URL('@devexpress/analytics-core/dist/css/dx-analytics.light.css', import.meta.url).href
|
|
||||||
|
|
||||||
let themeLink = document.getElementById(VIEWER_THEME_LINK_ID) as HTMLLinkElement | null
|
|
||||||
if (!themeLink) {
|
|
||||||
themeLink = document.createElement('link')
|
|
||||||
themeLink.id = VIEWER_THEME_LINK_ID
|
|
||||||
themeLink.rel = 'stylesheet'
|
|
||||||
document.head.appendChild(themeLink)
|
|
||||||
}
|
|
||||||
if (themeLink.href !== themeHref) {
|
|
||||||
themeLink.href = themeHref
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const DevexpressReportViewer: FC = () => {
|
const DevexpressReportViewer: FC = () => {
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
const { report, listFormCode, id } = useParams<{
|
const { report, listFormCode, id } = useParams<{
|
||||||
|
|
@ -86,17 +38,14 @@ const DevexpressReportViewer: FC = () => {
|
||||||
id: string
|
id: string
|
||||||
}>()
|
}>()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const token = useStoreState((state) => state.auth.session.token)
|
|
||||||
const themeMode = useStoreState((state) => state.theme.mode)
|
|
||||||
const cultureName = useStoreState((state) => state.locale.currentLang)
|
|
||||||
const configCultureName = useStoreState(
|
|
||||||
(state) => state.abpConfig.config?.localization.currentCulture.cultureName,
|
|
||||||
)
|
|
||||||
const [configuredRequestKey, setConfiguredRequestKey] = useState<string>()
|
|
||||||
const previewModelRef = useRef<ReportPreviewModel>()
|
const previewModelRef = useRef<ReportPreviewModel>()
|
||||||
const isDarkMode = themeMode === MODE_DARK
|
const { shellClassName } = useReportShellTheme()
|
||||||
const reportCultureName = cultureName || configCultureName
|
const { cultureName, isRequestConfigured } = useReportRequestSetup()
|
||||||
const requestConfigKey = `${token ?? ''}\u0000${reportCultureName ?? ''}`
|
const reportUrlWithParams = useReportUrl(
|
||||||
|
{ report, id, listFormCode },
|
||||||
|
location.search,
|
||||||
|
cultureName,
|
||||||
|
)
|
||||||
|
|
||||||
const getReportPreview = useCallback(
|
const getReportPreview = useCallback(
|
||||||
(eventOrModel?: ReportPreviewContainer | ReportPreviewModel) => {
|
(eventOrModel?: ReportPreviewContainer | ReportPreviewModel) => {
|
||||||
|
|
@ -139,44 +88,17 @@ const DevexpressReportViewer: FC = () => {
|
||||||
[getReportPreview],
|
[getReportPreview],
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadViewerCss(isDarkMode)
|
|
||||||
}, [isDarkMode])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const nextHeaders = { ...(ajaxSetup.ajaxSettings.headers ?? {}) }
|
|
||||||
delete nextHeaders.Authorization
|
|
||||||
delete nextHeaders['Accept-Language']
|
|
||||||
|
|
||||||
ajaxSetup.ajaxSettings.headers = {
|
|
||||||
...nextHeaders,
|
|
||||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
||||||
...(reportCultureName ? { 'Accept-Language': reportCultureName } : {}),
|
|
||||||
}
|
|
||||||
setConfiguredRequestKey(requestConfigKey)
|
|
||||||
}, [token, reportCultureName, requestConfigKey])
|
|
||||||
|
|
||||||
const reportUrlWithParams = useMemo(() => {
|
|
||||||
const searchParams = new URLSearchParams(location.search)
|
|
||||||
if (reportCultureName) {
|
|
||||||
searchParams.set('cultureName', reportCultureName)
|
|
||||||
}
|
|
||||||
|
|
||||||
const search = createReportQueryString(searchParams)
|
|
||||||
return createReportUrl({ report, id, listFormCode }, search ? `?${search}` : '')
|
|
||||||
}, [report, id, listFormCode, location.search, reportCultureName])
|
|
||||||
|
|
||||||
if (!reportUrlWithParams) return null
|
if (!reportUrlWithParams) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className={getViewerClassName(isDarkMode)}>
|
<Container className={shellClassName}>
|
||||||
<Helmet
|
<Helmet
|
||||||
titleTemplate={`%s | ${APP_NAME}`}
|
titleTemplate={`%s | ${APP_NAME}`}
|
||||||
title={translate('::App.Reports')}
|
title={translate('::App.Reports')}
|
||||||
defaultTitle={APP_NAME}
|
defaultTitle={APP_NAME}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{configuredRequestKey === requestConfigKey && (
|
{isRequestConfigured && (
|
||||||
<ReportViewer key={reportUrlWithParams} reportUrl={reportUrlWithParams}>
|
<ReportViewer key={reportUrlWithParams} reportUrl={reportUrlWithParams}>
|
||||||
<Callbacks BeforeRender={setDefaultZoom} DocumentReady={setDefaultZoom} />
|
<Callbacks BeforeRender={setDefaultZoom} DocumentReady={setDefaultZoom} />
|
||||||
<RequestOptions host={import.meta.env.VITE_API_URL} invokeAction="/DXXRDV" />
|
<RequestOptions host={import.meta.env.VITE_API_URL} invokeAction="/DXXRDV" />
|
||||||
|
|
|
||||||
148
ui/src/views/report/shared/reportShell.ts
Normal file
148
ui/src/views/report/shared/reportShell.ts
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import { ajaxSetup } from '@devexpress/analytics-core/analytics-utils-native'
|
||||||
|
import { MODE_DARK } from '@/constants/theme.constant'
|
||||||
|
import { useStoreState } from '@/store'
|
||||||
|
import {
|
||||||
|
createReportQueryString,
|
||||||
|
createReportUrl,
|
||||||
|
type ReportRouteParams,
|
||||||
|
} from '../reportRouteParams'
|
||||||
|
|
||||||
|
const REPORT_THEME_LINK_ID = 'devexpress-report-viewer-theme'
|
||||||
|
const REPORT_DESIGNER_CSS_ID = 'devexpress-report-designer-css'
|
||||||
|
|
||||||
|
const COMMON_STYLE_HREFS = [
|
||||||
|
new URL('@devexpress/analytics-core/dist/css/dx-analytics.common.css', import.meta.url).href,
|
||||||
|
new URL('devexpress-reporting/dist/css/dx-webdocumentviewer.css', import.meta.url).href,
|
||||||
|
]
|
||||||
|
const DARK_THEME_HREF = new URL(
|
||||||
|
'@devexpress/analytics-core/dist/css/dx-analytics.dark.css',
|
||||||
|
import.meta.url,
|
||||||
|
).href
|
||||||
|
const LIGHT_THEME_HREF = new URL(
|
||||||
|
'@devexpress/analytics-core/dist/css/dx-analytics.light.css',
|
||||||
|
import.meta.url,
|
||||||
|
).href
|
||||||
|
const DESIGNER_STYLE_HREF = new URL(
|
||||||
|
'devexpress-reporting/dist/css/dx-reportdesigner.css',
|
||||||
|
import.meta.url,
|
||||||
|
).href
|
||||||
|
|
||||||
|
const appendStylesheet = (href: string, id?: string) => {
|
||||||
|
if (id ? document.getElementById(id) : document.querySelector(`link[href="${href}"]`)) return
|
||||||
|
|
||||||
|
const link = document.createElement('link')
|
||||||
|
if (id) link.id = id
|
||||||
|
link.rel = 'stylesheet'
|
||||||
|
link.href = href
|
||||||
|
document.head.appendChild(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyThemeStylesheet = (isDarkMode: boolean) => {
|
||||||
|
const themeHref = isDarkMode ? DARK_THEME_HREF : LIGHT_THEME_HREF
|
||||||
|
|
||||||
|
let themeLink = document.getElementById(REPORT_THEME_LINK_ID) as HTMLLinkElement | null
|
||||||
|
if (!themeLink) {
|
||||||
|
themeLink = document.createElement('link')
|
||||||
|
themeLink.id = REPORT_THEME_LINK_ID
|
||||||
|
themeLink.rel = 'stylesheet'
|
||||||
|
document.head.appendChild(themeLink)
|
||||||
|
}
|
||||||
|
if (themeLink.href !== themeHref) {
|
||||||
|
themeLink.href = themeHref
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getReportShellClassName = (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(' ')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DevExpress rapor bileşenlerinin stillerini yükler ve aktif temaya göre kabuk sınıfını döner.
|
||||||
|
*/
|
||||||
|
export const useReportShellTheme = (includeDesignerStyles = false) => {
|
||||||
|
const themeMode = useStoreState((state) => state.theme.mode)
|
||||||
|
const isDarkMode = themeMode === MODE_DARK
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
COMMON_STYLE_HREFS.forEach((href) => appendStylesheet(href))
|
||||||
|
applyThemeStylesheet(isDarkMode)
|
||||||
|
if (includeDesignerStyles) {
|
||||||
|
appendStylesheet(DESIGNER_STYLE_HREF, REPORT_DESIGNER_CSS_ID)
|
||||||
|
}
|
||||||
|
}, [isDarkMode, includeDesignerStyles])
|
||||||
|
|
||||||
|
return { isDarkMode, shellClassName: getReportShellClassName(isDarkMode) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useReportCultureName = () => {
|
||||||
|
const cultureName = useStoreState((state) => state.locale.currentLang)
|
||||||
|
const configCultureName = useStoreState(
|
||||||
|
(state) => state.abpConfig.config?.localization.currentCulture.cultureName,
|
||||||
|
)
|
||||||
|
|
||||||
|
return cultureName || configCultureName
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DevExpress istekleri global ajax ayarları üzerinden gittiği için oturum ve dil
|
||||||
|
* başlıkları bileşen render edilmeden önce yazılır. `isRequestConfigured` false iken
|
||||||
|
* rapor bileşeni render edilmemelidir, aksi hâlde ilk istek yetkisiz gider.
|
||||||
|
*/
|
||||||
|
export const useReportRequestSetup = () => {
|
||||||
|
const token = useStoreState((state) => state.auth.session.token)
|
||||||
|
const cultureName = useReportCultureName()
|
||||||
|
const requestConfigKey = `${token ?? ''}|${cultureName ?? ''}`
|
||||||
|
const [configuredRequestKey, setConfiguredRequestKey] = useState<string>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const nextHeaders = { ...(ajaxSetup.ajaxSettings.headers ?? {}) }
|
||||||
|
delete nextHeaders.Authorization
|
||||||
|
delete nextHeaders['Accept-Language']
|
||||||
|
|
||||||
|
ajaxSetup.ajaxSettings.headers = {
|
||||||
|
...nextHeaders,
|
||||||
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||||
|
...(cultureName ? { 'Accept-Language': cultureName } : {}),
|
||||||
|
}
|
||||||
|
setConfiguredRequestKey(requestConfigKey)
|
||||||
|
}, [token, cultureName, requestConfigKey])
|
||||||
|
|
||||||
|
return {
|
||||||
|
cultureName,
|
||||||
|
isRequestConfigured: configuredRequestKey === requestConfigKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useReportUrl = (
|
||||||
|
params: ReportRouteParams,
|
||||||
|
search: string,
|
||||||
|
cultureName?: string,
|
||||||
|
action?: string,
|
||||||
|
) => {
|
||||||
|
const { report, id, listFormCode } = params
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const searchParams = new URLSearchParams(search)
|
||||||
|
if (cultureName) {
|
||||||
|
searchParams.set('cultureName', cultureName)
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryString = createReportQueryString(searchParams)
|
||||||
|
return createReportUrl(
|
||||||
|
{ report, id, listFormCode },
|
||||||
|
queryString ? `?${queryString}` : '',
|
||||||
|
action,
|
||||||
|
)
|
||||||
|
}, [report, id, listFormCode, search, cultureName, action])
|
||||||
|
}
|
||||||
46
ui/src/views/shared/HtmlEditorToolbarItems.tsx
Normal file
46
ui/src/views/shared/HtmlEditorToolbarItems.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
/**
|
||||||
|
* DevExtreme HtmlEditor için standart araç çubuğu.
|
||||||
|
* NoteModal ve TodoBoard içinde birebir aynı ~40 satırlık tanım tekrar ediyordu.
|
||||||
|
*/
|
||||||
|
import { Item, Toolbar } from 'devextreme-react/html-editor'
|
||||||
|
import {
|
||||||
|
fontFamilyOptions,
|
||||||
|
fontSizeOptions,
|
||||||
|
fontValues,
|
||||||
|
headerOptions,
|
||||||
|
headerValues,
|
||||||
|
sizeValues,
|
||||||
|
} from '@/proxy/reports/data'
|
||||||
|
|
||||||
|
export const HtmlEditorToolbar = () => (
|
||||||
|
<Toolbar multiline>
|
||||||
|
<Item name="undo" />
|
||||||
|
<Item name="redo" />
|
||||||
|
<Item name="separator" />
|
||||||
|
<Item name="size" acceptedValues={sizeValues} options={fontSizeOptions} />
|
||||||
|
<Item name="font" acceptedValues={fontValues} options={fontFamilyOptions} />
|
||||||
|
<Item name="separator" />
|
||||||
|
<Item name="bold" />
|
||||||
|
<Item name="italic" />
|
||||||
|
<Item name="underline" />
|
||||||
|
<Item name="strike" />
|
||||||
|
<Item name="separator" />
|
||||||
|
<Item name="orderedList" />
|
||||||
|
<Item name="bulletList" />
|
||||||
|
<Item name="separator" />
|
||||||
|
<Item name="header" acceptedValues={headerValues} options={headerOptions} />
|
||||||
|
<Item name="separator" />
|
||||||
|
<Item name="color" />
|
||||||
|
<Item name="background" />
|
||||||
|
<Item name="separator" />
|
||||||
|
<Item name="alignLeft" />
|
||||||
|
<Item name="alignCenter" />
|
||||||
|
<Item name="alignRight" />
|
||||||
|
<Item name="alignJustify" />
|
||||||
|
<Item name="separator" />
|
||||||
|
<Item name="link" />
|
||||||
|
<Item name="image" />
|
||||||
|
<Item name="separator" />
|
||||||
|
<Item name="clear" />
|
||||||
|
</Toolbar>
|
||||||
|
)
|
||||||
67
ui/src/views/shared/lookupCache.ts
Normal file
67
ui/src/views/shared/lookupCache.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
/**
|
||||||
|
* Lookup veri kaynakları için ortak, TTL'li in-memory cache.
|
||||||
|
*
|
||||||
|
* Daha önce `views/list/useListFormColumns.ts` (TTL + boyut limitli) ve
|
||||||
|
* `views/form/useLookupDataSource.ts` (limitsiz, hiç boşalmayan) içinde iki
|
||||||
|
* ayrı uygulama vardı; ikinci sürüm bellek sızdırıyor ve dil/tenant değişiminde
|
||||||
|
* bayat veri döndürüyordu. Tek noktadan yönetilmesi için buraya taşındı.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const CACHE_DURATION_MS = 5 * 60 * 1000
|
||||||
|
const MAX_CACHE_SIZE = 100
|
||||||
|
|
||||||
|
const lookupCache = new Map<string, { promise: Promise<any[]>; timestamp: number }>()
|
||||||
|
|
||||||
|
/** Cache'in en eski girdilerini atarak boyut limitini korur. */
|
||||||
|
const evictOldestEntry = () => {
|
||||||
|
let oldestKey: string | undefined
|
||||||
|
let oldestTimestamp = Number.POSITIVE_INFINITY
|
||||||
|
|
||||||
|
lookupCache.forEach((entry, key) => {
|
||||||
|
if (entry.timestamp < oldestTimestamp) {
|
||||||
|
oldestTimestamp = entry.timestamp
|
||||||
|
oldestKey = key
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (oldestKey !== undefined) {
|
||||||
|
lookupCache.delete(oldestKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aynı anahtarla yapılan lookup yüklemelerini tek bir isteğe indirger.
|
||||||
|
* Hata durumunda girdi silinir, böylece sonraki çağrı yeniden dener.
|
||||||
|
*/
|
||||||
|
export const cachedLookupLoad = (key: string, loader: () => Promise<any[]>): Promise<any[]> => {
|
||||||
|
const now = Date.now()
|
||||||
|
const cached = lookupCache.get(key)
|
||||||
|
|
||||||
|
if (cached && now - cached.timestamp < CACHE_DURATION_MS) {
|
||||||
|
return cached.promise
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lookupCache.size >= MAX_CACHE_SIZE) {
|
||||||
|
evictOldestEntry()
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = Promise.resolve()
|
||||||
|
.then(loader)
|
||||||
|
.then((result) => result ?? [])
|
||||||
|
.catch((error) => {
|
||||||
|
lookupCache.delete(key)
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
|
||||||
|
lookupCache.set(key, { promise, timestamp: now })
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tek bir anahtarı ya da anahtar verilmezse tüm cache'i temizler. */
|
||||||
|
export const clearLookupCache = (key?: string) => {
|
||||||
|
if (key === undefined) {
|
||||||
|
lookupCache.clear()
|
||||||
|
} else {
|
||||||
|
lookupCache.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue