Grid üzerinde Widgets çalışma şekli değiştirildi.

This commit is contained in:
Sedat ÖZTÜRK 2025-09-22 12:11:15 +03:00
parent acfe8030dd
commit 5dd6e0bcd4
12 changed files with 194 additions and 125 deletions

View file

@ -8,8 +8,10 @@ public class GridDto
{
ColumnFormats = new List<ColumnFormatDto>();
GridOptions = new GridOptionsDto();
Widgets = new List<WidgetDto>();
}
public List<ColumnFormatDto> ColumnFormats { get; set; }
public GridOptionsDto GridOptions { get; set; }
public List<WidgetDto> Widgets { get; set; }
}

View file

@ -22,6 +22,4 @@ public class SelectDto
/// <summary> Sonuc ile ilgili bazi bilgileride gonderir. Ornegin: filtre uygulandı mı? Sunucu filtresi uygulandı mı?
/// </summary>
public QueryInfoDto QueryInfos { get; set; }
public List<WidgetDto> Widgets { get; set; }
}

View file

@ -214,8 +214,12 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
IsAppliedServerFilter = selectQueryManager.IsAppliedServerFilter,
};
//Widgets verileri gösterilecek ise
result.Widgets = [];
return result;
}
private async Task<List<WidgetDto>> GetWidgetsAsync(ListForm listForm)
{
var Widgets = new List<WidgetDto>();
if (!listForm.WidgetsJson.IsNullOrWhiteSpace())
{
@ -224,6 +228,9 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
{
if (!string.IsNullOrWhiteSpace(widget.SqlQuery))
{
var (dynamicDataRepository, connectionString, dataSourceType) = await dynamicDataManager.GetAsync(listForm.IsTenant, listForm.DataSourceCode);
var w = new WidgetDto
{
ColGap = widget.ColGap,
@ -253,12 +260,12 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
}
result.Widgets.Add(w);
Widgets.Add(w);
}
}
}
return result;
return Widgets;
}
public async Task<GridDto> GetGridAsync(string ListFormCode)
@ -287,7 +294,8 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
var result = new GridDto
{
GridOptions = ObjectMapper.Map<ListForm, GridOptionsDto>(listForm),
ColumnFormats = ObjectMapper.Map<List<ListFormField>, List<ColumnFormatDto>>(fields)
ColumnFormats = ObjectMapper.Map<List<ListFormField>, List<ColumnFormatDto>>(fields),
Widgets = await GetWidgetsAsync(listForm)
};
var queryParameters = httpContextAccessor.HttpContext.Request.Query.ToDictionary(x => x.Key, x => x.Value);

View file

@ -12,5 +12,5 @@ RUN npm i && \
npm install @rollup/rollup-linux-x64-musl --save-dev && \
npm rebuild rollup
COPY . .
RUN npm run build -- --mode $ENV
RUN npm run build && node scripts/write-version.js -- --mode $ENV
CMD ["npm", "run", "preview"]

View file

@ -1,6 +1,20 @@
{
"commit": "a913220",
"commit": "acfe803",
"releases": [
{
"version": "1.0.10",
"buildDate": "2025-09-22",
"changeLog": [
"EditorOptions içerisine DataSource özelliği eklendi"
]
},
{
"version": "1.0.9",
"buildDate": "2025-09-21",
"changeLog": [
"Form komponentinin SelectBox -> lookup bilgisi varsa verileri dolduruyor"
]
},
{
"version": "1.0.8",
"buildDate": "2025-09-21",

View file

@ -31,6 +31,7 @@ export interface SelectListItem {
export interface GridDto {
columnFormats: ColumnFormatDto[]
gridOptions: GridOptionsDto
widgets: WidgetGroupDto[]
}
// 3 enums

View file

@ -23,7 +23,6 @@ const useListFormCustomDataSource = ({
listFormCode: string,
searchParams?: URLSearchParams,
cols?: GridColumnData[],
// setWidgetGroups?: (widgetGroups: WidgetGroupDto[]) => void,
) => {
const store: any = new CustomStore({
key: gridOptions.keyFieldName,
@ -139,8 +138,6 @@ const useListFormCustomDataSource = ({
: 'transparent',
)
//Widgets bilgisini store'un custom propertysinde tutuyoruz ki,
store._widgets = response.data.widgets ?? []
const retValue = {
data: response.data.data,
totalCount: response.data.totalCount,

View file

@ -67,8 +67,6 @@ const FormButtons = (props: {
getSelectedRowsData,
refreshData,
getFilter,
extraFilters,
setExtraFilters,
})
const handleDelete = async (e: any) => {

View file

@ -49,7 +49,6 @@ const FormDevExpress = (props: {
caption={formGroupItem.caption}
>
{(formGroupItem.items as SimpleItemWithColData[])?.map((formItem, i) => {
console.log('formItem', formItem.colData)
return formItem.editorType2 === PlatformEditorTypes.dxTagBox ? (
<SimpleItemDx
key={'formItem-' + i}

View file

@ -2,10 +2,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { EditingFormItemDto, GridDto, PlatformEditorTypes } from '@/proxy/form/models'
import { captionize } from 'devextreme/core/utils/inflector'
import { useListFormCustomDataSource } from '@/shared/useListFormCustomDataSource'
import { Pagination, Select } from '@/components/ui'
import { Button, Pagination, Select } from '@/components/ui'
import classNames from 'classnames'
import { getList } from '@/services/form.service'
import { FaInbox } from 'react-icons/fa'
import { FaInbox, FaSearch } from 'react-icons/fa'
import FormDevExpress from '../form/FormDevExpress'
import { GroupItem } from 'devextreme/ui/form'
import { Form as FormDx } from 'devextreme-react/form'
@ -17,6 +17,9 @@ import { PermissionResults, SimpleItemWithColData } from '../form/types'
import { usePermission } from '@/utils/hooks/usePermission'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { useLookupDataSource } from '../form/useLookupDataSource'
import { Container, Loading } from '@/components/shared'
import WidgetGroup from '@/components/common/WidgetGroup'
import { GridExtraFilterState } from './Utils'
interface CardProps {
listFormCode: string
@ -35,7 +38,7 @@ const CardItem = ({
listFormCode,
dataSource,
refreshData,
getCachedLookupDataSource, // 🔹 Carddan gelen cache fonksiyonu
getCachedLookupDataSource,
}: {
isSubForm?: boolean
row: any
@ -188,9 +191,10 @@ const Card = ({ listFormCode, searchParams }: CardProps) => {
const [currentPage, setCurrentPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
const [pageSizeOptions, setPageSizeOptions] = useState<Option[]>([])
const [dataSource, setDataSource] = useState<CustomStore>()
const [gridDataSource, setGridDataSource] = useState<CustomStore<any, any>>()
const [extraFilters, setExtraFilters] = useState<GridExtraFilterState[]>([])
const [layoutCount, setLayoutCount] = useState(4)
// ✅ Lookup cache burada (Card seviyesinde, tüm CardItemlar ortak kullanır)
const { getLookupDataSource } = useLookupDataSource({ listFormCode })
const lookupCache = useRef<Map<string, any>>(new Map())
@ -204,7 +208,7 @@ const Card = ({ listFormCode, searchParams }: CardProps) => {
}
return lookupCache.current.get(key)
},
[getLookupDataSource]
[getLookupDataSource],
)
const onPageSizeSelect = ({ value }: Option) => {
@ -222,13 +226,13 @@ const Card = ({ listFormCode, searchParams }: CardProps) => {
useEffect(() => {
if (gridDto) {
const store = createSelectDataSource(gridDto.gridOptions, listFormCode, searchParams)
setDataSource(store)
const dataSource = createSelectDataSource(gridDto.gridOptions, listFormCode, searchParams)
setGridDataSource(dataSource)
}
}, [gridDto, listFormCode, searchParams, createSelectDataSource])
const loadData = useCallback(() => {
if (!dataSource) return
if (!gridDataSource) return
const loadOptions = {
skip: (currentPage - 1) * pageSize,
@ -236,91 +240,139 @@ const Card = ({ listFormCode, searchParams }: CardProps) => {
requireTotalCount: true,
}
dataSource.load(loadOptions).then((res: any) => {
gridDataSource.load(loadOptions).then((res: any) => {
setData(res.data)
setTotalCount(res.totalCount || 0)
})
}, [dataSource, currentPage, pageSize])
}, [gridDataSource, currentPage, pageSize])
useEffect(() => {
if (dataSource) {
if (gridDataSource) {
loadData()
}
}, [dataSource, loadData])
}, [gridDataSource, loadData])
useEffect(() => {
if (!gridDto) return
const pagerOptions = gridDto.gridOptions.pagerOptionDto
const allowedSizes =
pagerOptions?.allowedPageSizes
?.split(',')
.map((s) => Number(s.trim()))
.filter((n) => !isNaN(n) && n > 0) || [10, 20, 50, 100]
const allowedSizes = pagerOptions?.allowedPageSizes
?.split(',')
.map((s) => Number(s.trim()))
.filter((n) => !isNaN(n) && n > 0) || [10, 20, 50, 100]
setPageSizeOptions(allowedSizes.map((size) => ({ value: size, label: `${size} page` })))
}, [gridDto, listFormCode, searchParams])
// Data güncellendiğinde sayfanın en üstüne kaydır
useEffect(() => {
if (data.length > 0) {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
}, [data])
if (!gridDto) return null
return (
<>
{data.length === 0 && (
<div className="flex flex-col items-center justify-center p-10 bg-gray-50 dark:bg-neutral-800/50 rounded-md border-2 border-dashed border-gray-200 dark:border-neutral-700">
<div className="text-center">
<FaInbox className="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" />
<p className="mt-4 text-lg font-semibold text-gray-700 dark:text-gray-300">
Kayıt Bulunamadı
</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
Görüntülenecek herhangi bir veri yok.
</p>
<WidgetGroup widgetGroups={gridDto.widgets || []} />
<Container>
<div className="bg-white border border-solid border-1 dark:bg-neutral-800 dark:border-neutral-700 flex justify-end items-center">
<div className="relative py-1 px-2 flex gap-1">
<FaSearch className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-sm" />
<input
type="text"
placeholder="Search..."
className="p-1 pl-6 pr-2 border border-1 outline-none text-xs text-gray-700 dark:text-gray-200 placeholder-gray-400 rounded"
/>
<Button
size="xs"
variant={layoutCount === 3 ? 'solid' : 'default'}
className="text-sm"
onClick={() => setLayoutCount(3)}
>
3
</Button>
<Button
size="xs"
variant={layoutCount === 4 ? 'solid' : 'default'}
className="text-sm"
onClick={() => setLayoutCount(4)}
>
4
</Button>
<Button
size="xs"
variant={layoutCount === 5 ? 'solid' : 'default'}
className="text-sm"
onClick={() => setLayoutCount(5)}
>
5
</Button>
</div>
</div>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-4 gap-4">
{dataSource &&
data.map((row, idx) => {
const keyField = gridDto.gridOptions.keyFieldName
const rowId = row[keyField!]
return (
<CardItem
isSubForm={true}
key={rowId || idx}
row={row}
gridDto={gridDto}
listFormCode={listFormCode}
dataSource={dataSource}
refreshData={loadData}
getCachedLookupDataSource={getCachedLookupDataSource} // 🔹 Prop olarak veriliyor
<div className={`grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-${layoutCount} gap-4`}>
{gridDataSource &&
data.map((row, idx) => {
const keyField = gridDto.gridOptions.keyFieldName
const rowId = row[keyField!]
return (
<CardItem
isSubForm={true}
key={rowId || idx}
row={row}
gridDto={gridDto}
listFormCode={listFormCode}
dataSource={gridDataSource}
refreshData={loadData}
getCachedLookupDataSource={getCachedLookupDataSource}
/>
)
})}
</div>
{gridDto.gridOptions.pagerOptionDto?.visible && totalCount > pageSize && (
<div className={classNames('flex items-center justify-between border-t-1 gap-4 mt-4')}>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-600 dark:text-gray-300">
Toplam {totalCount} kayıt
</span>
<Select
size="xs"
menuPlacement="top"
value={pageSizeOptions.find((o) => o.value === pageSize)}
options={pageSizeOptions}
onChange={(selected) => onPageSizeSelect(selected as Option)}
/>
)
})}
</div>
{gridDto.gridOptions.pagerOptionDto?.visible && totalCount > pageSize && (
<div className={classNames('flex items-center justify-between border-t-1 gap-4 mt-4')}>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-600 dark:text-gray-300">
Toplam {totalCount} kayıt
</span>
<Select
size="xs"
menuPlacement="top"
value={pageSizeOptions.find((o) => o.value === pageSize)}
options={pageSizeOptions}
onChange={(selected) => onPageSizeSelect(selected as Option)}
</div>
<Pagination
currentPage={currentPage}
total={totalCount}
pageSize={pageSize}
onChange={onPageChange}
/>
</div>
<Pagination
currentPage={currentPage}
total={totalCount}
pageSize={pageSize}
onChange={onPageChange}
/>
</div>
)}
)}
{data.length === 0 && (
<Loading loading={!data.length} />
// <div className="flex flex-col items-center justify-center p-10 bg-gray-50 dark:bg-neutral-800/50 rounded-md border-2 border-dashed border-gray-200 dark:border-neutral-700">
// <div className="text-center">
// <FaInbox className="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" />
// <p className="mt-4 text-lg font-semibold text-gray-700 dark:text-gray-300">
// Kayıt Bulunamadı
// </p>
// <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
// Görüntülenecek herhangi bir veri yok.
// </p>
// </div>
// </div>
)}
</Container>
</>
)
}

View file

@ -7,7 +7,6 @@ import {
GridDto,
ListFormCustomizationTypeEnum,
PlatformEditorTypes,
WidgetGroupDto,
} from '@/proxy/form/models'
import { getList } from '@/services/form.service'
import {
@ -93,7 +92,6 @@ const Grid = (props: GridProps) => {
const [columnData, setColumnData] = useState<GridColumnData[]>()
const [formData, setFormData] = useState<any>()
const [mode, setMode] = useState<RowMode>('view')
const [widgetGroups, setWidgetGroups] = useState<WidgetGroupDto[]>([])
const [extraFilters, setExtraFilters] = useState<GridExtraFilterState[]>([])
const preloadExportLibs = () => {
@ -120,8 +118,6 @@ const Grid = (props: GridProps) => {
getSelectedRowsData,
refreshData,
getFilter,
extraFilters,
setExtraFilters,
})
const { filterToolbarData, ...filterData } = useFilters({
@ -403,10 +399,6 @@ const Grid = (props: GridProps) => {
gridRef.current.instance.option('remoteOperations', false)
gridRef.current.instance.option('dataSource', undefined)
gridRef.current.instance.state(null)
//bir önceki gridin filtreleri kalmasın
setExtraFilters([])
setWidgetGroups([])
}
const initializeGrid = async () => {
@ -416,6 +408,10 @@ const Grid = (props: GridProps) => {
if (refListFormCode.current !== listFormCode) {
initializeGrid()
//bir önceki gridin filtreleri kalmasın
setExtraFilters([])
defaultSearchParamsFilter.current = null
}
}, [listFormCode])
@ -438,6 +434,7 @@ const Grid = (props: GridProps) => {
}
if (gridDto?.gridOptions.extraFilterDto) {
console.log('extraFilterDto:', gridDto.gridOptions.extraFilterDto)
setExtraFilters(
gridDto.gridOptions.extraFilterDto.map((f) => ({
fieldName: f.fieldName,
@ -466,35 +463,51 @@ const Grid = (props: GridProps) => {
useEffect(() => {
const activeFilters = extraFilters.filter((f) => f.value)
let filter: any = null
let base: any = null
if (defaultSearchParamsFilter.current) {
base = JSON.parse(defaultSearchParamsFilter.current)
}
if (activeFilters.length === 1) {
filter = [activeFilters[0].fieldName, activeFilters[0].operator, activeFilters[0].value]
} else if (activeFilters.length > 1) {
filter = activeFilters.reduce((acc, f, idx) => {
if (idx === 0) return [f.fieldName, f.operator, f.value]
return [acc, 'and', [f.fieldName, f.operator, f.value]]
// Default filter tripletlerini çıkar
const baseTriplets = extractSearchParamsFields(base)
// Extra filter tripletleri hazırla
const extraTriplets = activeFilters.map(
(f) => [f.fieldName, f.operator, f.value] as [string, string, any],
)
// Tripletleri birleştir, aynı field+op varsa sonuncuyu al
const merged = [...baseTriplets, ...extraTriplets].reduce(
(acc, cur) => {
const [field, op] = cur
const idx = acc.findIndex((a) => a[0] === field && a[1] === op)
if (idx >= 0) {
acc[idx] = cur // varsa üzerine yaz
} else {
acc.push(cur)
}
return acc
},
[] as [string, string, any][],
)
// Tek filtre varsa direkt yaz
let filter: any = null
if (merged.length === 1) {
filter = merged[0]
} else if (merged.length > 1) {
filter = merged.reduce((acc, f, idx) => {
if (idx === 0) return f
return [acc, 'and', f]
}, null as any)
}
if (filter) {
// hem defaultFilter hem extraFilter birleştir
if (defaultSearchParamsFilter.current) {
const base = JSON.parse(defaultSearchParamsFilter.current)
filter = [base, 'and', filter]
}
searchParams?.set('filter', JSON.stringify(filter))
} else {
// sadece default filter kalsın
if (defaultSearchParamsFilter.current) {
searchParams?.set('filter', defaultSearchParamsFilter.current)
} else {
searchParams?.delete('filter')
}
searchParams?.delete('filter')
}
//console.log('Filter Durumu:', filter, decodeURIComponent(searchParams?.toString()))
gridRef.current?.instance.refresh()
}, [extraFilters])
@ -594,7 +607,7 @@ const Grid = (props: GridProps) => {
return (
<>
<WidgetGroup widgetGroups={widgetGroups} />
<WidgetGroup widgetGroups={gridDto?.widgets ?? []} />
<Container className={DX_CLASSNAMES}>
{!isSubForm && (
@ -629,15 +642,6 @@ const Grid = (props: GridProps) => {
focusedRowEnabled={gridDto.gridOptions.columnOptionDto?.focusedRowEnabled}
showColumnHeaders={gridDto.gridOptions.columnOptionDto?.showColumnHeaders}
filterSyncEnabled={true}
onContentReady={(e) => {
const ds = e.component.getDataSource()
if (!ds) return
const store = ds.store() as any
if (store._widgets) {
setWidgetGroups(store._widgets)
}
}}
onSelectionChanged={onSelectionChanged}
onInitNewRow={onInitNewRow}
onCellPrepared={onCellPrepared}

View file

@ -24,8 +24,6 @@ const useToolbar = ({
getSelectedRowsData,
refreshData,
getFilter,
extraFilters,
setExtraFilters,
}: {
gridDto?: GridDto
listFormCode: string
@ -33,8 +31,6 @@ const useToolbar = ({
getSelectedRowsData: () => any
refreshData: () => void
getFilter: () => void
extraFilters: GridExtraFilterState[]
setExtraFilters: React.Dispatch<React.SetStateAction<GridExtraFilterState[]>>
}): {
toolbarData: ToolbarItem[]
toolbarModalData: ToolbarModalData | undefined