sozsoft-platform/ui/src/views/list/List.tsx
2026-08-28 16:44:14 +03:00

449 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, {
Suspense,
startTransition,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import '@/utils/registerDevExtremeEditors'
import { useParams, useSearchParams } from 'react-router-dom'
import classNames from 'classnames'
import Container from '@/components/shared/Container'
import { Badge, Button, Dialog } from '@/components/ui'
import RichText from '@/components/shared/RichText'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { useCurrentMenuIcon } from '@/utils/hooks/useCurrentMenuIcon'
import { useStoreActions, useStoreState } from '@/store/store'
import { GridDto } from '@/proxy/form/models'
import { getList } from '@/services/form.service'
import { ListViewLayoutType } from '../admin/listForm/edit/types'
import {
FaChartArea,
FaThLarge,
FaList,
FaSitemap,
FaTable,
FaCalendarAlt,
FaProjectDiagram,
FaTasks,
FaInfoCircle,
} from 'react-icons/fa'
/* =======================
🔥 LAZY VIEW IMPORTS
======================= */
const Grid = React.lazy(() => import('./Grid'))
const CardView = React.lazy(() => import('./CardView'))
const Pivot = React.lazy(() => import('./Pivot'))
const Tree = React.lazy(() => import('./Tree'))
const Chart = React.lazy(() => import('./Chart'))
const GanttView = React.lazy(() => import('./GanttView'))
const SchedulerView = React.lazy(() => import('./SchedulerView'))
const TodoBoard = React.lazy(() => import('./TodoBoard'))
function isLayoutValid(dto: GridDto, layout: ListViewLayoutType | undefined): boolean {
if (!layout || layout === 'grid') return true
if (layout === 'card') return true
if (layout === 'pivot') return !!dto.gridOptions?.layoutDto?.pivot
if (layout === 'chart') return !!dto.gridOptions?.layoutDto?.chart
if (layout === 'tree')
return !!(
dto.gridOptions?.layoutDto?.tree &&
dto.gridOptions?.treeOptionDto?.keyExpr &&
dto.gridOptions?.treeOptionDto?.parentIdExpr
)
if (layout === 'gantt')
return !!(
dto.gridOptions?.layoutDto?.gantt &&
dto.gridOptions?.ganttOptionDto?.keyExpr &&
dto.gridOptions?.ganttOptionDto?.parentIdExpr &&
dto.gridOptions?.ganttOptionDto?.titleExpr &&
dto.gridOptions?.ganttOptionDto?.startExpr &&
dto.gridOptions?.ganttOptionDto?.endExpr &&
dto.gridOptions?.ganttOptionDto?.progressExpr
)
if (layout === 'scheduler')
return !!(
dto.gridOptions?.layoutDto?.scheduler &&
dto.gridOptions?.schedulerOptionDto?.textExpr &&
dto.gridOptions?.schedulerOptionDto?.startDateExpr &&
dto.gridOptions?.schedulerOptionDto?.endDateExpr
)
if (layout === 'todo')
return !!(
dto.gridOptions?.layoutDto?.todo &&
dto.gridOptions?.todoOptionDto?.titleExpr &&
dto.gridOptions?.todoOptionDto?.statusExpr
)
return false
}
interface ListProps {
listFormCode?: string
gridDto?: GridDto
isSubForm?: boolean
onRefreshGridDto?: () => Promise<void> | void
/**
* Filter carrier of a list embedded in a page. A standalone list reads the
* page address, but an embedded one is filtered by its host — the designer
* builds these from static values, the URL or a Form record.
*/
searchParams?: URLSearchParams
}
const List: React.FC<ListProps> = ({
listFormCode: externalListFormCode,
gridDto: externalGridDto,
isSubForm = false,
onRefreshGridDto,
searchParams: externalSearchParams,
}) => {
const { listFormCode: routeListFormCode = '' } = useParams()
const listFormCode = externalListFormCode || routeListFormCode
const [routeSearchParams] = useSearchParams()
const searchParams = externalSearchParams ?? routeSearchParams
const { translate } = useLocalization()
const mode = useStoreState((state) => state.theme.mode)
const MenuIcon = useCurrentMenuIcon('w-5 h-5')
const { states } = useStoreState((state) => state.admin.lists)
const { setStates } = useStoreActions((a) => a.admin.lists)
const activeListFormCodeRef = useRef(listFormCode)
activeListFormCodeRef.current = listFormCode
const [gridDtoState, setGridDtoState] = useState<{
listFormCode: string
gridDto: GridDto | null
}>({ listFormCode: '', gridDto: null })
const gridDto =
externalGridDto ??
(gridDtoState.listFormCode === listFormCode ? gridDtoState.gridDto : null)
const [viewModeState, setViewModeState] = useState<{
listFormCode: string
layoutType?: ListViewLayoutType
}>({ listFormCode: '' })
const viewMode = viewModeState.listFormCode === listFormCode ? viewModeState.layoutType : undefined
/* =======================
🔹 GRID DTO FETCH
======================= */
const refreshGridDto = useCallback(async () => {
if (!listFormCode) return
if (onRefreshGridDto) {
await onRefreshGridDto()
return
}
const requestedListFormCode = listFormCode
try {
const response = await getList({ listFormCode: requestedListFormCode })
if (activeListFormCodeRef.current !== requestedListFormCode) return
setGridDtoState({ listFormCode: requestedListFormCode, gridDto: response.data })
} catch (err) {
console.error('GridDto load error:', err)
}
}, [listFormCode, onRefreshGridDto])
useEffect(() => {
if (!externalGridDto) void refreshGridDto()
}, [externalGridDto, refreshGridDto])
/* =======================
🔹 INITIAL VIEW MODE
======================= */
useEffect(() => {
if (!gridDto) return
const savedLayout = states.find((s) => s.listFormCode === listFormCode)?.layout
const defaultLayout = gridDto.gridOptions?.layoutDto?.defaultLayout
const candidate = savedLayout ?? defaultLayout
setViewModeState({
listFormCode,
layoutType: isLayoutValid(gridDto, candidate) ? candidate : 'grid',
})
}, [gridDto, states, listFormCode])
/* =======================
🔹 VIEW SWITCH HANDLER
======================= */
const setLayout = useCallback(
(layout: ListViewLayoutType) => {
startTransition(() => {
setViewModeState({ listFormCode, layoutType: layout })
})
setStates({ listFormCode, layout })
},
[listFormCode, setStates],
)
/* =======================
🔹 PRELOAD MAP (HOVER)
======================= */
const preload = useMemo(
() => ({
grid: () => import('./Grid'),
card: () => import('./CardView'),
pivot: () => import('./Pivot'),
tree: () => import('./Tree'),
chart: () => import('./Chart'),
gantt: () => import('./GanttView'),
scheduler: () => import('./SchedulerView'),
todo: () => import('./TodoBoard'),
}),
[],
)
/* =======================
🔹 HEADER DESCRIPTION (LISTFORM DESCRIPTION)
======================= */
// Description bir dil anahtarıdır; karşılığı yoksa translate anahtarı geri döndürür,
// o durumda kullanıcıya anahtar gösterilmez.
const [descriptionOpen, setDescriptionOpen] = useState(false)
const description = useMemo(() => {
const key = gridDto?.gridOptions?.description
if (!key) return null
const text = translate('::' + key)
return text && text !== key ? text : null
}, [gridDto?.gridOptions?.description, translate])
if (!listFormCode || !gridDto || !viewMode) {
return null
}
return (
<Container>
{/* =======================
HEADER
======================= */}
<div
className={classNames(
'flex items-center gap-2 pb-1 border-b',
mode === 'light' ? 'border-gray-200' : 'border-neutral-700',
)}
>
{MenuIcon}
{description ? (
<button
className="flex items-center gap-1 text-sm font-medium hover:underline"
title={translate('::App.Platform.ShowDescription')}
type="button"
onClick={() => setDescriptionOpen(true)}
>
<h4 className="text-sm font-medium">{translate('::' + gridDto.gridOptions.title)}</h4>
<FaInfoCircle className="text-xs opacity-60" />
</button>
) : (
<h4 className="text-sm font-medium">{translate('::' + gridDto.gridOptions.title)}</h4>
)}
<Badge content={viewMode} />
{/* =======================
VIEW BUTTONS
======================= */}
<div className="flex gap-1 ml-auto">
{gridDto?.gridOptions?.layoutDto.todo &&
gridDto?.gridOptions?.todoOptionDto?.titleExpr &&
gridDto?.gridOptions?.todoOptionDto?.statusExpr && (
<Button
size="sm"
icon={<FaTasks />}
variant={viewMode === 'todo' ? 'solid' : 'default'}
onClick={() => setLayout('todo')}
onMouseEnter={() => preload.todo()}
title={translate('::App.Platform.TabTodo')}
/>
)}
{gridDto?.gridOptions?.layoutDto.scheduler &&
gridDto?.gridOptions?.schedulerOptionDto?.textExpr &&
gridDto?.gridOptions?.schedulerOptionDto?.startDateExpr && (
<Button
size="sm"
icon={<FaCalendarAlt />}
variant={viewMode === 'scheduler' ? 'solid' : 'default'}
onClick={() => setLayout('scheduler')}
onMouseEnter={() => preload.scheduler()}
title={translate('::App.Platform.TabScheduler')}
></Button>
)}
{gridDto?.gridOptions?.layoutDto.gantt &&
gridDto?.gridOptions?.ganttOptionDto?.parentIdExpr &&
gridDto?.gridOptions?.ganttOptionDto?.titleExpr && (
<Button
size="sm"
icon={<FaProjectDiagram />}
variant={viewMode === 'gantt' ? 'solid' : 'default'}
onClick={() => setLayout('gantt')}
onMouseEnter={() => preload.gantt()}
title={translate('::App.Platform.TabGantt')}
></Button>
)}
{gridDto?.gridOptions?.layoutDto.tree &&
gridDto?.gridOptions?.treeOptionDto?.parentIdExpr && (
<Button
size="sm"
icon={<FaSitemap />}
variant={viewMode === 'tree' ? 'solid' : 'default'}
onClick={() => setLayout('tree')}
onMouseEnter={() => preload.tree()}
title={translate('::App.Platform.TabTree')}
></Button>
)}
<Button
size="sm"
icon={<FaList />}
variant={viewMode === 'grid' ? 'solid' : 'default'}
onClick={() => setLayout('grid')}
onMouseEnter={() => preload.grid()}
title={translate('::App.ListFormEdit.TabGrid')}
></Button>
{gridDto.gridOptions.layoutDto.card && (
<Button
size="sm"
icon={<FaThLarge />}
variant={viewMode === 'card' ? 'solid' : 'default'}
onClick={() => setLayout('card')}
onMouseEnter={() => preload.card()}
title={translate('::App.ListFormEdit.TabCard')}
></Button>
)}
{gridDto.gridOptions.layoutDto.pivot && (
<Button
size="sm"
icon={<FaTable />}
variant={viewMode === 'pivot' ? 'solid' : 'default'}
onClick={() => setLayout('pivot')}
onMouseEnter={() => preload.pivot()}
title={translate('::App.Platform.TabPivot')}
></Button>
)}
{gridDto.gridOptions.layoutDto.chart && (
<Button
size="sm"
icon={<FaChartArea />}
variant={viewMode === 'chart' ? 'solid' : 'default'}
onClick={() => setLayout('chart')}
onMouseEnter={() => preload.chart()}
title={translate('::App.Platform.Chart')}
></Button>
)}
</div>
</div>
{/* =======================
VIEW RENDER
======================= */}
<Suspense fallback={<div className="p-4 text-sm">{translate('::App.Platform.LoadingWithThreeDot')}</div>}>
{viewMode === 'grid' && (
<Grid
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={isSubForm}
gridDto={gridDto}
/>
)}
{viewMode === 'card' && (
<CardView
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={isSubForm}
gridDto={gridDto}
/>
)}
{viewMode === 'pivot' && (
<Pivot
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={isSubForm}
gridDto={gridDto}
refreshGridDto={refreshGridDto}
/>
)}
{viewMode === 'tree' && (
<Tree
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={isSubForm}
gridDto={gridDto}
/>
)}
{viewMode === 'scheduler' && (
<SchedulerView
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={isSubForm}
gridDto={gridDto}
/>
)}
{viewMode === 'gantt' && (
<GanttView
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={isSubForm}
gridDto={gridDto}
/>
)}
{viewMode === 'todo' && (
<TodoBoard
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={isSubForm}
gridDto={gridDto}
/>
)}
{viewMode === 'chart' && (
<Chart
id={gridDto?.gridOptions.id as string}
listFormCode={listFormCode}
filter={searchParams.toString()}
isSubForm={true}
gridDto={gridDto}
refreshGridDto={refreshGridDto}
/>
)}
</Suspense>
{description && (
<Dialog
isOpen={descriptionOpen}
width={860}
onClose={() => setDescriptionOpen(false)}
onRequestClose={() => setDescriptionOpen(false)}
>
<Dialog.Header className="-mt-5 p-2 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-800 dark:text-gray-100">
{translate('::' + gridDto.gridOptions.title)}
</h2>
</Dialog.Header>
<Dialog.Body className="mt-2 max-h-[70vh] select-text overflow-y-auto pr-1">
<RichText text={description} />
</Dialog.Body>
</Dialog>
)}
</Container>
)
}
export default List