Grid, Card, Pivot Layout

This commit is contained in:
Sedat ÖZTÜRK 2025-09-22 17:08:42 +03:00
parent 5dd6e0bcd4
commit b2e489d705
19 changed files with 522 additions and 228 deletions

View file

@ -305,4 +305,18 @@ public class GridOptionsDto : AuditedEntityDto<Guid>
}
set { ExtraFilterJson = JsonSerializer.Serialize(value); }
}
[JsonIgnore]
public string LayoutJson { get; set; }
public LayoutDto LayoutDto
{
get
{
if (!string.IsNullOrEmpty(LayoutJson))
return JsonSerializer.Deserialize<LayoutDto>(LayoutJson);
return new LayoutDto();
}
set { LayoutJson = JsonSerializer.Serialize(value); }
}
}

View file

@ -0,0 +1,10 @@
namespace Kurs.Platform.ListForms;
public class LayoutDto
{
public bool Grid { get; set; } = true;
public bool Card { get; set; } = true;
public bool Pivot { get; set; } = true;
public string DefaultLayout { get; set; } = "grid";
public int CardLayoutColumn { get; set; } = 4;
}

View file

@ -65,6 +65,7 @@ public class ListFormsAppService : CrudAppService<
item.Description = input.Description;
item.IsSubForm = input.IsSubForm;
item.ListFormType = input.ListFormType;
item.LayoutJson = JsonSerializer.Serialize(input.LayoutDto);
}
else if (input.EditType == ListFormEditTabs.Database.DataSourceForm)
{

View file

@ -2581,7 +2581,7 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
Visible = true,
IsActive = true,
IsDeleted = false,
AllowSearch = true,
AllowSearch = false,
LookupJson = JsonSerializer.Serialize(new LookupDto {
DataSourceType = UiLookupDataSourceTypeEnum.Query,
@ -3821,7 +3821,7 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
Visible = true,
IsActive = true,
IsDeleted = false,
AllowSearch = true,
AllowSearch = false,
ValidationRuleJson = JsonSerializer.Serialize(new ValidationRuleDto[] {
new ValidationRuleDto() { Type = Enum.GetName(UiColumnValidationRuleTypeEnum.required)}
}),
@ -4097,7 +4097,7 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
Visible = true,
IsActive = true,
IsDeleted = false,
AllowSearch = true,
AllowSearch = false,
LookupJson = JsonSerializer.Serialize(new LookupDto {
DataSourceType = UiLookupDataSourceTypeEnum.Query,
@ -5505,7 +5505,7 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
Visible = true,
IsActive = true,
IsDeleted = false,
AllowSearch = true,
AllowSearch = false,
LookupJson = JsonSerializer.Serialize(new LookupDto {
DataSourceType = UiLookupDataSourceTypeEnum.Query,
@ -5543,7 +5543,7 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
Visible = true,
IsActive = true,
IsDeleted = false,
AllowSearch = true,
AllowSearch = false,
LookupJson = JsonSerializer.Serialize(new LookupDto {
DataSourceType = UiLookupDataSourceTypeEnum.Query,
@ -5611,7 +5611,7 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
Visible = true,
IsActive = true,
IsDeleted = false,
AllowSearch = true,
AllowSearch = false,
LookupJson = JsonSerializer.Serialize(new LookupDto {
DataSourceType = UiLookupDataSourceTypeEnum.Query,
@ -5649,7 +5649,7 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
Visible = true,
IsActive = true,
IsDeleted = false,
AllowSearch = true,
AllowSearch = false,
LookupJson = JsonSerializer.Serialize(new LookupDto {
DataSourceType = UiLookupDataSourceTypeEnum.Query,
@ -12790,7 +12790,7 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
Visible = true,
IsActive = true,
IsDeleted = false,
AllowSearch = true,
AllowSearch = false,
ValidationRuleJson = JsonSerializer.Serialize(new[]
{
new ValidationRuleDto { Type = "required" }
@ -12849,7 +12849,7 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
Visible = true,
IsActive = true,
IsDeleted = false,
AllowSearch = true,
AllowSearch = false,
ValidationRuleJson = JsonSerializer.Serialize(new[]
{
new ValidationRuleDto { Type = "required" }
@ -13124,7 +13124,7 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
Visible = true,
IsActive = true,
IsDeleted = false,
AllowSearch = true,
AllowSearch = false,
ValidationRuleJson = JsonSerializer.Serialize(new[]
{
new ValidationRuleDto { Type = "required" }
@ -13183,7 +13183,7 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
Visible = true,
IsActive = true,
IsDeleted = false,
AllowSearch = true,
AllowSearch = false,
ValidationRuleJson = JsonSerializer.Serialize(new[]
{
new ValidationRuleDto { Type = "required" }
@ -13899,7 +13899,7 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
Visible = true,
IsActive = true,
IsDeleted = false,
AllowSearch = true,
AllowSearch = false,
ValidationRuleJson = JsonSerializer.Serialize(new[]
{
new ValidationRuleDto { Type = "required" }
@ -13932,7 +13932,7 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
Visible = true,
IsActive = true,
IsDeleted = false,
AllowSearch = true,
AllowSearch = false,
PermissionJson = JsonSerializer.Serialize(new ListFormFieldPermissionDto
{
C = AppCodes.Orders.Products + ".Create",
@ -15790,7 +15790,7 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
Visible = true,
IsActive = true,
IsDeleted = false,
AllowSearch = true,
AllowSearch = false,
LookupJson = JsonSerializer.Serialize(new LookupDto {
DataSourceType = UiLookupDataSourceTypeEnum.Query,
DisplayExpr = "Name",
@ -15848,7 +15848,7 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
Visible = true,
IsActive = true,
IsDeleted = false,
AllowSearch = true,
AllowSearch = false,
LookupJson = JsonSerializer.Serialize(new LookupDto {
DataSourceType = UiLookupDataSourceTypeEnum.Query,
DisplayExpr = "Name",
@ -16322,7 +16322,7 @@ public class ListFormsSeeder : IDataSeedContributor, ITransientDependency
Visible = true,
IsActive = true,
IsDeleted = false,
AllowSearch = true,
AllowSearch = false,
LookupJson = JsonSerializer.Serialize(new LookupDto {
DataSourceType = UiLookupDataSourceTypeEnum.Query,
DisplayExpr = "Name",

View file

@ -12510,6 +12510,36 @@
"en": "Height",
"tr": "Yükseklik"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormEdit.DetailsLayoutDto.GridLayout",
"en": "Grid Layout",
"tr": "Liste Düzeni"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormEdit.DetailsLayoutDto.CardLayout",
"en": "Card Layout",
"tr": "Kart Düzeni"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormEdit.DetailsLayoutDto.PivotLayout",
"en": "Pivot Layout",
"tr": "Pivot Düzeni"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormEdit.DetailsLayoutDto.DefaultLayout",
"en": "Default Layout",
"tr": "Varsayılan Düzen"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormEdit.DetailsLayoutDto.CardLayoutColumn",
"en": "Card Layout Column",
"tr": "Kart Düzeni Sütunu"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormEdit.DatabaseDataSource",

View file

@ -121,4 +121,7 @@ public class ListForm : FullAuditedEntity<Guid>
/// <summary>bu listform'un üstünde yer alan widgetların listesidir</summary>
public string ExtraFilterJson { get; set; }
/// <summary>bu listform'un üstünde yer alan widgetların listesidir</summary>
public string LayoutJson { get; set; }
}

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Kurs.Platform.Migrations
{
[DbContext(typeof(PlatformDbContext))]
[Migration("20250917121912_Initial")]
[Migration("20250922114945_Initial")]
partial class Initial
{
/// <inheritdoc />
@ -2869,6 +2869,9 @@ namespace Kurs.Platform.Migrations
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("LayoutJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("ListFormCode")
.IsRequired()
.HasColumnType("nvarchar(450)");

View file

@ -1241,6 +1241,7 @@ namespace Kurs.Platform.Migrations
SubFormsJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
WidgetsJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
ExtraFilterJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
LayoutJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),

View file

@ -2866,6 +2866,9 @@ namespace Kurs.Platform.Migrations
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("LayoutJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("ListFormCode")
.IsRequired()
.HasColumnType("nvarchar(450)");

View file

@ -19,6 +19,7 @@ import {
import { FormItemComponent } from 'devextreme/ui/form'
import { AuditedEntityDto } from '../abp'
import { EditorType2, RowMode } from '../../views/form/types'
import { bool } from 'yup'
//1
export interface SelectListItem {
@ -481,6 +482,7 @@ export interface GridOptionsDto extends AuditedEntityDto<string> {
subFormsDto: SubFormDto[]
extraFilterJson?: string
extraFilterDto: ExtraFilterDto[]
layoutDto: LayoutDto
}
export interface GridOptionsEditDto extends GridOptionsDto, Record<string, any> {
@ -727,6 +729,14 @@ export interface WidgetEditDto {
onClick: string
}
export interface LayoutDto {
grid: boolean
card: boolean
pivot: boolean
defaultLayout: 'grid' | 'card' | 'pivot'
cardLayoutColumn: number
}
export interface ExtraFilterDto {
fieldName: string
caption: string

View file

@ -292,7 +292,7 @@ function ChartEdit() {
<div className="lg:flex items-center justify-between mb-4 gap-3">
<div className="mb-4 lg:mb-0">
<h4>
<Link to={`/chart/${chartCode}`}>🔙 {chartCode}</Link>
<Link to={`/admin/chart/${chartCode}`}>🔙 {chartCode}</Link>
</h4>
{chartValues.isTenant && (
<Alert showIcon className="mb-4" type="warning">
@ -3450,7 +3450,7 @@ function ChartEdit() {
id: chartValues.id,
index: databaseOperationsModalData.index,
fieldName: 'serie',
itemSerie: { ...values },
itemSerie: { ...values } as ChartSeriesDto,
})
toast.push(

View file

@ -10,7 +10,7 @@ import { Field, FieldProps, Form, Formik } from 'formik'
import { useEffect, useState } from 'react'
import * as Yup from 'yup'
import { FormEditProps } from './FormEdit'
import { listFormTypeOptions } from './options'
import { listFormDefaultLayoutOptions, listFormTypeOptions } from './options'
const schema = Yup.object().shape({
cultureName: Yup.string().required('Culture Name Required'),
@ -18,6 +18,13 @@ const schema = Yup.object().shape({
name: Yup.string(),
pageSize: Yup.number(),
description: Yup.string(),
layoutDto: Yup.object().shape({
grid: Yup.boolean(),
card: Yup.boolean(),
pivot: Yup.boolean(),
defaultLayout: Yup.string(),
cardLayoutColumn: Yup.number(),
}),
})
function FormTabDetails(props: FormEditProps) {
@ -181,6 +188,92 @@ function FormTabDetails(props: FormEditProps) {
component={Input}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.GridLayout')}
invalid={errors.layoutDto?.grid && touched.layoutDto?.grid}
errorMessage={errors.layoutDto?.grid}
>
<Field
className="w-20"
autoComplete="off"
name="layoutDto.grid"
placeholder={translate('::ListForms.ListFormEdit.DetailsLayoutDto.GridLayout')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.CardLayout')}
invalid={errors.layoutDto?.card && touched.layoutDto?.card}
errorMessage={errors.layoutDto?.card}
>
<Field
className="w-20"
autoComplete="off"
name="layoutDto.card"
placeholder={translate('::ListForms.ListFormEdit.DetailsLayoutDto.CardLayout')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.PivotLayout')}
invalid={errors.layoutDto?.card && touched.layoutDto?.card}
errorMessage={errors.layoutDto?.card}
>
<Field
className="w-20"
autoComplete="off"
name="layoutDto.pivot"
placeholder={translate('::ListForms.ListFormEdit.DetailsLayoutDto.PivotLayout')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.DefaultLayout')}
invalid={errors.layoutDto?.defaultLayout && touched.layoutDto?.defaultLayout}
errorMessage={errors.layoutDto?.defaultLayout}
>
<Field
type="text"
autoComplete="off"
name="layoutDto.defaultLayout"
placeholder={translate('::ListForms.ListFormEdit.DetailsLayoutDto.DefaultLayout')}
>
{({ field, form }: FieldProps<SelectBox>) => (
<Select
field={field}
form={form}
isClearable={true}
options={listFormDefaultLayoutOptions}
value={listFormDefaultLayoutOptions?.filter(
(option) => option.value === values.layoutDto.defaultLayout,
)}
onChange={(option) => form.setFieldValue(field.name, option?.value)}
/>
)}
</Field>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.CardLayoutColumn')}
invalid={errors.layoutDto?.cardLayoutColumn && touched.layoutDto?.cardLayoutColumn}
errorMessage={errors.layoutDto?.cardLayoutColumn}
>
<Field
type="number"
className="w-20"
autoComplete="off"
name="layoutDto.cardLayoutColumn"
placeholder={translate(
'::ListForms.ListFormEdit.DetailsLayoutDto.CardLayoutColumn',
)}
component={Input}
/>
</FormItem>
<Button block variant="solid" loading={isSubmitting} type="submit">
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
</Button>

View file

@ -189,6 +189,12 @@ export const listFormTypeOptions = [
{ value: 'List', label: 'List' },
]
export const listFormDefaultLayoutOptions = [
{ value: 'grid', label: 'Grid' },
{ value: 'card', label: 'Card' },
{ value: 'pivot', label: 'Pivot' },
]
export const listFormAlignmentOptions = [
{ value: 'center', label: 'Center' },
{ value: 'left', label: 'Left' },

View file

@ -24,7 +24,7 @@ const FormDevExpress = (props: {
const { listFormCode, isSubForm, mode, refForm, formData, formItems, setFormData } = props
return (
<form className={`${DX_CLASSNAMES} p-2`}>
<form className={`${DX_CLASSNAMES} py-2`}>
<FormDx
ref={refForm}
formData={formData}

View file

@ -4,13 +4,12 @@ import { captionize } from 'devextreme/core/utils/inflector'
import { useListFormCustomDataSource } from '@/shared/useListFormCustomDataSource'
import { Button, Pagination, Select } from '@/components/ui'
import classNames from 'classnames'
import { getList } from '@/services/form.service'
import { FaInbox, FaSearch } from 'react-icons/fa'
import { FaSearch } from 'react-icons/fa'
import FormDevExpress from '../form/FormDevExpress'
import { GroupItem } from 'devextreme/ui/form'
import { Form as FormDx } from 'devextreme-react/form'
import FormButtons from '../form/FormButtons'
import { useNavigate } from 'react-router-dom'
import { Link, useNavigate } from 'react-router-dom'
import { ROUTES_ENUM } from '@/routes/route.constant'
import CustomStore from 'devextreme/data/custom_store'
import { PermissionResults, SimpleItemWithColData } from '../form/types'
@ -24,6 +23,10 @@ import { GridExtraFilterState } from './Utils'
interface CardProps {
listFormCode: string
searchParams?: URLSearchParams
isSubForm?: boolean
level?: number
refreshData?: () => Promise<void>
gridDto?: GridDto
}
type Option = {
@ -147,7 +150,7 @@ const CardItem = ({
return (
<div className="bg-white dark:bg-neutral-800 border dark:border-neutral-700 flex flex-col groupp p-2">
<div className={`flex items-center mb-2 ${isSubForm ? 'justify-center' : 'justify-between'}`}>
<div className={`flex items-center ${isSubForm ? 'justify-end' : 'justify-between'}`}>
{!isSubForm && <h3>{translate('::' + gridDto?.gridOptions.title)}</h3>}
{permissionResults && (
<FormButtons
@ -183,21 +186,22 @@ const CardItem = ({
)
}
const Card = ({ listFormCode, searchParams }: CardProps) => {
const Card = (props: CardProps) => {
const { listFormCode, searchParams, gridDto } = props
const { createSelectDataSource } = useListFormCustomDataSource({})
const [gridDto, setGridDto] = useState<GridDto>()
const [data, setData] = useState<any[]>([])
const [totalCount, setTotalCount] = useState(0)
const [currentPage, setCurrentPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
const [pageSizeOptions, setPageSizeOptions] = useState<Option[]>([])
const [gridDataSource, setGridDataSource] = useState<CustomStore<any, any>>()
const [extraFilters, setExtraFilters] = useState<GridExtraFilterState[]>([])
const [layoutCount, setLayoutCount] = useState(4)
const { getLookupDataSource } = useLookupDataSource({ listFormCode })
const lookupCache = useRef<Map<string, any>>(new Map())
const [layoutCount, setLayoutCount] = useState(4)
const [searchText, setSearchText] = useState('')
const [loading, setLoading] = useState(false)
const [extraFilters, setExtraFilters] = useState<GridExtraFilterState[]>([])
const lookupCache = useRef<Map<string, any>>(new Map())
const getCachedLookupDataSource = useCallback(
(editorOptions: any, colData: any) => {
const key = colData?.fieldName
@ -220,19 +224,70 @@ const Card = ({ listFormCode, searchParams }: CardProps) => {
setCurrentPage(page)
}
useEffect(() => {
getList({ listFormCode }).then((res: any) => setGridDto(res.data))
}, [listFormCode])
// props.searchParams varsa onunla başlat
const [urlSearchParams, setUrlSearchParams] = useState<URLSearchParams>(
searchParams ? new URLSearchParams(searchParams) : new URLSearchParams(),
)
const filtrele = useCallback(
(value?: string) => {
const text = value !== undefined ? value.trim() : searchText.trim()
if (!gridDto?.columnFormats) return
const newParams = new URLSearchParams(urlSearchParams.toString())
if (!text) {
newParams.delete('filter')
setUrlSearchParams(newParams)
return
}
const merged = gridDto.columnFormats
.filter(
(col) =>
col.dataType === 'string' &&
col.visible &&
col.width &&
col.allowSearch &&
col.width > 0,
)
.map((col) => [col.fieldName, 'contains', text])
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, 'or', f]
}, null as any)
}
if (filter) {
newParams.set('filter', JSON.stringify(filter))
} else {
newParams.delete('filter')
}
setUrlSearchParams(newParams) // ✅ state güncelleniyor
console.log('Applied filter:', filter)
},
[gridDto, urlSearchParams, searchText],
)
useEffect(() => {
if (gridDto) {
const dataSource = createSelectDataSource(gridDto.gridOptions, listFormCode, searchParams)
const dataSource = createSelectDataSource(gridDto.gridOptions, listFormCode, urlSearchParams)
setGridDataSource(dataSource)
setLayoutCount(gridDto.gridOptions.layoutDto.cardLayoutColumn || 4)
}
}, [gridDto, listFormCode, searchParams, createSelectDataSource])
}, [gridDto, listFormCode, urlSearchParams, createSelectDataSource])
const loadData = useCallback(() => {
if (!gridDataSource) return
setLoading(true)
const loadOptions = {
skip: (currentPage - 1) * pageSize,
@ -240,9 +295,15 @@ const Card = ({ listFormCode, searchParams }: CardProps) => {
requireTotalCount: true,
}
gridDataSource.load(loadOptions).then((res: any) => {
gridDataSource
.load(loadOptions)
.then((res: any) => {
setData(res.data)
setTotalCount(res.totalCount || 0)
setLoading(false)
})
.catch(() => {
setLoading(false)
})
}, [gridDataSource, currentPage, pageSize])
@ -264,10 +325,13 @@ const Card = ({ listFormCode, searchParams }: CardProps) => {
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' })
if (data.length < 3) {
setLayoutCount(data.length)
}
}
}, [data])
@ -278,20 +342,49 @@ const Card = ({ listFormCode, searchParams }: CardProps) => {
<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" />
<div className="p-1 bg-white dark:bg-neutral-800 dark:border-neutral-700 ">
<div className="flex justify-end items-center">
<div className="relative py-1 flex gap-1 border-b-1">
<FaSearch className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400 text-sm" />
<input
type="text"
placeholder="Search..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
filtrele(e.currentTarget.value)
}
}}
onBlur={(e) => {
filtrele(e.currentTarget.value)
}}
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 === 1 ? 'solid' : 'default'}
className="text-sm"
onClick={() => setLayoutCount(1)}
title="1 Sütunda Göster"
>
1
</Button>
<Button
size="xs"
variant={layoutCount === 2 ? 'solid' : 'default'}
className="text-sm"
onClick={() => setLayoutCount(2)}
title="2 Sütunda Göster"
>
2
</Button>
<Button
size="xs"
variant={layoutCount === 3 ? 'solid' : 'default'}
className="text-sm"
onClick={() => setLayoutCount(3)}
title="3 Sütunda Göster"
>
3
</Button>
@ -300,6 +393,7 @@ const Card = ({ listFormCode, searchParams }: CardProps) => {
variant={layoutCount === 4 ? 'solid' : 'default'}
className="text-sm"
onClick={() => setLayoutCount(4)}
title="4 Sütunda Göster"
>
4
</Button>
@ -308,13 +402,27 @@ const Card = ({ listFormCode, searchParams }: CardProps) => {
variant={layoutCount === 5 ? 'solid' : 'default'}
className="text-sm"
onClick={() => setLayoutCount(5)}
title="5 Sütunda Göster"
>
5
</Button>
</div>
</div>
<div className={`grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-${layoutCount} gap-4`}>
{loading ? (
<Loading loading={true} />
) : data.length === 0 ? (
<div className="text-center py-12">
<FaSearch className="mx-auto h-10 w-10 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-gray-200">
Uygun kayıt bulunamadı
</h3>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Farklı filtreler deneyin veya yeni bir kayıt ekleyin.
</p>
</div>
) : (
<div className={`bg-transparent grid grid-cols-1 lg:grid-cols-${layoutCount} gap-4`}>
{gridDataSource &&
data.map((row, idx) => {
const keyField = gridDto.gridOptions.keyFieldName
@ -334,9 +442,10 @@ const Card = ({ listFormCode, searchParams }: CardProps) => {
)
})}
</div>
)}
{gridDto.gridOptions.pagerOptionDto?.visible && totalCount > pageSize && (
<div className={classNames('flex items-center justify-between border-t-1 gap-4 mt-4')}>
<div className={classNames('flex items-center justify-between border-t-1 gap-4 mt-2')}>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-600 dark:text-gray-300">
Toplam {totalCount} kayıt
@ -357,21 +466,7 @@ const Card = ({ listFormCode, searchParams }: CardProps) => {
/>
</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>
)}
</div>
</Container>
</>
)

View file

@ -8,7 +8,6 @@ import {
ListFormCustomizationTypeEnum,
PlatformEditorTypes,
} from '@/proxy/form/models'
import { getList } from '@/services/form.service'
import {
getListFormCustomization,
postListFormCustomization,
@ -74,20 +73,19 @@ interface GridProps {
isSubForm?: boolean
level?: number
refreshData?: () => Promise<void>
onGridDtoLoad?: (gridDto: GridDto) => void
gridDto?: GridDto
}
const statedGridPanelColor = 'rgba(50, 200, 200, 0.5)' // kullanici tanimli gridState ile islem gormus gridin paneline ait renk
const Grid = (props: GridProps) => {
const { listFormCode, searchParams, isSubForm, level } = props
const { listFormCode, searchParams, isSubForm, level, gridDto } = props
const { translate } = useLocalization()
const { smaller } = useResponsive()
const gridRef = useRef<DataGrid>()
const refListFormCode = useRef('')
const [gridDto, setGridDto] = useState<GridDto>()
const [gridDataSource, setGridDataSource] = useState<CustomStore<any, any>>()
const [columnData, setColumnData] = useState<GridColumnData[]>()
const [formData, setFormData] = useState<any>()
@ -102,12 +100,12 @@ const Grid = (props: GridProps) => {
import('devextreme/pdf_exporter')
}
const defaultSearchParamsFilter = useRef<string | null>(null)
const defaultSearchParams = useRef<string | null>(null)
useEffect(() => {
// sadece 1 kere açılışta al
if (!defaultSearchParamsFilter.current) {
defaultSearchParamsFilter.current = searchParams?.get('filter') ?? null
if (!defaultSearchParams.current) {
defaultSearchParams.current = searchParams?.get('filter') ?? null
}
}, [searchParams])
@ -401,17 +399,9 @@ const Grid = (props: GridProps) => {
gridRef.current.instance.state(null)
}
const initializeGrid = async () => {
const response = await getList({ listFormCode })
setGridDto(response.data)
}
if (refListFormCode.current !== listFormCode) {
initializeGrid()
//bir önceki gridin filtreleri kalmasın
if (refListFormCode.current && refListFormCode.current !== listFormCode) {
setExtraFilters([])
defaultSearchParamsFilter.current = null
defaultSearchParams.current = null
}
}, [listFormCode])
@ -434,7 +424,6 @@ const Grid = (props: GridProps) => {
}
if (gridDto?.gridOptions.extraFilterDto) {
console.log('extraFilterDto:', gridDto.gridOptions.extraFilterDto)
setExtraFilters(
gridDto.gridOptions.extraFilterDto.map((f) => ({
fieldName: f.fieldName,
@ -444,10 +433,6 @@ const Grid = (props: GridProps) => {
})),
)
}
if (gridDto) {
props.onGridDtoLoad?.(gridDto)
}
}, [gridDto])
useEffect(() => {
@ -464,8 +449,8 @@ const Grid = (props: GridProps) => {
const activeFilters = extraFilters.filter((f) => f.value)
let base: any = null
if (defaultSearchParamsFilter.current) {
base = JSON.parse(defaultSearchParamsFilter.current)
if (defaultSearchParams.current) {
base = JSON.parse(defaultSearchParams.current)
}
// Default filter tripletlerini çıkar

View file

@ -1,8 +1,8 @@
import { useLocation, useParams, useSearchParams } from 'react-router-dom'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import Container from '@/components/shared/Container'
import Grid from './Grid'
import { FaList, FaTh, FaUser } from 'react-icons/fa'
import { FaChartPie, FaList, FaTable, FaTh, FaUser } from 'react-icons/fa'
import { useStoreState } from '@/store/store'
import classNames from 'classnames'
import { useLocalization } from '@/utils/hooks/useLocalization'
@ -11,19 +11,41 @@ import Card from './Card'
import { Button } from '@/components/ui'
import navigationIcon from '@/configs/navigation-icon.config'
import { navigationTreeToFlat } from '@/utils/navigation'
import Pivot from './Pivot'
import { getList } from '@/services/form.service'
import { Loading } from '@/components/shared'
const List = () => {
const params = useParams()
const { translate } = useLocalization()
const [searchParams] = useSearchParams()
const listFormCode = params?.listFormCode ?? ''
const [viewMode, setViewMode] = useState<'grid' | 'card'>('grid')
const [viewMode, setViewMode] = useState<'grid' | 'card' | 'pivot'>()
const mode = useStoreState((state) => state.theme.mode)
const [gridDto, setGridDto] = useState<GridDto>()
const mainMenu = useStoreState((state) => state.abpConfig.menu.mainMenu)
const location = useLocation()
if (!listFormCode) return null
const initializeGridAsync = async () => {
const response = await getList({ listFormCode })
setGridDto(response.data)
}
useEffect(() => {
initializeGridAsync()
}, [listFormCode])
useEffect(() => {
if (!gridDto) {
return
}
setViewMode(gridDto?.gridOptions?.layoutDto.defaultLayout)
}, [gridDto])
if (!listFormCode) {
return null
}
const getCurrentMenuIcon = (className = 'w-6 h-6'): JSX.Element => {
const menus = navigationTreeToFlat(mainMenu)
@ -38,6 +60,10 @@ const List = () => {
return (
<Container>
{!gridDto ? (
<Loading loading={true} />
) : (
<>
<div
className={classNames('flex items-center border-solid gap-1 pb-1', {
'border-gray-100': mode === 'light',
@ -45,19 +71,22 @@ const List = () => {
})}
>
<div className="flex items-center gap-2">
{getCurrentMenuIcon('w-6 h-6')}
<h3>{translate('::' + gridDto?.gridOptions?.title)}</h3>
{getCurrentMenuIcon('w-5 h-5')} {/* ikon biraz küçüldü */}
<h4 className="text-slate-700 text-sm font-medium leading-none">
{translate('::' + gridDto?.gridOptions?.title)}
</h4>
</div>
{gridDto?.gridOptions?.description === gridDto?.gridOptions?.title ? (
<p className="text-gray-600 mr-auto pt-1 ml-2"></p>
<p className="mr-auto"></p>
) : (
<p className="text-gray-600 mr-auto pt-1 ml-2">
<p className="text-slate-500 text-xs mr-auto ml-2 leading-none">
{translate('::' + gridDto?.gridOptions?.description)}
</p>
)}
<div className="flex gap-1">
{gridDto?.gridOptions?.layoutDto.grid && (
<Button
size="xs"
variant={viewMode === 'grid' ? 'solid' : 'default'}
@ -66,6 +95,9 @@ const List = () => {
>
<FaList className="w-4 h-4" />
</Button>
)}
{gridDto?.gridOptions?.layoutDto.card && (
<Button
size="xs"
variant={viewMode === 'card' ? 'solid' : 'default'}
@ -74,18 +106,43 @@ const List = () => {
>
<FaTh className="w-4 h-4" />
</Button>
)}
{gridDto?.gridOptions?.layoutDto.pivot && (
<Button
size="xs"
variant={viewMode === 'pivot' ? 'solid' : 'default'}
onClick={() => setViewMode('pivot')}
title="Pivot Görünümü"
>
<FaTable className="w-4 h-4" />
</Button>
)}
</div>
</div>
{viewMode === 'grid' ? (
{viewMode === 'pivot' ? (
<Pivot
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={false}
gridDto={gridDto}
/>
) : viewMode === 'card' ? (
<Card
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={false}
gridDto={gridDto}
/>
) : (
<Grid
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={false}
onGridDtoLoad={setGridDto}
gridDto={gridDto}
/>
) : (
<Card listFormCode={listFormCode} searchParams={searchParams} />
)}
</>
)}
</Container>
)

View file

@ -40,19 +40,19 @@ interface GridProps {
isSubForm?: boolean
level?: number
refreshData?: () => Promise<void>
gridDto?: GridDto
}
const statedGridPanelColor = 'rgba(50, 200, 200, 0.5)' // kullanici tanimli gridState ile islem gormus gridin paneline ait renk
const Pivot = (props: GridProps) => {
const { listFormCode, searchParams, isSubForm, level } = props
const { listFormCode, searchParams, isSubForm, level, gridDto } = props
const { translate } = useLocalization()
const gridRef = useRef<PivotGrid>()
const chartRef = useRef<Chart>(null)
const refListFormCode = useRef('')
const [gridDto, setGridDto] = useState<GridDto>()
const [gridDataSource, setGridDataSource] = useState<CustomStore<any, any>>()
const [columnData, setColumnData] = useState<GridColumnData[]>()
@ -137,15 +137,6 @@ const Pivot = (props: GridProps) => {
gridRef.current.instance.option('dataSource', undefined)
gridRef.current.instance.option('stateStoring', undefined)
}
const initializeGrid = async () => {
const response = await getList({ listFormCode })
setGridDto(response.data)
}
if (refListFormCode.current !== listFormCode) {
initializeGrid()
}
}, [listFormCode])
useEffect(() => {

View file

@ -8,7 +8,6 @@ import { ToolbarItem } from 'devextreme/ui/data_grid_types'
import { useEffect, useState } from 'react'
import { useDialogContext } from '../shared/DialogContext'
import { usePWA } from '@/utils/hooks/usePWA'
import { GridExtraFilterState } from './Utils'
type ToolbarModalData = {
open: boolean
@ -282,16 +281,9 @@ const useToolbar = ({
}
useEffect(() => {
if (!gridDto) return () => null
if (!listFormCode) return () => null
if (!gridDto && !listFormCode) return
const timeout = setTimeout(() => {
getToolbarData()
}, 100)
return () => {
clearTimeout(timeout)
}
}, [gridDto, listFormCode])
return {