Grid ve Card Görünümü

This commit is contained in:
Sedat Öztürk 2025-09-21 00:39:53 +03:00
parent a01422ca60
commit 3f69cc54e9
8 changed files with 363 additions and 144 deletions

View file

@ -18,7 +18,7 @@ export declare namespace TypeAttributes {
type Shape = 'round' | 'circle' | 'none' type Shape = 'round' | 'circle' | 'none'
type Status = 'success' | 'warning' | 'danger' | 'info' type Status = 'success' | 'warning' | 'danger' | 'info'
type FormLayout = 'horizontal' | 'vertical' | 'inline' type FormLayout = 'horizontal' | 'vertical' | 'inline'
type ControlSize = 'lg' | 'md' | 'sm' type ControlSize = 'lg' | 'md' | 'sm' | 'xs'
type MenuVariant = 'light' | 'dark' | 'themed' | 'transparent' type MenuVariant = 'light' | 'dark' | 'themed' | 'transparent'
type Direction = 'ltr' | 'rtl' type Direction = 'ltr' | 'rtl'
} }

View file

@ -8,132 +8,130 @@ import { useConfig } from '../ConfigProvider'
import type { CommonProps } from '../@types/common' import type { CommonProps } from '../@types/common'
export interface PaginationProps extends CommonProps { export interface PaginationProps extends CommonProps {
currentPage?: number currentPage?: number
displayTotal?: boolean displayTotal?: boolean
onChange?: (pageNumber: number) => void onChange?: (pageNumber: number) => void
pageSize?: number pageSize?: number
total?: number total?: number
} }
const Pagination = (props: PaginationProps) => { const Pagination = (props: PaginationProps) => {
const { const {
className, className,
currentPage = 1, currentPage = 1,
displayTotal = false, displayTotal = false,
onChange, onChange,
pageSize = 1, pageSize = 1,
total = 5, total = 5,
} = props } = props
const [paginationTotal, setPaginationTotal] = useState(total) const [paginationTotal, setPaginationTotal] = useState(total)
const [internalPageSize, setInternalPageSize] = useState(pageSize) const [internalPageSize, setInternalPageSize] = useState(pageSize)
const { themeColor, primaryColorLevel } = useConfig() const { themeColor, primaryColorLevel } = useConfig()
const getInternalPageCount = useMemo(() => { const getInternalPageCount = useMemo(() => {
if (typeof paginationTotal === 'number') { if (typeof paginationTotal === 'number') {
return Math.ceil(paginationTotal / internalPageSize) return Math.ceil(paginationTotal / internalPageSize)
}
return null
}, [paginationTotal, internalPageSize])
const getValidCurrentPage = useCallback(
(count: number | string) => {
const value = parseInt(count as string, 10)
const internalPageCount = getInternalPageCount
let resetValue
if (!internalPageCount) {
if (isNaN(value) || value < 1) {
resetValue = 1
} }
return null } else {
}, [paginationTotal, internalPageSize]) if (value < 1) {
resetValue = 1
const getValidCurrentPage = useCallback(
(count: number | string) => {
const value = parseInt(count as string, 10)
const internalPageCount = getInternalPageCount
let resetValue
if (!internalPageCount) {
if (isNaN(value) || value < 1) {
resetValue = 1
}
} else {
if (value < 1) {
resetValue = 1
}
if (value > internalPageCount) {
resetValue = internalPageCount
}
}
if (
(resetValue === undefined && isNaN(value)) ||
resetValue === 0
) {
resetValue = 1
}
return resetValue === undefined ? value : resetValue
},
[getInternalPageCount]
)
const [internalCurrentPage, setInternalCurrentPage] = useState(
currentPage ? getValidCurrentPage(currentPage) : 1
)
useEffect(() => {
if (total !== paginationTotal) {
setPaginationTotal(total)
} }
if (value > internalPageCount) {
if (pageSize !== internalPageSize) { resetValue = internalPageCount
setInternalPageSize(pageSize)
} }
}
if (currentPage !== internalCurrentPage) { if ((resetValue === undefined && isNaN(value)) || resetValue === 0) {
setInternalCurrentPage(currentPage) resetValue = 1
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [total, pageSize, currentPage])
const onPaginationChange = (val: number) => { return resetValue === undefined ? value : resetValue
setInternalCurrentPage(getValidCurrentPage(val)) },
onChange?.(getValidCurrentPage(val)) [getInternalPageCount],
)
const [internalCurrentPage, setInternalCurrentPage] = useState(
currentPage ? getValidCurrentPage(currentPage) : 1,
)
useEffect(() => {
if (total !== paginationTotal) {
setPaginationTotal(total)
} }
const onPrev = useCallback(() => { if (pageSize !== internalPageSize) {
const newPage = internalCurrentPage - 1 setInternalPageSize(pageSize)
setInternalCurrentPage(getValidCurrentPage(newPage))
onChange?.(getValidCurrentPage(newPage))
}, [onChange, internalCurrentPage, getValidCurrentPage])
const onNext = useCallback(() => {
const newPage = internalCurrentPage + 1
setInternalCurrentPage(getValidCurrentPage(newPage))
onChange?.(getValidCurrentPage(newPage))
}, [onChange, internalCurrentPage, getValidCurrentPage])
const pagerClass = {
default: 'pagination-pager',
inactive: 'pagination-pager-inactive',
active: `text-${themeColor}-${primaryColorLevel} bg-${themeColor}-50 hover:bg-${themeColor}-50 dark:bg-${themeColor}-${primaryColorLevel} dark:text-gray-100`,
disabled: 'pagination-pager-disabled',
} }
const paginationClass = classNames('pagination', className) if (currentPage !== internalCurrentPage) {
setInternalCurrentPage(currentPage)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [total, pageSize, currentPage])
return ( const onPaginationChange = (val: number) => {
<div className={paginationClass}> setInternalCurrentPage(getValidCurrentPage(val))
{displayTotal && <Total total={total} />} onChange?.(getValidCurrentPage(val))
<Prev }
currentPage={internalCurrentPage}
pagerClass={pagerClass} const onPrev = useCallback(() => {
onPrev={onPrev} const newPage = internalCurrentPage - 1
/> setInternalCurrentPage(getValidCurrentPage(newPage))
<Pager onChange?.(getValidCurrentPage(newPage))
pageCount={getInternalPageCount as number} }, [onChange, internalCurrentPage, getValidCurrentPage])
currentPage={internalCurrentPage}
pagerClass={pagerClass} const onNext = useCallback(() => {
onChange={onPaginationChange} const newPage = internalCurrentPage + 1
/> setInternalCurrentPage(getValidCurrentPage(newPage))
<Next onChange?.(getValidCurrentPage(newPage))
currentPage={internalCurrentPage} }, [onChange, internalCurrentPage, getValidCurrentPage])
pageCount={getInternalPageCount as number}
pagerClass={pagerClass} const pagerClass = {
onNext={onNext} default: 'pagination-pager px-2 py-1 text-xs rounded-md', // 🔽 küçük padding + küçük yazı
/> inactive: 'pagination-pager-inactive text-gray-500',
</div> active: `text-${themeColor}-${primaryColorLevel} bg-${themeColor}-50
) hover:bg-${themeColor}-100
dark:bg-${themeColor}-${primaryColorLevel} dark:text-gray-100`,
disabled: 'pagination-pager-disabled opacity-50',
}
const paginationClass = classNames(
'pagination flex items-center justify-center text-xs', // 🔽 daha küçük yazı + boşluk azaldı
className,
)
return (
<div className={paginationClass}>
{displayTotal && <Total total={total} />}
<Prev currentPage={internalCurrentPage} pagerClass={pagerClass} onPrev={onPrev} />
<Pager
pageCount={getInternalPageCount as number}
currentPage={internalCurrentPage}
pagerClass={pagerClass}
onChange={onPaginationChange}
/>
<Next
currentPage={internalCurrentPage}
pageCount={getInternalPageCount as number}
pagerClass={pagerClass}
onNext={onNext}
/>
</div>
)
} }
Pagination.displayName = 'Pagination' Pagination.displayName = 'Pagination'

View file

@ -104,7 +104,7 @@ const FormButtons = (props: {
return ( return (
<> <>
<div className="flex flex-row items-center gap-2"> <div className="flex flex-row items-center gap-1">
{toolbarData {toolbarData
?.filter( ?.filter(
(item) => (item) =>
@ -168,6 +168,21 @@ const FormButtons = (props: {
{!!commandColumnData?.buttons?.filter((item) => typeof item !== 'string').length && ( {!!commandColumnData?.buttons?.filter((item) => typeof item !== 'string').length && (
<Badge innerClass="bg-blue-500" /> <Badge innerClass="bg-blue-500" />
)} )}
<Button
variant="solid"
size="xs"
color="gray-500"
title={translate('::Cancel')}
onClick={() => {
if (onActionView && id) {
onActionView()
} else {
navigate(-1)
}
}}
>
<FaBackward />
</Button>
{mode != 'new' && ( {mode != 'new' && (
<Button <Button
variant="solid" variant="solid"
@ -241,23 +256,6 @@ const FormButtons = (props: {
<FaFileAlt /> <FaFileAlt />
</Button> </Button>
)} )}
{mode === 'new' && (
<Button
variant="solid"
size="xs"
color="gray-500"
title={translate('::Cancel')}
onClick={() => {
if (onActionView && id) {
onActionView()
} else {
navigate(-1)
}
}}
>
<FaBackward />
</Button>
)}
{(mode == 'edit' || mode == 'new') && ( {(mode == 'edit' || mode == 'new') && (
<Button <Button
variant="solid" variant="solid"

View file

@ -66,7 +66,7 @@ const FormEdit = (
defaultTitle="Sözsoft Kurs Platform" defaultTitle="Sözsoft Kurs Platform"
></Helmet> ></Helmet>
)} )}
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-2">
<h3>{translate('::' + gridDto?.gridOptions.title)}</h3> <h3>{translate('::' + gridDto?.gridOptions.title)}</h3>
{permissionResults && ( {permissionResults && (
<FormButtons <FormButtons

View file

@ -61,7 +61,7 @@ const FormView = (
defaultTitle="Sözsoft Kurs Platform" defaultTitle="Sözsoft Kurs Platform"
></Helmet> ></Helmet>
)} )}
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-2">
<h3>{translate('::' + gridDto?.gridOptions.title)}</h3> <h3>{translate('::' + gridDto?.gridOptions.title)}</h3>
{permissionResults && ( {permissionResults && (
<FormButtons <FormButtons

View file

@ -0,0 +1,167 @@
import React, { useCallback, useEffect, useState } from 'react'
import { GridDto } from '@/proxy/form/models'
import { captionize } from 'devextreme/core/utils/inflector'
import { useListFormCustomDataSource } from '@/shared/useListFormCustomDataSource'
import { useNavigate } from 'react-router-dom'
import { Pagination, Select } from '@/components/ui'
import classNames from 'classnames'
import { useStoreState } from '@/store/store'
import { getList } from '@/services/form.service'
import { FaAngleRight } from 'react-icons/fa'
interface MultiFormViewProps {
listFormCode: string
searchParams?: URLSearchParams
}
const CardView = ({ listFormCode, searchParams }: MultiFormViewProps) => {
const { createSelectDataSource } = useListFormCustomDataSource({})
const [gridDto, setGridDto] = useState<GridDto>()
const navigate = useNavigate()
const [data, setData] = useState<any[]>([])
const [totalCount, setTotalCount] = useState(0)
const [currentPage, setCurrentPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
const [pageSizeOptions, setPageSizeOptions] = useState<Option[]>([])
const mode = useStoreState((state) => state.theme.mode)
type Option = {
value: number
label: string
}
const onPageSizeSelect = ({ value }: Option) => {
setPageSize(value)
setCurrentPage(1) // Sayfa boyutu değiştiğinde ilk sayfaya dön
}
const onPageChange = (page: number) => {
setCurrentPage(page)
}
const loadData = useCallback(() => {
if (!gridDto) return
const store = createSelectDataSource(gridDto.gridOptions, listFormCode, searchParams)
const loadOptions = {
skip: (currentPage - 1) * pageSize,
take: pageSize,
requireTotalCount: true,
}
store.load(loadOptions).then((res: any) => {
setData(res.data)
setTotalCount(res.totalCount || 0)
})
}, [gridDto, listFormCode, searchParams, currentPage, pageSize])
useEffect(() => {
getList({ listFormCode }).then((res: any) => setGridDto(res.data))
}, [listFormCode])
useEffect(() => {
if (!gridDto) return
const pagerOptions = gridDto.gridOptions.pagerOptionDto
// const initialPageSize = gridDto.gridOptions.pageSize || 10
const allowedSizes = pagerOptions?.allowedPageSizes
?.split(',')
.map((s) => Number(s.trim()))
.filter((n) => !isNaN(n) && n > 0) || [20, 50, 100]
// setPageSize(initialPageSize)
setPageSizeOptions(allowedSizes.map((size) => ({ value: size, label: `${size} page` })))
}, [gridDto, listFormCode, searchParams])
useEffect(() => {
loadData()
}, [loadData])
if (!gridDto) return null
return (
<>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
{data.map((row, idx) => {
const keyField = gridDto.gridOptions.keyFieldName
const rowId = row[keyField!]
const handleCardClick = () => {
if (rowId) {
navigate(`/admin/form/${listFormCode}/${rowId}`)
}
}
return (
<div
key={rowId || idx}
className="bg-white dark:bg-neutral-800 hover:shadow-xl hover:border-blue-400 border dark:border-neutral-700 transition-all duration-300 cursor-pointer flex flex-col group"
onClick={handleCardClick}
>
<div className="p-4 flex-grow">
<div className="grid grid-cols-1 gap-y-3">
{gridDto.columnFormats
.filter((col) => col.visible && col.listOrderNo > 0)
.sort((a, b) => a.listOrderNo - b.listOrderNo)
.slice(0, 10) // İlk 10 görünür alanı gösterelim
.map((col, colIdx) => (
<div key={col.fieldName} className="truncate text-sm">
<p className="text-xs text-slate-500 dark:text-slate-400 mb-0.5">
{captionize(col.captionName || col.fieldName)}
</p>
<p
className={classNames(
'truncate',
colIdx === 0
? 'font-semibold text-base text-slate-800 dark:text-slate-100'
: 'text-slate-700 dark:text-slate-300',
)}
title={row[col.fieldName!]}
>
{row[col.fieldName!]}
</p>
</div>
))}
</div>
</div>
<div className="bg-gray-50 dark:bg-neutral-700/50 p-2 border-t border-gray-100 dark:border-neutral-700 flex items-center justify-end text-xs font-semibold text-blue-600 dark:text-blue-400 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
Görüntüle
<FaAngleRight className="ml-1 w-3 h-3" />
</div>
</div>
)
})}
</div>
{gridDto.gridOptions.pagerOptionDto?.visible && totalCount > pageSize && (
<div
className={classNames(
'flex items-center justify-between border-t-2 border-solid gap-4 p-2',
)}
>
<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>
)}
</>
)
}
export default CardView

View file

@ -75,6 +75,7 @@ interface GridProps {
isSubForm?: boolean isSubForm?: boolean
level?: number level?: number
refreshData?: () => Promise<void> refreshData?: () => Promise<void>
onGridDtoLoad?: (gridDto: GridDto) => void
} }
const statedGridPanelColor = 'rgba(50, 200, 200, 0.5)' // kullanici tanimli gridState ile islem gormus gridin paneline ait renk const statedGridPanelColor = 'rgba(50, 200, 200, 0.5)' // kullanici tanimli gridState ile islem gormus gridin paneline ait renk
@ -446,6 +447,10 @@ const Grid = (props: GridProps) => {
})), })),
) )
} }
if (gridDto) {
props.onGridDtoLoad?.(gridDto)
}
}, [gridDto]) }, [gridDto])
useEffect(() => { useEffect(() => {

View file

@ -1,24 +1,75 @@
import { CommonProps } from '@/@types/common'
import { Meta } from '@/@types/routes'
import Container from '@/components/shared/Container'
import { useParams, useSearchParams } from 'react-router-dom' import { useParams, useSearchParams } from 'react-router-dom'
import { useState } from 'react'
import Container from '@/components/shared/Container'
import Grid from './Grid' import Grid from './Grid'
import { Button } from '@/components/ui/button'
import { FaList, FaTh } from 'react-icons/fa'
import { useStoreState } from '@/store/store'
import classNames from 'classnames'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { GridDto } from '@/proxy/form/models'
import CardView from './CardView'
export interface FormProps extends CommonProps, Meta { const List = () => {
listFormCode?: string
}
const List = (props?: FormProps) => {
const params = useParams() const params = useParams()
const _listFormCode = props?.listFormCode ?? params?.listFormCode ?? '' const { translate } = useLocalization()
const [searchParams] = useSearchParams() const [searchParams] = useSearchParams()
const listFormCode = params?.listFormCode ?? ''
const [viewMode, setViewMode] = useState<'grid' | 'card'>('grid')
const mode = useStoreState((state) => state.theme.mode)
const [gridDto, setGridDto] = useState<GridDto>()
return _listFormCode ? ( if (!listFormCode) return null
return (
<Container> <Container>
<Grid listFormCode={_listFormCode} searchParams={searchParams} isSubForm={false}></Grid> <div
className={classNames('flex items-center border-b-2 border-solid gap-1 pb-1', {
'border-gray-100': mode === 'light',
'border-neutral-700': mode === 'dark',
})}
>
<h3>{translate('::' + gridDto?.gridOptions?.title)}</h3>
{gridDto?.gridOptions?.description === gridDto?.gridOptions?.title ? (
<p className="text-gray-600 mr-auto pt-1 ml-2"></p>
) : (
<p className="text-gray-600 mr-auto pt-1 ml-2">
{translate('::' + gridDto?.gridOptions?.description)}
</p>
)}
<div className="flex gap-1">
<Button
size="xs"
variant={viewMode === 'grid' ? 'solid' : 'default'}
onClick={() => setViewMode('grid')}
title="Grid Görünümü"
>
<FaList className="w-4 h-4" />
</Button>
<Button
size="xs"
variant={viewMode === 'card' ? 'solid' : 'default'}
onClick={() => setViewMode('card')}
title="Kart Görünümü"
>
<FaTh className="w-4 h-4" />
</Button>
</div>
</div>
{viewMode === 'grid' ? (
<Grid
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={false}
onGridDtoLoad={setGridDto}
/>
) : (
<CardView listFormCode={listFormCode} searchParams={searchParams} />
)}
</Container> </Container>
) : (
<></>
) )
} }