diff --git a/api/modules/Sozsoft.Settings/Sozsoft.Settings.Domain/Settings/SettingsSettingDefinitionProvider.cs b/api/modules/Sozsoft.Settings/Sozsoft.Settings.Domain/Settings/SettingsSettingDefinitionProvider.cs index 0037789..60e5b3e 100644 --- a/api/modules/Sozsoft.Settings/Sozsoft.Settings.Domain/Settings/SettingsSettingDefinitionProvider.cs +++ b/api/modules/Sozsoft.Settings/Sozsoft.Settings.Domain/Settings/SettingsSettingDefinitionProvider.cs @@ -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)) { diff --git a/api/src/Sozsoft.Platform.Application/Forum/ForumAppService.cs b/api/src/Sozsoft.Platform.Application/Forum/ForumAppService.cs index 0264244..88982ce 100644 --- a/api/src/Sozsoft.Platform.Application/Forum/ForumAppService.cs +++ b/api/src/Sozsoft.Platform.Application/Forum/ForumAppService.cs @@ -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 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(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) { diff --git a/api/src/Sozsoft.Platform.Application/ListForms/Administration/ListFormQueryPreviewAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/Administration/ListFormQueryPreviewAppService.cs index 9fd73cd..bc748fb 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/Administration/ListFormQueryPreviewAppService.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/Administration/ListFormQueryPreviewAppService.cs @@ -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; } diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormDataAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormDataAppService.cs index 3f333f1..106a5ff 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormDataAppService.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormDataAppService.cs @@ -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, diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormSelectAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormSelectAppService.cs index 1a619f8..f02b924 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormSelectAppService.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormSelectAppService.cs @@ -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(); diff --git a/api/src/Sozsoft.Platform.Domain/Queries/SelectQueryManager.cs b/api/src/Sozsoft.Platform.Domain/Queries/SelectQueryManager.cs index 684644b..feed8c3 100644 --- a/api/src/Sozsoft.Platform.Domain/Queries/SelectQueryManager.cs +++ b/api/src/Sozsoft.Platform.Domain/Queries/SelectQueryManager.cs @@ -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 SummaryQueries { get; } public bool IsAppliedGridFilter { get; } public bool IsAppliedServerFilter { get; } - void PrepareQueries(ListForm listform, - List listFormFields, - DataSourceTypeEnum dataSourceType, - List listFormCustomizations = null, - QueryParameters queryParams = null); + Task PrepareQueriesAsync(ListForm listform, + List listFormFields, + DataSourceTypeEnum dataSourceType, + List 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 listFormFields, - DataSourceTypeEnum dataSourceType, - List listFormCustomizations = null, - QueryParameters queryParams = null) + public async Task PrepareQueriesAsync(ListForm listform, + List listFormFields, + DataSourceTypeEnum dataSourceType, + List 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 whereFields = GetWhereFields(listform, listFormFields, queryParams); + List whereFields = await GetWhereFieldsAsync(listform, listFormFields, queryParams); WhereParts.AddRange(whereFields); List sortFields = GetSortFields(listform, listFormFields, queryParams?.Sort); @@ -343,7 +344,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager } } - private List GetWhereFields(ListForm listform, List listFormFields, QueryParameters queryParams = null) + private async Task> GetWhereFieldsAsync(ListForm listform, List listFormFields, QueryParameters queryParams = null) { var whereParts = new List(); @@ -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}'"))})"); diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs index d9f814a..57e0cb7 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicFormReport.cs @@ -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( diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs index 753627e..9bece30 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicGridReport.cs @@ -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); diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicTreeReport.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicTreeReport.cs index 2bad38c..07b261e 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicTreeReport.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicTreeReport.cs @@ -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); diff --git a/ui/src/services/api.service.ts b/ui/src/services/api.service.ts index d148794..8b3ec14 100644 --- a/ui/src/services/api.service.ts +++ b/ui/src/services/api.service.ts @@ -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>( param: AxiosRequestConfig, config?: Config, - ) { - return new Promise>((resolve, reject) => { - if (!config || config?.apiName === DEFAULT_API_NAME) { - platformApiService(param) - .then((response: AxiosResponse) => { - resolve(response) - }) - .catch((errors: AxiosError) => { - // console.error(errors) - reject(errors) - }) - } else if (config?.apiName === AUTH_API_NAME) { - authApiService(param) - .then((response: AxiosResponse) => { - resolve(response) - }) - .catch((errors: AxiosError) => { - console.error(errors) - reject(errors) - }) - } else { - reject(new Error('NO API DEFINED')) - } - }) + ): Promise> { + if (!config || config?.apiName === DEFAULT_API_NAME) { + return platformApiService(param) as Promise> + } + + if (config?.apiName === AUTH_API_NAME) { + return authApiService(param).catch((errors) => { + console.error(errors) + throw errors + }) as Promise> + } + + return Promise.reject(new Error('NO API DEFINED')) }, } diff --git a/ui/src/views/form/editors/GridBoxEditorComponent.tsx b/ui/src/views/form/editors/GridBoxEditorComponent.tsx index 11d117b..6742f27 100644 --- a/ui/src/views/form/editors/GridBoxEditorComponent.tsx +++ b/ui/src/views/form/editors/GridBoxEditorComponent.tsx @@ -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(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 ( - { - 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) => ( - <> - 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() - }} - > - {options?.selectionMode === 'multiple' && ( -
- - -
- )} - - )} - >
+ lookup={lookupDto} + options={options} + editorOptions={editorOptions} + onValueChanged={onValueChanged} + /> ) } diff --git a/ui/src/views/list/Grid.tsx b/ui/src/views/list/Grid.tsx index 8764160..2e1ae40 100644 --- a/ui/src/views/list/Grid.tsx +++ b/ui/src/views/list/Grid.tsx @@ -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( - - {translate('::ListForms.ListForm.GridStateSaved')} - , - { - placement: 'bottom-end', - }, - ) - }) - .catch((err) => { - toast.push( - - {translate('::ListForms.ListForm.GridStateSaveError')} - , - { - 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) diff --git a/ui/src/views/list/Pivot.tsx b/ui/src/views/list/Pivot.tsx index c69d502..ca65e8f 100644 --- a/ui/src/views/list/Pivot.tsx +++ b/ui/src/views/list/Pivot.tsx @@ -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 } -const statedGridPanelColor = 'rgba(50, 200, 200, 0.5)' // kullanici tanimli gridState ile islem gormus gridin paneline ait renk - type LookupDisplayValues = Record> const Pivot = (props: PivotProps) => { @@ -72,11 +68,12 @@ const Pivot = (props: PivotProps) => { const [lookupDisplayValues, setLookupDisplayValues] = useState({}) 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) { diff --git a/ui/src/views/list/Tree.tsx b/ui/src/views/list/Tree.tsx index da4652c..eac18e3 100644 --- a/ui/src/views/list/Tree.tsx +++ b/ui/src/views/list/Tree.tsx @@ -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( - - {translate('::ListForms.ListForm.GridStateSaved')} - , - { - placement: 'bottom-end', - }, - ) - }) - .catch((err) => { - toast.push( - - {translate('::ListForms.ListForm.GridStateSaveError')} - , - { - 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( diff --git a/ui/src/views/list/Utils.ts b/ui/src/views/list/Utils.ts index cbb8357..c8cbad2 100644 --- a/ui/src/views/list/Utils.ts +++ b/ui/src/views/list/Utils.ts @@ -22,6 +22,22 @@ export function extractSearchParamsFields(filter: any): [string, string, any][] return filter.flatMap((f) => extractSearchParamsFields(f)) } +export function safeJsonParse(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(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 { diff --git a/ui/src/views/list/editors/GridBoxEditorComponent.tsx b/ui/src/views/list/editors/GridBoxEditorComponent.tsx index bb8e2f0..85f21d9 100644 --- a/ui/src/views/list/editors/GridBoxEditorComponent.tsx +++ b/ui/src/views/list/editors/GridBoxEditorComponent.tsx @@ -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 - - 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 ( - { - 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) => ( - <> - c.toLowerCase())} //AliBaba -> alibaba - hoverStateEnabled={true} - height={options?.height} - selection={{ - mode: options?.selectionMode, - }} - onContentReady={(event) => { - gridRef = event.component as dxDataGrid - }} - 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() - }} - > - {options?.selectionMode === 'multiple' && ( -
- - -
- )} - - )} - >
+ lookup={lookup} + options={options} + editorOptions={col.extras.editorOptions} + dropDownOptions={{ width: options?.width, hideOnOutsideClick: true }} + onCommit={(nextValue) => cellElement.setValue(nextValue)} + setInitialValueOnContentReady + /> ) } diff --git a/ui/src/views/list/useListFormCustomDataSource.ts b/ui/src/views/list/useListFormCustomDataSource.ts index 49c3397..12a6466 100644 --- a/ui/src/views/list/useListFormCustomDataSource.ts +++ b/ui/src/views/list/useListFormCustomDataSource.ts @@ -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 diff --git a/ui/src/views/list/useListFormStateStoring.tsx b/ui/src/views/list/useListFormStateStoring.tsx new file mode 100644 index 0000000..9fee6ea --- /dev/null +++ b/ui/src/views/list/useListFormStateStoring.tsx @@ -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 + resolve: () => void + reject: (reason?: unknown) => void +} + +type UseListFormStateStoringParams = { + listFormCode: string + storageKey?: string | null + filterPrefix: 'list' | 'tree' | 'pivot' + skipSaveRef?: MutableRefObject + 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>() + const pendingStateRef = useRef() + const pendingSaveRef = useRef() + + 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( + + {translate('::ListForms.ListForm.GridStateSaved')} + , + { + placement: 'bottom-end', + }, + ) + }, [showSaveToast, translate]) + + const showSaveErrorToast = useCallback(() => { + if (!showSaveToast) { + return + } + + toast.push( + + {translate('::ListForms.ListForm.GridStateSaveError')} + , + { + 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((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, + } +} diff --git a/ui/src/views/shared/SharedGridBoxEditor.tsx b/ui/src/views/shared/SharedGridBoxEditor.tsx new file mode 100644 index 0000000..e1ba2c8 --- /dev/null +++ b/ui/src/views/shared/SharedGridBoxEditor.tsx @@ -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(null) + const currentValueRef = useRef(normalizeValue(value)) + currentValueRef.current = normalizeValue(value) + + const commitValue = (nextValue: any) => { + currentValueRef.current = nextValue + onCommit?.(nextValue) + } + + return ( + { + 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) => ( + <> + 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' && ( +
+
+ )} + + )} + /> + ) +} + +export { SharedGridBoxEditor }