State sistemi manuel olarak tetiklenecek hale getirildi.

This commit is contained in:
Sedat ÖZTÜRK 2026-02-05 10:03:17 +03:00
parent 6517fac26c
commit 809bfb7129
5 changed files with 231 additions and 131 deletions

View file

@ -7,7 +7,6 @@ using Erp.Platform.Enums;
using Erp.Platform.ListForms.Customization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Volo.Abp;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Repositories;

View file

@ -764,22 +764,23 @@ public class ListFormSeeder_Hr : IDataSeedContributor, ITransientDependency
DataSourceType = UiLookupDataSourceTypeEnum.StaticData,
DisplayExpr = "name",
ValueExpr = "key",
LookupQuery = JsonSerializer.Serialize(new LookupDataDto[] {
new () { Key= "⭐", Name= "⭐ Yıldız" },
new () { Key= "🏆", Name= "🏆 Kupa" },
new () { Key= "🥇", Name= "🥇 Altın Madalya" },
new () { Key= "🥈", Name= "🥈 Gümüş Madalya" },
new () { Key= "🥉", Name= "🥉 Bronz Madalya" },
new () { Key= "👑", Name= "👑 Taç" },
new () { Key= "💎", Name= "💎 Elmas" },
new () { Key= "💡", Name= "💡 Ampul" },
new () { Key= "🔥", Name= "🔥 Ateş" },
new () { Key= "âš¡", Name= "âš¡ Şimşek" },
new () { Key= "🎯", Name= "🎯 Hedef" },
new () { Key= "📈", Name= "📈 Grafik" },
new () { Key= "🚀", Name= "🚀 Roket" },
new () { Key= "💪", Name= "💪 Güç" },
new () { Key= "❤️", Name= "❤️ Kalp" }
LookupQuery = JsonSerializer.Serialize(new LookupDataDto[]
{
new() { Key = "⭐", Name = "⭐ Yıldız" },
new() { Key = "🏆", Name = "🏆 Kupa" },
new() { Key = "🥇", Name = "🥇 Altın Madalya" },
new() { Key = "🥈", Name = "🥈 Gümüş Madalya" },
new() { Key = "🥉", Name = "🥉 Bronz Madalya" },
new() { Key = "👑", Name = "👑 Taç" },
new() { Key = "💎", Name = "💎 Elmas" },
new() { Key = "💡", Name = "💡 Ampul" },
new() { Key = "🔥", Name = "🔥 Ateş" },
new() { Key = "⚡", Name = "⚡ Şimşek" },
new() { Key = "🎯", Name = "🎯 Hedef" },
new() { Key = "📈", Name = "📈 Grafik" },
new() { Key = "🚀", Name = "🚀 Roket" },
new() { Key = "💪", Name = "💪 Güç" },
new() { Key = "❤️", Name = "❤️ Kalp" }
}),
}),
ColumnCustomizationJson = DefaultColumnCustomizationJson,

View file

@ -409,10 +409,11 @@ const Grid = (props: GridProps) => {
}, [])
// Cascade parent fields mapping'i memoize et - popup açılışını hızlandırır
const cascadeFieldsMap = useMemo(() => {
if (!gridDto) return new Map()
// Ayrıca parent -> child ilişkisi için reverse map oluştur (performans için)
const { cascadeFieldsMap, parentToChildrenMap } = useMemo(() => {
if (!gridDto) return { cascadeFieldsMap: new Map(), parentToChildrenMap: new Map() }
const map = new Map<
const cascadeMap = new Map<
string,
{
parentFields: string[]
@ -420,16 +421,31 @@ const Grid = (props: GridProps) => {
}
>()
const parentChildMap = new Map<string, Set<string>>()
gridDto.columnFormats.forEach((col) => {
if (col.lookupDto?.cascadeParentFields && col.fieldName) {
map.set(col.fieldName, {
parentFields: col.lookupDto.cascadeParentFields.split(',').map((f: string) => f.trim()),
childFields: col.lookupDto.cascadeEmptyFields?.split(',').map((f: string) => f.trim()),
const parentFields = col.lookupDto.cascadeParentFields.split(',').map((f: string) => f.trim())
const childFields = col.lookupDto.cascadeEmptyFields?.split(',').map((f: string) => f.trim())
cascadeMap.set(col.fieldName, {
parentFields,
childFields,
})
// Her parent field için, hangi childları etkilediğini kaydet
parentFields.forEach((parentField) => {
if (!parentChildMap.has(parentField)) {
parentChildMap.set(parentField, new Set())
}
if (col.fieldName) {
parentChildMap.get(parentField)!.add(col.fieldName)
}
})
}
})
return map
return { cascadeFieldsMap: cascadeMap, parentToChildrenMap: parentChildMap }
}, [gridDto])
const onEditorPreparing = useCallback(
@ -439,50 +455,48 @@ const Grid = (props: GridProps) => {
.flatMap((group) => group.items || [])
.find((i) => i.dataField === editor.dataField)
// Cascade disabled mantığı
// Cascade mantığı
const cascadeInfo = cascadeFieldsMap.get(editor.dataField)
if (cascadeInfo) {
const parentFields = cascadeInfo.parentFields
const childFields = cascadeInfo.childFields
const affectedChildren = parentToChildrenMap.get(editor.dataField!)
const prevHandler = editor.editorOptions.onValueChanged
editor.editorOptions.onValueChanged = (e: any) => {
if (prevHandler) prevHandler(e)
prevHandler?.(e)
// Parent field değiştiğinde tüm cascade childları kontrol et
const grid = editor.component
const rowKey = grid.option('editing.editRowKey')
const rowIndex = grid.getRowIndexByKey(rowKey)
const rowData = grid.getVisibleRows().find((r) => r.key === rowKey)?.data || {}
// Bu field bir parent ise, child fieldleri temizle
if (cascadeInfo.childFields) {
cascadeInfo.childFields.forEach((childField: string) => {
if (rowIndex >= 0) {
grid.cellValue(rowIndex, childField, null)
}
// Parent field değiştiğinde child fieldleri temizle
if (childFields && rowIndex >= 0) {
childFields.forEach((childField: string) => {
grid.cellValue(rowIndex, childField, null)
})
}
// Tüm cascade fieldlerin disabled durumlarını güncelle - sadece değişen field için
const popup = grid.option('editing.popup')
if (popup) {
// Sadece bu parent'tan etkilenen childların disabled durumunu güncelle
if (affectedChildren?.size) {
const formInstance = grid.option('editing.form') as any
if (formInstance && formInstance.getEditor) {
// Sadece bu field'den etkilenen childları güncelle
cascadeFieldsMap.forEach((info, fieldName) => {
if (info.parentFields.includes(editor.dataField!)) {
const shouldDisable = info.parentFields.some((pf: string) => !rowData[pf])
try {
const editorInstance = formInstance.getEditor(fieldName)
if (editorInstance) {
editorInstance.option('disabled', shouldDisable)
}
} catch (err) {
console.debug('Cascade disabled update skipped for', fieldName, err)
if (formInstance?.getEditor) {
const visibleRows = grid.getVisibleRows()
const rowData = visibleRows.find((r) => r.key === rowKey)?.data
if (rowData) {
affectedChildren.forEach((childFieldName: string) => {
const childInfo = cascadeFieldsMap.get(childFieldName)
if (childInfo) {
const shouldDisable = childInfo.parentFields.some(
(pf: string) => !rowData[pf],
)
try {
formInstance.getEditor(childFieldName)?.option('disabled', shouldDisable)
} catch {}
}
}
})
})
}
}
}
}
@ -490,10 +504,10 @@ const Grid = (props: GridProps) => {
// İlk açılışta disabled durumunu kontrol et
const grid = editor.component
const rowKey = grid.option('editing.editRowKey')
const rowData = grid.getVisibleRows().find((r) => r.key === rowKey)?.data || {}
const shouldDisable = parentFields.some((pf: string) => !rowData[pf])
const visibleRows = grid.getVisibleRows()
const rowData = visibleRows.find((r) => r.key === rowKey)?.data
if (shouldDisable) {
if (rowData && parentFields.some((pf: string) => !rowData[pf])) {
editor.editorOptions.disabled = true
}
}
@ -800,43 +814,34 @@ const Grid = (props: GridProps) => {
refListFormCode.current = listFormCode
}, [listFormCode])
// StateStoring fonksiyonlarını ref'e kaydet - her render'da yeniden oluşturulmasın
const customSaveStateRef = useRef(customSaveState)
const customLoadStateRef = useRef(customLoadState)
// Component mount olduğunda state'i bir kez yükle
useEffect(() => {
customSaveStateRef.current = customSaveState
customLoadStateRef.current = customLoadState
}, [customSaveState, customLoadState])
if (!gridDto || !gridRef?.current || !gridDataSource || !columnData) return
// StateStoring'i sadece gridDto değiştiğinde ayarla
const instance = gridRef?.current?.instance()
if (instance) {
customLoadState().then((state) => {
if (state) {
instance.state(state)
}
}).catch((err) => {
console.error('State load error:', err)
})
}
}, [gridDto, gridDataSource, columnData])
// StateStoring'i devre dışı bırak - manuel kaydetme kullanılacak
useEffect(() => {
if (!gridDto || !gridRef?.current) return
const instance = gridRef?.current?.instance()
if (instance) {
const stateStoring: IStateStoringProps = {
enabled: gridDto?.gridOptions.stateStoringDto?.enabled,
type: gridDto?.gridOptions.stateStoringDto?.type,
savingTimeout: gridDto?.gridOptions.stateStoringDto?.savingTimeout,
storageKey: gridDto?.gridOptions.stateStoringDto?.storageKey,
}
if (
gridDto?.gridOptions.stateStoringDto?.enabled &&
gridDto?.gridOptions.stateStoringDto?.type === 'custom'
) {
// Ref'i kullan, direkt fonksiyon yerine
stateStoring.customSave = (state: any) => customSaveStateRef.current(state)
stateStoring.customLoad = () => customLoadStateRef.current()
}
instance.option('stateStoring', stateStoring)
// State'i yükle - dataSource ve columns hazır olduğunda
if (gridDataSource && columnData) {
instance.state(undefined) // undefined = reload from storage
}
// Otomatik state kaydetme/yükleme kapalı
instance.option('stateStoring', {
enabled: false,
})
}
}, [gridDto, gridDataSource, columnData]) // dataSource ve columns hazır olduğunda state yükle
}, [gridDto])
useEffect(() => {
refListFormCode.current = listFormCode

View file

@ -39,7 +39,7 @@ import {
import { useFilters } from './useFilters'
import WidgetGroup from '@/components/common/WidgetGroup'
import { Button, Notification, toast } from '@/components/ui'
import { FaCog, FaTimes, FaUndo } from 'react-icons/fa'
import { FaCog, FaSave, FaTimes, FaUndo } from 'react-icons/fa'
import { usePermission } from '@/utils/hooks/usePermission'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { usePWA } from '@/utils/hooks/usePWA'
@ -74,6 +74,11 @@ const Pivot = (props: PivotProps) => {
const [gridDataSource, setGridDataSource] = useState<CustomStore<any, any>>()
const [columnData, setColumnData] = useState<GridColumnData[]>()
// StateStoring için storageKey'i memoize et
const storageKey = useMemo(() => {
return gridDto?.gridOptions.stateStoringDto?.storageKey ?? ''
}, [gridDto?.gridOptions.stateStoringDto?.storageKey])
const { filterToolbarData, ...filterData } = useFilters({
gridDto,
gridRef,
@ -159,19 +164,32 @@ const Pivot = (props: PivotProps) => {
const resetPivotGridState = useCallback(async () => {
const grid = gridRef.current?.instance()
if (grid) {
// kullaniciya ait kayitli grid state i sil customizationData boşalt silinsin.
// State'i veritabanından sil
await postListFormCustomization({
listFormCode: listFormCode,
customizationType: ListFormCustomizationTypeEnum.GridState,
filterName: `pivot-${gridRef.current?.instance()?.option('stateStoring')?.storageKey ?? ''}`,
filterName: `pivot-${storageKey}`,
customizationData: '',
})
await props.refreshGridDto()
// DataSource'u resetle
const ds = grid.getDataSource()
if (ds && typeof ds.state === 'function') {
ds.state(null) // State'i temizle
}
// Filtreleri temizle
clearPivotFilters()
moveAllFieldsToFilterArea()
setGridPanelColor('transparent')
toast.push(
<Notification type="success" duration={2000}>
{translate('::ListForms.ListForm.GridStateReset')}
</Notification>,
{ placement: 'top-end' },
)
}
}, [listFormCode, props, clearPivotFilters, moveAllFieldsToFilterArea])
}, [listFormCode, storageKey, clearPivotFilters, translate])
const onExporting = useCallback(async (e: PivotGridTypes.ExportingEvent) => {
e.cancel = true
@ -217,13 +235,13 @@ const Pivot = (props: PivotProps) => {
return postListFormCustomization({
listFormCode: listFormCode,
customizationType: ListFormCustomizationTypeEnum.GridState,
filterName: `pivot-${gridRef.current?.instance()?.option('stateStoring')?.storageKey ?? ''}`,
filterName: `pivot-${storageKey}`,
customizationData: JSON.stringify(state),
}).then(() => {
setGridPanelColor(statedGridPanelColor)
})
},
[listFormCode],
[listFormCode, storageKey],
)
const customLoadState = useCallback(
@ -231,25 +249,18 @@ const Pivot = (props: PivotProps) => {
return getListFormCustomization(
listFormCode,
ListFormCustomizationTypeEnum.GridState,
`pivot-${gridRef.current?.instance()?.option('stateStoring')?.storageKey ?? ''}`,
`pivot-${storageKey}`,
).then((response: any) => {
setGridPanelColor(statedGridPanelColor)
if (response.data?.length > 0) {
setGridPanelColor(statedGridPanelColor)
return JSON.parse(response.data[0].customizationData)
}
return null
})
},
[listFormCode],
[listFormCode, storageKey],
)
const customSaveStateRef = useRef(customSaveState)
const customLoadStateRef = useRef(customLoadState)
useEffect(() => {
customSaveStateRef.current = customSaveState
customLoadStateRef.current = customLoadState
}, [customSaveState, customLoadState])
useEffect(() => {
refListFormCode.current = listFormCode
}, [listFormCode])
@ -307,7 +318,19 @@ const Pivot = (props: PivotProps) => {
}
}, [memoizedDataSource])
// Pivot dataSource ve fields'i set et + StateStoring ayarla
// StateStoring'i devre dışı bırak - manuel kaydetme kullanılacak
useEffect(() => {
if (!gridDto || !gridRef?.current) return
const instance = gridRef?.current?.instance()
if (instance) {
instance.option('stateStoring', {
enabled: false,
})
}
}, [gridDto])
// Pivot dataSource ve fields'i set et
useEffect(() => {
if (!columnData || !gridDataSource || !gridRef?.current || !gridDto) {
return
@ -316,23 +339,7 @@ const Pivot = (props: PivotProps) => {
const instance = gridRef?.current?.instance()
if (!instance) return
// 1. StateStoring'i ÖNCE ayarla
const stateStoring: any = {
enabled: gridDto?.gridOptions.stateStoringDto?.enabled,
type: gridDto?.gridOptions.stateStoringDto?.type,
savingTimeout: gridDto?.gridOptions.stateStoringDto?.savingTimeout,
storageKey: gridDto?.gridOptions.stateStoringDto?.storageKey,
}
if (
gridDto?.gridOptions.stateStoringDto?.enabled &&
gridDto?.gridOptions.stateStoringDto?.type === 'custom'
) {
stateStoring.customSave = (state: any) => customSaveStateRef.current(state)
stateStoring.customLoad = () => customLoadStateRef.current()
}
instance.option('stateStoring', stateStoring)
// 2. Default fields'i hazırla
// Default fields'i hazırla
const defaultFields: any = columnData?.map((b) => {
return {
dataField: b.dataField,
@ -350,7 +357,7 @@ const Pivot = (props: PivotProps) => {
} as Field
})
// 3. DataSource'u ayarla - fields olmadan
// DataSource'u ayarla
const dataSource: PivotGridTypes.Properties['dataSource'] = {
remoteOperations: true,
store: gridDataSource,
@ -358,16 +365,27 @@ const Pivot = (props: PivotProps) => {
}
instance.option('dataSource', dataSource)
// 4. DataSource set edildikten SONRA state'i yükle - state fields'i override edecek
setTimeout(() => {
const ds = instance.getDataSource()
if (ds && typeof ds.state === 'function') {
ds.state(null) // Bu saved state'teki fields'i kullanacak
}
}, 50)
}, [columnData, gridDataSource, gridDto])
// Component mount olduğunda state'i bir kez yükle
useEffect(() => {
if (!gridDto || !gridRef?.current || !columnData || !gridDataSource) return
const instance = gridRef?.current?.instance()
if (instance) {
customLoadState().then((state) => {
if (state) {
const ds = instance.getDataSource()
if (ds && typeof ds.state === 'function') {
ds.state(state)
}
}
}).catch((err) => {
console.error('Pivot state load error:', err)
})
}
}, [gridDto, columnData, gridDataSource, customLoadState])
// Chart binding - sadece bir kez
useEffect(() => {
if (!gridRef?.current || !chartRef?.current) return
@ -409,6 +427,39 @@ const Pivot = (props: PivotProps) => {
<FaTimes className="w-3 h-3" /> {translate('::ListForms.ListForm.RemoveFilter')}
</Button>
<Button
size="xs"
variant={'default'}
className="text-sm flex items-center gap-1"
onClick={() => {
const instance = gridRef.current?.instance()
if (instance) {
const ds = instance.getDataSource()
if (ds && typeof ds.state === 'function') {
const currentState = ds.state()
customSaveState(currentState).then(() => {
toast.push(
<Notification type="success" duration={2000}>
{translate('::ListForms.ListForm.GridStateSaved')}
</Notification>,
{ placement: 'top-end' },
)
}).catch(() => {
toast.push(
<Notification type="danger" duration={2500}>
{translate('::ListForms.ListForm.GridStateSaveError')}
</Notification>,
{ placement: 'top-end' },
)
})
}
}
}}
title={translate('::ListForms.ListForm.SaveGridState')}
>
<FaSave className="w-3 h-3" /> {translate('::ListForms.ListForm.SaveGridState')}
</Button>
<Button
size="xs"
variant={'default'}

View file

@ -4,7 +4,7 @@ import {
ListFormCustomizationForUserDto,
ListFormCustomizationTypeEnum,
} from '@/proxy/form/models'
import { getListFormCustomization } from '@/services/list-form-customization.service'
import { getListFormCustomization, postListFormCustomization } from '@/services/list-form-customization.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { DataGridRef } from 'devextreme-react/data-grid'
import { PivotGridRef } from 'devextreme-react/pivot-grid'
@ -173,6 +173,7 @@ const useFilters = ({
const [isImportModalOpen, setIsImportModalOpen] = 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 config = useStoreState((state) => state.abpConfig.config)
@ -239,6 +240,12 @@ const useFilters = ({
//Kullanicinin gridstate kaydi yapmasina izin verilmis ise gridState menu elemanlarini ekle
if (grdOpt?.stateStoringDto?.enabled) {
menus.push({
text: translate('::ListForms.ListForm.SaveGridState'),
id: 'saveGridState',
icon: 'save',
})
menus.push({
text: translate('::ListForms.ListForm.ResetGridState'),
id: 'resetGridState',
@ -310,6 +317,43 @@ const useFilters = ({
setGridPanelColor('transparent')
setToolbarItemValue(grid, 'selectCustomFilters', '')
} else if (itemData.id === 'saveGridState') {
// Grid'in mevcut state'ini kaydet
const grid = gridRef.current?.instance()
if (!grid) {
return
}
const currentState = grid.state()
const storageKey = gridDto?.gridOptions.stateStoringDto?.storageKey ?? ''
postListFormCustomization({
listFormCode: listFormCode,
customizationType: ListFormCustomizationTypeEnum.GridState,
filterName: `list-${storageKey}`,
customizationData: JSON.stringify(currentState),
})
.then(() => {
setGridPanelColor(statedGridPanelColor)
toast.push(
<Notification type="success" duration={2000}>
{translate('::ListForms.ListForm.GridStateSaved')}
</Notification>,
{
placement: 'top-end',
},
)
})
.catch((err) => {
toast.push(
<Notification type="danger" duration={2500}>
{translate('::ListForms.ListForm.GridStateSaveError')}
</Notification>,
{
placement: 'top-end',
},
)
})
} else if (itemData.id === 'resetGridState') {
// state ye kaydedilen grid ayarlarini siler ve gridi resetler
const grid = gridRef.current?.instance()