sozsoft-platform/ui/src/views/list/List.tsx

450 lines
15 KiB
TypeScript
Raw Normal View History

import React, {
Suspense,
startTransition,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
2026-07-11 20:35:32 +00:00
import '@/utils/registerDevExtremeEditors'
2026-02-24 20:44:16 +00:00
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'
2026-02-24 20:44:16 +00:00
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,
2026-07-16 08:19:17 +00:00
FaThLarge,
2026-02-24 20:44:16 +00:00
FaList,
FaSitemap,
FaTable,
FaCalendarAlt,
FaProjectDiagram,
2026-07-16 14:01:18 +00:00
FaTasks,
FaInfoCircle,
2026-02-24 20:44:16 +00:00
} from 'react-icons/fa'
/* =======================
🔥 LAZY VIEW IMPORTS
======================= */
const Grid = React.lazy(() => import('./Grid'))
2026-07-16 08:19:17 +00:00
const CardView = React.lazy(() => import('./CardView'))
2026-02-24 20:44:16 +00:00
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'))
2026-07-16 14:01:18 +00:00
const TodoBoard = React.lazy(() => import('./TodoBoard'))
2026-02-24 20:44:16 +00:00
2026-05-03 09:52:22 +00:00
function isLayoutValid(dto: GridDto, layout: ListViewLayoutType | undefined): boolean {
if (!layout || layout === 'grid') return true
2026-07-16 08:19:17 +00:00
if (layout === 'card') return true
2026-05-03 09:52:22 +00:00
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
)
2026-05-03 09:52:22 +00:00
if (layout === 'gantt')
return !!(
dto.gridOptions?.layoutDto?.gantt &&
dto.gridOptions?.ganttOptionDto?.keyExpr &&
2026-05-03 09:52:22 +00:00
dto.gridOptions?.ganttOptionDto?.parentIdExpr &&
dto.gridOptions?.ganttOptionDto?.titleExpr &&
dto.gridOptions?.ganttOptionDto?.startExpr &&
dto.gridOptions?.ganttOptionDto?.endExpr &&
dto.gridOptions?.ganttOptionDto?.progressExpr
2026-05-03 09:52:22 +00:00
)
if (layout === 'scheduler')
return !!(
dto.gridOptions?.layoutDto?.scheduler &&
dto.gridOptions?.schedulerOptionDto?.textExpr &&
dto.gridOptions?.schedulerOptionDto?.startDateExpr &&
dto.gridOptions?.schedulerOptionDto?.endDateExpr
2026-05-03 09:52:22 +00:00
)
2026-07-16 14:01:18 +00:00
if (layout === 'todo')
return !!(
dto.gridOptions?.layoutDto?.todo &&
dto.gridOptions?.todoOptionDto?.titleExpr &&
dto.gridOptions?.todoOptionDto?.statusExpr
)
2026-05-03 09:52:22 +00:00
return false
}
2026-08-05 20:51:43 +00:00
interface ListProps {
listFormCode?: string
gridDto?: GridDto
2026-08-06 13:17:59 +00:00
isSubForm?: boolean
2026-08-05 20:51:43 +00:00
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
2026-08-05 20:51:43 +00:00
}
const List: React.FC<ListProps> = ({
listFormCode: externalListFormCode,
gridDto: externalGridDto,
2026-08-06 13:17:59 +00:00
isSubForm = false,
2026-08-05 20:51:43 +00:00
onRefreshGridDto,
searchParams: externalSearchParams,
2026-08-05 20:51:43 +00:00
}) => {
const { listFormCode: routeListFormCode = '' } = useParams()
const listFormCode = externalListFormCode || routeListFormCode
const [routeSearchParams] = useSearchParams()
const searchParams = externalSearchParams ?? routeSearchParams
2026-02-24 20:44:16 +00:00
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 })
2026-08-05 20:51:43 +00:00
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
2026-02-24 20:44:16 +00:00
/* =======================
🔹 GRID DTO FETCH
======================= */
const refreshGridDto = useCallback(async () => {
if (!listFormCode) return
2026-08-05 20:51:43 +00:00
if (onRefreshGridDto) {
await onRefreshGridDto()
return
}
const requestedListFormCode = listFormCode
2026-02-24 20:44:16 +00:00
try {
const response = await getList({ listFormCode: requestedListFormCode })
if (activeListFormCodeRef.current !== requestedListFormCode) return
setGridDtoState({ listFormCode: requestedListFormCode, gridDto: response.data })
2026-02-24 20:44:16 +00:00
} catch (err) {
console.error('GridDto load error:', err)
}
2026-08-05 20:51:43 +00:00
}, [listFormCode, onRefreshGridDto])
2026-02-24 20:44:16 +00:00
useEffect(() => {
2026-08-05 20:51:43 +00:00
if (!externalGridDto) void refreshGridDto()
}, [externalGridDto, refreshGridDto])
2026-02-24 20:44:16 +00:00
/* =======================
🔹 INITIAL VIEW MODE
======================= */
useEffect(() => {
if (!gridDto) return
const savedLayout = states.find((s) => s.listFormCode === listFormCode)?.layout
const defaultLayout = gridDto.gridOptions?.layoutDto?.defaultLayout
2026-05-03 09:52:22 +00:00
const candidate = savedLayout ?? defaultLayout
setViewModeState({
listFormCode,
layoutType: isLayoutValid(gridDto, candidate) ? candidate : 'grid',
})
2026-02-24 20:44:16 +00:00
}, [gridDto, states, listFormCode])
/* =======================
🔹 VIEW SWITCH HANDLER
======================= */
const setLayout = useCallback(
(layout: ListViewLayoutType) => {
startTransition(() => {
setViewModeState({ listFormCode, layoutType: layout })
2026-02-24 20:44:16 +00:00
})
setStates({ listFormCode, layout })
},
[listFormCode, setStates],
)
/* =======================
🔹 PRELOAD MAP (HOVER)
======================= */
const preload = useMemo(
() => ({
grid: () => import('./Grid'),
2026-07-16 08:19:17 +00:00
card: () => import('./CardView'),
2026-02-24 20:44:16 +00:00
pivot: () => import('./Pivot'),
tree: () => import('./Tree'),
chart: () => import('./Chart'),
gantt: () => import('./GanttView'),
scheduler: () => import('./SchedulerView'),
2026-07-16 14:01:18 +00:00
todo: () => import('./TodoBoard'),
2026-02-24 20:44:16 +00:00
}),
[],
)
/* =======================
🔹 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])
2026-02-24 20:44:16 +00:00
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>
)}
2026-02-24 20:44:16 +00:00
<Badge content={viewMode} />
{/* =======================
VIEW BUTTONS
======================= */}
<div className="flex gap-1 ml-auto">
2026-07-16 14:01:18 +00:00
{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')}
2026-07-16 14:01:18 +00:00
/>
)}
2026-02-24 20:44:16 +00:00
{gridDto?.gridOptions?.layoutDto.scheduler &&
gridDto?.gridOptions?.schedulerOptionDto?.textExpr &&
gridDto?.gridOptions?.schedulerOptionDto?.startDateExpr && (
<Button
size="sm"
2026-05-12 12:32:40 +00:00
icon={<FaCalendarAlt />}
2026-02-24 20:44:16 +00:00
variant={viewMode === 'scheduler' ? 'solid' : 'default'}
onClick={() => setLayout('scheduler')}
onMouseEnter={() => preload.scheduler()}
title={translate('::App.Platform.TabScheduler')}
></Button>
2026-02-24 20:44:16 +00:00
)}
{gridDto?.gridOptions?.layoutDto.gantt &&
gridDto?.gridOptions?.ganttOptionDto?.parentIdExpr &&
gridDto?.gridOptions?.ganttOptionDto?.titleExpr && (
<Button
size="sm"
2026-05-12 12:32:40 +00:00
icon={<FaProjectDiagram />}
2026-02-24 20:44:16 +00:00
variant={viewMode === 'gantt' ? 'solid' : 'default'}
onClick={() => setLayout('gantt')}
onMouseEnter={() => preload.gantt()}
title={translate('::App.Platform.TabGantt')}
></Button>
2026-02-24 20:44:16 +00:00
)}
{gridDto?.gridOptions?.layoutDto.tree &&
gridDto?.gridOptions?.treeOptionDto?.parentIdExpr && (
<Button
size="sm"
2026-05-12 12:32:40 +00:00
icon={<FaSitemap />}
2026-02-24 20:44:16 +00:00
variant={viewMode === 'tree' ? 'solid' : 'default'}
onClick={() => setLayout('tree')}
onMouseEnter={() => preload.tree()}
title={translate('::App.Platform.TabTree')}
></Button>
2026-02-24 20:44:16 +00:00
)}
<Button
size="sm"
2026-05-12 12:32:40 +00:00
icon={<FaList />}
2026-02-24 20:44:16 +00:00
variant={viewMode === 'grid' ? 'solid' : 'default'}
onClick={() => setLayout('grid')}
onMouseEnter={() => preload.grid()}
title={translate('::App.ListFormEdit.TabGrid')}
></Button>
2026-02-24 20:44:16 +00:00
2026-07-16 08:19:17 +00:00
{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')}
2026-07-16 08:19:17 +00:00
></Button>
)}
2026-02-24 20:44:16 +00:00
{gridDto.gridOptions.layoutDto.pivot && (
<Button
size="sm"
2026-05-12 12:32:40 +00:00
icon={<FaTable />}
2026-02-24 20:44:16 +00:00
variant={viewMode === 'pivot' ? 'solid' : 'default'}
onClick={() => setLayout('pivot')}
onMouseEnter={() => preload.pivot()}
title={translate('::App.Platform.TabPivot')}
></Button>
2026-02-24 20:44:16 +00:00
)}
{gridDto.gridOptions.layoutDto.chart && (
<Button
size="sm"
2026-05-12 12:32:40 +00:00
icon={<FaChartArea />}
2026-02-24 20:44:16 +00:00
variant={viewMode === 'chart' ? 'solid' : 'default'}
onClick={() => setLayout('chart')}
onMouseEnter={() => preload.chart()}
title={translate('::App.Platform.Chart')}
></Button>
2026-02-24 20:44:16 +00:00
)}
</div>
</div>
{/* =======================
VIEW RENDER
======================= */}
<Suspense fallback={<div className="p-4 text-sm">{translate('::App.Platform.LoadingWithThreeDot')}</div>}>
2026-02-24 20:44:16 +00:00
{viewMode === 'grid' && (
<Grid
listFormCode={listFormCode}
searchParams={searchParams}
2026-08-06 13:17:59 +00:00
isSubForm={isSubForm}
2026-02-24 20:44:16 +00:00
gridDto={gridDto}
/>
)}
2026-07-16 08:19:17 +00:00
{viewMode === 'card' && (
<CardView
listFormCode={listFormCode}
searchParams={searchParams}
2026-08-06 13:17:59 +00:00
isSubForm={isSubForm}
2026-07-16 08:19:17 +00:00
gridDto={gridDto}
/>
)}
2026-02-24 20:44:16 +00:00
{viewMode === 'pivot' && (
<Pivot
listFormCode={listFormCode}
searchParams={searchParams}
2026-08-06 13:17:59 +00:00
isSubForm={isSubForm}
2026-02-24 20:44:16 +00:00
gridDto={gridDto}
refreshGridDto={refreshGridDto}
/>
)}
{viewMode === 'tree' && (
<Tree
listFormCode={listFormCode}
searchParams={searchParams}
2026-08-06 13:17:59 +00:00
isSubForm={isSubForm}
2026-02-24 20:44:16 +00:00
gridDto={gridDto}
/>
)}
{viewMode === 'scheduler' && (
<SchedulerView
listFormCode={listFormCode}
searchParams={searchParams}
2026-08-06 13:17:59 +00:00
isSubForm={isSubForm}
2026-02-24 20:44:16 +00:00
gridDto={gridDto}
/>
)}
{viewMode === 'gantt' && (
<GanttView
listFormCode={listFormCode}
searchParams={searchParams}
2026-08-06 13:17:59 +00:00
isSubForm={isSubForm}
2026-02-24 20:44:16 +00:00
gridDto={gridDto}
/>
)}
2026-07-16 14:01:18 +00:00
{viewMode === 'todo' && (
<TodoBoard
listFormCode={listFormCode}
searchParams={searchParams}
2026-08-06 13:17:59 +00:00
isSubForm={isSubForm}
2026-07-16 14:01:18 +00:00
gridDto={gridDto}
/>
)}
2026-02-24 20:44:16 +00:00
{viewMode === 'chart' && (
<Chart
2026-07-06 09:44:26 +00:00
id={gridDto?.gridOptions.id as string}
2026-02-24 20:44:16 +00:00
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>
)}
2026-02-24 20:44:16 +00:00
</Container>
)
}
export default List