IsLookupSelfSourced ve Widget tazelendi

This commit is contained in:
Sedat ÖZTÜRK 2026-09-30 16:45:44 +03:00
parent 6d5071237a
commit c62e2c8586
15 changed files with 271 additions and 39 deletions

View file

@ -428,6 +428,10 @@ Sorgu ekranın bağlantısında (`IsTenant` + `DataSourceCode` ile çözülen, `
ve isteğe bağlı `Value` ifadesi; alt başlık ekranın adıdır, değer tip çakışmasın diye metne
çevrilir. Gruplamalı sorgu için `DefaultWidgetsJson(sql, kartSayısı)`. Genel kart başlıkları
`App.Widget.*` dil anahtarlarındadır.
- Kartlar GridDto ile gelir; Grid, Tree, Card, Gantt ve Scheduler görünümünde Yenile düğmesi ve
satır ekleme/güncelleme/silme kartları yeniden çeker (`useListFormWidgets`). Yalnızca widget
listesi değişir, GridDto değiştirilmez — görünüm durumu (sayfa, seçim) korunur. Kart sorgusu
ekranın filtresine bağlı olmadığı için filtre değişikliği kartları yenilemez.
### 5.4 `LayoutJson` (`LayoutDto`)

View file

@ -1207,6 +1207,16 @@ anahtarları listeler; ortak anahtarlar (§4.3.3) her editörde geçerlidir.
hata vermez, **sessizce yok sayılır** — bu yüzden `columns` bir dizi, `maxDisplayedTags` bir
sayı, `multiple` bir boolean olmalıdır.
Lookup kolonu hücre metnini açılışta yüklenen lookup listesinden bulur. Lookup'ı ekranın kendi
kayıt kaynağından beslenen kolonda (ör. `SELECT DISTINCT CompanyName … FROM Hr_T_Visitor` ile
Autocomplete) kaydedilen yeni değer bu listede yoktur. Sunucu bu kolonları ekran açılırken
işaretler (`ColumnFormatDto.IsLookupSelfSourced`: Query lookup'ının FROM/JOIN nesnesi ekranın
`SelectCommand` nesnesiyle aynı; yalnızca `true` yazılır, seed dosyasına girmez). Grid, Tree ve
Card görünümünde satır eklenince/güncellenince yalnızca işaretli kolonlar ve yalnızca kaydedilen
değer listede yoksa lookup yeniden yüklenir (`refreshLookupsForRow`) — hücre, editörün açılır
listesi ve filtre satırı sayfa yenilenmeden güncellenir. Başka kaynaktan beslenen lookup, statik
veri, WebService ve cascade lookup için kayıtta hiç lookup isteği atılmaz.
**Lookup zorunlu** yazan editörlerde `LookupDataSourceType` + `LookupQuery` + `ValueExpr` +
`DisplayExpr` dolu olmalıdır; aksi hâlde alan boş bir liste gösterir:

View file

@ -184,6 +184,14 @@ public class ColumnFormatDto : AuditedEntityDto<Guid>
set { LookupJson = JsonSerializer.Serialize(value); }
}
/// <summary>
/// Lookup sorgusu ekranın kendi kayıt kaynağını okuyor; kaydedilen yeni değer lookup listesine
/// ancak kayıttan sonra girdiği için istemci lookup'ı yalnızca bu alanlarda yeniler. Ekran
/// açılırken hesaplanır; false yazılmadığı için seed dosyasına girmez.
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public bool IsLookupSelfSourced { get; set; }
[JsonIgnore]
public string ValidationRuleJson { get; set; }
public List<ValidationRuleDto> ValidationRuleDto

View file

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Sozsoft.Platform.DynamicData;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Enums;
using Sozsoft.Platform.Queries;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Users;
@ -282,6 +283,33 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
return Widgets;
}
/// <summary>
/// Query lookup'ı ekranın veri kaynağındaki bir nesneyi (FROM/JOIN) okuyan alanları işaretler.
/// WebService lookup'ının kaynağı sorgudan okunamadığı için dışarıda kalır.
/// </summary>
private static void MarkSelfSourcedLookups(ListForm listForm, List<ColumnFormatDto> columnFormats)
{
var sourceObjects = listForm.SelectCommandType == SelectCommandTypeEnum.Query
? WizardExportCollector.ExtractQueryObjectNames(listForm.SelectCommand).ToHashSet(StringComparer.OrdinalIgnoreCase)
: new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (listForm.SelectCommandType != SelectCommandTypeEnum.Query
&& WizardExportCollector.NormalizeDatabaseObjectName(listForm.SelectCommand) is { } objectName)
{
sourceObjects.Add(objectName);
}
if (sourceObjects.Count == 0)
return;
foreach (var columnFormat in columnFormats.Where(c => !string.IsNullOrWhiteSpace(c.LookupJson)))
{
var lookup = columnFormat.LookupDto;
columnFormat.IsLookupSelfSourced = lookup.DataSourceType == UiLookupDataSourceTypeEnum.Query
&& WizardExportCollector.ExtractQueryObjectNames(lookup.LookupQuery).Any(sourceObjects.Contains);
}
}
public async Task<GridDto> GetGridAsync(string ListFormCode)
{
//Izin logic process
@ -306,6 +334,7 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
ColumnFormats = ObjectMapper.Map<List<ListFormField>, List<ColumnFormatDto>>(fields),
Widgets = await GetWidgetsAsync(listForm)
};
MarkSelfSourcedLookups(listForm, result.ColumnFormats);
//Workflow kriterlerini alıp GridOptionsDto içine atıyoruz ki frontend'de kullanabilelim
var workflowDto = result.GridOptions.WorkflowDto;

File diff suppressed because one or more lines are too long

View file

@ -227,6 +227,8 @@ export interface ColumnFormatDto extends AuditedEntityDto<string> {
columnGroupSummaryDto: ColumnTotalSummaryDto
lookupJson?: string
lookupDto: LookupDto
/** Lookup ekranın kendi kayıt kaynağından besleniyor; kayıttan sonra lookup yenilenir. */
isLookupSelfSourced?: boolean
validationRuleJson?: string
validationRuleDto: ValidationRuleDto[]
columnStylingJson?: string

View file

@ -60,7 +60,7 @@ import {
import { getValueByField, resolveEditorType, withEditorTypeDefaults } from './shared/editingForm'
import { createEditingFormHandlers } from './shared/editingFormScripts'
import { flattenColumns } from './shared/columns'
import { getInitialExtraFilters, mergeFilterTriplets } from './shared/hooks'
import { getInitialExtraFilters, mergeFilterTriplets, useListFormWidgets } from './shared/hooks'
import { useFilters } from './useFilters'
import { useListFormColumns } from './useListFormColumns'
import { useListFormCustomDataSource } from './useListFormCustomDataSource'
@ -373,6 +373,7 @@ const CardView = (props: CardViewProps) => {
const [isPopupFullScreen, setIsPopupFullScreen] = useState(false)
const [formData, setFormData] = useState<Record<string, any>>()
const widgetGroupRef = useRef<HTMLDivElement>(null)
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto)
const formDataRef = useRef<Record<string, any> | undefined>(undefined)
useEffect(() => {
formDataRef.current = formData
@ -447,7 +448,12 @@ const CardView = (props: CardViewProps) => {
;(cardDataSource?.store() as any)?.invalidateSelectCache?.()
const reload = cardDataSource?.reload()
Promise.resolve(reload).finally(() => refreshParentData?.())
}, [refreshParentData])
void refreshWidgets()
}, [refreshParentData, refreshWidgets])
const onCardChanged = useCallback(() => {
refreshParentData?.()
void refreshWidgets()
}, [refreshParentData, refreshWidgets])
const setCardColumnCount = useCallback(
(cardColumnCount: number) => {
setListState({
@ -477,7 +483,7 @@ const CardView = (props: CardViewProps) => {
[gridDto, listFormCode],
)
const { getBandedColumns, loadLookupDisplayValues } = useListFormColumns({
const { getBandedColumns, loadLookupDisplayValues, refreshLookupsForRow } = useListFormColumns({
gridDto,
listFormCode,
isSubForm,
@ -729,6 +735,17 @@ const CardView = (props: CardViewProps) => {
]).then(([values, cascadeValues]) => applyLookupDisplayValues({ ...values, ...cascadeValues }))
}, [applyLookupDisplayValues, getLookupColumns, loadLookupDisplayValues])
// Kart metinleri lookup haritasından okunur; yenilenen lookup haritaya işlenip kartlar yeniden çizilir.
const refreshCardLookups = useCallback(
async (instance: any, rowData?: Record<string, any>) => {
if (!(await refreshLookupsForRow(instance, rowData))) return
applyLookupDisplayValues(await loadLookupDisplayValues(getLookupColumns()))
instance.repaint?.()
},
[applyLookupDisplayValues, getLookupColumns, loadLookupDisplayValues, refreshLookupsForRow],
)
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef: cardViewRef })
const dataSource = useMemo<CustomStore<any, any> | undefined>(() => {
if (!gridDto) return undefined
@ -1102,8 +1119,8 @@ const CardView = (props: CardViewProps) => {
return (
<>
<div ref={widgetGroupRef} className={(gridDto?.widgets?.length ?? 0) > 0 ? 'mt-2' : ''}>
<WidgetGroup widgetGroups={gridDto?.widgets ?? []} />
<div ref={widgetGroupRef} className={widgets.length > 0 ? 'mt-2' : ''}>
<WidgetGroup widgetGroups={widgets} />
</div>
<Container className={DX_CLASSNAMES}>
@ -1162,9 +1179,15 @@ const CardView = (props: CardViewProps) => {
{ placement: 'bottom-end' },
)
}}
onCardInserted={() => refreshParentData?.()}
onCardUpdated={() => refreshParentData?.()}
onCardRemoved={() => refreshParentData?.()}
onCardInserted={(e) => {
onCardChanged()
void refreshCardLookups(e.component, e.data)
}}
onCardUpdated={(e) => {
onCardChanged()
void refreshCardLookups(e.component, e.data)
}}
onCardRemoved={onCardChanged}
>
<CardHeader template={renderCardHeader} />
<Editing

View file

@ -36,6 +36,7 @@ import { useFilters } from './useFilters'
import {
useListFormCustomSources,
useListFormGridDto,
useListFormWidgets,
useWidgetGroupHeight,
} from './shared/hooks'
import GridFilterDialogs from './GridFilterDialogs'
@ -101,7 +102,8 @@ const GanttView = (props: GanttViewProps) => {
const pendingStateRef = useRef<Record<string, any> | undefined>(undefined)
const loadedStateKeyRef = useRef('')
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, gridDto?.widgets)
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto)
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets)
useListFormCustomSources(gridDto)
const { customSaveState, customLoadState, storageKey } = useListFormStateStoring({
@ -249,9 +251,10 @@ const GanttView = (props: GanttViewProps) => {
stylingMode: 'icon',
onClick: () => {
gridRef.current?.instance()?.refresh()
void refreshWidgets()
},
}),
[translate],
[translate, refreshWidgets],
)
// Gantt'in kendi sutun secicisi yok; ici TreeList oldugu icin onun secicisi acilir.
@ -424,8 +427,8 @@ const GanttView = (props: GanttViewProps) => {
return (
<>
<div ref={widgetGroupRef} className={(gridDto?.widgets?.length ?? 0) > 0 ? 'mt-2' : ''}>
<WidgetGroup widgetGroups={gridDto?.widgets ?? []} />
<div ref={widgetGroupRef} className={widgets.length > 0 ? 'mt-2' : ''}>
<WidgetGroup widgetGroups={widgets} />
</div>
<Container className={DX_CLASSNAMES}>

View file

@ -77,6 +77,7 @@ import {
useExtraFilters,
useListFormCustomSources,
useListFormGridDto,
useListFormWidgets,
useWidgetGroupHeight,
} from './shared/hooks'
import { startWorkflowForInsertedRow } from './shared/workflowRows'
@ -127,7 +128,8 @@ const Grid = (props: GridProps) => {
entityId: string
} | null>(null)
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, gridDto?.widgets)
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto)
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets)
useListFormCustomSources(gridDto)
const getGridInstance = useCallback(() => gridRef.current?.instance(), [])
@ -175,7 +177,7 @@ const Grid = (props: GridProps) => {
[gridDto, listFormCode],
)
const { getBandedColumns } = useListFormColumns({
const { getBandedColumns, refreshLookupsForRow } = useListFormColumns({
gridDto,
listFormCode,
isSubForm,
@ -205,7 +207,8 @@ const Grid = (props: GridProps) => {
const refreshData = useCallback(() => {
const grid = getGridInstance()
Promise.resolve(grid?.refresh()).finally(() => updateWorkflowApprovalButtons(grid))
}, [getGridInstance, updateWorkflowApprovalButtons])
void refreshWidgets()
}, [getGridInstance, updateWorkflowApprovalButtons, refreshWidgets])
const getSelectedRowKeys = useCallback(
async () => (await getGridInstance()?.getSelectedRowKeys()) ?? [],
@ -559,8 +562,8 @@ const Grid = (props: GridProps) => {
return (
<>
<div ref={widgetGroupRef} className={(gridDto?.widgets?.length ?? 0) > 0 ? 'mt-2' : ''}>
<WidgetGroup widgetGroups={gridDto?.widgets ?? []} />
<div ref={widgetGroupRef} className={widgets.length > 0 ? 'mt-2' : ''}>
<WidgetGroup widgetGroups={widgets} />
</div>
<Container className={DX_CLASSNAMES}>
@ -623,9 +626,18 @@ const Grid = (props: GridProps) => {
onRowInserted={(e) => {
startWorkflowForInsertedRow(e, gridDto, listFormCode, refreshGrid)
props.refreshData?.()
void refreshWidgets()
void refreshLookupsForRow(e.component, e.data)
}}
onRowUpdated={(e) => {
props.refreshData?.()
void refreshWidgets()
void refreshLookupsForRow(e.component, e.data)
}}
onRowRemoved={() => {
props.refreshData?.()
void refreshWidgets()
}}
onRowUpdated={() => props.refreshData?.()}
onRowRemoved={() => props.refreshData?.()}
onEditorPreparing={onEditorPreparing}
>
<Export

View file

@ -41,7 +41,12 @@ import {
} from './shared/editPopup'
import { getValueByField, resolveEditorType, withEditorTypeDefaults } from './shared/editingForm'
import { flattenGridColumns } from './shared/columns'
import { useListFormCustomSources, useListFormGridDto, useWidgetGroupHeight } from './shared/hooks'
import {
useListFormCustomSources,
useListFormGridDto,
useListFormWidgets,
useWidgetGroupHeight,
} from './shared/hooks'
interface SchedulerViewProps {
listFormCode: string
@ -68,7 +73,8 @@ const SchedulerView = (props: SchedulerViewProps) => {
const [userAvatarUrls, setUserAvatarUrls] = useState<Record<string, string>>({})
const layout = layoutTypes.scheduler || 'scheduler'
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, gridDto?.widgets)
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto)
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets)
useListFormCustomSources(gridDto)
useEffect(() => {
@ -161,7 +167,8 @@ const SchedulerView = (props: SchedulerViewProps) => {
dataSource.reload()
}
}
}, [])
void refreshWidgets()
}, [refreshWidgets])
const onCurrentViewChange = useCallback((value: string) => {
setCurrentView(value)
@ -769,8 +776,8 @@ const SchedulerView = (props: SchedulerViewProps) => {
return (
<>
<div ref={widgetGroupRef} className={(gridDto?.widgets?.length ?? 0) > 0 ? 'mt-2' : ''}>
<WidgetGroup widgetGroups={gridDto?.widgets ?? []} />
<div ref={widgetGroupRef} className={widgets.length > 0 ? 'mt-2' : ''}>
<WidgetGroup widgetGroups={widgets} />
</div>
<Container className={DX_CLASSNAMES}>
@ -820,6 +827,9 @@ const SchedulerView = (props: SchedulerViewProps) => {
onAppointmentDeleting={() => {
props.refreshData?.()
}}
onAppointmentAdded={() => void refreshWidgets()}
onAppointmentUpdated={() => void refreshWidgets()}
onAppointmentDeleted={() => void refreshWidgets()}
height={
gridDto.gridOptions.height > 0
? gridDto.gridOptions.height

View file

@ -73,6 +73,7 @@ import {
useExtraFilters,
useListFormCustomSources,
useListFormGridDto,
useListFormWidgets,
useWidgetGroupHeight,
} from './shared/hooks'
import { startWorkflowForInsertedRow } from './shared/workflowRows'
@ -117,7 +118,8 @@ const Tree = (props: TreeProps) => {
entityId: string
} | null>(null)
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, gridDto?.widgets)
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto)
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets)
useListFormCustomSources(gridDto)
const getTreeInstance = useCallback(() => gridRef.current?.instance(), [])
@ -165,7 +167,7 @@ const Tree = (props: TreeProps) => {
[gridDto, listFormCode],
)
const { getBandedColumns } = useListFormColumns({
const { getBandedColumns, refreshLookupsForRow } = useListFormColumns({
gridDto,
listFormCode,
isSubForm,
@ -195,7 +197,8 @@ const Tree = (props: TreeProps) => {
const refreshData = useCallback(() => {
const tree = getTreeInstance()
Promise.resolve(tree?.refresh()).finally(() => updateWorkflowApprovalButtons(tree))
}, [getTreeInstance, updateWorkflowApprovalButtons])
void refreshWidgets()
}, [getTreeInstance, updateWorkflowApprovalButtons, refreshWidgets])
const getSelectedRowKeys = useCallback(
async () => (await getTreeInstance()?.getSelectedRowKeys()) ?? [],
@ -535,8 +538,8 @@ const Tree = (props: TreeProps) => {
return (
<>
<div ref={widgetGroupRef} className={(gridDto?.widgets?.length ?? 0) > 0 ? 'mt-2' : ''}>
<WidgetGroup widgetGroups={gridDto?.widgets ?? []} />
<div ref={widgetGroupRef} className={widgets.length > 0 ? 'mt-2' : ''}>
<WidgetGroup widgetGroups={widgets} />
</div>
<Container className={DX_CLASSNAMES}>
@ -605,9 +608,18 @@ const Tree = (props: TreeProps) => {
onRowInserted={(e) => {
startWorkflowForInsertedRow(e, gridDto, listFormCode, refreshTree)
props.refreshData?.()
void refreshWidgets()
void refreshLookupsForRow(e.component, e.data)
}}
onRowUpdated={(e) => {
props.refreshData?.()
void refreshWidgets()
void refreshLookupsForRow(e.component, e.data)
}}
onRowRemoved={() => {
props.refreshData?.()
void refreshWidgets()
}}
onRowUpdated={() => props.refreshData?.()}
onRowRemoved={() => props.refreshData?.()}
onEditorPreparing={onEditorPreparing}
onContentReady={(e) => {
updateWorkflowApprovalButtons(e.component)

View file

@ -1,7 +1,7 @@
/**
* Liste görünümlerinin paylaştığı küçük React hook'ları.
*/
import { useEffect, useMemo, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { RefObject } from 'react'
import type { GridDto } from '@/proxy/form/models'
import { getList } from '@/services/form.service'
@ -48,6 +48,37 @@ export const useListFormGridDto = (listFormCode: string, externalGridDto?: GridD
return gridDto
}
/**
* Widget değerleri GridDto ile bir kez hesaplanır; görünüm yenilendiğinde ya da satır
* eklendiğinde/değiştiğinde değerler bayatlamasın diye `refreshWidgets` yalnızca
* widget'ları yeniden çeker. GridDto'nun kendisi değiştirilmez: yeni referans kolonları
* ve veri kaynağını baştan kurar, görünüm durumunu (sayfa, seçim, açık satır) sıfırlardı.
*/
export const useListFormWidgets = (listFormCode: string, gridDto?: GridDto) => {
const [state, setState] = useState<{ source?: GridDto; widgets: GridDto['widgets'] }>({
widgets: [],
})
const requestIdRef = useRef(0)
const widgets = state.source === gridDto ? state.widgets : (gridDto?.widgets ?? [])
const hasWidgets = (gridDto?.widgets?.length ?? 0) > 0
const refreshWidgets = useCallback(async () => {
if (!listFormCode || !gridDto || !hasWidgets) return
const requestId = ++requestIdRef.current
try {
const response = await getList({ listFormCode })
if (requestId !== requestIdRef.current) return
setState({ source: gridDto, widgets: response.data?.widgets ?? [] })
} catch (error) {
console.error('Widget refresh error:', error)
}
}, [listFormCode, gridDto, hasWidgets])
return { widgets, refreshWidgets }
}
/**
* WidgetGroup alanının yüksekliğini izler; `fullHeight` grid'lerinin
* `calc(100vh - …)` hesabında kullanılır.

View file

@ -17,7 +17,11 @@ import {
UiLookupDataSourceTypeEnum,
} from '@/proxy/form/models'
import { addCss, getEditingFormFields } from './Utils'
import { cachedLookupLoad, clearLookupCache } from '../shared/lookupCache'
import {
cachedLookupLoad,
clearLookupCache,
clearLookupCacheByPrefix,
} from '../shared/lookupCache'
import {
NO_IMAGE,
normalizeImageSize,
@ -269,6 +273,80 @@ const useListFormColumns = ({
[listFormCode, lookupDataSource],
)
/**
* Lookup'ı ekranın kendi kayıt kaynağından beslenen kolonda (`isLookupSelfSourced`, sunucu
* hesaplar) kaydedilen yeni değer, açılışta yüklenen lookup listesinde olmadığı için hücrede ve
* editörün açılır listesinde görünmez. Başka kaynaktan beslenen lookup kayıtla değişmez; kontrol
* yalnızca bu kolonlarda yapılır ve istek yalnızca kaydedilen değer listede yoksa atılır.
* Bir kolon yenilendiyse `true` döner.
*/
const refreshLookupsForRow = useCallback(
async (instance: any, rowData?: Record<string, any>) => {
if (!instance?.columnOption || !rowData || !gridDto) return false
const getCachePrefix = (colData: ColumnFormatDto) => {
const { lookupDto } = colData
if (lookupDto.dataSourceType == UiLookupDataSourceTypeEnum.Query) {
return `query:${listFormCode}:${colData.fieldName}:`
}
if (lookupDto.dataSourceType == UiLookupDataSourceTypeEnum.WebService) {
return `api:${lookupDto.lookupQuery}:`
}
return undefined
}
const updated = await Promise.all(
gridDto.columnFormats
.filter(
(colData) =>
colData.fieldName &&
colData.lookupDto?.dataSourceType &&
!colData.lookupDto.cascadeParentFields &&
colData.isLookupSelfSourced &&
rowData[colData.fieldName] !== undefined &&
rowData[colData.fieldName] !== null &&
rowData[colData.fieldName] !== '',
)
.map(async (colData) => {
const prefix = getCachePrefix(colData)
if (!prefix) return
const source = lookupDataSource({}, colData, listFormCode)
const store =
source instanceof CustomStore
? source
: (source as { store?: CustomStore<any, any> } | undefined)?.store
const items = ((await store?.load?.()) ?? []) as Record<string, any>[]
const valueExpr = colData.lookupDto.valueExpr?.toLowerCase()
const knownValues = new Set(
items.map((item) => {
const key = Object.keys(item).find((itemKey) => itemKey.toLowerCase() === valueExpr)
return key ? String(item[key]) : undefined
}),
)
const rawValue = rowData[colData.fieldName!]
const values = Array.isArray(rawValue) ? rawValue : String(rawValue).split('|')
if (values.every((value) => knownValues.has(String(value)))) return
clearLookupCacheByPrefix(prefix)
// `columnOption('lookup.dataSource', …)` kolonu yeniden kursa da DevExtreme yalnızca
// valueMap'i boş lookup'ları yükler; hücre metnini tutan valueMap ancak lookup'ın
// kendi update()'i ile yenilenir. CardView'da DevExtreme lookup'ı yoktur; metin
// haritasını çağıran taraf yeniden kurar.
const lookup = instance.columnOption(colData.fieldName, 'lookup')
if (lookup?.update) await lookup.update()
return true
}),
)
const changed = updated.some(Boolean)
if (changed) instance.repaint?.()
return changed
},
[gridDto, listFormCode, lookupDataSource],
)
const createLookupStaticDataSource = (
load: () => any,
filter: any = null,
@ -901,6 +979,7 @@ const useListFormColumns = ({
return {
getBandedColumns,
loadLookupDisplayValues,
refreshLookupsForRow,
}
}

View file

@ -374,11 +374,13 @@ const useListFormCustomDataSource = ({
}
const updateUrl = getServiceAddress(gridOptions.updateServiceAddress)
// DevExtreme, update'in döndürdüğü nesneyi onRowUpdated'ın `data`'sı yapar ve
// refreshMode reshape/repaint'te store'a iter; axios yanıtı değil gönderilen değerler dönmeli.
return dynamicFetch(updateUrl, 'POST', searchParams, {
keys: [toPrimaryKey(key)],
data: values,
listFormCode,
})
}).then(() => values)
},
insert: function (values) {
if (!gridOptions.insertServiceAddress) {

View file

@ -65,3 +65,10 @@ export const clearLookupCache = (key?: string) => {
lookupCache.delete(key)
}
}
/** Anahtarı verilen önekle başlayan girdileri (ör. bir kolonun bütün filtre varyantları) temizler. */
export const clearLookupCacheByPrefix = (prefix: string) => {
Array.from(lookupCache.keys())
.filter((key) => key.startsWith(prefix))
.forEach((key) => lookupCache.delete(key))
}