Cascading sistem AI tarafından elde geçirildi.

This commit is contained in:
Sedat ÖZTÜRK 2026-10-01 16:58:07 +03:00
parent 81b2202df0
commit 021ddd03d1
7 changed files with 101 additions and 304 deletions

View file

@ -1352,8 +1352,8 @@ cascade eksik yetenek değildir ve analizde `Ready to Start`'ı engellemez. Tam
| `DataSourceType` | `1` StaticData · `2` Query · `3` WebService |
| `ValueExpr` / `DisplayExpr` | Varsayılan `Key` / `Name` |
| `LookupQuery` | Kaynağın kendisi (tipe göre biçim değişir, aşağıda) |
| `CascadeParentFields` | **Çocuk alanda** doldurulur; değerleri sorguya taşınacak ebeveyn alan(lar), virgülle. Sırayla `@param0`, `@param1`… olur (`Query`/`WebService`); gelmeyen parametre `NULL` bağlanır |
| `CascadeRelationField` | **Çocuk alanda** doldurulur; `StaticData`'da ebeveynin değeriyle karşılaştırılan alan (öğedeki `group`) |
| `CascadeParentFields` | **Çocuk alanda** doldurulur; değerleri sorguya taşınacak ebeveyn alan(lar), virgülle. Sırayla `@param0`, `@param1`… olur (`Query`/`WebService`); boş ya da gelmeyen ebeveyn `NULL` bağlanır. Değer her zaman **metin** parametre olarak bağlanır (`ListFormSelectAppService.ToLookupParameter`), sayı anahtarlı ebeveyn de çalışır; dönüşümü kolon tipine göre veritabanı yapar. `WebService` gövdesine değer JSON kaçışlı yazılır |
| `CascadeRelationField` | **Çocuk alanda**, yalnızca `StaticData`'da anlamlıdır: öğede ilk ebeveynin değeriyle karşılaştırılan alan; boşsa `group`. Ebeveyn boşken liste süzülmez |
| `CascadeFilterOperator` | `StaticData` cascade filtresinin operatörü; varsayılan `=` |
| `CascadeEmptyFields` | **Ebeveyn alanda** doldurulur; bu alan değişince boşaltılacak çocuk alanlar, virgülle. Ara halka (İl) hem çocuk hem ebeveyn olduğu için iki tarafı birden taşır |
@ -1409,7 +1409,8 @@ ya da `EditorOptions`'ta `grouped: true` vardır ve öğeler `group` alanı taş
`[Key]` + `[Name]` takma adlarını döndürür ve `ValueExpr: "Key"`, `DisplayExpr: "Name"` yazılır.
Bağımlı liste `grouped` ile değil cascade ile kurulur.
> Lookup verisi önbelleklenir; aynı sorgu aynı filtrelerle bir kez yüklenir.
> Lookup verisi önbelleklenir; aynı sorgu aynı filtrelerle bir kez yüklenir. Grid, Tree, Card ve
> Form aynı veri kaynağını kullanır (`views/form/useLookupDataSource.ts`); cascade kuralı tek yerdedir.
### 4.3.5 Varsayılan değerler — `*FieldsDefaultValueJson`

View file

@ -4,6 +4,7 @@ public class LookupRequestDto
{
public string ListFormCode { get; set; }
public string ListFormFieldName { get; set; }
public string[] Filters { get; set; }
/// <summary>Cascade ebeveyn degerleri, <c>CascadeParentFields</c> sirasiyla; sayi ya da metin olabilir.</summary>
public object[] Filters { get; set; }
}

View file

@ -418,22 +418,17 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
var lookup = JsonSerializer.Deserialize<LookupDto>(field.LookupJson);
if (!string.IsNullOrWhiteSpace(lookup?.LookupQuery))
{
var parameters = new Dictionary<string, object>();
if (input.Filters != null)
{
for (int i = 0; i < input.Filters.Length; i++)
{
parameters.Add("@param" + i, input.Filters[i]);
}
}
// Cascade ebeveyn degerleri @param0… olarak baglanir; gelmeyen ebeveyn NULL olur ki
// sorgu "(Kolon = @param0 OR @param0 IS NULL)" kalibiyla tum listeyi donsun.
var cascadeParameterCount = lookup.CascadeParentFields?
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Length ?? 0;
var parameterCount = Math.Max(input.Filters?.Length ?? 0, cascadeParameterCount);
for (int i = input.Filters?.Length ?? 0; i < cascadeParameterCount; i++)
var parameters = new Dictionary<string, object>();
for (var i = 0; i < parameterCount; i++)
{
parameters.Add("@param" + i, null!);
parameters.Add("@param" + i, ToLookupParameter(input.Filters, i)!);
}
var lookupQuery = defaultValueHelper.GetDefaultValue(lookup.LookupQuery);
@ -444,6 +439,26 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
return new List<LookupDataDto>();
}
/// <summary>
/// Ebeveyn degeri istemcide sayi, metin ya da bool olabilir; parametre her zaman metin olarak
/// baglanir ki sorgudaki kolon tipine gore veritabani donustursun (int kolona "5" de gider).
/// </summary>
private static string? ToLookupParameter(object[]? filters, int index)
{
var value = filters is not null && index < filters.Length ? filters[index] : null;
return value switch
{
null => null,
JsonElement { ValueKind: JsonValueKind.Null or JsonValueKind.Undefined } => null,
JsonElement { ValueKind: JsonValueKind.String } element => NullIfEmpty(element.GetString()),
JsonElement element => element.GetRawText(),
_ => NullIfEmpty(Convert.ToString(value, CultureInfo.InvariantCulture))
};
static string? NullIfEmpty(string? text) => string.IsNullOrEmpty(text) ? null : text;
}
public async Task<string> GetNextSequenceValueAsync(string SequenceName)
{
var sequenceManager = LazyServiceProvider.LazyGetRequiredService<SequenceManager>();
@ -451,93 +466,6 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
return await sequenceManager.GetNextSequenceAsync(SequenceName);
}
private static string WrapLookupQueryWithInFilter(
string lookupQuery,
string listFormSelectCommand,
string listFormFieldName)
{
if (string.IsNullOrWhiteSpace(lookupQuery))
throw new ArgumentException("LookupQuery boş olamaz.");
if (string.IsNullOrWhiteSpace(listFormSelectCommand))
throw new ArgumentException("SelectCommand boş olamaz.");
if (string.IsNullOrWhiteSpace(listFormFieldName))
throw new ArgumentException("ListFormFieldName boş olamaz.");
return $@"
SELECT *
FROM (
{lookupQuery}
) t
WHERE ""Key"" IN (
SELECT ""{listFormFieldName}""
FROM ""{listFormSelectCommand}""
GROUP BY ""{listFormFieldName}""
)";
}
private static string AddCascadeWhereCondition(
string sql,
string cascadeParentFields,
int paramStartIndex = 0)
{
if (string.IsNullOrWhiteSpace(sql))
throw new ArgumentException("SQL boş olamaz.");
if (string.IsNullOrWhiteSpace(cascadeParentFields))
return sql;
var fields = cascadeParentFields
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(f => f.Trim())
.ToArray();
if (!fields.Any())
return sql;
var conditions = new List<string>();
for (int i = 0; i < fields.Length; i++)
{
var field = fields[i];
// Zaten " ile başlıyorsa dokunma
if (!field.StartsWith("\""))
field = $"\"{field}\"";
conditions.Add($"{field} = @param{paramStartIndex + i}");
}
string whereClause = string.Join(" AND ", conditions);
var upperSql = sql.ToUpperInvariant();
if (upperSql.Contains(" WHERE "))
{
return InsertBeforeClause(sql, "AND " + whereClause);
}
else
{
return InsertBeforeClause(sql, "WHERE " + whereClause);
}
}
private static string InsertBeforeClause(string sql, string insertText)
{
var upperSql = sql.ToUpperInvariant();
string[] clauses = { " GROUP BY ", " ORDER BY ", " HAVING " };
foreach (var clause in clauses)
{
int index = upperSql.IndexOf(clause);
if (index > -1)
{
return sql.Insert(index, " " + insertText + " ");
}
}
return sql + " " + insertText;
}
//Workflow kriterlerini alma
private async Task<List<ListFormWorkflowCriteriaDto>> GetWorkflowCriteriaAsync(string listFormCode)
{

View file

@ -39,7 +39,7 @@ type BuildFormItemsParams = {
mode: RowMode
columns?: GridColumnData[]
/** Lookup alanları için DevExtreme dataSource üretici. */
getLookupDataSource: (options: any, colData: ColumnFormatDto, formData: any) => any
getLookupDataSource: (colData: ColumnFormatDto, formData: any) => any
formData: any
}
@ -57,7 +57,7 @@ const toFormItem = (
const lookupEditorOptions = colFormat.lookupDto?.dataSourceType
? {
dataSource: getLookupDataSource(colFormat.editorOptions, colFormat, formData),
dataSource: getLookupDataSource(colFormat, formData),
valueExpr: colFormat.lookupDto?.valueExpr?.toLowerCase(),
displayExpr: colFormat.lookupDto?.displayExpr?.toLowerCase(),
}

View file

@ -262,7 +262,7 @@ const useGridData = (props: {
...item.editorOptions,
dataSource: keepCustomDataSource
? currentDataSource
: getLookupDataSource(colData?.editorOptions, colData, formData || null),
: getLookupDataSource(colData, formData || null),
valueExpr:
item.editorOptions?.valueExpr ?? colData?.lookupDto?.valueExpr?.toLowerCase(),
displayExpr:

View file

@ -14,17 +14,48 @@ export { clearLookupCache } from '../shared/lookupCache'
const isLookupAllowed = (listFormCode?: string, isSubForm?: boolean) =>
isSubForm || !listFormCode || window.location.pathname.includes(listFormCode)
const createLookupStaticDataSource = (
const splitFields = (value?: string | null) =>
(value ?? '')
.split(',')
.map((field) => field.trim())
.filter(Boolean)
const isEmptyValue = (value: unknown) => value === undefined || value === null || value === ''
/**
* Cascade filtresi. Query/WebService için ebeveyn değerleri sırayla `@param0…` olur; boş ebeveyn
* `null` gider ve sorgu `(Kolon = @param0 OR @param0 IS NULL)` kalıbıyla tüm listeyi döner.
* StaticData için öğelerdeki `CascadeRelationField` alanı (varsayılan `group`) ilk ebeveynin
* değeriyle karşılaştırılır; ebeveyn boşsa süzülmez.
*/
export const buildCascadeFilters = (lookupDto: any, rowData: any): any[] | null => {
const parentFields = splitFields(lookupDto?.cascadeParentFields)
if (!parentFields.length) return null
if (lookupDto.dataSourceType == UiLookupDataSourceTypeEnum.StaticData) {
const parentValue = rowData?.[parentFields[0]]
if (isEmptyValue(parentValue)) return null
return [
lookupDto.cascadeRelationField || 'group',
lookupDto.cascadeFilterOperator || '=',
parentValue,
]
}
return parentFields.map((field) => (isEmptyValue(rowData?.[field]) ? null : rowData[field]))
}
export const createLookupStaticDataSource = (
load: () => any,
filter: any = null,
key: any = 'static',
sort: any = 'name',
filter: any[] | null,
cacheKey: string,
sort = 'name',
) => ({
store: new CustomStore({
key,
loadMode: 'raw',
load: () =>
cachedLookupLoad(`static:${key}`, async () => {
cachedLookupLoad(`static:${cacheKey}`, async () => {
const result = await Promise.resolve(load())
if (result === undefined || result === null) return []
return Array.isArray(result) ? result : [result]
@ -34,10 +65,10 @@ const createLookupStaticDataSource = (
filter,
})
const createLookupQueryDataSource = (
export const createLookupQueryDataSource = (
listFormCode?: string,
listFormFieldName?: string,
filters?: any[],
filters?: any[] | null,
isSubForm?: boolean,
) =>
new CustomStore({
@ -72,10 +103,13 @@ const createLookupQueryDataSource = (
},
})
const createLookupApiDataSource = (
/** Gövde JSON şablonudur; değer tırnak içine yerleştiği için JSON kaçışlı yazılır. */
const toJsonText = (value: unknown) => (value === null ? 'null' : JSON.stringify(String(value)).slice(1, -1))
export const createLookupApiDataSource = (
listFormCode?: string,
lookupQuery?: string,
filters?: any[],
filters?: any[] | null,
keyName?: string,
isSubForm?: boolean,
) =>
@ -99,7 +133,7 @@ const createLookupApiDataSource = (
// Gövdedeki @param0, @param1… yer tutucularını cascade filtreleriyle doldur.
let resolvedBody = bodyTemplate ?? ''
filters?.forEach((filterValue, index) => {
resolvedBody = resolvedBody.replace(new RegExp(`@param${index}`, 'g'), String(filterValue))
resolvedBody = resolvedBody.replace(new RegExp(`@param${index}\\b`, 'g'), toJsonText(filterValue))
})
try {
@ -129,6 +163,10 @@ const createLookupApiDataSource = (
},
})
/**
* Lookup kolonunun veri kaynağı. Grid/Tree/Card (`useListFormColumns`) ve Form aynı kuralı
* kullanır; `rowData` cascade ebeveynlerinin okunduğu kayıttır.
*/
export const useLookupDataSource = ({
listFormCode,
isSubForm,
@ -139,24 +177,9 @@ export const useLookupDataSource = ({
const { translate, cultureName } = useLocalization()
const getLookupDataSource = useCallback(
(options: any, colData: any, data: any) => {
(colData: any, rowData: any) => {
const { lookupDto } = colData
const filters: any[] = []
// Cascading alanlar: parent değeri yoksa null gönderilir, istek yine de yapılır.
if (lookupDto.cascadeParentFields) {
if (lookupDto.dataSourceType == UiLookupDataSourceTypeEnum.StaticData) {
filters.push([
lookupDto?.cascadeRelationField,
lookupDto?.cascadeFilterOperator,
options?.data?.[lookupDto?.cascadeParentField] ?? null,
])
} else {
for (const cascadeParentField of lookupDto.cascadeParentFields.split(',')) {
filters.push(data?.[cascadeParentField.trim()] ?? null)
}
}
}
const filters = buildCascadeFilters(lookupDto, rowData)
switch (lookupDto.dataSourceType) {
case UiLookupDataSourceTypeEnum.StaticData:
@ -166,8 +189,8 @@ export const useLookupDataSource = ({
...item,
name: typeof item.name === 'string' ? translate(`::${item.name}`) : item.name,
})),
filters.length ? filters : null,
`static:${cultureName}:${listFormCode}:${colData.fieldName}`,
filters,
`${cultureName}:${listFormCode}:${colData.fieldName}`,
)
case UiLookupDataSourceTypeEnum.Query:
return createLookupQueryDataSource(listFormCode, colData.fieldName, filters, isSubForm)

View file

@ -17,11 +17,8 @@ import {
UiLookupDataSourceTypeEnum,
} from '@/proxy/form/models'
import { addCss, getEditingFormFields } from './Utils'
import {
cachedLookupLoad,
clearLookupCache,
clearLookupCacheByPrefix,
} from '../shared/lookupCache'
import { clearLookupCache, clearLookupCacheByPrefix } from '../shared/lookupCache'
import { useLookupDataSource } from '../form/useLookupDataSource'
import {
NO_IMAGE,
normalizeImageSize,
@ -169,7 +166,7 @@ const useListFormColumns = ({
onShowNote?: (rowData: Record<string, any>) => void
}) => {
const dialog: any = useDialogContext()
const { translate, cultureName } = useLocalization()
const { translate } = useLocalization()
const { checkPermission } = usePermission()
const isPwaMode = usePWA()
@ -178,55 +175,12 @@ const useListFormColumns = ({
clearLookupCache()
}, [listFormCode])
const { getLookupDataSource } = useLookupDataSource({ listFormCode, isSubForm })
// DataGrid lookup'ı satırı `options.data` ile verir; görünen metin yüklemesi satırsız çağırır.
const lookupDataSource = useCallback(
(options: any, colData: any, listFormCode: string) => {
const { lookupDto } = colData
const filters = []
if (lookupDto.cascadeParentFields) {
if (lookupDto.dataSourceType == UiLookupDataSourceTypeEnum.StaticData) {
filters.push([
lookupDto?.cascadeRelationField,
lookupDto?.cascadeFilterOperator,
options?.data[lookupDto?.cascadeParentField],
])
//TODO: Statik data test edilecek
} else {
const data = options?.data ?? options
for (const cascadeParentField of lookupDto.cascadeParentFields.split(',')) {
filters.push(data[cascadeParentField])
}
}
}
//UiLookupDataSourceTypeEnum :
// Data = 1 (Statik Data),
// Query = 2 (API'den geliyor fakat API query çalıştırıyor)
// WebService = 3 (API servisten geliyor)
if (lookupDto.dataSourceType == UiLookupDataSourceTypeEnum.StaticData) {
return createLookupStaticDataSource(
() =>
JSON.parse(lookupDto?.lookupQuery).map((item: any) => ({
...item,
name: typeof item.name === 'string' ? translate(`::${item.name}`) : item.name,
})),
filters.length ? filters : null,
`static:${cultureName}:${listFormCode}:${colData.fieldName}`, // cache key
)
} else if (lookupDto.dataSourceType == UiLookupDataSourceTypeEnum.Query) {
return createLookupQueryDataSource(listFormCode, colData.fieldName, filters)
} else if (lookupDto.dataSourceType == UiLookupDataSourceTypeEnum.WebService) {
return createLookupApiDataSource(
listFormCode,
lookupDto?.lookupQuery,
filters,
colData.lookupDto?.valueExpr?.toLowerCase(),
)
} else {
return {
store: [],
}
}
},
[listFormCode, translate, cultureName],
(options: any, colData: any) => getLookupDataSource(colData, options?.data ?? options),
[getLookupDataSource],
)
const loadLookupDisplayValues = useCallback(
@ -241,7 +195,7 @@ const useListFormColumns = ({
)
.map(async (column) => {
const lookup = column.colData!.lookupDto
const source = lookupDataSource({}, column.colData, listFormCode)
const source = lookupDataSource({}, column.colData)
const store =
source instanceof CustomStore
? source
@ -270,7 +224,7 @@ const useListFormColumns = ({
return Object.fromEntries(entries) as Record<string, Map<string, string>>
},
[listFormCode, lookupDataSource],
[lookupDataSource],
)
/**
@ -311,7 +265,7 @@ const useListFormColumns = ({
const prefix = getCachePrefix(colData)
if (!prefix) return
const source = lookupDataSource({}, colData, listFormCode)
const source = lookupDataSource({}, colData)
const store =
source instanceof CustomStore
? source
@ -347,115 +301,6 @@ const useListFormColumns = ({
[gridDto, listFormCode, lookupDataSource],
)
const createLookupStaticDataSource = (
load: () => any,
filter: any = null,
key: any = 'static',
sort: any = 'name',
) => ({
store: new CustomStore({
key,
loadMode: 'raw',
load: async () => {
// load fonksiyonu sync sonuç döndürüyor olabilir, o yüzden Promise.resolve ile sar
return cachedLookupLoad(`static:${key}`, async () => {
const result = await Promise.resolve(load())
if (result === undefined || result === null) return []
return Array.isArray(result) ? result : [result]
})
},
}),
sort,
filter,
})
const createLookupQueryDataSource = (
listFormCode?: string,
listFormFieldName?: string,
filters?: any[],
) => {
return new CustomStore({
loadMode: 'raw',
load: async () => {
if (!isSubForm && listFormCode && !window.location.pathname.includes(listFormCode)) {
return []
}
try {
const cacheKey = `query:${listFormCode}:${listFormFieldName}:${JSON.stringify(filters ?? null)}`
return cachedLookupLoad(cacheKey, async () => {
const response = await dynamicFetch('list-form-select/lookup', 'POST', null, {
listFormCode,
listFormFieldName,
filters,
})
return (response.data ?? []).map((a: any) => ({
key: a.Key,
name: a.Name,
group: a.Group,
...a,
}))
})
} catch (error) {
console.error('Lookup query data could not be loaded:', error)
return []
}
},
})
}
const createLookupApiDataSource = (
listFormCode?: string,
lookupQuery?: string,
filters?: any[],
keyName?: string,
) => {
return new CustomStore({
key: keyName,
loadMode: 'raw',
load: async () => {
if (!isSubForm && listFormCode && !window.location.pathname.includes(listFormCode)) {
return []
}
if (!lookupQuery) {
return []
}
const [method, url, body, keySelector, nameSelector, groupSelector] = lookupQuery.split(';')
// body içindeki @paramN'leri filtrelerle değiştir
let resolvedBody = body
if (filters?.length) {
for (let i = 0; i < filters.length; i++) {
resolvedBody = resolvedBody.replace(new RegExp(`@param${i}`, 'g'), filters[i])
}
}
try {
const cacheKey = `api:${lookupQuery}:${JSON.stringify(filters ?? null)}`
return cachedLookupLoad(cacheKey, async () => {
const response = await dynamicFetch(url, method, null, resolvedBody)
let { data } = response
if (!data) return []
if (!Array.isArray(data)) data = [data]
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 []
}
},
})
}
const getCommandColumn = useCallback((): GridColumnData | undefined => {
if (!gridDto) {
return
@ -817,9 +662,8 @@ const useListFormColumns = ({
column.lookup = {
valueExpr: colData.lookupDto?.valueExpr?.toLowerCase(),
displayExpr: colData.lookupDto?.displayExpr?.toLowerCase(),
dataSource: (o) => lookupDataSource(o, colData, listFormCode),
dataSource: (o) => lookupDataSource(o, colData),
}
//column.lookup.dataSource = lookupDataSource(null, colData)
//cascadeEmptyFields verisi dolu ise bu kolon/field bir parent field dir
if (colData.lookupDto.cascadeEmptyFields) {
@ -891,7 +735,7 @@ const useListFormColumns = ({
return columns
},
[gridDto, lookupDataSource, translate, checkPermission, dialog, isPwaMode, listFormCode],
[gridDto, lookupDataSource, translate],
)
const getBandedColumns = useCallback(() => {