Optimizasyon güncellemeleri

This commit is contained in:
Sedat ÖZTÜRK 2026-07-10 12:22:54 +03:00
parent d982a3ce42
commit 7051a84b4b
19 changed files with 525 additions and 465 deletions

View file

@ -5,6 +5,7 @@ using Sozsoft.Settings.Localization;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Localization;
using Volo.Abp.Settings;
using Volo.Abp.Threading;
using SettingDefinition = Sozsoft.Settings.Entities.SettingDefinition;
namespace Sozsoft.Settings;
@ -22,8 +23,7 @@ public class SettingsDefinitionProvider : SettingDefinitionProvider
public override void Define(ISettingDefinitionContext context)
{
repository.DisableTracking();
var settingDefinitions = repository.GetListAsync().Result;
//var settingDefinitions = AsyncHelper.RunSync(() => repository.ToListAsync());
var settingDefinitions = AsyncHelper.RunSync(() => repository.GetListAsync());
foreach (var item in settingDefinitions.OrderBy(a => a.Order))
{

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Authorization;
using Volo.Abp.Domain.Entities;
@ -272,16 +273,28 @@ public class ForumAppService : PlatformAppService, IForumAppService
public async Task<ForumTopicDto> GetTopicAsync(Guid id)
{
var topic = await _topicRepository.GetAsync(id);
var queryable = await _topicRepository.GetQueryableAsync();
var affectedRows = await queryable
.Where(t => t.Id == id)
.ExecuteUpdateAsync(setters =>
setters.SetProperty(t => t.ViewCount, t => t.ViewCount + 1)
);
// View count artırma işlemi arka planda yapılmalı (performans için)
// Her okumada update yapmak performans sorununa neden olur
_ = Task.Run(async () =>
if (affectedRows == 0)
{
var t = await _topicRepository.GetAsync(id);
t.ViewCount++;
await _topicRepository.UpdateAsync(t);
});
throw new EntityNotFoundException(typeof(ForumTopic), id);
}
var topic = await AsyncExecuter.FirstOrDefaultAsync(
queryable
.AsNoTracking()
.Where(t => t.Id == id)
);
if (topic == null)
{
throw new EntityNotFoundException(typeof(ForumTopic), id);
}
return ObjectMapper.Map<ForumTopic, ForumTopicDto>(topic);
}
@ -454,14 +467,13 @@ public class ForumAppService : PlatformAppService, IForumAppService
topic.ReplyCount = Math.Max(0, topic.ReplyCount - 1);
category.PostCount = Math.Max(0, category.PostCount ?? 0 - 1);
// 🔁 Last post değişti mi kontrol et
var latestPost = await _postRepository
.GetQueryableAsync()
.ContinueWith(q => q.Result
// Last post değişti mi kontrol et
var postsQueryable = await _postRepository.GetQueryableAsync();
var latestPost = await AsyncExecuter.FirstOrDefaultAsync(
postsQueryable
.Where(p => p.TopicId == topic.Id)
.OrderByDescending(p => p.CreationTime)
.FirstOrDefault()
);
);
if (latestPost != null)
{

View file

@ -73,7 +73,7 @@ public class ListFormQueryPreviewAppService : PlatformAppService
var (_, _, dataSourceType) = await dynamicDataManager.GetAsync(listForm.IsTenant, listForm.DataSourceCode);
selectQueryManager.PrepareQueries(listForm, readableFields, dataSourceType, customizations);
await selectQueryManager.PrepareQueriesAsync(listForm, readableFields, dataSourceType, customizations);
return selectQueryManager.SelectQuery;
}

View file

@ -55,12 +55,12 @@ public class ListFormDataAppService : PlatformAppService
var queryParameters = httpContext.Request.Query.ToDictionary(x => x.Key, x => x.Value);
object filter = new object[] { input.Data[0], "=", input.Keys[0] };
object[] filter = [input.Data[0], "=", input.Keys[0]];
var selectRequest = new SelectRequestDto
{
ListFormCode = input.ListFormCode,
Filter = filter.ToString(),
Filter = JsonSerializer.Serialize(filter),
Skip = 0,
Take = 1,
RequireTotalCount = false,

View file

@ -160,7 +160,7 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
var (dynamicDataRepository, connectionString, dataSourceType) = await dynamicDataManager.GetAsync(listForm.IsTenant, listForm.DataSourceCode);
selectQueryManager.PrepareQueries(listForm, fields, dataSourceType, customizations, queryParams);
await selectQueryManager.PrepareQueriesAsync(listForm, fields, dataSourceType, customizations, queryParams);
var param = selectQueryManager.SelectQueryParameters;
if (!string.IsNullOrEmpty(queryParams.Group) && !string.IsNullOrEmpty(selectQueryManager.GroupQuery))
@ -293,11 +293,6 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
//kullaniciya ait ListFormField verilerini al
var fields = await listFormFieldManager.GetUserListFormFields(ListFormCode);
// selectQueryManager.PrepareQueries(listForm, fields);
// fields = fields.Where(c => selectQueryManager.SelectFields.Any(sc => sc.FieldName == c.FieldName))
// .OrderBy(c => c.ListOrderNo)
// .ThenBy(c => c.CaptionName)
// .ToList();
fields = fields.OrderBy(c => c.ListOrderNo)
.ThenBy(c => c.CaptionName)
.ToList();

View file

@ -4,6 +4,7 @@ using System.Data;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Enums;
using Sozsoft.Platform.Extensions;
@ -32,11 +33,11 @@ public interface ISelectQueryManager
List<string> SummaryQueries { get; }
public bool IsAppliedGridFilter { get; }
public bool IsAppliedServerFilter { get; }
void PrepareQueries(ListForm listform,
List<ListFormField> listFormFields,
DataSourceTypeEnum dataSourceType,
List<ListFormCustomization> listFormCustomizations = null,
QueryParameters queryParams = null);
Task PrepareQueriesAsync(ListForm listform,
List<ListFormField> listFormFields,
DataSourceTypeEnum dataSourceType,
List<ListFormCustomization> listFormCustomizations = null,
QueryParameters queryParams = null);
}
public class SelectField
@ -129,11 +130,11 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
DefaultValueHelper = defaultValueHelper;
}
public void PrepareQueries(ListForm listform,
List<ListFormField> listFormFields,
DataSourceTypeEnum dataSourceType,
List<ListFormCustomization> listFormCustomizations = null,
QueryParameters queryParams = null)
public async Task PrepareQueriesAsync(ListForm listform,
List<ListFormField> listFormFields,
DataSourceTypeEnum dataSourceType,
List<ListFormCustomization> listFormCustomizations = null,
QueryParameters queryParams = null)
{
ResetQueryState();
@ -170,7 +171,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
var joinParts = SelectFields.Where(c => !string.IsNullOrEmpty(c.JoinSql)).Select(c => c.JoinSql).Distinct();
JoinParts.AddRange(joinParts);
List<string> whereFields = GetWhereFields(listform, listFormFields, queryParams);
List<string> whereFields = await GetWhereFieldsAsync(listform, listFormFields, queryParams);
WhereParts.AddRange(whereFields);
List<string> sortFields = GetSortFields(listform, listFormFields, queryParams?.Sort);
@ -343,7 +344,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
}
}
private List<string> GetWhereFields(ListForm listform, List<ListFormField> listFormFields, QueryParameters queryParams = null)
private async Task<List<string>> GetWhereFieldsAsync(ListForm listform, List<ListFormField> listFormFields, QueryParameters queryParams = null)
{
var whereParts = new List<string>();
@ -436,7 +437,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
whereParts.Add("AND");
}
var ids = BranchUsersRepository.GetListAsync((a) => a.UserId == CurrentUser.Id.Value).Result;
var ids = await BranchUsersRepository.GetListAsync((a) => a.UserId == CurrentUser.Id.Value);
if (ids.Count > 0)
{
whereParts.Add($"\"BranchId\" IN ({string.Join(",", ids.Select(a => $"'{a.BranchId}'"))})");
@ -454,7 +455,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
whereParts.Add("AND");
}
var ids = OuRepository.GetOrganizationUnitIdsWithChildren(CurrentUser.Id.Value).Result;
var ids = await OuRepository.GetOrganizationUnitIdsWithChildren(CurrentUser.Id.Value);
if (ids.Count > 0)
{
whereParts.Add($"\"OrganizationUnitId\" IN ({string.Join(",", ids.Select(a => $"'{a}'"))})");

View file

@ -104,7 +104,7 @@ public class DynamicFormReport : XtraReport
Skip = 0
};
selectQueryManager.PrepareQueries(listForm, fields, dataSourceType, customizations, queryParams);
await selectQueryManager.PrepareQueriesAsync(listForm, fields, dataSourceType, customizations, queryParams);
var reportFields = GetReportFields(listForm, fields, selectQueryManager, defaultValueHelper);
var selectQuery = DynamicReportImageHelper.StripReportPaging(DynamicReportImageHelper.ApplyQueryParameters(

View file

@ -105,7 +105,7 @@ public class DynamicGridReport : XtraReport
Skip = 0
};
selectQueryManager.PrepareQueries(listForm, fields, dataSourceType, customizations, queryParams);
await selectQueryManager.PrepareQueriesAsync(listForm, fields, dataSourceType, customizations, queryParams);
var selectFieldsByName = DynamicReportImageHelper.CreateSelectFieldMap(selectQueryManager.SelectFields);
var editorTypesByField = DynamicReportImageHelper.CreateEditorTypeMap(listForm);

View file

@ -112,7 +112,7 @@ public class DynamicTreeReport : XtraReport
Skip = 0
};
selectQueryManager.PrepareQueries(listForm, fields, dataSourceType, customizations, queryParams);
await selectQueryManager.PrepareQueriesAsync(listForm, fields, dataSourceType, customizations, queryParams);
var selectFieldsByName = DynamicReportImageHelper.CreateSelectFieldMap(selectQueryManager.SelectFields);
var editorTypesByField = DynamicReportImageHelper.CreateEditorTypeMap(listForm);

View file

@ -1,5 +1,5 @@
import { AUTH_API_NAME, DEFAULT_API_NAME } from '@/constants/app.constant'
import type { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'
import type { AxiosRequestConfig, AxiosResponse } from 'axios'
import authApiService from './authApi.service'
import platformApiService from './platformApi.service'
@ -11,29 +11,18 @@ export default {
fetchData<Response = unknown, Request = Record<string, unknown>>(
param: AxiosRequestConfig<Request>,
config?: Config,
) {
return new Promise<AxiosResponse<Response>>((resolve, reject) => {
if (!config || config?.apiName === DEFAULT_API_NAME) {
platformApiService(param)
.then((response: AxiosResponse<Response>) => {
resolve(response)
})
.catch((errors: AxiosError) => {
// console.error(errors)
reject(errors)
})
} else if (config?.apiName === AUTH_API_NAME) {
authApiService(param)
.then((response: AxiosResponse<Response>) => {
resolve(response)
})
.catch((errors: AxiosError) => {
console.error(errors)
reject(errors)
})
} else {
reject(new Error('NO API DEFINED'))
}
})
): Promise<AxiosResponse<Response>> {
if (!config || config?.apiName === DEFAULT_API_NAME) {
return platformApiService(param) as Promise<AxiosResponse<Response>>
}
if (config?.apiName === AUTH_API_NAME) {
return authApiService(param).catch((errors) => {
console.error(errors)
throw errors
}) as Promise<AxiosResponse<Response>>
}
return Promise.reject(new Error('NO API DEFINED'))
},
}

View file

@ -1,12 +1,9 @@
import { ColumnFormatDto, GridBoxOptionsDto } from '@/proxy/form/models'
import { Button } from 'devextreme-react/button'
import DataGrid, { DataGridRef } from 'devextreme-react/data-grid'
import DropDownBox from 'devextreme-react/drop-down-box'
import { ReactElement, useRef } from 'react'
import { ReactElement } from 'react'
import { SharedGridBoxEditor } from '@/views/shared/SharedGridBoxEditor'
const GridBoxEditorComponent = ({
value,
values,
options,
onValueChanged,
col,
@ -19,99 +16,17 @@ const GridBoxEditorComponent = ({
col?: ColumnFormatDto
editorOptions: any
}): ReactElement => {
const gridRef = useRef<DataGridRef>(null)
const val = Array.isArray(value) ? value : [value]
const lookupDto = col?.lookupDto
// const dataSource: any =
// typeof lookupDto?.dataSource === 'function'
// ? lookupDto.dataSource(values)
// : (lookupDto?.dataSource ?? [])
return (
<DropDownBox
{...editorOptions}
value={val}
<SharedGridBoxEditor
value={value}
dataSource={editorOptions?.dataSource}
valueExpr={lookupDto?.valueExpr}
displayExpr={lookupDto?.displayExpr}
showClearButton={options?.showClearButton}
acceptCustomValue={options?.acceptCustomValue}
onValueChanged={(e) => {
onValueChanged?.(typeof e.value === 'string' ? e.value?.trim() : e.value)
//Eğer clear button kullanılırsa altında grid bilgileride otomatik deselect olmalı ve dropbox kapanmalı
if (e.value === null) {
e.component.close()
gridRef.current?.instance().deselectAll()
}
}}
contentRender={(e) => (
<>
<DataGrid
ref={gridRef}
dataSource={editorOptions?.dataSource}
key={lookupDto?.valueExpr}
filterRow={{
visible: options?.filterRowVisible,
applyFilter: 'auto',
}}
columns={options?.columns.map((c: string) => c.toLowerCase())} //AliBaba -> alibaba
hoverStateEnabled={true}
height={options?.height}
selection={{
mode: options?.selectionMode,
}}
onRowClick={(event) => {
//Single seçimlerde otomatik kapanmalı ve seçtiği değer yazılmalı
if (options?.selectionMode === 'single') {
e.component.option('value', event.data.key)
e.component.close()
}
}}
onEditorPrepared={(event) => {
const getListValues = async () => {
const data = await editorOptions?.dataSource?.load()
const listValues = data.map((a: any) => ({
key: a.key,
name: a.name,
}))
event.component.option(
'selectedRowKeys',
listValues.filter((a: any) => val.includes(a.key)),
)
}
getListValues()
}}
></DataGrid>
{options?.selectionMode === 'multiple' && (
<div className="text-center">
<Button
text="OK"
className="mr-2 min-w-[100px]"
onClick={() => {
e.component.option(
'value',
gridRef.current?.instance().getSelectedRowKeys().map((a: any) => a.key),
)
e.component.close()
}}
></Button>
<Button
text="Cancel"
className="min-w-[100px]"
onClick={() => {
e.component.close()
}}
></Button>
</div>
)}
</>
)}
></DropDownBox>
lookup={lookupDto}
options={options}
editorOptions={editorOptions}
onValueChanged={onValueChanged}
/>
)
}

View file

@ -6,14 +6,9 @@ import {
EditingFormItemDto,
FieldCustomValueTypeEnum,
GridDto,
ListFormCustomizationTypeEnum,
PlatformEditorTypes,
SubFormTabTypeEnum,
} from '@/proxy/form/models'
import {
getListFormCustomization,
postListFormCustomization,
} from '@/services/list-form-customization.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { executeEditorScript } from '@/utils/editorScriptRuntime'
import { Template } from 'devextreme-react/core/template'
@ -58,8 +53,8 @@ import {
controlStyleCondition,
extractSearchParamsFields,
GridExtraFilterState,
safeJsonParse,
setFormEditingExtraItemValues,
setGridPanelColor,
} from './Utils'
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent'
@ -79,6 +74,7 @@ import { Loading } from '@/components/shared'
import { useStoreState } from '@/store'
import { workflowService } from '@/services/workflow.service'
import { NotePanel } from '../form/notes/NotePanel'
import { useListFormStateStoring } from './useListFormStateStoring'
interface GridProps {
listFormCode: string
@ -89,8 +85,6 @@ interface GridProps {
gridDto?: GridDto
}
const statedGridPanelColor = 'rgba(50, 200, 200, 0.5)' // kullanici tanimli gridState ile islem gormus gridin paneline ait renk
const isTemporaryDxKey = (key: unknown) => typeof key === 'string' && key.startsWith('_DX_KEY_')
const getPersistedInsertedKey = (
@ -322,47 +316,12 @@ const Grid = (props: GridProps) => {
}
}, [searchParams])
// StateStoring için storageKey'i memoize et
const storageKey = useMemo(() => {
return gridDto?.gridOptions.stateStoringDto?.storageKey ?? ''
}, [gridDto?.gridOptions.stateStoringDto?.storageKey])
const customSaveState = useCallback(
(state: any) => {
if (isEditingRef.current) {
return Promise.resolve()
}
return postListFormCustomization({
listFormCode: listFormCode,
customizationType: ListFormCustomizationTypeEnum.GridState,
filterName: `list-${storageKey}`,
customizationData: JSON.stringify(state),
})
.then(() => {
setGridPanelColor(statedGridPanelColor)
toast.push(
<Notification type="success" duration={2000}>
{translate('::ListForms.ListForm.GridStateSaved')}
</Notification>,
{
placement: 'bottom-end',
},
)
})
.catch((err) => {
toast.push(
<Notification type="danger" duration={2500}>
{translate('::ListForms.ListForm.GridStateSaveError')}
</Notification>,
{
placement: 'bottom-end',
},
)
throw err
})
},
[listFormCode, storageKey, translate],
)
const { customSaveState, customLoadState } = useListFormStateStoring({
listFormCode,
storageKey: gridDto?.gridOptions.stateStoringDto?.storageKey,
filterPrefix: 'list',
skipSaveRef: isEditingRef,
})
const { filterToolbarData, ...filterData } = useFilters({
gridDto,
@ -839,7 +798,9 @@ const Grid = (props: GridProps) => {
} else {
childEditor?.option('disabled', shouldDisable)
}
} catch {}
} catch (err) {
console.log(`Error occurred while updating child editor for field "${childFieldName}":`, err)
}
}
})
}
@ -928,21 +889,6 @@ const Grid = (props: GridProps) => {
[gridDto, cascadeFieldsMap, parentToChildrenMap, mode],
)
const customLoadState = useCallback(() => {
return getListFormCustomization(
listFormCode,
ListFormCustomizationTypeEnum.GridState,
`list-${storageKey}`,
).then((response: any) => {
if (response.data?.length > 0) {
setGridPanelColor(statedGridPanelColor)
return JSON.parse(response.data[0].customizationData)
}
// Veri yoksa null dön (DevExtreme bunu default state olarak algılar)
return null
})
}, [listFormCode, storageKey])
useEffect(() => {
// React state'i sıfırla - eski değerlerin customLoadState'i erken tetiklemesini önle
setGridDataSource(undefined)
@ -1005,7 +951,7 @@ const Grid = (props: GridProps) => {
let base: any = null
if (defaultSearchParams.current) {
base = JSON.parse(defaultSearchParams.current)
base = safeJsonParse(defaultSearchParams.current, null, 'Search filter parse error:')
}
// Default filter tripletlerini çıkar
@ -1168,7 +1114,7 @@ const Grid = (props: GridProps) => {
instance.option('remoteOperations', remoteOps)
instance.option('dataSource', gridDataSource)
})
}, [gridDto, gridDataSource, columnData])
}, [gridDto, gridDataSource, columnData, customLoadState])
// StateStoring'i devre dışı bırak - manuel kaydetme kullanılacak
useEffect(() => {
@ -1201,7 +1147,7 @@ const Grid = (props: GridProps) => {
const parsed = JSON.parse(rawFilter)
const filters = extractSearchParamsFields(parsed)
const hasFilter = filters.some(([field, op, val]) => field === i.dataField)
const hasFilter = filters.some(([field]) => field === i.dataField)
if (hasFilter) {
const existsInExtra = extraFilters.some((f) => f.fieldName === i.dataField && !!f.value)
@ -1211,7 +1157,9 @@ const Grid = (props: GridProps) => {
}
}
}
} catch {}
} catch (err) {
console.error('EditorOptions parse error:', i.dataField, err)
}
const fieldName = i.dataField.split(':')[0]
const listFormField = gridDto?.columnFormats.find((x: any) => x.fieldName === fieldName)

View file

@ -1,10 +1,7 @@
import Container from '@/components/shared/Container'
import { APP_NAME, DX_CLASSNAMES } from '@/constants/app.constant'
import { GridDto, ListFormCustomizationTypeEnum } from '@/proxy/form/models'
import {
getListFormCustomization,
postListFormCustomization,
} from '@/services/list-form-customization.service'
import { postListFormCustomization } from '@/services/list-form-customization.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
import Chart, { ChartRef, CommonSeriesSettings, Size, Tooltip } from 'devextreme-react/chart'
import PivotGrid, {
@ -41,6 +38,7 @@ import { useListFormColumns } from './useListFormColumns'
import { useStoreState } from '@/store'
import Checkbox from '@/components/ui/Checkbox'
import Toolbar, { Item } from 'devextreme-react/toolbar'
import { useListFormStateStoring } from './useListFormStateStoring'
interface PivotProps {
listFormCode: string
@ -52,8 +50,6 @@ interface PivotProps {
refreshGridDto: () => Promise<void>
}
const statedGridPanelColor = 'rgba(50, 200, 200, 0.5)' // kullanici tanimli gridState ile islem gormus gridin paneline ait renk
type LookupDisplayValues = Record<string, Map<string, string>>
const Pivot = (props: PivotProps) => {
@ -72,11 +68,12 @@ const Pivot = (props: PivotProps) => {
const [lookupDisplayValues, setLookupDisplayValues] = useState<LookupDisplayValues>({})
const [showChart, setShowChart] = useState(false)
// StateStoring için storageKey'i memoize et
const storageKey = useMemo(() => {
return gridDto?.gridOptions.stateStoringDto?.storageKey ?? ''
}, [gridDto?.gridOptions.stateStoringDto?.storageKey])
const { customSaveState, customLoadState, storageKey } = useListFormStateStoring({
listFormCode,
storageKey: gridDto?.gridOptions.stateStoringDto?.storageKey,
filterPrefix: 'pivot',
showSaveToast: false,
})
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef })
const { getBandedColumns, loadLookupDisplayValues } = useListFormColumns({
@ -212,35 +209,6 @@ const Pivot = (props: PivotProps) => {
[listFormCode, translate],
)
// StateStoring fonksiyonlarını ref'e kaydet
const customSaveState = useCallback(
(state: any) => {
return postListFormCustomization({
listFormCode: listFormCode,
customizationType: ListFormCustomizationTypeEnum.GridState,
filterName: `pivot-${storageKey}`,
customizationData: JSON.stringify(state),
}).then(() => {
setGridPanelColor(statedGridPanelColor)
})
},
[listFormCode, storageKey],
)
const customLoadState = useCallback(() => {
return getListFormCustomization(
listFormCode,
ListFormCustomizationTypeEnum.GridState,
`pivot-${storageKey}`,
).then((response: any) => {
if (response.data?.length > 0) {
setGridPanelColor(statedGridPanelColor)
return JSON.parse(response.data[0].customizationData)
}
return null
})
}, [listFormCode, storageKey])
const savePivotGridState = useCallback(() => {
const instance = gridRef.current?.instance()
if (!instance) {

View file

@ -6,14 +6,9 @@ import {
DbTypeEnum,
EditingFormItemDto,
GridDto,
ListFormCustomizationTypeEnum,
PlatformEditorTypes,
SubFormTabTypeEnum,
} from '@/proxy/form/models'
import {
getListFormCustomization,
postListFormCustomization,
} from '@/services/list-form-customization.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { captionize } from 'devextreme/core/utils/inflector'
import { Template } from 'devextreme-react/core/template'
@ -49,8 +44,8 @@ import {
autoNumber,
controlStyleCondition,
GridExtraFilterState,
safeJsonParse,
setFormEditingExtraItemValues,
setGridPanelColor,
} from './Utils'
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
import { ImageViewerEditorComponent } from './editors/ImageViewerEditorComponent'
@ -70,6 +65,7 @@ import SubForms from '../form/SubForms'
import { ImportDashboard } from '@/components/importManager/ImportDashboard'
import { workflowService } from '@/services/workflow.service'
import { NotePanel } from '../form/notes/NotePanel'
import { useListFormStateStoring } from './useListFormStateStoring'
interface TreeProps {
listFormCode: string
@ -80,8 +76,6 @@ interface TreeProps {
gridDto?: GridDto
}
const statedGridPanelColor = 'rgba(50, 200, 200, 0.5)'
const isTemporaryDxKey = (key: unknown) => typeof key === 'string' && key.startsWith('_DX_KEY_')
const getPersistedInsertedKey = (e: any, keyFieldName?: string) => {
@ -312,62 +306,12 @@ const Tree = (props: TreeProps) => {
}
}, [searchParams])
// StateStoring için storageKey'i memoize et
const storageKey = useMemo(() => {
return gridDto?.gridOptions.stateStoringDto?.storageKey ?? ''
}, [gridDto?.gridOptions.stateStoringDto?.storageKey])
const customSaveState = useCallback(
(state: any) => {
if (isEditingRef.current) {
return Promise.resolve()
}
return postListFormCustomization({
listFormCode: listFormCode,
customizationType: ListFormCustomizationTypeEnum.GridState,
filterName: `tree-${storageKey}`,
customizationData: JSON.stringify(state),
})
.then(() => {
setGridPanelColor(statedGridPanelColor)
toast.push(
<Notification type="success" duration={2000}>
{translate('::ListForms.ListForm.GridStateSaved')}
</Notification>,
{
placement: 'bottom-end',
},
)
})
.catch((err) => {
toast.push(
<Notification type="danger" duration={2500}>
{translate('::ListForms.ListForm.GridStateSaveError')}
</Notification>,
{
placement: 'bottom-end',
},
)
throw err
})
},
[listFormCode, storageKey, translate],
)
const customLoadState = useCallback(() => {
return getListFormCustomization(
listFormCode,
ListFormCustomizationTypeEnum.GridState,
`tree-${storageKey}`,
).then((response: any) => {
if (response.data?.length > 0) {
setGridPanelColor(statedGridPanelColor)
return JSON.parse(response.data[0].customizationData)
}
// Veri yoksa null dön (DevExtreme bunu default state olarak algılar)
return null
})
}, [listFormCode, storageKey])
const { customSaveState, customLoadState } = useListFormStateStoring({
listFormCode,
storageKey: gridDto?.gridOptions.stateStoringDto?.storageKey,
filterPrefix: 'tree',
skipSaveRef: isEditingRef,
})
const { filterToolbarData, ...filterData } = useFilters({
gridDto,
@ -593,7 +537,7 @@ const Tree = (props: TreeProps) => {
typeof colFormat.defaultValue === 'string' &&
colFormat.defaultValue === '@AUTONUMBER'
) {
e.data[colFormat.fieldName] = autoNumber
e.data[colFormat.fieldName] = autoNumber()
} else {
e.data[colFormat.fieldName] = colFormat.defaultValue
}
@ -631,8 +575,12 @@ const Tree = (props: TreeProps) => {
case 'object':
try {
e.data[colFormat.fieldName] = JSON.parse(val)
}
catch {}
} catch (err) {
console.error(
`Error occurred while parsing JSON for field "${colFormat.fieldName}":`,
err,
)
}
break
default:
e.data[colFormat.fieldName] = val
@ -983,7 +931,7 @@ const Tree = (props: TreeProps) => {
let base: any = null
if (defaultSearchParams.current) {
base = JSON.parse(defaultSearchParams.current)
base = safeJsonParse(defaultSearchParams.current, null, 'Search filter parse error:')
}
const baseTriplets = extractSearchParamsFields(base)
@ -1063,7 +1011,7 @@ const Tree = (props: TreeProps) => {
console.error('State load error:', err)
})
}
}, [gridDto, treeListDataSource, columnData])
}, [gridDto, treeListDataSource, columnData, customLoadState])
// StateStoring'i devre dışı bırak - manuel kaydetme kullanılacak
useEffect(() => {
@ -1479,7 +1427,9 @@ const Tree = (props: TreeProps) => {
}
}
}
} catch {}
} catch (err){
console.error('EditorOptions parse error:', i.dataField, err)
}
const fieldName = i.dataField.split(':')[0]
const listFormField = gridDto.columnFormats.find(

View file

@ -22,6 +22,22 @@ export function extractSearchParamsFields(filter: any): [string, string, any][]
return filter.flatMap((f) => extractSearchParamsFields(f))
}
export function safeJsonParse<T>(value: unknown, fallback: T, errorMessage?: string): T {
if (typeof value !== 'string' || value.trim() === '') {
return fallback
}
try {
return JSON.parse(value) as T
} catch (error) {
if (errorMessage) {
console.error(errorMessage, error)
}
return fallback
}
}
// sayfaya dinamik olarak css style ekler
export function addCss(css: string) {
if (css.startsWith('http') || css.startsWith('/')) {
@ -53,42 +69,47 @@ export function controlStyleCondition(data: any, fieldName: any, style: any) {
// condition sadece data satırlarında yapılır
if (style.rowType != 'data') return true
const value = data?.[fieldName]
const conditionValue = style.conditionValue
const stringValue = value == null ? '' : String(value)
const stringConditionValue = conditionValue == null ? '' : String(conditionValue)
const getConditionValues = () =>
safeJsonParse<any[]>(conditionValue, [], 'Style condition parse error:')
switch (style.condition) {
case '=':
return style.conditionValue == data[fieldName]
return conditionValue == value
case '<>':
return style.conditionValue != data[fieldName]
return conditionValue != value
case '<':
return style.conditionValue > data[fieldName]
return conditionValue > value
case '<=':
return style.conditionValue >= data[fieldName]
return conditionValue >= value
case '>':
return style.conditionValue < data[fieldName]
return conditionValue < value
case '>=':
return style.conditionValue <= data[fieldName]
return conditionValue <= value
case 'contains':
return data[fieldName].includes(style.conditionValue)
return stringValue.includes(stringConditionValue)
case 'endswith':
return data[fieldName].endsWith(style.conditionValue)
return stringValue.endsWith(stringConditionValue)
case 'isblank':
return data[fieldName] == null
return value == null || value === ''
case 'isnotblank':
return data[fieldName] != null
return value != null && value !== ''
case 'notcontains':
return data[fieldName].indexOf(style.conditionValue) === -1
return !stringValue.includes(stringConditionValue)
case 'startswith':
return data[fieldName].startsWith(style.conditionValue)
return stringValue.startsWith(stringConditionValue)
case 'between':
return false // TODO:
case 'anyof': // anyof icin style.conditionValue obje olmalıdır: ['Val1','Val2']
return JSON.parse(style.conditionValue).some(data[fieldName])
return getConditionValues().some((el) => el == value)
case 'noneof': // noneof icin style.conditionValue obje olmalıdır: ['Val1','Val2']
return JSON.parse(style.conditionValue).findIndex((el: any) => el == data[fieldName]) < 0
return getConditionValues().every((el) => el != value)
default:
return false
}
return false
}
export function pivotFieldConvertDataType(fieldDbType?: DataType): PivotGridDataType {

View file

@ -1,8 +1,5 @@
import Button from 'devextreme-react/button'
import DataGrid from 'devextreme-react/data-grid'
import DropDownBox from 'devextreme-react/drop-down-box'
import dxDataGrid from 'devextreme/ui/data_grid'
import { ReactElement } from 'react'
import { SharedGridBoxEditor } from '@/views/shared/SharedGridBoxEditor'
const GridBoxEditorComponent = (cellElement: any): ReactElement => {
const col = cellElement.column
@ -10,9 +7,6 @@ const GridBoxEditorComponent = (cellElement: any): ReactElement => {
return <></>
}
const options = col.extras.gridBoxOptions
let gridRef: dxDataGrid<any, any>
let val = Array.isArray(cellElement.value) ? cellElement.value : [cellElement.value]
const lookup = col?.lookup
const dataSource: any =
typeof lookup?.dataSource === 'function'
@ -20,97 +14,16 @@ const GridBoxEditorComponent = (cellElement: any): ReactElement => {
: (lookup?.dataSource ?? [])
return (
<DropDownBox
{...col.extras.editorOptions}
dropDownOptions={{ width: options.width, hideOnOutsideClick: true }}
<SharedGridBoxEditor
value={cellElement.value}
dataSource={dataSource}
valueExpr={lookup?.valueExpr}
displayExpr={lookup?.displayExpr}
showClearButton={options?.showClearButton}
acceptCustomValue={options?.acceptCustomValue}
onValueChanged={(e) => {
if (e.value === null) {
val = null
cellElement.setValue(val)
e.component.close()
if (gridRef) {
gridRef.deselectAll()
}
}
}}
onContentReady={(e) => {
e.component.option('value', val)
}}
contentRender={(e) => (
<>
<DataGrid
dataSource={dataSource}
filterRow={{
visible: options?.filterRowVisible,
applyFilter: 'auto',
}}
columns={options?.columns.map((c: string) => c.toLowerCase())} //AliBaba -> alibaba
hoverStateEnabled={true}
height={options?.height}
selection={{
mode: options?.selectionMode,
}}
onContentReady={(event) => {
gridRef = event.component as dxDataGrid<any, any>
}}
onRowClick={(event) => {
if (options?.selectionMode === 'single') {
e.component.option('value', event.data.key)
cellElement.setValue(event.data.key)
e.component.close()
}
}}
onEditorPrepared={(event) => {
const getListValues = async () => {
const data = await dataSource?.load()
const listValues = data.map((a: any) => ({
key: a.key,
name: a.name,
}))
event.component.option(
'selectedRowKeys',
listValues.filter((a: any) => val.includes(a.key)),
)
}
getListValues()
}}
></DataGrid>
{options?.selectionMode === 'multiple' && (
<div className="text-center">
<Button
text="OK"
className="mr-2 min-w-[100px]"
onClick={() => {
val = gridRef?.getSelectedRowKeys().map((a: any) => a.key)
e.component.option('value', val)
cellElement.setValue(val)
e.component.close()
}}
></Button>
<Button
text="Cancel"
className="min-w-[100px]"
onClick={() => {
e.component.close()
}}
></Button>
</div>
)}
</>
)}
></DropDownBox>
lookup={lookup}
options={options}
editorOptions={col.extras.editorOptions}
dropDownOptions={{ width: options?.width, hideOnOutsideClick: true }}
onCommit={(nextValue) => cellElement.setValue(nextValue)}
setInitialValueOnContentReady
/>
)
}

View file

@ -3,7 +3,7 @@ import { PivotGridRef } from 'devextreme-react/pivot-grid'
import CustomStore from 'devextreme/data/custom_store'
import { MutableRefObject, useCallback } from 'react'
import { layoutTypes, ListViewLayoutType } from '@/views/admin/listForm/edit/types'
import { getLoadOptions, getServiceAddress, setGridPanelColor } from './Utils'
import { getLoadOptions, getServiceAddress, safeJsonParse, setGridPanelColor } from './Utils'
import { GridOptionsDto } from '@/proxy/form/models'
import { GridColumnData } from './GridColumnData'
import { dynamicFetch } from '@/services/form.service'
@ -72,11 +72,7 @@ const useListFormCustomDataSource = ({
// URL'den sort parametresini al ve loadOptions'a ekle
const urlSort = searchParams?.get('sort')
if (urlSort && !loadOptions.sort) {
try {
loadOptions.sort = JSON.parse(urlSort)
} catch (e) {
console.error('Sort parse error:', e)
}
loadOptions.sort = safeJsonParse(urlSort, undefined, 'Sort parse error:')
}
const parameters = getLoadOptions(loadOptions, {
@ -125,9 +121,11 @@ const useListFormCustomDataSource = ({
Object.assign(parameters, chartParameters)
}
// 1. Default filter'ı al
const defaultFilter = searchParams?.get('filter')
? JSON.parse(searchParams.get('filter')!)
: null
const defaultFilter = safeJsonParse(
searchParams?.get('filter'),
null,
'Search filter parse error:',
)
let combinedFilter: any = parameters.filter
@ -312,9 +310,11 @@ const useListFormCustomDataSource = ({
})
// 1. Default filter'ı al
const defaultFilter = searchParams?.get('filter')
? JSON.parse(searchParams.get('filter')!)
: null
const defaultFilter = safeJsonParse(
searchParams?.get('filter'),
null,
'Search filter parse error:',
)
let combinedFilter: any = parameters.filter

View file

@ -0,0 +1,188 @@
import { Notification, toast } from '@/components/ui'
import { ListFormCustomizationTypeEnum } from '@/proxy/form/models'
import {
getListFormCustomization,
postListFormCustomization,
} from '@/services/list-form-customization.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { MutableRefObject, useCallback, useEffect, useMemo, useRef } from 'react'
import { setGridPanelColor } from './Utils'
const statedGridPanelColor = 'rgba(50, 200, 200, 0.5)'
const defaultSaveDebounceMs = 500
type PendingSave = {
promise: Promise<void>
resolve: () => void
reject: (reason?: unknown) => void
}
type UseListFormStateStoringParams = {
listFormCode: string
storageKey?: string | null
filterPrefix: 'list' | 'tree' | 'pivot'
skipSaveRef?: MutableRefObject<boolean>
showSaveToast?: boolean
saveDebounceMs?: number
}
export const useListFormStateStoring = ({
listFormCode,
storageKey,
filterPrefix,
skipSaveRef,
showSaveToast = true,
saveDebounceMs = defaultSaveDebounceMs,
}: UseListFormStateStoringParams) => {
const { translate } = useLocalization()
const normalizedStorageKey = useMemo(() => storageKey?.trim() ?? '', [storageKey])
const filterName = useMemo(
() => (normalizedStorageKey ? `${filterPrefix}-${normalizedStorageKey}` : ''),
[filterPrefix, normalizedStorageKey],
)
const saveTimerRef = useRef<ReturnType<typeof setTimeout>>()
const pendingStateRef = useRef<any>()
const pendingSaveRef = useRef<PendingSave>()
useEffect(() => {
return () => {
if (saveTimerRef.current) {
clearTimeout(saveTimerRef.current)
}
pendingSaveRef.current?.resolve()
saveTimerRef.current = undefined
pendingSaveRef.current = undefined
pendingStateRef.current = undefined
}
}, [filterName, listFormCode])
const showSaveSuccessToast = useCallback(() => {
if (!showSaveToast) {
return
}
toast.push(
<Notification type="success" duration={2000}>
{translate('::ListForms.ListForm.GridStateSaved')}
</Notification>,
{
placement: 'bottom-end',
},
)
}, [showSaveToast, translate])
const showSaveErrorToast = useCallback(() => {
if (!showSaveToast) {
return
}
toast.push(
<Notification type="danger" duration={2500}>
{translate('::ListForms.ListForm.GridStateSaveError')}
</Notification>,
{
placement: 'bottom-end',
},
)
}, [showSaveToast, translate])
const customSaveState = useCallback(
(state: any) => {
if (skipSaveRef?.current || !listFormCode || !filterName) {
return Promise.resolve()
}
pendingStateRef.current = state
if (!pendingSaveRef.current) {
let resolveSave!: () => void
let rejectSave!: (reason?: unknown) => void
const promise = new Promise<void>((resolve, reject) => {
resolveSave = resolve
rejectSave = reject
})
pendingSaveRef.current = {
promise,
resolve: resolveSave,
reject: rejectSave,
}
}
if (saveTimerRef.current) {
clearTimeout(saveTimerRef.current)
}
saveTimerRef.current = setTimeout(() => {
const pendingSave = pendingSaveRef.current
const pendingState = pendingStateRef.current
saveTimerRef.current = undefined
pendingSaveRef.current = undefined
pendingStateRef.current = undefined
let customizationData = ''
try {
customizationData = JSON.stringify(pendingState)
} catch (err) {
showSaveErrorToast()
pendingSave?.reject(err)
return
}
postListFormCustomization({
listFormCode,
customizationType: ListFormCustomizationTypeEnum.GridState,
filterName,
customizationData,
})
.then(() => {
setGridPanelColor(statedGridPanelColor)
showSaveSuccessToast()
pendingSave?.resolve()
})
.catch((err) => {
showSaveErrorToast()
pendingSave?.reject(err)
})
}, saveDebounceMs)
return pendingSaveRef.current.promise
},
[filterName, listFormCode, saveDebounceMs, showSaveErrorToast, showSaveSuccessToast, skipSaveRef],
)
const customLoadState = useCallback(() => {
if (!listFormCode || !filterName) {
return Promise.resolve(null)
}
return getListFormCustomization(
listFormCode,
ListFormCustomizationTypeEnum.GridState,
filterName,
).then((response: any) => {
const customizationData = response.data?.[0]?.customizationData
if (!customizationData) {
return null
}
try {
const state = JSON.parse(customizationData)
setGridPanelColor(statedGridPanelColor)
return state
} catch (err) {
console.error('Grid state parse error:', err)
return null
}
})
}, [filterName, listFormCode])
return {
customSaveState,
customLoadState,
storageKey: normalizedStorageKey,
}
}

View file

@ -0,0 +1,160 @@
import Button from 'devextreme-react/button'
import DataGrid, { DataGridRef } from 'devextreme-react/data-grid'
import DropDownBox from 'devextreme-react/drop-down-box'
import { ReactElement, useRef } from 'react'
import { GridBoxOptionsDto } from '@/proxy/form/models'
type GridBoxLookup = {
valueExpr?: string
displayExpr?: string
}
type SharedGridBoxEditorProps = {
value: any
dataSource: any
lookup?: GridBoxLookup
options?: GridBoxOptionsDto
editorOptions?: any
dropDownOptions?: any
onValueChanged?: (value: any) => void
onCommit?: (value: any) => void
setInitialValueOnContentReady?: boolean
}
const normalizeValue = (value: any) => (Array.isArray(value) ? value : [value])
const includesValue = (value: any, itemKey: any) =>
Array.isArray(value) ? value.includes(itemKey) : value === itemKey
const loadDataSourceItems = async (dataSource: any) => {
if (typeof dataSource?.load === 'function') {
return await dataSource.load()
}
if (Array.isArray(dataSource)) {
return dataSource
}
return []
}
const SharedGridBoxEditor = ({
value,
dataSource,
lookup,
options,
editorOptions,
dropDownOptions,
onValueChanged,
onCommit,
setInitialValueOnContentReady,
}: SharedGridBoxEditorProps): ReactElement => {
const gridRef = useRef<DataGridRef>(null)
const currentValueRef = useRef<any>(normalizeValue(value))
currentValueRef.current = normalizeValue(value)
const commitValue = (nextValue: any) => {
currentValueRef.current = nextValue
onCommit?.(nextValue)
}
return (
<DropDownBox
{...editorOptions}
value={currentValueRef.current}
dataSource={dataSource}
valueExpr={lookup?.valueExpr}
displayExpr={lookup?.displayExpr}
dropDownOptions={dropDownOptions}
showClearButton={options?.showClearButton}
acceptCustomValue={options?.acceptCustomValue}
onContentReady={(e) => {
if (setInitialValueOnContentReady) {
e.component.option('value', currentValueRef.current)
}
editorOptions?.onContentReady?.(e)
}}
onValueChanged={(e) => {
const nextValue = typeof e.value === 'string' ? e.value.trim() : e.value
currentValueRef.current = nextValue
onValueChanged?.(nextValue)
if (e.value === null) {
commitValue(null)
e.component.close()
gridRef.current?.instance().deselectAll()
}
}}
contentRender={(dropDown) => (
<>
<DataGrid
ref={gridRef}
dataSource={dataSource}
key={lookup?.valueExpr}
filterRow={{
visible: options?.filterRowVisible,
applyFilter: 'auto',
}}
columns={options?.columns?.map((column: string) => column.toLowerCase())}
hoverStateEnabled={true}
height={options?.height}
selection={{
mode: options?.selectionMode,
}}
onRowClick={(event) => {
if (options?.selectionMode === 'single') {
const nextValue = event.data.key
dropDown.component.option('value', nextValue)
commitValue(nextValue)
dropDown.component.close()
}
}}
onEditorPrepared={(event) => {
const selectCurrentRows = async () => {
const data = await loadDataSourceItems(dataSource)
const selectedRows = data
.map((item: any) => ({
key: item.key,
name: item.name,
}))
.filter((item: any) => includesValue(currentValueRef.current, item.key))
event.component.option('selectedRowKeys', selectedRows)
}
selectCurrentRows()
}}
/>
{options?.selectionMode === 'multiple' && (
<div className="text-center">
<Button
text="OK"
className="mr-2 min-w-[100px]"
onClick={() => {
const selectedValue = gridRef.current
?.instance()
.getSelectedRowKeys()
.map((item: any) => item.key)
dropDown.component.option('value', selectedValue)
commitValue(selectedValue)
dropDown.component.close()
}}
/>
<Button
text="Cancel"
className="min-w-[100px]"
onClick={() => {
dropDown.component.close()
}}
/>
</div>
)}
</>
)}
/>
)
}
export { SharedGridBoxEditor }