449 lines
15 KiB
TypeScript
449 lines
15 KiB
TypeScript
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
|