Card ve CardItem silindi. CardView eklendi.
This commit is contained in:
parent
b3989e4a08
commit
bc23a0aef8
12 changed files with 7 additions and 1169 deletions
|
|
@ -4,7 +4,6 @@ public class LayoutDto
|
||||||
{
|
{
|
||||||
public bool Grid { get; set; } = true;
|
public bool Grid { get; set; } = true;
|
||||||
public bool Card { get; set; } = true;
|
public bool Card { get; set; } = true;
|
||||||
public bool CardView { get; set; } = true;
|
|
||||||
public bool Pivot { get; set; } = true;
|
public bool Pivot { get; set; } = true;
|
||||||
public bool Chart { get; set; } = true;
|
public bool Chart { get; set; } = true;
|
||||||
public bool Tree { get; set; } = true;
|
public bool Tree { get; set; } = true;
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,6 @@ public static class SeederDefaults
|
||||||
{
|
{
|
||||||
Grid = true,
|
Grid = true,
|
||||||
Card = true,
|
Card = true,
|
||||||
CardView = true,
|
|
||||||
Pivot = true,
|
Pivot = true,
|
||||||
Chart = true,
|
Chart = true,
|
||||||
Tree = true,
|
Tree = true,
|
||||||
|
|
|
||||||
|
|
@ -894,7 +894,6 @@ export interface WidgetEditDto {
|
||||||
export interface LayoutDto {
|
export interface LayoutDto {
|
||||||
grid: boolean
|
grid: boolean
|
||||||
card: boolean
|
card: boolean
|
||||||
cardView: boolean
|
|
||||||
pivot: boolean
|
pivot: boolean
|
||||||
tree: boolean
|
tree: boolean
|
||||||
chart: boolean
|
chart: boolean
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ const schema = Yup.object().shape({
|
||||||
layoutDto: Yup.object().shape({
|
layoutDto: Yup.object().shape({
|
||||||
grid: Yup.boolean(),
|
grid: Yup.boolean(),
|
||||||
card: Yup.boolean(),
|
card: Yup.boolean(),
|
||||||
cardView: Yup.boolean(),
|
|
||||||
pivot: Yup.boolean(),
|
pivot: Yup.boolean(),
|
||||||
chart: Yup.boolean(),
|
chart: Yup.boolean(),
|
||||||
tree: Yup.boolean(),
|
tree: Yup.boolean(),
|
||||||
|
|
@ -311,20 +310,6 @@ function FormTabDetails(
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem
|
|
||||||
label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.CardViewLayout')}
|
|
||||||
invalid={errors.layoutDto?.cardView && touched.layoutDto?.cardView}
|
|
||||||
errorMessage={errors.layoutDto?.cardView}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
className="w-20"
|
|
||||||
autoComplete="off"
|
|
||||||
name="layoutDto.cardView"
|
|
||||||
placeholder={translate('::ListForms.ListFormEdit.DetailsLayoutDto.CardViewLayout')}
|
|
||||||
component={Checkbox}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem
|
<FormItem
|
||||||
label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.PivotLayout')}
|
label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.PivotLayout')}
|
||||||
invalid={errors.layoutDto?.pivot && touched.layoutDto?.pivot}
|
invalid={errors.layoutDto?.pivot && touched.layoutDto?.pivot}
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,6 @@ export const listFormTypeOptions = [
|
||||||
export const listFormDefaultLayoutOptions = [
|
export const listFormDefaultLayoutOptions = [
|
||||||
{ value: 'grid', label: 'Grid' },
|
{ value: 'grid', label: 'Grid' },
|
||||||
{ value: 'card', label: 'Card' },
|
{ value: 'card', label: 'Card' },
|
||||||
{ value: 'cardView', label: 'Card View' },
|
|
||||||
{ value: 'pivot', label: 'Pivot' },
|
{ value: 'pivot', label: 'Pivot' },
|
||||||
{ value: 'chart', label: 'Chart' },
|
{ value: 'chart', label: 'Chart' },
|
||||||
{ value: 'tree', label: 'Tree' },
|
{ value: 'tree', label: 'Tree' },
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
export type ChartOperation = '' | 'select' | 'insert' | 'update' | 'delete'
|
export type ChartOperation = '' | 'select' | 'insert' | 'update' | 'delete'
|
||||||
export type ChartDialogType = '' | 'pane' | 'serie' | 'annotation' | 'axis'
|
export type ChartDialogType = '' | 'pane' | 'serie' | 'annotation' | 'axis'
|
||||||
export type ListViewLayoutType = 'grid' | 'card' | 'cardView' | 'pivot' | 'tree' | 'chart' | 'gantt' | 'scheduler'
|
export type ListViewLayoutType = 'grid' | 'card' | 'pivot' | 'tree' | 'chart' | 'gantt' | 'scheduler'
|
||||||
|
|
||||||
export const layoutTypes = {
|
export const layoutTypes = {
|
||||||
grid: 'Grid',
|
grid: 'Grid',
|
||||||
card: 'Card',
|
card: 'Card',
|
||||||
cardView: 'CardView',
|
|
||||||
pivot: 'Pivot',
|
pivot: 'Pivot',
|
||||||
tree: 'Tree',
|
tree: 'Tree',
|
||||||
chart: 'Chart',
|
chart: 'Chart',
|
||||||
|
|
|
||||||
|
|
@ -277,7 +277,7 @@ function RolesPermission({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
<div style={{ width: '30%' }} className="max-h-[470px] overflow-y-auto">
|
<div style={{ width: '30%' }} className="max-h-[450px] overflow-y-auto">
|
||||||
<hr className="mb-2"></hr>
|
<hr className="mb-2"></hr>
|
||||||
<Menu
|
<Menu
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
|
@ -297,7 +297,7 @@ function RolesPermission({
|
||||||
))}
|
))}
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ width: '70%' }} className="max-h-[470px] overflow-y-auto">
|
<div style={{ width: '70%' }} className="max-h-[450px] overflow-y-auto">
|
||||||
<hr className="mb-2"></hr>
|
<hr className="mb-2"></hr>
|
||||||
<Input
|
<Input
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
|
||||||
|
|
@ -237,7 +237,7 @@ function UsersPermission({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
<div style={{ width: '30%' }} className="max-h-[470px] overflow-y-auto">
|
<div style={{ width: '30%' }} className="max-h-[450px] overflow-y-auto">
|
||||||
<hr className="mt-2 mb-2"></hr>
|
<hr className="mt-2 mb-2"></hr>
|
||||||
<Menu variant={mode} defaultActiveKeys={[selectedGroup?.displayName ?? '']}>
|
<Menu variant={mode} defaultActiveKeys={[selectedGroup?.displayName ?? '']}>
|
||||||
{permissionList?.groups.map((group) => (
|
{permissionList?.groups.map((group) => (
|
||||||
|
|
@ -255,7 +255,7 @@ function UsersPermission({
|
||||||
))}
|
))}
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ width: '70%' }} className="max-h-[470px] overflow-y-auto">
|
<div style={{ width: '70%' }} className="max-h-[450px] overflow-y-auto">
|
||||||
<hr className="mt-2 mb-2"></hr>
|
<hr className="mt-2 mb-2"></hr>
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
{selectedGroupPermissions.map((permission) => (
|
{selectedGroupPermissions.map((permission) => (
|
||||||
|
|
|
||||||
|
|
@ -1,806 +0,0 @@
|
||||||
import { useCallback, useEffect, useState, useRef, KeyboardEvent, useMemo } from 'react'
|
|
||||||
import { GridDto } from '@/proxy/form/models'
|
|
||||||
import { Button, Pagination, Select, Checkbox } from '@/components/ui'
|
|
||||||
import classNames from 'classnames'
|
|
||||||
import { FaCog, FaSearch, FaSortAmountDown, FaSortAmountUp } from 'react-icons/fa'
|
|
||||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
|
||||||
import CustomStore from 'devextreme/data/custom_store'
|
|
||||||
import { usePermission } from '@/utils/hooks/usePermission'
|
|
||||||
import { useLookupDataSource } from '../form/useLookupDataSource'
|
|
||||||
import { Container, Loading } from '@/components/shared'
|
|
||||||
import WidgetGroup from '@/components/common/WidgetGroup'
|
|
||||||
import { GridExtraFilterState } from './Utils'
|
|
||||||
import { useStoreActions, useStoreState } from '@/store/store'
|
|
||||||
import { usePWA } from '@/utils/hooks/usePWA'
|
|
||||||
import CardItem from './CardItem'
|
|
||||||
import { layoutTypes } from '../admin/listForm/edit/types'
|
|
||||||
import { useListFormCustomDataSource } from './useListFormCustomDataSource'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
|
|
||||||
interface CardProps {
|
|
||||||
listFormCode: string
|
|
||||||
searchParams?: URLSearchParams
|
|
||||||
isSubForm?: boolean
|
|
||||||
level?: number
|
|
||||||
refreshData?: () => Promise<void>
|
|
||||||
gridDto?: GridDto
|
|
||||||
}
|
|
||||||
|
|
||||||
type Option = {
|
|
||||||
value: number
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const Card = (props: CardProps) => {
|
|
||||||
const { listFormCode, searchParams, gridDto } = props
|
|
||||||
const { translate } = useLocalization()
|
|
||||||
const { createSelectDataSource } = useListFormCustomDataSource({} as any)
|
|
||||||
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 { getLookupDataSource } = useLookupDataSource({ listFormCode })
|
|
||||||
// Memoize getLookupDataSource to prevent recalculation on each render
|
|
||||||
const memoizedGetLookupDataSource = useCallback(
|
|
||||||
(editorOptions: any, colData: any, row?: any) => getLookupDataSource(editorOptions, colData, row),
|
|
||||||
[getLookupDataSource]
|
|
||||||
)
|
|
||||||
const [layoutCount, setLayoutCount] = useState(4)
|
|
||||||
const [searchText, setSearchText] = useState('')
|
|
||||||
const [prevValue, setPrevValue] = useState('')
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
|
|
||||||
// Progressive rendering state
|
|
||||||
const [renderedCount, setRenderedCount] = useState(0)
|
|
||||||
const [isProgressiveRendering, setIsProgressiveRendering] = useState(false)
|
|
||||||
|
|
||||||
const { checkPermission } = usePermission()
|
|
||||||
const isPwaMode = usePWA()
|
|
||||||
const [extraFilters, setExtraFilters] = useState<GridExtraFilterState[]>([])
|
|
||||||
|
|
||||||
// Selection features - gridDto değiştiğinde güncellenmeli
|
|
||||||
const [selectedKeys, setSelectedKeys] = useState<Set<any>>(new Set())
|
|
||||||
const selectionMode: 'none' | 'single' | 'multiple' = useMemo(() => {
|
|
||||||
if (!gridDto?.gridOptions?.selectionDto?.mode) return 'none'
|
|
||||||
const mode = gridDto.gridOptions.selectionDto.mode.toLowerCase()
|
|
||||||
if (mode === 'single') return 'single'
|
|
||||||
if (mode === 'multiple') return 'multiple'
|
|
||||||
return 'none'
|
|
||||||
}, [gridDto])
|
|
||||||
|
|
||||||
// Sorting features
|
|
||||||
const [sortColumn, setSortColumn] = useState<string | null>(null)
|
|
||||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc')
|
|
||||||
|
|
||||||
// Focus and keyboard navigation
|
|
||||||
const [focusedCardIndex, setFocusedCardIndex] = useState<number>(-1)
|
|
||||||
const cardRefs = useRef<(HTMLDivElement | null)[]>([])
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
// Hover state
|
|
||||||
const [hoveredCardKey, setHoveredCardKey] = useState<any>(null)
|
|
||||||
|
|
||||||
const { states } = useStoreState((state) => state.admin.lists)
|
|
||||||
const { setStates } = useStoreActions((a) => a.admin.lists)
|
|
||||||
|
|
||||||
// props.searchParams varsa onunla başlat
|
|
||||||
const [urlSearchParams, setUrlSearchParams] = useState<URLSearchParams>(() => {
|
|
||||||
const params = searchParams ? new URLSearchParams(searchParams) : new URLSearchParams()
|
|
||||||
|
|
||||||
// Initialize sort state from URL params
|
|
||||||
const sortParam = params.get('sort')
|
|
||||||
if (sortParam) {
|
|
||||||
try {
|
|
||||||
const sortArray = JSON.parse(sortParam)
|
|
||||||
if (sortArray && sortArray.length > 0) {
|
|
||||||
setSortColumn(sortArray[0].selector)
|
|
||||||
setSortOrder(sortArray[0].desc ? 'desc' : 'asc')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Sort param parse error:', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return params
|
|
||||||
})
|
|
||||||
|
|
||||||
const onPageSizeSelect = ({ value }: Option) => {
|
|
||||||
setPageSize(value)
|
|
||||||
setCurrentPage(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onPageChange = (page: number) => {
|
|
||||||
setCurrentPage(page)
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleSort = useCallback((columnName: string) => {
|
|
||||||
const newParams = new URLSearchParams(urlSearchParams.toString())
|
|
||||||
|
|
||||||
if (sortColumn === columnName) {
|
|
||||||
const newOrder = sortOrder === 'asc' ? 'desc' : 'asc'
|
|
||||||
setSortOrder(newOrder)
|
|
||||||
newParams.set('sort', JSON.stringify([{ selector: columnName, desc: newOrder === 'desc' }]))
|
|
||||||
} else {
|
|
||||||
setSortColumn(columnName)
|
|
||||||
setSortOrder('asc')
|
|
||||||
newParams.set('sort', JSON.stringify([{ selector: columnName, desc: false }]))
|
|
||||||
}
|
|
||||||
|
|
||||||
setUrlSearchParams(newParams)
|
|
||||||
setCurrentPage(1) // Reset to first page when sorting
|
|
||||||
}, [sortColumn, sortOrder, urlSearchParams])
|
|
||||||
|
|
||||||
const handleSelectAll = useCallback(async (checked: boolean) => {
|
|
||||||
if (checked) {
|
|
||||||
const keyField = gridDto?.gridOptions.keyFieldName
|
|
||||||
const selectAllMode = gridDto?.gridOptions?.selectionDto?.selectAllMode?.toLowerCase()
|
|
||||||
|
|
||||||
if (!keyField) return
|
|
||||||
|
|
||||||
if (selectAllMode === 'allpages') {
|
|
||||||
// Tüm sayfalardan tüm kayıtları al
|
|
||||||
if (!gridDataSource) return
|
|
||||||
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
const loadOptions = {
|
|
||||||
skip: 0,
|
|
||||||
take: totalCount, // Tüm kayıtları al
|
|
||||||
requireTotalCount: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
const res: any = await gridDataSource.load(loadOptions)
|
|
||||||
const allKeys = new Set(res.data.map((row: any) => row[keyField]))
|
|
||||||
setSelectedKeys(allKeys)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Select all pages error:', err)
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Sadece mevcut sayfadaki kayıtları seç (page veya default)
|
|
||||||
const allKeys = new Set(data.map(row => row[keyField]))
|
|
||||||
setSelectedKeys(allKeys)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setSelectedKeys(new Set())
|
|
||||||
}
|
|
||||||
}, [gridDto, gridDataSource, totalCount, data])
|
|
||||||
|
|
||||||
const handleCardSelection = useCallback((key: any, checked: boolean) => {
|
|
||||||
const newSelection = new Set(selectedKeys)
|
|
||||||
if (checked) {
|
|
||||||
if (selectionMode === 'single') {
|
|
||||||
newSelection.clear()
|
|
||||||
}
|
|
||||||
newSelection.add(key)
|
|
||||||
} else {
|
|
||||||
newSelection.delete(key)
|
|
||||||
}
|
|
||||||
setSelectedKeys(newSelection)
|
|
||||||
}, [selectedKeys, selectionMode])
|
|
||||||
|
|
||||||
const handleCardClick = useCallback((key: any, index: number, event: React.MouseEvent) => {
|
|
||||||
setFocusedCardIndex(index)
|
|
||||||
if (selectionMode !== 'none' && event.ctrlKey) {
|
|
||||||
handleCardSelection(key, !selectedKeys.has(key))
|
|
||||||
}
|
|
||||||
}, [selectionMode, handleCardSelection, selectedKeys])
|
|
||||||
|
|
||||||
const handleCardDoubleClick = useCallback((key: any, row: any) => {
|
|
||||||
// Navigate to edit page on double click
|
|
||||||
if (!gridDto?.gridOptions.editingOptionDto?.allowUpdating) return
|
|
||||||
|
|
||||||
window.open(
|
|
||||||
ROUTES_ENUM.protected.admin.formEdit
|
|
||||||
.replace(':listFormCode', listFormCode)
|
|
||||||
.replace(':id', key),
|
|
||||||
isPwaMode ? '_self' : '_blank'
|
|
||||||
)
|
|
||||||
}, [gridDto, listFormCode, isPwaMode])
|
|
||||||
|
|
||||||
const handleExport = async (format: 'xlsx' | 'csv' | 'pdf', selectedOnly: boolean = false) => {
|
|
||||||
try {
|
|
||||||
const exportData = selectedOnly
|
|
||||||
? data.filter(row => {
|
|
||||||
const keyField = gridDto?.gridOptions.keyFieldName
|
|
||||||
return keyField && selectedKeys.has(row[keyField])
|
|
||||||
})
|
|
||||||
: data
|
|
||||||
|
|
||||||
if (exportData.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (format === 'xlsx' || format === 'csv') {
|
|
||||||
const [{ Workbook }, { saveAs }] = await Promise.all([
|
|
||||||
import('exceljs'),
|
|
||||||
import('file-saver'),
|
|
||||||
])
|
|
||||||
|
|
||||||
const workbook = new Workbook()
|
|
||||||
const worksheet = workbook.addWorksheet(`${listFormCode}_sheet`)
|
|
||||||
|
|
||||||
// Header row - visible columns only
|
|
||||||
const visibleColumns = gridDto?.columnFormats?.filter(
|
|
||||||
col => col.visible && col.dataType !== 'buttons'
|
|
||||||
) || []
|
|
||||||
|
|
||||||
const headerRow = visibleColumns.map(col => col.fieldName)
|
|
||||||
worksheet.addRow(headerRow)
|
|
||||||
worksheet.getRow(1).font = { bold: true }
|
|
||||||
|
|
||||||
// Data rows
|
|
||||||
exportData.forEach(row => {
|
|
||||||
const dataRow = visibleColumns.map(col => row[col.fieldName!])
|
|
||||||
worksheet.addRow(dataRow)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Auto-fit columns
|
|
||||||
worksheet.columns.forEach((column: any) => {
|
|
||||||
column.width = 15
|
|
||||||
})
|
|
||||||
|
|
||||||
if (format === 'xlsx') {
|
|
||||||
const buffer = await workbook.xlsx.writeBuffer()
|
|
||||||
saveAs(
|
|
||||||
new Blob([buffer], { type: 'application/octet-stream' }),
|
|
||||||
`${listFormCode}_export${selectedOnly ? '_selected' : ''}.xlsx`,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
const buffer = await workbook.csv.writeBuffer()
|
|
||||||
saveAs(
|
|
||||||
new Blob([buffer], { type: 'application/octet-stream' }),
|
|
||||||
`${listFormCode}_export${selectedOnly ? '_selected' : ''}.csv`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (format === 'pdf') {
|
|
||||||
const [jspdfMod] = await Promise.all([
|
|
||||||
import('jspdf'),
|
|
||||||
])
|
|
||||||
|
|
||||||
const JsPDFCtor = (jspdfMod as any).default ?? (jspdfMod as any).jsPDF
|
|
||||||
const doc = new JsPDFCtor({ orientation: 'landscape' })
|
|
||||||
|
|
||||||
// Header
|
|
||||||
const visibleColumns = gridDto?.columnFormats?.filter(
|
|
||||||
col => col.visible && col.dataType !== 'buttons'
|
|
||||||
) || []
|
|
||||||
|
|
||||||
let yPos = 10
|
|
||||||
doc.setFontSize(16)
|
|
||||||
doc.text(gridDto?.gridOptions.title || listFormCode, 10, yPos)
|
|
||||||
yPos += 10
|
|
||||||
|
|
||||||
// Table
|
|
||||||
doc.setFontSize(10)
|
|
||||||
const headers = visibleColumns.map(col => col.fieldName)
|
|
||||||
const rows = exportData.map(row =>
|
|
||||||
visibleColumns.map(col => String(row[col.fieldName!] || ''))
|
|
||||||
)
|
|
||||||
|
|
||||||
// Simple table rendering
|
|
||||||
doc.text(headers.join(' | '), 10, yPos)
|
|
||||||
yPos += 7
|
|
||||||
|
|
||||||
rows.forEach(row => {
|
|
||||||
if (yPos > 190) {
|
|
||||||
doc.addPage()
|
|
||||||
yPos = 10
|
|
||||||
}
|
|
||||||
doc.text(row.join(' | '), 10, yPos)
|
|
||||||
yPos += 7
|
|
||||||
})
|
|
||||||
|
|
||||||
doc.save(`${listFormCode}_export${selectedOnly ? '_selected' : ''}.pdf`)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Export error:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
|
||||||
if (data.length === 0) return
|
|
||||||
|
|
||||||
const keyField = gridDto?.gridOptions.keyFieldName
|
|
||||||
if (!keyField) return
|
|
||||||
|
|
||||||
switch (e.key) {
|
|
||||||
case 'ArrowRight':
|
|
||||||
e.preventDefault()
|
|
||||||
if (focusedCardIndex < data.length - 1) {
|
|
||||||
const newIndex = focusedCardIndex + 1
|
|
||||||
setFocusedCardIndex(newIndex)
|
|
||||||
cardRefs.current[newIndex]?.focus()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'ArrowLeft':
|
|
||||||
e.preventDefault()
|
|
||||||
if (focusedCardIndex > 0) {
|
|
||||||
const newIndex = focusedCardIndex - 1
|
|
||||||
setFocusedCardIndex(newIndex)
|
|
||||||
cardRefs.current[newIndex]?.focus()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'ArrowDown':
|
|
||||||
e.preventDefault()
|
|
||||||
if (focusedCardIndex + layoutCount < data.length) {
|
|
||||||
const newIndex = focusedCardIndex + layoutCount
|
|
||||||
setFocusedCardIndex(newIndex)
|
|
||||||
cardRefs.current[newIndex]?.focus()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'ArrowUp':
|
|
||||||
e.preventDefault()
|
|
||||||
if (focusedCardIndex - layoutCount >= 0) {
|
|
||||||
const newIndex = focusedCardIndex - layoutCount
|
|
||||||
setFocusedCardIndex(newIndex)
|
|
||||||
cardRefs.current[newIndex]?.focus()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'Enter':
|
|
||||||
e.preventDefault()
|
|
||||||
if (focusedCardIndex >= 0 && focusedCardIndex < data.length) {
|
|
||||||
const row = data[focusedCardIndex]
|
|
||||||
handleCardDoubleClick(row[keyField], row)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case ' ': // Space key for selection
|
|
||||||
e.preventDefault()
|
|
||||||
if (selectionMode !== 'none' && focusedCardIndex >= 0) {
|
|
||||||
const key = data[focusedCardIndex][keyField]
|
|
||||||
handleCardSelection(key, !selectedKeys.has(key))
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}, [data, gridDto, focusedCardIndex, layoutCount, handleCardDoubleClick, selectionMode, handleCardSelection, selectedKeys])
|
|
||||||
|
|
||||||
const onFilter = 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)
|
|
||||||
},
|
|
||||||
[gridDto, urlSearchParams, searchText],
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (gridDto) {
|
|
||||||
const dataSource = createSelectDataSource(
|
|
||||||
gridDto.gridOptions,
|
|
||||||
listFormCode,
|
|
||||||
urlSearchParams,
|
|
||||||
layoutTypes.card
|
|
||||||
)
|
|
||||||
setGridDataSource(dataSource)
|
|
||||||
|
|
||||||
//listFormStates
|
|
||||||
const listFormStates = states.find((a) => a.listFormCode === listFormCode)
|
|
||||||
if (listFormStates) {
|
|
||||||
setLayoutCount(listFormStates.cardLayoutColumn || 4)
|
|
||||||
} else {
|
|
||||||
setLayoutCount(gridDto.gridOptions.layoutDto.cardLayoutColumn || 4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [gridDto, listFormCode, urlSearchParams, createSelectDataSource, states])
|
|
||||||
|
|
||||||
const loadData = useCallback(() => {
|
|
||||||
if (!gridDataSource) return
|
|
||||||
setLoading(true)
|
|
||||||
setIsProgressiveRendering(false)
|
|
||||||
setRenderedCount(0)
|
|
||||||
|
|
||||||
const loadOptions = {
|
|
||||||
skip: (currentPage - 1) * pageSize,
|
|
||||||
take: pageSize,
|
|
||||||
requireTotalCount: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
gridDataSource
|
|
||||||
.load(loadOptions)
|
|
||||||
.then((res: any) => {
|
|
||||||
setData(res.data)
|
|
||||||
setTotalCount(res.totalCount || 0)
|
|
||||||
setLoading(false)
|
|
||||||
|
|
||||||
// Veri yüklendi, şimdi progressive rendering başlat
|
|
||||||
if (res.data && res.data.length > 0) {
|
|
||||||
setIsProgressiveRendering(true)
|
|
||||||
setRenderedCount(0)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setLoading(false)
|
|
||||||
})
|
|
||||||
}, [gridDataSource, currentPage, pageSize])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (gridDataSource) {
|
|
||||||
loadData()
|
|
||||||
|
|
||||||
// selectionMode = page ise sayfa değiştiğinde seçimi temizle
|
|
||||||
const selectAllMode = gridDto?.gridOptions?.selectionDto?.selectAllMode?.toLowerCase()
|
|
||||||
if (selectAllMode === 'page') {
|
|
||||||
setSelectedKeys(new Set())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [gridDataSource, loadData, currentPage])
|
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
setPageSizeOptions(allowedSizes.map((size) => ({ value: size, label: `${size} page` })))
|
|
||||||
}, [gridDto, listFormCode, searchParams])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Sadece yeni data yüklendiginde scroll yap - smooth yerine auto kullanılarak performans artırılıyor
|
|
||||||
if (data.length > 0 && currentPage === 1) {
|
|
||||||
window.scrollTo({ top: 0, behavior: 'auto' })
|
|
||||||
}
|
|
||||||
}, [currentPage])
|
|
||||||
|
|
||||||
// Progressive rendering effect - kartları küçük batch'lerde render et
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isProgressiveRendering || renderedCount >= data.length) {
|
|
||||||
if (renderedCount >= data.length && isProgressiveRendering) {
|
|
||||||
setIsProgressiveRendering(false)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Her seferde 3 kart render et - daha küçük batch size daha smooth experience
|
|
||||||
const batchSize = 3
|
|
||||||
const timeoutId = setTimeout(() => {
|
|
||||||
setRenderedCount(prev => Math.min(prev + batchSize, data.length))
|
|
||||||
}, 10) // 10ms gecikme - UI'ın nefes almasını sağlar
|
|
||||||
|
|
||||||
return () => clearTimeout(timeoutId)
|
|
||||||
}, [isProgressiveRendering, renderedCount, data.length])
|
|
||||||
|
|
||||||
if (!gridDto) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<WidgetGroup widgetGroups={gridDto.widgets || []} />
|
|
||||||
|
|
||||||
<Container>
|
|
||||||
<div
|
|
||||||
ref={containerRef}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
tabIndex={0}
|
|
||||||
className="outline-none"
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-center mb-2">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
{selectionMode === 'multiple' && gridDto?.gridOptions?.selectionDto?.allowSelectAll && (
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Checkbox
|
|
||||||
checked={
|
|
||||||
selectedKeys.size > 0 &&
|
|
||||||
(gridDto?.gridOptions?.selectionDto?.selectAllMode?.toLowerCase() === 'allpages'
|
|
||||||
? selectedKeys.size === totalCount
|
|
||||||
: selectedKeys.size === data.length)
|
|
||||||
}
|
|
||||||
onChange={(checked: boolean) => handleSelectAll(checked)}
|
|
||||||
className="cursor-pointer"
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-gray-600 dark:text-gray-300">
|
|
||||||
{selectedKeys.size > 0 ? `${selectedKeys.size} ${translate('::App.Platform.Card.Selected')}` : translate('::App.Platform.Card.SelectAll')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectionMode !== 'none' && selectedKeys.size > 0 && !gridDto?.gridOptions?.selectionDto?.allowSelectAll && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-sm text-gray-600 dark:text-gray-300">
|
|
||||||
{selectedKeys.size} {translate('::App.Platform.Card.Selected')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end items-center gap-2">
|
|
||||||
{/* Export Buttons */}
|
|
||||||
{gridDto?.gridOptions?.exportDto?.enabled && (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
variant="default"
|
|
||||||
onClick={() => handleExport('xlsx', selectionMode !== 'none' && selectedKeys.size > 0)}
|
|
||||||
title={selectionMode !== 'none' && selectedKeys.size > 0 ? `${translate('::App.Platform.Card.ExportSelectedToExcel')} (${selectedKeys.size})` : translate('::App.Platform.Card.ExportAllToExcel')}
|
|
||||||
>
|
|
||||||
📊 {selectionMode !== 'none' && selectedKeys.size > 0 ? `${translate('::App.Platform.Card.ExportSelectedToExcel')} (${selectedKeys.size})` : translate('::App.Platform.Card.ExportAllToExcel')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Sort Dropdown */}
|
|
||||||
{gridDto && gridDto.columnFormats && gridDto.columnFormats.length > 0 && (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<select
|
|
||||||
className="text-xs border rounded px-2 py-1 dark:bg-neutral-700 dark:text-white dark:border-neutral-600"
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e.target.value) {
|
|
||||||
toggleSort(e.target.value)
|
|
||||||
} else {
|
|
||||||
// "Sıralama Yok" seçildiğinde temizle
|
|
||||||
const newParams = new URLSearchParams(urlSearchParams.toString())
|
|
||||||
newParams.delete('sort')
|
|
||||||
setUrlSearchParams(newParams)
|
|
||||||
setSortColumn(null)
|
|
||||||
setSortOrder('asc')
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
value={sortColumn || ''}
|
|
||||||
>
|
|
||||||
<option value="">{translate('::App.Platform.Card.NoSorting')}</option>
|
|
||||||
{gridDto.columnFormats
|
|
||||||
.filter((col) => col.visible && col.dataType !== 'buttons')
|
|
||||||
.map((col) => (
|
|
||||||
<option key={col.fieldName} value={col.fieldName}>
|
|
||||||
{col.fieldName}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
{sortColumn && (
|
|
||||||
<button
|
|
||||||
onClick={() => toggleSort(sortColumn)}
|
|
||||||
className="text-gray-600 dark:text-gray-300 hover:text-blue-500 p-1"
|
|
||||||
title={translate('::App.Platform.Card.ChangeSortDirection')}
|
|
||||||
>
|
|
||||||
{sortOrder === 'asc' ? <FaSortAmountUp className="w-3 h-3" /> : <FaSortAmountDown className="w-3 h-3" />}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<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={translate('::App.Search')}
|
|
||||||
value={searchText}
|
|
||||||
onChange={(e) => setSearchText(e.target.value)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
onFilter(e.currentTarget.value)
|
|
||||||
setPrevValue(e.currentTarget.value.trim()) // Enter ile tetiklenirse güncelle
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onBlur={(e) => {
|
|
||||||
const newValue = e.currentTarget.value.trim()
|
|
||||||
|
|
||||||
// 1. Değer değişmemişse => hiçbir şey yapma
|
|
||||||
if (newValue === prevValue) return
|
|
||||||
|
|
||||||
// 2. Yeni değer boş, ama eskiden değer vardı => filtre temizle
|
|
||||||
// 3. Yeni değer dolu ve eskisinden farklı => filtre uygula
|
|
||||||
onFilter(newValue)
|
|
||||||
setPrevValue(newValue)
|
|
||||||
}}
|
|
||||||
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)
|
|
||||||
setStates({ listFormCode, layout: 'card', cardLayoutColumn: 1 })
|
|
||||||
}}
|
|
||||||
title={translate('::App.Platform.Card.ShowIn1Column')}
|
|
||||||
>
|
|
||||||
1
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
variant={layoutCount === 2 ? 'solid' : 'default'}
|
|
||||||
className="text-sm"
|
|
||||||
onClick={() => {
|
|
||||||
setLayoutCount(2)
|
|
||||||
setStates({ listFormCode, layout: 'card', cardLayoutColumn: 2 })
|
|
||||||
}}
|
|
||||||
title={translate('::App.Platform.Card.ShowIn2Columns')}
|
|
||||||
>
|
|
||||||
2
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
variant={layoutCount === 3 ? 'solid' : 'default'}
|
|
||||||
className="text-sm"
|
|
||||||
onClick={() => {
|
|
||||||
setLayoutCount(3)
|
|
||||||
setStates({ listFormCode, layout: 'card', cardLayoutColumn: 3 })
|
|
||||||
}}
|
|
||||||
title={translate('::App.Platform.Card.ShowIn3Columns')}
|
|
||||||
>
|
|
||||||
3
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
variant={layoutCount === 4 ? 'solid' : 'default'}
|
|
||||||
className="text-sm"
|
|
||||||
onClick={() => {
|
|
||||||
setLayoutCount(4)
|
|
||||||
setStates({ listFormCode, layout: 'card', cardLayoutColumn: 4 })
|
|
||||||
}}
|
|
||||||
title={translate('::App.Platform.Card.ShowIn4Columns')}
|
|
||||||
>
|
|
||||||
4
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
variant={layoutCount === 5 ? 'solid' : 'default'}
|
|
||||||
className="text-sm"
|
|
||||||
onClick={() => {
|
|
||||||
setLayoutCount(5)
|
|
||||||
setStates({ listFormCode, layout: 'card', cardLayoutColumn: 5 })
|
|
||||||
}}
|
|
||||||
title={translate('::App.Platform.Card.ShowIn5Columns')}
|
|
||||||
>
|
|
||||||
5
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{checkPermission(gridDto?.gridOptions.permissionDto.u) && (
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
variant={'default'}
|
|
||||||
className="text-sm"
|
|
||||||
onClick={() => {
|
|
||||||
window.open(
|
|
||||||
ROUTES_ENUM.protected.saas.listFormManagement.edit.replace(
|
|
||||||
':listFormCode',
|
|
||||||
listFormCode,
|
|
||||||
),
|
|
||||||
isPwaMode ? '_self' : '_blank',
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
title={translate('::ListForms.ListForm.Manage')}
|
|
||||||
>
|
|
||||||
<FaCog className="w-3 h-3" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{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">
|
|
||||||
{translate('::App.Platform.Card.NoRecordsFound')}
|
|
||||||
</h3>
|
|
||||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{translate('::App.Platform.Card.TryDifferentFilters')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className="bg-transparent grid gap-4"
|
|
||||||
style={{
|
|
||||||
gridTemplateColumns: `repeat(${layoutCount}, minmax(0, 1fr))`
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{gridDataSource &&
|
|
||||||
data.slice(0, isProgressiveRendering ? renderedCount : data.length).map((row, idx) => {
|
|
||||||
const keyField = gridDto.gridOptions.keyFieldName
|
|
||||||
const rowId = row[keyField!]
|
|
||||||
const isSelected = selectedKeys.has(rowId)
|
|
||||||
const isFocused = focusedCardIndex === idx
|
|
||||||
const isHovered = hoveredCardKey === rowId
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CardItem
|
|
||||||
ref={(el) => (cardRefs.current[idx] = el)}
|
|
||||||
isSubForm={true}
|
|
||||||
key={rowId || idx}
|
|
||||||
row={row}
|
|
||||||
gridDto={gridDto}
|
|
||||||
listFormCode={listFormCode}
|
|
||||||
dataSource={gridDataSource}
|
|
||||||
refreshData={loadData}
|
|
||||||
getCachedLookupDataSource={memoizedGetLookupDataSource}
|
|
||||||
isSelected={isSelected}
|
|
||||||
isFocused={isFocused}
|
|
||||||
isHovered={isHovered}
|
|
||||||
onSelectionChange={(checked) => handleCardSelection(rowId, checked)}
|
|
||||||
onClick={(e) => handleCardClick(rowId, idx, e)}
|
|
||||||
onDoubleClick={() => handleCardDoubleClick(rowId, row)}
|
|
||||||
onMouseEnter={() => setHoveredCardKey(rowId)}
|
|
||||||
onMouseLeave={() => setHoveredCardKey(null)}
|
|
||||||
tabIndex={isFocused ? 0 : -1}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Progressive rendering sırasında loading göster */}
|
|
||||||
{isProgressiveRendering && renderedCount < data.length && (
|
|
||||||
<div className="text-center py-4">
|
|
||||||
<div className="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500"></div>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
|
||||||
{translate('::App.Platform.Card.Loading')} ({renderedCount}/{data.length})
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{gridDto.gridOptions.pagerOptionDto?.visible && totalCount > pageSize && (
|
|
||||||
<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">
|
|
||||||
{translate('::App.Platform.Card.TotalRecords').replace('{0}', totalCount.toString())}
|
|
||||||
</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>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Card
|
|
||||||
|
|
@ -1,314 +0,0 @@
|
||||||
import { EditingFormItemDto, GridDto, PlatformEditorTypes } from "@/proxy/form/models"
|
|
||||||
import { useLocalization } from "@/utils/hooks/useLocalization"
|
|
||||||
import { usePermission } from "@/utils/hooks/usePermission"
|
|
||||||
import { usePWA } from "@/utils/hooks/usePWA"
|
|
||||||
import CustomStore from "devextreme/data/custom_store"
|
|
||||||
import { useMemo, useRef, useState, forwardRef, memo, useEffect } from "react"
|
|
||||||
import { useNavigate } from "react-router-dom"
|
|
||||||
import { GroupItem } from 'devextreme/ui/form'
|
|
||||||
import { PermissionResults, SimpleItemWithColData } from "../form/types"
|
|
||||||
import { captionize } from 'devextreme/core/utils/inflector'
|
|
||||||
import { ROUTES_ENUM } from "@/routes/route.constant"
|
|
||||||
import FormButtons from "../form/FormButtons"
|
|
||||||
import FormDevExpress from "../form/FormDevExpress"
|
|
||||||
import { FormRef } from "devextreme-react/cjs/form"
|
|
||||||
import { Checkbox } from "@/components/ui"
|
|
||||||
import classNames from "classnames"
|
|
||||||
|
|
||||||
interface CardItemProps {
|
|
||||||
isSubForm?: boolean
|
|
||||||
row: any
|
|
||||||
gridDto: GridDto
|
|
||||||
listFormCode: string
|
|
||||||
dataSource: CustomStore
|
|
||||||
refreshData: () => void
|
|
||||||
getCachedLookupDataSource: (editorOptions: any, colData: any, row?: any) => any
|
|
||||||
// Selection and interaction props
|
|
||||||
isSelected?: boolean
|
|
||||||
isFocused?: boolean
|
|
||||||
isHovered?: boolean
|
|
||||||
onSelectionChange?: (checked: boolean) => void
|
|
||||||
onClick?: (e: React.MouseEvent) => void
|
|
||||||
onDoubleClick?: () => void
|
|
||||||
onMouseEnter?: () => void
|
|
||||||
onMouseLeave?: () => void
|
|
||||||
tabIndex?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const CardItem = forwardRef<HTMLDivElement, CardItemProps>((
|
|
||||||
{
|
|
||||||
isSubForm,
|
|
||||||
row,
|
|
||||||
gridDto,
|
|
||||||
listFormCode,
|
|
||||||
dataSource,
|
|
||||||
refreshData,
|
|
||||||
getCachedLookupDataSource,
|
|
||||||
isSelected = false,
|
|
||||||
isFocused = false,
|
|
||||||
isHovered = false,
|
|
||||||
onSelectionChange,
|
|
||||||
onClick,
|
|
||||||
onDoubleClick,
|
|
||||||
onMouseEnter,
|
|
||||||
onMouseLeave,
|
|
||||||
tabIndex = -1,
|
|
||||||
},
|
|
||||||
ref
|
|
||||||
) => {
|
|
||||||
// Selection mode'u gridDto'dan al
|
|
||||||
const selectionMode = useMemo(() => {
|
|
||||||
if (!gridDto?.gridOptions?.selectionDto?.mode) return 'none'
|
|
||||||
const mode = gridDto.gridOptions.selectionDto.mode.toLowerCase()
|
|
||||||
if (mode === 'single') return 'single'
|
|
||||||
if (mode === 'multiple') return 'multiple'
|
|
||||||
return 'none'
|
|
||||||
}, [gridDto])
|
|
||||||
const [formData, setFormData] = useState(row)
|
|
||||||
const refForm = useRef<FormRef>(null)
|
|
||||||
const cardElementRef = useRef<HTMLDivElement | null>(null)
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const { translate } = useLocalization()
|
|
||||||
const { checkPermission } = usePermission()
|
|
||||||
const isPwaMode = usePWA()
|
|
||||||
|
|
||||||
// Lazy load form with Intersection Observer - sadece görünür kartları render et
|
|
||||||
const [shouldRenderForm, setShouldRenderForm] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const element = cardElementRef.current
|
|
||||||
if (!element) return
|
|
||||||
|
|
||||||
// Intersection Observer ile görünürlük kontrolü
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
// Kart görünür hale geldiğinde form'u render et
|
|
||||||
setShouldRenderForm(true)
|
|
||||||
// Bir kez render edildikten sonra observer'ı kaldır
|
|
||||||
observer.disconnect()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rootMargin: '50px', // 50px önceden yüklemeye başla
|
|
||||||
threshold: 0.01, // %1 görünür olduğunda tetikle
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
observer.observe(element)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
observer.disconnect()
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const keyField = gridDto.gridOptions.keyFieldName
|
|
||||||
const rowId = row[keyField!]
|
|
||||||
|
|
||||||
// Form Items - memoized to prevent recalculation on every render
|
|
||||||
const formItems: GroupItem[] = useMemo(() => {
|
|
||||||
if (!gridDto) return []
|
|
||||||
|
|
||||||
return gridDto.gridOptions.editingFormDto
|
|
||||||
?.sort((a, b) => (a.order >= b.order ? 1 : -1))
|
|
||||||
.map((e) => {
|
|
||||||
return {
|
|
||||||
itemType: e.itemType,
|
|
||||||
colCount: e.colCount,
|
|
||||||
colSpan: e.colSpan,
|
|
||||||
caption: e.caption,
|
|
||||||
items: e.items
|
|
||||||
?.sort((a, b) => (a.order >= b.order ? 1 : -1))
|
|
||||||
.map((i: EditingFormItemDto) => {
|
|
||||||
let editorOptions = {}
|
|
||||||
const colData = gridDto.columnFormats.find((x) => x.fieldName === i.dataField)
|
|
||||||
|
|
||||||
try {
|
|
||||||
editorOptions = i.editorOptions && JSON.parse(i.editorOptions)
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
const item: SimpleItemWithColData = {
|
|
||||||
canRead: colData?.canRead ?? false,
|
|
||||||
canUpdate: colData?.canUpdate ?? false,
|
|
||||||
canCreate: colData?.canCreate ?? false,
|
|
||||||
canExport: colData?.canExport ?? false,
|
|
||||||
dataField: i.dataField,
|
|
||||||
name: i.dataField,
|
|
||||||
label: { text: colData?.captionName ? translate('::' + colData?.captionName) : i.dataField },
|
|
||||||
editorType2: i.editorType2,
|
|
||||||
editorType:
|
|
||||||
i.editorType2 == PlatformEditorTypes.dxGridBox ? 'dxDropDownBox' : i.editorType2,
|
|
||||||
colSpan: i.colSpan,
|
|
||||||
isRequired: i.isRequired,
|
|
||||||
editorOptions: {
|
|
||||||
...editorOptions,
|
|
||||||
...(colData?.lookupDto?.dataSourceType
|
|
||||||
? {
|
|
||||||
dataSource: getCachedLookupDataSource(colData?.editorOptions, colData),
|
|
||||||
valueExpr: colData?.lookupDto?.valueExpr?.toLowerCase(),
|
|
||||||
displayExpr: colData?.lookupDto?.displayExpr?.toLowerCase(),
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
colData,
|
|
||||||
tagBoxOptions: i.tagBoxOptions,
|
|
||||||
gridBoxOptions: i.gridBoxOptions,
|
|
||||||
editorScript: i.editorScript,
|
|
||||||
}
|
|
||||||
if (i.dataField.indexOf(':') >= 0) {
|
|
||||||
item.label = { text: captionize(i.dataField.split(':')[1]) }
|
|
||||||
}
|
|
||||||
item.editorOptions = {
|
|
||||||
...item.editorOptions,
|
|
||||||
readOnly: true,
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
}),
|
|
||||||
} as GroupItem
|
|
||||||
})
|
|
||||||
}, [gridDto, getCachedLookupDataSource, translate])
|
|
||||||
|
|
||||||
const permissionResults: PermissionResults = {
|
|
||||||
c:
|
|
||||||
gridDto?.gridOptions.editingOptionDto.allowAdding === true &&
|
|
||||||
checkPermission(gridDto?.gridOptions.permissionDto.c),
|
|
||||||
r: checkPermission(gridDto?.gridOptions.permissionDto.r),
|
|
||||||
u:
|
|
||||||
gridDto?.gridOptions.editingOptionDto.allowUpdating === true &&
|
|
||||||
checkPermission(gridDto?.gridOptions.permissionDto.u),
|
|
||||||
d:
|
|
||||||
gridDto?.gridOptions.editingOptionDto.allowDeleting === true &&
|
|
||||||
checkPermission(gridDto?.gridOptions.permissionDto.d),
|
|
||||||
e: checkPermission(gridDto?.gridOptions.permissionDto.e),
|
|
||||||
i: checkPermission(gridDto?.gridOptions.permissionDto.i),
|
|
||||||
}
|
|
||||||
|
|
||||||
const onActionEdit = () => {
|
|
||||||
navigate(
|
|
||||||
ROUTES_ENUM.protected.admin.formEdit
|
|
||||||
.replace(':listFormCode', listFormCode)
|
|
||||||
.replace(':id', rowId!),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onActionNew = () => {
|
|
||||||
navigate(ROUTES_ENUM.protected.admin.formNew.replace(':listFormCode', listFormCode))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={(element) => {
|
|
||||||
// Forward ref to parent
|
|
||||||
if (typeof ref === 'function') {
|
|
||||||
ref(element)
|
|
||||||
} else if (ref) {
|
|
||||||
ref.current = element
|
|
||||||
}
|
|
||||||
// Also save to local ref for Intersection Observer
|
|
||||||
cardElementRef.current = element
|
|
||||||
}}
|
|
||||||
tabIndex={tabIndex}
|
|
||||||
onClick={onClick}
|
|
||||||
onDoubleClick={onDoubleClick}
|
|
||||||
onMouseEnter={onMouseEnter}
|
|
||||||
onMouseLeave={onMouseLeave}
|
|
||||||
className={classNames(
|
|
||||||
"bg-white dark:bg-neutral-800 border flex flex-col transition-all duration-200 outline-none",
|
|
||||||
{
|
|
||||||
"border-blue-500 shadow-lg ring-2 ring-blue-300": isSelected,
|
|
||||||
"border-blue-400 shadow-md": isFocused && !isSelected,
|
|
||||||
"border-gray-300 dark:border-neutral-600": !isSelected && !isFocused,
|
|
||||||
"shadow-md transform scale-[1.02]": isHovered,
|
|
||||||
"cursor-pointer": selectionMode !== 'none',
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
role="article"
|
|
||||||
aria-selected={isSelected}
|
|
||||||
aria-label={`Card ${rowId}`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`flex items-center pt-2 px-2 gap-2 ${isSubForm ? 'justify-between' : 'justify-between'}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{selectionMode !== 'none' && onSelectionChange && (
|
|
||||||
<div
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
onSelectionChange(!isSelected)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
checked={isSelected}
|
|
||||||
onChange={(checked: boolean) => {
|
|
||||||
onSelectionChange(checked)
|
|
||||||
}}
|
|
||||||
className="cursor-pointer"
|
|
||||||
aria-label="Select card"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isSubForm && (
|
|
||||||
<h3>{translate('::' + gridDto?.gridOptions.title)}</h3>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{permissionResults && (
|
|
||||||
<FormButtons
|
|
||||||
isSubForm={true}
|
|
||||||
mode="view"
|
|
||||||
listFormCode={listFormCode}
|
|
||||||
id={rowId}
|
|
||||||
gridDto={gridDto}
|
|
||||||
commandColumnData={{ buttons: [] }}
|
|
||||||
dataSource={dataSource}
|
|
||||||
permissions={permissionResults}
|
|
||||||
handleSubmit={() => ({})}
|
|
||||||
refreshData={refreshData}
|
|
||||||
getSelectedRowKeys={() => [rowId]}
|
|
||||||
getSelectedRowsData={() => [row]}
|
|
||||||
getFilter={() => null}
|
|
||||||
onActionEdit={onActionEdit}
|
|
||||||
onActionNew={onActionNew}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex-grow">
|
|
||||||
{shouldRenderForm ? (
|
|
||||||
<FormDevExpress
|
|
||||||
listFormCode={listFormCode}
|
|
||||||
mode="view"
|
|
||||||
refForm={refForm}
|
|
||||||
formData={formData}
|
|
||||||
formItems={formItems}
|
|
||||||
setFormData={setFormData}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="p-4">
|
|
||||||
<div className="animate-pulse space-y-2">
|
|
||||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
|
|
||||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/2"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
CardItem.displayName = 'CardItem'
|
|
||||||
|
|
||||||
// Memoize to prevent unnecessary re-renders when parent re-renders
|
|
||||||
export default memo(CardItem, (prevProps, nextProps) => {
|
|
||||||
// Custom comparison to prevent re-render when not needed
|
|
||||||
return (
|
|
||||||
prevProps.row === nextProps.row &&
|
|
||||||
prevProps.isSelected === nextProps.isSelected &&
|
|
||||||
prevProps.isFocused === nextProps.isFocused &&
|
|
||||||
prevProps.isHovered === nextProps.isHovered &&
|
|
||||||
prevProps.gridDto === nextProps.gridDto &&
|
|
||||||
prevProps.listFormCode === nextProps.listFormCode
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
@ -649,7 +649,7 @@ const CardView = (props: CardViewProps) => {
|
||||||
gridDto.gridOptions,
|
gridDto.gridOptions,
|
||||||
listFormCode,
|
listFormCode,
|
||||||
searchParams,
|
searchParams,
|
||||||
layoutTypes.cardView,
|
layoutTypes.card,
|
||||||
columnData
|
columnData
|
||||||
)
|
)
|
||||||
setCardViewDataSource(ds)
|
setCardViewDataSource(ds)
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import { getList } from '@/services/form.service'
|
||||||
import { useCurrentMenuIcon } from '@/utils/hooks/useCurrentMenuIcon'
|
import { useCurrentMenuIcon } from '@/utils/hooks/useCurrentMenuIcon'
|
||||||
import { ListViewLayoutType } from '../admin/listForm/edit/types'
|
import { ListViewLayoutType } from '../admin/listForm/edit/types'
|
||||||
import Chart from './Chart'
|
import Chart from './Chart'
|
||||||
import Card from './Card'
|
|
||||||
import CardView from './CardView'
|
import CardView from './CardView'
|
||||||
import GanttView from './GanttView'
|
import GanttView from './GanttView'
|
||||||
import SchedulerView from './SchedulerView'
|
import SchedulerView from './SchedulerView'
|
||||||
|
|
@ -165,21 +164,7 @@ const List = () => {
|
||||||
setViewMode('card')
|
setViewMode('card')
|
||||||
setStates({ listFormCode, layout: 'card' })
|
setStates({ listFormCode, layout: 'card' })
|
||||||
}}
|
}}
|
||||||
title="Kart Görünümü"
|
title="Card Görünümü"
|
||||||
>
|
|
||||||
<FaTh className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{gridDto?.gridOptions?.layoutDto.cardView && (
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
variant={viewMode === 'cardView' ? 'solid' : 'default'}
|
|
||||||
onClick={() => {
|
|
||||||
setViewMode('cardView')
|
|
||||||
setStates({ listFormCode, layout: 'cardView' })
|
|
||||||
}}
|
|
||||||
title="CardView Görünümü"
|
|
||||||
>
|
>
|
||||||
<FaIdCard className="w-4 h-4" />
|
<FaIdCard className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -245,13 +230,6 @@ const List = () => {
|
||||||
gridDto={gridDto}
|
gridDto={gridDto}
|
||||||
/>
|
/>
|
||||||
) : viewMode === 'card' ? (
|
) : viewMode === 'card' ? (
|
||||||
<Card
|
|
||||||
listFormCode={listFormCode}
|
|
||||||
searchParams={searchParams}
|
|
||||||
isSubForm={false}
|
|
||||||
gridDto={gridDto}
|
|
||||||
/>
|
|
||||||
) : viewMode === 'cardView' ? (
|
|
||||||
<CardView
|
<CardView
|
||||||
listFormCode={listFormCode}
|
listFormCode={listFormCode}
|
||||||
searchParams={searchParams}
|
searchParams={searchParams}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue