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

View file

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

View file

@ -73,7 +73,7 @@ public class ListFormQueryPreviewAppService : PlatformAppService
var (_, _, dataSourceType) = await dynamicDataManager.GetAsync(listForm.IsTenant, listForm.DataSourceCode); 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; 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); 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 var selectRequest = new SelectRequestDto
{ {
ListFormCode = input.ListFormCode, ListFormCode = input.ListFormCode,
Filter = filter.ToString(), Filter = JsonSerializer.Serialize(filter),
Skip = 0, Skip = 0,
Take = 1, Take = 1,
RequireTotalCount = false, RequireTotalCount = false,

View file

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

View file

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

View file

@ -104,7 +104,7 @@ public class DynamicFormReport : XtraReport
Skip = 0 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 reportFields = GetReportFields(listForm, fields, selectQueryManager, defaultValueHelper);
var selectQuery = DynamicReportImageHelper.StripReportPaging(DynamicReportImageHelper.ApplyQueryParameters( var selectQuery = DynamicReportImageHelper.StripReportPaging(DynamicReportImageHelper.ApplyQueryParameters(

View file

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

View file

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

View file

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

View file

@ -1,12 +1,9 @@
import { ColumnFormatDto, GridBoxOptionsDto } from '@/proxy/form/models' import { ColumnFormatDto, GridBoxOptionsDto } from '@/proxy/form/models'
import { Button } from 'devextreme-react/button' import { ReactElement } from 'react'
import DataGrid, { DataGridRef } from 'devextreme-react/data-grid' import { SharedGridBoxEditor } from '@/views/shared/SharedGridBoxEditor'
import DropDownBox from 'devextreme-react/drop-down-box'
import { ReactElement, useRef } from 'react'
const GridBoxEditorComponent = ({ const GridBoxEditorComponent = ({
value, value,
values,
options, options,
onValueChanged, onValueChanged,
col, col,
@ -19,99 +16,17 @@ const GridBoxEditorComponent = ({
col?: ColumnFormatDto col?: ColumnFormatDto
editorOptions: any editorOptions: any
}): ReactElement => { }): ReactElement => {
const gridRef = useRef<DataGridRef>(null)
const val = Array.isArray(value) ? value : [value]
const lookupDto = col?.lookupDto const lookupDto = col?.lookupDto
// const dataSource: any =
// typeof lookupDto?.dataSource === 'function'
// ? lookupDto.dataSource(values)
// : (lookupDto?.dataSource ?? [])
return ( return (
<DropDownBox <SharedGridBoxEditor
{...editorOptions} value={value}
value={val}
dataSource={editorOptions?.dataSource} dataSource={editorOptions?.dataSource}
valueExpr={lookupDto?.valueExpr} lookup={lookupDto}
displayExpr={lookupDto?.displayExpr} options={options}
showClearButton={options?.showClearButton} editorOptions={editorOptions}
acceptCustomValue={options?.acceptCustomValue} onValueChanged={onValueChanged}
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>
) )
} }

View file

@ -6,14 +6,9 @@ import {
EditingFormItemDto, EditingFormItemDto,
FieldCustomValueTypeEnum, FieldCustomValueTypeEnum,
GridDto, GridDto,
ListFormCustomizationTypeEnum,
PlatformEditorTypes, PlatformEditorTypes,
SubFormTabTypeEnum, SubFormTabTypeEnum,
} from '@/proxy/form/models' } from '@/proxy/form/models'
import {
getListFormCustomization,
postListFormCustomization,
} from '@/services/list-form-customization.service'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { executeEditorScript } from '@/utils/editorScriptRuntime' import { executeEditorScript } from '@/utils/editorScriptRuntime'
import { Template } from 'devextreme-react/core/template' import { Template } from 'devextreme-react/core/template'
@ -58,8 +53,8 @@ import {
controlStyleCondition, controlStyleCondition,
extractSearchParamsFields, extractSearchParamsFields,
GridExtraFilterState, GridExtraFilterState,
safeJsonParse,
setFormEditingExtraItemValues, setFormEditingExtraItemValues,
setGridPanelColor,
} from './Utils' } from './Utils'
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent' import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent' import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent'
@ -79,6 +74,7 @@ import { Loading } from '@/components/shared'
import { useStoreState } from '@/store' import { useStoreState } from '@/store'
import { workflowService } from '@/services/workflow.service' import { workflowService } from '@/services/workflow.service'
import { NotePanel } from '../form/notes/NotePanel' import { NotePanel } from '../form/notes/NotePanel'
import { useListFormStateStoring } from './useListFormStateStoring'
interface GridProps { interface GridProps {
listFormCode: string listFormCode: string
@ -89,8 +85,6 @@ interface GridProps {
gridDto?: GridDto 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 isTemporaryDxKey = (key: unknown) => typeof key === 'string' && key.startsWith('_DX_KEY_')
const getPersistedInsertedKey = ( const getPersistedInsertedKey = (
@ -322,47 +316,12 @@ const Grid = (props: GridProps) => {
} }
}, [searchParams]) }, [searchParams])
// StateStoring için storageKey'i memoize et const { customSaveState, customLoadState } = useListFormStateStoring({
const storageKey = useMemo(() => { listFormCode,
return gridDto?.gridOptions.stateStoringDto?.storageKey ?? '' storageKey: gridDto?.gridOptions.stateStoringDto?.storageKey,
}, [gridDto?.gridOptions.stateStoringDto?.storageKey]) filterPrefix: 'list',
skipSaveRef: isEditingRef,
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 { filterToolbarData, ...filterData } = useFilters({ const { filterToolbarData, ...filterData } = useFilters({
gridDto, gridDto,
@ -839,7 +798,9 @@ const Grid = (props: GridProps) => {
} else { } else {
childEditor?.option('disabled', shouldDisable) 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], [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(() => { useEffect(() => {
// React state'i sıfırla - eski değerlerin customLoadState'i erken tetiklemesini önle // React state'i sıfırla - eski değerlerin customLoadState'i erken tetiklemesini önle
setGridDataSource(undefined) setGridDataSource(undefined)
@ -1005,7 +951,7 @@ const Grid = (props: GridProps) => {
let base: any = null let base: any = null
if (defaultSearchParams.current) { if (defaultSearchParams.current) {
base = JSON.parse(defaultSearchParams.current) base = safeJsonParse(defaultSearchParams.current, null, 'Search filter parse error:')
} }
// Default filter tripletlerini çıkar // Default filter tripletlerini çıkar
@ -1168,7 +1114,7 @@ const Grid = (props: GridProps) => {
instance.option('remoteOperations', remoteOps) instance.option('remoteOperations', remoteOps)
instance.option('dataSource', gridDataSource) instance.option('dataSource', gridDataSource)
}) })
}, [gridDto, gridDataSource, columnData]) }, [gridDto, gridDataSource, columnData, customLoadState])
// StateStoring'i devre dışı bırak - manuel kaydetme kullanılacak // StateStoring'i devre dışı bırak - manuel kaydetme kullanılacak
useEffect(() => { useEffect(() => {
@ -1201,7 +1147,7 @@ const Grid = (props: GridProps) => {
const parsed = JSON.parse(rawFilter) const parsed = JSON.parse(rawFilter)
const filters = extractSearchParamsFields(parsed) const filters = extractSearchParamsFields(parsed)
const hasFilter = filters.some(([field, op, val]) => field === i.dataField) const hasFilter = filters.some(([field]) => field === i.dataField)
if (hasFilter) { if (hasFilter) {
const existsInExtra = extraFilters.some((f) => f.fieldName === i.dataField && !!f.value) 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 fieldName = i.dataField.split(':')[0]
const listFormField = gridDto?.columnFormats.find((x: any) => x.fieldName === fieldName) const listFormField = gridDto?.columnFormats.find((x: any) => x.fieldName === fieldName)

View file

@ -1,10 +1,7 @@
import Container from '@/components/shared/Container' import Container from '@/components/shared/Container'
import { APP_NAME, DX_CLASSNAMES } from '@/constants/app.constant' import { APP_NAME, DX_CLASSNAMES } from '@/constants/app.constant'
import { GridDto, ListFormCustomizationTypeEnum } from '@/proxy/form/models' import { GridDto, ListFormCustomizationTypeEnum } from '@/proxy/form/models'
import { import { postListFormCustomization } from '@/services/list-form-customization.service'
getListFormCustomization,
postListFormCustomization,
} from '@/services/list-form-customization.service'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import Chart, { ChartRef, CommonSeriesSettings, Size, Tooltip } from 'devextreme-react/chart' import Chart, { ChartRef, CommonSeriesSettings, Size, Tooltip } from 'devextreme-react/chart'
import PivotGrid, { import PivotGrid, {
@ -41,6 +38,7 @@ import { useListFormColumns } from './useListFormColumns'
import { useStoreState } from '@/store' import { useStoreState } from '@/store'
import Checkbox from '@/components/ui/Checkbox' import Checkbox from '@/components/ui/Checkbox'
import Toolbar, { Item } from 'devextreme-react/toolbar' import Toolbar, { Item } from 'devextreme-react/toolbar'
import { useListFormStateStoring } from './useListFormStateStoring'
interface PivotProps { interface PivotProps {
listFormCode: string listFormCode: string
@ -52,8 +50,6 @@ interface PivotProps {
refreshGridDto: () => Promise<void> 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>> type LookupDisplayValues = Record<string, Map<string, string>>
const Pivot = (props: PivotProps) => { const Pivot = (props: PivotProps) => {
@ -72,11 +68,12 @@ const Pivot = (props: PivotProps) => {
const [lookupDisplayValues, setLookupDisplayValues] = useState<LookupDisplayValues>({}) const [lookupDisplayValues, setLookupDisplayValues] = useState<LookupDisplayValues>({})
const [showChart, setShowChart] = useState(false) const [showChart, setShowChart] = useState(false)
// StateStoring için storageKey'i memoize et const { customSaveState, customLoadState, storageKey } = useListFormStateStoring({
const storageKey = useMemo(() => { listFormCode,
return gridDto?.gridOptions.stateStoringDto?.storageKey ?? '' storageKey: gridDto?.gridOptions.stateStoringDto?.storageKey,
}, [gridDto?.gridOptions.stateStoringDto?.storageKey]) filterPrefix: 'pivot',
showSaveToast: false,
})
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef }) const { createSelectDataSource } = useListFormCustomDataSource({ gridRef })
const { getBandedColumns, loadLookupDisplayValues } = useListFormColumns({ const { getBandedColumns, loadLookupDisplayValues } = useListFormColumns({
@ -212,35 +209,6 @@ const Pivot = (props: PivotProps) => {
[listFormCode, translate], [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 savePivotGridState = useCallback(() => {
const instance = gridRef.current?.instance() const instance = gridRef.current?.instance()
if (!instance) { if (!instance) {

View file

@ -6,14 +6,9 @@ import {
DbTypeEnum, DbTypeEnum,
EditingFormItemDto, EditingFormItemDto,
GridDto, GridDto,
ListFormCustomizationTypeEnum,
PlatformEditorTypes, PlatformEditorTypes,
SubFormTabTypeEnum, SubFormTabTypeEnum,
} from '@/proxy/form/models' } from '@/proxy/form/models'
import {
getListFormCustomization,
postListFormCustomization,
} from '@/services/list-form-customization.service'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { captionize } from 'devextreme/core/utils/inflector' import { captionize } from 'devextreme/core/utils/inflector'
import { Template } from 'devextreme-react/core/template' import { Template } from 'devextreme-react/core/template'
@ -49,8 +44,8 @@ import {
autoNumber, autoNumber,
controlStyleCondition, controlStyleCondition,
GridExtraFilterState, GridExtraFilterState,
safeJsonParse,
setFormEditingExtraItemValues, setFormEditingExtraItemValues,
setGridPanelColor,
} from './Utils' } from './Utils'
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent' import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
import { ImageViewerEditorComponent } from './editors/ImageViewerEditorComponent' import { ImageViewerEditorComponent } from './editors/ImageViewerEditorComponent'
@ -70,6 +65,7 @@ import SubForms from '../form/SubForms'
import { ImportDashboard } from '@/components/importManager/ImportDashboard' import { ImportDashboard } from '@/components/importManager/ImportDashboard'
import { workflowService } from '@/services/workflow.service' import { workflowService } from '@/services/workflow.service'
import { NotePanel } from '../form/notes/NotePanel' import { NotePanel } from '../form/notes/NotePanel'
import { useListFormStateStoring } from './useListFormStateStoring'
interface TreeProps { interface TreeProps {
listFormCode: string listFormCode: string
@ -80,8 +76,6 @@ interface TreeProps {
gridDto?: GridDto gridDto?: GridDto
} }
const statedGridPanelColor = 'rgba(50, 200, 200, 0.5)'
const isTemporaryDxKey = (key: unknown) => typeof key === 'string' && key.startsWith('_DX_KEY_') const isTemporaryDxKey = (key: unknown) => typeof key === 'string' && key.startsWith('_DX_KEY_')
const getPersistedInsertedKey = (e: any, keyFieldName?: string) => { const getPersistedInsertedKey = (e: any, keyFieldName?: string) => {
@ -312,62 +306,12 @@ const Tree = (props: TreeProps) => {
} }
}, [searchParams]) }, [searchParams])
// StateStoring için storageKey'i memoize et const { customSaveState, customLoadState } = useListFormStateStoring({
const storageKey = useMemo(() => { listFormCode,
return gridDto?.gridOptions.stateStoringDto?.storageKey ?? '' storageKey: gridDto?.gridOptions.stateStoringDto?.storageKey,
}, [gridDto?.gridOptions.stateStoringDto?.storageKey]) filterPrefix: 'tree',
skipSaveRef: isEditingRef,
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 { filterToolbarData, ...filterData } = useFilters({ const { filterToolbarData, ...filterData } = useFilters({
gridDto, gridDto,
@ -593,7 +537,7 @@ const Tree = (props: TreeProps) => {
typeof colFormat.defaultValue === 'string' && typeof colFormat.defaultValue === 'string' &&
colFormat.defaultValue === '@AUTONUMBER' colFormat.defaultValue === '@AUTONUMBER'
) { ) {
e.data[colFormat.fieldName] = autoNumber e.data[colFormat.fieldName] = autoNumber()
} else { } else {
e.data[colFormat.fieldName] = colFormat.defaultValue e.data[colFormat.fieldName] = colFormat.defaultValue
} }
@ -631,8 +575,12 @@ const Tree = (props: TreeProps) => {
case 'object': case 'object':
try { try {
e.data[colFormat.fieldName] = JSON.parse(val) e.data[colFormat.fieldName] = JSON.parse(val)
} catch (err) {
console.error(
`Error occurred while parsing JSON for field "${colFormat.fieldName}":`,
err,
)
} }
catch {}
break break
default: default:
e.data[colFormat.fieldName] = val e.data[colFormat.fieldName] = val
@ -983,7 +931,7 @@ const Tree = (props: TreeProps) => {
let base: any = null let base: any = null
if (defaultSearchParams.current) { if (defaultSearchParams.current) {
base = JSON.parse(defaultSearchParams.current) base = safeJsonParse(defaultSearchParams.current, null, 'Search filter parse error:')
} }
const baseTriplets = extractSearchParamsFields(base) const baseTriplets = extractSearchParamsFields(base)
@ -1063,7 +1011,7 @@ const Tree = (props: TreeProps) => {
console.error('State load error:', err) console.error('State load error:', err)
}) })
} }
}, [gridDto, treeListDataSource, columnData]) }, [gridDto, treeListDataSource, columnData, customLoadState])
// StateStoring'i devre dışı bırak - manuel kaydetme kullanılacak // StateStoring'i devre dışı bırak - manuel kaydetme kullanılacak
useEffect(() => { 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 fieldName = i.dataField.split(':')[0]
const listFormField = gridDto.columnFormats.find( 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)) 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 // sayfaya dinamik olarak css style ekler
export function addCss(css: string) { export function addCss(css: string) {
if (css.startsWith('http') || css.startsWith('/')) { 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 // condition sadece data satırlarında yapılır
if (style.rowType != 'data') return true 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) { switch (style.condition) {
case '=': case '=':
return style.conditionValue == data[fieldName] return conditionValue == value
case '<>': case '<>':
return style.conditionValue != data[fieldName] return conditionValue != value
case '<': case '<':
return style.conditionValue > data[fieldName] return conditionValue > value
case '<=': case '<=':
return style.conditionValue >= data[fieldName] return conditionValue >= value
case '>': case '>':
return style.conditionValue < data[fieldName] return conditionValue < value
case '>=': case '>=':
return style.conditionValue <= data[fieldName] return conditionValue <= value
case 'contains': case 'contains':
return data[fieldName].includes(style.conditionValue) return stringValue.includes(stringConditionValue)
case 'endswith': case 'endswith':
return data[fieldName].endsWith(style.conditionValue) return stringValue.endsWith(stringConditionValue)
case 'isblank': case 'isblank':
return data[fieldName] == null return value == null || value === ''
case 'isnotblank': case 'isnotblank':
return data[fieldName] != null return value != null && value !== ''
case 'notcontains': case 'notcontains':
return data[fieldName].indexOf(style.conditionValue) === -1 return !stringValue.includes(stringConditionValue)
case 'startswith': case 'startswith':
return data[fieldName].startsWith(style.conditionValue) return stringValue.startsWith(stringConditionValue)
case 'between': case 'between':
return false // TODO: return false // TODO:
case 'anyof': // anyof icin style.conditionValue obje olmalıdır: ['Val1','Val2'] 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'] 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: default:
return false return false
} }
return false
} }
export function pivotFieldConvertDataType(fieldDbType?: DataType): PivotGridDataType { 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 { ReactElement } from 'react'
import { SharedGridBoxEditor } from '@/views/shared/SharedGridBoxEditor'
const GridBoxEditorComponent = (cellElement: any): ReactElement => { const GridBoxEditorComponent = (cellElement: any): ReactElement => {
const col = cellElement.column const col = cellElement.column
@ -10,9 +7,6 @@ const GridBoxEditorComponent = (cellElement: any): ReactElement => {
return <></> return <></>
} }
const options = col.extras.gridBoxOptions 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 lookup = col?.lookup
const dataSource: any = const dataSource: any =
typeof lookup?.dataSource === 'function' typeof lookup?.dataSource === 'function'
@ -20,97 +14,16 @@ const GridBoxEditorComponent = (cellElement: any): ReactElement => {
: (lookup?.dataSource ?? []) : (lookup?.dataSource ?? [])
return ( return (
<DropDownBox <SharedGridBoxEditor
{...col.extras.editorOptions} value={cellElement.value}
dropDownOptions={{ width: options.width, hideOnOutsideClick: true }}
dataSource={dataSource} dataSource={dataSource}
valueExpr={lookup?.valueExpr} lookup={lookup}
displayExpr={lookup?.displayExpr} options={options}
showClearButton={options?.showClearButton} editorOptions={col.extras.editorOptions}
acceptCustomValue={options?.acceptCustomValue} dropDownOptions={{ width: options?.width, hideOnOutsideClick: true }}
onValueChanged={(e) => { onCommit={(nextValue) => cellElement.setValue(nextValue)}
if (e.value === null) { setInitialValueOnContentReady
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>
) )
} }

View file

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